Outgoing iOS screen-share via ReplayKit broadcast extension
Lets a native-call user share their iPhone screen (whole device, works backgrounded). LiveKit 2.15.3 ships the broadcast stack (BroadcastManager + LKSampleHandler + IPC), so: - New Broadcast Upload Extension target "BroadcastExtension" (com.bizgaze.connect.broadcast): SampleHandler.swift subclasses LKSampleHandler; injected by mobile/scripts/add-broadcast-extension.rb which also LINKS the LiveKit SPM product into the extension + sets the App Group. Sources in mobile/ios-broadcast/. - Plugin: Room created with ScreenShareCaptureOptions(useBroadcastExtension: true); startScreenShare -> BroadcastManager.requestActivation() (system picker); stopScreenShare -> requestStop(); BroadcastManagerDelegate -> fires screenShareState to the web. LiveKit auto-publishes the track. - ios-patch.sh: RTCScreenSharingExtension + RTCAppGroupIdentifier keys. - codemagic.yaml: run the injector + sign the 3rd bundle id (.broadcast). - home.html: toggleScreen native -> start/stop; screenShareState listener reflects state + broadcasts meeting-screen so peers' stage shows it. REQUIRES a one-time manual Apple portal step: enable the App Group on the com.bizgaze.connect.broadcast App ID (see mobile/IOS_SETUP.md) or the archive fails code-signing. SPM-linked extension is new on our CI — expect build iteration. Web deployed (no-ops on builds without startScreenShare). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ import LiveKit // SPM product name is 'LiveKit' (Package.swift). (The old CocoaP
|
||||
// the token from the WebView via reportOutgoingCall(). The WebView is UI only for native calls (it must
|
||||
// NOT also join the room — LiveKit allows one connection per identity).
|
||||
@objc(NativeCallPlugin)
|
||||
public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelegate, CXProviderDelegate {
|
||||
public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelegate, CXProviderDelegate, BroadcastManagerDelegate {
|
||||
public let identifier = "NativeCallPlugin"
|
||||
public let jsName = "NativeCall"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
@@ -29,6 +29,8 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
CAPPluginMethod(name: "setMuted", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "setCamera", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "switchCamera", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "startScreenShare", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "stopScreenShare", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "syncVideoTiles", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "endCall", returnType: CAPPluginReturnPromise)
|
||||
]
|
||||
@@ -75,6 +77,10 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
// preferSpeaker and fixed it). Listening for route changes makes that self-healing.
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(audioRouteChanged(_:)),
|
||||
name: AVAudioSession.routeChangeNotification, object: nil)
|
||||
|
||||
// Screen sharing (ReplayKit broadcast extension). LiveKit tells us when a broadcast starts/stops and
|
||||
// (with shouldPublishTrack=true, the default) auto-publishes/unpublishes the screen-share track.
|
||||
BroadcastManager.shared.delegate = self
|
||||
}
|
||||
|
||||
@objc private func audioRouteChanged(_ note: Notification) {
|
||||
@@ -91,7 +97,10 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
// engine in didActivate can block on first use — determining it up front avoids that.
|
||||
AVAudioSession.sharedInstance().requestRecordPermission { _ in }
|
||||
let old = room
|
||||
let r = Room()
|
||||
// Route screen-share through the ReplayKit broadcast extension (so the user can share their screen
|
||||
// even when the app is backgrounded, and it captures the whole phone, not just the WebView).
|
||||
let opts = RoomOptions(defaultScreenShareCaptureOptions: ScreenShareCaptureOptions(useBroadcastExtension: true))
|
||||
let r = Room(roomOptions: opts)
|
||||
room = r
|
||||
Task { [weak self] in
|
||||
await old?.disconnect() // drop any previous/stale connection so we never leave DUPLICATE participants
|
||||
@@ -331,6 +340,26 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
}
|
||||
}
|
||||
|
||||
// Screen sharing from iOS: show the system broadcast picker. When the user starts the broadcast, the
|
||||
// extension streams the screen to us over IPC and LiveKit publishes it (BroadcastManager.shouldPublishTrack
|
||||
// defaults true). broadcastManager(didChangeState:) fires screenShareState back to the web either way.
|
||||
@objc func startScreenShare(_ call: CAPPluginCall) {
|
||||
guard room != nil else { call.reject("no active call"); return }
|
||||
DispatchQueue.main.async { BroadcastManager.shared.requestActivation() } // presents RPSystemBroadcastPickerView
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func stopScreenShare(_ call: CAPPluginCall) {
|
||||
BroadcastManager.shared.requestStop()
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
// MARK: - BroadcastManagerDelegate
|
||||
|
||||
public func broadcastManager(didChangeState isBroadcasting: Bool) {
|
||||
notifyListeners("screenShareState", data: ["sharing": isBroadcasting])
|
||||
}
|
||||
|
||||
// Position native video views to match the web meeting tiles. `tiles` = [{uid, local, x, y, w, h}] in
|
||||
// CSS px (== points; getBoundingClientRect coords). We create/move a VideoView for each participant that
|
||||
// has a live camera track, and remove views for tiles that are gone or whose camera is off (so the web
|
||||
|
||||
Reference in New Issue
Block a user