Native video Increment 2b: remote tiles + self-view on tile; speaker fix
Video (2b): render every participant's camera natively, positioned to
match the web meeting tiles. The web has no LiveKit connection on a native
call, so the plugin draws native VideoViews over the WebView. New
NativeCall.syncVideoTiles({tiles:[{uid,local,x,y,w,h}]}) — the web polls
each tile's getBoundingClientRect + user id (400ms + on camera toggle) and
the plugin places a VideoView (subview of the WKWebView, so CSS-px rects ==
points) for whoever has a live, unmuted camera track; camera-off keeps the
web avatar. Replaces the 2a fixed-corner self-view: your own camera now
renders on the __local tile. Remote video correlated by LiveKit identity ==
meetPeerUids user id. APIs verified vs client-sdk-swift 2.15.3 source:
Room.remoteParticipants[Participant.Identity(from:)], Participant.videoTracks,
TrackPublication.source/.track/.isMuted, Track.Source.camera.
Audio: fix "sound starts on the earpiece until I tap something" — the
LiveKit audio engine starting after CallKit activates the session flips the
route to the receiver. Added an AVAudioSession routeChange observer that
re-asserts the loudspeaker (via preferSpeaker) whenever we land on the
built-in receiver mid-call (headset/BT still win).
Web change is safe on the current (2a) build: syncVideoTiles is absent so
the poll no-ops. Needs a Codemagic build to take effect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+28
-1
@@ -5361,10 +5361,35 @@ function bzUnlockAudio(){
|
||||
document.addEventListener('touchend', function(){ if(meetState==='call') bzUnlockAudio(); }, {passive:true}); // fallback: any tap during a call restarts silent remote audio
|
||||
document.addEventListener('click', function(){ if(meetState==='call') bzUnlockAudio(); }, {passive:true});
|
||||
let meetNative=false, meetNativeUuid=null, _ncEnding=false; // native call: WebView joins the mesh for UI; the plugin owns media/CallKit
|
||||
// ---- Native call video (Increment 2b) ----------------------------------------------------------------
|
||||
// On a native call the WebView owns the meeting grid but has NO LiveKit connection, so it can't render any
|
||||
// video — the media lives only in the native plugin. So we hand the plugin each tile's on-screen rect + the
|
||||
// participant's user id, and it draws a native VideoView over each tile for whoever has a live camera
|
||||
// (camera-off participants keep showing the web avatar). Polled so it follows layout / rotation / scroll /
|
||||
// join-leave / camera-toggle changes; a no-op on web calls. getBoundingClientRect coords are CSS px, which
|
||||
// equal points in the WKWebView, so the plugin can use them directly.
|
||||
let _ncTileTimer=null;
|
||||
function bzNativeSyncTiles(){
|
||||
if(!meetNative) return;
|
||||
const NC=nativeCallPlugin(); if(!NC||!NC.syncVideoTiles) return;
|
||||
const tiles=[];
|
||||
document.querySelectorAll('#meetGrid .meet-tile').forEach(el=>{
|
||||
const id=el.id ? el.id.replace('meet-tile-','') : ''; if(!id || id==='__waiting') return;
|
||||
const r=el.getBoundingClientRect(); if(r.width<2||r.height<2) return;
|
||||
let uid, local=false;
|
||||
if(id==='__local'){ uid=(ME&&ME.id)||''; local=true; } else { uid=meetPeerUids.get(id)||''; }
|
||||
if(!uid) return;
|
||||
tiles.push({ uid, local, x:r.left, y:r.top, w:r.width, h:r.height });
|
||||
});
|
||||
try{ NC.syncVideoTiles({ tiles }); }catch(_){}
|
||||
}
|
||||
function bzNativeStartTiles(){ if(_ncTileTimer||!meetNative) return; _ncTileTimer=setInterval(bzNativeSyncTiles, 400); bzNativeSyncTiles(); }
|
||||
function bzNativeStopTiles(){ if(_ncTileTimer){ clearInterval(_ncTileTimer); _ncTileTimer=null; } const NC=nativeCallPlugin(); if(NC&&NC.syncVideoTiles){ try{ NC.syncVideoTiles({ tiles:[] }); }catch(_){} } }
|
||||
async function enterMeeting(code, audioOnly, opts){
|
||||
bzUnlockAudio(); // runs inside the Join tap → unlock playback so remote audio isn't silent until you tap
|
||||
if(meetState==='call'){ switchTab('meeting'); return; } // already in a call — ignore double-join
|
||||
meetNative=!!(opts&&opts.native); meetNativeUuid=(opts&&opts.uuid)||null; // native → skip our own SFU media; mute/end bridge to the plugin
|
||||
if(meetNative) bzNativeStartTiles(); // native call → drive the plugin's native video overlays from the web tiles
|
||||
// Joining this room → clear any lingering incoming-call invite popup for it (and stop its ring).
|
||||
// Fixes: joining via the header "Join" button left the Join/Decline popup on screen. Belt-and-braces
|
||||
// we clear ALL open invites, since you can only be in one call at a time.
|
||||
@@ -5473,7 +5498,8 @@ async function toggleCam(){
|
||||
const next=!meetCam; const NC=nativeCallPlugin();
|
||||
if(!NC||!NC.setCamera){ toast('Update the app to use video on calls'); return; }
|
||||
try{ await NC.setCamera({ on:next }); }catch(e){ toast('Could not turn the camera '+(next?'on':'off')); return; }
|
||||
meetCam=next; meetAudioOnly=false; updateCamBtn();
|
||||
meetCam=next; meetAudioOnly=false; updateCamBtn(); bzNativeSyncTiles(); // reflect the new camera state on the tiles now
|
||||
bzNativeStartTiles(); // ensure the overlay poll is running (first camera-on)
|
||||
// The web __local tile stays an avatar — the WebView has no camera stream in a native call (the plugin
|
||||
// owns media and draws its own self-view). Tell peers our camera state so their SFU tile shows the video.
|
||||
meetSend({type:'meeting-state', muted:!meetMic, camOff:!meetCam});
|
||||
@@ -5505,6 +5531,7 @@ function leaveMeeting(forced){
|
||||
// Native call: also end the CallKit/plugin call. _ncEnding tells the plugin's endCall listener this leave
|
||||
// originated here, so it doesn't call leaveMeeting again (the plugin ending the call re-fires endCall).
|
||||
if(meetNative && meetNativeUuid){ _ncEnding=true; callkitEnd(meetNativeUuid); }
|
||||
bzNativeStopTiles(); // stop the native video overlay poll + clear any native tiles
|
||||
meetNative=false; meetNativeUuid=null;
|
||||
stopRingback(); removeWaitingTile(); _dmCallWaiting=null; // #17/#7: any call exit stops the ring + ringing tile
|
||||
const isDm=!!(meetReturn && meetReturn.kind==='dm');
|
||||
|
||||
Reference in New Issue
Block a user