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:
2026-08-06 21:09:02 +05:30
parent aed3d22675
commit 27c582bceb
9 changed files with 259 additions and 3 deletions
+8 -1
View File
@@ -3879,6 +3879,8 @@ async function setupNativeCall(){
NC.addListener('callError', (e)=>{ console.warn('[callkit] call error:', (e&&e.error)||''); });
// Muted from the CallKit system UI → reflect it in the meeting window's mic button.
NC.addListener('setMuted', (e)=>{ if(!e||meetState!=='call'||!meetNative) return; meetMic=!e.muted; try{ updateMicBtn(); setTileMute('__local', !meetMic); meetSend({type:'meeting-state', muted:!meetMic, camOff:!meetCam}); }catch(_){} });
// Screen share (ReplayKit) started/stopped from the system — reflect it and tell peers so their stage shows it.
NC.addListener('screenShareState', (e)=>{ if(meetState!=='call'||!meetNative) return; const on=!!(e&&e.sharing); meetScreen=on; try{ updateScreenBtn(); setLocalSharing(on); meetSend({type:'meeting-screen', on}); }catch(_){} });
// Ended on the CallKit system screen / plugin ended on remote hang-up. Leave the meeting window too (guarded
// so our own in-app hang-up, which already called the plugin, doesn't loop). Then tell the server.
NC.addListener('endCall', (d)=>{ try{
@@ -5226,7 +5228,12 @@ function meetMakePeer(peerId, name){
// (no extra tiles, the peer's video just shows the screen). Falls back to addTrack+renegotiate if
// no video sender exists yet (camera never turned on). Stopping restores the camera (or avatar).
async function toggleScreen(){
if(meetNative){ toast('Sharing your screen from iPhone isnt supported yet — but youll see others shared screens.'); return; } // iOS has no getDisplayMedia / ReplayKit ext wired
if(meetNative){ // native call: the plugin shares the whole phone screen via the ReplayKit broadcast extension
const NC=nativeCallPlugin();
if(!NC||!NC.startScreenShare){ toast('Update the app to share your screen'); return; }
try{ if(meetScreen) await NC.stopScreenShare(); else await NC.startScreenShare(); }catch(_){}
return; // meetScreen + the button flip happen on the authoritative screenShareState event
}
if(meetScreen){ stopScreen(); return; }
if(!meetMultiShare && meetSharers.size>0){ toast('Someone is already sharing their screen'); return; }
if(SFU.on){