@hyperframes/engine 0.7.59 → 0.7.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/dist/config.d.ts +95 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +131 -16
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/services/audioMixer.d.ts.map +1 -1
- package/dist/services/audioMixer.js +38 -3
- package/dist/services/audioMixer.js.map +1 -1
- package/dist/services/browserLeasePool.d.ts +48 -0
- package/dist/services/browserLeasePool.d.ts.map +1 -0
- package/dist/services/browserLeasePool.js +210 -0
- package/dist/services/browserLeasePool.js.map +1 -0
- package/dist/services/browserManager.d.ts +6 -7
- package/dist/services/browserManager.d.ts.map +1 -1
- package/dist/services/browserManager.js +134 -177
- package/dist/services/browserManager.js.map +1 -1
- package/dist/services/chunkEncoder.d.ts.map +1 -1
- package/dist/services/chunkEncoder.js +45 -134
- package/dist/services/chunkEncoder.js.map +1 -1
- package/dist/services/frameCapture.d.ts +38 -1
- package/dist/services/frameCapture.d.ts.map +1 -1
- package/dist/services/frameCapture.js +119 -8
- package/dist/services/frameCapture.js.map +1 -1
- package/dist/services/pageNavigationTimeoutErrorHint.d.ts +79 -0
- package/dist/services/pageNavigationTimeoutErrorHint.d.ts.map +1 -0
- package/dist/services/pageNavigationTimeoutErrorHint.js +112 -0
- package/dist/services/pageNavigationTimeoutErrorHint.js.map +1 -0
- package/dist/services/parallelCoordinator.d.ts +21 -0
- package/dist/services/parallelCoordinator.d.ts.map +1 -1
- package/dist/services/parallelCoordinator.js +40 -1
- package/dist/services/parallelCoordinator.js.map +1 -1
- package/dist/services/protocolTimeoutErrorHint.d.ts +28 -0
- package/dist/services/protocolTimeoutErrorHint.d.ts.map +1 -0
- package/dist/services/protocolTimeoutErrorHint.js +45 -0
- package/dist/services/protocolTimeoutErrorHint.js.map +1 -0
- package/dist/services/streamingEncoder.d.ts.map +1 -1
- package/dist/services/streamingEncoder.js +23 -58
- package/dist/services/streamingEncoder.js.map +1 -1
- package/dist/services/systemMemory.d.ts.map +1 -1
- package/dist/services/systemMemory.js +4 -1
- package/dist/services/systemMemory.js.map +1 -1
- package/dist/services/videoFrameExtractor.d.ts +4 -5
- package/dist/services/videoFrameExtractor.d.ts.map +1 -1
- package/dist/services/videoFrameExtractor.js +57 -112
- package/dist/services/videoFrameExtractor.js.map +1 -1
- package/dist/types.d.ts +28 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/ffmpegBinaries.d.ts +2 -2
- package/dist/utils/ffmpegBinaries.d.ts.map +1 -1
- package/dist/utils/ffmpegBinaries.js +8 -82
- package/dist/utils/ffmpegBinaries.js.map +1 -1
- package/dist/utils/ffprobe.d.ts +3 -1
- package/dist/utils/ffprobe.d.ts.map +1 -1
- package/dist/utils/ffprobe.js +35 -42
- package/dist/utils/ffprobe.js.map +1 -1
- package/dist/utils/gpuEncoder.d.ts.map +1 -1
- package/dist/utils/gpuEncoder.js +32 -52
- package/dist/utils/gpuEncoder.js.map +1 -1
- package/dist/utils/gpuParityDiff.d.ts +142 -0
- package/dist/utils/gpuParityDiff.d.ts.map +1 -0
- package/dist/utils/gpuParityDiff.js +205 -0
- package/dist/utils/gpuParityDiff.js.map +1 -0
- package/dist/utils/managedChildProcess.d.ts +50 -0
- package/dist/utils/managedChildProcess.d.ts.map +1 -0
- package/dist/utils/managedChildProcess.js +148 -0
- package/dist/utils/managedChildProcess.js.map +1 -0
- package/dist/utils/processTracker.js +1 -1
- package/dist/utils/runFfmpeg.d.ts +3 -0
- package/dist/utils/runFfmpeg.d.ts.map +1 -1
- package/dist/utils/runFfmpeg.js +16 -50
- package/dist/utils/runFfmpeg.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GPU Parity Diff — diagnostic helpers for detecting shape-dependent
|
|
3
|
+
* hardware-GPU capture bugs by comparing frames captured via the two
|
|
4
|
+
* capture paths (hardware-GPU vs software-GPU / screenshot bypass).
|
|
5
|
+
*
|
|
6
|
+
* Field pattern this exists to catch:
|
|
7
|
+
*
|
|
8
|
+
* • ts=1784049136 · hardware-GPU capture emitted intermittent black
|
|
9
|
+
* rectangles in one scene; `--no-browser-gpu --low-memory-mode
|
|
10
|
+
* --workers 1` (software-GPU screenshot mode) resolved it.
|
|
11
|
+
* • ts=1784032286 · animated `clip-path` on a large image → intermittent
|
|
12
|
+
* black rectangles from Chrome capture; deterministic precomposited
|
|
13
|
+
* swipe states removed the artifact.
|
|
14
|
+
* • Baseline · JetBrains Mono glyph-drop on parallel/BeginFrame text
|
|
15
|
+
* capture.
|
|
16
|
+
*
|
|
17
|
+
* Common shape: hardware-GPU writes solid-black regions where content
|
|
18
|
+
* should exist; software-GPU / screenshot fallback renders the same
|
|
19
|
+
* region correctly. A raw per-pixel diff is a coarse signal for this —
|
|
20
|
+
* on its own it fires on every animation frame's compositor jitter, so
|
|
21
|
+
* this module also isolates the *asymmetric* black-in-A-only pattern
|
|
22
|
+
* that is the diagnostic fingerprint of the bug.
|
|
23
|
+
*
|
|
24
|
+
* This module is the reduced-scope first pass: a pure helper + unit
|
|
25
|
+
* tests. Wiring a `hyperframes verify-gpu-parity` CLI surface, capture
|
|
26
|
+
* orchestration, and integration coverage is intentionally deferred to
|
|
27
|
+
* a follow-up so the diagnostic primitive can land and be exercised
|
|
28
|
+
* against real captured frames in isolation.
|
|
29
|
+
*/
|
|
30
|
+
import { decodePng } from "./alphaBlit.js";
|
|
31
|
+
// ── Defaults ────────────────────────────────────────────────────────────────
|
|
32
|
+
const DEFAULT_PIXEL_CHANNEL_TOLERANCE = 8;
|
|
33
|
+
const DEFAULT_BLACK_SUM_THRESHOLD = 12;
|
|
34
|
+
const DEFAULT_CONTENT_SUM_THRESHOLD = 32;
|
|
35
|
+
const DEFAULT_BLACK_ONLY_FRACTION_THRESHOLD = 0.001;
|
|
36
|
+
// ── Core diff ───────────────────────────────────────────────────────────────
|
|
37
|
+
/**
|
|
38
|
+
* Compare two RGBA frames captured via different GPU paths.
|
|
39
|
+
*
|
|
40
|
+
* The two inputs must have identical dimensions; on mismatch this throws
|
|
41
|
+
* (with the mismatched sizes surfaced in the message) so callers cannot
|
|
42
|
+
* silently compare misaligned frames.
|
|
43
|
+
*
|
|
44
|
+
* Alpha channel is *not* considered for the black-only detection — Chrome's
|
|
45
|
+
* capture output is opaque along the code paths this diagnostic targets,
|
|
46
|
+
* and considering alpha would false-positive on legitimately transparent
|
|
47
|
+
* pixels. `pixelChannelTolerance` also compares only R/G/B for the same
|
|
48
|
+
* reason.
|
|
49
|
+
*/
|
|
50
|
+
export function diffGpuParityFrames(a, b, options = {}) {
|
|
51
|
+
if (a.width !== b.width || a.height !== b.height) {
|
|
52
|
+
throw new Error(`diffGpuParityFrames: frame size mismatch — A is ${a.width}x${a.height}, ` +
|
|
53
|
+
`B is ${b.width}x${b.height}`);
|
|
54
|
+
}
|
|
55
|
+
const expectedLen = a.width * a.height * 4;
|
|
56
|
+
if (a.data.length !== expectedLen) {
|
|
57
|
+
throw new Error(`diffGpuParityFrames: frame A data length ${a.data.length} does not match ` +
|
|
58
|
+
`expected ${expectedLen} (${a.width}x${a.height} * 4)`);
|
|
59
|
+
}
|
|
60
|
+
if (b.data.length !== expectedLen) {
|
|
61
|
+
throw new Error(`diffGpuParityFrames: frame B data length ${b.data.length} does not match ` +
|
|
62
|
+
`expected ${expectedLen} (${b.width}x${b.height} * 4)`);
|
|
63
|
+
}
|
|
64
|
+
const tol = options.pixelChannelTolerance ?? DEFAULT_PIXEL_CHANNEL_TOLERANCE;
|
|
65
|
+
const blackSum = options.blackSumThreshold ?? DEFAULT_BLACK_SUM_THRESHOLD;
|
|
66
|
+
const contentSum = options.contentSumThreshold ?? DEFAULT_CONTENT_SUM_THRESHOLD;
|
|
67
|
+
if (contentSum <= blackSum) {
|
|
68
|
+
throw new Error(`diffGpuParityFrames: contentSumThreshold (${contentSum}) must be strictly greater ` +
|
|
69
|
+
`than blackSumThreshold (${blackSum}) — overlapping thresholds would double-classify ` +
|
|
70
|
+
`borderline pixels`);
|
|
71
|
+
}
|
|
72
|
+
const total = a.width * a.height;
|
|
73
|
+
const aData = a.data;
|
|
74
|
+
const bData = b.data;
|
|
75
|
+
let diffPixels = 0;
|
|
76
|
+
let blackOnlyAPixels = 0;
|
|
77
|
+
let blackOnlyBPixels = 0;
|
|
78
|
+
let aMinX = a.width;
|
|
79
|
+
let aMinY = a.height;
|
|
80
|
+
let aMaxX = -1;
|
|
81
|
+
let aMaxY = -1;
|
|
82
|
+
let bMinX = a.width;
|
|
83
|
+
let bMinY = a.height;
|
|
84
|
+
let bMaxX = -1;
|
|
85
|
+
let bMaxY = -1;
|
|
86
|
+
for (let y = 0; y < a.height; y++) {
|
|
87
|
+
for (let x = 0; x < a.width; x++) {
|
|
88
|
+
const i = (y * a.width + x) * 4;
|
|
89
|
+
const ar = aData[i] ?? 0;
|
|
90
|
+
const ag = aData[i + 1] ?? 0;
|
|
91
|
+
const ab = aData[i + 2] ?? 0;
|
|
92
|
+
const br = bData[i] ?? 0;
|
|
93
|
+
const bg = bData[i + 1] ?? 0;
|
|
94
|
+
const bb = bData[i + 2] ?? 0;
|
|
95
|
+
if (Math.abs(ar - br) > tol || Math.abs(ag - bg) > tol || Math.abs(ab - bb) > tol) {
|
|
96
|
+
diffPixels++;
|
|
97
|
+
}
|
|
98
|
+
const aSum = ar + ag + ab;
|
|
99
|
+
const bSum = br + bg + bb;
|
|
100
|
+
if (aSum <= blackSum && bSum >= contentSum) {
|
|
101
|
+
blackOnlyAPixels++;
|
|
102
|
+
if (x < aMinX)
|
|
103
|
+
aMinX = x;
|
|
104
|
+
if (y < aMinY)
|
|
105
|
+
aMinY = y;
|
|
106
|
+
if (x > aMaxX)
|
|
107
|
+
aMaxX = x;
|
|
108
|
+
if (y > aMaxY)
|
|
109
|
+
aMaxY = y;
|
|
110
|
+
}
|
|
111
|
+
else if (bSum <= blackSum && aSum >= contentSum) {
|
|
112
|
+
blackOnlyBPixels++;
|
|
113
|
+
if (x < bMinX)
|
|
114
|
+
bMinX = x;
|
|
115
|
+
if (y < bMinY)
|
|
116
|
+
bMinY = y;
|
|
117
|
+
if (x > bMaxX)
|
|
118
|
+
bMaxX = x;
|
|
119
|
+
if (y > bMaxY)
|
|
120
|
+
bMaxY = y;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
width: a.width,
|
|
126
|
+
height: a.height,
|
|
127
|
+
totalPixels: total,
|
|
128
|
+
diffPixels,
|
|
129
|
+
diffFraction: diffPixels / total,
|
|
130
|
+
blackOnlyInA: {
|
|
131
|
+
pixels: blackOnlyAPixels,
|
|
132
|
+
fraction: blackOnlyAPixels / total,
|
|
133
|
+
boundingBox: blackOnlyAPixels === 0
|
|
134
|
+
? null
|
|
135
|
+
: { x: aMinX, y: aMinY, width: aMaxX - aMinX + 1, height: aMaxY - aMinY + 1 },
|
|
136
|
+
},
|
|
137
|
+
blackOnlyInB: {
|
|
138
|
+
pixels: blackOnlyBPixels,
|
|
139
|
+
fraction: blackOnlyBPixels / total,
|
|
140
|
+
boundingBox: blackOnlyBPixels === 0
|
|
141
|
+
? null
|
|
142
|
+
: { x: bMinX, y: bMinY, width: bMaxX - bMinX + 1, height: bMaxY - bMinY + 1 },
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* PNG-buffer convenience wrapper around `diffGpuParityFrames`. Decodes
|
|
148
|
+
* both inputs via `decodePng` and delegates. Errors from the decoder are
|
|
149
|
+
* re-thrown with `cause` preserved so the failing side (A or B) is
|
|
150
|
+
* obvious in the stack.
|
|
151
|
+
*/
|
|
152
|
+
export function diffGpuParityPngs(pngA, pngB, options = {}) {
|
|
153
|
+
let a;
|
|
154
|
+
let b;
|
|
155
|
+
try {
|
|
156
|
+
a = decodePng(pngA);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
throw new Error(`diffGpuParityPngs: failed to decode frame A`, { cause: err });
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
b = decodePng(pngB);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
throw new Error(`diffGpuParityPngs: failed to decode frame B`, { cause: err });
|
|
166
|
+
}
|
|
167
|
+
return diffGpuParityFrames(a, b, options);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Verdict wrapper — returns `{ ok, reason }` suitable for a follow-up
|
|
171
|
+
* CLI or gate to emit. `ok === false` when either black-only fraction
|
|
172
|
+
* exceeds `blackOnlyFractionThreshold`. Raw diff numbers are always
|
|
173
|
+
* included on both branches so callers can log or gate on their own
|
|
174
|
+
* metrics without a second pass.
|
|
175
|
+
*/
|
|
176
|
+
export function verifyGpuParity(a, b, options = {}) {
|
|
177
|
+
const diff = diffGpuParityFrames(a, b, options);
|
|
178
|
+
const threshold = options.blackOnlyFractionThreshold ?? DEFAULT_BLACK_ONLY_FRACTION_THRESHOLD;
|
|
179
|
+
if (diff.blackOnlyInA.fraction > threshold) {
|
|
180
|
+
const bb = diff.blackOnlyInA.boundingBox;
|
|
181
|
+
const region = bb ? ` (bbox ${bb.x},${bb.y} ${bb.width}x${bb.height})` : "";
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
reason: `hardware-GPU frame has ${diff.blackOnlyInA.pixels} pixel(s) ` +
|
|
185
|
+
`(${(diff.blackOnlyInA.fraction * 100).toFixed(3)}%) that are solid-black ` +
|
|
186
|
+
`while software-GPU frame has content there${region} — likely shape-dependent ` +
|
|
187
|
+
`hardware-GPU capture bug`,
|
|
188
|
+
diff,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (diff.blackOnlyInB.fraction > threshold) {
|
|
192
|
+
const bb = diff.blackOnlyInB.boundingBox;
|
|
193
|
+
const region = bb ? ` (bbox ${bb.x},${bb.y} ${bb.width}x${bb.height})` : "";
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
reason: `software-GPU frame has ${diff.blackOnlyInB.pixels} pixel(s) ` +
|
|
197
|
+
`(${(diff.blackOnlyInB.fraction * 100).toFixed(3)}%) that are solid-black ` +
|
|
198
|
+
`while hardware-GPU frame has content there${region} — unexpected inverse ` +
|
|
199
|
+
`pattern, worth investigating`,
|
|
200
|
+
diff,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return { ok: true, reason: "", diff };
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=gpuParityDiff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gpuParityDiff.js","sourceRoot":"","sources":["../../src/utils/gpuParityDiff.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
export type ManagedProcessTerminationReason = "exit" | "abort" | "deadline" | "inactivity" | "spawn_error";
|
|
3
|
+
export interface ManagedChildProcessOutcome {
|
|
4
|
+
reason: ManagedProcessTerminationReason;
|
|
5
|
+
exitCode: number | null;
|
|
6
|
+
signal: NodeJS.Signals | null;
|
|
7
|
+
stderr: string;
|
|
8
|
+
durationMs: number;
|
|
9
|
+
error?: Error;
|
|
10
|
+
}
|
|
11
|
+
export interface ManagedChildProcessOptions {
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
deadlineAtMs?: number;
|
|
14
|
+
inactivityTimeoutMs?: number;
|
|
15
|
+
terminationGraceMs?: number;
|
|
16
|
+
stderrMaxBytes?: number;
|
|
17
|
+
onStderr?: (chunk: string) => void;
|
|
18
|
+
now?: () => number;
|
|
19
|
+
}
|
|
20
|
+
/** Owns cancellation, escalation, stderr and reaping for one child process. */
|
|
21
|
+
export declare class ManagedChildProcess {
|
|
22
|
+
readonly child: ChildProcess;
|
|
23
|
+
private readonly options;
|
|
24
|
+
private readonly startedAtMs;
|
|
25
|
+
private readonly now;
|
|
26
|
+
private readonly outcomePromise;
|
|
27
|
+
private resolveOutcome;
|
|
28
|
+
private requestedReason;
|
|
29
|
+
private stderrTail;
|
|
30
|
+
private spawned;
|
|
31
|
+
private settled;
|
|
32
|
+
private deadlineTimer;
|
|
33
|
+
private inactivityTimer;
|
|
34
|
+
private escalationTimer;
|
|
35
|
+
constructor(child: ChildProcess, options?: ManagedChildProcessOptions);
|
|
36
|
+
wait(): Promise<ManagedChildProcessOutcome>;
|
|
37
|
+
get isSettled(): boolean;
|
|
38
|
+
markActivity(): void;
|
|
39
|
+
private readonly onStderr;
|
|
40
|
+
private readonly onSpawn;
|
|
41
|
+
private readonly onClose;
|
|
42
|
+
private readonly onError;
|
|
43
|
+
private installAbort;
|
|
44
|
+
private readonly onAbort;
|
|
45
|
+
private installDeadline;
|
|
46
|
+
private requestTermination;
|
|
47
|
+
private settle;
|
|
48
|
+
private clearTimers;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=managedChildProcess.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"managedChildProcess.d.ts","sourceRoot":"","sources":["../../src/utils/managedChildProcess.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,MAAM,+BAA+B,GACvC,MAAM,GACN,OAAO,GACP,UAAU,GACV,YAAY,GACZ,aAAa,CAAC;AAElB,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,+BAA+B,CAAC;IACxC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAKD,+EAA+E;AAC/E,qBAAa,mBAAmB;IAe5B,QAAQ,CAAC,KAAK,EAAE,YAAY;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAf1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsC;IACrE,OAAO,CAAC,cAAc,CAAiD;IACvE,OAAO,CAAC,eAAe,CAChB;IACP,OAAO,CAAC,UAAU,CAAmB;IACrC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAA+B;IACpD,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,eAAe,CAA+B;gBAG3C,KAAK,EAAE,YAAY,EACX,OAAO,GAAE,0BAA+B;IAiB3D,IAAI,IAAI,OAAO,CAAC,0BAA0B,CAAC;IAI3C,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,YAAY,IAAI,IAAI;IAUpB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAQvB;IAEF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAEtB;IAEF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAQtB;IAEF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAUtB;IAEF,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAEtB;IAEF,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,kBAAkB;IAyB1B,OAAO,CAAC,MAAM;IAYd,OAAO,CAAC,WAAW;CAQpB"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const DEFAULT_TERMINATION_GRACE_MS = 2_000;
|
|
2
|
+
const DEFAULT_STDERR_MAX_BYTES = 64 * 1024;
|
|
3
|
+
/** Owns cancellation, escalation, stderr and reaping for one child process. */
|
|
4
|
+
export class ManagedChildProcess {
|
|
5
|
+
child;
|
|
6
|
+
options;
|
|
7
|
+
startedAtMs;
|
|
8
|
+
now;
|
|
9
|
+
outcomePromise;
|
|
10
|
+
resolveOutcome;
|
|
11
|
+
requestedReason = null;
|
|
12
|
+
stderrTail = Buffer.alloc(0);
|
|
13
|
+
spawned = false;
|
|
14
|
+
settled = false;
|
|
15
|
+
deadlineTimer = null;
|
|
16
|
+
inactivityTimer = null;
|
|
17
|
+
escalationTimer = null;
|
|
18
|
+
constructor(child, options = {}) {
|
|
19
|
+
this.child = child;
|
|
20
|
+
this.options = options;
|
|
21
|
+
this.now = options.now ?? Date.now;
|
|
22
|
+
this.startedAtMs = this.now();
|
|
23
|
+
this.outcomePromise = new Promise((resolve) => {
|
|
24
|
+
this.resolveOutcome = resolve;
|
|
25
|
+
});
|
|
26
|
+
child.stderr?.on("data", this.onStderr);
|
|
27
|
+
child.once("spawn", this.onSpawn);
|
|
28
|
+
child.once("close", this.onClose);
|
|
29
|
+
child.on("error", this.onError);
|
|
30
|
+
this.installAbort();
|
|
31
|
+
this.installDeadline();
|
|
32
|
+
this.markActivity();
|
|
33
|
+
}
|
|
34
|
+
wait() {
|
|
35
|
+
return this.outcomePromise;
|
|
36
|
+
}
|
|
37
|
+
get isSettled() {
|
|
38
|
+
return this.settled;
|
|
39
|
+
}
|
|
40
|
+
markActivity() {
|
|
41
|
+
if (this.settled || this.options.inactivityTimeoutMs === undefined)
|
|
42
|
+
return;
|
|
43
|
+
if (this.inactivityTimer)
|
|
44
|
+
clearTimeout(this.inactivityTimer);
|
|
45
|
+
this.inactivityTimer = setTimeout(() => this.requestTermination("inactivity"), Math.max(0, this.options.inactivityTimeoutMs));
|
|
46
|
+
this.inactivityTimer.unref?.();
|
|
47
|
+
}
|
|
48
|
+
onStderr = (data) => {
|
|
49
|
+
const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
50
|
+
const maxBytes = this.options.stderrMaxBytes ?? DEFAULT_STDERR_MAX_BYTES;
|
|
51
|
+
this.stderrTail = Buffer.concat([this.stderrTail, chunk]);
|
|
52
|
+
if (this.stderrTail.byteLength > maxBytes) {
|
|
53
|
+
this.stderrTail = this.stderrTail.subarray(this.stderrTail.byteLength - maxBytes);
|
|
54
|
+
}
|
|
55
|
+
this.options.onStderr?.(chunk.toString());
|
|
56
|
+
};
|
|
57
|
+
onSpawn = () => {
|
|
58
|
+
this.spawned = true;
|
|
59
|
+
};
|
|
60
|
+
onClose = (exitCode, signal) => {
|
|
61
|
+
this.settle({
|
|
62
|
+
reason: this.requestedReason ?? "exit",
|
|
63
|
+
exitCode,
|
|
64
|
+
signal,
|
|
65
|
+
stderr: this.stderrTail.toString(),
|
|
66
|
+
durationMs: this.now() - this.startedAtMs,
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
onError = (error) => {
|
|
70
|
+
if (this.spawned)
|
|
71
|
+
return;
|
|
72
|
+
this.settle({
|
|
73
|
+
reason: "spawn_error",
|
|
74
|
+
exitCode: null,
|
|
75
|
+
signal: null,
|
|
76
|
+
stderr: this.stderrTail.length > 0 ? this.stderrTail.toString() : error.message,
|
|
77
|
+
durationMs: this.now() - this.startedAtMs,
|
|
78
|
+
error,
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
installAbort() {
|
|
82
|
+
const signal = this.options.signal;
|
|
83
|
+
if (!signal)
|
|
84
|
+
return;
|
|
85
|
+
if (signal.aborted) {
|
|
86
|
+
this.requestTermination("abort");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
signal.addEventListener("abort", this.onAbort, { once: true });
|
|
90
|
+
}
|
|
91
|
+
onAbort = () => {
|
|
92
|
+
this.requestTermination("abort");
|
|
93
|
+
};
|
|
94
|
+
installDeadline() {
|
|
95
|
+
if (this.options.deadlineAtMs === undefined)
|
|
96
|
+
return;
|
|
97
|
+
const remainingMs = Math.max(0, this.options.deadlineAtMs - this.now());
|
|
98
|
+
this.deadlineTimer = setTimeout(() => this.requestTermination("deadline"), remainingMs);
|
|
99
|
+
this.deadlineTimer.unref?.();
|
|
100
|
+
}
|
|
101
|
+
requestTermination(reason) {
|
|
102
|
+
if (this.settled || this.requestedReason)
|
|
103
|
+
return;
|
|
104
|
+
this.requestedReason = reason;
|
|
105
|
+
try {
|
|
106
|
+
this.child.kill("SIGTERM");
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// A close/error event owns settlement; escalation remains the backstop.
|
|
110
|
+
}
|
|
111
|
+
const graceMs = this.options.terminationGraceMs ?? DEFAULT_TERMINATION_GRACE_MS;
|
|
112
|
+
this.escalationTimer = setTimeout(() => {
|
|
113
|
+
if (this.settled)
|
|
114
|
+
return;
|
|
115
|
+
try {
|
|
116
|
+
this.child.kill("SIGKILL");
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// The child may have exited between the settled check and kill.
|
|
120
|
+
}
|
|
121
|
+
}, Math.max(0, graceMs));
|
|
122
|
+
this.escalationTimer.unref?.();
|
|
123
|
+
}
|
|
124
|
+
settle(outcome) {
|
|
125
|
+
if (this.settled)
|
|
126
|
+
return;
|
|
127
|
+
this.settled = true;
|
|
128
|
+
this.clearTimers();
|
|
129
|
+
this.options.signal?.removeEventListener("abort", this.onAbort);
|
|
130
|
+
this.child.stderr?.off("data", this.onStderr);
|
|
131
|
+
this.child.off("spawn", this.onSpawn);
|
|
132
|
+
this.child.off("close", this.onClose);
|
|
133
|
+
this.child.off("error", this.onError);
|
|
134
|
+
this.resolveOutcome(outcome);
|
|
135
|
+
}
|
|
136
|
+
clearTimers() {
|
|
137
|
+
if (this.deadlineTimer)
|
|
138
|
+
clearTimeout(this.deadlineTimer);
|
|
139
|
+
if (this.inactivityTimer)
|
|
140
|
+
clearTimeout(this.inactivityTimer);
|
|
141
|
+
if (this.escalationTimer)
|
|
142
|
+
clearTimeout(this.escalationTimer);
|
|
143
|
+
this.deadlineTimer = null;
|
|
144
|
+
this.inactivityTimer = null;
|
|
145
|
+
this.escalationTimer = null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=managedChildProcess.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"managedChildProcess.js","sourceRoot":"","sources":["../../src/utils/managedChildProcess.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ export function trackChildProcess(proc) {
|
|
|
3
3
|
tracked.add(proc);
|
|
4
4
|
const remove = () => tracked.delete(proc);
|
|
5
5
|
proc.once("exit", remove);
|
|
6
|
-
proc.once("
|
|
6
|
+
proc.once("close", remove);
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* SIGTERM all tracked child processes, then SIGKILL any that survive
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Extracts the repeated spawn-stderr-timeout-abort-close-error pattern
|
|
5
5
|
* that appears across audioMixer and chunkEncoder into a single helper.
|
|
6
6
|
*/
|
|
7
|
+
import { type ManagedProcessTerminationReason } from "./managedChildProcess.js";
|
|
7
8
|
export interface RunFfmpegOptions {
|
|
8
9
|
signal?: AbortSignal;
|
|
9
10
|
timeout?: number;
|
|
@@ -14,6 +15,8 @@ export interface RunFfmpegResult {
|
|
|
14
15
|
exitCode: number | null;
|
|
15
16
|
stderr: string;
|
|
16
17
|
durationMs: number;
|
|
18
|
+
terminationReason: ManagedProcessTerminationReason;
|
|
19
|
+
error?: Error;
|
|
17
20
|
}
|
|
18
21
|
/**
|
|
19
22
|
* Build a user-facing error message for a failed ffmpeg invocation.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runFfmpeg.d.ts","sourceRoot":"","sources":["../../src/utils/runFfmpeg.ts"],"names":[],"mappings":"AACA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"runFfmpeg.d.ts","sourceRoot":"","sources":["../../src/utils/runFfmpeg.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AAKH,OAAO,EAEL,KAAK,+BAA+B,EACrC,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,+BAA+B,CAAC;IACnD,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AA+BD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,SAAS,GAAE,MAAkC,GAC5C,MAAM,CAgBR;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkBjG"}
|
package/dist/utils/runFfmpeg.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { spawn } from "child_process";
|
|
9
9
|
import { getFfmpegBinary } from "./ffmpegBinaries.js";
|
|
10
10
|
import { trackChildProcess } from "./processTracker.js";
|
|
11
|
+
import { ManagedChildProcess, } from "./managedChildProcess.js";
|
|
11
12
|
const DEFAULT_TIMEOUT = 300_000;
|
|
12
13
|
const DEFAULT_STDERR_TAIL_LINES = 15;
|
|
13
14
|
function formatWindowsFfmpegExit(exitCode) {
|
|
@@ -57,57 +58,22 @@ export function formatFfmpegError(exitCode, stderr, tailLines = DEFAULT_STDERR_T
|
|
|
57
58
|
: `FFmpeg exited with code ${exitCode}`;
|
|
58
59
|
}
|
|
59
60
|
export async function runFfmpeg(args, opts) {
|
|
60
|
-
const startMs = Date.now();
|
|
61
|
-
const signal = opts?.signal;
|
|
62
61
|
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
ffmpeg.kill("SIGTERM");
|
|
70
|
-
};
|
|
71
|
-
if (signal) {
|
|
72
|
-
if (signal.aborted) {
|
|
73
|
-
ffmpeg.kill("SIGTERM");
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
const timer = setTimeout(() => {
|
|
80
|
-
ffmpeg.kill("SIGTERM");
|
|
81
|
-
}, timeout);
|
|
82
|
-
ffmpeg.stderr.on("data", (data) => {
|
|
83
|
-
const chunk = data.toString();
|
|
84
|
-
stderr += chunk;
|
|
85
|
-
if (onStderr) {
|
|
86
|
-
onStderr(chunk);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
ffmpeg.on("close", (code) => {
|
|
90
|
-
clearTimeout(timer);
|
|
91
|
-
if (signal)
|
|
92
|
-
signal.removeEventListener("abort", onAbort);
|
|
93
|
-
resolve({
|
|
94
|
-
success: !signal?.aborted && code === 0,
|
|
95
|
-
exitCode: code,
|
|
96
|
-
stderr,
|
|
97
|
-
durationMs: Date.now() - startMs,
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
ffmpeg.on("error", (err) => {
|
|
101
|
-
clearTimeout(timer);
|
|
102
|
-
if (signal)
|
|
103
|
-
signal.removeEventListener("abort", onAbort);
|
|
104
|
-
resolve({
|
|
105
|
-
success: false,
|
|
106
|
-
exitCode: null,
|
|
107
|
-
stderr: err.message,
|
|
108
|
-
durationMs: Date.now() - startMs,
|
|
109
|
-
});
|
|
110
|
-
});
|
|
62
|
+
const ffmpeg = spawn(getFfmpegBinary(), args);
|
|
63
|
+
trackChildProcess(ffmpeg);
|
|
64
|
+
const managed = new ManagedChildProcess(ffmpeg, {
|
|
65
|
+
signal: opts?.signal,
|
|
66
|
+
deadlineAtMs: Date.now() + timeout,
|
|
67
|
+
onStderr: opts?.onStderr,
|
|
111
68
|
});
|
|
69
|
+
const outcome = await managed.wait();
|
|
70
|
+
return {
|
|
71
|
+
success: outcome.reason === "exit" && outcome.exitCode === 0,
|
|
72
|
+
exitCode: outcome.exitCode,
|
|
73
|
+
stderr: outcome.stderr,
|
|
74
|
+
durationMs: outcome.durationMs,
|
|
75
|
+
terminationReason: outcome.reason,
|
|
76
|
+
error: outcome.error,
|
|
77
|
+
};
|
|
112
78
|
}
|
|
113
79
|
//# sourceMappingURL=runFfmpeg.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runFfmpeg.js","sourceRoot":"","sources":["../../src/utils/runFfmpeg.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC;;;;;GAKG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"runFfmpeg.js","sourceRoot":"","sources":["../../src/utils/runFfmpeg.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/engine",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.61",
|
|
4
4
|
"description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"linkedom": "^0.18.12",
|
|
39
39
|
"puppeteer": "^25.2.1",
|
|
40
40
|
"puppeteer-core": "^25.2.1",
|
|
41
|
-
"@hyperframes/core": "^0.7.
|
|
41
|
+
"@hyperframes/core": "^0.7.61",
|
|
42
|
+
"@hyperframes/parsers": "^0.7.61"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/node": "^25.0.10",
|