-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Properly handle broadcast capture state (#551)
When using the broadcast capturer on iOS, the broadcast extension now drives whether or not screen sharing is enabled, publishing a screen sharing track when the extension begins broadcasting upon user approval. Additionally, the new `BroadcastManager` class gives developers control over the broadcast state and track publication. Fixes #444 and fixes #472. **Minor breaking changes** - Calling `setScreenShare(enabled: true)` or `set(source:enabled:captureOptions:publishOptions:)` on `LocalParticipant` currently returns a `LocalTrackPublication` representing the newly published screen share track. After this change, when using the broadcast capturer on iOS, this method will return `nil`, as the track is published asynchronously pending user approval. Developers should treat enabling screen share as a request that might not be fulfilled and should not interpret a `nil` return value from this method as an error. - Since track publication is now asynchronous, capture options must be set as room defaults rather than being passed to `set(source:enabled:captureOptions:publishOptions:)` when enabling screen sharing. --------- Co-authored-by: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com>
- Loading branch information
1 parent
f5a5958
commit fbec343
Showing
10 changed files
with
527 additions
and
68 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
#if os(iOS) | ||
|
||
import Foundation | ||
|
||
final class BroadcastBundleInfo { | ||
|
||
/// 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 else { return nil } | ||
return Self.socketPath(for: groupIdentifier) | ||
} | ||
|
||
/// Whether or not a broadcast extension has been configured. | ||
static var hasExtension: Bool { | ||
socketPath != nil && screenSharingExtension != nil | ||
} | ||
|
||
private static let socketFileDescriptor = "rtc_SSFD" | ||
|
||
private static func socketPath(for groupIdentifier: String) -> String? { | ||
guard let sharedContainer = FileManager.default | ||
.containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier) | ||
else { return nil } | ||
return sharedContainer.appendingPathComponent(Self.socketFileDescriptor).path | ||
} | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
#if os(iOS) | ||
|
||
import Combine | ||
import Foundation | ||
|
||
#if canImport(ReplayKit) | ||
import ReplayKit | ||
#endif | ||
|
||
/// Manages the broadcast state and track publication for screen sharing on iOS. | ||
public final class BroadcastManager: Sendable { | ||
/// Shared broadcast manager instance. | ||
public static let shared = BroadcastManager() | ||
|
||
private struct State { | ||
var shouldPublishTrack = true | ||
|
||
var cancellable = Set<AnyCancellable>() | ||
let isBroadcastingSubject: CurrentValueSubject<Bool, Never> | ||
|
||
weak var delegate: BroadcastManagerDelegate? | ||
} | ||
|
||
private let _state: StateSync<State> | ||
|
||
/// A delegate for handling broadcast state changes. | ||
public var delegate: BroadcastManagerDelegate? { | ||
get { _state.delegate } | ||
set { _state.mutate { $0.delegate = newValue } } | ||
} | ||
|
||
/// Indicates whether a broadcast is currently in progress. | ||
public var isBroadcasting: Bool { | ||
_state.isBroadcastingSubject.value | ||
} | ||
|
||
/// A publisher that emits the current broadcast state as a Boolean value. | ||
public var isBroadcastingPublisher: AnyPublisher<Bool, Never> { | ||
_state.isBroadcastingSubject.eraseToAnyPublisher() | ||
} | ||
|
||
/// Determines whether a screen share track should be automatically published when broadcasting starts. | ||
/// | ||
/// Set this to `false` to manually manage track publication when the broadcast starts. | ||
/// | ||
public var shouldPublishTrack: Bool { | ||
get { _state.shouldPublishTrack } | ||
set { _state.mutate { $0.shouldPublishTrack = newValue } } | ||
} | ||
|
||
/// Displays the system broadcast picker, allowing the user to start the broadcast. | ||
/// | ||
/// - Note: This is merely a request and does not guarantee the user will choose to start the broadcast. | ||
/// | ||
public func requestActivation() { | ||
Task { | ||
await RPSystemBroadcastPickerView.show( | ||
for: BroadcastBundleInfo.screenSharingExtension, | ||
showsMicrophoneButton: false | ||
) | ||
} | ||
} | ||
|
||
/// Requests to stop the broadcast. | ||
/// | ||
/// If a screen share track is published, it will also be unpublished once the broadcast ends. | ||
/// This method has no effect if no broadcast is currently in progress. | ||
/// | ||
public func requestStop() { | ||
DarwinNotificationCenter.shared.postNotification(.broadcastRequestStop) | ||
} | ||
|
||
init() { | ||
var cancellable = Set<AnyCancellable>() | ||
defer { _state.mutate { $0.cancellable = cancellable } } | ||
|
||
let subject = CurrentValueSubject<Bool, Never>(false) | ||
|
||
Publishers.Merge( | ||
DarwinNotificationCenter.shared.publisher(for: .broadcastStarted).map { _ in true }, | ||
DarwinNotificationCenter.shared.publisher(for: .broadcastStopped).map { _ in false } | ||
) | ||
.subscribe(subject) | ||
.store(in: &cancellable) | ||
|
||
_state = StateSync(State(isBroadcastingSubject: subject)) | ||
|
||
subject.sink { [weak self] in | ||
self?._state.delegate?.broadcastManager(didChangeState: $0) | ||
} | ||
.store(in: &cancellable) | ||
} | ||
} | ||
|
||
/// A delegate protocol for receiving updates about the broadcast state. | ||
@objc | ||
public protocol BroadcastManagerDelegate { | ||
/// Invoked when the broadcast state changes. | ||
/// - Parameter isBroadcasting: A Boolean value indicating whether a broadcast is currently in progress. | ||
func broadcastManager(didChangeState isBroadcasting: Bool) | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.