feat(native): add window-scoped process audio (#93)

Add opt-in Windows 11 target-process audio capture, one AudioEncoder Opus stream, and one shared Pion audio track beside native video.
This commit is contained in:
AAA authored and GitHub committed 2026-08-21 02:44:46 +08:00
1 parent ce3cbe2efd
commit 6cd99545e6
25 files changed
+1516 -109

No files matched your search

+33 -2
View File
@@ -119,6 +119,9 @@ interface ViewerSnapshot {
videoHeight: number;
videoCurrentTime: number;
renderedFrames: number;
audioTrackCount: number;
audioPacketsReceived: number;
audioBytesReceived: number;
}
class CdpConnection {
@@ -274,6 +277,7 @@ async function main(): Promise<void> {
const chromePath = requiredEnvironmentPath("SCREENER_NATIVE_GATE_CHROME");
const goPath = process.env.SCREENER_NATIVE_GATE_GO?.trim() || "go";
const gateCodec = process.env.SCREENER_NATIVE_GATE_CODEC?.trim() === "h264" ? "h264" : "vp8";
const audioTargetTitle = process.env.SCREENER_NATIVE_GATE_AUDIO_TARGET_TITLE?.trim() || "";
const accessKey = randomBytes(9).toString("base64url");
const report: GateReport = {
schemaVersion: 1,
@@ -283,7 +287,9 @@ async function main(): Promise<void> {
limitations: [
"The source is a real animated Chrome tab captured through getDisplayMedia, not a game workload.",
"This run proves one WebCodecs object, not one physical or hardware encoder.",
"The loopback run does not prove public STUN, restrictive networks, TURN, audio, packaging, or endurance.",
audioTargetTitle
? "The loopback run does not prove public STUN, restrictive networks, TURN, packaging, or endurance."
: "The loopback run does not prove public STUN, restrictive networks, TURN, audio, packaging, or endurance.",
],
};
let server: ScreenerServer | null = null;
@@ -366,10 +372,22 @@ async function main(): Promise<void> {
await createPage(cdp, animatedSourceUrl());
const activeSenderPage = await createPage(cdp, launchUrl, senderProbe());
senderPage = activeSenderPage;
let audioTargetId = "";
if (audioTargetTitle) {
await waitForSample(
(deadline) => evaluate<string>(cdp!, activeSenderPage,
`([...document.querySelector('#audio-target').options].find((option) => option.text.includes(${JSON.stringify(audioTargetTitle)}))?.value || '')`, deadline),
Boolean,
5_000,
);
audioTargetId = await evaluate<string>(cdp, activeSenderPage,
`([...document.querySelector('#audio-target').options].find((option) => option.text.includes(${JSON.stringify(audioTargetTitle)}))?.value || '')`, Date.now() + 2_000);
}
await evaluate<void>(cdp, activeSenderPage, `(() => {
document.querySelector('#server-url').value = ${JSON.stringify(baseUrl)};
document.querySelector('#password').value = ${JSON.stringify(accessKey)};
document.querySelector('#codec').value = ${JSON.stringify(gateCodec)};
document.querySelector('#audio-target').value = ${JSON.stringify(audioTargetId)};
document.querySelector('#start').click();
})()`, Date.now() + 5_000);
await waitForSenderStart(
@@ -464,6 +482,10 @@ async function main(): Promise<void> {
sameBridge: retainsFirstBridgeSend(sender),
oneEncoderObject: sender.encoderInstances === 1,
criticalErrors: sender.fatalEvents === 0 && sender.encoderErrors === 0,
...(audioTargetTitle ? {
audioTrack: viewer.audioTrackCount === 1,
audioInbound: viewer.audioPacketsReceived > beforeViewer.audioPacketsReceived,
} : {}),
};
report.stages["viewer-media"] = {
...checks,
@@ -471,6 +493,7 @@ async function main(): Promise<void> {
byteDelta: viewer.bytesReceived - beforeViewer.bytesReceived,
decodedDelta: viewer.framesDecoded - beforeViewer.framesDecoded,
renderedDelta: viewer.renderedFrames - beforeViewer.renderedFrames,
audioPacketDelta: viewer.audioPacketsReceived - beforeViewer.audioPacketsReceived,
pionPacketDelta: (sender.peers[0]?.packetsSent ?? 0) - (beforeSender.peers[0]?.packetsSent ?? 0),
pcOrdinal: viewer.identity.pcOrdinal,
pionSlot: sender.peers[0]?.slot ?? -1,
@@ -648,6 +671,7 @@ function viewerProbe(): string {
consistent: true, pendingOffer: null,
pcs: [], boundPcOrdinal: 0, boundConnectionOrdinal: 0, boundSocketOrdinal: 0,
boundAuthGeneration: 0, trackCount: 0, trackOrdinal: 0, trackPcOrdinal: 0,
audioTrackCount: 0,
renderedFrames: 0, observedVideo: null };
const connectionOrdinal = (value) => {
let index = state.connectionIds.indexOf(value);
@@ -733,6 +757,7 @@ function viewerProbe(): string {
state.pcs.push(this);
this.__gatePcOrdinal = state.pcs.length;
this.addEventListener('track', (event) => {
if (event.track?.kind === 'audio') { state.audioTrackCount += 1; return; }
if (event.track?.kind !== 'video') return;
state.trackCount += 1; state.trackOrdinal = state.trackCount;
state.trackPcOrdinal = this.__gatePcOrdinal;
@@ -762,6 +787,7 @@ function viewerProbe(): string {
}
const pc = state.pcs[state.boundPcOrdinal - 1];
let packetsReceived = 0, bytesReceived = 0, framesDecoded = 0;
let audioPacketsReceived = 0, audioBytesReceived = 0;
if (pc) {
const stats = await pc.getStats();
stats.forEach((entry) => {
@@ -770,6 +796,10 @@ function viewerProbe(): string {
bytesReceived += finite(entry.bytesReceived);
framesDecoded += finite(entry.framesDecoded);
}
if (entry.type === 'inbound-rtp' && entry.kind === 'audio' && entry.isRemote !== true) {
audioPacketsReceived += finite(entry.packetsReceived);
audioBytesReceived += finite(entry.bytesReceived);
}
});
}
const activeOrdinal = state.activeConnectionId === null ? 0 :
@@ -787,7 +817,8 @@ function viewerProbe(): string {
iceConnectionState: pc?.iceConnectionState || 'none', packetsReceived, bytesReceived, framesDecoded,
videoReadyState: finite(video?.readyState), videoWidth: finite(video?.videoWidth),
videoHeight: finite(video?.videoHeight), videoCurrentTime: finite(video?.currentTime),
renderedFrames: state.renderedFrames };
renderedFrames: state.renderedFrames, audioTrackCount: state.audioTrackCount,
audioPacketsReceived, audioBytesReceived };
}
const finite = (value) => Number.isFinite(value) ? value : 0;
Object.defineProperty(globalThis, '__NATIVE_VIEWER_GATE__', {