feat(media): share Browser encoding and complete local diagnostics

* fix: allow Linux interface discovery in embedded media service

* feat: expand local diagnostic reports

* research: validate independent Browser encoding producers

* feat: prototype Browser pool ownership and record remaining media gates

* feat: refine Browser shared encoders and native output reuse

* Refine carrier costs and simplify release entry points

* Close media phase and prepare piik design handoff
This commit is contained in:
AAA authored and GitHub committed 2026-09-09 20:59:44 +08:00
1 parent cfc323adce
commit c9a6642ac1
79 files changed
+3955 -1176

No files matched your search

+375
View File
@@ -0,0 +1,375 @@
// Actual HostPeer/BrowserEncodingPool comparison; synthetic source and local receivers only.
import { BrowserEncodingPool as ProbePool } from '../src/client/media/browser-encoding-pool';
import { HostPeer as ProbeHostPeer } from '../src/client/webrtc/host-peer';
import { applyVideoCaptureProfile as probeApplyProfile } from '../src/client/media/quality';
import { browserDebugEnabled, exportBrowserDebug } from '../src/client/lib/debug';
const settings = new URLSearchParams(location.search);
const mode = settings.get('mode') || 'carrier', codec = settings.get('codec') || 'VP8';
const network = settings.get('network') === '1', automatic = settings.get('automatic') === '1';
const high = settings.get('high') === '1', av = settings.get('av') === '1';
const single = settings.get('single') === '1', late = settings.get('late') === '1';
const nativeSource = settings.get('nativeSource') === '1', background = settings.get('background') === '1';
const lifecycle = settings.get('lifecycle') === '1', shapedRate = Number(settings.get('rate') || 120000);
const relay = settings.get('relay') === '1';
const selectedProfile = () => ({ resolution: high ? '1080p' : '480p', maxFramerate: 30,
maxBitrate: high ? 5000000 : 1700000,
degradationPreference: settings.get('preference') || (high ? 'maintain-resolution' : 'balanced'),
screenAudioQuality: 'music' });
const productPool = mode === 'carrier' ? new ProbePool() : null;
const productPeers = [], peers = [], tracks = [], edges = [], monitors = [], audioMonitors = [];
const errors = [], sourcePulses = [], controls = [], visibility = [], productTrace = [];
const sourceTimes = new Map(), seenProductGroups = new Map();
let rawSource, drawing, productTraceTimer, audioContext, audioTrack, audioGain, upstreamPeer;
let producing = true, sourceFrame = 0, phase = 'startup';
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
const poolState = () => productPool ? {
members: [...productPool.members].map(m => ({ id: m.id, group: m.current?.producer.id,
pending: m.pending?.group.producer.id, selecting: m.pending?.selecting, budget: m.budget,
mixed: m.mixed, disabled: m.disabled, carrier: m.carrier, replacing: !!m.replacement,
output: m.output, queued: m.encoded.current?.frames.length,
waitingKey: m.encoded.current?.needKey, pendingFrames: m.encoded.pending?.queue.frames.length })),
groups: [...productPool.groups].map(g => ({ id: g.producer.id, budget: g.budget,
ready: g.ready, failed: g.failed, output: g.output }))
} : null;
window.probeProgress = () => ({ phase, sourceFrame, errors, pool: poolState(),
peers: peers.map(p => ({ connection: p.connectionState, ice: p.iceConnectionState })),
audio: audioMonitors.map(m => ({ role: m.role, attached: m.attached, pulses: m.pulses.length })) });
function watch(video, role) {
const canvas = document.createElement('canvas');
canvas.width = 640; canvas.height = 360;
const context = canvas.getContext('2d', { willReadFrequently: true }), samples = [];
let stopped = false;
const next = () => video.requestVideoFrameCallback((_time, metadata) => {
if (stopped) return;
if (nativeSource) {
samples.push({ at: performance.now(), phase, id: metadata.presentedFrames, age: null,
width: video.videoWidth, height: video.videoHeight });
} else {
try {
context.drawImage(video, 0, 0, 640, 360);
const pixels = context.getImageData(0, 0, 640, 30).data;
let id = 0;
for (let bit = 0; bit < 12; bit++) {
const index = (15 * 640 + 20 + bit * 50) * 4;
if (pixels[index] + pixels[index + 1] + pixels[index + 2] > 384) id |= 1 << bit;
}
const now = performance.now(), created = sourceTimes.get(id);
samples.push({ at: now, phase, id, age: created === undefined ? null : now - created,
width: video.videoWidth, height: video.videoHeight });
} catch (error) { errors.push(String(error.message)); }
}
next();
});
next();
monitors.push({ role, samples, stop: () => { stopped = true; } });
}
function watchAudio(video, role) {
// Observe element-level played audio, not the earlier receiver track or physical speakers.
const played = video.captureStream();
const monitor = { role, pulses: [], attached: false, active: false,
stop() { this.active = false; played.getTracks().forEach(t => t.stop()); } };
audioMonitors.push(monitor);
const attach = () => {
if (monitor.attached || !played.getAudioTracks().length) return;
monitor.attached = monitor.active = true;
const source = audioContext.createMediaStreamSource(played), analyser = audioContext.createAnalyser();
analyser.fftSize = 512;
const silent = audioContext.createGain();
silent.gain.value = 0;
source.connect(analyser).connect(silent).connect(audioContext.destination);
const values = new Float32Array(analyser.fftSize);
let sounded = false;
const sample = () => {
if (!monitor.active) { source.disconnect(); analyser.disconnect(); silent.disconnect(); return; }
analyser.getFloatTimeDomainData(values);
const loud = values.some(value => Math.abs(value) > .01);
if (loud && !sounded) monitor.pulses.push({ at: performance.now(), phase });
sounded = loud;
requestAnimationFrame(sample);
};
sample();
};
played.addEventListener('addtrack', attach);
attach();
}
async function pair(role) {
const receive = new RTCPeerConnection({ iceServers: [] }), pending = [];
peers.push(receive);
const stream = new MediaStream([rawSource, ...(audioTrack ? [audioTrack] : [])]);
let peer;
peer = new ProbeHostPeer(role, { iceServers: [] }, stream, selectedProfile(), {
sendSignal(_id, payload) {
if (payload.kind === 'candidate' && !(network && role === 'A')) {
if (receive.remoteDescription) receive.addIceCandidate(payload.candidate).catch(error => errors.push(String(error)));
else pending.push(payload.candidate);
}
return true;
},
onUpdate(snapshot) { if (snapshot.error) errors.push(snapshot.error); }
}, { primary: codec.toLowerCase(), vp8Fallback: false }, undefined, false, role === 'U' ? null : productPool);
if (role === 'U') upstreamPeer = peer;
else productPeers.push(peer);
peers.push(peer.connection);
const video = document.createElement('video'), received = new MediaStream();
video.autoplay = video.muted = video.playsInline = true;
video.width = 320; video.srcObject = received;
if (role !== 'U') document.body.append(video);
receive.ontrack = event => { received.addTrack(event.track); video.play().catch(() => {}); };
if (av && role !== 'U') video.addEventListener('loadeddata', () => watchAudio(video, role), { once: true });
if (!(network && role === 'A')) receive.onicecandidate = event => {
if (event.candidate) void peer.acceptSignal({ kind: 'candidate',
connectionId: peer.connectionId, candidate: event.candidate.toJSON() });
};
if (!await peer.start()) throw Error('Product HostPeer failed to start');
const edge = { role, peer, send: peer.connection, receive, sender: peer.videoSender, pending, received };
edges.push(edge);
if (role !== 'U') watch(video, role);
return edge;
}
async function connect(edge) {
if (network && edge.role === 'A') {
const gathered = async pc => {
const end = performance.now() + 5000;
while (pc.iceGatheringState !== 'complete' && performance.now() < end) await wait(20);
};
const firstPort = sdp => Number(sdp.match(/^a=candidate:\S+ 1 udp \d+ \S+ (\d+)/mi)?.[1]);
const proxySDP = (description, port) => ({ ...description, sdp: description.sdp.replace(/^a=candidate:.*\r?\n/gm, '')
.replace(/^a=mid:[^\r\n]+\r?$/gm, line => line.trimEnd() + '\r\na=candidate:proxy 1 udp 2130706431 127.0.0.1 ' + port + ' typ host\r') });
const proxy = await fetch('/shaper').then(r => r.json());
await gathered(edge.send);
const offer = { type: 'offer', sdp: edge.send.localDescription.sdp };
if (!offer.sdp.includes('a=end-of-candidates')) offer.sdp += 'a=end-of-candidates\r\n';
await edge.receive.setRemoteDescription(proxySDP(offer, proxy.ports.right));
await edge.receive.setLocalDescription(await edge.receive.createAnswer());
await gathered(edge.receive);
const answer = { type: 'answer', sdp: edge.receive.localDescription.sdp };
if (!answer.sdp.includes('a=end-of-candidates')) answer.sdp += 'a=end-of-candidates\r\n';
await fetch('/shaper?left=' + firstPort(offer.sdp) + '&right=' + firstPort(answer.sdp));
await edge.peer.acceptSignal({ kind: 'description', connectionId: edge.peer.connectionId,
description: proxySDP(answer, proxy.ports.left) });
} else {
await edge.receive.setRemoteDescription(edge.send.localDescription);
for (const candidate of edge.pending.splice(0)) await edge.receive.addIceCandidate(candidate);
await edge.receive.setLocalDescription(await edge.receive.createAnswer());
await edge.peer.acceptSignal({ kind: 'description', connectionId: edge.peer.connectionId,
description: edge.receive.localDescription });
}
const deadline = performance.now() + 6000;
while (edge.send.connectionState !== 'connected' && performance.now() < deadline) await wait(30);
if (edge.send.connectionState !== 'connected') throw Error('Connection deadline');
}
const selectReport = report => Array.from(report.values())
.filter(r => ['outbound-rtp', 'inbound-rtp', 'remote-inbound-rtp', 'remote-outbound-rtp',
'candidate-pair', 'transport', 'local-candidate', 'remote-candidate'].includes(r.type))
.map(r => {
const fields = ['type', 'kind', 'id', 'localCandidateId', 'remoteCandidateId', 'selectedCandidatePairId',
'port', 'nominated', 'framesEncoded', 'framesDecoded', 'framesSent', 'framesReceived', 'bytesSent',
'bytesReceived', 'totalEncodeTime', 'totalDecodeTime', 'frameWidth', 'frameHeight', 'framesPerSecond',
'targetBitrate', 'qualityLimitationReason', 'qpSum', 'pliCount', 'nackCount', 'retransmittedBytesSent',
'packetsLost', 'jitter', 'jitterBufferDelay', 'jitterBufferEmittedCount', 'freezeCount',
'totalFreezesDuration', 'reportsSent', 'remoteTimestamp', 'currentRoundTripTime', 'availableOutgoingBitrate', 'state'];
return Object.fromEntries(fields.filter(key => r[key] !== undefined).map(key => [key, r[key]]));
});
async function statistics() {
const result = {};
for (const edge of edges) {
const metrics = edge.peer.getSnapshot().metrics;
result[edge.role] = { send: selectReport(await edge.send.getStats()),
receive: selectReport(await edge.receive.getStats()),
details: Object.fromEntries(['frameWidth', 'frameHeight', 'framesPerSecond', 'bitrateKbps',
'qualityLimitationReason', 'nativeEdgeQualityState', 'intervalFramesSent', 'intervalFramesEncoded']
.map(key => [key, metrics[key]])) };
}
if (productPool) {
for (const group of productPool.groups) if (!seenProductGroups.has(group.producer.id))
seenProductGroups.set(group.producer.id, { name: 'G' + seenProductGroups.size, group });
for (const entry of seenProductGroups.values()) {
const { name, group } = entry;
const report = await group.producer.report().catch(() => entry.report ?? group.report);
if (report) {
entry.report = report;
result[name] = { send: selectReport(report), receive: [], retired: !productPool.groups.has(group) };
}
}
}
return result;
}
async function snapshot() {
return { phase, at: performance.now(), stats: await statistics(),
process: await fetch('/process').then(r => r.json()),
...(network ? { shaper: await fetch('/shaper').then(r => r.json()) } : {}) };
}
async function run() {
if (av) {
audioContext = new AudioContext({ sampleRate: 48000 });
await audioContext.resume();
const destination = audioContext.createMediaStreamDestination(), tone = audioContext.createOscillator();
tone.frequency.value = 1000;
audioGain = audioContext.createGain(); audioGain.gain.value = 0;
tone.connect(audioGain).connect(destination); tone.start();
audioTrack = destination.stream.getAudioTracks()[0]; tracks.push(audioTrack);
}
const source = document.createElement('canvas');
source.width = high ? 1920 : 640; source.height = high ? 1080 : 360;
if (!nativeSource) document.body.prepend(source);
const ctx = source.getContext('2d');
if (high) ctx.scale(3, 3);
rawSource = nativeSource ? (await navigator.mediaDevices.getUserMedia({
video: { width: source.width, height: source.height, frameRate: 30 }, audio: false
})).getVideoTracks()[0] : source.captureStream(0).getVideoTracks()[0];
rawSource.contentHint = 'motion'; tracks.push(rawSource);
await probeApplyProfile(rawSource, selectedProfile());
const main = rawSource;
const connectChildren = async () => {
for (const role of single ? ['A'] : ['A', 'B']) await pair(role);
for (const edge of edges) if (edge.role !== 'U' && (!late || edge.role !== 'B')) await connect(edge);
};
if (!relay) await connectChildren();
if (!nativeSource) drawing = setInterval(() => {
if (!producing) return;
const id = sourceFrame++ % 4096;
sourceTimes.set(id, performance.now());
if (audioGain) {
audioGain.gain.setValueAtTime(id % 60 < 6 ? .1 : 0, audioContext.currentTime);
if (id % 60 === 0) sourcePulses.push({ id, at: performance.now() });
}
ctx.fillStyle = '#172b3a'; ctx.fillRect(0, 0, 640, 360);
ctx.fillStyle = '#48c98b'; ctx.fillRect((sourceFrame * 11) % 540, 65, 100, 220);
ctx.fillStyle = '#f7b744'; ctx.font = '40px sans-serif'; ctx.fillText(String(id), 30, 335);
for (let bit = 0; bit < 12; bit++) {
ctx.fillStyle = id & (1 << bit) ? '#fff' : '#000'; ctx.fillRect(bit * 50, 0, 50, 30);
}
(relay ? main : rawSource).requestFrame();
}, 1000 / (lifecycle ? 60 : 30));
if (productPool) productTraceTimer = setInterval(() => productTrace.push({ at: performance.now(), phase, ...poolState() }), 500);
if (relay) {
const upstream = await pair('U');
await connect(upstream);
rawSource = upstream.received.getVideoTracks()[0];
audioTrack = upstream.received.getAudioTracks()[0];
if (!rawSource || av && !audioTrack) throw Error('Relay upstream did not supply its tracks');
rawSource.contentHint = 'motion';
await connectChildren();
}
if (late) { await wait(1000); await connect(edges.find(e => e.role === 'B')); }
await wait(1900);
if (productPool) {
const deadline = performance.now() + 12000;
let activated = null;
for (;;) {
const members = [...productPool.members];
const sharing = members.length === productPeers.length && members.every(m => m.current && m.carrier && !m.replacement) &&
new Set(members.map(m => m.current)).size === 1;
if (sharing) activated ??= performance.now();
else activated = null;
if (activated !== null && monitors.every(m => {
const last = m.samples.at(-1);
return last?.width > 16 && (nativeSource ? last.at >= activated :
last.age !== null && (sourceTimes.get(last.id) ?? -1) >= activated);
})) break;
if (performance.now() >= deadline) throw Error('Product shared output did not become fresh');
await wait(30);
}
}
const descriptions = edges.map(e => ({ role: e.role,
extensions: e.send.localDescription?.sdp.match(/^a=extmap:.*$/gm),
parameters: e.sender.getParameters().encodings }));
const snapshots = [await snapshot()];
phase = 'healthy'; await wait(6000); snapshots.push(await snapshot());
if (lifecycle) {
let appliedProfile = selectedProfile();
const apply = async profile => {
const at = performance.now();
await probeApplyProfile(rawSource, profile);
const applied = await Promise.all(productPeers.map(peer => peer.updateCaptureProfile(profile)));
controls.push({ phase, profile, elapsedMs: performance.now() - at, applied });
if (!applied.every(Boolean)) throw Error('Product profile update failed');
appliedProfile = profile;
};
const observe = async (name, duration = 6000) => {
phase = name; await wait(duration); snapshots.push(await snapshot());
if (name.startsWith('quiet-') || name.startsWith('paused-')) return;
for (const monitor of monitors) {
const last = monitor.samples.at(-1), ceiling = { '480p': 480, '720p': 720, '1080p': 1080 }[appliedProfile.resolution];
if (last?.phase !== name || last.age === null || last.age >= 1000 ||
performance.now() - last.at >= 1000 || last.height > ceiling)
throw Error(name + ': receiver did not deliver current frames within the requested ceiling');
}
};
const p60 = { ...selectedProfile(), resolution: '720p', maxFramerate: 60, maxBitrate: 5000000 };
phase = 'profile-720p60'; await apply(p60); await observe(phase);
producing = false; phase = 'quiet-profile-480p30';
await apply({ ...p60, resolution: '480p', maxFramerate: 30, maxBitrate: 3000000 });
await observe(phase, 2500);
producing = true; await observe('resumed-480p30');
rawSource.enabled = false; productPeers.forEach(peer => peer.setPaused(true));
phase = 'paused-profile-720p30'; await apply({ ...p60, maxFramerate: 30 }); await observe(phase, 2000);
rawSource.enabled = true; productPeers.forEach(peer => peer.setPaused(false));
await observe('resumed-720p30');
const oldSource = rawSource;
rawSource = source.captureStream(0).getVideoTracks()[0];
rawSource.contentHint = 'motion'; tracks.push(rawSource);
await probeApplyProfile(rawSource, appliedProfile);
const stream = new MediaStream([rawSource, ...(audioTrack ? [audioTrack] : [])]);
const replaced = await Promise.all(productPeers.map(peer => peer.replaceStream(stream)));
controls.push({ action: 'source-replaced', replaced });
if (!replaced.every(Boolean)) throw Error('Product source replacement failed');
oldSource.stop(); await observe('replaced-source');
if (productPool && [...productPool.groups].some(group => group.source !== rawSource)) throw Error('Old source group survived replacement');
if (productPool) {
for (const group of [...productPool.groups]) group.producer.fail();
await observe('ordinary-after-producer-failure');
if (productPool.groups.size || productPeers.some(peer => !peer.isConnected())) throw Error('Ordinary recovery did not retire the pool');
}
}
if (background) {
phase = 'hidden'; await fetch('/visibility?hidden=1');
visibility.push({ at: performance.now(), requested: 'hidden', actual: document.visibilityState });
if (document.visibilityState !== 'hidden') throw Error('Page was not actually hidden');
await wait(10000); snapshots.push(await snapshot());
phase = 'visible-again'; await fetch('/visibility?hidden=0');
visibility.push({ at: performance.now(), requested: 'visible', actual: document.visibilityState });
if (document.visibilityState !== 'visible') throw Error('Page was not actually restored');
await wait(6000); snapshots.push(await snapshot());
}
if (network) {
await fetch('/shaper?rate=' + shapedRate);
phase = 'limited'; await wait(14000); snapshots.push(await snapshot());
await fetch('/shaper?rate=0');
// --auto extends recovery observation; adaptation always belongs to the product.
phase = 'released'; await wait(automatic ? 40000 : 10000); snapshots.push(await snapshot());
}
return { descriptions, snapshots, browser: navigator.userAgent, sourcePulses,
audioMonitors: audioMonitors.map(({ role, pulses, attached }) => ({ role, pulses, attached })),
monitors: monitors.map(({ role, samples }) => ({ role, samples })) };
}
const configuration = { mode, codec, product: true, network, automatic, av, high, single, late,
background, nativeSource, lifecycle, relay, shapedRate, profile: selectedProfile() };
try {
window.probeResult = { ...configuration, ...await run(), controls, visibility, productTrace, errors };
if (browserDebugEnabled) {
const report = JSON.parse(await exportBrowserDebug());
window.probeResult.debug = report;
if (productPool && (!report.events.some(e => e.scope === 'encoding-pool' && e.event === 'sample') ||
!report.events.some(e => e.scope === 'webrtc' && e.event === 'stats' && e.details.role === 'pool-producer')))
throw Error('Debug report omitted the pool or producer evidence');
}
} catch (error) {
window.probeResult = { ...configuration, error: String(error?.stack || error?.message || error),
progress: window.probeProgress(), controls, visibility, productTrace, errors };
} finally {
clearInterval(drawing); clearInterval(productTraceTimer);
monitors.forEach(m => m.stop()); audioMonitors.forEach(m => m.stop());
productPeers.forEach(peer => peer.dispose()); productPool?.dispose();
upstreamPeer?.dispose();
tracks.forEach(t => t.stop()); peers.forEach(pc => pc.close());
await audioContext?.close();
window.probeDone = true;
}
+105
View File
@@ -0,0 +1,105 @@
import { createServer } from 'node:http';
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { build } from 'esbuild';
import { join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { CdpConnection, cleanupRun, createPage, evaluate, launchChrome, reservePort, waitForSample, waitForVersion } from './browser-gate-harness';
import { createPoolShaper } from './browser-pool-shaper.mjs';
const mode = process.argv[2] || 'carrier', args = process.argv.slice(3);
const flags = new Set(['--product', '--1080', '--h264', '--single', '--av', '--late', '--background',
'--native-source', '--lifecycle', '--network', '--auto', '--relay', '--debug']);
if (!['ordinary', 'carrier'].includes(mode)) throw Error('Expected ordinary|carrier');
for (const arg of args) if (!flags.has(arg) && !arg.startsWith('--rate=') && !arg.startsWith('--preference='))
throw Error('Unsupported option: ' + arg);
const codec = args.includes('--h264') ? 'H264' : 'VP8', network = args.includes('--network');
const automatic = args.includes('--auto'), high = args.includes('--1080'), av = args.includes('--av');
const single = args.includes('--single'), late = args.includes('--late'), background = args.includes('--background');
const nativeSource = background || args.includes('--native-source'), lifecycle = args.includes('--lifecycle');
const relay = args.includes('--relay');
const rate = Number(args.find(arg => arg.startsWith('--rate='))?.split('=')[1] || 120000);
const preference = args.find(arg => arg.startsWith('--preference='))?.split('=')[1] || (high ? 'maintain-resolution' : 'balanced');
if (!['balanced', 'maintain-resolution', 'maintain-framerate'].includes(preference)) throw Error('Unsupported degradation preference');
if (!Number.isInteger(rate) || rate <= 0) throw Error('Rate must be a positive integer in bps');
if (lifecycle && (nativeSource || network || relay)) throw Error('Lifecycle uses direct canvas capture without network shaping');
if (single && late) throw Error('Single-viewer control cannot also late-join B');
const root = resolve(import.meta.dirname, '..');
const bundle = await build({ entryPoints: [join(root, 'scripts/browser-local-pool-page.js')],
bundle: true, write: false, format: 'esm', platform: 'browser', logLevel: 'error' });
const body = Buffer.from(bundle.outputFiles[0].contents);
let cdp: CdpConnection | null = null;
let page: Awaited<ReturnType<typeof createPage>> | undefined;
let originalTarget: string | undefined, backgroundTarget: string | undefined;
const shaper = network ? await createPoolShaper() : null;
const server = createServer((request, response) => {
const url = new URL(request.url!, 'http://127.0.0.1');
if (url.pathname === '/visibility' && cdp && page) {
void (async () => {
originalTarget ??= (await cdp!.call<{ targetInfo: { targetId: string } }>(
'Target.getTargetInfo', {}, page!.sessionId, Date.now() + 3000)).targetInfo.targetId;
backgroundTarget ??= (await cdp!.call<{ targetId: string }>(
'Target.createTarget', { url: 'about:blank', background: true }, undefined, Date.now() + 3000)).targetId;
await cdp!.call('Target.activateTarget', { targetId: url.searchParams.get('hidden') === '1' ? backgroundTarget : originalTarget },
undefined, Date.now() + 3000);
response.end('{}');
})().catch(() => { response.statusCode = 500; response.end('{}'); });
return;
}
if (url.pathname === '/process' && cdp) {
void cdp.call<{ processInfo: Array<{ cpuTime: number }> }>('SystemInfo.getProcessInfo', {}, undefined, Date.now() + 3000)
.then(info => response.end(JSON.stringify({ cpuSeconds: info.processInfo.reduce((sum, p) => sum + p.cpuTime, 0) })),
() => response.end('{}'));
return;
}
if (url.pathname === '/shaper' && shaper) {
if (url.searchParams.has('left')) shaper.setTargets(Number(url.searchParams.get('left')), Number(url.searchParams.get('right')));
if (url.searchParams.has('rate')) shaper.setRate(Number(url.searchParams.get('rate')));
response.setHeader('Content-Type', 'application/json');
response.end(JSON.stringify({ ports: shaper.ports, stats: shaper.stats() }));
return;
}
response.setHeader('Content-Type', url.pathname === '/probe.js' ? 'text/javascript' : 'text/html');
response.end(url.pathname === '/probe.js' ? body :
'<!doctype html><meta charset=utf-8><title>Browser local pool</title><script type=module src=/probe.js></script>');
});
await new Promise<void>(r => server.listen(0, '127.0.0.1', r));
const port = (server.address() as { port: number }).port, debugPort = await reservePort();
const profile = await mkdtemp(join(tmpdir(), 'screener-client-media-'));
const chrome = launchChrome(process.env.CHROME_PATH || 'C:/Program Files/Google/Chrome/Application/chrome.exe', debugPort, profile,
['--headless=new', '--no-first-run', '--autoplay-policy=no-user-gesture-required',
...(background ? [] : ['--disable-background-timer-throttling', '--disable-renderer-backgrounding']),
...(nativeSource ? ['--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream'] : [])]);
chrome.stdout.resume(); chrome.stderr.resume();
let result: Record<string, unknown> = { mode, codec, product: true, network, automatic, high, av, single, late,
background, nativeSource, lifecycle, relay, shapedRate: rate, preference };
try {
const version = await waitForVersion(debugPort, chrome);
cdp = await CdpConnection.connect(version.webSocketDebuggerUrl, Date.now() + 5000);
page = await createPage(cdp, 'about:blank');
const query = new URLSearchParams({ mode, codec, rate: String(rate), preference });
if (process.argv.includes('--debug')) query.set('debug', '1');
for (const [key, value] of Object.entries({ network, automatic, high, av, single, late, background, nativeSource, lifecycle, relay }))
query.set(key, value ? '1' : '0');
await cdp.call('Page.navigate', { url: 'http://127.0.0.1:' + port + '/?' + query }, page.sessionId, Date.now() + 5000);
await waitForSample(() => evaluate<boolean>(cdp!, page!, 'window.probeDone===true', Date.now() + 2000),
Boolean, automatic || lifecycle ? 110000 : network || background ? 60000 : 25000);
result = await evaluate<Record<string, unknown>>(cdp, page, 'window.probeResult', Date.now() + 3000);
} catch (error) {
result.error = String(error);
if (cdp && page) result.partial = await evaluate(cdp, page, 'window.probeResult || window.probeProgress?.()', Date.now() + 3000).catch(() => null);
} finally {
const clean = await cleanupRun({ cdp, native: null, chrome,
server: { close: () => new Promise<void>((resolve, reject) => server.close(e => e ? reject(e) : resolve())) },
profile, ports: [port, debugPort] });
if (shaper) { result.shaper = shaper.stats(); await shaper.close(); }
result.probeSha256 = createHash('sha256').update(body).digest('hex');
result.cleanup = clean;
await mkdir(join(root, 'build/browser-local-pool'), { recursive: true });
const name = [mode, codec.toLowerCase(), network && 'network', automatic && 'auto', high && '1080',
single && 'single', late && 'late', background && 'background', lifecycle && 'lifecycle', relay && 'relay', Date.now()].filter(Boolean).join('-');
const output = join(root, 'build/browser-local-pool', name + '.json');
await writeFile(output, JSON.stringify(result, null, 2) + '\n');
console.log(JSON.stringify({ mode, codec, network, error: result.error ?? null, output, cleanup: clean }));
if (result.error || (result.errors as string[] | undefined)?.length || Object.values(clean).some(v => !v)) process.exitCode = 1;
}
+63
View File
@@ -0,0 +1,63 @@
import { readFile } from 'node:fs/promises';
for (const filename of process.argv.slice(2)) {
const result = JSON.parse(await readFile(filename, 'utf8'));
const completed = Array.isArray(result.snapshots) && !result.error && !result.errors?.length &&
result.cleanup && Object.values(result.cleanup).every(Boolean);
const configuration = { file: filename, probeSha256: result.probeSha256, mode: result.mode, codec: result.codec,
product: result.product, high: result.high, single: result.single, late: result.late, network: result.network,
automatic: result.automatic, av: result.av, background: result.background, nativeSource: result.nativeSource,
lifecycle: result.lifecycle, relay: result.relay, shapedRate: result.shapedRate, profile: result.profile,
completed: !!completed, error: result.error, errors: result.errors, cleanup: result.cleanup };
if (!completed) process.exitCode = 1;
if (!result.snapshots) {
console.log(JSON.stringify({ ...configuration, progress: result.progress, partial: result.partial }));
continue;
}
const phases = [];
for (let i = 1; i < result.snapshots.length; i++) {
const prev = result.snapshots[i - 1], next = result.snapshots[i], seconds = (next.at - prev.at) / 1000;
const row = { phase: next.phase, seconds, browserCpuSeconds: next.process && prev.process ? next.process.cpuSeconds - prev.process.cpuSeconds : null, edges: {}, viewers: {} };
for (const role of Object.keys(next.stats)) {
const before = prev.stats[role], after = next.stats[role];
const outbound = s => s?.send.find(r => r.type === 'outbound-rtp' && r.kind === 'video') || {};
const inbound = s => s?.receive.find(r => r.type === 'inbound-rtp' && r.kind === 'video') || {};
const a = outbound(before), b = outbound(after), c = inbound(before), d = inbound(after);
row.edges[role] = { encoded: (b.framesEncoded || 0) - (a.framesEncoded || 0), encodeMs: 1000 * ((b.totalEncodeTime || 0) - (a.totalEncodeTime || 0)),
decoded: (d.framesDecoded || 0) - (c.framesDecoded || 0), decodeMs: 1000 * ((d.totalDecodeTime || 0) - (c.totalDecodeTime || 0)),
sentFrames: (b.framesSent || 0) - (a.framesSent || 0), kbps: 8 * ((b.bytesSent || 0) - (a.bytesSent || 0)) / seconds / 1000,
output: [b.frameWidth, b.frameHeight], receive: [d.frameWidth, d.frameHeight], reason: b.qualityLimitationReason, target: b.targetBitrate,
retired: after.retired ?? false, details: after.details,
meanQp: b.qpSum !== undefined && b.framesEncoded > (a.framesEncoded || 0) ? (b.qpSum - (a.qpSum || 0)) / (b.framesEncoded - (a.framesEncoded || 0)) : null,
senderReports: after.receive.filter(r => r.type === 'remote-outbound-rtp').reduce((s, r) => s + (r.reportsSent || 0), 0) - (before?.receive || []).filter(r => r.type === 'remote-outbound-rtp').reduce((s, r) => s + (r.reportsSent || 0), 0), pli: (d.pliCount || 0) - (c.pliCount || 0) };
}
for (const monitor of result.monitors) {
const samples = monitor.samples.filter(s => s.phase === next.phase), ages = samples.filter(s => s.age !== null).map(s => s.age).sort((a, b) => a - b);
let backwards = 0, duplicates = 0, maxGapMs = samples[0] ? samples[0].at - prev.at : seconds * 1000;
for (let j = 1; j < samples.length; j++) {
maxGapMs = Math.max(maxGapMs, samples[j].at - samples[j - 1].at);
if (samples[j].id < samples[j - 1].id)
backwards++;
if (samples[j].id === samples[j - 1].id)
duplicates++;
}
if (samples.length) maxGapMs = Math.max(maxGapMs, next.at - samples.at(-1).at);
row.viewers[monitor.role] = { rendered: samples.length, fresh: result.nativeSource ? null : samples.filter(s => s.age !== null && s.age < 1000).length, backwards, duplicates, maxGapMs,
ageP50: ages[Math.floor(ages.length * .5)] ?? null, ageP95: ages[Math.floor(ages.length * .95)] ?? null };
if (result.av) {
const sound = result.audioMonitors.find(m => m.role === monitor.role);
const offsets = [];
let missed = 0;
for (const pulse of result.sourcePulses.filter(p => p.at >= prev.at && p.at < next.at - 500)) {
const video = samples.find(s => s.id >= pulse.id && s.id < pulse.id + 6);
const audio = sound?.pulses.find(p => p.at >= pulse.at && p.at < pulse.at + 1500);
if (video && audio) offsets.push(video.at - audio.at);
else missed++;
}
row.viewers[monitor.role].av = { attached: sound?.attached ?? false, videoMinusAudioMs: offsets, missed };
}
}
phases.push(row);
}
console.log(JSON.stringify({ ...configuration, phases, controls: result.controls, visibility: result.visibility,
shaper: result.shaper ?? null }));
}
+66
View File
@@ -0,0 +1,66 @@
import { createSocket } from 'node:dgram';
// Test-only UDP bridge. Both ICE descriptions point at it, so the recorded
// packet counters prove the tested media path crossed the shaping boundary.
export async function createPoolShaper() {
const left = createSocket('udp4'), right = createSocket('udp4');
await Promise.all([left, right].map(s => new Promise(r => s.bind(0, '127.0.0.1', r))));
let targetLeft = 0, targetRight = 0, rate = 0, next = 0, closed = false, timer = null;
const queue = [];
const counters = { forwardedBytes: 0, droppedBytes: 0, packets: 0, reverseBytes: 0, maxQueueBytes: 0 };
let queuedBytes = 0;
function flush() {
timer = null;
if (closed)
return;
const now = performance.now();
while (queue.length && queue[0].at <= now) {
const packet = queue.shift();
queuedBytes -= packet.data.length;
right.send(packet.data, targetRight, '127.0.0.1');
counters.forwardedBytes += packet.data.length;
counters.packets++;
}
if (queue.length)
timer = setTimeout(flush, Math.max(1, queue[0].at - performance.now()));
}
left.on('message', data => {
if (!targetRight || closed)
return;
if (!rate) {
right.send(data, targetRight, '127.0.0.1');
counters.forwardedBytes += data.length;
counters.packets++;
return;
}
const now = performance.now();
if (next - now > 250) {
counters.droppedBytes += data.length;
return;
}
next = Math.max(now, next) + data.length * 8 * 1000 / rate;
queue.push({ at: next, data });
queuedBytes += data.length;
counters.maxQueueBytes = Math.max(counters.maxQueueBytes, queuedBytes);
if (!timer)
flush();
});
right.on('message', data => { if (targetLeft && !closed) {
left.send(data, targetLeft, '127.0.0.1');
counters.reverseBytes += data.length;
} });
return {
ports: { left: left.address().port, right: right.address().port },
setTargets(a, b) { targetLeft = a; targetRight = b; },
setRate(value) { rate = value; if (!rate) {
next = 0;
for (const item of queue)
item.at = 0;
if (timer)
clearTimeout(timer);
flush();
} },
stats() { return { ...counters, queuedBytes, rate }; },
async close() { closed = true; if (timer)
clearTimeout(timer); queue.length = 0; await Promise.all([left, right].map(s => new Promise(r => s.close(r)))); }
};
}
+1 -1
View File
@@ -766,7 +766,7 @@ async function main(): Promise<void> {
stage = "host-page";
const host = await createPage(
cdp,
"http://localhost:" + appPort + "/#" + hostBootstrap,
"http://localhost:" + appPort + "/?debug=1#" + hostBootstrap,
CODEC_PROBE,
true,
);