diff --git a/Sources/LiveKit/Broadcast/BroadcastScreenCapturer.swift b/Sources/LiveKit/Broadcast/BroadcastScreenCapturer.swift index 61aa31847..bf759af97 100644 --- a/Sources/LiveKit/Broadcast/BroadcastScreenCapturer.swift +++ b/Sources/LiveKit/Broadcast/BroadcastScreenCapturer.swift @@ -29,10 +29,6 @@ internal import LiveKitWebRTC #endif class BroadcastScreenCapturer: BufferCapturer { - static let kRTCScreensharingSocketFD = "rtc_SSFD" - static let kAppGroupIdentifierKey = "RTCAppGroupIdentifier" - static let kRTCScreenSharingExtension = "RTCScreenSharingExtension" - var frameReader: SocketConnectionFrameReader? override func startCapture() async throws -> Bool { @@ -40,8 +36,12 @@ class BroadcastScreenCapturer: BufferCapturer { guard didStart else { return false } - guard let identifier = lookUpAppGroupIdentifier(), - let filePath = filePathForIdentifier(identifier) else { return false } + guard let groupIdentifier = Self.groupIdentifier, + let socketPath = Self.socketPath(for: groupIdentifier) + else { + logger.error("Bundle settings improperly configured for screen capture") + return false + } let bounds = await UIScreen.main.bounds let width = bounds.size.width @@ -57,7 +57,7 @@ class BroadcastScreenCapturer: BufferCapturer { set(dimensions: targetDimensions) let frameReader = SocketConnectionFrameReader() - guard let socketConnection = BroadcastServerSocketConnection(filePath: filePath, streamDelegate: frameReader) + guard let socketConnection = BroadcastServerSocketConnection(filePath: socketPath, streamDelegate: frameReader) else { return false } frameReader.didCapture = { pixelBuffer, rotation in self.capture(pixelBuffer, rotation: rotation.toLKType()) @@ -85,16 +85,27 @@ class BroadcastScreenCapturer: BufferCapturer { return true } - private func lookUpAppGroupIdentifier() -> String? { - Bundle.main.infoDictionary?[BroadcastScreenCapturer.kAppGroupIdentifierKey] as? String + /// Identifier of the app group shared by the primary app and broadcast extension. + @BundleInfo("RTCAppGroupIdentifier") + static var groupIdentifier: String? + + /// Bundle identifier of the broadcast extension. + @BundleInfo("RTCScreenSharingExtension") + static var screenSharingExtension: String? + + /// Path to the socket file used for interprocess communication. + static var socketPath: String? { + guard let groupIdentifier = Self.groupIdentifier else { return nil } + return Self.socketPath(for: groupIdentifier) } - private func filePathForIdentifier(_ identifier: String) -> String? { - guard let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: identifier) - else { return nil } + private static let kRTCScreensharingSocketFD = "rtc_SSFD" - let filePath = sharedContainer.appendingPathComponent(BroadcastScreenCapturer.kRTCScreensharingSocketFD).path - return filePath + private static func socketPath(for groupIdentifier: String) -> String? { + guard let sharedContainer = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier) + else { return nil } + return sharedContainer.appendingPathComponent(Self.kRTCScreensharingSocketFD).path } } diff --git a/Sources/LiveKit/Broadcast/BundleInfo.swift b/Sources/LiveKit/Broadcast/BundleInfo.swift new file mode 100644 index 000000000..94d5cca8d --- /dev/null +++ b/Sources/LiveKit/Broadcast/BundleInfo.swift @@ -0,0 +1,37 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// A property wrapper type that reflects a value from a bundle's info dictionary. +@propertyWrapper +struct BundleInfo: Sendable { + private let key: String + private let bundle: Bundle + + init(_ key: String, bundle: Bundle = .main) { + self.key = key + self.bundle = bundle + } + + var wrappedValue: Value? { + guard let value = bundle.infoDictionary?[key] as? Value else { + logger.warning("Missing bundle property with key `\(key)`") + return nil + } + return value + } +} diff --git a/Sources/LiveKit/Broadcast/Uploader/LKSampleHandler.swift b/Sources/LiveKit/Broadcast/Uploader/LKSampleHandler.swift index 4646241d7..8a972aa9a 100644 --- a/Sources/LiveKit/Broadcast/Uploader/LKSampleHandler.swift +++ b/Sources/LiveKit/Broadcast/Uploader/LKSampleHandler.swift @@ -27,24 +27,14 @@ open class LKSampleHandler: RPBroadcastSampleHandler { private var clientConnection: BroadcastUploadSocketConnection? private var uploader: SampleUploader? - public var appGroupIdentifier: String? { - Bundle.main.infoDictionary?[BroadcastScreenCapturer.kAppGroupIdentifierKey] as? String - } - - public var socketFilePath: String { - guard let appGroupIdentifier, - let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) - else { - return "" - } - - return sharedContainer.appendingPathComponent(BroadcastScreenCapturer.kRTCScreensharingSocketFD).path - } - override public init() { super.init() - if let connection = BroadcastUploadSocketConnection(filePath: socketFilePath) { + let socketPath = BroadcastScreenCapturer.socketPath + if socketPath == nil { + logger.error("Bundle settings improperly configured for screen capture") + } + if let connection = BroadcastUploadSocketConnection(filePath: socketPath ?? "") { clientConnection = connection setupConnection() diff --git a/Sources/LiveKit/Participant/LocalParticipant.swift b/Sources/LiveKit/Participant/LocalParticipant.swift index 2eafd55ee..8085a8e6d 100644 --- a/Sources/LiveKit/Participant/LocalParticipant.swift +++ b/Sources/LiveKit/Participant/LocalParticipant.swift @@ -339,8 +339,10 @@ public extension LocalParticipant { let localTrack: LocalVideoTrack let options = (captureOptions as? ScreenShareCaptureOptions) ?? room._state.roomOptions.defaultScreenShareCaptureOptions if options.useBroadcastExtension { - let screenShareExtensionId = Bundle.main.infoDictionary?[BroadcastScreenCapturer.kRTCScreenSharingExtension] as? String - await RPSystemBroadcastPickerView.show(for: screenShareExtensionId, showsMicrophoneButton: false) + await RPSystemBroadcastPickerView.show( + for: BroadcastScreenCapturer.screenSharingExtension, + showsMicrophoneButton: false + ) localTrack = LocalVideoTrack.createBroadcastScreenCapturerTrack(options: options) } else { localTrack = LocalVideoTrack.createInAppScreenShareTrack(options: options)