// native-motion.mjs — the app shares a 1080p page scrolling at 30 fps // and the browser measures what actually arrives: frames a second, // frame size and freezes on the received screen, beside the app's own // account of its sender (encoder, limitation, bitrate, path). // // Written for a report on 2026-09-24 that a screen shared from the Mac // app "isn't keeping up with what I'm doing on the screen". It measures // each "Optimise for" mode twice — on an open local link, and with the // app's uplink estimate capped the way a thin home connection caps it — // and holds motion (and the balanced default) to a frame rate a scroll // can be followed at. // // Run by TestNativeMacScreenMotion (make native-browser). Environment: // MOTION_MODES comma-separated modes, default "balanced,detail,motion" // MOTION_UPLINKS comma-separated caps in bit/s, 0 for none, default "0,1500000" // MOTION_ASSERT "0" to measure without asserting import { syntheticMedia } from './synthetic-media.mjs'; import { chromium } from 'playwright'; import { spawn } from 'node:child_process'; import { createInterface } from 'node:readline'; const [origin, room, binary] = process.argv.slice(2); const modes = (process.env.MOTION_MODES || 'balanced,detail,motion').split(',').filter(Boolean); const uplinks = (process.env.MOTION_UPLINKS || '0,1500000').split(',').map(Number); const asserting = process.env.MOTION_ASSERT !== '0'; // A scroll followed at fewer frames than this reads as a slideshow. const MOTION_FLOOR_FPS = Number(process.env.MOTION_FLOOR_FPS || 15); const browser = await chromium.launch({ headless: true, args: [ '--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream', '--autoplay-policy=no-user-gesture-required', '--disable-features=WebRtcHideLocalIpsWithMdns', '--mute-audio', ] }); const context = await browser.newContext({ permissions: ['camera', 'microphone'] }); if (process.env.CALLS_SYNTHETIC_MEDIA) await context.addInitScript(syntheticMedia); 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.__nativeInteropPCs = new Set(); window.RTCPeerConnection = class extends Native { constructor(...args) { super(...args); window.__nativeInteropPCs.add(this); } }; }); let native; let failed = false; try { await page.goto(`${origin}/m/${room}`); await page.waitForFunction(() => !!window.meet); native = spawn(binary, [], { stdio: ['pipe', 'pipe', 'pipe'] }); native.stdin.write(process.env.CALLS_NATIVE_INPUT + '\n'); let sample = null; let exited = false; native.on('exit', () => { exited = true; }); createInterface({ input: native.stdout }).on('line', (line) => { try { const value = JSON.parse(line); if (value.stats) sample = value; } catch {} }); native.stderr.resume(); const wait = async (predicate, label, seconds = 35) => { 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)); }; const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000)); await wait(() => sample?.stats.framesDecoded > 15 && sample?.stats.videoBytesSent > 0, 'native and browser connect'); // The received screen's counters, found by the share tile's own track. const received = () => page.evaluate(async () => { const v = document.querySelector('#tiles figure.calls-tile-share video'); const track = v && v.srcObject ? v.srcObject.getVideoTracks()[0] : null; if (!track) return null; for (const pc of window.__nativeInteropPCs) { if (pc.connectionState === 'closed') continue; const stats = await pc.getStats(); for (const r of stats.values()) { if (r.type === 'inbound-rtp' && r.kind === 'video' && r.trackIdentifier === track.id) { return { at: r.timestamp, decoded: r.framesDecoded || 0, width: r.frameWidth || 0, height: r.frameHeight || 0, freezes: r.freezeCount || 0, frozen: r.totalFreezesDuration || 0, jitter: r.jitterBufferDelay || 0, emitted: r.jitterBufferEmittedCount || 0, codec: (stats.get(r.codecId) || {}).mimeType || '', decoder: r.decoderImplementation || '', }; } } } return null; }); const results = []; for (const mode of modes) { native.stdin.write(`screen-mode ${mode}\n`); native.stdin.write('screen-motion\n'); await wait(() => sample?.sharingScreen === true, 'native shares the moving screen'); await wait(async () => (await received())?.decoded > 30, 'the browser decodes the moving screen'); for (const uplink of uplinks) { native.stdin.write(`uplink ${uplink}\n`); // Long enough for the estimate to ramp or fall and the encoder to // settle on it; then ten seconds measured. await sleep(uplink ? 12 : 15); const a = await received(); const s0 = sample.stats; const p0 = sample.syntheticScreenFrames || 0; await sleep(10); const b = await received(); const s1 = sample.stats; const p1 = sample.syntheticScreenFrames || 0; const seconds = (b.at - a.at) / 1000; const result = { mode, uplink, receivedFps: +((b.decoded - a.decoded) / seconds).toFixed(1), receivedSize: `${b.width}x${b.height}`, freezes: b.freezes - a.freezes, frozenSeconds: +(b.frozen - a.frozen).toFixed(2), jitterBufferMs: b.emitted > a.emitted ? Math.round(((b.jitter - a.jitter) / (b.emitted - a.emitted)) * 1000) : 0, codec: b.codec, decoder: b.decoder, sent: `${s1.screenSendWidth}x${s1.screenSendHeight}`, sentFps: (s1.screenSendFpsTenths || 0) / 10, producedFps: +((p1 - p0) / seconds).toFixed(1), capturedFps: (s1.screenCapturedFpsTenths || 0) / 10, encodedFps: +(((s1.screenFramesEncoded || 0) - (s0.screenFramesEncoded || 0)) / seconds).toFixed(1), limitation: ['none', 'cpu', 'bandwidth', 'other'][s1.screenLimitation || 0], sentKbps: Math.round((s1.screenSentBitrate || 0) / 1000), targetKbps: Math.round((s1.screenTargetBitrate || 0) / 1000), cameraKbps: Math.round(((s1.videoBytesSent || 0) - (s0.videoBytesSent || 0)) * 8 / seconds / 1000), encodeMs: (s1.screenEncodeMsTenths || 0) / 10, hardware: s1.screenHardwareEncoder === 1, path: ['', 'host', 'srflx', 'prflx', 'relay'][s1.screenPath || 0], }; results.push(result); console.log('MEASURE ' + JSON.stringify(result)); } native.stdin.write('uplink 0\n'); native.stdin.write('screen-off\n'); await wait(() => sample?.sharingScreen === false, 'native stops sharing'); await wait(async () => (await received()) === null, 'the browser drops the screen'); } if (asserting) { for (const r of results) { if (r.mode === 'motion' || r.mode === 'balanced') { if (r.receivedFps < MOTION_FLOOR_FPS) { failed = true; console.log(`FAIL ${r.mode} at uplink ${r.uplink || 'open'}: ${r.receivedFps} fps received, below ${MOTION_FLOOR_FPS}`); } else { console.log(`PASS ${r.mode} at uplink ${r.uplink || 'open'}: ${r.receivedFps} fps received at ${r.receivedSize}`); } } if (r.mode === 'detail' && r.receivedSize !== '1920x1080') { failed = true; console.log(`FAIL detail at uplink ${r.uplink || 'open'} gave up resolution: ${r.receivedSize}`); } } } native.stdin.write('leave\n'); await sleep(1); } finally { native?.kill('SIGTERM'); await browser.close(); } if (failed) process.exit(1);