From 531cbd1676638f001a1ec40f633cc8ac68b42a1e Mon Sep 17 00:00:00 2001 From: davidliu Date: Sun, 10 Jul 2022 22:43:20 +0900 Subject: [PATCH] iOS screen share (#133) --- .../LiveKit Broadcast Extension/Atomic.swift | 37 +++ .../DarwinNotificationCenter.swift | 29 ++ .../LiveKit Broadcast Extension/Info.plist | 15 + .../LiveKit Broadcast Extension.entitlements | 10 + .../SampleHandler.swift | 103 +++++++ .../SampleUploader.swift | 147 ++++++++++ .../SocketConnection.swift | 199 ++++++++++++++ example/ios/Podfile.lock | 4 +- example/ios/Runner.xcodeproj/project.pbxproj | 258 +++++++++++++++++- example/ios/Runner/Info.plist | 8 +- example/ios/Runner/Runner.entitlements | 10 + example/lib/pages/connect.dart | 13 +- example/pubspec.lock | 4 +- lib/src/track/options.dart | 18 +- pubspec.lock | 2 +- pubspec.yaml | 2 +- 16 files changed, 838 insertions(+), 21 deletions(-) create mode 100644 example/ios/LiveKit Broadcast Extension/Atomic.swift create mode 100644 example/ios/LiveKit Broadcast Extension/DarwinNotificationCenter.swift create mode 100644 example/ios/LiveKit Broadcast Extension/Info.plist create mode 100644 example/ios/LiveKit Broadcast Extension/LiveKit Broadcast Extension.entitlements create mode 100644 example/ios/LiveKit Broadcast Extension/SampleHandler.swift create mode 100644 example/ios/LiveKit Broadcast Extension/SampleUploader.swift create mode 100644 example/ios/LiveKit Broadcast Extension/SocketConnection.swift create mode 100644 example/ios/Runner/Runner.entitlements diff --git a/example/ios/LiveKit Broadcast Extension/Atomic.swift b/example/ios/LiveKit Broadcast Extension/Atomic.swift new file mode 100644 index 0000000..248410f --- /dev/null +++ b/example/ios/LiveKit Broadcast Extension/Atomic.swift @@ -0,0 +1,37 @@ +// +// Atomic.swift +// Broadcast Extension +// +// Created by Maksym Shcheglov. +// https://www.onswiftwings.com/posts/atomic-property-wrapper/ +// + +import Foundation + +@propertyWrapper +struct Atomic { + + private var value: Value + private let lock = NSLock() + + init(wrappedValue value: Value) { + self.value = value + } + + var wrappedValue: Value { + get { load() } + set { store(newValue: newValue) } + } + + func load() -> Value { + lock.lock() + defer { lock.unlock() } + return value + } + + mutating func store(newValue: Value) { + lock.lock() + defer { lock.unlock() } + value = newValue + } +} diff --git a/example/ios/LiveKit Broadcast Extension/DarwinNotificationCenter.swift b/example/ios/LiveKit Broadcast Extension/DarwinNotificationCenter.swift new file mode 100644 index 0000000..3501486 --- /dev/null +++ b/example/ios/LiveKit Broadcast Extension/DarwinNotificationCenter.swift @@ -0,0 +1,29 @@ +// +// DarwinNotificationCenter.swift +// Broadcast Extension +// +// Created by Alex-Dan Bumbu on 23/03/2021. +// Copyright © 2021 8x8, Inc. All rights reserved. +// + +import Foundation + +enum DarwinNotification: String { + case broadcastStarted = "iOS_BroadcastStarted" + case broadcastStopped = "iOS_BroadcastStopped" +} + +class DarwinNotificationCenter { + + static let shared = DarwinNotificationCenter() + + private let notificationCenter: CFNotificationCenter + + init() { + notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() + } + + func postNotification(_ name: DarwinNotification) { + CFNotificationCenterPostNotification(notificationCenter, CFNotificationName(rawValue: name.rawValue as CFString), nil, nil, true) + } +} diff --git a/example/ios/LiveKit Broadcast Extension/Info.plist b/example/ios/LiveKit Broadcast Extension/Info.plist new file mode 100644 index 0000000..e936790 --- /dev/null +++ b/example/ios/LiveKit Broadcast Extension/Info.plist @@ -0,0 +1,15 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.broadcast-services-upload + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).SampleHandler + RPBroadcastProcessMode + RPBroadcastProcessModeSampleBuffer + + + diff --git a/example/ios/LiveKit Broadcast Extension/LiveKit Broadcast Extension.entitlements b/example/ios/LiveKit Broadcast Extension/LiveKit Broadcast Extension.entitlements new file mode 100644 index 0000000..c4df1dc --- /dev/null +++ b/example/ios/LiveKit Broadcast Extension/LiveKit Broadcast Extension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.io.livekit.example.flutter + + + diff --git a/example/ios/LiveKit Broadcast Extension/SampleHandler.swift b/example/ios/LiveKit Broadcast Extension/SampleHandler.swift new file mode 100644 index 0000000..6bac452 --- /dev/null +++ b/example/ios/LiveKit Broadcast Extension/SampleHandler.swift @@ -0,0 +1,103 @@ +// +// SampleHandler.swift +// Broadcast Extension +// +// Created by Alex-Dan Bumbu on 04.06.2021. +// + +import ReplayKit +import OSLog + +let broadcastLogger = OSLog(subsystem: "io.livekit.example.flutter", category: "Broadcast") +private enum Constants { + // the App Group ID value that the app and the broadcast extension targets are setup with. It differs for each app. + static let appGroupIdentifier = "group.io.livekit.example.flutter" +} + +class SampleHandler: RPBroadcastSampleHandler { + + private var clientConnection: SocketConnection? + private var uploader: SampleUploader? + + private var frameCount: Int = 0 + + var socketFilePath: String { + let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupIdentifier) + return sharedContainer?.appendingPathComponent("rtc_SSFD").path ?? "" + } + + override init() { + super.init() + if let connection = SocketConnection(filePath: socketFilePath) { + clientConnection = connection + setupConnection() + + uploader = SampleUploader(connection: connection) + } + os_log(.debug, log: broadcastLogger, "%{public}s", socketFilePath) + } + + override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) { + // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional. + frameCount = 0 + + DarwinNotificationCenter.shared.postNotification(.broadcastStarted) + openConnection() + } + + override func broadcastPaused() { + // User has requested to pause the broadcast. Samples will stop being delivered. + } + + override func broadcastResumed() { + // User has requested to resume the broadcast. Samples delivery will resume. + } + + override func broadcastFinished() { + // User has requested to finish the broadcast. + DarwinNotificationCenter.shared.postNotification(.broadcastStopped) + clientConnection?.close() + } + + override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) { + switch sampleBufferType { + case RPSampleBufferType.video: + uploader?.send(sample: sampleBuffer) + default: + break + } + } +} + +private extension SampleHandler { + + func setupConnection() { + clientConnection?.didClose = { [weak self] error in + os_log(.debug, log: broadcastLogger, "client connection did close \(String(describing: error))") + + if let error = error { + self?.finishBroadcastWithError(error) + } else { + // the displayed failure message is more user friendly when using NSError instead of Error + let JMScreenSharingStopped = 10001 + let customError = NSError(domain: RPRecordingErrorDomain, code: JMScreenSharingStopped, userInfo: [NSLocalizedDescriptionKey: "Screen sharing stopped"]) + self?.finishBroadcastWithError(customError) + } + } + } + + func openConnection() { + let queue = DispatchQueue(label: "broadcast.connectTimer") + let timer = DispatchSource.makeTimerSource(queue: queue) + timer.schedule(deadline: .now(), repeating: .milliseconds(100), leeway: .milliseconds(500)) + timer.setEventHandler { [weak self] in + guard self?.clientConnection?.open() == true else { + return + } + + timer.cancel() + } + + timer.resume() + } +} diff --git a/example/ios/LiveKit Broadcast Extension/SampleUploader.swift b/example/ios/LiveKit Broadcast Extension/SampleUploader.swift new file mode 100644 index 0000000..6205ad2 --- /dev/null +++ b/example/ios/LiveKit Broadcast Extension/SampleUploader.swift @@ -0,0 +1,147 @@ +// +// SampleUploader.swift +// Broadcast Extension +// +// Created by Alex-Dan Bumbu on 22/03/2021. +// Copyright © 2021 8x8, Inc. All rights reserved. +// + +import Foundation +import ReplayKit +import OSLog + +private enum Constants { + static let bufferMaxLength = 10240 +} + +class SampleUploader { + + private static var imageContext = CIContext(options: nil) + + @Atomic private var isReady = false + private var connection: SocketConnection + + private var dataToSend: Data? + private var byteIndex = 0 + + private let serialQueue: DispatchQueue + + init(connection: SocketConnection) { + self.connection = connection + self.serialQueue = DispatchQueue(label: "org.jitsi.meet.broadcast.sampleUploader") + + setupConnection() + } + + @discardableResult func send(sample buffer: CMSampleBuffer) -> Bool { + guard isReady else { + return false + } + + isReady = false + + dataToSend = prepare(sample: buffer) + byteIndex = 0 + + serialQueue.async { [weak self] in + self?.sendDataChunk() + } + + return true + } +} + +private extension SampleUploader { + + func setupConnection() { + connection.didOpen = { [weak self] in + self?.isReady = true + } + connection.streamHasSpaceAvailable = { [weak self] in + self?.serialQueue.async { + if let success = self?.sendDataChunk() { + self?.isReady = !success + } + } + } + } + + @discardableResult func sendDataChunk() -> Bool { + guard let dataToSend = dataToSend else { + return false + } + + var bytesLeft = dataToSend.count - byteIndex + var length = bytesLeft > Constants.bufferMaxLength ? Constants.bufferMaxLength : bytesLeft + + length = dataToSend[byteIndex..<(byteIndex + length)].withUnsafeBytes { + guard let ptr = $0.bindMemory(to: UInt8.self).baseAddress else { + return 0 + } + + return connection.writeToStream(buffer: ptr, maxLength: length) + } + + if length > 0 { + byteIndex += length + bytesLeft -= length + + if bytesLeft == 0 { + self.dataToSend = nil + byteIndex = 0 + } + } else { + os_log(.debug, log: broadcastLogger, "writeBufferToStream failure") + } + + return true + } + + func prepare(sample buffer: CMSampleBuffer) -> Data? { + guard let imageBuffer = CMSampleBufferGetImageBuffer(buffer) else { + os_log(.debug, log: broadcastLogger, "image buffer not available") + return nil + } + + CVPixelBufferLockBaseAddress(imageBuffer, .readOnly) + + let scaleFactor = 1.0 + let width = CVPixelBufferGetWidth(imageBuffer)/Int(scaleFactor) + let height = CVPixelBufferGetHeight(imageBuffer)/Int(scaleFactor) + let orientation = CMGetAttachment(buffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil)?.uintValue ?? 0 + + let scaleTransform = CGAffineTransform(scaleX: CGFloat(1.0/scaleFactor), y: CGFloat(1.0/scaleFactor)) + let bufferData = self.jpegData(from: imageBuffer, scale: scaleTransform) + + CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly) + + guard let messageData = bufferData else { + os_log(.debug, log: broadcastLogger, "corrupted image buffer") + return nil + } + + let httpResponse = CFHTTPMessageCreateResponse(nil, 200, nil, kCFHTTPVersion1_1).takeRetainedValue() + CFHTTPMessageSetHeaderFieldValue(httpResponse, "Content-Length" as CFString, String(messageData.count) as CFString) + CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Width" as CFString, String(width) as CFString) + CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Height" as CFString, String(height) as CFString) + CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Orientation" as CFString, String(orientation) as CFString) + + CFHTTPMessageSetBody(httpResponse, messageData as CFData) + + let serializedMessage = CFHTTPMessageCopySerializedMessage(httpResponse)?.takeRetainedValue() as Data? + + return serializedMessage + } + + func jpegData(from buffer: CVPixelBuffer, scale scaleTransform: CGAffineTransform) -> Data? { + let image = CIImage(cvPixelBuffer: buffer).transformed(by: scaleTransform) + + guard let colorSpace = image.colorSpace else { + return nil + } + + let options: [CIImageRepresentationOption: Float] = [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 1.0] + + return SampleUploader.imageContext.jpegRepresentation(of: image, colorSpace: colorSpace, options: options) + } +} diff --git a/example/ios/LiveKit Broadcast Extension/SocketConnection.swift b/example/ios/LiveKit Broadcast Extension/SocketConnection.swift new file mode 100644 index 0000000..a2a73a9 --- /dev/null +++ b/example/ios/LiveKit Broadcast Extension/SocketConnection.swift @@ -0,0 +1,199 @@ +// +// SocketConnection.swift +// Broadcast Extension +// +// Created by Alex-Dan Bumbu on 22/03/2021. +// Copyright © 2021 Atlassian Inc. All rights reserved. +// + +import Foundation +import OSLog + +class SocketConnection: NSObject { + var didOpen: (() -> Void)? + var didClose: ((Error?) -> Void)? + var streamHasSpaceAvailable: (() -> Void)? + + private let filePath: String + private var socketHandle: Int32 = -1 + private var address: sockaddr_un? + + private var inputStream: InputStream? + private var outputStream: OutputStream? + + private var networkQueue: DispatchQueue? + private var shouldKeepRunning = false + + init?(filePath path: String) { + filePath = path + socketHandle = Darwin.socket(AF_UNIX, SOCK_STREAM, 0) + + guard socketHandle != -1 else { + os_log(.debug, log: broadcastLogger, "failure: create socket") + return nil + } + } + + func open() -> Bool { + os_log(.debug, log: broadcastLogger, "open socket connection") + + guard FileManager.default.fileExists(atPath: filePath) else { + os_log(.debug, log: broadcastLogger, "failure: socket file missing") + return false + } + + guard setupAddress() == true else { + return false + } + + guard connectSocket() == true else { + return false + } + + setupStreams() + + inputStream?.open() + outputStream?.open() + + return true + } + + func close() { + unscheduleStreams() + + inputStream?.delegate = nil + outputStream?.delegate = nil + + inputStream?.close() + outputStream?.close() + + inputStream = nil + outputStream = nil + } + + func writeToStream(buffer: UnsafePointer, maxLength length: Int) -> Int { + outputStream?.write(buffer, maxLength: length) ?? 0 + } +} + +extension SocketConnection: StreamDelegate { + + func stream(_ aStream: Stream, handle eventCode: Stream.Event) { + switch eventCode { + case .openCompleted: + os_log(.debug, log: broadcastLogger, "client stream open completed") + if aStream == outputStream { + didOpen?() + } + case .hasBytesAvailable: + if aStream == inputStream { + var buffer: UInt8 = 0 + let numberOfBytesRead = inputStream?.read(&buffer, maxLength: 1) + if numberOfBytesRead == 0 && aStream.streamStatus == .atEnd { + os_log(.debug, log: broadcastLogger, "server socket closed") + close() + notifyDidClose(error: nil) + } + } + case .hasSpaceAvailable: + if aStream == outputStream { + streamHasSpaceAvailable?() + } + case .errorOccurred: + os_log(.debug, log: broadcastLogger, "client stream error occured: \(String(describing: aStream.streamError))") + close() + notifyDidClose(error: aStream.streamError) + + default: + break + } + } +} + +private extension SocketConnection { + + func setupAddress() -> Bool { + var addr = sockaddr_un() + guard filePath.count < MemoryLayout.size(ofValue: addr.sun_path) else { + os_log(.debug, log: broadcastLogger, "failure: fd path is too long") + return false + } + + _ = withUnsafeMutablePointer(to: &addr.sun_path.0) { ptr in + filePath.withCString { + strncpy(ptr, $0, filePath.count) + } + } + + address = addr + return true + } + + func connectSocket() -> Bool { + guard var addr = address else { + return false + } + + let status = withUnsafePointer(to: &addr) { ptr in + ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { + Darwin.connect(socketHandle, $0, socklen_t(MemoryLayout.size)) + } + } + + guard status == noErr else { + os_log(.debug, log: broadcastLogger, "failure: \(status)") + return false + } + + return true + } + + func setupStreams() { + var readStream: Unmanaged? + var writeStream: Unmanaged? + + CFStreamCreatePairWithSocket(kCFAllocatorDefault, socketHandle, &readStream, &writeStream) + + inputStream = readStream?.takeRetainedValue() + inputStream?.delegate = self + inputStream?.setProperty(kCFBooleanTrue, forKey: Stream.PropertyKey(kCFStreamPropertyShouldCloseNativeSocket as String)) + + outputStream = writeStream?.takeRetainedValue() + outputStream?.delegate = self + outputStream?.setProperty(kCFBooleanTrue, forKey: Stream.PropertyKey(kCFStreamPropertyShouldCloseNativeSocket as String)) + + scheduleStreams() + } + + func scheduleStreams() { + shouldKeepRunning = true + + networkQueue = DispatchQueue.global(qos: .userInitiated) + networkQueue?.async { [weak self] in + self?.inputStream?.schedule(in: .current, forMode: .common) + self?.outputStream?.schedule(in: .current, forMode: .common) + RunLoop.current.run() + + var isRunning = false + + repeat { + isRunning = self?.shouldKeepRunning ?? false && RunLoop.current.run(mode: .default, before: .distantFuture) + } while (isRunning) + } + } + + func unscheduleStreams() { + networkQueue?.sync { [weak self] in + self?.inputStream?.remove(from: .current, forMode: .common) + self?.outputStream?.remove(from: .current, forMode: .common) + } + + shouldKeepRunning = false + } + + func notifyDidClose(error: Error?) { + if didClose != nil { + didClose?(error) + } + } +} diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 4d3ce2c..75c7ae4 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2,7 +2,7 @@ PODS: - device_info_plus (0.0.1): - Flutter - Flutter (1.0.0) - - flutter_webrtc (0.7.1): + - flutter_webrtc (0.8.0): - Flutter - WebRTC-SDK (= 97.4692.05) - livekit_client (1.0.0): @@ -43,7 +43,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a - flutter_webrtc: c4d09b864e7a8e93ad931bb84eed0ceb1ed51444 + flutter_webrtc: 1062ea68c08f530ad3d97f743729dd390d8cbf7b livekit_client: bfe6ba8be81816356cf2b06483179227c832a986 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index bfd4215..9193e1d 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -14,8 +14,25 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + D7FECD282860997500D04D1C /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D7FECD272860997400D04D1C /* ReplayKit.framework */; }; + D7FECD2F2860997500D04D1C /* LiveKit Broadcast Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D7FECD262860997400D04D1C /* LiveKit Broadcast Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + D7FECD3B286099EE00D04D1C /* SocketConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FECD36286099EE00D04D1C /* SocketConnection.swift */; }; + D7FECD3C286099EE00D04D1C /* SampleHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FECD37286099EE00D04D1C /* SampleHandler.swift */; }; + D7FECD3D286099EE00D04D1C /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FECD38286099EE00D04D1C /* Atomic.swift */; }; + D7FECD3E286099EE00D04D1C /* DarwinNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FECD39286099EE00D04D1C /* DarwinNotificationCenter.swift */; }; + D7FECD3F286099EE00D04D1C /* SampleUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FECD3A286099EE00D04D1C /* SampleUploader.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + D7FECD2D2860997500D04D1C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = D7FECD252860997400D04D1C; + remoteInfo = "LiveKit Broadcast Extension"; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -27,6 +44,17 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + D7FECD302860997500D04D1C /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + D7FECD2F2860997500D04D1C /* LiveKit Broadcast Extension.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -45,6 +73,16 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D7FECD262860997400D04D1C /* LiveKit Broadcast Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "LiveKit Broadcast Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + D7FECD272860997400D04D1C /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; }; + D7FECD2C2860997500D04D1C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D7FECD352860998A00D04D1C /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + D7FECD36286099EE00D04D1C /* SocketConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketConnection.swift; sourceTree = ""; }; + D7FECD37286099EE00D04D1C /* SampleHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleHandler.swift; sourceTree = ""; }; + D7FECD38286099EE00D04D1C /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; + D7FECD39286099EE00D04D1C /* DarwinNotificationCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarwinNotificationCenter.swift; sourceTree = ""; }; + D7FECD3A286099EE00D04D1C /* SampleUploader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUploader.swift; sourceTree = ""; }; + D7FECD402860A32000D04D1C /* LiveKit Broadcast Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "LiveKit Broadcast Extension.entitlements"; sourceTree = ""; }; D921B8649EE4CE6E91156304 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; ED81679F29CEB979FBD03BCE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -58,6 +96,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D7FECD232860997400D04D1C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D7FECD282860997500D04D1C /* ReplayKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -65,6 +111,7 @@ isa = PBXGroup; children = ( 09B43753FB1E220F505A0AA8 /* Pods_Runner.framework */, + D7FECD272860997400D04D1C /* ReplayKit.framework */, ); name = Frameworks; sourceTree = ""; @@ -85,6 +132,7 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + D7FECD292860997500D04D1C /* LiveKit Broadcast Extension */, 97C146EF1CF9000F007C117D /* Products */, E484AC95536901EEBD680DBE /* Pods */, 3D7376885FAAA7D47D78511C /* Frameworks */, @@ -95,6 +143,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + D7FECD262860997400D04D1C /* LiveKit Broadcast Extension.appex */, ); name = Products; sourceTree = ""; @@ -102,6 +151,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + D7FECD352860998A00D04D1C /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, @@ -114,6 +164,20 @@ path = Runner; sourceTree = ""; }; + D7FECD292860997500D04D1C /* LiveKit Broadcast Extension */ = { + isa = PBXGroup; + children = ( + D7FECD402860A32000D04D1C /* LiveKit Broadcast Extension.entitlements */, + D7FECD38286099EE00D04D1C /* Atomic.swift */, + D7FECD39286099EE00D04D1C /* DarwinNotificationCenter.swift */, + D7FECD37286099EE00D04D1C /* SampleHandler.swift */, + D7FECD3A286099EE00D04D1C /* SampleUploader.swift */, + D7FECD36286099EE00D04D1C /* SocketConnection.swift */, + D7FECD2C2860997500D04D1C /* Info.plist */, + ); + path = "LiveKit Broadcast Extension"; + sourceTree = ""; + }; E484AC95536901EEBD680DBE /* Pods */ = { isa = PBXGroup; children = ( @@ -139,22 +203,42 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, D9316257C44ACC699FF042FD /* [CP] Embed Pods Frameworks */, + D7FECD302860997500D04D1C /* Embed App Extensions */, ); buildRules = ( ); dependencies = ( + D7FECD2E2860997500D04D1C /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; + D7FECD252860997400D04D1C /* LiveKit Broadcast Extension */ = { + isa = PBXNativeTarget; + buildConfigurationList = D7FECD342860997500D04D1C /* Build configuration list for PBXNativeTarget "LiveKit Broadcast Extension" */; + buildPhases = ( + D7FECD222860997400D04D1C /* Sources */, + D7FECD232860997400D04D1C /* Frameworks */, + D7FECD242860997400D04D1C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "LiveKit Broadcast Extension"; + productName = "LiveKit Broadcast Extension"; + productReference = D7FECD262860997400D04D1C /* LiveKit Broadcast Extension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 1340; LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { @@ -162,6 +246,10 @@ CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; + D7FECD252860997400D04D1C = { + CreatedOnToolsVersion = 13.4.1; + LastSwiftMigration = 1340; + }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -178,6 +266,7 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + D7FECD252860997400D04D1C /* LiveKit Broadcast Extension */, ); }; /* End PBXProject section */ @@ -194,6 +283,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D7FECD242860997400D04D1C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -276,8 +372,28 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D7FECD222860997400D04D1C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D7FECD3C286099EE00D04D1C /* SampleHandler.swift in Sources */, + D7FECD3D286099EE00D04D1C /* Atomic.swift in Sources */, + D7FECD3E286099EE00D04D1C /* DarwinNotificationCenter.swift in Sources */, + D7FECD3F286099EE00D04D1C /* SampleUploader.swift in Sources */, + D7FECD3B286099EE00D04D1C /* SocketConnection.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + D7FECD2E2860997500D04D1C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D7FECD252860997400D04D1C /* LiveKit Broadcast Extension */; + targetProxy = D7FECD2D2860997500D04D1C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -353,19 +469,22 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-LiveKit"; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = J48VV6BZV9; + DEVELOPMENT_TEAM = 76TVFCUKK7; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example; + PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example.flutter; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -488,19 +607,22 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-LiveKit"; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = J48VV6BZV9; + DEVELOPMENT_TEAM = 76TVFCUKK7; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example; + PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example.flutter; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -514,19 +636,22 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-LiveKit"; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = J48VV6BZV9; + DEVELOPMENT_TEAM = 76TVFCUKK7; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example; + PRODUCT_BUNDLE_IDENTIFIER = io.livekit.example.flutter; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -535,6 +660,117 @@ }; name = Release; }; + D7FECD312860997500D04D1C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "LiveKit Broadcast Extension/LiveKit Broadcast Extension.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 76TVFCUKK7; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "LiveKit Broadcast Extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "LiveKit Broadcast Extension"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "io.livekit.example.flutter.LiveKit-Broadcast-Extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D7FECD322860997500D04D1C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "LiveKit Broadcast Extension/LiveKit Broadcast Extension.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 76TVFCUKK7; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "LiveKit Broadcast Extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "LiveKit Broadcast Extension"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "io.livekit.example.flutter.LiveKit-Broadcast-Extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D7FECD332860997500D04D1C /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "LiveKit Broadcast Extension/LiveKit Broadcast Extension.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 76TVFCUKK7; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "LiveKit Broadcast Extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "LiveKit Broadcast Extension"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "io.livekit.example.flutter.LiveKit-Broadcast-Extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -558,6 +794,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D7FECD342860997500D04D1C /* Build configuration list for PBXNativeTarget "LiveKit Broadcast Extension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D7FECD312860997500D04D1C /* Debug */, + D7FECD322860997500D04D1C /* Release */, + D7FECD332860997500D04D1C /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 3329c40..13aa324 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -2,6 +2,12 @@ + RTCScreenSharingExtension + io.livekit.example.flutter.LiveKit-Broadcast-Extension + RTCAppGroupIdentifier + group.io.livekit.example.flutter + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -49,7 +55,5 @@ UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone - diff --git a/example/ios/Runner/Runner.entitlements b/example/ios/Runner/Runner.entitlements new file mode 100644 index 0000000..c4df1dc --- /dev/null +++ b/example/ios/Runner/Runner.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.io.livekit.example.flutter + + + diff --git a/example/lib/pages/connect.dart b/example/lib/pages/connect.dart index ec72304..0dc82a2 100644 --- a/example/lib/pages/connect.dart +++ b/example/lib/pages/connect.dart @@ -90,12 +90,13 @@ class _ConnectPageState extends State { _uriCtrl.text, _tokenCtrl.text, roomOptions: RoomOptions( - adaptiveStream: _adaptiveStream, - dynacast: _dynacast, - defaultVideoPublishOptions: VideoPublishOptions( - simulcast: _simulcast, - ), - ), + adaptiveStream: _adaptiveStream, + dynacast: _dynacast, + defaultVideoPublishOptions: VideoPublishOptions( + simulcast: _simulcast, + ), + defaultScreenShareCaptureOptions: const ScreenShareCaptureOptions( + useiOSBroadcastExtension: true)), ); await Navigator.push( diff --git a/example/pubspec.lock b/example/pubspec.lock index b2c70f9..9061893 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -176,7 +176,7 @@ packages: name: flutter_webrtc url: "https://pub.dartlang.org" source: hosted - version: "0.8.9" + version: "0.8.10" google_fonts: dependency: "direct main" description: @@ -225,7 +225,7 @@ packages: path: ".." relative: true source: path - version: "1.0.0" + version: "1.0.1" logging: dependency: "direct main" description: diff --git a/lib/src/track/options.dart b/lib/src/track/options.dart index 993ad42..2d66129 100644 --- a/lib/src/track/options.dart +++ b/lib/src/track/options.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/foundation.dart' show kIsWeb; import '../track/local/audio.dart'; @@ -74,12 +76,26 @@ class CameraCaptureOptions extends VideoCaptureOptions { /// Options used when creating a [LocalVideoTrack] that captures the screen. class ScreenShareCaptureOptions extends VideoCaptureOptions { + final bool useiOSBroadcastExtension; + const ScreenShareCaptureOptions({ + this.useiOSBroadcastExtension = false, VideoParameters params = VideoParametersPresets.screenShareH720FPS15, }) : super(params: params); - ScreenShareCaptureOptions.from({required VideoCaptureOptions captureOptions}) + ScreenShareCaptureOptions.from( + {this.useiOSBroadcastExtension = false, + required VideoCaptureOptions captureOptions}) : super(params: captureOptions.params); + + @override + Map toMediaConstraintsMap() { + var constraints = super.toMediaConstraintsMap(); + if (useiOSBroadcastExtension && Platform.isIOS) { + constraints['deviceId'] = 'broadcast'; + } + return constraints; + } } /// Base class for track options. diff --git a/pubspec.lock b/pubspec.lock index 0a1c2cb..f2ad67e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -218,7 +218,7 @@ packages: name: flutter_webrtc url: "https://pub.dartlang.org" source: hosted - version: "0.8.9" + version: "0.8.10" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 142ac09..d381c34 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: uuid: ^3.0.6 synchronized: ^3.0.0+2 protobuf: ^2.0.1 - flutter_webrtc: ^0.8.9 + flutter_webrtc: ^0.8.10 dart_webrtc: ^1.0.6 device_info_plus: ^3.2.3