Files
Piik/scripts/embedded-sfu-gate.ts
T
AAA 1332473513 feat: rename product and runtime to Piik
Rename the active product, Go module, binaries, configuration, protocols and documentation to Piik while retaining the TV mascot and existing media/room behavior.

Use one current private contract without old-name aliases. Preserve SQLite authority and copy compatible local settings once; other settings start fresh. Git-message cleanup and the infrastructure name cutover are coordinated with this phase, with original history and deployment data archived externally.

Validated locally with Web and Go checks, Windows capture compilation, matched Client/Server builds, packaged Client control handshake and history-rewrite integrity checks. Skip duplicate hosted CI for this owner-authorized local release.

[skip ci]
2026-09-10 11:10:12 +08:00

304 lines
19 KiB
TypeScript

import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";
import { createSocket } from "node:dgram";
import { mkdir, mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { createServer as createViteServer, type ViteDevServer } from "vite";
import type { CreateRoomResponse } from "../src/shared/protocol";
import {
CdpConnection, cleanupRun, createPage, evaluate, fetchJsonBefore, launchChrome,
reservePort, waitForSample, waitForVersion, withDeadline, type PageHandle,
} from "./browser-gate-harness";
import type { snapshot } from "./embedded-sfu-page";
import { readClientEndpoint } from "./client-gate-endpoint";
import { SOURCE_TITLE, sourceServer, startSourceBrowser, waitForCaptureWindow } from "./client-native-host-gate";
const ROOT = resolve(import.meta.dirname, "..");
const BUILD_ROOT = join(ROOT, "build", "embedded-media");
type Snapshot = ReturnType<typeof snapshot>;
const AUDIO_PROBE = `(() => {
const peers = [], BrowserPeer = RTCPeerConnection;
window.RTCPeerConnection = class extends BrowserPeer {
constructor(...args) { super(...args); peers.push(this); }
};
window.__piikGateAudioEnergy = async () => {
const trackId = document.querySelector('video')?.srcObject?.getAudioTracks()[0]?.id;
const diagnostic = window.__piikGateAudioStats = { hasAudioTrack: !!trackId, peerCount: peers.length, inbound: [] };
if (!trackId) return 0;
for (const peer of peers) {
if (peer.connectionState !== 'connected') continue;
const report = await peer.getStats();
for (const row of report.values()) {
if (row.type === 'inbound-rtp' && row.kind === 'audio') diagnostic.inbound.push({
trackMatches: row.trackIdentifier === trackId, energy: row.totalAudioEnergy ?? null,
sampleDuration: row.totalSamplesDuration ?? null, receivedSamples: row.totalSamplesReceived ?? null,
});
if (row.type === 'inbound-rtp' && row.kind === 'audio' && row.trackIdentifier === trackId &&
Number.isFinite(row.totalAudioEnergy) && row.totalAudioEnergy > 0) return row.totalAudioEnergy;
}
}
return 0;
};
})()`;
async function udpPort(requested = 0): Promise<number> {
const socket = createSocket("udp4");
return await new Promise((resolvePort, reject) => {
socket.once("error", reject);
socket.bind(requested, "127.0.0.1", () => {
const port = socket.address().port;
socket.close(() => resolvePort(port));
});
});
}
async function main(): Promise<void> {
if (process.env.PIIK_EMBEDDED_SFU_GATE !== "true") {
throw new Error("PIIK_EMBEDDED_SFU_GATE=true is required; this is an explicit local Browser media gate");
}
const chromePath = process.env.CHROME_PATH?.trim();
if (!chromePath) throw new Error("CHROME_PATH is required");
const nativeArm = process.env.PIIK_EMBEDDED_SFU_NATIVE === "true";
const twoRooms = process.env.PIIK_EMBEDDED_SFU_TWO_ROOMS === "true";
if (twoRooms && !nativeArm) throw new Error("Two-room arm requires Native publication");
const codec = process.env.PIIK_EMBEDDED_SFU_CODEC?.trim() || "vp8";
if (codec !== "vp8" && codec !== "h264") throw new Error("SFU gate codec must be vp8 or h264");
const captureBinary = process.env.PIIK_CAPTURE_PROCESS?.trim();
if (nativeArm && (process.platform !== "win32" || !captureBinary)) {
throw new Error("Native arm requires Windows and PIIK_CAPTURE_PROCESS");
}
await mkdir(BUILD_ROOT, { recursive: true });
const binary = join(BUILD_ROOT, `piik-server${process.platform === "win32" ? ".exe" : ""}`);
const clientBinary = join(BUILD_ROOT, "piik-client.exe");
for (const command of nativeArm ? ["piik-server", "piik-client"] : ["piik-server"]) {
const build = spawnSync(process.env.PIIK_GO?.trim() || "go", [
"build", "-p", "1", "-trimpath", "-o", command === "piik-server" ? binary : clientBinary, `./cmd/${command}`,
], { cwd: ROOT, env: { ...process.env, GOMAXPROCS: "2" }, encoding: "utf8", windowsHide: true, timeout: 120_000 });
if (build.error || build.status !== 0) throw build.error ?? new Error(build.stderr || `${command} build failed`);
}
const [serverPort, vitePort, debugPort, mediaPort] = await Promise.all([
reservePort(), reservePort(), reservePort(), udpPort(),
]);
const origin = `http://127.0.0.1:${vitePort}`;
const backend = `http://127.0.0.1:${serverPort}`;
const profile = await mkdtemp(join(tmpdir(), "piik-client-media-"));
const sourceProfile = nativeArm ? await mkdtemp(join(tmpdir(), "piik-client-media-")) : null;
const [sourcePort, sourceDebugPort] = nativeArm ? [await reservePort(), await reservePort()] : [0, 0];
const result = { passed: false, arm: `${nativeArm ? "native" : "browser"}-${codec}-simulcast${twoRooms ? "-two-rooms" : ""}`, stage: "start",
startedAt: new Date().toISOString(), finishedAt: "", processes: [] as Array<{ role: string; pid: number | null }>,
high: null as { frames: number; width: number; height: number } | null,
low: null as { frames: number; width: number; height: number } | null,
audioEnergy: { high: 0, low: 0 },
audioDiagnostics: null as unknown,
secondRoom: twoRooms ? { connected: false, framesAfterFirstStopped: 0, audioBefore: 0, audioAfter: 0, stopped: false } : null,
host: null as Snapshot | null, viewer: null as Snapshot | null,
publicationRetired: false, nativeShareStopped: !nativeArm, udpReleased: false, cleanup: false, error: null as string | null };
let server: ChildProcessWithoutNullStreams | null = null;
let chrome: ChildProcessWithoutNullStreams | null = null;
let vite: ViteDevServer | null = null;
let cdp: CdpConnection | null = null;
let host: PageHandle | null = null;
let viewer: PageHandle | null = null;
let secondHost: PageHandle | null = null;
let secondViewer: PageHandle | null = null;
let serverError = "";
let client: ChildProcessWithoutNullStreams | null = null;
let clientPort = 0;
let sourceChrome: ChildProcessWithoutNullStreams | null = null;
let sourceCdp: CdpConnection | null = null;
let sourceHTTP: Awaited<ReturnType<typeof sourceServer>> | null = null;
const call = async <T>(page: PageHandle, expression: string, timeout = 25_000): Promise<T> => {
const value = await evaluate<{ value?: T; error?: string }>(cdp!, page,
`import('/scripts/embedded-sfu-page.ts').then(async (gate) => { try { return { value: await (${expression}) }; } catch (error) { return { error: String(error?.message || error) }; } })`,
Date.now() + timeout);
if (value.error) throw new Error(value.error);
return value.value as T;
};
const until = async (page: PageHandle, accept: (value: Snapshot) => boolean) => {
let last: Snapshot | null = null;
try {
await waitForSample(() => call<Snapshot>(page, "gate.snapshot()", 3_000),
(value) => { last = value; return !!value.error || accept(value); }, 25_000);
} catch { throw new Error(`Browser stage timed out: ${JSON.stringify(last)}`); }
if (last!.error) throw new Error(last!.error);
return last!;
};
const processStarted = (role: string, child: ChildProcessWithoutNullStreams) => {
const entry = { role, pid: child.pid ?? null };
result.processes.push(entry);
process.stderr.write(`${JSON.stringify({ ...entry, startedAt: new Date().toISOString() })}\n`);
};
try {
server = spawn(binary, [], { cwd: ROOT, stdio: "pipe", windowsHide: true,
env: { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot,
PIIK_ENV: "development", LISTEN_HOST: "127.0.0.1", PORT: String(serverPort),
PUBLIC_BASE_URL: origin, ALLOWED_ORIGINS: origin,
SFU_LISTEN_HOST: "127.0.0.1", SFU_UDP_PORT: String(mediaPort), SFU_PUBLIC_IP: "127.0.0.1" },
});
processStarted("server", server);
server.stdout.resume();
server.stderr.on("data", (data: Buffer) => { serverError = (serverError + data.toString()).slice(-2000); });
await waitForSample((deadline) => fetchJsonBefore<{ status: string }>(`${backend}/healthz`, deadline),
() => true, 15_000);
vite = await createViteServer({ root: ROOT, configFile: false, appType: "custom", logLevel: "silent",
server: { host: "127.0.0.1", port: vitePort, strictPort: true, watch: null,
proxy: { "/api": { target: backend }, "/signal": { target: backend, ws: true } } },
plugins: [{ name: "embedded-sfu-gate", configureServer(instance) {
instance.middlewares.use((request, response, next) => {
if (request.url !== "/embedded-sfu-gate") return next();
response.setHeader("Content-Type", "text/html; charset=utf-8");
response.end("<!doctype html><html><head><title>Embedded SFU acceptance</title></head><body></body></html>");
});
} }],
});
await vite.listen();
if (nativeArm) {
result.stage = "native-client-start";
client = spawn(clientBinary, ["--site", origin, "--capture-process", captureBinary!,
"--config", join(profile, "client.json")], { cwd: ROOT, stdio: "pipe", windowsHide: true,
env: { ...process.env, GOMAXPROCS: "2", PIIK_CLIENT_GATE_NO_BROWSER: "true" } });
processStarted("client", client);
client.stderr.resume();
clientPort = (await readClientEndpoint(client, { timeoutMs: 15_000, ignoreNonEndpointLines: true })).port;
client.stdout.resume();
result.stage = "native-source-window";
sourceHTTP = await sourceServer(sourcePort);
({ child: sourceChrome, cdp: sourceCdp } = await startSourceBrowser(chromePath, sourceProfile!, sourceDebugPort, sourcePort));
processStarted("source-browser", sourceChrome);
await waitForCaptureWindow(captureBinary!);
const targets = await sourceCdp.call<{ targetInfos: Array<{ targetId: string; url: string }> }>(
"Target.getTargets", {}, undefined, Date.now() + 5_000);
const target = targets.targetInfos.find((entry) => entry.url === `http://127.0.0.1:${sourcePort}/`);
if (!target) throw new Error("Gate source page is unavailable");
const attached = await sourceCdp.call<{ sessionId: string }>("Target.attachToTarget",
{ targetId: target.targetId, flatten: true }, undefined, Date.now() + 5_000);
const audioState = await evaluate<string>(sourceCdp, attached, `(async () => {
const audio = new AudioContext(), tone = audio.createOscillator(), gain = audio.createGain();
gain.gain.value = 0.01; tone.connect(gain).connect(audio.destination); tone.start();
window.__gateAudio = audio; await audio.resume(); return audio.state;
})()`, Date.now() + 5_000);
if (audioState !== "running") throw new Error("Native source tone did not start");
}
chrome = launchChrome(chromePath, debugPort, profile, [
"--headless=new", "--disable-gpu", "--no-first-run", "--no-default-browser-check",
"--disable-extensions", "--disable-background-networking", "--autoplay-policy=no-user-gesture-required",
"--disable-background-timer-throttling", "--disable-backgrounding-occluded-windows",
"--disable-renderer-backgrounding", "--disable-features=WebRtcHideLocalIpsWithMdns",
]);
processStarted("decoder-browser", chrome);
chrome.stdout.resume();
chrome.stderr.resume();
const version = await waitForVersion(debugPort, chrome);
cdp = await CdpConnection.connect(version.webSocketDebuggerUrl, Date.now() + 10_000);
if (nativeArm) await cdp.call("Browser.setPermission", {
permission: { name: "local-network-access" }, setting: "granted", origin,
}, undefined, Date.now() + 5_000);
result.stage = "gate-page";
const gatePage = await fetch(`${origin}/embedded-sfu-gate`, { signal: AbortSignal.timeout(5_000) });
if (!gatePage.ok || !(await gatePage.text()).includes("Embedded SFU acceptance")) {
throw new Error("Local SFU gate page is unavailable");
}
host = await createPage(cdp, `${origin}/embedded-sfu-gate`);
viewer = await createPage(cdp, `${origin}/embedded-sfu-gate`, AUDIO_PROBE);
result.stage = "authenticate";
const room = await call<CreateRoomResponse>(host, nativeArm
? `gate.startHost(${JSON.stringify({ title: SOURCE_TITLE, port: clientPort })}, ${JSON.stringify(codec)})`
: `gate.startHost(undefined, ${JSON.stringify(codec)})`);
await until(host, (value) => value.authenticated);
await call(viewer, `gate.startViewer(${JSON.stringify(room)})`);
result.stage = "simulcast-first-frame";
result.viewer = await until(viewer, (value) => value.committed && value.audioKbps > 0);
if (twoRooms) {
result.stage = "second-native-room";
secondHost = await createPage(cdp, `${origin}/embedded-sfu-gate`);
secondViewer = await createPage(cdp, `${origin}/embedded-sfu-gate`, AUDIO_PROBE);
const secondRoom = await call<CreateRoomResponse>(secondHost,
`gate.startHost(${JSON.stringify({ title: SOURCE_TITLE, port: clientPort })}, ${JSON.stringify(codec)})`);
if (secondRoom.roomId === room.roomId) throw new Error("Native sessions did not create independent rooms");
await until(secondHost, (value) => value.authenticated);
await call(secondViewer, `gate.startViewer(${JSON.stringify(secondRoom)})`);
await until(secondViewer, (value) => value.committed && value.audioKbps > 0);
await call(secondViewer, "gate.waitForFrames(1280, 720)");
await waitForSample(() => call<number>(secondViewer!, "gate.audioEnergy()", 3_000),
(energy) => { result.secondRoom!.audioBefore = energy; return energy > 0; }, 5_000);
const second = await call<Snapshot>(secondHost, "gate.snapshot()");
result.secondRoom!.connected = second.published && second.simulcast && second.nativeShareStarted;
}
result.stage = "simulcast-first-frame";
await cdp.call("Page.bringToFront", {}, viewer.sessionId, Date.now() + 3_000);
result.host = await call(host, "gate.snapshot()");
result.high = await call(viewer, nativeArm ? "gate.waitForFrames(1280, 720, 300)" : "gate.waitForFrames(1280, 720)", nativeArm ? 32_000 : 25_000);
result.stage = "decoded-audio-energy";
await waitForSample(() => call<number>(viewer!, "gate.audioEnergy()", 3_000),
(energy) => { result.audioEnergy.high = energy; return energy > 0; }, 5_000);
result.host = await call(host, "gate.snapshot()");
result.stage = "live-profile";
if (!await call<boolean>(host, "gate.lowerProfile()")) {
result.host = await call(host, "gate.snapshot()");
throw new Error("Live profile update failed");
}
result.low = await call(viewer, "gate.waitForFrames(854, 480)");
await waitForSample(() => call<number>(viewer!, "gate.audioEnergy()", 3_000),
(energy) => { result.audioEnergy.low = energy; return energy > result.audioEnergy.high; }, 5_000);
result.stage = "publication-retirement";
await call(host, "gate.stopSharing()");
await until(host, (value) => value.publicationRetired);
await until(viewer, (value) => value.sharingStopped);
result.publicationRetired = true;
if (secondHost && secondViewer) {
result.stage = "second-room-isolation";
if (!await call<boolean>(host, "gate.stop()")) throw new Error("First Native session did not stop");
if (!(await call<Snapshot>(host, "gate.snapshot()")).nativeShareStopped) throw new Error("First Native capture did not stop");
result.secondRoom!.audioBefore = await call<number>(secondViewer, "gate.audioEnergy()");
await cdp.call("Page.bringToFront", {}, secondViewer.sessionId, Date.now() + 3_000);
const continued = await call<{ frames: number }>(secondViewer, "gate.waitForFrames(1280, 720, 30)");
result.secondRoom!.framesAfterFirstStopped = continued.frames;
await waitForSample(() => call<number>(secondViewer!, "gate.audioEnergy()", 3_000),
(energy) => { result.secondRoom!.audioAfter = energy; return energy > result.secondRoom!.audioBefore; }, 5_000);
if ((await call<Snapshot>(secondHost, "gate.snapshot()")).error) throw new Error("First session retirement affected second Native Host");
await call(secondHost, "gate.stopSharing()");
await until(secondHost, (value) => value.publicationRetired);
await until(secondViewer, (value) => value.sharingStopped);
if (!await call<boolean>(secondHost, "gate.stop()")) throw new Error("Second Native session did not stop");
result.secondRoom!.stopped = (await call<Snapshot>(secondHost, "gate.snapshot()")).nativeShareStopped;
}
} catch (error) {
result.error = error instanceof Error ? error.message : String(error);
if (viewer && cdp && result.stage === "decoded-audio-energy") result.audioDiagnostics = await evaluate(
cdp, viewer, "window.__piikGateAudioStats ?? null", Date.now() + 3_000).catch(() => null);
if (server?.exitCode !== null && serverError) result.error += `; server: ${serverError}`;
} finally {
const pagesStopped = await Promise.all([host, viewer, secondHost, secondViewer].map((page) =>
page && cdp ? call<boolean>(page, "gate.stop()", 5_000).catch(() => false) : true));
if (host && cdp && nativeArm) {
result.nativeShareStopped = await call<Snapshot>(host, "gate.snapshot()", 3_000)
.then((value) => value.nativeShareStopped, () => false);
}
const viteClosed = vite
? await withDeadline(() => vite!.close(), Date.now() + 5_000).then(() => true, () => false)
: true;
const cleanup = await cleanupRun({ cdp, native: client, chrome, server, profile,
ports: [serverPort, vitePort, debugPort, ...(clientPort ? [clientPort] : [])] });
const sourceCleanup = sourceProfile ? await cleanupRun({ cdp: sourceCdp, native: null,
chrome: sourceChrome, server: sourceHTTP, profile: sourceProfile, ports: [sourcePort, sourceDebugPort] }) : null;
result.udpReleased = await withDeadline(() => udpPort(mediaPort), Date.now() + 3_000)
.then(() => true, () => false);
result.cleanup = viteClosed && result.udpReleased && pagesStopped.every(Boolean) &&
Object.values(cleanup).every(Boolean) && (!sourceCleanup || Object.values(sourceCleanup).every(Boolean));
}
result.finishedAt = new Date().toISOString();
result.passed = !result.error && result.host?.published === true && result.host.simulcast &&
result.viewer?.decoded === true && result.viewer.committed && result.viewer.peerFailures > 0 &&
result.viewer.audioKbps > 0 && result.audioEnergy.high > 0 && result.audioEnergy.low > result.audioEnergy.high &&
result.high !== null && result.low !== null &&
result.publicationRetired && result.cleanup && result.nativeShareStopped &&
(!result.secondRoom || result.secondRoom.connected && result.secondRoom.framesAfterFirstStopped >= 30 &&
result.secondRoom.audioBefore > 0 && result.secondRoom.audioAfter > result.secondRoom.audioBefore && result.secondRoom.stopped) &&
(!nativeArm || result.host.nativeShareStarted && result.host.nativeFramesPerSecond > 0 && result.host.nativeBitrateKbps > 0 && result.host.nativeEncodingCount === 2);
process.stdout.write(`${JSON.stringify(result)}\n`);
if (!result.passed) process.exitCode = 1;
}
await main();