@hyperframes/engine 0.6.119 → 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,342 +0,0 @@
|
|
|
1
|
-
// fallow-ignore-file code-duplication
|
|
2
|
-
import { EventEmitter } from "events";
|
|
3
|
-
import { readFileSync } from "fs";
|
|
4
|
-
import { resolve } from "path";
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
import { extractMediaMetadata, extractPngMetadataFromBuffer } from "./ffprobe.js";
|
|
7
|
-
|
|
8
|
-
function crc32(buf: Buffer): number {
|
|
9
|
-
let crc = 0xffffffff;
|
|
10
|
-
for (let i = 0; i < buf.length; i++) {
|
|
11
|
-
crc ^= buf[i] ?? 0;
|
|
12
|
-
for (let bit = 0; bit < 8; bit++) {
|
|
13
|
-
const mask = -(crc & 1);
|
|
14
|
-
crc = (crc >>> 1) ^ (0xedb88320 & mask);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return (crc ^ 0xffffffff) >>> 0;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function pngChunk(type: string, data: number[]): Buffer {
|
|
21
|
-
const chunkData = Buffer.from(data);
|
|
22
|
-
const header = Buffer.alloc(8);
|
|
23
|
-
header.writeUInt32BE(chunkData.length, 0);
|
|
24
|
-
header.write(type, 4, 4, "ascii");
|
|
25
|
-
const crc = Buffer.alloc(4);
|
|
26
|
-
crc.writeUInt32BE(crc32(Buffer.concat([Buffer.from(type, "ascii"), chunkData])), 0);
|
|
27
|
-
return Buffer.concat([header, chunkData, crc]);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function buildPngWithChunks(chunks: Buffer[]): Buffer {
|
|
31
|
-
return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), ...chunks]);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function buildMinimalPng(options?: {
|
|
35
|
-
cIcpAfterIdat?: boolean;
|
|
36
|
-
invalidCrc?: boolean;
|
|
37
|
-
longCicp?: boolean;
|
|
38
|
-
}) {
|
|
39
|
-
const ihdr = pngChunk("IHDR", [0, 0, 0, 1, 0, 0, 0, 1, 16, 2, 0, 0, 0]);
|
|
40
|
-
const cicpData = options?.longCicp ? [9, 16, 0, 1, 255] : [9, 16, 0, 1];
|
|
41
|
-
let cicp = pngChunk("cICP", cicpData);
|
|
42
|
-
if (options?.invalidCrc) {
|
|
43
|
-
cicp = Buffer.from(cicp);
|
|
44
|
-
cicp[cicp.length - 1] ^= 0xff;
|
|
45
|
-
}
|
|
46
|
-
const idat = pngChunk(
|
|
47
|
-
"IDAT",
|
|
48
|
-
[0x78, 0x9c, 0x63, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01],
|
|
49
|
-
);
|
|
50
|
-
const iend = pngChunk("IEND", []);
|
|
51
|
-
return options?.cIcpAfterIdat
|
|
52
|
-
? buildPngWithChunks([ihdr, idat, cicp, iend])
|
|
53
|
-
: buildPngWithChunks([ihdr, cicp, idat, iend]);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
describe("extractMediaMetadata", () => {
|
|
57
|
-
it("reads HDR PNG cICP metadata when ffprobe color fields are absent", async () => {
|
|
58
|
-
const fixturePath = resolve(
|
|
59
|
-
__dirname,
|
|
60
|
-
"../../../producer/tests/hdr-regression/src/hdr-photo-pq.png",
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const metadata = await extractMediaMetadata(fixturePath);
|
|
64
|
-
|
|
65
|
-
expect(metadata.colorSpace).toEqual({
|
|
66
|
-
colorPrimaries: "bt2020",
|
|
67
|
-
colorTransfer: "smpte2084",
|
|
68
|
-
colorSpace: "gbr",
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe("extractPngMetadataFromBuffer", () => {
|
|
74
|
-
it("accepts a valid cICP chunk before IDAT", () => {
|
|
75
|
-
const metadata = extractPngMetadataFromBuffer(buildMinimalPng());
|
|
76
|
-
expect(metadata?.colorSpace).toEqual({
|
|
77
|
-
colorPrimaries: "bt2020",
|
|
78
|
-
colorTransfer: "smpte2084",
|
|
79
|
-
colorSpace: "gbr",
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("rejects cICP chunks after IDAT", () => {
|
|
84
|
-
const metadata = extractPngMetadataFromBuffer(buildMinimalPng({ cIcpAfterIdat: true }));
|
|
85
|
-
expect(metadata).toEqual({
|
|
86
|
-
width: 1,
|
|
87
|
-
height: 1,
|
|
88
|
-
colorSpace: null,
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("rejects cICP chunks with invalid CRC", () => {
|
|
93
|
-
expect(extractPngMetadataFromBuffer(buildMinimalPng({ invalidCrc: true }))).toBeNull();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("rejects cICP chunks whose payload is not exactly four bytes", () => {
|
|
97
|
-
const metadata = extractPngMetadataFromBuffer(buildMinimalPng({ longCicp: true }));
|
|
98
|
-
expect(metadata).toEqual({
|
|
99
|
-
width: 1,
|
|
100
|
-
height: 1,
|
|
101
|
-
colorSpace: null,
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("continues to parse the checked-in HDR PNG fixture", () => {
|
|
106
|
-
const fixture = readFileSync(
|
|
107
|
-
resolve(__dirname, "../../../producer/tests/hdr-regression/src/hdr-photo-pq.png"),
|
|
108
|
-
);
|
|
109
|
-
expect(extractPngMetadataFromBuffer(fixture)?.colorSpace?.colorTransfer).toBe("smpte2084");
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
interface SpawnCall {
|
|
114
|
-
command: string;
|
|
115
|
-
args: readonly string[];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
interface FakeProc extends EventEmitter {
|
|
119
|
-
stdout: EventEmitter;
|
|
120
|
-
stderr: EventEmitter;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
type SpawnOutcome =
|
|
124
|
-
| { kind: "missing" }
|
|
125
|
-
| { kind: "error"; message: string; code?: string }
|
|
126
|
-
| { kind: "exit"; code: number; stdout?: string; stderr?: string };
|
|
127
|
-
|
|
128
|
-
function createSpawnSpy(outcomes: SpawnOutcome[]): {
|
|
129
|
-
spawn: (command: string, args: readonly string[]) => FakeProc;
|
|
130
|
-
calls: SpawnCall[];
|
|
131
|
-
} {
|
|
132
|
-
const calls: SpawnCall[] = [];
|
|
133
|
-
let invocation = 0;
|
|
134
|
-
const spawn = (command: string, args: readonly string[]): FakeProc => {
|
|
135
|
-
calls.push({ command, args });
|
|
136
|
-
const outcome = outcomes[invocation] ?? outcomes[outcomes.length - 1];
|
|
137
|
-
invocation += 1;
|
|
138
|
-
|
|
139
|
-
const proc = new EventEmitter() as FakeProc;
|
|
140
|
-
proc.stdout = new EventEmitter();
|
|
141
|
-
proc.stderr = new EventEmitter();
|
|
142
|
-
|
|
143
|
-
process.nextTick(() => {
|
|
144
|
-
if (!outcome) return;
|
|
145
|
-
if (outcome.kind === "missing") {
|
|
146
|
-
const err = new Error("spawn ffprobe ENOENT") as NodeJS.ErrnoException;
|
|
147
|
-
err.code = "ENOENT";
|
|
148
|
-
proc.emit("error", err);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
if (outcome.kind === "error") {
|
|
152
|
-
const err = new Error(outcome.message) as NodeJS.ErrnoException;
|
|
153
|
-
if (outcome.code) err.code = outcome.code;
|
|
154
|
-
proc.emit("error", err);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
if (outcome.stdout) proc.stdout.emit("data", Buffer.from(outcome.stdout));
|
|
158
|
-
if (outcome.stderr) proc.stderr.emit("data", Buffer.from(outcome.stderr));
|
|
159
|
-
proc.emit("close", outcome.code);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
return proc;
|
|
163
|
-
};
|
|
164
|
-
return { spawn, calls };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
describe("ffprobe missing-binary fallback", () => {
|
|
168
|
-
const originalFfprobePath = process.env.HYPERFRAMES_FFPROBE_PATH;
|
|
169
|
-
|
|
170
|
-
afterEach(() => {
|
|
171
|
-
vi.resetModules();
|
|
172
|
-
vi.doUnmock("child_process");
|
|
173
|
-
if (originalFfprobePath === undefined) delete process.env.HYPERFRAMES_FFPROBE_PATH;
|
|
174
|
-
else process.env.HYPERFRAMES_FFPROBE_PATH = originalFfprobePath;
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("spawns the configured absolute FFprobe path when HYPERFRAMES_FFPROBE_PATH is set", async () => {
|
|
178
|
-
process.env.HYPERFRAMES_FFPROBE_PATH = "/tools/ffprobe.exe";
|
|
179
|
-
const { spawn, calls } = createSpawnSpy([
|
|
180
|
-
{
|
|
181
|
-
kind: "exit",
|
|
182
|
-
code: 0,
|
|
183
|
-
stdout: JSON.stringify({
|
|
184
|
-
streams: [{ codec_type: "audio", codec_name: "aac", sample_rate: "48000", channels: 2 }],
|
|
185
|
-
format: { duration: "1.25", bit_rate: "128000" },
|
|
186
|
-
}),
|
|
187
|
-
},
|
|
188
|
-
]);
|
|
189
|
-
vi.resetModules();
|
|
190
|
-
vi.doMock("child_process", () => ({ spawn }));
|
|
191
|
-
|
|
192
|
-
const { extractAudioMetadata } = await import("./ffprobe.js");
|
|
193
|
-
const meta = await extractAudioMetadata("/tmp/uses-configured-ffprobe.wav");
|
|
194
|
-
|
|
195
|
-
expect(meta.durationSeconds).toBe(1.25);
|
|
196
|
-
expect(calls[0]?.command).toBe(resolve("/tools/ffprobe.exe"));
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("extractMediaMetadata falls back to PNG cICP metadata when ffprobe is missing", async () => {
|
|
200
|
-
const { spawn, calls } = createSpawnSpy([{ kind: "missing" }]);
|
|
201
|
-
vi.resetModules();
|
|
202
|
-
vi.doMock("child_process", () => ({ spawn }));
|
|
203
|
-
|
|
204
|
-
const { extractMediaMetadata: extractMediaMetadataMocked } = await import("./ffprobe.js");
|
|
205
|
-
const fixture = resolve(
|
|
206
|
-
__dirname,
|
|
207
|
-
"../../../producer/tests/hdr-regression/src/hdr-photo-pq.png",
|
|
208
|
-
);
|
|
209
|
-
const meta = await extractMediaMetadataMocked(fixture);
|
|
210
|
-
|
|
211
|
-
expect(calls.length).toBe(1);
|
|
212
|
-
expect(calls[0]?.command).toBe("ffprobe");
|
|
213
|
-
expect(meta.videoCodec).toBe("png");
|
|
214
|
-
expect(meta.durationSeconds).toBe(0);
|
|
215
|
-
expect(meta.fps).toBe(0);
|
|
216
|
-
expect(meta.hasAudio).toBe(false);
|
|
217
|
-
expect(meta.isVFR).toBe(false);
|
|
218
|
-
expect(meta.hasAlpha).toBe(false);
|
|
219
|
-
expect(meta.colorSpace?.colorTransfer).toBe("smpte2084");
|
|
220
|
-
expect(meta.colorSpace?.colorPrimaries).toBe("bt2020");
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("extractMediaMetadata detects VP9 alpha_mode streams", async () => {
|
|
224
|
-
const { spawn } = createSpawnSpy([
|
|
225
|
-
{
|
|
226
|
-
kind: "exit",
|
|
227
|
-
code: 0,
|
|
228
|
-
stdout: JSON.stringify({
|
|
229
|
-
streams: [
|
|
230
|
-
{
|
|
231
|
-
codec_type: "video",
|
|
232
|
-
codec_name: "vp9",
|
|
233
|
-
width: 320,
|
|
234
|
-
height: 180,
|
|
235
|
-
r_frame_rate: "30/1",
|
|
236
|
-
avg_frame_rate: "30/1",
|
|
237
|
-
pix_fmt: "yuv420p",
|
|
238
|
-
tags: { alpha_mode: "1" },
|
|
239
|
-
},
|
|
240
|
-
],
|
|
241
|
-
format: { duration: "1.5" },
|
|
242
|
-
}),
|
|
243
|
-
},
|
|
244
|
-
]);
|
|
245
|
-
vi.resetModules();
|
|
246
|
-
vi.doMock("child_process", () => ({ spawn }));
|
|
247
|
-
|
|
248
|
-
const { extractMediaMetadata: extractMediaMetadataMocked } = await import("./ffprobe.js");
|
|
249
|
-
const meta = await extractMediaMetadataMocked("/tmp/alpha.webm");
|
|
250
|
-
|
|
251
|
-
expect(meta.videoCodec).toBe("vp9");
|
|
252
|
-
expect(meta.hasAlpha).toBe(true);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
// Regression: newer libavformat builds (and the output of `hyperframes
|
|
256
|
-
// remove-background` itself) write the VP9-alpha sidecar tag as
|
|
257
|
-
// `ALPHA_MODE` (uppercase). The lowercase-only check classified those
|
|
258
|
-
// files as having no alpha, the producer extracted them as JPGs, and
|
|
259
|
-
// the injected <img> overlays were fully opaque rectangles that hid
|
|
260
|
-
// every static element below them on the z-stack. The bug was silent —
|
|
261
|
-
// studio preview rendered correctly via native <video> playback while
|
|
262
|
-
// production renders covered headlines and captions with the avatar.
|
|
263
|
-
it("extractMediaMetadata detects ALPHA_MODE (uppercase) streams from newer ffmpeg builds", async () => {
|
|
264
|
-
const { spawn } = createSpawnSpy([
|
|
265
|
-
{
|
|
266
|
-
kind: "exit",
|
|
267
|
-
code: 0,
|
|
268
|
-
stdout: JSON.stringify({
|
|
269
|
-
streams: [
|
|
270
|
-
{
|
|
271
|
-
codec_type: "video",
|
|
272
|
-
codec_name: "vp9",
|
|
273
|
-
width: 320,
|
|
274
|
-
height: 180,
|
|
275
|
-
r_frame_rate: "30/1",
|
|
276
|
-
avg_frame_rate: "30/1",
|
|
277
|
-
pix_fmt: "yuv420p",
|
|
278
|
-
tags: { ALPHA_MODE: "1" },
|
|
279
|
-
},
|
|
280
|
-
],
|
|
281
|
-
format: { duration: "1.5" },
|
|
282
|
-
}),
|
|
283
|
-
},
|
|
284
|
-
]);
|
|
285
|
-
vi.resetModules();
|
|
286
|
-
vi.doMock("child_process", () => ({ spawn }));
|
|
287
|
-
|
|
288
|
-
const { extractMediaMetadata: extractMediaMetadataMocked } = await import("./ffprobe.js");
|
|
289
|
-
const meta = await extractMediaMetadataMocked("/tmp/alpha-uppercase.webm");
|
|
290
|
-
|
|
291
|
-
expect(meta.videoCodec).toBe("vp9");
|
|
292
|
-
expect(meta.hasAlpha).toBe(true);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it("extractMediaMetadata rethrows ffprobe-missing error for non-image files without fallback", async () => {
|
|
296
|
-
const { spawn } = createSpawnSpy([{ kind: "missing" }]);
|
|
297
|
-
vi.resetModules();
|
|
298
|
-
vi.doMock("child_process", () => ({ spawn }));
|
|
299
|
-
|
|
300
|
-
const { extractMediaMetadata: extractMediaMetadataMocked } = await import("./ffprobe.js");
|
|
301
|
-
|
|
302
|
-
await expect(extractMediaMetadataMocked("/tmp/no-such-video.mp4")).rejects.toThrow(/ffprobe/);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it("extractAudioMetadata surfaces a ffprobe-missing error verbatim", async () => {
|
|
306
|
-
const { spawn, calls } = createSpawnSpy([{ kind: "missing" }]);
|
|
307
|
-
vi.resetModules();
|
|
308
|
-
vi.doMock("child_process", () => ({ spawn }));
|
|
309
|
-
|
|
310
|
-
const { extractAudioMetadata } = await import("./ffprobe.js");
|
|
311
|
-
|
|
312
|
-
await expect(extractAudioMetadata("/tmp/no-such-audio.wav")).rejects.toThrow(
|
|
313
|
-
/ffprobe not found/,
|
|
314
|
-
);
|
|
315
|
-
expect(calls.length).toBe(1);
|
|
316
|
-
expect(calls[0]?.command).toBe("ffprobe");
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("analyzeKeyframeIntervals surfaces a ffprobe-missing error verbatim", async () => {
|
|
320
|
-
const { spawn, calls } = createSpawnSpy([{ kind: "missing" }]);
|
|
321
|
-
vi.resetModules();
|
|
322
|
-
vi.doMock("child_process", () => ({ spawn }));
|
|
323
|
-
|
|
324
|
-
const { analyzeKeyframeIntervals } = await import("./ffprobe.js");
|
|
325
|
-
|
|
326
|
-
await expect(analyzeKeyframeIntervals("/tmp/no-such-video.mp4")).rejects.toThrow(
|
|
327
|
-
/ffprobe not found/,
|
|
328
|
-
);
|
|
329
|
-
expect(calls.length).toBe(1);
|
|
330
|
-
expect(calls[0]?.command).toBe("ffprobe");
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it("ffprobe-missing error message includes install hint", async () => {
|
|
334
|
-
const { spawn } = createSpawnSpy([{ kind: "missing" }]);
|
|
335
|
-
vi.resetModules();
|
|
336
|
-
vi.doMock("child_process", () => ({ spawn }));
|
|
337
|
-
|
|
338
|
-
const { extractAudioMetadata } = await import("./ffprobe.js");
|
|
339
|
-
|
|
340
|
-
await expect(extractAudioMetadata("/tmp/example.mp3")).rejects.toThrow(/install FFmpeg/i);
|
|
341
|
-
});
|
|
342
|
-
});
|