168 lines
4.2 KiB
JavaScript
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,
|
|
),
|
|
);
|