@hyperframes/engine 0.6.118 → 0.6.120
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/package.json +24 -7
- package/scripts/generate-lut-reference.py +0 -168
- package/scripts/test-fitTextFontSize-browser.ts +0 -135
- package/src/cdp-headless-experimental.d.ts +0 -54
- package/src/config.test.ts +0 -213
- package/src/config.ts +0 -417
- package/src/index.ts +0 -273
- package/src/services/audioMixer.test.ts +0 -326
- package/src/services/audioMixer.ts +0 -604
- package/src/services/audioMixer.types.ts +0 -35
- package/src/services/audioVolumeEnvelope.test.ts +0 -176
- package/src/services/audioVolumeEnvelope.ts +0 -138
- package/src/services/browserManager.test.ts +0 -330
- package/src/services/browserManager.ts +0 -670
- package/src/services/chunkEncoder.test.ts +0 -1415
- package/src/services/chunkEncoder.ts +0 -831
- package/src/services/chunkEncoder.types.ts +0 -60
- package/src/services/extractionCache.test.ts +0 -199
- package/src/services/extractionCache.ts +0 -216
- package/src/services/fileServer.ts +0 -110
- package/src/services/frameCapture-discardWarmup.test.ts +0 -183
- package/src/services/frameCapture-namePolyfill.test.ts +0 -78
- package/src/services/frameCapture-pollImagesReady.test.ts +0 -153
- package/src/services/frameCapture-staticDedupIndex.test.ts +0 -76
- package/src/services/frameCapture-warmupTicks.test.ts +0 -174
- package/src/services/frameCapture.test.ts +0 -192
- package/src/services/frameCapture.ts +0 -1934
- package/src/services/hdrCapture.test.ts +0 -159
- package/src/services/hdrCapture.ts +0 -315
- package/src/services/parallelCoordinator.test.ts +0 -139
- package/src/services/parallelCoordinator.ts +0 -437
- package/src/services/screenshotService.test.ts +0 -510
- package/src/services/screenshotService.ts +0 -615
- package/src/services/streamingEncoder.test.ts +0 -832
- package/src/services/streamingEncoder.ts +0 -594
- package/src/services/systemMemory.test.ts +0 -324
- package/src/services/systemMemory.ts +0 -180
- package/src/services/videoFrameExtractor.test.ts +0 -1062
- package/src/services/videoFrameExtractor.ts +0 -1139
- package/src/services/videoFrameInjector.test.ts +0 -300
- package/src/services/videoFrameInjector.ts +0 -687
- package/src/services/vp9Options.ts +0 -13
- package/src/types.ts +0 -191
- package/src/utils/alphaBlit.test.ts +0 -1349
- package/src/utils/alphaBlit.ts +0 -1015
- package/src/utils/assertSwiftShader.test.ts +0 -130
- package/src/utils/assertSwiftShader.ts +0 -126
- package/src/utils/ffmpegBinaries.test.ts +0 -43
- package/src/utils/ffmpegBinaries.ts +0 -63
- package/src/utils/ffprobe.test.ts +0 -342
- package/src/utils/ffprobe.ts +0 -457
- package/src/utils/gpuEncoder.test.ts +0 -140
- package/src/utils/gpuEncoder.ts +0 -268
- package/src/utils/hdr.test.ts +0 -191
- package/src/utils/hdr.ts +0 -137
- package/src/utils/hdrCompositing.test.ts +0 -130
- package/src/utils/htmlTemplate.test.ts +0 -42
- package/src/utils/htmlTemplate.ts +0 -42
- package/src/utils/layerCompositor.test.ts +0 -150
- package/src/utils/layerCompositor.ts +0 -58
- package/src/utils/parityContract.ts +0 -1
- package/src/utils/processTracker.test.ts +0 -74
- package/src/utils/processTracker.ts +0 -41
- package/src/utils/readWebGlVendorInfoFromCanvas.ts +0 -52
- package/src/utils/runFfmpeg.test.ts +0 -102
- package/src/utils/runFfmpeg.ts +0 -136
- package/src/utils/shaderTransitions.test.ts +0 -738
- package/src/utils/shaderTransitions.ts +0 -1130
- package/src/utils/uint16-alignment-audit.test.ts +0 -125
- package/src/utils/urlDownloader.test.ts +0 -65
- package/src/utils/urlDownloader.ts +0 -143
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -7
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
|
|
6
|
-
const { runFfmpegMock } = vi.hoisted(() => ({
|
|
7
|
-
runFfmpegMock: vi.fn(async () => ({
|
|
8
|
-
success: true,
|
|
9
|
-
durationMs: 1,
|
|
10
|
-
stderr: "",
|
|
11
|
-
exitCode: 0,
|
|
12
|
-
})),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
vi.mock("../utils/runFfmpeg.js", () => ({
|
|
16
|
-
runFfmpeg: runFfmpegMock,
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
import { processCompositionAudio } from "./audioMixer.js";
|
|
20
|
-
|
|
21
|
-
describe("processCompositionAudio", () => {
|
|
22
|
-
const tempDirs: string[] = [];
|
|
23
|
-
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
runFfmpegMock.mockClear();
|
|
26
|
-
for (const dir of tempDirs.splice(0)) {
|
|
27
|
-
rmSync(dir, { recursive: true, force: true });
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("preserves muted tracks and uses unity master gain by default", async () => {
|
|
32
|
-
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
|
|
33
|
-
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
|
|
34
|
-
tempDirs.push(baseDir, workDir);
|
|
35
|
-
|
|
36
|
-
writeFileSync(join(baseDir, "voice.wav"), "stub");
|
|
37
|
-
|
|
38
|
-
const result = await processCompositionAudio(
|
|
39
|
-
[
|
|
40
|
-
{
|
|
41
|
-
id: "voice",
|
|
42
|
-
src: "voice.wav",
|
|
43
|
-
start: 0,
|
|
44
|
-
end: 2,
|
|
45
|
-
mediaStart: 0,
|
|
46
|
-
layer: 0,
|
|
47
|
-
volume: 0,
|
|
48
|
-
type: "audio",
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
baseDir,
|
|
52
|
-
workDir,
|
|
53
|
-
join(baseDir, "out.m4a"),
|
|
54
|
-
2,
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
expect(result.success).toBe(true);
|
|
58
|
-
expect(runFfmpegMock).toHaveBeenCalledTimes(2);
|
|
59
|
-
|
|
60
|
-
const mixArgs = runFfmpegMock.mock.calls[1]?.[0];
|
|
61
|
-
const filterIndex = mixArgs.indexOf("-filter_complex");
|
|
62
|
-
const filter = mixArgs[filterIndex + 1];
|
|
63
|
-
|
|
64
|
-
expect(filter).toContain("volume=0");
|
|
65
|
-
expect(filter).toContain("[mixed]volume=1[out]");
|
|
66
|
-
expect(filter).not.toContain("normalize=");
|
|
67
|
-
expect(filter).not.toContain("weights=");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("compensates amix normalization so multi-track master gain equals track count", async () => {
|
|
71
|
-
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
|
|
72
|
-
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
|
|
73
|
-
tempDirs.push(baseDir, workDir);
|
|
74
|
-
|
|
75
|
-
writeFileSync(join(baseDir, "a.wav"), "stub");
|
|
76
|
-
writeFileSync(join(baseDir, "b.wav"), "stub");
|
|
77
|
-
writeFileSync(join(baseDir, "c.wav"), "stub");
|
|
78
|
-
|
|
79
|
-
const result = await processCompositionAudio(
|
|
80
|
-
[
|
|
81
|
-
{
|
|
82
|
-
id: "a",
|
|
83
|
-
src: "a.wav",
|
|
84
|
-
start: 0,
|
|
85
|
-
end: 2,
|
|
86
|
-
mediaStart: 0,
|
|
87
|
-
layer: 0,
|
|
88
|
-
volume: 0.8,
|
|
89
|
-
type: "audio",
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
id: "b",
|
|
93
|
-
src: "b.wav",
|
|
94
|
-
start: 0,
|
|
95
|
-
end: 2,
|
|
96
|
-
mediaStart: 0,
|
|
97
|
-
layer: 1,
|
|
98
|
-
volume: 1,
|
|
99
|
-
type: "audio",
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
id: "c",
|
|
103
|
-
src: "c.wav",
|
|
104
|
-
start: 0,
|
|
105
|
-
end: 2,
|
|
106
|
-
mediaStart: 0,
|
|
107
|
-
layer: 2,
|
|
108
|
-
volume: 0.5,
|
|
109
|
-
type: "audio",
|
|
110
|
-
},
|
|
111
|
-
],
|
|
112
|
-
baseDir,
|
|
113
|
-
workDir,
|
|
114
|
-
join(baseDir, "out.m4a"),
|
|
115
|
-
2,
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
expect(result.success).toBe(true);
|
|
119
|
-
// 3 prepare calls (one per track via Promise.all) precede the mix call,
|
|
120
|
-
// so the mix is at index 3, not index 1.
|
|
121
|
-
expect(runFfmpegMock).toHaveBeenCalledTimes(4);
|
|
122
|
-
const mixArgs = runFfmpegMock.mock.calls[3]?.[0];
|
|
123
|
-
const filter = mixArgs[mixArgs.indexOf("-filter_complex") + 1];
|
|
124
|
-
|
|
125
|
-
expect(filter).toContain("amix=inputs=3");
|
|
126
|
-
expect(filter).not.toContain("normalize=");
|
|
127
|
-
// masterOutputGain(1) × tracks(3) = 3
|
|
128
|
-
expect(filter).toContain("[mixed]volume=3[out]");
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("uses frame-evaluated volume automation when keyframes are present", async () => {
|
|
132
|
-
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
|
|
133
|
-
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
|
|
134
|
-
tempDirs.push(baseDir, workDir);
|
|
135
|
-
|
|
136
|
-
writeFileSync(join(baseDir, "voice.wav"), "stub");
|
|
137
|
-
|
|
138
|
-
const result = await processCompositionAudio(
|
|
139
|
-
[
|
|
140
|
-
{
|
|
141
|
-
id: "voice",
|
|
142
|
-
src: "voice.wav",
|
|
143
|
-
start: 2,
|
|
144
|
-
end: 5,
|
|
145
|
-
mediaStart: 0,
|
|
146
|
-
layer: 0,
|
|
147
|
-
volume: 0,
|
|
148
|
-
volumeKeyframes: [
|
|
149
|
-
{ time: 2, volume: 0 },
|
|
150
|
-
{ time: 3, volume: 1 },
|
|
151
|
-
{ time: 5, volume: 0.5 },
|
|
152
|
-
],
|
|
153
|
-
type: "audio",
|
|
154
|
-
},
|
|
155
|
-
],
|
|
156
|
-
baseDir,
|
|
157
|
-
workDir,
|
|
158
|
-
join(baseDir, "out.m4a"),
|
|
159
|
-
5,
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
expect(result.success).toBe(true);
|
|
163
|
-
|
|
164
|
-
const mixArgs = runFfmpegMock.mock.calls[1]?.[0];
|
|
165
|
-
const filterIndex = mixArgs.indexOf("-filter_complex");
|
|
166
|
-
const filter = mixArgs[filterIndex + 1];
|
|
167
|
-
|
|
168
|
-
expect(filter).toContain("volume=");
|
|
169
|
-
expect(filter).toContain(":eval=frame");
|
|
170
|
-
expect(filter).toContain("lt(t\\,1)");
|
|
171
|
-
expect(filter).toContain("adelay=2000|2000");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("bounds expression nesting for dense keyframe automation without dropping the envelope", async () => {
|
|
175
|
-
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
|
|
176
|
-
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
|
|
177
|
-
tempDirs.push(baseDir, workDir);
|
|
178
|
-
|
|
179
|
-
writeFileSync(join(baseDir, "bgm.wav"), "stub");
|
|
180
|
-
|
|
181
|
-
// Mirrors the 60 Hz timeline probe: a 10s eased fade emits hundreds of
|
|
182
|
-
// keyframes. The nested-if volume expression must not grow one level per
|
|
183
|
-
// keyframe — past ~95 levels FFmpeg fails filter-graph init and the audio
|
|
184
|
-
// track is dropped entirely (GH #1066 follow-up).
|
|
185
|
-
const keyframes = Array.from({ length: 300 }, (_, i) => {
|
|
186
|
-
const time = (i / 299) * 10;
|
|
187
|
-
const volume =
|
|
188
|
-
time < 3 ? 0.8 * (time / 3) ** 2 : time < 7 ? 0.8 : 0.8 * (1 - (time - 7) / 3) ** 2;
|
|
189
|
-
return { time, volume };
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
const result = await processCompositionAudio(
|
|
193
|
-
[
|
|
194
|
-
{
|
|
195
|
-
id: "bgm",
|
|
196
|
-
src: "bgm.wav",
|
|
197
|
-
start: 0,
|
|
198
|
-
end: 10,
|
|
199
|
-
mediaStart: 0,
|
|
200
|
-
layer: 0,
|
|
201
|
-
volume: 0,
|
|
202
|
-
volumeKeyframes: keyframes,
|
|
203
|
-
type: "audio",
|
|
204
|
-
},
|
|
205
|
-
],
|
|
206
|
-
baseDir,
|
|
207
|
-
workDir,
|
|
208
|
-
join(baseDir, "out.m4a"),
|
|
209
|
-
10,
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
expect(result.success).toBe(true);
|
|
213
|
-
|
|
214
|
-
const mixArgs = runFfmpegMock.mock.calls[1]?.[0];
|
|
215
|
-
const filterIndex = mixArgs.indexOf("-filter_complex");
|
|
216
|
-
const filter = mixArgs[filterIndex + 1];
|
|
217
|
-
|
|
218
|
-
// One nested `if(lt(...))` is emitted per segment; cap it well under the
|
|
219
|
-
// FFmpeg evaluator's nesting limit (MAX_VOLUME_SEGMENTS = 32).
|
|
220
|
-
const nestingDepth = (filter.match(/if\(lt\(t/g) ?? []).length;
|
|
221
|
-
expect(nestingDepth).toBeGreaterThan(1);
|
|
222
|
-
expect(nestingDepth).toBeLessThan(32);
|
|
223
|
-
|
|
224
|
-
// The simplified envelope still spans the clip: silent start, audible peak.
|
|
225
|
-
expect(filter).toContain(":eval=frame");
|
|
226
|
-
expect(filter).toMatch(/volume=if\(lt\(t\\,[0-9.]+\)\\,0\+/);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("falls back to a static-volume mix instead of dropping audio when the automated mix fails", async () => {
|
|
230
|
-
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
|
|
231
|
-
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
|
|
232
|
-
tempDirs.push(baseDir, workDir);
|
|
233
|
-
|
|
234
|
-
writeFileSync(join(baseDir, "bgm.wav"), "stub");
|
|
235
|
-
|
|
236
|
-
// Simulate an ffmpeg build that rejects the automation expression: the
|
|
237
|
-
// first mix attempt fails, the static-volume retry succeeds. (prepare =
|
|
238
|
-
// call 0, automated mix = call 1, fallback mix = call 2.)
|
|
239
|
-
runFfmpegMock
|
|
240
|
-
.mockImplementationOnce(async () => ({
|
|
241
|
-
success: true,
|
|
242
|
-
durationMs: 1,
|
|
243
|
-
stderr: "",
|
|
244
|
-
exitCode: 0,
|
|
245
|
-
}))
|
|
246
|
-
.mockImplementationOnce(async () => ({
|
|
247
|
-
success: false,
|
|
248
|
-
durationMs: 1,
|
|
249
|
-
stderr: "Error initializing filters",
|
|
250
|
-
exitCode: 234,
|
|
251
|
-
}));
|
|
252
|
-
|
|
253
|
-
const result = await processCompositionAudio(
|
|
254
|
-
[
|
|
255
|
-
{
|
|
256
|
-
id: "bgm",
|
|
257
|
-
src: "bgm.wav",
|
|
258
|
-
start: 0,
|
|
259
|
-
end: 5,
|
|
260
|
-
mediaStart: 0,
|
|
261
|
-
layer: 0,
|
|
262
|
-
volume: 0.8,
|
|
263
|
-
volumeKeyframes: [
|
|
264
|
-
{ time: 0, volume: 0.8 },
|
|
265
|
-
{ time: 5, volume: 0 },
|
|
266
|
-
],
|
|
267
|
-
type: "audio",
|
|
268
|
-
},
|
|
269
|
-
],
|
|
270
|
-
baseDir,
|
|
271
|
-
workDir,
|
|
272
|
-
join(baseDir, "out.m4a"),
|
|
273
|
-
5,
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
expect(result.success).toBe(true);
|
|
277
|
-
expect(result.tracksProcessed).toBe(1);
|
|
278
|
-
expect(runFfmpegMock).toHaveBeenCalledTimes(3);
|
|
279
|
-
// Degradation is surfaced, not silent — the track rendered at base volume.
|
|
280
|
-
expect(result.error).toMatch(/base volume/i);
|
|
281
|
-
|
|
282
|
-
// The fallback mix omits the automation expression (base volume only).
|
|
283
|
-
const fallbackArgs = runFfmpegMock.mock.calls[2]?.[0];
|
|
284
|
-
const fallbackFilter = fallbackArgs[fallbackArgs.indexOf("-filter_complex") + 1];
|
|
285
|
-
expect(fallbackFilter).not.toContain(":eval=frame");
|
|
286
|
-
expect(fallbackFilter).toContain("volume=0.8");
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("prepares percent-encoded non-Latin audio srcs from decoded filesystem paths", async () => {
|
|
290
|
-
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
|
|
291
|
-
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
|
|
292
|
-
tempDirs.push(baseDir, workDir);
|
|
293
|
-
|
|
294
|
-
const encodedFilename =
|
|
295
|
-
"%D9%87%D9%86%D8%A7%20%D9%85%D8%B1%D9%88%D8%A7%20-%20%D9%85%D8%A8%D8%A7%D8%B1%D9%83.mp4";
|
|
296
|
-
const filename = decodeURIComponent(encodedFilename);
|
|
297
|
-
mkdirSync(join(baseDir, "assets"), { recursive: true });
|
|
298
|
-
writeFileSync(join(baseDir, "assets", filename), "stub");
|
|
299
|
-
|
|
300
|
-
const result = await processCompositionAudio(
|
|
301
|
-
[
|
|
302
|
-
{
|
|
303
|
-
id: "voice",
|
|
304
|
-
src: `assets/${encodedFilename}`,
|
|
305
|
-
start: 0,
|
|
306
|
-
end: 2,
|
|
307
|
-
mediaStart: 0,
|
|
308
|
-
layer: 0,
|
|
309
|
-
volume: 1,
|
|
310
|
-
type: "audio",
|
|
311
|
-
},
|
|
312
|
-
],
|
|
313
|
-
baseDir,
|
|
314
|
-
workDir,
|
|
315
|
-
join(baseDir, "out.m4a"),
|
|
316
|
-
2,
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
expect(result.success).toBe(true);
|
|
320
|
-
expect(result.error).toBeUndefined();
|
|
321
|
-
expect(runFfmpegMock).toHaveBeenCalledTimes(2);
|
|
322
|
-
|
|
323
|
-
const prepareArgs = runFfmpegMock.mock.calls[0]?.[0];
|
|
324
|
-
expect(prepareArgs).toContain(join(baseDir, "assets", filename));
|
|
325
|
-
});
|
|
326
|
-
});
|