Files
Twitch-VOD-Manager/scripts/smoke-test-cutter-media-matrix.js
T
Sucukdeluxe 6547e6dbbc
Windows CI / verify (push) Failing after 1m26s
fix(ci): canonicalize 8.3 short paths in cutter matrix ownership checks
assertPathInside compared managed-tool paths with path.resolve only, so
on runners whose temporary directory surfaces as a Windows 8.3 short
path (RUNNER~1) the canonical long executable paths appeared to live
outside the owned root and the cutter matrix provisioning contract
failed. Resolve both sides through fs.realpathSync.native, walking up
through not-yet-existing segments so planned directories keep working,
and cover the contract with a real ShortPath regression test plus
mixed-existence canonicalization cases.
2026-08-14 13:06:52 +02:00

1069 lines
51 KiB
JavaScript

const { _electron: electron } = require('playwright');
const nodeCrypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawn, spawnSync } = require('node:child_process');
const { requireFileCapability } = require('./file-capability-contract');
const {
cleanupE2eEnvironment,
createE2eEnvironment,
getElectronLaunchOptions,
verifyE2eIsolation
} = require('./e2e-test-environment');
const fixtureDuration = 3;
const exportTrimStart = 0.25;
const exportTrimEnd = 2.75;
const expectedExportDuration = exportTrimEnd - exportTrimStart;
const projectRoot = path.resolve(__dirname, '..');
const offlineProxy = 'http://127.0.0.1:1';
const cutterMatrixTimeouts = Object.freeze({
prepareSource: 90000,
exportSource: 180000,
appClose: 15000,
appProcessExit: 5000,
diagnostics: 30000
});
const markerFrequencies = Object.freeze({
initial: 220,
start: 440,
end: 1760
});
const markerTimes = Object.freeze({
initialEnd: 0.2,
startEnd: 0.55,
endStart: 2.55,
sourceStartSample: 0.3,
sourceEndSample: 2.65,
outputStartSample: 0.05,
outputEndSample: expectedExportDuration - 0.15
});
const markerAudioSampleRate = 48000;
function resolveCanonicalPath(candidatePath) {
let existingPath = path.resolve(candidatePath);
const missingSegments = [];
while (true) {
try {
return path.join(fs.realpathSync.native(existingPath), ...missingSegments.reverse());
} catch (error) {
if (error?.code !== 'ENOENT') throw error;
const parentPath = path.dirname(existingPath);
if (parentPath === existingPath) return path.resolve(candidatePath);
missingSegments.push(path.basename(existingPath));
existingPath = parentPath;
}
}
}
function assertPathInside(targetPath, parentPath, label) {
const resolvedTarget = resolveCanonicalPath(targetPath);
const resolvedParent = resolveCanonicalPath(parentPath);
const relative = path.relative(resolvedParent, resolvedTarget);
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
throw new Error(`${label} is outside the owned managed-tool directory: ${resolvedTarget}`);
}
}
function isDigitCharacter(character) {
return character >= '0' && character <= '9';
}
function isBuildSuffixCharacter(character) {
return isDigitCharacter(character)
|| (character >= 'A' && character <= 'Z')
|| (character >= 'a' && character <= 'z')
|| character === '.'
|| character === '_'
|| character === '-'
|| character === '+';
}
function parseVersionToken(versionToken) {
if (!versionToken) return null;
const delimiterIndexes = [versionToken.indexOf('-'), versionToken.indexOf('+')].filter((index) => index >= 0);
const delimiterIndex = delimiterIndexes.length > 0 ? Math.min(...delimiterIndexes) : -1;
const numericVersion = delimiterIndex >= 0 ? versionToken.slice(0, delimiterIndex) : versionToken;
const suffix = delimiterIndex >= 0 ? versionToken.slice(delimiterIndex + 1) : null;
const delimiter = delimiterIndex >= 0 ? versionToken[delimiterIndex] : null;
const numericSegments = numericVersion.split('.');
if (numericSegments.length < 2 || numericSegments.some((segment) => segment.length === 0 || [...segment].some((character) => !isDigitCharacter(character)))) return null;
if (suffix !== null && (suffix.length === 0 || [...suffix].some((character) => !isBuildSuffixCharacter(character)))) return null;
return { numericVersion, delimiter, suffix };
}
function assertPinnedVersion(output, version, label) {
const executable = String(label).toLowerCase().includes('streamlink')
? 'streamlink'
: String(label).toLowerCase().includes('ffprobe')
? 'ffprobe'
: 'ffmpeg';
const firstLine = String(output).split(/\r?\n/, 1)[0].trim();
const tokens = firstLine.split(/\s+/);
const versionToken = executable === 'streamlink' ? tokens[1] : tokens[2];
const hasExpectedFormat = executable === 'streamlink' || tokens[1]?.toLowerCase() === 'version';
const parsedVersion = parseVersionToken(versionToken);
if (tokens[0]?.toLowerCase() !== executable || !hasExpectedFormat || !parsedVersion) {
throw new Error(`${label} does not expose a parseable version: ${output}`);
}
if (parsedVersion.numericVersion !== String(version)) {
throw new Error(`${label} version output does not match pinned ${version}: ${output}`);
}
const expectedGyanSuffix = executable === 'streamlink' ? null : 'essentials_build-www.gyan.dev';
if (parsedVersion.suffix !== null && (parsedVersion.delimiter !== '-' || parsedVersion.suffix !== expectedGyanSuffix)) {
throw new Error(`${label} does not expose a parseable version: ${output}`);
}
}
async function withOperationTimeout(operation, label, timeoutMs) {
let timeout;
try {
return await Promise.race([
Promise.resolve().then(operation),
new Promise((_, reject) => {
timeout = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
})
]);
} finally {
if (timeout) clearTimeout(timeout);
}
}
function runBinary(binary, args, timeout = 120000, execute = spawnSync) {
const result = execute(binary, args, {
windowsHide: true,
stdio: 'pipe',
encoding: 'utf8',
timeout
});
if (result.error) throw result.error;
if (result.status !== 0) throw new Error(result.stderr.slice(0, 2000) || `${binary} exited with ${result.status}`);
return result.stdout;
}
function runBinaryBuffer(binary, args, timeout = 120000, execute = spawnSync) {
const result = execute(binary, args, {
windowsHide: true,
stdio: 'pipe',
encoding: null,
timeout
});
if (result.error) throw result.error;
if (result.status !== 0) throw new Error(Buffer.from(result.stderr || '').toString('utf8').slice(0, 2000) || `${binary} exited with ${result.status}`);
return Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout || '');
}
function runVersionCheck(executablePath, args, label) {
const output = runBinary(executablePath, args, 60000);
if (!output.trim()) throw new Error(`${label} version check produced no output`);
return output.split(/\r?\n/, 1)[0];
}
function loadBuiltToolArtifacts() {
const toolsPath = path.join(projectRoot, 'dist', 'tools.js');
const manifestPath = path.join(projectRoot, 'dist', 'main', 'domain', 'tool-manifest.js');
if (!fs.existsSync(toolsPath)) throw new Error('Build output is missing; run npm run build first');
if (!fs.existsSync(manifestPath)) throw new Error('Built tool manifest is missing; run npm run build first');
return {
tools: require(toolsPath),
manifest: require(manifestPath).APPLICATION_TOOL_MANIFEST
};
}
function assertVerifiedManagedStatus(status, expectedVersion, label) {
if (!status?.verified || status.state !== 'verified' || status.version !== expectedVersion) {
throw new Error(`${label} is not verified at pinned ${expectedVersion}: ${JSON.stringify(status)}`);
}
}
async function provisionManagedCutterTools(environment, options = {}) {
const streamlinkDirectory = path.join(environment.appDataDir, 'tools', 'streamlink');
const ffmpegDirectory = path.join(environment.appDataDir, 'tools', 'ffmpeg');
const temporaryDirectory = path.join(environment.rootDir, 'managed-tools-temp');
for (const directory of [streamlinkDirectory, ffmpegDirectory, temporaryDirectory]) {
assertPathInside(directory, environment.rootDir, 'Managed-tool directory');
fs.mkdirSync(directory, { recursive: true });
}
const loadArtifacts = options.loadBuiltArtifacts || loadBuiltToolArtifacts;
const checkVersion = options.runVersionCheck || runVersionCheck;
const { tools, manifest } = loadArtifacts();
tools.initToolDirs(streamlinkDirectory, ffmpegDirectory, () => temporaryDirectory);
const repair = await tools.repairManagedTools();
if (!repair.success) throw new Error(`Pinned product-tool provisioning failed: ${JSON.stringify(repair.statuses)}`);
assertVerifiedManagedStatus(repair.statuses.streamlink, manifest.streamlink.version, 'Streamlink');
assertVerifiedManagedStatus(repair.statuses.ffmpeg, manifest.ffmpeg.version, 'FFmpeg');
const paths = {
streamlink: fs.realpathSync.native(tools.getStreamlinkPath()),
ffmpeg: fs.realpathSync.native(tools.getFFmpegPath()),
ffprobe: fs.realpathSync.native(tools.getFFprobePath())
};
assertPathInside(paths.streamlink, streamlinkDirectory, 'Streamlink path');
assertPathInside(paths.ffmpeg, ffmpegDirectory, 'FFmpeg path');
assertPathInside(paths.ffprobe, ffmpegDirectory, 'FFprobe path');
const versions = {
streamlink: checkVersion(paths.streamlink, ['--version'], 'Streamlink'),
ffmpeg: checkVersion(paths.ffmpeg, ['-version'], 'FFmpeg'),
ffprobe: checkVersion(paths.ffprobe, ['-version'], 'FFprobe')
};
assertPinnedVersion(versions.streamlink, manifest.streamlink.version, 'Streamlink');
assertPinnedVersion(versions.ffmpeg, manifest.ffmpeg.version, 'FFmpeg');
assertPinnedVersion(versions.ffprobe, manifest.ffmpeg.version, 'FFprobe');
return { manifest, paths, statuses: repair.statuses, versions };
}
function createManagedMediaRuntime(paths, execute = spawnSync) {
if (!path.isAbsolute(paths.ffmpeg) || !path.isAbsolute(paths.ffprobe)) {
throw new Error('Managed media runtime requires absolute product-tool paths');
}
return {
ffmpeg: (args, timeout) => runBinary(paths.ffmpeg, args, timeout, execute),
ffmpegBuffer: (args, timeout) => runBinaryBuffer(paths.ffmpeg, args, timeout, execute),
ffprobe: (args, timeout) => runBinary(paths.ffprobe, args, timeout, execute)
};
}
function setEnvironmentValue(environmentVariables, expectedKey, value, snapshots) {
const key = Object.keys(environmentVariables).find((candidate) => candidate.toLowerCase() === expectedKey.toLowerCase()) || expectedKey;
snapshots.push({ key, existed: Object.prototype.hasOwnProperty.call(environmentVariables, key), value: environmentVariables[key] });
environmentVariables[key] = value;
}
function getEnvironmentValue(environmentVariables, expectedKey) {
const key = Object.keys(environmentVariables).find((candidate) => candidate.toLowerCase() === expectedKey.toLowerCase());
return key ? environmentVariables[key] : undefined;
}
function activateOfflineRunnerEnvironment(environment, environmentVariables = process.env) {
const directories = {
path: path.join(environment.rootDir, 'offline-path'),
localAppData: path.join(environment.rootDir, 'localappdata'),
roamingAppData: path.join(environment.rootDir, 'roamingappdata'),
temp: path.join(environment.rootDir, 'runtime-temp')
};
for (const directory of Object.values(directories)) {
assertPathInside(directory, environment.rootDir, 'Offline runner directory');
fs.mkdirSync(directory, { recursive: true });
}
const snapshots = [];
const values = {
PATH: directories.path,
HTTP_PROXY: offlineProxy,
HTTPS_PROXY: offlineProxy,
ALL_PROXY: offlineProxy,
NO_PROXY: '',
http_proxy: offlineProxy,
https_proxy: offlineProxy,
all_proxy: offlineProxy,
no_proxy: '',
LOCALAPPDATA: directories.localAppData,
APPDATA: directories.roamingAppData,
TEMP: directories.temp,
TMP: directories.temp,
PROGRAMDATA: environment.programDataDir || path.dirname(environment.appDataDir)
};
for (const [key, value] of Object.entries(values)) setEnvironmentValue(environmentVariables, key, value, snapshots);
let restored = false;
return () => {
if (restored) return;
restored = true;
for (const snapshot of snapshots.reverse()) {
if (snapshot.existed) environmentVariables[snapshot.key] = snapshot.value;
else delete environmentVariables[snapshot.key];
}
};
}
function resolvePowerShellExecutable() {
const windowsDirectory = process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows';
const executable = path.join(windowsDirectory, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
if (!fs.existsSync(executable)) throw new Error(`PowerShell is missing: ${executable}`);
return fs.realpathSync.native(executable);
}
function probeMedia(runtime, filePath) {
const args = ['-v', 'error', '-print_format', 'json', '-show_format', '-show_streams'];
args.push(filePath);
return JSON.parse(runtime.ffprobe(args));
}
function probeVideoTimestamps(runtime, filePath) {
const output = runtime.ffprobe([
'-v', 'error', '-print_format', 'json', '-select_streams', 'v:0',
'-show_frames', '-show_entries', 'frame=best_effort_timestamp_time', filePath
]);
const frames = JSON.parse(output).frames || [];
return frames.map((frame) => Number(frame.best_effort_timestamp_time)).filter(Number.isFinite);
}
function assertVideoCadence(runtime, definition, filePath) {
const timestamps = probeVideoTimestamps(runtime, filePath);
assertCondition(timestamps.length > 1, `${definition.name} output has fewer than two decoded video frames`);
assertCondition(timestamps.every((timestamp, index) => index === 0 || timestamp > timestamps[index - 1]), `${definition.name} output frame timestamps are not strictly monotone`);
const deltas = timestamps.slice(1).map((timestamp, index) => timestamp - timestamps[index]);
const expectedDelta = 1 / definition.expectedOutputRealFps;
const cadenceTolerance = Math.max(0.0015, expectedDelta * 0.08);
if (definition.variableFrameRate) {
const cadenceMultiples = deltas.map((delta) => Math.round(delta / expectedDelta));
assertCondition(new Set(cadenceMultiples).size >= 2, `${definition.name} output lost its variable frame cadence`);
assertCondition(deltas.every((delta, index) => cadenceMultiples[index] >= 1 && cadenceMultiples[index] <= 2 && Math.abs(delta - cadenceMultiples[index] * expectedDelta) <= cadenceTolerance), `${definition.name} output cadence contains unexpected VFR deltas: ${JSON.stringify([...new Set(deltas.map((delta) => Number(delta.toFixed(6))))])}`);
} else {
assertCondition(deltas.every((delta) => Math.abs(delta - expectedDelta) <= cadenceTolerance), `${definition.name} output cadence contains unexpected CFR deltas: ${JSON.stringify([...new Set(deltas.map((delta) => Number(delta.toFixed(6))))])}`);
}
return {
frames: timestamps.length,
start: timestamps[0],
end: timestamps.at(-1) + expectedDelta,
distinctDeltas: [...new Set(deltas.map((delta) => Number(delta.toFixed(6))))]
};
}
function probePacketTimeline(runtime, filePath, streamSelector) {
const output = runtime.ffprobe([
'-v', 'error', '-print_format', 'json', '-select_streams', streamSelector,
'-show_packets', '-show_entries', 'packet=pts_time,duration_time', filePath
]);
const packets = (JSON.parse(output).packets || []).map((packet) => ({
start: Number(packet.pts_time),
duration: Number(packet.duration_time)
})).filter((packet) => Number.isFinite(packet.start));
assertCondition(packets.length > 0, `${path.basename(filePath)} has no ${streamSelector} packets`);
return {
start: packets[0].start,
end: Math.max(...packets.map((packet) => packet.start + (Number.isFinite(packet.duration) ? packet.duration : 0))),
packets: packets.length
};
}
function decodeMedia(runtime, filePath) {
runtime.ffmpeg(['-v', 'error', '-i', filePath, '-map', '0:v:0', '-map', '0:a:0?', '-f', 'null', '-']);
}
function sha256(filePath) {
return nodeCrypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
}
function mediaDuration(probe) {
return Number(probe.format?.duration || probe.streams?.find((stream) => stream.codec_type === 'video')?.duration || 0);
}
function parseRate(value) {
if (typeof value !== 'string') return 0;
const [numerator, denominator = '1'] = value.split('/').map(Number);
return Number.isFinite(numerator) && Number.isFinite(denominator) && denominator !== 0 ? numerator / denominator : 0;
}
function assertCondition(condition, message) {
if (!condition) throw new Error(message);
}
function assertNoExportArtifacts(directory, outputName) {
const outputExtension = path.extname(outputName);
const outputStem = path.basename(outputName, outputExtension);
const artifacts = fs.readdirSync(directory).filter((name) =>
name.includes(`${outputStem}.`) && (name.includes('.tvm-edit') || name.includes('.tvm-backup'))
);
assertCondition(artifacts.length === 0, `Temporary export artifacts remain for ${outputName}: ${JSON.stringify(artifacts)}`);
}
function createVideoMarkerSource(rate) {
return [
`color=c=blue:size=320x180:rate=${rate}:duration=${fixtureDuration}`,
`drawbox=color=red:t=fill:enable='lt(t,${markerTimes.initialEnd})'`,
`drawbox=color=lime:t=fill:enable='between(t,${markerTimes.initialEnd},${markerTimes.startEnd})'`,
`drawbox=color=magenta:t=fill:enable='between(t,2.2,${markerTimes.endStart})'`,
`drawbox=color=cyan:t=fill:enable='gte(t,${markerTimes.endStart})'`
].join(',');
}
function createAudioMarkerSource(middleFrequency) {
const expression = [
`if(lt(t\\,${markerTimes.initialEnd})\\,sin(2*PI*${markerFrequencies.initial}*t)`,
`if(lt(t\\,${markerTimes.startEnd})\\,sin(2*PI*${markerFrequencies.start}*t)`,
`if(lt(t\\,${markerTimes.endStart})\\,sin(2*PI*${middleFrequency}*t)`,
`sin(2*PI*${markerFrequencies.end}*t))))`
].join('\\,');
return `aevalsrc=${expression}:s=${markerAudioSampleRate}:d=${fixtureDuration}`;
}
function sampleVideoRgb(runtime, filePath, time) {
const output = runtime.ffmpegBuffer([
'-v', 'error', '-i', filePath, '-ss', String(time),
'-frames:v', '1', '-vf', 'scale=1:1:flags=area,format=rgb24',
'-f', 'rawvideo', 'pipe:1'
]);
assertCondition(output.length >= 3, `${path.basename(filePath)} produced no decoded RGB marker`);
return [output[0], output[1], output[2]];
}
function estimatePcmFrequency(buffer, sampleRate = markerAudioSampleRate) {
if (!Buffer.isBuffer(buffer) || buffer.length < 4 || buffer.length % 2 !== 0) return 0;
const crossings = [];
let state = 0;
for (let index = 0; index < buffer.length / 2; index += 1) {
const sample = buffer.readInt16LE(index * 2);
if (sample <= -500) state = -1;
else if (sample >= 500 && state === -1) {
crossings.push(index);
state = 1;
}
}
if (crossings.length < 2) return 0;
return sampleRate * (crossings.length - 1) / (crossings.at(-1) - crossings[0]);
}
function sampleAudioFrequency(runtime, filePath, time) {
const output = runtime.ffmpegBuffer([
'-v', 'error', '-i', filePath, '-ss', String(time), '-t', '0.1',
'-map', '0:a:0', '-ac', '1', '-ar', String(markerAudioSampleRate),
'-f', 's16le', 'pipe:1'
]);
return estimatePcmFrequency(output, markerAudioSampleRate);
}
function classifyVideoMarker(rgb) {
const [red, green, blue] = rgb;
if (green > red + 35 && green > blue + 35) return 'green';
if (green > red + 35 && blue > red + 35 && Math.abs(green - blue) <= 80) return 'cyan';
if (red > green + 35 && red > blue + 35) return 'red';
if (red > green + 35 && blue > green + 35) return 'magenta';
return 'unknown';
}
function assertTrimBoundaryMarkers(observation, label) {
const startVideoMarker = classifyVideoMarker(observation.startVideoRgb);
const endVideoMarker = classifyVideoMarker(observation.endVideoRgb);
assertCondition(startVideoMarker === 'green', `${label} start video marker is ${startVideoMarker}: ${JSON.stringify(observation.startVideoRgb)}`);
assertCondition(endVideoMarker === 'cyan', `${label} end video marker is ${endVideoMarker}: ${JSON.stringify(observation.endVideoRgb)}`);
assertCondition(Math.abs(observation.startAudioFrequency - markerFrequencies.start) <= markerFrequencies.start * 0.12, `${label} start audio marker is ${observation.startAudioFrequency}`);
assertCondition(Math.abs(observation.endAudioFrequency - markerFrequencies.end) <= markerFrequencies.end * 0.12, `${label} end audio marker is ${observation.endAudioFrequency}`);
}
function observeTrimBoundaryMarkers(runtime, filePath, startTime, endTime) {
return {
startVideoRgb: sampleVideoRgb(runtime, filePath, startTime),
endVideoRgb: sampleVideoRgb(runtime, filePath, endTime),
startAudioFrequency: sampleAudioFrequency(runtime, filePath, startTime),
endAudioFrequency: sampleAudioFrequency(runtime, filePath, endTime)
};
}
function createFixture(environment, runtime, definition) {
const filePath = path.join(environment.mediaDir, definition.fileName);
runtime.ffmpeg(definition.ffmpegArgs(filePath));
const probe = probeMedia(runtime, filePath);
const video = probe.streams.find((stream) => stream.codec_type === 'video');
const audio = probe.streams.filter((stream) => stream.codec_type === 'audio');
assertCondition(video?.codec_name === definition.sourceVideoCodec, `${definition.name} source codec is ${video?.codec_name}`);
assertCondition(audio.length === definition.audioFrequencies.length, `${definition.name} source has ${audio.length} audio streams`);
assertCondition(JSON.stringify(audio.map((stream) => stream.channels)) === JSON.stringify(definition.sourceAudioChannels), `${definition.name} source audio channels are ${JSON.stringify(audio.map((stream) => stream.channels))}`);
assertCondition(Math.abs(mediaDuration(probe) - fixtureDuration) <= 0.25, `${definition.name} source duration is ${mediaDuration(probe)}`);
const averageRate = parseRate(video.avg_frame_rate);
const realRate = parseRate(video.r_frame_rate);
assertCondition(Math.abs(averageRate - definition.sourceFps) <= definition.sourceFpsTolerance, `${definition.name} source FPS is ${averageRate}`);
assertCondition(Math.abs(realRate - definition.sourceRealFps) <= definition.sourceFpsTolerance, `${definition.name} source real FPS is ${realRate}`);
if (definition.variableFrameRate) {
assertCondition(Math.abs(averageRate - realRate) / Math.max(averageRate, realRate) > 0.005, `${definition.name} source rates do not satisfy production VFR detection: ${averageRate}/${realRate}`);
const timestamps = probeVideoTimestamps(runtime, filePath);
const deltas = timestamps.slice(1).map((timestamp, index) => Number((timestamp - timestamps[index]).toFixed(6)));
assertCondition(new Set(deltas).size >= 2, `${definition.name} fixture is not variable frame rate: ${JSON.stringify([...new Set(deltas)])}`);
}
const trimMarkers = observeTrimBoundaryMarkers(runtime, filePath, markerTimes.sourceStartSample, markerTimes.sourceEndSample);
assertTrimBoundaryMarkers(trimMarkers, `${definition.name} source`);
return { filePath, probe, hash: sha256(filePath), trimMarkers };
}
function createMatrixDefinitions() {
const createInputs = (rate, audioFrequencies) => [
'-hide_banner', '-loglevel', 'error',
'-f', 'lavfi', '-i', createVideoMarkerSource(rate),
...audioFrequencies.flatMap((frequency) => ['-f', 'lavfi', '-i', createAudioMarkerSource(frequency)])
];
return [
{
id: 'av1-multi',
name: 'MKV AV1 29.97 multi-audio',
fileName: 'matrix-av1-2997-multi.mkv',
outputName: 'matrix-av1-2997-multi-export.mp4',
profile: 'quality',
audioStreamIndex: 1,
audioFrequencies: [440, 880],
sourceAudioChannels: [1, 2],
expectedAudioChannels: 2,
sourceVideoCodec: 'av1',
sourceFps: 30000 / 1001,
sourceRealFps: 30000 / 1001,
sourceFpsTolerance: 0.01,
expectedOutputRealFps: 30000 / 1001,
outputFpsTolerance: 0.02,
variableFrameRate: false,
ffmpegArgs: (filePath) => [
...createInputs('30000/1001', [440, 880]),
'-map', '0:v:0', '-map', '1:a:0', '-map', '2:a:0',
'-t', String(fixtureDuration), '-c:v', 'libaom-av1', '-cpu-used', '8', '-crf', '42', '-b:v', '0', '-pix_fmt', 'yuv420p',
'-c:a', 'aac', '-ac:a:0', '1', '-ac:a:1', '2', '-metadata:s:a:0', 'language=eng', '-metadata:s:a:1', 'language=deu', '-shortest', '-y', filePath
]
},
{
id: 'hevc-ts',
name: 'MPEG-TS HEVC 59.94',
fileName: 'matrix-hevc-5994.ts',
outputName: 'matrix-hevc-5994-export.mp4',
profile: 'fast',
audioStreamIndex: 0,
audioFrequencies: [660],
sourceAudioChannels: [1],
expectedAudioChannels: 1,
sourceVideoCodec: 'hevc',
sourceFps: 60000 / 1001,
sourceRealFps: 60000 / 1001,
sourceFpsTolerance: 0.01,
expectedOutputRealFps: 60000 / 1001,
outputFpsTolerance: 0.02,
variableFrameRate: false,
ffmpegArgs: (filePath) => [
...createInputs('60000/1001', [660]),
'-t', String(fixtureDuration), '-c:v', 'libx265', '-preset', 'ultrafast', '-x265-params', 'log-level=error', '-crf', '35', '-pix_fmt', 'yuv420p',
'-c:a', 'aac', '-ac', '1', '-f', 'mpegts', '-shortest', '-y', filePath
]
},
{
id: 'avi',
name: 'AVI MPEG-4',
fileName: 'matrix-mpeg4.avi',
outputName: 'matrix-mpeg4-export.mkv',
profile: 'archive',
audioStreamIndex: 0,
audioFrequencies: [330],
sourceAudioChannels: [1],
expectedAudioChannels: 1,
sourceVideoCodec: 'mpeg4',
sourceFps: 25,
sourceRealFps: 25,
sourceFpsTolerance: 0.01,
expectedOutputRealFps: 25,
outputFpsTolerance: 0.01,
variableFrameRate: false,
ffmpegArgs: (filePath) => [
...createInputs('25', [330]),
'-t', String(fixtureDuration), '-c:v', 'mpeg4', '-q:v', '4', '-pix_fmt', 'yuv420p',
'-c:a', 'libmp3lame', '-ac', '1', '-shortest', '-y', filePath
]
},
{
id: 'vfr',
name: 'MP4 H.264 VFR',
fileName: 'matrix-vfr.mp4',
outputName: 'matrix-vfr-export.mp4',
profile: 'balanced',
audioStreamIndex: 0,
audioFrequencies: [550],
sourceAudioChannels: [1],
expectedAudioChannels: 1,
sourceVideoCodec: 'h264',
sourceFps: 50,
sourceRealFps: 60,
sourceFpsTolerance: 1,
expectedOutputRealFps: 60,
outputFpsTolerance: 3,
variableFrameRate: true,
ffmpegArgs: (filePath) => [
...createInputs('60', [550]),
'-vf', "select='if(lt(t,1),not(mod(n,2)),1)'", '-fps_mode', 'vfr',
'-t', String(fixtureDuration), '-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '32', '-pix_fmt', 'yuv420p',
'-c:a', 'aac', '-ac', '1', '-shortest', '-y', filePath
]
}
];
}
async function createCutterCapability(win, filePath) {
const inputId = `cutter-matrix-${nodeCrypto.randomUUID()}`;
await win.evaluate((id) => {
const input = document.createElement('input');
input.type = 'file';
input.id = id;
document.body.appendChild(input);
}, inputId);
await win.locator(`#${inputId}`).setInputFiles(filePath);
const capability = await win.evaluate(async (id) => {
const input = document.getElementById(id);
const file = input instanceof HTMLInputElement ? input.files?.[0] : null;
const selection = file ? await window.api.selectDroppedVideo(file) : null;
input?.remove();
return selection;
}, inputId);
return requireFileCapability(capability);
}
async function prepareSource(win, filePath, options = {}) {
const timeoutMs = options.timeoutMs ?? cutterMatrixTimeouts.prepareSource;
const createCapability = options.createCapability || createCutterCapability;
return await withOperationTimeout(async () => {
const capability = await createCapability(win, filePath);
const media = await win.evaluate((selection) => window.api.prepareVideoEditorMedia(selection.token), capability);
return { capability, media };
}, 'prepareSource', timeoutMs);
}
async function exportSource(win, definition, source, timeoutMs = cutterMatrixTimeouts.exportSource) {
return await withOperationTimeout(() => win.evaluate(({ inputCapability, outputName, trimStart, trimEnd, profile, audioStreamIndex }) => window.api.exportVideoEdit({
inputCapability,
outputName,
trimStart,
trimEnd,
cuts: [],
profile,
encoder: 'software',
audioStreamIndex
}), {
inputCapability: source.capability.token,
outputName: definition.outputName,
trimStart: exportTrimStart,
trimEnd: exportTrimEnd,
profile: definition.profile,
audioStreamIndex: definition.audioStreamIndex
}), 'exportSource', timeoutMs);
}
function analyzeExport(environment, runtime, definition, source) {
const outputFile = path.join(environment.mediaDir, definition.outputName);
assertCondition(fs.existsSync(outputFile), `${definition.name} output was not created`);
const probe = probeMedia(runtime, outputFile);
const videoStreams = probe.streams.filter((stream) => stream.codec_type === 'video');
const audioStreams = probe.streams.filter((stream) => stream.codec_type === 'audio');
assertCondition(videoStreams.length === 1, `${definition.name} output has ${videoStreams.length} video streams`);
assertCondition(audioStreams.length === 1, `${definition.name} output has ${audioStreams.length} audio streams`);
assertCondition(videoStreams[0].codec_name === (definition.profile === 'archive' ? 'ffv1' : 'h264'), `${definition.name} output video codec is ${videoStreams[0].codec_name}`);
assertCondition(audioStreams[0].codec_name === (definition.profile === 'archive' ? 'flac' : 'aac'), `${definition.name} output audio codec is ${audioStreams[0].codec_name}`);
assertCondition(audioStreams[0].channels === definition.expectedAudioChannels, `${definition.name} selected audio stream has ${audioStreams[0].channels} channels instead of ${definition.expectedAudioChannels}`);
const duration = mediaDuration(probe);
assertCondition(Math.abs(duration - expectedExportDuration) <= 0.16, `${definition.name} output duration is ${duration}`);
const realRate = parseRate(videoStreams[0].r_frame_rate);
assertCondition(Math.abs(realRate - definition.expectedOutputRealFps) <= definition.outputFpsTolerance, `${definition.name} output real FPS is ${realRate}`);
const videoCadence = assertVideoCadence(runtime, definition, outputFile);
const audioTimeline = probePacketTimeline(runtime, outputFile, 'a:0');
assertCondition(Math.abs(videoCadence.start - audioTimeline.start) <= 0.08, `${definition.name} A/V start differs by ${Math.abs(videoCadence.start - audioTimeline.start)}`);
assertCondition(Math.abs(videoCadence.end - audioTimeline.end) <= 0.12, `${definition.name} A/V end differs by ${Math.abs(videoCadence.end - audioTimeline.end)}`);
assertCondition(videoStreams[0].width === 320 && videoStreams[0].height === 180, `${definition.name} output dimensions are ${videoStreams[0].width}x${videoStreams[0].height}`);
decodeMedia(runtime, outputFile);
const trimMarkers = observeTrimBoundaryMarkers(runtime, outputFile, markerTimes.outputStartSample, markerTimes.outputEndSample);
assertTrimBoundaryMarkers(trimMarkers, `${definition.name} output`);
assertCondition(sha256(source.filePath) === source.hash, `${definition.name} source changed during export`);
assertNoExportArtifacts(environment.mediaDir, definition.outputName);
return {
outputName: definition.outputName,
duration,
videoCodec: videoStreams[0].codec_name,
audioCodec: audioStreams[0].codec_name,
videoRate: videoStreams[0].avg_frame_rate,
realVideoRate: videoStreams[0].r_frame_rate,
audioChannels: audioStreams[0].channels,
videoCadence,
audioTimeline,
trimMarkers
};
}
function sameResolvedPath(left, right) {
if (typeof left !== 'string' || typeof right !== 'string') return false;
const resolvedLeft = path.resolve(left);
const resolvedRight = path.resolve(right);
return process.platform === 'win32'
? resolvedLeft.toLowerCase() === resolvedRight.toLowerCase()
: resolvedLeft === resolvedRight;
}
function assertManagedExecutionDiagnostics(options) {
const {
diagnostics,
expectedPaths,
streamlinkDirectory,
ffmpegDirectory,
electronPath,
expectedElectronPath,
previousDiagnostics = null,
requiredTools = [],
label
} = options;
assertCondition(diagnostics !== null && typeof diagnostics === 'object', `${label} managed execution diagnostics are unavailable`);
const tools = [
{ key: 'ffmpeg', name: 'FFmpeg', expectedPath: expectedPaths.ffmpeg, directory: ffmpegDirectory },
{ key: 'ffprobe', name: 'FFprobe', expectedPath: expectedPaths.ffprobe, directory: ffmpegDirectory },
{ key: 'streamlink', name: 'Streamlink', expectedPath: expectedPaths.streamlink, directory: streamlinkDirectory }
];
for (const tool of tools) {
const record = diagnostics[tool.key];
assertCondition(record !== null && typeof record === 'object' && Number.isInteger(record.count) && record.count >= 0, `${label} ${tool.name} execution record is invalid: ${JSON.stringify(record)}`);
if (record.count === 0) {
assertCondition(record.path === null, `${label} ${tool.name} reported a path without an execution: ${record.path}`);
} else {
assertCondition(sameResolvedPath(record.path, tool.expectedPath), `${label} did not execute the provisioned ${tool.name} path: ${record.path}`);
assertPathInside(record.path, tool.directory, `${label} ${tool.name} path`);
}
if (previousDiagnostics) {
const previousRecord = previousDiagnostics[tool.key];
assertCondition(previousRecord !== null && typeof previousRecord === 'object' && Number.isInteger(previousRecord.count) && previousRecord.count >= 0, `${label} previous ${tool.name} execution record is invalid: ${JSON.stringify(previousRecord)}`);
assertCondition(record.count >= previousRecord.count, `${label} ${tool.name} execution count regressed from ${previousRecord.count} to ${record.count}`);
if (requiredTools.includes(tool.key)) assertCondition(record.count > previousRecord.count, `${label} did not record a new ${tool.name} execution`);
}
}
assertCondition(sameResolvedPath(electronPath, expectedElectronPath), `${label} Electron PATH escaped isolation: ${electronPath}`);
}
async function readManagedExecutionDiagnostics(win, timeoutMs = cutterMatrixTimeouts.diagnostics) {
return await withOperationTimeout(
() => win.evaluate(() => window.api.getManagedToolExecutionDiagnostics()),
'managed tool execution diagnostics',
timeoutMs
);
}
async function readDebugLog(win, timeoutMs = cutterMatrixTimeouts.diagnostics) {
return await withOperationTimeout(
() => win.evaluate(() => window.api.getDebugLog(1000)),
'debug log read',
timeoutMs
);
}
function assertLockedTargetFailure({ result, debugBefore, debugAfter, outputFile, runtimeIssues }) {
assertCondition(!result?.rejected, `Locked target export must resolve through the product IPC: ${result?.rejected}`);
assertCondition(result?.success === false, `Locked target export unexpectedly succeeded: ${JSON.stringify(result)}`);
assertCondition(result.cancelled !== true, `Locked target export must not be reported as cancelled: ${JSON.stringify(result)}`);
assertCondition(runtimeIssues.length === 0, `Locked target export produced runtime issues: ${runtimeIssues.join(' | ')}`);
const delta = debugAfter.startsWith(debugBefore) ? debugAfter.slice(debugBefore.length) : debugAfter;
const hasMatchingFailure = delta.split(/\r?\n/).some((line) => {
if (!/video-editor-export-failed/.test(line)
|| !/(?:EPERM|EBUSY|EACCES|operation not permitted|being used by another process|cannot access)/i.test(line)) return false;
const paths = extractRenamePaths(line);
if (!paths) return false;
const outputDirectory = path.dirname(outputFile);
const stagingPublish = sameResolvedPath(paths.destination, outputFile)
&& sameResolvedPath(path.dirname(paths.source), outputDirectory)
&& path.basename(paths.source).toLowerCase().includes('.tvm-edit');
const backupPublish = sameResolvedPath(paths.source, outputFile)
&& sameResolvedPath(path.dirname(paths.destination), outputDirectory)
&& path.basename(paths.destination).startsWith(`${path.basename(outputFile)}.`)
&& path.basename(paths.destination).toLowerCase().endsWith('.tvm-backup');
return stagingPublish || backupPublish;
});
assertCondition(hasMatchingFailure, `Locked target export did not emit an atomic publish lock diagnostic: ${delta}`);
}
function extractRenamePaths(line) {
const renameIndex = line.toLowerCase().lastIndexOf('rename ');
if (renameIndex < 0) return null;
const remainder = line.slice(renameIndex + 7).trim();
const sourceQuote = remainder[0];
if (sourceQuote !== "'" && sourceQuote !== '"') return null;
const sourceEnd = remainder.indexOf(sourceQuote, 1);
if (sourceEnd < 1) return null;
const afterSource = remainder.slice(sourceEnd + 1).trim();
if (!afterSource.startsWith('->')) return null;
const destinationPart = afterSource.slice(2).trim();
const destinationQuote = destinationPart[0];
if (destinationQuote !== "'" && destinationQuote !== '"') return null;
const destinationEnd = destinationPart.indexOf(destinationQuote, 1);
if (destinationEnd < 1 || destinationPart.slice(destinationEnd + 1).trim()) return null;
return {
source: remainder.slice(1, sourceEnd),
destination: destinationPart.slice(1, destinationEnd)
};
}
async function stopLockProcess(lockProcess, timeoutMs = 5000) {
const processExit = new Promise((resolve) => lockProcess.once('exit', resolve));
lockProcess.kill();
await withOperationTimeout(() => processExit, 'locked target helper exit', timeoutMs);
}
async function runLockedTargetCase(win, environment, powerShellExecutable, definition, source, runtimeIssues) {
const outputFile = path.join(environment.mediaDir, definition.outputName);
const sentinel = Buffer.from(`locked-target-${nodeCrypto.randomUUID()}`);
fs.writeFileSync(outputFile, sentinel);
let lockProcess = null;
const originalDirectoryMode = fs.statSync(environment.mediaDir).mode & 0o777;
try {
if (process.platform === 'win32') {
const lockScript = "$stream=[IO.File]::Open($env:TWITCH_VOD_MANAGER_LOCK_TARGET,[IO.FileMode]::Open,[IO.FileAccess]::ReadWrite,[IO.FileShare]::None);[Console]::Out.WriteLine('ready');[Console]::Out.Flush();while($true){Start-Sleep -Milliseconds 250}";
lockProcess = spawn(powerShellExecutable, ['-NoProfile', '-Command', lockScript], {
windowsHide: true,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, TWITCH_VOD_MANAGER_LOCK_TARGET: outputFile }
});
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Locked target helper did not start')), 5000);
lockProcess.stdout.once('data', () => {
clearTimeout(timeout);
resolve();
});
lockProcess.once('exit', (code) => reject(new Error(`Locked target helper exited with ${code}`)));
});
} else {
fs.chmodSync(environment.mediaDir, 0o500);
}
const debugBefore = await readDebugLog(win);
const result = await exportSource(win, definition, source);
const debugAfter = await readDebugLog(win);
assertLockedTargetFailure({ result, debugBefore, debugAfter, outputFile, runtimeIssues });
if (lockProcess) {
await stopLockProcess(lockProcess);
lockProcess = null;
}
assertCondition(fs.readFileSync(outputFile).equals(sentinel), 'Locked target contents changed after failed export');
assertNoExportArtifacts(environment.mediaDir, definition.outputName);
return result;
} finally {
if (lockProcess) await stopLockProcess(lockProcess).catch(() => {});
if (process.platform !== 'win32' && fs.existsSync(environment.mediaDir)) fs.chmodSync(environment.mediaDir, originalDirectoryMode);
}
}
async function runCorruptSourceCase(win, environment) {
const corruptFile = path.join(environment.mediaDir, 'matrix-corrupt.mkv');
fs.writeFileSync(corruptFile, nodeCrypto.randomBytes(2048));
const corruptHash = sha256(corruptFile);
const outputName = 'matrix-corrupt-export.mp4';
const target = path.join(environment.mediaDir, outputName);
const sentinel = Buffer.from(`corrupt-target-${nodeCrypto.randomUUID()}`);
fs.writeFileSync(target, sentinel);
const prepared = await prepareSource(win, corruptFile);
assertCondition(prepared.media === null, `Corrupt source unexpectedly prepared: ${JSON.stringify(prepared.media)}`);
const result = await exportSource(win, { outputName, profile: 'balanced', audioStreamIndex: 0 }, prepared);
assertCondition(!result.success, `Corrupt source export unexpectedly succeeded: ${JSON.stringify(result)}`);
assertCondition(sha256(corruptFile) === corruptHash, 'Corrupt source changed during failed export');
assertCondition(fs.readFileSync(target).equals(sentinel), 'Existing corrupt-source target changed after failed export');
assertNoExportArtifacts(environment.mediaDir, outputName);
return result;
}
async function terminateElectronProcess(app, timeoutMs) {
const child = app.process?.();
if (!child || child.exitCode !== null) return;
await new Promise((resolve, reject) => {
let timeout;
const cleanup = () => {
if (timeout) clearTimeout(timeout);
child.removeListener('exit', onExit);
child.removeListener('error', onError);
};
const settle = (error) => {
cleanup();
if (error) reject(error);
else resolve();
};
const onExit = () => settle();
const onError = (error) => settle(error);
child.once('exit', onExit);
child.once('error', onError);
timeout = setTimeout(() => settle(new Error(`Electron process exit timed out after ${timeoutMs}ms`)), timeoutMs);
try {
child.kill();
if (child.exitCode !== null) settle();
} catch (error) {
settle(error);
}
});
}
async function closeElectronApp(app, closeTimeoutMs = cutterMatrixTimeouts.appClose, processExitTimeoutMs = cutterMatrixTimeouts.appProcessExit) {
try {
return await withOperationTimeout(() => app.close(), 'app.close', closeTimeoutMs);
} catch (error) {
await terminateElectronProcess(app, processExitTimeoutMs);
throw error;
}
}
async function runCutterMatrixLifecycle(options) {
const environment = options.createEnvironment();
let app = null;
let restoreEnvironment = () => {};
const closeApp = async () => {
const activeApp = app;
app = null;
if (activeApp) await options.closeApp(activeApp);
};
try {
return await options.execute({
environment,
closeApp,
setApp: (value) => { app = value; },
setRestoreEnvironment: (value) => { restoreEnvironment = value; }
});
} finally {
try {
await closeApp();
} finally {
try {
restoreEnvironment();
} finally {
options.cleanupEnvironment(environment);
}
}
}
}
async function executeCutterMatrix({ environment, closeApp, setApp, setRestoreEnvironment }) {
assertCondition(process.platform === 'win32', 'Cutter media matrix requires the Windows product toolchain');
let app;
const provisionedTools = await provisionManagedCutterTools(environment);
const runtime = createManagedMediaRuntime(provisionedTools.paths);
const powerShellExecutable = resolvePowerShellExecutable();
setRestoreEnvironment(activateOfflineRunnerEnvironment(environment));
const definitions = createMatrixDefinitions();
const caseArgument = process.argv.find((argument) => argument.startsWith('--case='));
const requestedCases = caseArgument ? new Set(caseArgument.slice('--case='.length).split(',').filter(Boolean)) : null;
const knownCases = new Set([...definitions.map((definition) => definition.id), 'errors']);
if (requestedCases) {
const unknownCases = [...requestedCases].filter((id) => !knownCases.has(id));
assertCondition(unknownCases.length === 0, `Unknown matrix cases: ${unknownCases.join(', ')}`);
}
const selectedDefinitions = requestedCases ? definitions.filter((definition) => requestedCases.has(definition.id)) : definitions;
const includeErrors = !requestedCases || requestedCases.has('errors');
const fixtureDefinitions = [...selectedDefinitions];
if (includeErrors && !fixtureDefinitions.some((definition) => definition.id === definitions[0].id)) fixtureDefinitions.push(definitions[0]);
assertCondition(selectedDefinitions.length > 0 || includeErrors, 'No cutter matrix cases selected');
const fixtures = fixtureDefinitions.map((definition) => ({ definition, source: createFixture(environment, runtime, definition) }));
if (process.argv.includes('--fixtures-only')) {
console.log(JSON.stringify({
fixtures: fixtures.map((fixture) => ({
case: fixture.definition.name,
fileName: path.basename(fixture.source.filePath),
duration: mediaDuration(fixture.source.probe),
videoRate: fixture.source.probe.streams.find((stream) => stream.codec_type === 'video')?.avg_frame_rate,
realVideoRate: fixture.source.probe.streams.find((stream) => stream.codec_type === 'video')?.r_frame_rate,
audioChannels: fixture.source.probe.streams.filter((stream) => stream.codec_type === 'audio').map((stream) => stream.channels)
}))
}, null, 2));
return;
}
const launchOptions = getElectronLaunchOptions(environment);
launchOptions.env = {
...launchOptions.env,
TWITCH_VOD_MANAGER_E2E_CUTTER_OUTPUT_ROOT: environment.mediaDir
};
app = await electron.launch(launchOptions);
setApp(app);
const appProcessId = app.process().pid;
const win = await app.firstWindow();
const runtimeIssues = [];
win.on('pageerror', (error) => runtimeIssues.push(`pageerror: ${String(error)}`));
win.on('console', (message) => {
if (message.type() === 'error') runtimeIssues.push(`console.error: ${message.text()}`);
});
await verifyE2eIsolation(app, win, environment);
const productToolStatuses = await win.evaluate(() => window.api.getManagedToolStatus());
assertVerifiedManagedStatus(productToolStatuses.streamlink, provisionedTools.manifest.streamlink.version, 'Electron Streamlink');
assertVerifiedManagedStatus(productToolStatuses.ffmpeg, provisionedTools.manifest.ffmpeg.version, 'Electron FFmpeg');
const expectedElectronPath = path.join(environment.rootDir, 'offline-path');
const electronPath = getEnvironmentValue(launchOptions.env, 'PATH');
const streamlinkDirectory = path.join(environment.appDataDir, 'tools', 'streamlink');
const ffmpegDirectory = path.join(environment.appDataDir, 'tools', 'ffmpeg');
const verifyManagedExecution = async (label, previousDiagnostics = null, requiredTools = []) => {
const diagnostics = await readManagedExecutionDiagnostics(win);
assertManagedExecutionDiagnostics({
diagnostics,
expectedPaths: provisionedTools.paths,
streamlinkDirectory,
ffmpegDirectory,
electronPath,
expectedElectronPath,
previousDiagnostics,
requiredTools,
label
});
return diagnostics;
};
let executionDiagnostics = await verifyManagedExecution('Cutter startup');
const matrix = [];
for (const entry of fixtures.filter((fixture) => selectedDefinitions.includes(fixture.definition))) {
const prepared = await prepareSource(win, entry.source.filePath);
assertCondition(prepared.media !== null, `${entry.definition.name} source was rejected during production media preparation`);
assertCondition(prepared.media.info.videoCodec === entry.definition.sourceVideoCodec, `${entry.definition.name} production probe reported ${prepared.media.info.videoCodec}`);
assertCondition(prepared.media.info.audioStreams.length === entry.definition.audioFrequencies.length, `${entry.definition.name} production probe found ${prepared.media.info.audioStreams.length} audio streams`);
if (entry.definition.variableFrameRate) assertCondition(prepared.media.info.variableFrameRate, `${entry.definition.name} production probe did not report VFR`);
const prepareDiagnostics = await verifyManagedExecution(`${entry.definition.name} after prepare`, executionDiagnostics, ['ffprobe']);
executionDiagnostics = prepareDiagnostics;
const result = await exportSource(win, entry.definition, prepared);
assertCondition(result.success, `${entry.definition.name} production export failed: ${JSON.stringify(result)}`);
const exportDiagnostics = await verifyManagedExecution(`${entry.definition.name} after export`, executionDiagnostics, ['ffmpeg', 'ffprobe']);
executionDiagnostics = exportDiagnostics;
matrix.push({
case: entry.definition.name,
prepared: {
duration: prepared.media.info.duration,
fps: prepared.media.info.fps,
variableFrameRate: prepared.media.info.variableFrameRate,
audioStreams: prepared.media.info.audioStreams
},
result,
output: analyzeExport(environment, runtime, entry.definition, entry.source),
executionDiagnostics: { prepare: prepareDiagnostics, export: exportDiagnostics }
});
}
let lockedTarget = null;
let corruptSource = null;
if (includeErrors) {
const lockedDefinition = { ...definitions[0], outputName: 'matrix-locked-existing.mp4' };
const lockedSource = fixtures.find((fixture) => fixture.definition.id === definitions[0].id).source;
const lockedPrepared = await prepareSource(win, lockedSource.filePath);
assertCondition(lockedPrepared.media !== null, 'Locked target source could not be prepared');
executionDiagnostics = await verifyManagedExecution('Locked target after prepare', executionDiagnostics, ['ffprobe']);
lockedTarget = await runLockedTargetCase(win, environment, powerShellExecutable, lockedDefinition, { ...lockedSource, capability: lockedPrepared.capability }, runtimeIssues);
executionDiagnostics = await verifyManagedExecution('Locked target after export', executionDiagnostics, ['ffmpeg', 'ffprobe']);
corruptSource = await runCorruptSourceCase(win, environment);
await verifyManagedExecution('Corrupt source after export', executionDiagnostics, ['ffprobe']);
}
assertCondition(runtimeIssues.length === 0, `Cutter matrix runtime issues occurred: ${runtimeIssues.join(' | ')}`);
await closeApp();
const cutterTempPrefixes = ['media', 'waveform', 'preview'].map((kind) => `tvm-editor-${kind}-${appProcessId}-`);
const cutterTempDirectoriesAfterShutdown = fs.readdirSync(os.tmpdir()).filter((name) => cutterTempPrefixes.some((prefix) => name.startsWith(prefix)));
assertCondition(cutterTempDirectoriesAfterShutdown.length === 0, `Cutter matrix left temporary directories after shutdown: ${JSON.stringify(cutterTempDirectoriesAfterShutdown)}`);
console.log(JSON.stringify({
toolchain: {
statuses: provisionedTools.statuses,
versions: provisionedTools.versions,
paths: provisionedTools.paths
},
matrix,
lockedTarget,
corruptSource,
runtimeIssues,
cutterTempDirectoriesAfterShutdown
}, null, 2));
}
async function run() {
return await runCutterMatrixLifecycle({
createEnvironment: () => createE2eEnvironment('cutter-media-matrix', { language: 'en', theme: 'twitch' }),
cleanupEnvironment: cleanupE2eEnvironment,
closeApp: closeElectronApp,
execute: executeCutterMatrix
});
}
if (require.main === module) {
run().catch((error) => {
console.error(error);
process.exitCode = 1;
});
}
module.exports = {
activateOfflineRunnerEnvironment,
assertLockedTargetFailure,
assertManagedExecutionDiagnostics,
assertPinnedVersion,
assertTrimBoundaryMarkers,
closeElectronApp,
createManagedMediaRuntime,
estimatePcmFrequency,
exportSource,
prepareSource,
provisionManagedCutterTools,
runCutterMatrixLifecycle,
sampleVideoRgb
};