fix(android-call): no hole-punch without native video (white meeting bg)

On an Android native call the WebView turned on hole-punch (transparent
meeting grid, so native video can show BEHIND the WebView) whenever the
plugin exposed setHolePunch — but on the 2a audio-only build there is no
native video behind it, so the transparent grid revealed the white Android
window (reported: white meeting background; tiles stayed visible as they're
opaque web elements). iOS never showed this because CallKit covers the view.

Gate hole-punch on native video actually existing: new ANDROID_NATIVE_VIDEO
server flag (mirrors ANDROID_NATIVE_MEDIA, default off) exposed via
/api/meetings/config. Web: bzNativeVideoOn() — iOS always, Android only when
the flag is on (Increment 2b APK). While off, hole-punch stays off and the
grid keeps its dark background; the native-tile poll doesn't run. iOS
unchanged. Flip the flag on once the 2b native-video APK is installed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
SravanandClaude Opus 4.8 committed 2026-11-03 20:12:21 +05:30
1 parent ea4d65274e
commit ae607f44b5
3 files changed
+20 -4

No files matched your search

+9
View File
@@ -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,
+9 -2
View File
@@ -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(){
+2 -2
View File
@@ -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.