diff --git a/server/config.js b/server/config.js index 0200fc8..18c4093 100644 --- a/server/config.js +++ b/server/config.js @@ -63,6 +63,14 @@ const ANDROID_CALLS = process.env.ANDROID_CALLS === '1'; // the LiveKit-Android plugin. Set ANDROID_NATIVE_MEDIA=1. const ANDROID_NATIVE_MEDIA = process.env.ANDROID_NATIVE_MEDIA === '1'; +// Native call VIDEO on Android (Increment 2b): the plugin captures/publishes the camera and renders remote video +// tiles natively via hole-punch (a transparent WebView over native VideoViews), mirroring iOS. INDEPENDENT of +// ANDROID_NATIVE_MEDIA (audio) so video can be enabled only once a 2b-capable APK is installed. OFF (default) → +// Android native calls are AUDIO-ONLY and the WebView keeps its normal (opaque, dark) meeting grid — hole-punch +// stays off, so no transparent grid reveals the page behind it. ON → native video + hole-punch. Requires an app +// build carrying the 2b native-video plugin. Set ANDROID_NATIVE_VIDEO=1. +const ANDROID_NATIVE_VIDEO = process.env.ANDROID_NATIVE_VIDEO === '1'; + module.exports = { PORT: process.env.PORT || 8090, HTTPS_PORT: process.env.HTTPS_PORT || 8443, @@ -82,6 +90,7 @@ module.exports = { CALLKIT_ENABLED, ANDROID_CALLS, ANDROID_NATIVE_MEDIA, + ANDROID_NATIVE_VIDEO, PUBLIC_DIR, REC_DIR, TRANS_DIR, diff --git a/server/public/home.html b/server/public/home.html index 30c2999..37f4352 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -4336,6 +4336,7 @@ async function unsubscribePush(){ // (onDmCall / the group invite check nativeCallOn()). let _callkitReady=false; let _callWebViewMedia=false; // Android Increment 1: native full-screen ring, but MEDIA stays in the WebView (iOS runs the call media natively via the plugin). Governs the answer/outgoing branches below. +let _androidNativeVideo=false; // Android Increment 2b: plugin renders native video tiles via hole-punch. OFF until a 2b APK is installed (server androidNativeVideo). While off, hole-punch stays off so the grid keeps its dark background (a transparent grid with no native video behind = white page). iOS video is always native. let _nativeAnswered=new Set(); // rooms this device answered natively (so a "call-taken" from the server doesn't end our own call) let _myVoipTok=''; // this device's own iOS VoIP (ios-voip) token — sent on answer so the answered-elsewhere cancel skips us function nativeCallPlugin(){ const P=window.Capacitor&&window.Capacitor.Plugins; return (P&&P.NativeCall)||null; } @@ -4354,8 +4355,9 @@ async function setupNativeCall(){ // Speaker/Bluetooth) and the WebView must NOT also join SFU media. Until then (default), media stays in the // WebView (video calls included). androidNativeMedia === true → _callWebViewMedia=false (native media path). _callWebViewMedia = !(cfg && cfg.androidNativeMedia); + _androidNativeVideo = !!(cfg && cfg.androidNativeVideo); // Increment 2b: native video tiles + hole-punch if(!_callWebViewMedia){ - console.log('[nativecall] Android native call media ON — background audio + native routing'); + console.log('[nativecall] Android native call media ON — background audio + native routing' + (_androidNativeVideo?' + native video (2b)':' (audio-only; video in WebView path off)')); // Android CAN switch the output (unlike iOS): mirror the plugin's real route into the speaker button. try{ NC.addListener('audioRoute', (e)=>{ bzOnAndroidRoute(e); }); }catch(_){} } @@ -6183,10 +6185,15 @@ function bzNativeSyncTiles(){ }); try{ NC.syncVideoTiles({ tiles }); }catch(_){} } +// Native video available? iOS: always (shipped). Android: only when a 2b-capable APK is active (server flag). +// While false we must NOT enable hole-punch — a transparent grid with no native video behind it shows the white +// page (the 2a audio-only bug). Keeps the opaque dark grid + web avatars instead. +function bzNativeVideoOn(){ try{ const p=nativePlatform(); if(p==='ios') return true; if(p==='android') return !!_androidNativeVideo; return false; }catch(_){ return false; } } function bzNativeStartTiles(){ if(!meetNative) return; const NC=nativeCallPlugin(); - if(NC && NC.setHolePunch){ bzHolePunch=true; try{ NC.setHolePunch({ on:true }); }catch(_){} document.body.classList.add('bz-hp'); } // permanent fix: video behind transparent WebView + if(NC && NC.setHolePunch && bzNativeVideoOn()){ bzHolePunch=true; try{ NC.setHolePunch({ on:true }); }catch(_){} document.body.classList.add('bz-hp'); } // video behind transparent WebView (only when native video exists) + if(!bzNativeVideoOn()) return; // no native video (e.g. Android 2a audio-only): nothing to render; keep the normal web grid if(_ncTileTimer) return; _ncTileTimer=setInterval(bzNativeSyncTiles, 400); bzNativeSyncTiles(); } function bzNativeStopTiles(){ diff --git a/server/routes.js b/server/routes.js index 946f4dd..d063b72 100644 --- a/server/routes.js +++ b/server/routes.js @@ -116,7 +116,7 @@ const API_KEY_SCOPES = ['report:read', 'audit:read']; const { onlineAgents, meetingRooms, groupCalls, dmCalls } = require('./presence'); const CALLS = require('./calls'); require('./reminders'); // start the 10-minute meeting-reminder loop -const { REC_DIR, TRANS_DIR, UPLOADS_DIR, SESSION_TTL, REFRESH_TTL, LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_ENABLED, PUBLIC_BASE_URL, GIPHY_API_KEY, CALLKIT_ENABLED, ANDROID_CALLS, ANDROID_NATIVE_MEDIA } = require('./config'); +const { REC_DIR, TRANS_DIR, UPLOADS_DIR, SESSION_TTL, REFRESH_TTL, LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_ENABLED, PUBLIC_BASE_URL, GIPHY_API_KEY, CALLKIT_ENABLED, ANDROID_CALLS, ANDROID_NATIVE_MEDIA, ANDROID_NATIVE_VIDEO } = require('./config'); const https = require('https'); // Small GET-JSON helper for the GIPHY proxy (keeps the key server-side). function fetchJSON(url) { @@ -993,7 +993,7 @@ route('POST', '/api/calls/invite', async (req, res) => { // Does this deployment use the LiveKit SFU for meeting media? The client asks on load; if sfu is // false it uses the built-in P2P mesh. url is the browser-facing signaling endpoint (wss://…). route('GET', '/api/meetings/config', (req, res) => { - json(res, 200, { sfu: LIVEKIT_ENABLED, url: LIVEKIT_ENABLED ? LIVEKIT_URL : '', callkit: CALLKIT_ENABLED, androidCall: ANDROID_CALLS, androidNativeMedia: ANDROID_NATIVE_MEDIA, fcm: PUSH.fcmReady() }); + json(res, 200, { sfu: LIVEKIT_ENABLED, url: LIVEKIT_ENABLED ? LIVEKIT_URL : '', callkit: CALLKIT_ENABLED, androidCall: ANDROID_CALLS, androidNativeMedia: ANDROID_NATIVE_MEDIA, androidNativeVideo: ANDROID_NATIVE_VIDEO, fcm: PUSH.fcmReady() }); }); // #5 GIF search — server-side GIPHY proxy so the API key never reaches the browser. Empty q → trending.