Files
Piik/scripts/browser-local-pool-page.js
T
AAA c9a6642ac1 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
2026-09-09 20:59:44 +08:00

376 lines
22 KiB
JavaScript

// 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;
}