iOS meeting screen share: publish ReplayKit broadcast into the SFU room
Gap: on iOS, screen share only worked in native CallKit calls. In a scheduled / code-joined SFU meeting the webview called setScreenShareEnabled() -> getDisplayMedia(), which WKWebView does not implement, so it failed silently. Production meetings are real LiveKit rooms, so we can publish natively instead. Fix (native ReplayKit -> same LiveKit room as a dedicated screen participant): - server: /api/meetings/token accepts screen:true -> mints a distinct <peerId>-screen identity (LiveKit allows one connection per identity), so the native publisher doesn't collide with the webview's own connection. - native plugin: startMeetingScreenShare/stopMeetingScreenShare + connectScreenRoom (a screen-only LiveKit connection: mic off, no camera, no callConnected) that reuses the existing broadcast-extension publishing path. - webview: toggleScreen routes to the native method on iOS SFU meetings; the screenShareState listener now drives the SFU case too (and tears the screen connection down on the system "Stop Broadcast"); sfuAttach/sfuDetach map the '<peerId>-screen' participant's screen track onto the sharer's tile and suppress the phantom person tile + the sharer's own self-view. Web/server deploy now; the native method needs the next Codemagic build to test on device. Verified: db-smoke 22/22; Swift braces balanced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,8 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
CAPPluginMethod(name: "switchCamera", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "startScreenShare", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "stopScreenShare", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "startMeetingScreenShare", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "stopMeetingScreenShare", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "setHolePunch", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "setTileZoom", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "syncVideoTiles", returnType: CAPPluginReturnPromise),
|
||||
@@ -391,6 +393,41 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
// Screen share into a SCHEDULED / code-joined SFU meeting. The WebView already holds the meeting
|
||||
// connection (identity = peerId) and WKWebView has no getDisplayMedia — so we open a SECOND, screen-ONLY
|
||||
// native LiveKit connection under a distinct `<peerId>-screen` identity and publish the ReplayKit
|
||||
// broadcast into the SAME room. The WebView maps that identity back onto the sharer's tile.
|
||||
@objc func startMeetingScreenShare(_ call: CAPPluginCall) {
|
||||
let url = call.getString("url") ?? ""
|
||||
let token = call.getString("token") ?? ""
|
||||
guard !url.isEmpty, !token.isEmpty else { call.reject("url/token required"); return }
|
||||
connectScreenRoom(url: url, token: token)
|
||||
DispatchQueue.main.async { BroadcastManager.shared.requestActivation() } // system broadcast picker
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func stopMeetingScreenShare(_ call: CAPPluginCall) {
|
||||
BroadcastManager.shared.requestStop()
|
||||
let r = room; room = nil
|
||||
Task { await r?.disconnect() } // drop the screen-only connection (no call/tile/transcriber side effects)
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
// A screen-ONLY LiveKit connection (mic off, no camera) used purely to publish the ReplayKit broadcast
|
||||
// into an SFU meeting. Unlike connectRoom() it enables no mic and fires no callConnected — this is not a call.
|
||||
private func connectScreenRoom(url: String, token: String) {
|
||||
guard !url.isEmpty, !token.isEmpty else { return }
|
||||
let old = room
|
||||
let opts = RoomOptions(defaultScreenShareCaptureOptions: ScreenShareCaptureOptions(useBroadcastExtension: true))
|
||||
let r = Room(roomOptions: opts)
|
||||
room = r
|
||||
Task { [weak self] in
|
||||
await old?.disconnect() // never leave a duplicate connection
|
||||
do { try await r.connect(url: url, token: token) }
|
||||
catch { self?.notifyListeners("screenShareState", data: ["sharing": false, "error": String(describing: error)]) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - #5 Live transcript (native SFSpeechRecognizer)
|
||||
|
||||
// The current published local mic track — the source we tap for transcription. nil until the user has
|
||||
|
||||
Reference in New Issue
Block a user