// native-webkit.mjs — the Mac app in a call with Safari's engine. // // Driven by TestNativeMacWebKit (make native-webkit). 2026-10-03: Till, // on an iPhone in Safari, said a call with Paul on the Mac app "kept // cutting in and out", with nothing recorded. This is the nearest this // machine gets: Playwright's WebKit with its mock camera and microphone, // on macOS, against the shipping app core (CallsInterop). It is NOT iOS // Safari — no cellular radio, no background suspension, no audio session // interruptions, no Low Power Mode — and it cannot shape packet loss or // jitter (that needs root for dnctl/pfctl). What it can hold: // // - media both ways, and what was negotiated: the codecs each way and // Opus's in-band FEC; // - the app's sender giving way on a thin path (its bandwidth estimate // capped, as `uplink` does) while WebKit keeps decoding and hearing; // - the tab coming back after the system ended its camera and // microphone (iOS does, in the background): the room opens them again // and the app hears and sees it again (room.js comeBack). // // node test/browser/native-webkit.mjs import { webkit } from 'playwright'; import { spawn } from 'node:child_process'; import { createInterface } from 'node:readline'; import assert from 'node:assert/strict'; const [origin, room, binary] = process.argv.slice(2); const browser = await webkit.launch({ headless: true }); const context = await browser.newContext({ permissions: ['camera', 'microphone'] }); await context.addCookies([{ name: process.env.CALLS_COOKIE_NAME, value: process.env.CALLS_COOKIE_A, url: origin }]); const page = await context.newPage(); await page.addInitScript(() => { const Native = window.RTCPeerConnection; window.__pcs = new Set(); window.RTCPeerConnection = class extends Native { constructor(...args) { super(...args); window.__pcs.add(this); } }; // The streams the room opened, so the test can end their tracks as the // system would. window.__gum = []; const gum = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices); navigator.mediaDevices.getUserMedia = async (c) => { const s = await gum(c); window.__gum.push(s); return s; }; }); let native; let sample = null; let exited = false; const wait = async (predicate, label, seconds = 40) => { const until = Date.now() + seconds * 1000; while (Date.now() < until) { if (exited) throw new Error('native executable exited'); if (await predicate()) return; await new Promise((r) => setTimeout(r, 250)); } throw new Error(label + ': ' + JSON.stringify(sample && sample.stats)); }; // What WebKit is receiving from the app and sending to it, off its own // connection's stats. const webkitStats = () => page.evaluate(async () => { const out = { videoIn: null, audioIn: null, videoOut: null, audioOut: null, fec: null, pair: null }; for (const pc of window.__pcs) { if (pc.connectionState !== 'connected') continue; const stats = await pc.getStats(); const codecs = {}; stats.forEach((r) => { if (r.type === 'codec') codecs[r.id] = r; }); stats.forEach((r) => { const codec = r.codecId && codecs[r.codecId] ? codecs[r.codecId].mimeType + (codecs[r.codecId].sdpFmtpLine ? ' ' + codecs[r.codecId].sdpFmtpLine : '') : ''; if (r.type === 'inbound-rtp' && r.kind === 'video') { out.videoIn = { codec, framesDecoded: r.framesDecoded || 0, width: r.frameWidth || 0, height: r.frameHeight || 0, fps: r.framesPerSecond || 0, bytes: r.bytesReceived || 0, lost: r.packetsLost || 0, freezes: r.freezeCount || 0 }; } if (r.type === 'inbound-rtp' && r.kind === 'audio') { out.audioIn = { codec, bytes: r.bytesReceived || 0, energy: r.totalAudioEnergy || 0, concealed: r.concealedSamples || 0, samples: r.totalSamplesReceived || 0, lost: r.packetsLost || 0, fecRecovered: r.fecPacketsReceived || 0 }; } if (r.type === 'outbound-rtp' && r.kind === 'video') out.videoOut = { codec, frames: r.framesEncoded || 0 }; if (r.type === 'outbound-rtp' && r.kind === 'audio') out.audioOut = { codec, bytes: r.bytesSent || 0 }; if (r.type === 'candidate-pair' && r.nominated && r.state === 'succeeded') out.pair = { rtt: r.currentRoundTripTime }; }); const sdp = (pc.remoteDescription && pc.remoteDescription.sdp) || ''; const opus = /a=rtpmap:(\d+) opus\/48000/i.exec(sdp); if (opus) { const fmtp = new RegExp('a=fmtp:' + opus[1] + ' ([^\\r\\n]*)').exec(sdp); out.fec = fmtp ? fmtp[1] : ''; } break; } return out; }); try { page.on('pageerror', (e) => console.log('page error', String(e))); await page.goto(`${origin}/m/${room}`); await page.waitForFunction(() => !!window.meet); native = spawn(binary, [], { stdio: ['pipe', 'pipe', 'pipe'] }); native.on('exit', () => { exited = true; }); native.stdin.write(process.env.CALLS_NATIVE_INPUT + '\n'); createInterface({ input: native.stdout }).on('line', (text) => { try { const value = JSON.parse(text); if (value.stats) sample = value; } catch {} }); native.stderr.resume(); // WebKit wants a gesture before a remote