Files
openscreen/scripts/test-windows-wgc-helper.mjs
T
2026-05-10 15:11:16 +02:00

168 lines
4.2 KiB
JavaScript

import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.join(__dirname, "..");
const HELPER_PATH =
process.env.OPENSCREEN_WGC_CAPTURE_EXE ??
path.join(ROOT, "electron", "native", "bin", "win32-x64", "wgc-capture.exe");
const DURATION_MS = Number(process.env.OPENSCREEN_WGC_TEST_DURATION_MS ?? 5000);
const WITH_SYSTEM_AUDIO =
process.env.OPENSCREEN_WGC_TEST_SYSTEM_AUDIO === "true" ||
process.argv.includes("--system-audio");
function runHelper(config) {
return new Promise((resolve, reject) => {
const child = spawn(HELPER_PATH, [JSON.stringify(config)], {
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.once("error", reject);
child.once("exit", (code) => {
resolve({ code, stdout, stderr });
});
setTimeout(() => {
child.stdin.write("stop\n");
}, DURATION_MS);
});
}
function probeStreams(outputPath) {
const ffprobe = spawnSync(
"ffprobe",
["-v", "error", "-show_streams", "-of", "json", outputPath],
{ encoding: "utf8", windowsHide: true },
);
if (ffprobe.status !== 0) {
throw new Error(`ffprobe failed: ${ffprobe.stderr || ffprobe.stdout}`);
}
return JSON.parse(ffprobe.stdout).streams ?? [];
}
function measureFirstFrameLuma(outputPath) {
const ffmpeg = spawnSync(
"ffmpeg",
[
"-v",
"error",
"-i",
outputPath,
"-frames:v",
"1",
"-f",
"rawvideo",
"-pix_fmt",
"gray",
"pipe:1",
],
{ windowsHide: true, maxBuffer: 64 * 1024 * 1024 },
);
if (ffmpeg.status !== 0) {
throw new Error(`ffmpeg frame extraction failed: ${ffmpeg.stderr?.toString() ?? ""}`);
}
const data = ffmpeg.stdout;
if (!data || data.length === 0) {
throw new Error(`ffmpeg did not return frame data for ${outputPath}`);
}
let sum = 0;
let max = 0;
for (const value of data) {
sum += value;
if (value > max) {
max = value;
}
}
return { average: sum / data.length, max };
}
if (process.platform !== "win32") {
console.log("Skipping WGC helper smoke test: Windows-only.");
process.exit(0);
}
if (!fs.existsSync(HELPER_PATH)) {
throw new Error(`WGC helper not found at ${HELPER_PATH}. Run npm run build:native:win first.`);
}
const outputPath = path.join(
os.tmpdir(),
`openscreen-wgc-helper-${WITH_SYSTEM_AUDIO ? "audio" : "video"}-${Date.now()}.mp4`,
);
const config = {
schemaVersion: 2,
recordingId: Date.now(),
outputPath,
sourceType: "display",
sourceId: "screen:0:0",
displayId: 0,
fps: 30,
videoWidth: 1280,
videoHeight: 720,
displayX: 0,
displayY: 0,
displayW: 1920,
displayH: 1080,
hasDisplayBounds: true,
captureSystemAudio: WITH_SYSTEM_AUDIO,
captureMic: false,
webcamEnabled: false,
outputs: { screenPath: outputPath },
};
const result = await runHelper(config);
if (result.code !== 0) {
throw new Error(`WGC helper exited with ${result.code}\n${result.stdout}\n${result.stderr}`);
}
if (!fs.existsSync(outputPath) || fs.statSync(outputPath).size === 0) {
throw new Error(`WGC helper did not produce a video at ${outputPath}`);
}
const streams = probeStreams(outputPath);
const hasVideo = streams.some((stream) => stream.codec_type === "video");
const hasAudio = streams.some((stream) => stream.codec_type === "audio");
if (!hasVideo) {
throw new Error(`WGC helper output has no video stream: ${outputPath}`);
}
if (WITH_SYSTEM_AUDIO && !hasAudio) {
throw new Error(`WGC helper output has no audio stream: ${outputPath}`);
}
const frameLuma = measureFirstFrameLuma(outputPath);
if (frameLuma.average < 1 && frameLuma.max < 5) {
throw new Error(`WGC helper output first frame is black: ${outputPath}`);
}
console.log(
JSON.stringify(
{
success: true,
outputPath,
bytes: fs.statSync(outputPath).size,
streams: streams.map((stream) => ({
index: stream.index,
codecType: stream.codec_type,
codecName: stream.codec_name,
duration: stream.duration,
})),
firstFrameLuma: frameLuma,
},
null,
2,
),
);