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:
2026-08-22 01:01:42 +05:30
parent 14eb99edaf
commit 467a8b9c6e
3 changed files with 82 additions and 2 deletions
+7 -1
View File
@@ -1055,7 +1055,13 @@ route('POST', '/api/meetings/token', async (req, res) => {
try { const peers = require('./presence').meetingRooms.get(rm); const pr = peers && peers.get(pid); if (pr && pr.uid && pr.uid !== u.id) ok = false; } catch (_) {}
if (ok) identity = pid;
}
const metadata = JSON.stringify({ avatarUrl: u.avatar_url || '' });
// Screen-share publisher (iOS): the WebView already holds the meeting connection under `identity`, and
// LiveKit allows one connection per identity — so the native ReplayKit publisher joins the SAME room under
// a DISTINCT `<identity>-screen` id. The client maps that suffix back onto the sharer's tile. WKWebView has
// no getDisplayMedia, so this native second connection is the only way to screen-share in an SFU meeting.
const screen = body.screen === true || body.screen === 1 || body.screen === '1';
if (screen) identity = identity + '-screen';
const metadata = JSON.stringify({ avatarUrl: u.avatar_url || '', screen });
const token = livekitToken(identity, u.name || u.email, rm, metadata);
json(res, 200, { token, url: LIVEKIT_URL, identity, name: u.name || u.email });
});