@poe-platform/safe-bash 0.1.843 → 0.1.845
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/dist/safe-bash/command-chunks/{chunk-MJB4WWYP.js → chunk-4KM2VJRB.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-SBBMNLD6.js → chunk-CMGVQM4Y.js} +15 -5
- package/dist/safe-bash-command-ffmpeg/index.js +1336 -1115
- package/dist/safe-bash-command-ffprobe/index.js +2 -2
- package/dist/safe-bash-command-ffprobe/media.js +2 -2
- package/dist/safe-bash-command-imagemagick/index.d.ts +1 -0
- package/dist/safe-bash-command-imagemagick/index.js +138 -98
- package/package.json +2 -2
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
sliceMp4,
|
|
24
24
|
srtAst,
|
|
25
25
|
webvttAst
|
|
26
|
-
} from "../safe-bash/command-chunks/chunk-
|
|
26
|
+
} from "../safe-bash/command-chunks/chunk-CMGVQM4Y.js";
|
|
27
27
|
import "../safe-bash/command-chunks/chunk-65DS4IOB.js";
|
|
28
28
|
|
|
29
29
|
// packages/safe-bash-command-ffmpeg/src/index.ts
|
|
@@ -78,6 +78,73 @@ async function resampleAudio(channels, outputLength, rateRatio, budget, signal)
|
|
|
78
78
|
|
|
79
79
|
// packages/safe-bash-command-ffmpeg/src/audio-tempo.ts
|
|
80
80
|
import { yieldTurn as yieldTurn2 } from "../safe-bash-contracts/yield.js";
|
|
81
|
+
async function filterUnityTempo(channels, sampleRate, pcm16, budget, signal) {
|
|
82
|
+
const length = channels[0]?.length ?? 0;
|
|
83
|
+
const window = 2 ** Math.ceil(Math.log2(Math.max(2, Math.floor(sampleRate / 24))));
|
|
84
|
+
const hop = window / 2;
|
|
85
|
+
if (length < hop) return channels.map(() => new Float32Array(0));
|
|
86
|
+
const hann = Float32Array.from({ length: window }, (_, i) => (1 - Math.cos(2 * Math.PI * i / (window - 1))) / 2);
|
|
87
|
+
const mono = new Float32Array(length);
|
|
88
|
+
for (let i = 0; i < length; i++) {
|
|
89
|
+
if (i % 16384 === 0) {
|
|
90
|
+
await yieldTurn2(signal);
|
|
91
|
+
signal?.throwIfAborted();
|
|
92
|
+
budget.checkCpu();
|
|
93
|
+
}
|
|
94
|
+
for (const channel of channels) {
|
|
95
|
+
if (Math.abs(channel[i]) > Math.abs(mono[i])) mono[i] = channel[i];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const output = channels.map(() => []);
|
|
99
|
+
let previous = -hop;
|
|
100
|
+
let position = -hop;
|
|
101
|
+
let work = 0;
|
|
102
|
+
while (true) {
|
|
103
|
+
let current = previous + hop;
|
|
104
|
+
position += hop;
|
|
105
|
+
const drift = position - hop - previous;
|
|
106
|
+
const start = Math.min(window, Math.max(0, -drift));
|
|
107
|
+
const end = Math.max(0, Math.min(window - drift, window - window / 16));
|
|
108
|
+
let correction = -drift;
|
|
109
|
+
let best = -Infinity;
|
|
110
|
+
for (let lag = start; lag < end; lag++) {
|
|
111
|
+
let correlation = 0;
|
|
112
|
+
for (let j = Math.max(0, -current); j < Math.min(window - lag, length - current, length - previous - lag); j++) {
|
|
113
|
+
correlation += (mono[previous + j + lag] ?? 0) * mono[current + j];
|
|
114
|
+
if (++work % 16384 === 0) {
|
|
115
|
+
await yieldTurn2(signal);
|
|
116
|
+
signal?.throwIfAborted();
|
|
117
|
+
budget.checkCpu();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const metric = correlation * (drift + lag) * (lag - start) * (end - lag);
|
|
121
|
+
if (metric > best) {
|
|
122
|
+
best = metric;
|
|
123
|
+
correction = lag - hop;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
current -= correction;
|
|
127
|
+
const count = Math.max(0, Math.min(window, length - current));
|
|
128
|
+
const overlap = Math.min(hop, count, length - previous - hop);
|
|
129
|
+
for (let c = 0; c < channels.length; c++) {
|
|
130
|
+
const channel = channels[c];
|
|
131
|
+
const dst = output[c];
|
|
132
|
+
for (let i = 0; i < overlap; i++) {
|
|
133
|
+
const a = channel[previous + hop + i] ?? 0;
|
|
134
|
+
const b = channel[current + i] ?? 0;
|
|
135
|
+
const sample = current + i < 0 ? a : Math.fround(a * hann[hop + i] + Math.fround(b * hann[i]));
|
|
136
|
+
dst.push(pcm16 ? Math.trunc(sample * 32768) / 32768 : sample);
|
|
137
|
+
}
|
|
138
|
+
if (current + count >= length) {
|
|
139
|
+
for (let i = overlap; i < count; i++) dst.push(channel[current + i] ?? 0);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
budget.checkDuration((output[0]?.length ?? 0) / sampleRate);
|
|
143
|
+
if (current + count >= length) break;
|
|
144
|
+
previous = current;
|
|
145
|
+
}
|
|
146
|
+
return output.map((channel) => Float32Array.from(channel));
|
|
147
|
+
}
|
|
81
148
|
async function stretchAudio(channels, sampleRate, tempo, budget, signal) {
|
|
82
149
|
const length = channels[0]?.length ?? 0;
|
|
83
150
|
if (tempo === 1 || length === 0) return channels;
|
|
@@ -275,6 +342,21 @@ import {
|
|
|
275
342
|
resizeImage,
|
|
276
343
|
rotateImage
|
|
277
344
|
} from "../image-ast/portable.js";
|
|
345
|
+
function loopToOutputEnd(doc, endSeconds, budget) {
|
|
346
|
+
if (endSeconds === void 0 || !Number.isFinite(endSeconds)) {
|
|
347
|
+
throw new Error("-stream_loop -1 requires a finite output -t or -to");
|
|
348
|
+
}
|
|
349
|
+
budget.checkDuration(endSeconds);
|
|
350
|
+
if (doc.durationSeconds <= 0 || endSeconds <= doc.durationSeconds) return doc;
|
|
351
|
+
const copies = Math.ceil(endSeconds / doc.durationSeconds);
|
|
352
|
+
budget.checkConcatInputs(copies);
|
|
353
|
+
budget.allocateMemory(copies * 8);
|
|
354
|
+
try {
|
|
355
|
+
return concatMp4(Array.from({ length: copies }, () => doc), { budget });
|
|
356
|
+
} finally {
|
|
357
|
+
budget.releaseMemory(copies * 8);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
278
360
|
function isStdout(path) {
|
|
279
361
|
return ["-", "pipe:", "pipe:1", "/dev/stdout", "/dev/fd/1"].includes(path);
|
|
280
362
|
}
|
|
@@ -1310,6 +1392,23 @@ function selectAutomaticAudio(documents) {
|
|
|
1310
1392
|
tracks: document.tracks.filter((track) => track.type !== "audio" || track === selected)
|
|
1311
1393
|
}));
|
|
1312
1394
|
}
|
|
1395
|
+
function limitAudioCopyPackets(document, limit) {
|
|
1396
|
+
return {
|
|
1397
|
+
...document,
|
|
1398
|
+
tracks: document.tracks.map((track) => {
|
|
1399
|
+
if (track.type !== "audio" || track.samples.length <= limit) return track;
|
|
1400
|
+
const samples = track.samples.slice(0, limit);
|
|
1401
|
+
return { ...track, samples, duration: samples.reduce((total, sample) => total + sample.duration, 0) };
|
|
1402
|
+
})
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
function validateAudioEncoder(codec, doc, plugins) {
|
|
1406
|
+
if (codec === void 0 || codec === "copy" || !doc.tracks.some((track) => track.type === "audio")) return;
|
|
1407
|
+
const name = codec === "libopus" ? "opus" : codec === "libvorbis" ? "vorbis" : codec;
|
|
1408
|
+
if (!plugins.some((plugin) => plugin.canMux && plugin.supportedAudioCodecs.includes(name))) {
|
|
1409
|
+
throw new Error(`Unknown encoder '${codec}'`);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1313
1412
|
function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
|
|
1314
1413
|
const inBytes = Array.isArray(inBytesOrArgs) ? void 0 : inBytesOrArgs;
|
|
1315
1414
|
const args = Array.isArray(inBytesOrArgs) ? inBytesOrArgs : maybeArgs ?? [];
|
|
@@ -1325,6 +1424,7 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
|
|
|
1325
1424
|
const budget = new MediaBudgetTracker();
|
|
1326
1425
|
const inputs = [];
|
|
1327
1426
|
let pendingFormat;
|
|
1427
|
+
let pendingAudioDecoder;
|
|
1328
1428
|
let pendingSs;
|
|
1329
1429
|
let pendingTo;
|
|
1330
1430
|
let pendingDuration;
|
|
@@ -1347,10 +1447,11 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
|
|
|
1347
1447
|
let faststart = true;
|
|
1348
1448
|
let fragmented = false;
|
|
1349
1449
|
let maxVideoFrames;
|
|
1450
|
+
let maxAudioFrames;
|
|
1350
1451
|
let outputFps;
|
|
1351
1452
|
let outputSize;
|
|
1352
1453
|
let rotation;
|
|
1353
|
-
let noOverwrite
|
|
1454
|
+
let noOverwrite;
|
|
1354
1455
|
const vfFilters = [];
|
|
1355
1456
|
let chapterSource;
|
|
1356
1457
|
let metadataSource;
|
|
@@ -1360,9 +1461,10 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
|
|
|
1360
1461
|
let outputTarget;
|
|
1361
1462
|
for (let i = 0; i < args.length; i++) {
|
|
1362
1463
|
const arg = args[i];
|
|
1363
|
-
if (arg === "-y"
|
|
1364
|
-
|
|
1365
|
-
|
|
1464
|
+
if (arg === "-y" || arg === "-n") {
|
|
1465
|
+
if (noOverwrite !== void 0 && noOverwrite !== (arg === "-n")) return void 0;
|
|
1466
|
+
noOverwrite = arg === "-n";
|
|
1467
|
+
} else if (arg === "-v" || arg === "-loglevel" || arg === "-safe" || arg === "-threads" || arg === "-pix_fmt" || arg === "-preset" || arg === "-crf" || arg === "-b:v" || arg === "-b:a" || arg === "-bsf:v" || arg === "-bsf:a" || arg === "-tag:v" || arg === "-tag:a" || arg === "-vsync" || arg === "-fps_mode") i++;
|
|
1366
1468
|
else if (arg === "-map_chapters") {
|
|
1367
1469
|
const value = args[++i];
|
|
1368
1470
|
chapterSource = value?.trim() ? Number(value) : NaN;
|
|
@@ -1376,8 +1478,10 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
|
|
|
1376
1478
|
if (!Number.isSafeInteger(val) || val < 0) return void 0;
|
|
1377
1479
|
if (args.slice(i + 1).includes("-i")) pendingStartNumber = val;
|
|
1378
1480
|
else outputStartNumber = val;
|
|
1379
|
-
} else if (arg === "-ar")
|
|
1380
|
-
|
|
1481
|
+
} else if (arg === "-ar") {
|
|
1482
|
+
audioRate = parseInt(args[++i] ?? "0", 10) || void 0;
|
|
1483
|
+
if (audioRate !== void 0 && audioRate < 0) return void 0;
|
|
1484
|
+
} else if (arg === "-ac") audioChannels = parseInt(args[++i] ?? "0", 10) || void 0;
|
|
1381
1485
|
else if (arg === "-hide_banner" || arg === "-nostdin" || arg === "-stats") continue;
|
|
1382
1486
|
else if (arg === "-f") {
|
|
1383
1487
|
const val = args[++i];
|
|
@@ -1393,6 +1497,7 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
|
|
|
1393
1497
|
else outputTo = val;
|
|
1394
1498
|
} else if (arg === "-t") {
|
|
1395
1499
|
const val = parseFfmpegTimestamp(args[++i] ?? "0");
|
|
1500
|
+
if (val < 0) return void 0;
|
|
1396
1501
|
if (args.slice(i + 1).includes("-i")) pendingDuration = val;
|
|
1397
1502
|
else outputDuration = val;
|
|
1398
1503
|
} else if (arg === "-stream_loop") pendingLoop = parseInt(args[++i] ?? "0", 10) || 0;
|
|
@@ -1402,6 +1507,8 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
|
|
|
1402
1507
|
else outputFps = val;
|
|
1403
1508
|
} else if (arg === "-s") outputSize = args[++i];
|
|
1404
1509
|
else if (arg === "-i") {
|
|
1510
|
+
if (pendingAudioDecoder !== void 0 && !astPlugins.some((plugin) => plugin.canDemux && plugin.supportedAudioCodecs.includes(pendingAudioDecoder))) return void 0;
|
|
1511
|
+
pendingAudioDecoder = void 0;
|
|
1405
1512
|
inputs.push({
|
|
1406
1513
|
path: args[++i] ?? "",
|
|
1407
1514
|
format: pendingFormat,
|
|
@@ -1424,22 +1531,28 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
|
|
|
1424
1531
|
videoCodec = val;
|
|
1425
1532
|
audioCodec = val;
|
|
1426
1533
|
} else if (arg === "-c:v" || arg === "-vcodec" || arg === "-codec:v") videoCodec = args[++i];
|
|
1427
|
-
else if (arg === "-c:a" || arg === "-acodec" || arg === "-codec:a")
|
|
1428
|
-
|
|
1534
|
+
else if (arg === "-c:a" || arg === "-acodec" || arg === "-codec:a") {
|
|
1535
|
+
const value = args[++i];
|
|
1536
|
+
if (args.slice(i + 1).includes("-i")) pendingAudioDecoder = value;
|
|
1537
|
+
else audioCodec = value;
|
|
1538
|
+
} else if (arg === "-c:s" || arg === "-scodec" || arg === "-codec:s") i++;
|
|
1429
1539
|
else if (arg === "-an") stripAudio = true;
|
|
1430
1540
|
else if (arg === "-vn") stripVideo = true;
|
|
1431
1541
|
else if (arg === "-sn") stripSubtitles = true;
|
|
1432
1542
|
else if (arg === "-shortest") shortest = true;
|
|
1433
1543
|
else if (arg === "-frames:v" || arg === "-vframes") maxVideoFrames = parseInt(args[++i] ?? "1", 10) || 1;
|
|
1434
|
-
else if (arg === "-frames:a" || arg === "-aframes")
|
|
1435
|
-
|
|
1544
|
+
else if (arg === "-frames:a" || arg === "-aframes") {
|
|
1545
|
+
const value = args[++i];
|
|
1546
|
+
maxAudioFrames = value?.trim() ? Number(value) : NaN;
|
|
1547
|
+
if (!Number.isSafeInteger(maxAudioFrames) || maxAudioFrames < 0) return void 0;
|
|
1548
|
+
} else if (arg === "-movflags") {
|
|
1436
1549
|
const flags = args[++i] ?? "";
|
|
1437
1550
|
if (flags.includes("faststart")) faststart = true;
|
|
1438
1551
|
if (flags.includes("frag_keyframe") || flags.includes("empty_moov")) fragmented = true;
|
|
1439
1552
|
} else if (arg === "-vf" || arg === "-filter:v") {
|
|
1440
1553
|
const f = args[++i];
|
|
1441
1554
|
if (f) vfFilters.push(f);
|
|
1442
|
-
} else if (arg === "-af" || arg === "-filter:a") {
|
|
1555
|
+
} else if (arg === "-af" || arg === "-filter:a" || arg.startsWith("-af:") || arg.startsWith("-filter:a:")) {
|
|
1443
1556
|
return void 0;
|
|
1444
1557
|
} else if (arg === "-disposition" || arg.startsWith("-disposition:")) {
|
|
1445
1558
|
streamDispositions.push([arg.slice("-disposition:".length), args[++i] ?? ""]);
|
|
@@ -1456,13 +1569,17 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
|
|
|
1456
1569
|
}
|
|
1457
1570
|
} else if (arg === "-display_rotation") rotation = Math.abs(parseInt(args[++i] ?? "0", 10) || 0);
|
|
1458
1571
|
else if (arg.startsWith("-") && arg !== "-") return void 0;
|
|
1459
|
-
else
|
|
1572
|
+
else {
|
|
1573
|
+
if (i + 1 < args.length) return void 0;
|
|
1574
|
+
outputTarget = arg;
|
|
1575
|
+
}
|
|
1460
1576
|
}
|
|
1461
1577
|
if (inputs.length === 0 || !outputTarget) return void 0;
|
|
1462
1578
|
if (outputDuration === void 0 && outputTo !== void 0 && outputTo <= (outputSs ?? 0)) return void 0;
|
|
1463
1579
|
const isNullMux = outputFormat === "null" || outputTarget === "/dev/null";
|
|
1580
|
+
if (!isNullMux && !isStdout(outputTarget) && inputs.some((input) => input.format !== "lavfi" && !isStdin(input.path) && input.path === outputTarget)) return void 0;
|
|
1464
1581
|
if (!isNullMux && !writeFileSync) return void 0;
|
|
1465
|
-
if (noOverwrite && !isNullMux && (!readFileSync || readFileSync(outputTarget))) return void 0;
|
|
1582
|
+
if (noOverwrite !== false && !isNullMux && (!readFileSync || readFileSync(outputTarget))) return void 0;
|
|
1466
1583
|
const loadedDocs = [];
|
|
1467
1584
|
for (const inp of inputs) {
|
|
1468
1585
|
let doc;
|
|
@@ -1472,7 +1589,7 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
|
|
|
1472
1589
|
const rawBytes = isStdin(inp.path) ? inBytes : readFileSync?.(inp.path);
|
|
1473
1590
|
if (!rawBytes || rawBytes.byteLength > 262144) return void 0;
|
|
1474
1591
|
budget.checkInputBytes(rawBytes.byteLength);
|
|
1475
|
-
const plugin = registry.detect(rawBytes, inp.path
|
|
1592
|
+
const plugin = inp.format !== void 0 ? registry.findByFormatName(inp.format) : registry.detect(rawBytes, inp.path);
|
|
1476
1593
|
if (!plugin || !plugin.canDemux) return void 0;
|
|
1477
1594
|
doc = plugin.parse(rawBytes, { filename: inp.path, budget });
|
|
1478
1595
|
const imageOutput = (outputFormat ? registry.findByFormatName(outputFormat) : registry.findByFilename(outputTarget))?.id === "image2";
|
|
@@ -1481,12 +1598,15 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
|
|
|
1481
1598
|
if (inp.startSeconds !== void 0 || inp.endSeconds !== void 0 || inp.durationSeconds !== void 0) {
|
|
1482
1599
|
doc = sliceMp4(doc, { startSeconds: inp.startSeconds, endSeconds: inp.endSeconds, durationSeconds: inp.durationSeconds });
|
|
1483
1600
|
}
|
|
1484
|
-
if (inp.streamLoop
|
|
1601
|
+
if (inp.streamLoop === -1) {
|
|
1602
|
+
doc = loopToOutputEnd(doc, outputDuration === void 0 ? outputTo : (outputSs ?? 0) + outputDuration, budget);
|
|
1603
|
+
} else if (inp.streamLoop && inp.streamLoop > 0) {
|
|
1485
1604
|
doc = concatMp4(Array.from({ length: inp.streamLoop + 1 }, () => doc), { budget });
|
|
1486
1605
|
}
|
|
1487
1606
|
loadedDocs.push(doc);
|
|
1488
1607
|
}
|
|
1489
1608
|
let workingDoc = muxMp4(selectAutomaticAudio(loadedDocs), { stripAudio, stripVideo, stripSubtitles, shortest, rotation });
|
|
1609
|
+
validateAudioEncoder(audioCodec, workingDoc, astPlugins);
|
|
1490
1610
|
if (outputSs !== void 0 || outputTo !== void 0 || outputDuration !== void 0) {
|
|
1491
1611
|
workingDoc = sliceMp4(workingDoc, { startSeconds: outputSs, endSeconds: outputDuration === void 0 ? outputTo : void 0, durationSeconds: outputDuration });
|
|
1492
1612
|
}
|
|
@@ -1514,6 +1634,9 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
|
|
|
1514
1634
|
})
|
|
1515
1635
|
};
|
|
1516
1636
|
}
|
|
1637
|
+
if (audioCodec === "copy" && maxAudioFrames !== void 0) {
|
|
1638
|
+
workingDoc = limitAudioCopyPackets(workingDoc, maxAudioFrames);
|
|
1639
|
+
}
|
|
1517
1640
|
if (chapterSource !== void 0) {
|
|
1518
1641
|
if (chapterSource >= loadedDocs.length) return void 0;
|
|
1519
1642
|
workingDoc = { ...workingDoc, chapters: chapterSource < 0 ? void 0 : loadedDocs[chapterSource].chapters };
|
|
@@ -1570,7 +1693,7 @@ function createFfmpegCommand(options = {}) {
|
|
|
1570
1693
|
description: "Multimedia converter, merger, and stream processor powered by pluggable ASTs",
|
|
1571
1694
|
async execute(context) {
|
|
1572
1695
|
const budget = new MediaBudgetTracker(options.limits);
|
|
1573
|
-
const
|
|
1696
|
+
const readSource = createInputReader(context, budget);
|
|
1574
1697
|
const argsObj = getCommandArguments(context);
|
|
1575
1698
|
const args = argsObj.args;
|
|
1576
1699
|
if (args.length === 0) {
|
|
@@ -1593,1178 +1716,1276 @@ function createFfmpegCommand(options = {}) {
|
|
|
1593
1716
|
}
|
|
1594
1717
|
const inputs = [];
|
|
1595
1718
|
let pendingFormat;
|
|
1719
|
+
let pendingAudioDecoder;
|
|
1596
1720
|
let pendingSs;
|
|
1597
1721
|
let pendingTo;
|
|
1598
1722
|
let pendingDuration;
|
|
1599
1723
|
let pendingLoop;
|
|
1600
1724
|
let pendingFps;
|
|
1601
1725
|
let pendingStartNumber;
|
|
1602
|
-
let
|
|
1603
|
-
let outputFormat;
|
|
1604
|
-
let outputSs;
|
|
1605
|
-
let outputTo;
|
|
1606
|
-
let outputDuration;
|
|
1607
|
-
let videoCodec;
|
|
1608
|
-
let audioCodec;
|
|
1609
|
-
let audioRate;
|
|
1610
|
-
let audioChannels;
|
|
1611
|
-
let hlsTime = 2;
|
|
1612
|
-
let hlsListSize = 5;
|
|
1613
|
-
let hlsDeleteSegments = false;
|
|
1614
|
-
let hlsOmitEndlist = false;
|
|
1615
|
-
let hlsIndependentSegments = false;
|
|
1616
|
-
let hlsDeleteThreshold = 1;
|
|
1617
|
-
let hlsSegmentFilename;
|
|
1618
|
-
let hlsSegmentType;
|
|
1619
|
-
let stripAudio = false;
|
|
1620
|
-
let stripVideo = false;
|
|
1621
|
-
let stripSubtitles = false;
|
|
1622
|
-
let shortest = false;
|
|
1623
|
-
let faststart = true;
|
|
1624
|
-
let fragmented = false;
|
|
1625
|
-
let maxVideoFrames;
|
|
1626
|
-
let outputFps;
|
|
1627
|
-
let videoSync = "auto";
|
|
1628
|
-
let outputSize;
|
|
1629
|
-
let rotation;
|
|
1630
|
-
let overwrite = false;
|
|
1631
|
-
const maps = [];
|
|
1632
|
-
const vfFilters = [];
|
|
1633
|
-
const afFilters = [];
|
|
1726
|
+
let overwrite;
|
|
1634
1727
|
let filterComplex;
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
const
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
const
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
if (
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
if (
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1728
|
+
const outputs = [];
|
|
1729
|
+
const inputBytes = /* @__PURE__ */ new Map();
|
|
1730
|
+
const readInput = async (path) => {
|
|
1731
|
+
if (outputs.length === 1) return readSource(path);
|
|
1732
|
+
let bytes = inputBytes.get(path);
|
|
1733
|
+
if (!bytes) {
|
|
1734
|
+
bytes = await readSource(path);
|
|
1735
|
+
inputBytes.set(path, bytes);
|
|
1736
|
+
}
|
|
1737
|
+
return bytes.slice();
|
|
1738
|
+
};
|
|
1739
|
+
let i = 0;
|
|
1740
|
+
while (i < args.length) {
|
|
1741
|
+
let outputStartNumber;
|
|
1742
|
+
let outputFormat;
|
|
1743
|
+
let outputSs;
|
|
1744
|
+
let outputTo;
|
|
1745
|
+
let outputDuration;
|
|
1746
|
+
let videoCodec;
|
|
1747
|
+
let audioCodec;
|
|
1748
|
+
let audioRate;
|
|
1749
|
+
let audioChannels;
|
|
1750
|
+
let hlsTime = 2;
|
|
1751
|
+
let hlsListSize = 5;
|
|
1752
|
+
let hlsDeleteSegments = false;
|
|
1753
|
+
let hlsOmitEndlist = false;
|
|
1754
|
+
let hlsIndependentSegments = false;
|
|
1755
|
+
let hlsDeleteThreshold = 1;
|
|
1756
|
+
let hlsSegmentFilename;
|
|
1757
|
+
let hlsSegmentType;
|
|
1758
|
+
let stripAudio = false;
|
|
1759
|
+
let stripVideo = false;
|
|
1760
|
+
let stripSubtitles = false;
|
|
1761
|
+
let shortest = false;
|
|
1762
|
+
let faststart = true;
|
|
1763
|
+
let fragmented = false;
|
|
1764
|
+
let maxVideoFrames;
|
|
1765
|
+
let maxAudioFrames;
|
|
1766
|
+
let outputFps;
|
|
1767
|
+
let videoSync = "auto";
|
|
1768
|
+
let outputSize;
|
|
1769
|
+
let rotation;
|
|
1770
|
+
const maps = [];
|
|
1771
|
+
const vfFilters = [];
|
|
1772
|
+
const afFilters = [];
|
|
1773
|
+
let chapterSource;
|
|
1774
|
+
let metadataSource;
|
|
1775
|
+
const metadata = {};
|
|
1776
|
+
const streamLanguages = [];
|
|
1777
|
+
const streamDispositions = [];
|
|
1778
|
+
let outputTarget;
|
|
1779
|
+
for (; i < args.length; i++) {
|
|
1780
|
+
const arg = args[i];
|
|
1781
|
+
if (arg === "-y" || arg === "-n") {
|
|
1782
|
+
if (overwrite !== void 0 && overwrite !== (arg === "-y")) {
|
|
1783
|
+
await writeBytes(context.stderr, encodeUtf8("ffmpeg: both -y and -n supplied\n"), context.signal);
|
|
1784
|
+
return { exitCode: 234 };
|
|
1785
|
+
}
|
|
1786
|
+
overwrite = arg === "-y";
|
|
1787
|
+
} else if (arg === "-v" || arg === "-loglevel" || arg === "-safe" || arg === "-threads" || arg === "-pix_fmt" || arg === "-preset" || arg === "-crf" || arg === "-b:v" || arg === "-b:a" || arg === "-bsf:v" || arg === "-bsf:a" || arg === "-tag:v" || arg === "-tag:a" || arg === "-update" || arg === "-loop") {
|
|
1788
|
+
i++;
|
|
1789
|
+
} else if (arg === "-vsync" || arg === "-fps_mode") {
|
|
1790
|
+
videoSync = args[++i] ?? "";
|
|
1791
|
+
} else if (arg === "-hls_time" || arg === "-seg_duration") {
|
|
1792
|
+
hlsTime = parseFloat(args[++i] ?? "2") || 2;
|
|
1793
|
+
} else if (arg === "-hls_segment_filename") {
|
|
1794
|
+
hlsSegmentFilename = args[++i];
|
|
1795
|
+
} else if (arg === "-hls_segment_type") {
|
|
1796
|
+
hlsSegmentType = args[++i];
|
|
1797
|
+
} else if (arg === "-hls_list_size") {
|
|
1798
|
+
const value = args[++i];
|
|
1799
|
+
hlsListSize = value?.trim() ? Number(value) : NaN;
|
|
1800
|
+
if (!Number.isSafeInteger(hlsListSize) || hlsListSize < 0) {
|
|
1801
|
+
await writeBytes(context.stderr, encodeUtf8("ffmpeg: invalid hls_list_size\n"), context.signal);
|
|
1802
|
+
return { exitCode: 1 };
|
|
1803
|
+
}
|
|
1804
|
+
} else if (arg === "-hls_flags") {
|
|
1805
|
+
const value = args[++i] ?? "";
|
|
1806
|
+
if (!value.startsWith("+") && !value.startsWith("-")) {
|
|
1807
|
+
hlsDeleteSegments = false;
|
|
1808
|
+
hlsOmitEndlist = false;
|
|
1809
|
+
hlsIndependentSegments = false;
|
|
1810
|
+
}
|
|
1811
|
+
for (const flag of value.replaceAll("-", "+-").split("+")) {
|
|
1812
|
+
if (flag === "delete_segments") hlsDeleteSegments = true;
|
|
1813
|
+
if (flag === "-delete_segments") hlsDeleteSegments = false;
|
|
1814
|
+
if (flag === "independent_segments") hlsIndependentSegments = true;
|
|
1815
|
+
if (flag === "-independent_segments") hlsIndependentSegments = false;
|
|
1816
|
+
if (flag === "omit_endlist") hlsOmitEndlist = true;
|
|
1817
|
+
if (flag === "-omit_endlist") hlsOmitEndlist = false;
|
|
1818
|
+
}
|
|
1819
|
+
} else if (arg === "-hls_delete_threshold") {
|
|
1820
|
+
const value = args[++i];
|
|
1821
|
+
hlsDeleteThreshold = value?.trim() ? Number(value) : NaN;
|
|
1822
|
+
if (!Number.isSafeInteger(hlsDeleteThreshold) || hlsDeleteThreshold < 1) {
|
|
1823
|
+
await writeBytes(context.stderr, encodeUtf8("ffmpeg: invalid hls_delete_threshold\n"), context.signal);
|
|
1824
|
+
return { exitCode: 1 };
|
|
1825
|
+
}
|
|
1826
|
+
} else if (arg === "-map_chapters") {
|
|
1827
|
+
const value = args[++i];
|
|
1828
|
+
chapterSource = value?.trim() ? Number(value) : NaN;
|
|
1829
|
+
if (!Number.isSafeInteger(chapterSource)) {
|
|
1830
|
+
await writeBytes(context.stderr, encodeUtf8("ffmpeg: invalid chapter input index\n"), context.signal);
|
|
1831
|
+
return { exitCode: 1 };
|
|
1832
|
+
}
|
|
1833
|
+
} else if (arg === "-map_metadata") {
|
|
1834
|
+
const value = args[++i];
|
|
1835
|
+
metadataSource = value?.trim() ? Number(value) : NaN;
|
|
1836
|
+
if (!Number.isSafeInteger(metadataSource)) {
|
|
1837
|
+
await writeBytes(context.stderr, encodeUtf8("ffmpeg: invalid metadata input index\n"), context.signal);
|
|
1838
|
+
return { exitCode: 1 };
|
|
1839
|
+
}
|
|
1840
|
+
} else if (arg === "-start_number") {
|
|
1841
|
+
const val = Number(args[++i]);
|
|
1842
|
+
if (!Number.isSafeInteger(val) || val < 0) {
|
|
1843
|
+
await writeBytes(context.stderr, encodeUtf8("ffmpeg: invalid start_number\n"), context.signal);
|
|
1844
|
+
return { exitCode: 1 };
|
|
1845
|
+
}
|
|
1846
|
+
if (args.slice(i + 1).includes("-i")) pendingStartNumber = val;
|
|
1847
|
+
else outputStartNumber = val;
|
|
1848
|
+
} else if (arg === "-ar") {
|
|
1849
|
+
audioRate = parseInt(args[++i] ?? "0", 10) || void 0;
|
|
1850
|
+
if (audioRate !== void 0 && audioRate < 0) {
|
|
1851
|
+
await writeBytes(context.stderr, encodeUtf8("ffmpeg: sample rate must be nonnegative\n"), context.signal);
|
|
1852
|
+
return { exitCode: 1 };
|
|
1853
|
+
}
|
|
1854
|
+
} else if (arg === "-ac") {
|
|
1855
|
+
audioChannels = parseInt(args[++i] ?? "0", 10) || void 0;
|
|
1856
|
+
} else if (arg === "-hide_banner" || arg === "-nostdin" || arg === "-stats") {
|
|
1857
|
+
} else if (arg === "-f") {
|
|
1858
|
+
const val = args[++i];
|
|
1859
|
+
if (inputs.length === 0 || i + 1 < args.length && args.slice(i + 1).includes("-i")) {
|
|
1860
|
+
pendingFormat = val;
|
|
1861
|
+
} else {
|
|
1862
|
+
outputFormat = val;
|
|
1863
|
+
}
|
|
1864
|
+
} else if (arg === "-ss") {
|
|
1865
|
+
const val = parseFfmpegTimestamp(args[++i] ?? "0");
|
|
1866
|
+
if (args.slice(i + 1).includes("-i")) {
|
|
1867
|
+
pendingSs = val;
|
|
1868
|
+
} else {
|
|
1869
|
+
outputSs = val;
|
|
1870
|
+
}
|
|
1871
|
+
} else if (arg === "-to") {
|
|
1872
|
+
const val = parseFfmpegTimestamp(args[++i] ?? "0");
|
|
1873
|
+
if (args.slice(i + 1).includes("-i")) {
|
|
1874
|
+
pendingTo = val;
|
|
1875
|
+
} else {
|
|
1876
|
+
outputTo = val;
|
|
1877
|
+
}
|
|
1878
|
+
} else if (arg === "-t") {
|
|
1879
|
+
const val = parseFfmpegTimestamp(args[++i] ?? "0");
|
|
1880
|
+
if (val < 0) {
|
|
1881
|
+
await writeBytes(context.stderr, encodeUtf8("ffmpeg: -t duration must be nonnegative\n"), context.signal);
|
|
1882
|
+
return { exitCode: 1 };
|
|
1883
|
+
}
|
|
1884
|
+
if (args.slice(i + 1).includes("-i")) {
|
|
1885
|
+
pendingDuration = val;
|
|
1886
|
+
} else {
|
|
1887
|
+
outputDuration = val;
|
|
1888
|
+
}
|
|
1889
|
+
} else if (arg === "-stream_loop") {
|
|
1890
|
+
pendingLoop = parseInt(args[++i] ?? "0", 10) || 0;
|
|
1891
|
+
} else if (arg === "-r" || arg === "-framerate") {
|
|
1892
|
+
const val = parseFloat(args[++i] ?? "25") || 25;
|
|
1893
|
+
if (args.slice(i + 1).includes("-i")) {
|
|
1894
|
+
pendingFps = val;
|
|
1895
|
+
} else {
|
|
1896
|
+
outputFps = val;
|
|
1897
|
+
}
|
|
1898
|
+
} else if (arg === "-s") {
|
|
1899
|
+
outputSize = args[++i];
|
|
1900
|
+
} else if (arg === "-i") {
|
|
1901
|
+
if (pendingAudioDecoder !== void 0 && !astPlugins.some((plugin) => plugin.canDemux && plugin.supportedAudioCodecs.includes(pendingAudioDecoder))) {
|
|
1902
|
+
await writeBytes(context.stderr, encodeUtf8(`ffmpeg: Unknown decoder '${pendingAudioDecoder}'
|
|
1903
|
+
`), context.signal);
|
|
1904
|
+
return { exitCode: 1 };
|
|
1905
|
+
}
|
|
1906
|
+
pendingAudioDecoder = void 0;
|
|
1907
|
+
const path = args[++i] ?? "";
|
|
1908
|
+
inputs.push({
|
|
1909
|
+
path,
|
|
1910
|
+
format: pendingFormat,
|
|
1911
|
+
startSeconds: pendingSs,
|
|
1912
|
+
endSeconds: pendingTo,
|
|
1913
|
+
durationSeconds: pendingDuration,
|
|
1914
|
+
streamLoop: pendingLoop,
|
|
1915
|
+
fps: pendingFps,
|
|
1916
|
+
startNumber: pendingStartNumber
|
|
1917
|
+
});
|
|
1918
|
+
pendingFormat = void 0;
|
|
1919
|
+
pendingSs = void 0;
|
|
1920
|
+
pendingTo = void 0;
|
|
1921
|
+
pendingDuration = void 0;
|
|
1922
|
+
pendingLoop = void 0;
|
|
1923
|
+
pendingFps = void 0;
|
|
1924
|
+
pendingStartNumber = void 0;
|
|
1925
|
+
} else if (arg === "-c" || arg === "-codec") {
|
|
1926
|
+
const val = args[++i];
|
|
1927
|
+
videoCodec = val;
|
|
1928
|
+
audioCodec = val;
|
|
1929
|
+
} else if (arg === "-c:v" || arg === "-vcodec" || arg === "-codec:v") {
|
|
1930
|
+
videoCodec = args[++i];
|
|
1931
|
+
} else if (arg === "-c:a" || arg === "-acodec" || arg === "-codec:a") {
|
|
1932
|
+
const value = args[++i];
|
|
1933
|
+
if (args.slice(i + 1).includes("-i")) pendingAudioDecoder = value;
|
|
1934
|
+
else audioCodec = value;
|
|
1935
|
+
} else if (arg === "-c:s" || arg === "-scodec" || arg === "-codec:s") {
|
|
1936
|
+
i++;
|
|
1937
|
+
} else if (arg === "-an") {
|
|
1938
|
+
stripAudio = true;
|
|
1939
|
+
} else if (arg === "-vn") {
|
|
1940
|
+
stripVideo = true;
|
|
1941
|
+
} else if (arg === "-sn") {
|
|
1942
|
+
stripSubtitles = true;
|
|
1943
|
+
} else if (arg === "-shortest") {
|
|
1944
|
+
shortest = true;
|
|
1945
|
+
} else if (arg === "-frames:v" || arg === "-vframes") {
|
|
1946
|
+
maxVideoFrames = parseInt(args[++i] ?? "1", 10) || 1;
|
|
1947
|
+
} else if (arg === "-frames:a" || arg === "-aframes") {
|
|
1948
|
+
const value = args[++i];
|
|
1949
|
+
maxAudioFrames = value?.trim() ? Number(value) : NaN;
|
|
1950
|
+
if (!Number.isSafeInteger(maxAudioFrames) || maxAudioFrames < 0) {
|
|
1951
|
+
await writeBytes(context.stderr, encodeUtf8("ffmpeg: invalid audio frame limit\n"), context.signal);
|
|
1952
|
+
return { exitCode: 1 };
|
|
1953
|
+
}
|
|
1954
|
+
} else if (arg === "-movflags") {
|
|
1955
|
+
const flags = args[++i] ?? "";
|
|
1956
|
+
if (flags.includes("faststart")) faststart = true;
|
|
1957
|
+
if (flags.includes("frag_keyframe") || flags.includes("empty_moov")) {
|
|
1958
|
+
fragmented = true;
|
|
1959
|
+
}
|
|
1960
|
+
} else if (arg === "-map") {
|
|
1961
|
+
const m = args[++i];
|
|
1962
|
+
if (m) maps.push(m);
|
|
1963
|
+
} else if (arg === "-vf" || arg === "-filter:v") {
|
|
1964
|
+
const f = args[++i];
|
|
1965
|
+
if (f) vfFilters.push(f);
|
|
1966
|
+
} else if (arg === "-af" || arg === "-filter:a" || arg.startsWith("-af:") || arg.startsWith("-filter:a:")) {
|
|
1967
|
+
const f = args[++i];
|
|
1968
|
+
const selector = arg.startsWith("-af:") ? arg.slice(4) : arg.startsWith("-filter:a:") ? arg.slice(10) : void 0;
|
|
1969
|
+
const ordinal = selector === void 0 ? void 0 : Number(selector);
|
|
1970
|
+
if (selector !== void 0 && (!selector || !Array.from(selector).every((char) => char >= "0" && char <= "9") || !Number.isSafeInteger(ordinal))) {
|
|
1971
|
+
throw new Error(`Unsupported audio stream specifier: ${selector}`);
|
|
1972
|
+
}
|
|
1973
|
+
if (f) afFilters.push({ ordinal, expression: f });
|
|
1974
|
+
} else if (arg === "-filter_complex" || arg === "-lavfi") {
|
|
1975
|
+
filterComplex = args[++i];
|
|
1976
|
+
} else if (arg === "-disposition" || arg.startsWith("-disposition:")) {
|
|
1977
|
+
streamDispositions.push([arg.slice("-disposition:".length), args[++i] ?? ""]);
|
|
1978
|
+
} else if (arg === "-metadata" || arg.startsWith("-metadata:")) {
|
|
1979
|
+
const kv = args[++i] ?? "";
|
|
1980
|
+
const eq = kv.indexOf("=");
|
|
1981
|
+
if (eq >= 0) {
|
|
1982
|
+
const k = kv.slice(0, eq).trim().toLowerCase();
|
|
1983
|
+
const v = kv.slice(eq + 1).trim();
|
|
1984
|
+
if (k === "language" && (arg === "-metadata:s" || arg.startsWith("-metadata:s:"))) {
|
|
1985
|
+
streamLanguages.push([arg.slice("-metadata:s:".length), v]);
|
|
1986
|
+
} else if (k === "rotate") rotation = parseInt(v, 10) || 0;
|
|
1987
|
+
else metadata[k] = v;
|
|
1988
|
+
}
|
|
1989
|
+
} else if (arg === "-display_rotation") {
|
|
1990
|
+
rotation = Math.abs(parseInt(args[++i] ?? "0", 10) || 0);
|
|
1991
|
+
} else if (!arg.startsWith("-") || arg === "-") {
|
|
1992
|
+
outputTarget = arg;
|
|
1993
|
+
i++;
|
|
1994
|
+
break;
|
|
1747
1995
|
} else {
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
outputSize = args[++i];
|
|
1752
|
-
} else if (arg === "-i") {
|
|
1753
|
-
const path = args[++i] ?? "";
|
|
1754
|
-
inputs.push({
|
|
1755
|
-
path,
|
|
1756
|
-
format: pendingFormat,
|
|
1757
|
-
startSeconds: pendingSs,
|
|
1758
|
-
endSeconds: pendingTo,
|
|
1759
|
-
durationSeconds: pendingDuration,
|
|
1760
|
-
streamLoop: pendingLoop,
|
|
1761
|
-
fps: pendingFps,
|
|
1762
|
-
startNumber: pendingStartNumber
|
|
1763
|
-
});
|
|
1764
|
-
pendingFormat = void 0;
|
|
1765
|
-
pendingSs = void 0;
|
|
1766
|
-
pendingTo = void 0;
|
|
1767
|
-
pendingDuration = void 0;
|
|
1768
|
-
pendingLoop = void 0;
|
|
1769
|
-
pendingFps = void 0;
|
|
1770
|
-
pendingStartNumber = void 0;
|
|
1771
|
-
} else if (arg === "-c" || arg === "-codec") {
|
|
1772
|
-
const val = args[++i];
|
|
1773
|
-
videoCodec = val;
|
|
1774
|
-
audioCodec = val;
|
|
1775
|
-
} else if (arg === "-c:v" || arg === "-vcodec" || arg === "-codec:v") {
|
|
1776
|
-
videoCodec = args[++i];
|
|
1777
|
-
} else if (arg === "-c:a" || arg === "-acodec" || arg === "-codec:a") {
|
|
1778
|
-
audioCodec = args[++i];
|
|
1779
|
-
} else if (arg === "-c:s" || arg === "-scodec" || arg === "-codec:s") {
|
|
1780
|
-
i++;
|
|
1781
|
-
} else if (arg === "-an") {
|
|
1782
|
-
stripAudio = true;
|
|
1783
|
-
} else if (arg === "-vn") {
|
|
1784
|
-
stripVideo = true;
|
|
1785
|
-
} else if (arg === "-sn") {
|
|
1786
|
-
stripSubtitles = true;
|
|
1787
|
-
} else if (arg === "-shortest") {
|
|
1788
|
-
shortest = true;
|
|
1789
|
-
} else if (arg === "-frames:v" || arg === "-vframes") {
|
|
1790
|
-
maxVideoFrames = parseInt(args[++i] ?? "1", 10) || 1;
|
|
1791
|
-
} else if (arg === "-frames:a" || arg === "-aframes") {
|
|
1792
|
-
i++;
|
|
1793
|
-
} else if (arg === "-movflags") {
|
|
1794
|
-
const flags = args[++i] ?? "";
|
|
1795
|
-
if (flags.includes("faststart")) faststart = true;
|
|
1796
|
-
if (flags.includes("frag_keyframe") || flags.includes("empty_moov")) {
|
|
1797
|
-
fragmented = true;
|
|
1996
|
+
await writeBytes(context.stderr, encodeUtf8(`ffmpeg: Unrecognized option '${arg.slice(1)}'
|
|
1997
|
+
`), context.signal);
|
|
1998
|
+
return { exitCode: 8 };
|
|
1798
1999
|
}
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
const f = args[++i];
|
|
1804
|
-
if (f) vfFilters.push(f);
|
|
1805
|
-
} else if (arg === "-af" || arg === "-filter:a") {
|
|
1806
|
-
const f = args[++i];
|
|
1807
|
-
if (f) afFilters.push(f);
|
|
1808
|
-
} else if (arg === "-filter_complex" || arg === "-lavfi") {
|
|
1809
|
-
filterComplex = args[++i];
|
|
1810
|
-
} else if (arg === "-disposition" || arg.startsWith("-disposition:")) {
|
|
1811
|
-
streamDispositions.push([arg.slice("-disposition:".length), args[++i] ?? ""]);
|
|
1812
|
-
} else if (arg === "-metadata" || arg.startsWith("-metadata:")) {
|
|
1813
|
-
const kv = args[++i] ?? "";
|
|
1814
|
-
const eq = kv.indexOf("=");
|
|
1815
|
-
if (eq >= 0) {
|
|
1816
|
-
const k = kv.slice(0, eq).trim().toLowerCase();
|
|
1817
|
-
const v = kv.slice(eq + 1).trim();
|
|
1818
|
-
if (k === "language" && (arg === "-metadata:s" || arg.startsWith("-metadata:s:"))) {
|
|
1819
|
-
streamLanguages.push([arg.slice("-metadata:s:".length), v]);
|
|
1820
|
-
} else if (k === "rotate") rotation = parseInt(v, 10) || 0;
|
|
1821
|
-
else metadata[k] = v;
|
|
2000
|
+
if (i >= args.length) {
|
|
2001
|
+
await writeBytes(context.stderr, encodeUtf8(`ffmpeg: Missing argument for option '${arg.slice(1)}'
|
|
2002
|
+
`), context.signal);
|
|
2003
|
+
return { exitCode: 234 };
|
|
1822
2004
|
}
|
|
1823
|
-
} else if (arg === "-display_rotation") {
|
|
1824
|
-
rotation = Math.abs(parseInt(args[++i] ?? "0", 10) || 0);
|
|
1825
|
-
} else if (!arg.startsWith("-") || arg === "-") {
|
|
1826
|
-
outputTarget = arg;
|
|
1827
2005
|
}
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
return { exitCode: 1 };
|
|
1836
|
-
}
|
|
1837
|
-
if (!outputTarget) {
|
|
1838
|
-
await writeBytes(
|
|
1839
|
-
context.stderr,
|
|
1840
|
-
encodeUtf8("ffmpeg: must specify an output file\n"),
|
|
1841
|
-
context.signal
|
|
1842
|
-
);
|
|
1843
|
-
return { exitCode: 1 };
|
|
1844
|
-
}
|
|
1845
|
-
if (outputDuration === void 0 && outputTo !== void 0 && outputTo <= (outputSs ?? 0)) {
|
|
1846
|
-
await writeBytes(context.stderr, encodeUtf8("ffmpeg: -to value smaller than -ss; aborting.\n"), context.signal);
|
|
1847
|
-
return { exitCode: 1 };
|
|
1848
|
-
}
|
|
1849
|
-
try {
|
|
1850
|
-
if (!overwrite && !isStdout(outputTarget) && outputFormat !== "null") {
|
|
1851
|
-
const outFull = resolvePath(context.cwd, outputTarget);
|
|
1852
|
-
try {
|
|
1853
|
-
await context.fs.stat(outFull, { signal: context.signal });
|
|
1854
|
-
await writeBytes(
|
|
1855
|
-
context.stderr,
|
|
1856
|
-
encodeUtf8(`File '${outputTarget}' already exists. Exiting.
|
|
1857
|
-
`),
|
|
1858
|
-
context.signal
|
|
1859
|
-
);
|
|
1860
|
-
return { exitCode: 1 };
|
|
1861
|
-
} catch (error) {
|
|
1862
|
-
rethrowRuntimeError(context, error);
|
|
1863
|
-
if (error.code !== "ENOENT") throw error;
|
|
1864
|
-
}
|
|
2006
|
+
if (inputs.length === 0) {
|
|
2007
|
+
await writeBytes(
|
|
2008
|
+
context.stderr,
|
|
2009
|
+
encodeUtf8("ffmpeg: must specify at least one input (-i)\n"),
|
|
2010
|
+
context.signal
|
|
2011
|
+
);
|
|
2012
|
+
return { exitCode: 1 };
|
|
1865
2013
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
while (!step.done) {
|
|
1874
|
-
await yieldTurn5(context.signal);
|
|
1875
|
-
context.signal.throwIfAborted();
|
|
1876
|
-
step = steps.next();
|
|
1877
|
-
}
|
|
1878
|
-
return step.value;
|
|
2014
|
+
if (!outputTarget) break;
|
|
2015
|
+
const target = outputTarget;
|
|
2016
|
+
outputs.push(async () => {
|
|
2017
|
+
const outputTarget2 = target;
|
|
2018
|
+
if (outputDuration === void 0 && outputTo !== void 0 && outputTo <= (outputSs ?? 0)) {
|
|
2019
|
+
await writeBytes(context.stderr, encodeUtf8("ffmpeg: -to value smaller than -ss; aborting.\n"), context.signal);
|
|
2020
|
+
return { exitCode: 1 };
|
|
1879
2021
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
for (const p of parts) {
|
|
1885
|
-
docs.push(await loadSingleDocument(p));
|
|
2022
|
+
try {
|
|
2023
|
+
if (outputFormat !== "null" && outputTarget2 !== "/dev/null" && !isStdout(outputTarget2)) {
|
|
2024
|
+
const inputIndex = inputs.findIndex((input) => input.format !== "lavfi" && !isStdin(input.path) && input.path === outputTarget2);
|
|
2025
|
+
if (inputIndex >= 0) throw new Error(`Output ${outputTarget2} same as Input #${inputIndex} - exiting. FFmpeg cannot edit existing files in-place.`);
|
|
1886
2026
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
if (/%0?\d*d/.test(filePath)) {
|
|
1890
|
-
const frames = [];
|
|
1891
|
-
const fps = fpsHint ?? 25;
|
|
1892
|
-
for (let idx = input?.startNumber ?? 0; ; idx++) {
|
|
1893
|
-
budget.checkCpu();
|
|
1894
|
-
context.signal.throwIfAborted();
|
|
1895
|
-
const formatted = filePath.replace(/%0?(\d*)d/, (_, widthDigits) => {
|
|
1896
|
-
const padLen = parseInt(widthDigits || "0", 10) || 0;
|
|
1897
|
-
return String(idx).padStart(padLen, "0");
|
|
1898
|
-
});
|
|
1899
|
-
const resolved = resolvePath(context.cwd, formatted);
|
|
1900
|
-
let imgBytes;
|
|
2027
|
+
if (!overwrite && !isStdout(outputTarget2) && outputFormat !== "null") {
|
|
2028
|
+
const outFull = resolvePath(context.cwd, outputTarget2);
|
|
1901
2029
|
try {
|
|
1902
|
-
|
|
2030
|
+
await context.fs.stat(outFull, { signal: context.signal });
|
|
2031
|
+
await writeBytes(
|
|
2032
|
+
context.stderr,
|
|
2033
|
+
encodeUtf8(`File '${outputTarget2}' already exists. Exiting.
|
|
2034
|
+
`),
|
|
2035
|
+
context.signal
|
|
2036
|
+
);
|
|
2037
|
+
return { exitCode: 1 };
|
|
1903
2038
|
} catch (error) {
|
|
1904
2039
|
rethrowRuntimeError(context, error);
|
|
1905
2040
|
if (error.code !== "ENOENT") throw error;
|
|
1906
|
-
if (idx === 0 && input?.startNumber === void 0) continue;
|
|
1907
|
-
break;
|
|
1908
2041
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
if (
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2042
|
+
}
|
|
2043
|
+
const loadSingleDocument = async (filePath, explicitFormat, fpsHint, input) => {
|
|
2044
|
+
if (explicitFormat === "lavfi") {
|
|
2045
|
+
if (features.lavfiSources === false) {
|
|
2046
|
+
throw new Error("lavfi synthetic sources are disabled by consumer feature configuration");
|
|
2047
|
+
}
|
|
2048
|
+
const steps = lavfiSteps(filePath, budget, input?.durationSeconds !== void 0 ? (input.startSeconds ?? 0) + input.durationSeconds : input?.endSeconds ?? (outputDuration !== void 0 ? (input?.startSeconds ?? 0) + (outputSs ?? 0) + outputDuration : outputTo !== void 0 ? (input?.startSeconds ?? 0) + outputTo : void 0));
|
|
2049
|
+
let step = steps.next();
|
|
2050
|
+
while (!step.done) {
|
|
2051
|
+
await yieldTurn5(context.signal);
|
|
2052
|
+
context.signal.throwIfAborted();
|
|
2053
|
+
step = steps.next();
|
|
2054
|
+
}
|
|
2055
|
+
return step.value;
|
|
2056
|
+
}
|
|
2057
|
+
if (filePath.startsWith("concat:")) {
|
|
2058
|
+
const parts = filePath.slice("concat:".length).split("|").map((x) => x.trim()).filter(Boolean);
|
|
2059
|
+
budget.checkConcatInputs(parts.length);
|
|
2060
|
+
const docs = [];
|
|
2061
|
+
for (const p of parts) {
|
|
2062
|
+
docs.push(await loadSingleDocument(p));
|
|
2063
|
+
}
|
|
2064
|
+
return concatMp4(docs, { limits: options.limits, budget });
|
|
2065
|
+
}
|
|
2066
|
+
if (/%0?\d*d/.test(filePath)) {
|
|
2067
|
+
const frames = [];
|
|
2068
|
+
const fps = fpsHint ?? 25;
|
|
2069
|
+
for (let idx = input?.startNumber ?? 0; ; idx++) {
|
|
2070
|
+
budget.checkCpu();
|
|
2071
|
+
context.signal.throwIfAborted();
|
|
2072
|
+
const formatted = filePath.replace(/%0?(\d*)d/, (_, widthDigits) => {
|
|
2073
|
+
const padLen = parseInt(widthDigits || "0", 10) || 0;
|
|
2074
|
+
return String(idx).padStart(padLen, "0");
|
|
2075
|
+
});
|
|
2076
|
+
const resolved = resolvePath(context.cwd, formatted);
|
|
2077
|
+
let imgBytes;
|
|
2078
|
+
try {
|
|
2079
|
+
imgBytes = await readInput(resolved);
|
|
2080
|
+
} catch (error) {
|
|
2081
|
+
rethrowRuntimeError(context, error);
|
|
2082
|
+
if (error.code !== "ENOENT") throw error;
|
|
2083
|
+
if (idx === 0 && input?.startNumber === void 0) continue;
|
|
2084
|
+
break;
|
|
2085
|
+
}
|
|
2086
|
+
{
|
|
2087
|
+
const decoded = decodeImage(imgBytes);
|
|
2088
|
+
detachBytesBuffer(imgBytes);
|
|
2089
|
+
if (decoded.width * decoded.height >= 16384) {
|
|
2090
|
+
const gc = globalThis.gc;
|
|
2091
|
+
if (typeof gc === "function") {
|
|
2092
|
+
gc();
|
|
2093
|
+
gc();
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
budget.recordFrame(decoded.width, decoded.height);
|
|
2097
|
+
frames.push({
|
|
2098
|
+
width: decoded.width,
|
|
2099
|
+
height: decoded.height,
|
|
2100
|
+
data: decoded.data,
|
|
2101
|
+
ptsSeconds: frames.length / fps,
|
|
2102
|
+
durationSeconds: 1 / fps,
|
|
2103
|
+
keyframe: true
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
if (frames.length === 0) {
|
|
2108
|
+
throw new Error(`${filePath}: No matching image sequence files found`);
|
|
2109
|
+
}
|
|
2110
|
+
const first = frames[0];
|
|
2111
|
+
const synthBytes = (await yieldTurn5(context.signal), context.signal?.throwIfAborted(), createSyntheticMp4({
|
|
2112
|
+
width: 16,
|
|
2113
|
+
height: 16,
|
|
2114
|
+
fps,
|
|
2115
|
+
frameCount: 1,
|
|
2116
|
+
includeAudio: false
|
|
2117
|
+
}));
|
|
2118
|
+
const base = parseMp4(synthBytes);
|
|
2119
|
+
detachBytesBuffer(synthBytes);
|
|
2120
|
+
return {
|
|
2121
|
+
...base,
|
|
2122
|
+
tracks: base.tracks.map(
|
|
2123
|
+
(t) => t.type === "video" ? { ...t, width: first.width, height: first.height, samples: [], decodedVideoFrames: frames } : t
|
|
2124
|
+
)
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
const rawBytes = await readInput(filePath);
|
|
2128
|
+
if (explicitFormat === "concat") {
|
|
2129
|
+
const listText = new TextDecoder().decode(rawBytes);
|
|
2130
|
+
const listDir = filePath === "-" ? context.cwd : dirnameOf(resolvePath(context.cwd, filePath));
|
|
2131
|
+
const concatDocs = [];
|
|
2132
|
+
let currentInpoint;
|
|
2133
|
+
let currentOutpoint;
|
|
2134
|
+
let currentDuration;
|
|
2135
|
+
const lines = listText.split(/\r?\n/);
|
|
2136
|
+
for (let l = 0; l < lines.length; l++) {
|
|
2137
|
+
const line = lines[l].trim();
|
|
2138
|
+
if (!line || line.startsWith("#") || line.startsWith("ffconcat")) continue;
|
|
2139
|
+
if (line.startsWith("file ")) {
|
|
2140
|
+
let target2 = line.slice(5).trim();
|
|
2141
|
+
if (target2.startsWith("'") && target2.endsWith("'") || target2.startsWith('"') && target2.endsWith('"')) {
|
|
2142
|
+
target2 = target2.slice(1, -1);
|
|
2143
|
+
}
|
|
2144
|
+
const resolvedItem = resolvePath(listDir, target2);
|
|
2145
|
+
currentInpoint = void 0;
|
|
2146
|
+
currentOutpoint = void 0;
|
|
2147
|
+
currentDuration = void 0;
|
|
2148
|
+
while (l + 1 < lines.length) {
|
|
2149
|
+
const nextLine = lines[l + 1].trim();
|
|
2150
|
+
if (nextLine.startsWith("inpoint ")) {
|
|
2151
|
+
currentInpoint = parseFfmpegTimestamp(nextLine.slice(8));
|
|
2152
|
+
l++;
|
|
2153
|
+
} else if (nextLine.startsWith("outpoint ")) {
|
|
2154
|
+
currentOutpoint = parseFfmpegTimestamp(nextLine.slice(9));
|
|
2155
|
+
l++;
|
|
2156
|
+
} else if (nextLine.startsWith("duration ")) {
|
|
2157
|
+
currentDuration = parseFfmpegTimestamp(nextLine.slice(9));
|
|
2158
|
+
l++;
|
|
2159
|
+
} else {
|
|
2160
|
+
break;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
let itemDoc = await loadSingleDocument(resolvedItem);
|
|
2164
|
+
if (currentInpoint !== void 0 || currentOutpoint !== void 0 || currentDuration !== void 0) {
|
|
2165
|
+
itemDoc = sliceMp4(itemDoc, {
|
|
2166
|
+
startSeconds: currentInpoint,
|
|
2167
|
+
endSeconds: currentOutpoint,
|
|
2168
|
+
durationSeconds: currentDuration
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
concatDocs.push(itemDoc);
|
|
1917
2172
|
}
|
|
1918
2173
|
}
|
|
1919
|
-
budget.
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2174
|
+
budget.checkConcatInputs(concatDocs.length);
|
|
2175
|
+
return concatMp4(concatDocs, { limits: options.limits, budget });
|
|
2176
|
+
}
|
|
2177
|
+
const plugin = explicitFormat !== void 0 ? registry.findByFormatName(explicitFormat) : registry.detect(rawBytes, filePath);
|
|
2178
|
+
if (!plugin || !plugin.canDemux) {
|
|
2179
|
+
throw new Error(
|
|
2180
|
+
explicitFormat ? `Unknown input format: '${explicitFormat}' (AST not registered)` : `${filePath}: Unsupported input format (AST not registered)`
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
2183
|
+
const resolveResource = await loadManifestResources(plugin, rawBytes, filePath, context.cwd, readInput, budget);
|
|
2184
|
+
return plugin.parse(rawBytes, {
|
|
2185
|
+
resolveResource,
|
|
2186
|
+
filename: filePath,
|
|
2187
|
+
limits: options.limits,
|
|
2188
|
+
budget
|
|
2189
|
+
});
|
|
2190
|
+
};
|
|
2191
|
+
const loadedDocs = [];
|
|
2192
|
+
for (const inp of inputs) {
|
|
2193
|
+
await yieldTurn5(context.signal);
|
|
2194
|
+
context.signal.throwIfAborted();
|
|
2195
|
+
let doc = await loadSingleDocument(inp.path, inp.format, inp.fps, inp);
|
|
2196
|
+
if (inp.startSeconds !== void 0 || inp.endSeconds !== void 0 || inp.durationSeconds !== void 0) {
|
|
2197
|
+
doc = sliceMp4(doc, {
|
|
2198
|
+
startSeconds: inp.startSeconds,
|
|
2199
|
+
endSeconds: inp.endSeconds,
|
|
2200
|
+
durationSeconds: inp.durationSeconds
|
|
1927
2201
|
});
|
|
1928
2202
|
}
|
|
2203
|
+
if (inp.streamLoop === -1) {
|
|
2204
|
+
doc = loopToOutputEnd(doc, outputDuration === void 0 ? outputTo : (outputSs ?? 0) + outputDuration, budget);
|
|
2205
|
+
} else if (inp.streamLoop && inp.streamLoop > 0) {
|
|
2206
|
+
const copies = Array.from({ length: inp.streamLoop + 1 }, () => doc);
|
|
2207
|
+
doc = concatMp4(copies, { limits: options.limits, budget });
|
|
2208
|
+
}
|
|
2209
|
+
loadedDocs.push(doc);
|
|
1929
2210
|
}
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
detachBytesBuffer(synthBytes);
|
|
1943
|
-
return {
|
|
1944
|
-
...base,
|
|
1945
|
-
tracks: base.tracks.map(
|
|
1946
|
-
(t) => t.type === "video" ? { ...t, width: first.width, height: first.height, samples: [], decodedVideoFrames: frames } : t
|
|
1947
|
-
)
|
|
1948
|
-
};
|
|
1949
|
-
}
|
|
1950
|
-
const rawBytes = await readInput(filePath);
|
|
1951
|
-
if (explicitFormat === "concat") {
|
|
1952
|
-
const listText = new TextDecoder().decode(rawBytes);
|
|
1953
|
-
const listDir = filePath === "-" ? context.cwd : dirnameOf(resolvePath(context.cwd, filePath));
|
|
1954
|
-
const concatDocs = [];
|
|
1955
|
-
let currentInpoint;
|
|
1956
|
-
let currentOutpoint;
|
|
1957
|
-
let currentDuration;
|
|
1958
|
-
const lines = listText.split(/\r?\n/);
|
|
1959
|
-
for (let l = 0; l < lines.length; l++) {
|
|
1960
|
-
const line = lines[l].trim();
|
|
1961
|
-
if (!line || line.startsWith("#") || line.startsWith("ffconcat")) continue;
|
|
1962
|
-
if (line.startsWith("file ")) {
|
|
1963
|
-
let target = line.slice(5).trim();
|
|
1964
|
-
if (target.startsWith("'") && target.endsWith("'") || target.startsWith('"') && target.endsWith('"')) {
|
|
1965
|
-
target = target.slice(1, -1);
|
|
2211
|
+
let workingDoc;
|
|
2212
|
+
if (filterComplex) {
|
|
2213
|
+
if (features.filterGraph === false) {
|
|
2214
|
+
throw new Error("Filtergraph processing is disabled by consumer feature configuration");
|
|
2215
|
+
}
|
|
2216
|
+
if (filterComplex.includes("concat=")) {
|
|
2217
|
+
workingDoc = concatMp4(loadedDocs, { limits: options.limits, budget });
|
|
2218
|
+
if (/concat=[^;[]*\ba=0\b/.test(filterComplex)) {
|
|
2219
|
+
workingDoc = {
|
|
2220
|
+
...workingDoc,
|
|
2221
|
+
tracks: workingDoc.tracks.filter((t) => t.type !== "audio")
|
|
2222
|
+
};
|
|
1966
2223
|
}
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2224
|
+
if (/concat=[^;[]*\bv=0\b/.test(filterComplex)) {
|
|
2225
|
+
workingDoc = {
|
|
2226
|
+
...workingDoc,
|
|
2227
|
+
tracks: workingDoc.tracks.filter((t) => t.type !== "video")
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
} else if (filterComplex.includes("xfade") && loadedDocs.length >= 2) {
|
|
2231
|
+
const track0 = loadedDocs[0].tracks.find((t) => t.type === "video");
|
|
2232
|
+
const track1 = loadedDocs[1].tracks.find((t) => t.type === "video");
|
|
2233
|
+
const f0 = await ensureDecodedFrames(
|
|
2234
|
+
track0,
|
|
2235
|
+
loadedDocs[0].timescale,
|
|
2236
|
+
budget,
|
|
2237
|
+
context.signal
|
|
2238
|
+
);
|
|
2239
|
+
const f1 = await ensureDecodedFrames(
|
|
2240
|
+
track1,
|
|
2241
|
+
loadedDocs[1].timescale,
|
|
2242
|
+
budget,
|
|
2243
|
+
context.signal
|
|
2244
|
+
);
|
|
2245
|
+
if (f0[0] && f1[0]) {
|
|
2246
|
+
if (f0[0].width !== f1[0].width || f0[0].height !== f1[0].height) {
|
|
2247
|
+
throw new Error(`xfade input size ${f0[0].width}x${f0[0].height} does not match ${f1[0].width}x${f1[0].height}`);
|
|
2248
|
+
}
|
|
2249
|
+
const timebase0 = inputs[0].format === "lavfi" ? f0[0].durationSeconds : 1 / track0.timescale;
|
|
2250
|
+
const timebase1 = inputs[1].format === "lavfi" ? f1[0].durationSeconds : 1 / track1.timescale;
|
|
2251
|
+
if (timebase0 !== timebase1) {
|
|
2252
|
+
throw new Error(`xfade input timebase ${timebase0} does not match ${timebase1}`);
|
|
1984
2253
|
}
|
|
1985
2254
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2255
|
+
const xfadeSpec = filterComplex.slice(filterComplex.indexOf("xfade") + "xfade".length).split("[")[0].split(",")[0].split(";")[0].trim();
|
|
2256
|
+
const positional = (xfadeSpec.startsWith("=") ? xfadeSpec.slice(1) : "").split(":");
|
|
2257
|
+
const named = {};
|
|
2258
|
+
for (const part of positional) {
|
|
2259
|
+
const eqIdx = part.indexOf("=");
|
|
2260
|
+
if (eqIdx >= 0) named[part.slice(0, eqIdx).trim()] = part.slice(eqIdx + 1).trim();
|
|
2261
|
+
}
|
|
2262
|
+
const transition = named.transition ?? (positional[0].includes("=") ? "fade" : positional[0] || "fade");
|
|
2263
|
+
if (transition !== "fade" && transition !== "wipeleft") {
|
|
2264
|
+
throw new Error(`Unsupported xfade transition: ${transition}`);
|
|
2265
|
+
}
|
|
2266
|
+
const fadeDur = parseFloat(named.duration ?? "0.5") || 0.5;
|
|
2267
|
+
const fps = f0[0]?.durationSeconds ? Math.round(1 / f0[0].durationSeconds) : 25;
|
|
2268
|
+
const overlapFrames = Math.max(1, Math.min(f0.length, f1.length, Math.round(fadeDur * fps)));
|
|
2269
|
+
const offsetSec = named.offset !== void 0 ? parseFloat(named.offset) : Math.max(0, (f0.length - overlapFrames) / fps);
|
|
2270
|
+
const preCount = Math.max(0, Math.min(f0.length - overlapFrames, Math.round(offsetSec * fps)));
|
|
2271
|
+
const outFrames = [];
|
|
2272
|
+
let cursor = 0;
|
|
2273
|
+
for (let i2 = 0; i2 < preCount; i2++) {
|
|
2274
|
+
const f = f0[i2];
|
|
2275
|
+
outFrames.push({ ...f, ptsSeconds: cursor });
|
|
2276
|
+
cursor += f.durationSeconds;
|
|
2277
|
+
}
|
|
2278
|
+
for (let k = 0; k < overlapFrames; k++) {
|
|
2279
|
+
const a = f0[Math.min(f0.length - 1, preCount + k)];
|
|
2280
|
+
const b = f1[Math.min(f1.length - 1, k)];
|
|
2281
|
+
const progress = k / (fadeDur * fps);
|
|
2282
|
+
const blended = new Uint8Array(a.data.byteLength);
|
|
2283
|
+
for (let pIdx = 0; pIdx < blended.byteLength; pIdx += 4) {
|
|
2284
|
+
const alpha = transition === "wipeleft" ? pIdx / 4 % a.width > Math.floor(a.width * (1 - progress)) ? 1 : 0 : progress;
|
|
2285
|
+
blended[pIdx] = Math.round(a.data[pIdx] * (1 - alpha) + b.data[pIdx] * alpha);
|
|
2286
|
+
blended[pIdx + 1] = Math.round(a.data[pIdx + 1] * (1 - alpha) + b.data[pIdx + 1] * alpha);
|
|
2287
|
+
blended[pIdx + 2] = Math.round(a.data[pIdx + 2] * (1 - alpha) + b.data[pIdx + 2] * alpha);
|
|
2288
|
+
blended[pIdx + 3] = 255;
|
|
2289
|
+
}
|
|
2290
|
+
outFrames.push({
|
|
2291
|
+
width: a.width,
|
|
2292
|
+
height: a.height,
|
|
2293
|
+
data: blended,
|
|
2294
|
+
ptsSeconds: cursor,
|
|
2295
|
+
durationSeconds: a.durationSeconds,
|
|
2296
|
+
keyframe: true
|
|
2297
|
+
});
|
|
2298
|
+
cursor += a.durationSeconds;
|
|
2299
|
+
}
|
|
2300
|
+
for (let j = overlapFrames; j < f1.length; j++) {
|
|
2301
|
+
const b = f1[j];
|
|
2302
|
+
outFrames.push({ ...b, ptsSeconds: cursor });
|
|
2303
|
+
cursor += b.durationSeconds;
|
|
2304
|
+
}
|
|
2305
|
+
workingDoc = {
|
|
2306
|
+
...loadedDocs[0],
|
|
2307
|
+
durationSeconds: cursor,
|
|
2308
|
+
tracks: loadedDocs[0].tracks.map(
|
|
2309
|
+
(t) => t.type === "video" ? { ...t, samples: [], decodedVideoFrames: outFrames } : t
|
|
2310
|
+
)
|
|
2311
|
+
};
|
|
2312
|
+
} else if ((filterComplex.includes("vstack") || filterComplex.includes("hstack")) && loadedDocs.length >= 2) {
|
|
2313
|
+
const f0 = await ensureDecodedFrames(
|
|
2314
|
+
loadedDocs[0].tracks.find((t) => t.type === "video"),
|
|
2315
|
+
loadedDocs[0].timescale,
|
|
2316
|
+
budget,
|
|
2317
|
+
context.signal
|
|
2318
|
+
);
|
|
2319
|
+
const f1 = await ensureDecodedFrames(
|
|
2320
|
+
loadedDocs[1].tracks.find((t) => t.type === "video"),
|
|
2321
|
+
loadedDocs[1].timescale,
|
|
2322
|
+
budget,
|
|
2323
|
+
context.signal
|
|
2324
|
+
);
|
|
2325
|
+
const vertical = filterComplex.includes("vstack");
|
|
2326
|
+
const filter = vertical ? "vstack" : "hstack";
|
|
2327
|
+
const spec = filterComplex.slice(filterComplex.indexOf(filter) + filter.length).split("[")[0].split(",")[0].split(";")[0].trim();
|
|
2328
|
+
const options2 = (spec.startsWith("=") ? spec.slice(1) : "").split(":");
|
|
2329
|
+
const shortestOption = options2.find((option) => option.trim().startsWith("shortest="))?.split("=")[1]?.trim() ?? options2[1]?.trim();
|
|
2330
|
+
const stopAtShortest = ["1", "true", "yes", "on"].includes(shortestOption ?? "0");
|
|
2331
|
+
const count = stopAtShortest || !f0.length || !f1.length ? Math.min(f0.length, f1.length) : Math.max(f0.length, f1.length);
|
|
2332
|
+
const stacked = [];
|
|
2333
|
+
for (let i2 = 0; i2 < count; i2++) {
|
|
2334
|
+
const a = f0[Math.min(i2, f0.length - 1)];
|
|
2335
|
+
const b = f1[Math.min(i2, f1.length - 1)];
|
|
2336
|
+
const timing = f0[i2] ?? f1[i2];
|
|
2337
|
+
const axis = vertical ? "width" : "height";
|
|
2338
|
+
if (a[axis] !== b[axis]) {
|
|
2339
|
+
throw new Error(`Input 1 ${axis} ${b[axis]} does not match input 0 ${axis} ${a[axis]}`);
|
|
2340
|
+
}
|
|
2341
|
+
const w = vertical ? a.width : a.width + b.width;
|
|
2342
|
+
const h = vertical ? a.height + b.height : a.height;
|
|
2343
|
+
const outData = new Uint8Array(w * h * 4);
|
|
2344
|
+
for (let y = 0; y < a.height; y++) {
|
|
2345
|
+
outData.set(a.data.subarray(y * a.width * 4, (y + 1) * a.width * 4), y * w * 4);
|
|
2346
|
+
}
|
|
2347
|
+
for (let y = 0; y < b.height; y++) {
|
|
2348
|
+
outData.set(b.data.subarray(y * b.width * 4, (y + 1) * b.width * 4), (vertical ? (a.height + y) * w : y * w + a.width) * 4);
|
|
2349
|
+
}
|
|
2350
|
+
stacked.push({
|
|
2351
|
+
width: w,
|
|
2352
|
+
height: h,
|
|
2353
|
+
data: outData,
|
|
2354
|
+
ptsSeconds: timing.ptsSeconds,
|
|
2355
|
+
durationSeconds: timing.durationSeconds,
|
|
2356
|
+
keyframe: true
|
|
1992
2357
|
});
|
|
1993
2358
|
}
|
|
1994
|
-
|
|
2359
|
+
workingDoc = {
|
|
2360
|
+
...loadedDocs[0],
|
|
2361
|
+
tracks: loadedDocs[0].tracks.map(
|
|
2362
|
+
(t) => t.type === "video" ? {
|
|
2363
|
+
...t,
|
|
2364
|
+
width: stacked[0]?.width ?? t.width,
|
|
2365
|
+
height: stacked[0]?.height ?? t.height,
|
|
2366
|
+
samples: [],
|
|
2367
|
+
decodedVideoFrames: stacked
|
|
2368
|
+
} : t
|
|
2369
|
+
)
|
|
2370
|
+
};
|
|
2371
|
+
} else if (filterComplex.includes("overlay") && loadedDocs.length >= 2) {
|
|
2372
|
+
const f0 = await ensureDecodedFrames(
|
|
2373
|
+
loadedDocs[0].tracks.find((t) => t.type === "video"),
|
|
2374
|
+
loadedDocs[0].timescale,
|
|
2375
|
+
budget,
|
|
2376
|
+
context.signal
|
|
2377
|
+
);
|
|
2378
|
+
const f1 = await ensureDecodedFrames(
|
|
2379
|
+
loadedDocs[1].tracks.find((t) => t.type === "video"),
|
|
2380
|
+
loadedDocs[1].timescale,
|
|
2381
|
+
budget,
|
|
2382
|
+
context.signal
|
|
2383
|
+
);
|
|
2384
|
+
const overlaySpec = filterComplex.slice(filterComplex.indexOf("overlay") + "overlay".length).split("[")[0].split(",")[0].split(";")[0].trim();
|
|
2385
|
+
const positional = (overlaySpec.startsWith("=") ? overlaySpec.slice(1) : "").split(":");
|
|
2386
|
+
const named = {};
|
|
2387
|
+
for (const part of positional) {
|
|
2388
|
+
const eqIdx = part.indexOf("=");
|
|
2389
|
+
if (eqIdx >= 0) named[part.slice(0, eqIdx).trim()] = part.slice(eqIdx + 1).trim();
|
|
2390
|
+
}
|
|
2391
|
+
const xExpression = named.x ?? (positional[0] && !positional[0].includes("=") ? positional[0] : "0");
|
|
2392
|
+
const yExpression = named.y ?? (positional[1] && !positional[1].includes("=") ? positional[1] : "0");
|
|
2393
|
+
const format = named.format ?? "yuv420";
|
|
2394
|
+
const horizontalGrid = format.startsWith("yuv420") || format.startsWith("yuv422") ? 2 : 1;
|
|
2395
|
+
const verticalGrid = format.startsWith("yuv420") ? 2 : 1;
|
|
2396
|
+
const overlaid = f0.map((a, idx) => {
|
|
2397
|
+
const b = f1[Math.min(idx, f1.length - 1)];
|
|
2398
|
+
if (!b) return a;
|
|
2399
|
+
const variables = {
|
|
2400
|
+
main_w: a.width,
|
|
2401
|
+
main_h: a.height,
|
|
2402
|
+
overlay_w: b.width,
|
|
2403
|
+
overlay_h: b.height,
|
|
2404
|
+
W: a.width,
|
|
2405
|
+
H: a.height,
|
|
2406
|
+
w: b.width,
|
|
2407
|
+
h: b.height
|
|
2408
|
+
};
|
|
2409
|
+
const ox = Math.floor(Math.trunc(parseFilterExpression(xExpression, "overlay", variables)) / horizontalGrid) * horizontalGrid;
|
|
2410
|
+
const oy = Math.floor(Math.trunc(parseFilterExpression(yExpression, "overlay", variables)) / verticalGrid) * verticalGrid;
|
|
2411
|
+
const outData = new Uint8Array(a.data);
|
|
2412
|
+
for (let y = 0; y < b.height && oy + y < a.height; y++) {
|
|
2413
|
+
if (oy + y < 0) continue;
|
|
2414
|
+
for (let x = 0; x < b.width && ox + x < a.width; x++) {
|
|
2415
|
+
if (ox + x < 0) continue;
|
|
2416
|
+
const srcOff = (y * b.width + x) * 4;
|
|
2417
|
+
const dstOff = ((oy + y) * a.width + (ox + x)) * 4;
|
|
2418
|
+
const alpha = b.data[srcOff + 3];
|
|
2419
|
+
if (alpha === 0) continue;
|
|
2420
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
2421
|
+
outData[dstOff + channel] = Math.round(
|
|
2422
|
+
(b.data[srcOff + channel] * alpha + outData[dstOff + channel] * (255 - alpha)) / 255
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2425
|
+
outData[dstOff + 3] = 255;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
return { ...a, data: outData };
|
|
2429
|
+
});
|
|
2430
|
+
workingDoc = {
|
|
2431
|
+
...loadedDocs[0],
|
|
2432
|
+
tracks: loadedDocs[0].tracks.map(
|
|
2433
|
+
(t) => t.type === "video" ? { ...t, samples: [], decodedVideoFrames: overlaid } : t
|
|
2434
|
+
)
|
|
2435
|
+
};
|
|
2436
|
+
} else {
|
|
2437
|
+
workingDoc = await muxMp4Cooperatively(loadedDocs, { shortest }, context.signal);
|
|
2438
|
+
const cleanedComplex = filterComplex.replace(/\[[^\]]+\]/g, "").trim();
|
|
2439
|
+
if (cleanedComplex) vfFilters.push(cleanedComplex);
|
|
2440
|
+
}
|
|
2441
|
+
} else if (maps.length > 0) {
|
|
2442
|
+
let selectedTracks = [];
|
|
2443
|
+
let mapped = false;
|
|
2444
|
+
for (const map of maps) {
|
|
2445
|
+
const exclude = map.startsWith("-");
|
|
2446
|
+
let clean = exclude ? map.slice(1) : map;
|
|
2447
|
+
const optional = clean.endsWith("?");
|
|
2448
|
+
if (optional) clean = clean.slice(0, -1);
|
|
2449
|
+
if (clean.startsWith("[")) continue;
|
|
2450
|
+
const [input, ...specifier] = clean.split(":");
|
|
2451
|
+
const selector = specifier[0];
|
|
2452
|
+
const inputIdx = Number(input);
|
|
2453
|
+
const srcDoc = loadedDocs[inputIdx];
|
|
2454
|
+
if (!srcDoc) {
|
|
2455
|
+
await writeBytes(context.stderr, encodeUtf8(`ffmpeg: Invalid input file index: ${inputIdx}.
|
|
2456
|
+
`), context.signal);
|
|
2457
|
+
return { exitCode: 234 };
|
|
2458
|
+
}
|
|
2459
|
+
const type = selector === "v" ? "video" : selector === "a" ? "audio" : selector === "s" ? "subtitle" : void 0;
|
|
2460
|
+
let matches = type ? srcDoc.tracks.filter((t) => t.type === type) : srcDoc.tracks;
|
|
2461
|
+
const streamSpecifier = type ? specifier.slice(1) : specifier;
|
|
2462
|
+
const [index, key, ...valueParts] = streamSpecifier;
|
|
2463
|
+
if (index?.startsWith("#")) {
|
|
2464
|
+
matches = matches.filter((track) => track.id === Number(index.slice(1)));
|
|
2465
|
+
} else if (index === "m") {
|
|
2466
|
+
const value = valueParts.length ? valueParts.join(":") : void 0;
|
|
2467
|
+
matches = matches.filter((track) => key === "language" && (value === void 0 || track.language === value));
|
|
2468
|
+
} else if (index !== void 0) {
|
|
2469
|
+
matches = matches.filter((_, i2) => i2 === Number(index));
|
|
2470
|
+
}
|
|
2471
|
+
if ((matches.length === 0 || exclude && !mapped) && !optional) {
|
|
2472
|
+
await writeBytes(context.stderr, encodeUtf8(`ffmpeg: Stream map '${clean}' matches no streams.
|
|
2473
|
+
To ignore this, add a trailing '?' to the map.
|
|
2474
|
+
`), context.signal);
|
|
2475
|
+
return { exitCode: 234 };
|
|
2476
|
+
}
|
|
2477
|
+
if (exclude) {
|
|
2478
|
+
selectedTracks = selectedTracks.filter((entry) => entry.input !== inputIdx || !matches.includes(entry.track));
|
|
2479
|
+
} else {
|
|
2480
|
+
mapped ||= matches.length > 0;
|
|
2481
|
+
selectedTracks.push(...matches.map((track) => ({ input: inputIdx, track })));
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
const filteredTracks = selectedTracks.map((entry) => entry.track);
|
|
2485
|
+
if (mapped && filteredTracks.length === 0) {
|
|
2486
|
+
await writeBytes(context.stderr, encodeUtf8("ffmpeg: Output file does not contain any stream.\n"), context.signal);
|
|
2487
|
+
return { exitCode: 234 };
|
|
2488
|
+
}
|
|
2489
|
+
if (!mapped) {
|
|
2490
|
+
const tracks = selectAutomaticAudio(loadedDocs).flatMap((doc) => doc.tracks);
|
|
2491
|
+
for (const type of ["video", "audio"]) {
|
|
2492
|
+
const track = tracks.find((track2) => track2.type === type);
|
|
2493
|
+
if (track) filteredTracks.push(track);
|
|
2494
|
+
}
|
|
1995
2495
|
}
|
|
2496
|
+
workingDoc = await muxMp4Cooperatively(
|
|
2497
|
+
[{ ...loadedDocs[0], tracks: filteredTracks }],
|
|
2498
|
+
{ stripAudio, stripVideo, stripSubtitles, shortest, rotation },
|
|
2499
|
+
context.signal
|
|
2500
|
+
);
|
|
2501
|
+
} else {
|
|
2502
|
+
workingDoc = await muxMp4Cooperatively(selectAutomaticAudio(loadedDocs), {
|
|
2503
|
+
stripAudio,
|
|
2504
|
+
stripVideo,
|
|
2505
|
+
stripSubtitles,
|
|
2506
|
+
shortest,
|
|
2507
|
+
rotation
|
|
2508
|
+
}, context.signal);
|
|
1996
2509
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
};
|
|
2014
|
-
const loadedDocs = [];
|
|
2015
|
-
for (const inp of inputs) {
|
|
2016
|
-
await yieldTurn5(context.signal);
|
|
2017
|
-
context.signal.throwIfAborted();
|
|
2018
|
-
let doc = await loadSingleDocument(inp.path, inp.format, inp.fps, inp);
|
|
2019
|
-
if (inp.startSeconds !== void 0 || inp.endSeconds !== void 0 || inp.durationSeconds !== void 0) {
|
|
2020
|
-
doc = sliceMp4(doc, {
|
|
2021
|
-
startSeconds: inp.startSeconds,
|
|
2022
|
-
endSeconds: inp.endSeconds,
|
|
2023
|
-
durationSeconds: inp.durationSeconds
|
|
2024
|
-
});
|
|
2025
|
-
}
|
|
2026
|
-
if (inp.streamLoop && inp.streamLoop > 0) {
|
|
2027
|
-
const copies = Array.from({ length: inp.streamLoop + 1 }, () => doc);
|
|
2028
|
-
doc = concatMp4(copies, { limits: options.limits, budget });
|
|
2029
|
-
}
|
|
2030
|
-
loadedDocs.push(doc);
|
|
2031
|
-
}
|
|
2032
|
-
let workingDoc;
|
|
2033
|
-
if (filterComplex) {
|
|
2034
|
-
if (features.filterGraph === false) {
|
|
2035
|
-
throw new Error("Filtergraph processing is disabled by consumer feature configuration");
|
|
2036
|
-
}
|
|
2037
|
-
if (filterComplex.includes("concat=")) {
|
|
2038
|
-
workingDoc = concatMp4(loadedDocs, { limits: options.limits, budget });
|
|
2039
|
-
if (/concat=[^;[]*\ba=0\b/.test(filterComplex)) {
|
|
2040
|
-
workingDoc = {
|
|
2041
|
-
...workingDoc,
|
|
2042
|
-
tracks: workingDoc.tracks.filter((t) => t.type !== "audio")
|
|
2043
|
-
};
|
|
2510
|
+
validateAudioEncoder(audioCodec, workingDoc, astPlugins);
|
|
2511
|
+
const audioTrackCount = workingDoc.tracks.filter((track) => track.type === "audio").length;
|
|
2512
|
+
if (audioCodec === "copy" && audioTrackCount > 0 && afFilters.some((filter) => filter.ordinal === void 0 || filter.ordinal < audioTrackCount)) {
|
|
2513
|
+
await writeBytes(context.stderr, encodeUtf8("ffmpeg: Filtering and streamcopy cannot be used together for audio.\n"), context.signal);
|
|
2514
|
+
return { exitCode: 218 };
|
|
2515
|
+
}
|
|
2516
|
+
if (outputSs !== void 0 || outputTo !== void 0 || outputDuration !== void 0) {
|
|
2517
|
+
workingDoc = sliceMp4(workingDoc, {
|
|
2518
|
+
startSeconds: outputSs,
|
|
2519
|
+
endSeconds: outputDuration === void 0 ? outputTo : void 0,
|
|
2520
|
+
durationSeconds: outputDuration
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
if (outputSize && videoCodec !== "copy") {
|
|
2524
|
+
const [w, h] = outputSize.toLowerCase().split("x");
|
|
2525
|
+
if (w && h) vfFilters.push(`scale=${w}:${h}`);
|
|
2044
2526
|
}
|
|
2045
|
-
if (
|
|
2527
|
+
if (outputFps && videoCodec !== "copy") {
|
|
2528
|
+
vfFilters.push(`fps=${outputFps}`);
|
|
2529
|
+
}
|
|
2530
|
+
const imageOutput = (outputFormat ? registry.findByFormatName(outputFormat) : registry.findByFilename(outputTarget2))?.id === "image2";
|
|
2531
|
+
if (imageOutput || maxVideoFrames !== void 0 && videoCodec !== "copy") {
|
|
2046
2532
|
workingDoc = {
|
|
2047
2533
|
...workingDoc,
|
|
2048
|
-
tracks: workingDoc.tracks
|
|
2534
|
+
tracks: await mapWork(workingDoc.tracks, async (track) => {
|
|
2535
|
+
if (track.type !== "video") return track;
|
|
2536
|
+
const limit = vfFilters.length === 0 ? maxVideoFrames : void 0;
|
|
2537
|
+
const frames = await ensureDecodedFrames(track, workingDoc.timescale, budget, context.signal, limit);
|
|
2538
|
+
return { ...track, samples: [], decodedVideoFrames: frames };
|
|
2539
|
+
}, context.signal)
|
|
2049
2540
|
};
|
|
2050
2541
|
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
const f0 = await ensureDecodedFrames(
|
|
2055
|
-
track0,
|
|
2056
|
-
loadedDocs[0].timescale,
|
|
2057
|
-
budget,
|
|
2058
|
-
context.signal
|
|
2059
|
-
);
|
|
2060
|
-
const f1 = await ensureDecodedFrames(
|
|
2061
|
-
track1,
|
|
2062
|
-
loadedDocs[1].timescale,
|
|
2063
|
-
budget,
|
|
2064
|
-
context.signal
|
|
2065
|
-
);
|
|
2066
|
-
if (f0[0] && f1[0]) {
|
|
2067
|
-
if (f0[0].width !== f1[0].width || f0[0].height !== f1[0].height) {
|
|
2068
|
-
throw new Error(`xfade input size ${f0[0].width}x${f0[0].height} does not match ${f1[0].width}x${f1[0].height}`);
|
|
2542
|
+
if (vfFilters.length > 0) {
|
|
2543
|
+
if (features.videoTranscode === false || features.filterGraph === false) {
|
|
2544
|
+
throw new Error("Video transcoding / filtergraph is disabled by consumer feature configuration");
|
|
2069
2545
|
}
|
|
2070
|
-
const
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2546
|
+
const combinedChain = vfFilters.join(",");
|
|
2547
|
+
let loadedSubtitleCues;
|
|
2548
|
+
const subMatch = /subtitles=(?:filename=)?['"]?([^:'",]+)['"]?/.exec(combinedChain);
|
|
2549
|
+
if (subMatch?.[1]) {
|
|
2550
|
+
const subPath = resolvePath(context.cwd, subMatch[1]);
|
|
2551
|
+
const subBytes = await readInput(subPath);
|
|
2552
|
+
const subFmt = subPath.toLowerCase().endsWith(".vtt") ? "webvtt" : "srt";
|
|
2553
|
+
const subDoc = parseSubtitleDocument(subBytes, subFmt);
|
|
2554
|
+
const st = subDoc.tracks.find((t) => t.type === "subtitle");
|
|
2555
|
+
if (st) {
|
|
2556
|
+
const sts = st.timescale || 1e3;
|
|
2557
|
+
loadedSubtitleCues = st.samples.map((s) => ({
|
|
2558
|
+
startSec: s.pts / sts,
|
|
2559
|
+
endSec: (s.pts + s.duration) / sts,
|
|
2560
|
+
text: decodeUtf8(s.data).trim()
|
|
2561
|
+
}));
|
|
2562
|
+
}
|
|
2074
2563
|
}
|
|
2564
|
+
workingDoc = {
|
|
2565
|
+
...workingDoc,
|
|
2566
|
+
tracks: await mapWork(workingDoc.tracks, async (t) => {
|
|
2567
|
+
if (t.type !== "video") return t;
|
|
2568
|
+
let presentationOffset = 0;
|
|
2569
|
+
for (const edit of t.editList ?? []) {
|
|
2570
|
+
if (edit.mediaTime < 0) presentationOffset -= edit.segmentDuration / workingDoc.timescale;
|
|
2571
|
+
else {
|
|
2572
|
+
presentationOffset += edit.mediaTime / t.timescale;
|
|
2573
|
+
break;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
const offsetTicks = Math.round(presentationOffset * t.timescale);
|
|
2577
|
+
const decoded = await ensureDecodedFrames(t, workingDoc.timescale, budget, context.signal);
|
|
2578
|
+
const displayed = decoded.map((frame) => ({ ...frame, ptsSeconds: (Math.round(frame.ptsSeconds * t.timescale) - offsetTicks) / t.timescale }));
|
|
2579
|
+
const filtered = await applyVideoFilterChain(displayed, combinedChain, budget, loadedSubtitleCues, context.signal);
|
|
2580
|
+
const first = filtered[0];
|
|
2581
|
+
return {
|
|
2582
|
+
...t,
|
|
2583
|
+
width: first?.width ?? t.width,
|
|
2584
|
+
height: first?.height ?? t.height,
|
|
2585
|
+
samples: [],
|
|
2586
|
+
decodedVideoFrames: filtered.map((frame) => ({ ...frame, ptsSeconds: (Math.round(frame.ptsSeconds * t.timescale) + offsetTicks) / t.timescale }))
|
|
2587
|
+
};
|
|
2588
|
+
}, context.signal)
|
|
2589
|
+
};
|
|
2075
2590
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2591
|
+
if (maxVideoFrames !== void 0 && !outputTarget2.includes("%")) {
|
|
2592
|
+
workingDoc = {
|
|
2593
|
+
...workingDoc,
|
|
2594
|
+
tracks: workingDoc.tracks.map((t) => {
|
|
2595
|
+
if (t.type !== "video") return t;
|
|
2596
|
+
if (t.decodedVideoFrames && t.decodedVideoFrames.length > maxVideoFrames) {
|
|
2597
|
+
return {
|
|
2598
|
+
...t,
|
|
2599
|
+
samples: [],
|
|
2600
|
+
decodedVideoFrames: t.decodedVideoFrames.slice(0, maxVideoFrames)
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
if (t.samples.length > maxVideoFrames) {
|
|
2604
|
+
const slicedSamples = t.samples.slice(0, maxVideoFrames);
|
|
2605
|
+
const dur = slicedSamples.reduce((acc, s) => acc + s.duration, 0);
|
|
2606
|
+
return {
|
|
2607
|
+
...t,
|
|
2608
|
+
duration: dur,
|
|
2609
|
+
samples: slicedSamples
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
return t;
|
|
2613
|
+
})
|
|
2614
|
+
};
|
|
2082
2615
|
}
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
throw new Error(`Unsupported xfade transition: ${transition}`);
|
|
2616
|
+
if (audioCodec === "copy" && maxAudioFrames !== void 0) {
|
|
2617
|
+
workingDoc = limitAudioCopyPackets(workingDoc, maxAudioFrames);
|
|
2086
2618
|
}
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2619
|
+
if ((audioRate !== void 0 || audioChannels !== void 0 || afFilters.length > 0) && audioCodec !== "copy") {
|
|
2620
|
+
let audioOrdinal = 0;
|
|
2621
|
+
workingDoc = {
|
|
2622
|
+
...workingDoc,
|
|
2623
|
+
tracks: await mapWork(workingDoc.tracks, async (t) => {
|
|
2624
|
+
let work = 0;
|
|
2625
|
+
if (t.type !== "audio") return t;
|
|
2626
|
+
const ordinal = audioOrdinal++;
|
|
2627
|
+
const filter = afFilters.filter((filter2) => filter2.ordinal === void 0 || filter2.ordinal === ordinal).at(-1);
|
|
2628
|
+
if (audioRate === void 0 && audioChannels === void 0 && !filter) return t;
|
|
2629
|
+
const audio = t.decodedAudio ?? (t.codecDescriptions[0]?.codecName === "aac" ? decodeAacTrack(workingDoc, t) : void 0);
|
|
2630
|
+
if (!audio) throw new Error("Audio filtering requires decoded PCM audio");
|
|
2631
|
+
const origRate = audio.sampleRate;
|
|
2632
|
+
const origCh = audio.channels;
|
|
2633
|
+
const targetRate = audioRate ?? origRate;
|
|
2634
|
+
const targetCh = audioChannels ?? origCh;
|
|
2635
|
+
const channelMask = audio.channelMask;
|
|
2636
|
+
const surroundMono = origCh === 6 && targetCh === 1;
|
|
2637
|
+
const monoStereo = origCh === 1 && (targetCh === 2 || targetCh === 3);
|
|
2638
|
+
const pcm16MonoStereo = monoStereo && !filter && targetRate === origRate && t.codecDescriptions[0]?.codecName === "pcm_s16le" && (audioCodec === "pcm_s16le" || audioCodec === void 0 && (outputFormat ?? outputTarget2.split(".").pop()?.toLowerCase()) === "wav");
|
|
2639
|
+
const monoSurround = origCh === 1 && targetCh >= 4 && targetCh <= 8;
|
|
2640
|
+
if (targetCh !== origCh && origCh > 2 && (!surroundMono || channelMask && channelMask !== 63 && channelMask !== 1551)) {
|
|
2641
|
+
throw new Error(`Unsupported audio channel layout conversion: ${origCh} channels (mask ${channelMask ?? 0}) to ${targetCh}`);
|
|
2642
|
+
}
|
|
2643
|
+
const volFactor = t.volume ?? 1;
|
|
2644
|
+
let stretched = audio.channelData;
|
|
2645
|
+
if (filter) {
|
|
2646
|
+
for (const item of filter.expression.split(",")) {
|
|
2647
|
+
if (++work % 1024 === 0) {
|
|
2648
|
+
await yieldTurn5(context.signal);
|
|
2649
|
+
context.signal?.throwIfAborted();
|
|
2650
|
+
}
|
|
2651
|
+
const trimmed = item.trim();
|
|
2652
|
+
if (trimmed.startsWith("volume=")) {
|
|
2653
|
+
const volSpec = trimmed.slice("volume=".length).trim();
|
|
2654
|
+
const decibels = volSpec.endsWith("dB");
|
|
2655
|
+
const gain = parseFilterExpression(decibels ? volSpec.slice(0, -2) : volSpec, "volume", {});
|
|
2656
|
+
const factor = decibels ? Math.pow(10, gain / 20) : gain;
|
|
2657
|
+
if (!Number.isFinite(factor)) throw new Error("volume: invalid gain");
|
|
2658
|
+
stretched = await mapWork(stretched, async (channel) => {
|
|
2659
|
+
const output = new Float32Array(channel.length);
|
|
2660
|
+
for (let i2 = 0; i2 < channel.length; i2++) {
|
|
2661
|
+
if (++work % 1024 === 0) {
|
|
2662
|
+
await yieldTurn5(context.signal);
|
|
2663
|
+
context.signal?.throwIfAborted();
|
|
2664
|
+
}
|
|
2665
|
+
output[i2] = channel[i2] * factor;
|
|
2666
|
+
}
|
|
2667
|
+
return output;
|
|
2668
|
+
}, context.signal);
|
|
2669
|
+
} else if (trimmed.startsWith("atempo=")) {
|
|
2670
|
+
const tempo = Number(trimmed.slice(7));
|
|
2671
|
+
if (!Number.isFinite(tempo) || tempo < 0.5 || tempo > 100) {
|
|
2672
|
+
throw new Error("atempo must be in the range [0.5, 100]");
|
|
2673
|
+
}
|
|
2674
|
+
stretched = tempo === 1 ? await filterUnityTempo(stretched, origRate, t.codecDescriptions[0]?.codecName === "pcm_s16le", budget, context.signal) : await stretchAudio(stretched, origRate, tempo, budget, context.signal);
|
|
2675
|
+
} else if (trimmed === "areverse") {
|
|
2676
|
+
stretched = await mapWork(stretched, async (channel) => channel.slice().reverse(), context.signal);
|
|
2677
|
+
} else {
|
|
2678
|
+
throw new Error(`Unsupported audio filter: ${trimmed}`);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
const targetSamples = Math.max(0, Math.round((stretched[0]?.length ?? 0) * targetRate / origRate));
|
|
2683
|
+
if (targetRate !== origRate) stretched = await resampleAudio(stretched, targetSamples, origRate / targetRate, budget, context.signal);
|
|
2684
|
+
const newChannelData = [];
|
|
2685
|
+
for (let c = 0; c < targetCh; c++) {
|
|
2686
|
+
if (++work % 1024 === 0) {
|
|
2687
|
+
await yieldTurn5(context.signal);
|
|
2688
|
+
context.signal?.throwIfAborted();
|
|
2689
|
+
}
|
|
2690
|
+
const dst = new Float32Array(targetSamples);
|
|
2691
|
+
const srcCh = monoStereo && c >= 2 || monoSurround && c !== 2 ? void 0 : stretched[origCh === 1 ? 0 : c];
|
|
2692
|
+
const rightCh = origCh === 2 && targetCh === 1 ? stretched[1] : void 0;
|
|
2693
|
+
if (srcCh && srcCh.length > 0) {
|
|
2694
|
+
for (let i2 = 0; i2 < targetSamples; i2++) {
|
|
2695
|
+
if (++work % 1024 === 0) {
|
|
2696
|
+
await yieldTurn5(context.signal);
|
|
2697
|
+
context.signal?.throwIfAborted();
|
|
2698
|
+
}
|
|
2699
|
+
const srcIdx = Math.min(srcCh.length - 1, i2);
|
|
2700
|
+
const sample = surroundMono ? ((stretched[0][srcIdx] + stretched[1][srcIdx]) * Math.SQRT1_2 + stretched[2][srcIdx] + (stretched[4][srcIdx] + stretched[5][srcIdx]) / 2) / (2 + Math.SQRT2) : rightCh ? (srcCh[srcIdx] + rightCh[srcIdx]) / 2 : srcCh[srcIdx];
|
|
2701
|
+
const mixed = pcm16MonoStereo ? Math.floor((sample * 32768 * 23170 + 16384) / 32768) / 32768 : sample * (monoStereo ? Math.SQRT1_2 : 1);
|
|
2702
|
+
dst[i2] = Math.max(-1, Math.min(1, mixed * volFactor));
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
newChannelData.push(dst);
|
|
2706
|
+
}
|
|
2707
|
+
const nextDescs = t.codecDescriptions.map((d) => ({
|
|
2708
|
+
...d,
|
|
2709
|
+
sampleRate: targetRate,
|
|
2710
|
+
channels: targetCh
|
|
2711
|
+
}));
|
|
2712
|
+
return {
|
|
2713
|
+
...t,
|
|
2714
|
+
timescale: targetRate,
|
|
2715
|
+
duration: targetSamples,
|
|
2716
|
+
volume: volFactor,
|
|
2717
|
+
codecDescriptions: nextDescs,
|
|
2718
|
+
samples: [],
|
|
2719
|
+
decodedAudio: {
|
|
2720
|
+
sampleRate: targetRate,
|
|
2721
|
+
channels: targetCh,
|
|
2722
|
+
channelData: newChannelData,
|
|
2723
|
+
...targetCh === origCh ? { channelMask } : {}
|
|
2724
|
+
}
|
|
2725
|
+
};
|
|
2726
|
+
}, context.signal)
|
|
2727
|
+
};
|
|
2098
2728
|
}
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
const progress = k / (fadeDur * fps);
|
|
2103
|
-
const blended = new Uint8Array(a.data.byteLength);
|
|
2104
|
-
for (let pIdx = 0; pIdx < blended.byteLength; pIdx += 4) {
|
|
2105
|
-
const alpha = transition === "wipeleft" ? pIdx / 4 % a.width > Math.floor(a.width * (1 - progress)) ? 1 : 0 : progress;
|
|
2106
|
-
blended[pIdx] = Math.round(a.data[pIdx] * (1 - alpha) + b.data[pIdx] * alpha);
|
|
2107
|
-
blended[pIdx + 1] = Math.round(a.data[pIdx + 1] * (1 - alpha) + b.data[pIdx + 1] * alpha);
|
|
2108
|
-
blended[pIdx + 2] = Math.round(a.data[pIdx + 2] * (1 - alpha) + b.data[pIdx + 2] * alpha);
|
|
2109
|
-
blended[pIdx + 3] = 255;
|
|
2729
|
+
if (chapterSource !== void 0) {
|
|
2730
|
+
if (chapterSource >= loadedDocs.length) {
|
|
2731
|
+
throw new Error(`Invalid chapter input index: ${chapterSource}`);
|
|
2110
2732
|
}
|
|
2111
|
-
|
|
2112
|
-
width: a.width,
|
|
2113
|
-
height: a.height,
|
|
2114
|
-
data: blended,
|
|
2115
|
-
ptsSeconds: cursor,
|
|
2116
|
-
durationSeconds: a.durationSeconds,
|
|
2117
|
-
keyframe: true
|
|
2118
|
-
});
|
|
2119
|
-
cursor += a.durationSeconds;
|
|
2733
|
+
workingDoc = { ...workingDoc, chapters: chapterSource < 0 ? void 0 : loadedDocs[chapterSource].chapters };
|
|
2120
2734
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
outFrames.push({ ...b, ptsSeconds: cursor });
|
|
2124
|
-
cursor += b.durationSeconds;
|
|
2735
|
+
if (metadataSource !== void 0 && metadataSource >= loadedDocs.length) {
|
|
2736
|
+
throw new Error(`Invalid metadata input index: ${metadataSource}`);
|
|
2125
2737
|
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2738
|
+
const sourceMetadata = metadataSource === void 0 ? loadedDocs[0].metadata : metadataSource < 0 ? {} : loadedDocs[metadataSource].metadata;
|
|
2739
|
+
const metaTags = {
|
|
2740
|
+
...sourceMetadata,
|
|
2741
|
+
title: metadata.title ?? sourceMetadata.title,
|
|
2742
|
+
artist: metadata.artist ?? sourceMetadata.artist,
|
|
2743
|
+
album: metadata.album ?? sourceMetadata.album,
|
|
2744
|
+
date: metadata.date ?? metadata.year ?? sourceMetadata.date,
|
|
2745
|
+
comment: metadata.comment ?? sourceMetadata.comment,
|
|
2746
|
+
genre: metadata.genre ?? sourceMetadata.genre,
|
|
2747
|
+
encoder: metadata.encoder ?? sourceMetadata.encoder
|
|
2132
2748
|
};
|
|
2133
|
-
} else if ((filterComplex.includes("vstack") || filterComplex.includes("hstack")) && loadedDocs.length >= 2) {
|
|
2134
|
-
const f0 = await ensureDecodedFrames(
|
|
2135
|
-
loadedDocs[0].tracks.find((t) => t.type === "video"),
|
|
2136
|
-
loadedDocs[0].timescale,
|
|
2137
|
-
budget,
|
|
2138
|
-
context.signal
|
|
2139
|
-
);
|
|
2140
|
-
const f1 = await ensureDecodedFrames(
|
|
2141
|
-
loadedDocs[1].tracks.find((t) => t.type === "video"),
|
|
2142
|
-
loadedDocs[1].timescale,
|
|
2143
|
-
budget,
|
|
2144
|
-
context.signal
|
|
2145
|
-
);
|
|
2146
|
-
const vertical = filterComplex.includes("vstack");
|
|
2147
|
-
const filter = vertical ? "vstack" : "hstack";
|
|
2148
|
-
const spec = filterComplex.slice(filterComplex.indexOf(filter) + filter.length).split("[")[0].split(",")[0].split(";")[0].trim();
|
|
2149
|
-
const options2 = (spec.startsWith("=") ? spec.slice(1) : "").split(":");
|
|
2150
|
-
const shortestOption = options2.find((option) => option.trim().startsWith("shortest="))?.split("=")[1]?.trim() ?? options2[1]?.trim();
|
|
2151
|
-
const stopAtShortest = ["1", "true", "yes", "on"].includes(shortestOption ?? "0");
|
|
2152
|
-
const count = stopAtShortest || !f0.length || !f1.length ? Math.min(f0.length, f1.length) : Math.max(f0.length, f1.length);
|
|
2153
|
-
const stacked = [];
|
|
2154
|
-
for (let i = 0; i < count; i++) {
|
|
2155
|
-
const a = f0[Math.min(i, f0.length - 1)];
|
|
2156
|
-
const b = f1[Math.min(i, f1.length - 1)];
|
|
2157
|
-
const timing = f0[i] ?? f1[i];
|
|
2158
|
-
const axis = vertical ? "width" : "height";
|
|
2159
|
-
if (a[axis] !== b[axis]) {
|
|
2160
|
-
throw new Error(`Input 1 ${axis} ${b[axis]} does not match input 0 ${axis} ${a[axis]}`);
|
|
2161
|
-
}
|
|
2162
|
-
const w = vertical ? a.width : a.width + b.width;
|
|
2163
|
-
const h = vertical ? a.height + b.height : a.height;
|
|
2164
|
-
const outData = new Uint8Array(w * h * 4);
|
|
2165
|
-
for (let y = 0; y < a.height; y++) {
|
|
2166
|
-
outData.set(a.data.subarray(y * a.width * 4, (y + 1) * a.width * 4), y * w * 4);
|
|
2167
|
-
}
|
|
2168
|
-
for (let y = 0; y < b.height; y++) {
|
|
2169
|
-
outData.set(b.data.subarray(y * b.width * 4, (y + 1) * b.width * 4), (vertical ? (a.height + y) * w : y * w + a.width) * 4);
|
|
2170
|
-
}
|
|
2171
|
-
stacked.push({
|
|
2172
|
-
width: w,
|
|
2173
|
-
height: h,
|
|
2174
|
-
data: outData,
|
|
2175
|
-
ptsSeconds: timing.ptsSeconds,
|
|
2176
|
-
durationSeconds: timing.durationSeconds,
|
|
2177
|
-
keyframe: true
|
|
2178
|
-
});
|
|
2179
|
-
}
|
|
2180
2749
|
workingDoc = {
|
|
2181
|
-
...
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
width: stacked[0]?.width ?? t.width,
|
|
2186
|
-
height: stacked[0]?.height ?? t.height,
|
|
2187
|
-
samples: [],
|
|
2188
|
-
decodedVideoFrames: stacked
|
|
2189
|
-
} : t
|
|
2190
|
-
)
|
|
2750
|
+
...workingDoc,
|
|
2751
|
+
faststart,
|
|
2752
|
+
metadata: metaTags,
|
|
2753
|
+
tracks: applyStreamDispositions(applyStreamLanguages(workingDoc.tracks, streamLanguages), streamDispositions)
|
|
2191
2754
|
};
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
loadedDocs[0].timescale,
|
|
2196
|
-
budget,
|
|
2197
|
-
context.signal
|
|
2198
|
-
);
|
|
2199
|
-
const f1 = await ensureDecodedFrames(
|
|
2200
|
-
loadedDocs[1].tracks.find((t) => t.type === "video"),
|
|
2201
|
-
loadedDocs[1].timescale,
|
|
2202
|
-
budget,
|
|
2203
|
-
context.signal
|
|
2204
|
-
);
|
|
2205
|
-
const overlaySpec = filterComplex.slice(filterComplex.indexOf("overlay") + "overlay".length).split("[")[0].split(",")[0].split(";")[0].trim();
|
|
2206
|
-
const positional = (overlaySpec.startsWith("=") ? overlaySpec.slice(1) : "").split(":");
|
|
2207
|
-
const named = {};
|
|
2208
|
-
for (const part of positional) {
|
|
2209
|
-
const eqIdx = part.indexOf("=");
|
|
2210
|
-
if (eqIdx >= 0) named[part.slice(0, eqIdx).trim()] = part.slice(eqIdx + 1).trim();
|
|
2755
|
+
if (outputFormat === "null" || outputTarget2 === "/dev/null") {
|
|
2756
|
+
options.onMetrics?.(budget.getStats());
|
|
2757
|
+
return { exitCode: 0 };
|
|
2211
2758
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
const
|
|
2219
|
-
|
|
2220
|
-
const variables = {
|
|
2221
|
-
main_w: a.width,
|
|
2222
|
-
main_h: a.height,
|
|
2223
|
-
overlay_w: b.width,
|
|
2224
|
-
overlay_h: b.height,
|
|
2225
|
-
W: a.width,
|
|
2226
|
-
H: a.height,
|
|
2227
|
-
w: b.width,
|
|
2228
|
-
h: b.height
|
|
2229
|
-
};
|
|
2230
|
-
const ox = Math.floor(Math.trunc(parseFilterExpression(xExpression, "overlay", variables)) / horizontalGrid) * horizontalGrid;
|
|
2231
|
-
const oy = Math.floor(Math.trunc(parseFilterExpression(yExpression, "overlay", variables)) / verticalGrid) * verticalGrid;
|
|
2232
|
-
const outData = new Uint8Array(a.data);
|
|
2233
|
-
for (let y = 0; y < b.height && oy + y < a.height; y++) {
|
|
2234
|
-
if (oy + y < 0) continue;
|
|
2235
|
-
for (let x = 0; x < b.width && ox + x < a.width; x++) {
|
|
2236
|
-
if (ox + x < 0) continue;
|
|
2237
|
-
const srcOff = (y * b.width + x) * 4;
|
|
2238
|
-
const dstOff = ((oy + y) * a.width + (ox + x)) * 4;
|
|
2239
|
-
const alpha = b.data[srcOff + 3];
|
|
2240
|
-
if (alpha === 0) continue;
|
|
2241
|
-
for (let channel = 0; channel < 3; channel++) {
|
|
2242
|
-
outData[dstOff + channel] = Math.round(
|
|
2243
|
-
(b.data[srcOff + channel] * alpha + outData[dstOff + channel] * (255 - alpha)) / 255
|
|
2244
|
-
);
|
|
2245
|
-
}
|
|
2246
|
-
outData[dstOff + 3] = 255;
|
|
2247
|
-
}
|
|
2759
|
+
if (outputFormat === "dash" || outputTarget2.toLowerCase().endsWith(".mpd")) {
|
|
2760
|
+
if (!registry.findByFormatName("dash")?.canMux) throw new Error("DASH AST is not registered");
|
|
2761
|
+
const outResolved = resolvePath(context.cwd, outputTarget2);
|
|
2762
|
+
const outDir = outResolved.slice(0, outResolved.lastIndexOf("/")) || "/";
|
|
2763
|
+
const { manifest, resources } = serializeDashDocument(workingDoc, { limits: options.limits, budget });
|
|
2764
|
+
await context.fs.mkdir(outDir, { recursive: true, signal: context.signal });
|
|
2765
|
+
for (const [uri, bytes] of resources) {
|
|
2766
|
+
await writeFileOutput(context, bytes, (data) => context.fs.writeFile(resolvePath(outDir, uri), data, { signal: context.signal }));
|
|
2248
2767
|
}
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
...loadedDocs[0],
|
|
2253
|
-
tracks: loadedDocs[0].tracks.map(
|
|
2254
|
-
(t) => t.type === "video" ? { ...t, samples: [], decodedVideoFrames: overlaid } : t
|
|
2255
|
-
)
|
|
2256
|
-
};
|
|
2257
|
-
} else {
|
|
2258
|
-
workingDoc = await muxMp4Cooperatively(loadedDocs, { shortest }, context.signal);
|
|
2259
|
-
const cleanedComplex = filterComplex.replace(/\[[^\]]+\]/g, "").trim();
|
|
2260
|
-
if (cleanedComplex) vfFilters.push(cleanedComplex);
|
|
2261
|
-
}
|
|
2262
|
-
} else if (maps.length > 0) {
|
|
2263
|
-
let selectedTracks = [];
|
|
2264
|
-
let mapped = false;
|
|
2265
|
-
for (const map of maps) {
|
|
2266
|
-
const exclude = map.startsWith("-");
|
|
2267
|
-
let clean = exclude ? map.slice(1) : map;
|
|
2268
|
-
const optional = clean.endsWith("?");
|
|
2269
|
-
if (optional) clean = clean.slice(0, -1);
|
|
2270
|
-
if (clean.startsWith("[")) continue;
|
|
2271
|
-
const [input, selector, ordinal] = clean.split(":");
|
|
2272
|
-
const inputIdx = Number(input);
|
|
2273
|
-
const srcDoc = loadedDocs[inputIdx];
|
|
2274
|
-
if (!srcDoc) {
|
|
2275
|
-
await writeBytes(context.stderr, encodeUtf8(`ffmpeg: Invalid input file index: ${inputIdx}.
|
|
2276
|
-
`), context.signal);
|
|
2277
|
-
return { exitCode: 234 };
|
|
2278
|
-
}
|
|
2279
|
-
const type = selector === "v" ? "video" : selector === "a" ? "audio" : selector === "s" ? "subtitle" : void 0;
|
|
2280
|
-
let matches = type ? srcDoc.tracks.filter((t) => t.type === type) : srcDoc.tracks;
|
|
2281
|
-
const index = type ? ordinal : selector;
|
|
2282
|
-
if (index !== void 0) matches = matches.filter((_, i) => i === Number(index));
|
|
2283
|
-
if (matches.length === 0 && !optional) {
|
|
2284
|
-
await writeBytes(context.stderr, encodeUtf8(`ffmpeg: Stream map '${clean}' matches no streams.
|
|
2285
|
-
To ignore this, add a trailing '?' to the map.
|
|
2286
|
-
`), context.signal);
|
|
2287
|
-
return { exitCode: 234 };
|
|
2288
|
-
}
|
|
2289
|
-
if (exclude) {
|
|
2290
|
-
selectedTracks = selectedTracks.filter((entry) => entry.input !== inputIdx || !matches.includes(entry.track));
|
|
2291
|
-
} else {
|
|
2292
|
-
mapped ||= matches.length > 0;
|
|
2293
|
-
selectedTracks.push(...matches.map((track) => ({ input: inputIdx, track })));
|
|
2294
|
-
}
|
|
2295
|
-
}
|
|
2296
|
-
const filteredTracks = selectedTracks.map((entry) => entry.track);
|
|
2297
|
-
if (!mapped) {
|
|
2298
|
-
const tracks = selectAutomaticAudio(loadedDocs).flatMap((doc) => doc.tracks);
|
|
2299
|
-
for (const type of ["video", "audio"]) {
|
|
2300
|
-
const track = tracks.find((track2) => track2.type === type);
|
|
2301
|
-
if (track) filteredTracks.push(track);
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
workingDoc = await muxMp4Cooperatively(
|
|
2305
|
-
[{ ...loadedDocs[0], tracks: filteredTracks }],
|
|
2306
|
-
{ stripAudio, stripVideo, stripSubtitles, shortest, rotation },
|
|
2307
|
-
context.signal
|
|
2308
|
-
);
|
|
2309
|
-
} else {
|
|
2310
|
-
workingDoc = await muxMp4Cooperatively(selectAutomaticAudio(loadedDocs), {
|
|
2311
|
-
stripAudio,
|
|
2312
|
-
stripVideo,
|
|
2313
|
-
stripSubtitles,
|
|
2314
|
-
shortest,
|
|
2315
|
-
rotation
|
|
2316
|
-
}, context.signal);
|
|
2317
|
-
}
|
|
2318
|
-
if (outputSs !== void 0 || outputTo !== void 0 || outputDuration !== void 0) {
|
|
2319
|
-
workingDoc = sliceMp4(workingDoc, {
|
|
2320
|
-
startSeconds: outputSs,
|
|
2321
|
-
endSeconds: outputDuration === void 0 ? outputTo : void 0,
|
|
2322
|
-
durationSeconds: outputDuration
|
|
2323
|
-
});
|
|
2324
|
-
}
|
|
2325
|
-
if (outputSize && videoCodec !== "copy") {
|
|
2326
|
-
const [w, h] = outputSize.toLowerCase().split("x");
|
|
2327
|
-
if (w && h) vfFilters.push(`scale=${w}:${h}`);
|
|
2328
|
-
}
|
|
2329
|
-
if (outputFps && videoCodec !== "copy") {
|
|
2330
|
-
vfFilters.push(`fps=${outputFps}`);
|
|
2331
|
-
}
|
|
2332
|
-
const imageOutput = (outputFormat ? registry.findByFormatName(outputFormat) : registry.findByFilename(outputTarget))?.id === "image2";
|
|
2333
|
-
if (imageOutput || maxVideoFrames !== void 0 && videoCodec !== "copy") {
|
|
2334
|
-
workingDoc = {
|
|
2335
|
-
...workingDoc,
|
|
2336
|
-
tracks: await mapWork(workingDoc.tracks, async (track) => {
|
|
2337
|
-
if (track.type !== "video") return track;
|
|
2338
|
-
const limit = vfFilters.length === 0 ? maxVideoFrames : void 0;
|
|
2339
|
-
const frames = await ensureDecodedFrames(track, workingDoc.timescale, budget, context.signal, limit);
|
|
2340
|
-
return { ...track, samples: [], decodedVideoFrames: frames };
|
|
2341
|
-
}, context.signal)
|
|
2342
|
-
};
|
|
2343
|
-
}
|
|
2344
|
-
if (vfFilters.length > 0) {
|
|
2345
|
-
if (features.videoTranscode === false || features.filterGraph === false) {
|
|
2346
|
-
throw new Error("Video transcoding / filtergraph is disabled by consumer feature configuration");
|
|
2347
|
-
}
|
|
2348
|
-
const combinedChain = vfFilters.join(",");
|
|
2349
|
-
let loadedSubtitleCues;
|
|
2350
|
-
const subMatch = /subtitles=(?:filename=)?['"]?([^:'",]+)['"]?/.exec(combinedChain);
|
|
2351
|
-
if (subMatch?.[1]) {
|
|
2352
|
-
const subPath = resolvePath(context.cwd, subMatch[1]);
|
|
2353
|
-
const subBytes = await readInput(subPath);
|
|
2354
|
-
const subFmt = subPath.toLowerCase().endsWith(".vtt") ? "webvtt" : "srt";
|
|
2355
|
-
const subDoc = parseSubtitleDocument(subBytes, subFmt);
|
|
2356
|
-
const st = subDoc.tracks.find((t) => t.type === "subtitle");
|
|
2357
|
-
if (st) {
|
|
2358
|
-
const sts = st.timescale || 1e3;
|
|
2359
|
-
loadedSubtitleCues = st.samples.map((s) => ({
|
|
2360
|
-
startSec: s.pts / sts,
|
|
2361
|
-
endSec: (s.pts + s.duration) / sts,
|
|
2362
|
-
text: decodeUtf8(s.data).trim()
|
|
2363
|
-
}));
|
|
2768
|
+
await writeFileOutput(context, manifest, (data) => context.fs.writeFile(outResolved, data, { signal: context.signal }));
|
|
2769
|
+
options.onMetrics?.(budget.getStats());
|
|
2770
|
+
return { exitCode: 0 };
|
|
2364
2771
|
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
else {
|
|
2374
|
-
presentationOffset += edit.mediaTime / t.timescale;
|
|
2375
|
-
break;
|
|
2376
|
-
}
|
|
2377
|
-
}
|
|
2378
|
-
const offsetTicks = Math.round(presentationOffset * t.timescale);
|
|
2379
|
-
const decoded = await ensureDecodedFrames(t, workingDoc.timescale, budget, context.signal);
|
|
2380
|
-
const displayed = decoded.map((frame) => ({ ...frame, ptsSeconds: (Math.round(frame.ptsSeconds * t.timescale) - offsetTicks) / t.timescale }));
|
|
2381
|
-
const filtered = await applyVideoFilterChain(displayed, combinedChain, budget, loadedSubtitleCues, context.signal);
|
|
2382
|
-
const first = filtered[0];
|
|
2383
|
-
return {
|
|
2384
|
-
...t,
|
|
2385
|
-
width: first?.width ?? t.width,
|
|
2386
|
-
height: first?.height ?? t.height,
|
|
2387
|
-
samples: [],
|
|
2388
|
-
decodedVideoFrames: filtered.map((frame) => ({ ...frame, ptsSeconds: (Math.round(frame.ptsSeconds * t.timescale) + offsetTicks) / t.timescale }))
|
|
2389
|
-
};
|
|
2390
|
-
}, context.signal)
|
|
2391
|
-
};
|
|
2392
|
-
}
|
|
2393
|
-
if (maxVideoFrames !== void 0 && !outputTarget.includes("%")) {
|
|
2394
|
-
workingDoc = {
|
|
2395
|
-
...workingDoc,
|
|
2396
|
-
tracks: workingDoc.tracks.map((t) => {
|
|
2397
|
-
if (t.type !== "video") return t;
|
|
2398
|
-
if (t.decodedVideoFrames && t.decodedVideoFrames.length > maxVideoFrames) {
|
|
2399
|
-
return {
|
|
2400
|
-
...t,
|
|
2401
|
-
samples: [],
|
|
2402
|
-
decodedVideoFrames: t.decodedVideoFrames.slice(0, maxVideoFrames)
|
|
2403
|
-
};
|
|
2404
|
-
}
|
|
2405
|
-
if (t.samples.length > maxVideoFrames) {
|
|
2406
|
-
const slicedSamples = t.samples.slice(0, maxVideoFrames);
|
|
2407
|
-
const dur = slicedSamples.reduce((acc, s) => acc + s.duration, 0);
|
|
2408
|
-
return {
|
|
2409
|
-
...t,
|
|
2410
|
-
duration: dur,
|
|
2411
|
-
samples: slicedSamples
|
|
2412
|
-
};
|
|
2413
|
-
}
|
|
2414
|
-
return t;
|
|
2415
|
-
})
|
|
2416
|
-
};
|
|
2417
|
-
}
|
|
2418
|
-
if ((audioRate !== void 0 || audioChannels !== void 0 || afFilters.length > 0) && audioCodec !== "copy") {
|
|
2419
|
-
workingDoc = {
|
|
2420
|
-
...workingDoc,
|
|
2421
|
-
tracks: await mapWork(workingDoc.tracks, async (t) => {
|
|
2422
|
-
let work = 0;
|
|
2423
|
-
if (t.type !== "audio") return t;
|
|
2424
|
-
const audio = t.decodedAudio ?? (t.codecDescriptions[0]?.codecName === "aac" ? decodeAacTrack(workingDoc, t) : void 0);
|
|
2425
|
-
if (!audio) throw new Error("Audio filtering requires decoded PCM audio");
|
|
2426
|
-
const origRate = audio.sampleRate;
|
|
2427
|
-
const origCh = audio.channels;
|
|
2428
|
-
const targetRate = audioRate ?? origRate;
|
|
2429
|
-
const targetCh = audioChannels ?? origCh;
|
|
2430
|
-
const channelMask = audio.channelMask;
|
|
2431
|
-
const surroundMono = origCh === 6 && targetCh === 1;
|
|
2432
|
-
const monoStereo = origCh === 1 && (targetCh === 2 || targetCh === 3);
|
|
2433
|
-
if (targetCh !== origCh && origCh > 2 && (!surroundMono || channelMask && channelMask !== 63 && channelMask !== 1551)) {
|
|
2434
|
-
throw new Error(`Unsupported audio channel layout conversion: ${origCh} channels (mask ${channelMask ?? 0}) to ${targetCh}`);
|
|
2772
|
+
if (outputFormat === "hls" || /\.m3u8?$/i.test(outputTarget2)) {
|
|
2773
|
+
const outResolved = resolvePath(context.cwd, outputTarget2);
|
|
2774
|
+
const outDir = outResolved.includes("/") ? outResolved.slice(0, outResolved.lastIndexOf("/")) || "/" : "/";
|
|
2775
|
+
const isFmp4 = hlsSegmentType === "fmp4";
|
|
2776
|
+
const segExt = isFmp4 ? "m4s" : "ts";
|
|
2777
|
+
const segPlugin = registry.findByFormatName(isFmp4 ? "mp4" : "mpegts");
|
|
2778
|
+
if (!segPlugin) {
|
|
2779
|
+
throw new Error(`HLS segment format AST (${isFmp4 ? "mp4" : "mpegts"}) is not registered`);
|
|
2435
2780
|
}
|
|
2436
|
-
const
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
if (
|
|
2447
|
-
|
|
2448
|
-
context.signal?.throwIfAborted();
|
|
2449
|
-
}
|
|
2450
|
-
const trimmed = item.trim();
|
|
2451
|
-
if (trimmed.startsWith("volume=")) {
|
|
2452
|
-
const volSpec = trimmed.slice("volume=".length).trim();
|
|
2453
|
-
const decibels = volSpec.endsWith("dB");
|
|
2454
|
-
const gain = parseFilterExpression(decibels ? volSpec.slice(0, -2) : volSpec, "volume", {});
|
|
2455
|
-
const factor = decibels ? Math.pow(10, gain / 20) : gain;
|
|
2456
|
-
if (!Number.isFinite(factor)) throw new Error("volume: invalid gain");
|
|
2457
|
-
volFactor *= factor;
|
|
2458
|
-
} else if (trimmed.startsWith("atempo=")) {
|
|
2459
|
-
const tempo = Number(trimmed.slice(7));
|
|
2460
|
-
if (!Number.isFinite(tempo) || tempo < 0.5 || tempo > 100) {
|
|
2461
|
-
throw new Error("atempo must be in the range [0.5, 100]");
|
|
2462
|
-
}
|
|
2463
|
-
tempoFactor *= tempo;
|
|
2464
|
-
} else if (trimmed === "areverse") {
|
|
2465
|
-
reverseAudio = !reverseAudio;
|
|
2466
|
-
} else {
|
|
2467
|
-
throw new Error(`Unsupported audio filter: ${trimmed}`);
|
|
2781
|
+
const vTrk = workingDoc.tracks.find((t) => t.type === "video");
|
|
2782
|
+
const totalSec = Math.max(
|
|
2783
|
+
0.1,
|
|
2784
|
+
vTrk ? vTrk.duration / Math.max(1, vTrk.timescale) : workingDoc.durationSeconds || 1
|
|
2785
|
+
);
|
|
2786
|
+
const segDur = Math.max(0.2, hlsTime);
|
|
2787
|
+
const boundaries = [0];
|
|
2788
|
+
if (vTrk?.samples.length) {
|
|
2789
|
+
for (const sample of vTrk.samples) {
|
|
2790
|
+
const seconds = sample.dts / vTrk.timescale;
|
|
2791
|
+
if (sample.isKeyframe && seconds >= boundaries[boundaries.length - 1] + segDur - 1e-6 && seconds < totalSec) {
|
|
2792
|
+
boundaries.push(seconds);
|
|
2468
2793
|
}
|
|
2469
2794
|
}
|
|
2795
|
+
} else {
|
|
2796
|
+
for (let seconds = segDur; seconds < totalSec - 1e-6; seconds += segDur) boundaries.push(seconds);
|
|
2470
2797
|
}
|
|
2471
|
-
|
|
2472
|
-
let
|
|
2473
|
-
|
|
2474
|
-
const
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2798
|
+
boundaries.push(totalSec);
|
|
2799
|
+
let targetDuration = 0;
|
|
2800
|
+
for (let i2 = 1; i2 < boundaries.length; i2++) targetDuration = Math.max(targetDuration, boundaries[i2] - boundaries[i2 - 1]);
|
|
2801
|
+
const firstSegment = hlsListSize === 0 ? 0 : Math.max(0, boundaries.length - 1 - hlsListSize);
|
|
2802
|
+
const independentSegments = hlsIndependentSegments && !!vTrk && workingDoc.tracks.filter((track) => track.type === "video").every((track) => boundaries.slice(firstSegment, -1).every((start, index) => {
|
|
2803
|
+
const first = track.samples.find((sample) => sample.dts >= start * track.timescale);
|
|
2804
|
+
return first?.isKeyframe && first.dts < boundaries[firstSegment + index + 1] * track.timescale;
|
|
2805
|
+
}));
|
|
2806
|
+
const playlistLines = [
|
|
2807
|
+
"#EXTM3U",
|
|
2808
|
+
isFmp4 ? "#EXT-X-VERSION:7" : independentSegments ? "#EXT-X-VERSION:6" : "#EXT-X-VERSION:3",
|
|
2809
|
+
`#EXT-X-TARGETDURATION:${Math.ceil(targetDuration)}`,
|
|
2810
|
+
`#EXT-X-MEDIA-SEQUENCE:${(outputStartNumber ?? 0) + firstSegment}`
|
|
2811
|
+
];
|
|
2812
|
+
if (independentSegments) playlistLines.push("#EXT-X-INDEPENDENT-SEGMENTS");
|
|
2813
|
+
if (isFmp4) playlistLines.push('#EXT-X-MAP:URI="init.mp4"');
|
|
2814
|
+
const retainedSegments = [];
|
|
2815
|
+
for (let sIdx = 0; sIdx < boundaries.length - 1; sIdx++) {
|
|
2816
|
+
const st = boundaries[sIdx];
|
|
2817
|
+
const en = boundaries[sIdx + 1];
|
|
2818
|
+
const actualDur = Math.max(0.04, en - st);
|
|
2819
|
+
const segmentDoc = {
|
|
2820
|
+
...workingDoc,
|
|
2821
|
+
tracks: workingDoc.tracks.map((track) => ({
|
|
2822
|
+
...track,
|
|
2823
|
+
samples: track.samples.filter((sample) => sample.dts >= st * track.timescale && sample.dts < en * track.timescale)
|
|
2824
|
+
}))
|
|
2825
|
+
};
|
|
2826
|
+
let segBytes = segPlugin.serialize(segmentDoc, {
|
|
2827
|
+
fragmented: isFmp4,
|
|
2828
|
+
limits: options.limits,
|
|
2829
|
+
budget
|
|
2830
|
+
});
|
|
2831
|
+
if (isFmp4) {
|
|
2832
|
+
const moof = parseMp4Boxes(segBytes, 0, 0, budget).find((box) => box.type === "moof");
|
|
2833
|
+
if (!moof) throw new Error("HLS fMP4 output requires fragmented MP4 samples");
|
|
2834
|
+
if (sIdx === 0) {
|
|
2835
|
+
await context.fs.mkdir(outDir, { recursive: true, signal: context.signal });
|
|
2836
|
+
await writeFileOutput(context, segBytes.subarray(0, moof.offset), (data) => context.fs.writeFile(resolvePath(outDir, "init.mp4"), data, { signal: context.signal }));
|
|
2837
|
+
}
|
|
2838
|
+
segBytes = segBytes.subarray(moof.offset);
|
|
2479
2839
|
}
|
|
2480
|
-
const
|
|
2481
|
-
const
|
|
2482
|
-
const
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2840
|
+
const outName = outResolved.slice(outResolved.lastIndexOf("/") + 1);
|
|
2841
|
+
const segPattern = hlsSegmentFilename ?? `${outName.includes(".") ? outName.slice(0, outName.lastIndexOf(".")) : outName}%d.${segExt}`;
|
|
2842
|
+
const formattedSeg = segPattern.replace(/%0?(\d*)d/, (_, widthDigits) => {
|
|
2843
|
+
const padLen = parseInt(widthDigits || "0", 10) || 0;
|
|
2844
|
+
return String(sIdx + (outputStartNumber ?? 0)).padStart(padLen, "0");
|
|
2845
|
+
});
|
|
2846
|
+
const fullSegPath = resolvePath(hlsSegmentFilename === void 0 ? outDir : context.cwd, formattedSeg);
|
|
2847
|
+
const relSegName = fullSegPath.startsWith(outDir === "/" ? "/" : `${outDir}/`) ? fullSegPath.slice(outDir === "/" ? 1 : outDir.length + 1) : formattedSeg;
|
|
2848
|
+
budget.checkOutputBytes(segBytes.byteLength);
|
|
2849
|
+
await context.fs.mkdir(fullSegPath.slice(0, fullSegPath.lastIndexOf("/")) || "/", { recursive: true, signal: context.signal });
|
|
2850
|
+
await writeFileOutput(context, segBytes, (data) => context.fs.writeFile(fullSegPath, data, { signal: context.signal }));
|
|
2851
|
+
if (sIdx >= firstSegment) playlistLines.push(`#EXTINF:${actualDur.toFixed(6)},`, relSegName);
|
|
2852
|
+
if (hlsDeleteSegments && hlsListSize > 0) {
|
|
2853
|
+
retainedSegments.push({ path: fullSegPath, duration: actualDur });
|
|
2854
|
+
const expiredCount = retainedSegments.length - hlsListSize;
|
|
2855
|
+
if (expiredCount > 0) {
|
|
2856
|
+
const playlistDuration = boundaries[sIdx + 1] - boundaries[sIdx + 1 - hlsListSize];
|
|
2857
|
+
const retentionDuration = playlistDuration + retainedSegments[expiredCount - 1].duration;
|
|
2858
|
+
let duration = 0;
|
|
2859
|
+
let count = 0;
|
|
2860
|
+
for (let i2 = expiredCount - 1; i2 >= 0; i2--) {
|
|
2861
|
+
duration += retainedSegments[i2].duration;
|
|
2862
|
+
count++;
|
|
2863
|
+
if (count >= hlsDeleteThreshold || duration >= retentionDuration) break;
|
|
2864
|
+
}
|
|
2865
|
+
for (const segment of retainedSegments.splice(0, expiredCount - count)) {
|
|
2866
|
+
await context.fs.rm(segment.path, { signal: context.signal });
|
|
2488
2867
|
}
|
|
2489
|
-
const srcIdx = Math.min(srcCh.length - 1, i);
|
|
2490
|
-
const sample = surroundMono ? ((stretched[0][srcIdx] + stretched[1][srcIdx]) * Math.SQRT1_2 + stretched[2][srcIdx] + (stretched[4][srcIdx] + stretched[5][srcIdx]) / 2) / (2 + Math.SQRT2) : rightCh ? (srcCh[srcIdx] + rightCh[srcIdx]) / 2 : srcCh[srcIdx];
|
|
2491
|
-
dst[i] = Math.max(-1, Math.min(1, sample * volFactor * (monoStereo ? Math.SQRT1_2 : 1)));
|
|
2492
2868
|
}
|
|
2493
2869
|
}
|
|
2494
|
-
if (reverseAudio) dst.reverse();
|
|
2495
|
-
newChannelData.push(dst);
|
|
2496
2870
|
}
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
}));
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2871
|
+
if (!hlsOmitEndlist) playlistLines.push("#EXT-X-ENDLIST");
|
|
2872
|
+
playlistLines.push("");
|
|
2873
|
+
const m3u8Bytes = encodeUtf8(playlistLines.join("\n"));
|
|
2874
|
+
await context.fs.mkdir(outDir, { recursive: true, signal: context.signal });
|
|
2875
|
+
await writeFileOutput(context, m3u8Bytes, (data) => context.fs.writeFile(outResolved, data, { signal: context.signal }));
|
|
2876
|
+
options.onMetrics?.(budget.getStats());
|
|
2877
|
+
return { exitCode: 0 };
|
|
2878
|
+
}
|
|
2879
|
+
if (imageOutput && !workingDoc.tracks.some((track) => track.type === "video" && track.decodedVideoFrames?.length)) {
|
|
2880
|
+
throw new Error("No video frame in the requested presentation interval");
|
|
2881
|
+
}
|
|
2882
|
+
if (/%0?\d*d/.test(outputTarget2)) {
|
|
2883
|
+
const outExt2 = outputTarget2.split(".").pop()?.toLowerCase() ?? "png";
|
|
2884
|
+
const imgPlugin = registry.findByFilename(`x.${outExt2}`);
|
|
2885
|
+
if (!imgPlugin || !imgPlugin.canMux) {
|
|
2886
|
+
throw new Error(`Unsupported output format for '${outputTarget2}' (AST not registered)`);
|
|
2887
|
+
}
|
|
2888
|
+
const vTrack = workingDoc.tracks.find((t) => t.type === "video");
|
|
2889
|
+
const frames = vTrack ? await ensureDecodedFrames(vTrack, workingDoc.timescale, budget, context.signal) : [];
|
|
2890
|
+
const limit = maxVideoFrames ?? Infinity;
|
|
2891
|
+
const duration = frames[0]?.durationSeconds ?? 1;
|
|
2892
|
+
const constantRate = ["auto", "-1", "cfr", "1"].includes(videoSync);
|
|
2893
|
+
let nextPts = (frames[0]?.ptsSeconds ?? 0) / duration;
|
|
2894
|
+
let idx = 0;
|
|
2895
|
+
const imgFmt = outExt2 === "jpg" || outExt2 === "jpeg" ? "jpeg" : outExt2 === "webp" ? "webp" : outExt2 === "gif" ? "gif" : outExt2 === "bmp" ? "bmp" : outExt2 === "ppm" ? "ppm" : "png";
|
|
2896
|
+
for (let sourceIndex = 0; sourceIndex < frames.length && idx < limit; sourceIndex++) {
|
|
2897
|
+
const source = frames[sourceIndex];
|
|
2898
|
+
const delta0 = source.ptsSeconds / duration - nextPts;
|
|
2899
|
+
const delta = delta0 + source.durationSeconds / duration;
|
|
2900
|
+
const copies = constantRate ? delta < -1.1 ? 0 : delta > 1.1 ? Math.round(delta) : 1 : 1;
|
|
2901
|
+
const previousCopies = constantRate && delta0 > 1.1 ? Math.max(0, Math.round(delta0 - 0.6)) : 0;
|
|
2902
|
+
if (!Number.isSafeInteger(copies)) throw new Error("Invalid output frame timestamp");
|
|
2903
|
+
for (let copy = 0; copy < copies && idx < limit; copy++, idx++, nextPts++) {
|
|
2904
|
+
budget.checkCpu();
|
|
2905
|
+
budget.checkSamples(idx + 1);
|
|
2906
|
+
budget.checkDuration((idx + 1) * duration);
|
|
2907
|
+
const f = copy < previousCopies && sourceIndex > 0 ? frames[sourceIndex - 1] : source;
|
|
2908
|
+
if (copy > 0) budget.recordFrame(f.width, f.height);
|
|
2909
|
+
const fileName = outputTarget2.replace(/%0?(\d*)d/, (_, widthDigits) => {
|
|
2910
|
+
const padLen = parseInt(widthDigits || "0", 10) || 0;
|
|
2911
|
+
return String(idx + (outputStartNumber ?? 1)).padStart(padLen, "0");
|
|
2912
|
+
});
|
|
2913
|
+
const fullOutPath = resolvePath(context.cwd, fileName);
|
|
2914
|
+
const encoded = (await yieldTurn5(context.signal), context.signal?.throwIfAborted(), encodeImage(makeRgbaImg(f.width, f.height, f.data), {
|
|
2915
|
+
format: imgFmt
|
|
2916
|
+
})).data;
|
|
2917
|
+
budget.checkOutputBytes(encoded.byteLength);
|
|
2918
|
+
await writeFileOutput(context, encoded, (data) => context.fs.writeFile(fullOutPath, data, { signal: context.signal }));
|
|
2514
2919
|
}
|
|
2515
|
-
};
|
|
2516
|
-
}, context.signal)
|
|
2517
|
-
};
|
|
2518
|
-
}
|
|
2519
|
-
if (chapterSource !== void 0) {
|
|
2520
|
-
if (chapterSource >= loadedDocs.length) {
|
|
2521
|
-
throw new Error(`Invalid chapter input index: ${chapterSource}`);
|
|
2522
|
-
}
|
|
2523
|
-
workingDoc = { ...workingDoc, chapters: chapterSource < 0 ? void 0 : loadedDocs[chapterSource].chapters };
|
|
2524
|
-
}
|
|
2525
|
-
if (metadataSource !== void 0 && metadataSource >= loadedDocs.length) {
|
|
2526
|
-
throw new Error(`Invalid metadata input index: ${metadataSource}`);
|
|
2527
|
-
}
|
|
2528
|
-
const sourceMetadata = metadataSource === void 0 ? loadedDocs[0].metadata : metadataSource < 0 ? {} : loadedDocs[metadataSource].metadata;
|
|
2529
|
-
const metaTags = {
|
|
2530
|
-
...sourceMetadata,
|
|
2531
|
-
title: metadata.title ?? sourceMetadata.title,
|
|
2532
|
-
artist: metadata.artist ?? sourceMetadata.artist,
|
|
2533
|
-
album: metadata.album ?? sourceMetadata.album,
|
|
2534
|
-
date: metadata.date ?? metadata.year ?? sourceMetadata.date,
|
|
2535
|
-
comment: metadata.comment ?? sourceMetadata.comment,
|
|
2536
|
-
genre: metadata.genre ?? sourceMetadata.genre,
|
|
2537
|
-
encoder: metadata.encoder ?? sourceMetadata.encoder
|
|
2538
|
-
};
|
|
2539
|
-
workingDoc = {
|
|
2540
|
-
...workingDoc,
|
|
2541
|
-
faststart,
|
|
2542
|
-
metadata: metaTags,
|
|
2543
|
-
tracks: applyStreamDispositions(applyStreamLanguages(workingDoc.tracks, streamLanguages), streamDispositions)
|
|
2544
|
-
};
|
|
2545
|
-
if (outputFormat === "null" || outputTarget === "/dev/null") {
|
|
2546
|
-
options.onMetrics?.(budget.getStats());
|
|
2547
|
-
return { exitCode: 0 };
|
|
2548
|
-
}
|
|
2549
|
-
if (outputFormat === "dash" || outputTarget.toLowerCase().endsWith(".mpd")) {
|
|
2550
|
-
if (!registry.findByFormatName("dash")?.canMux) throw new Error("DASH AST is not registered");
|
|
2551
|
-
const outResolved = resolvePath(context.cwd, outputTarget);
|
|
2552
|
-
const outDir = outResolved.slice(0, outResolved.lastIndexOf("/")) || "/";
|
|
2553
|
-
const { manifest, resources } = serializeDashDocument(workingDoc, { limits: options.limits, budget });
|
|
2554
|
-
await context.fs.mkdir(outDir, { recursive: true, signal: context.signal });
|
|
2555
|
-
for (const [uri, bytes] of resources) {
|
|
2556
|
-
await writeFileOutput(context, bytes, (data) => context.fs.writeFile(resolvePath(outDir, uri), data, { signal: context.signal }));
|
|
2557
|
-
}
|
|
2558
|
-
await writeFileOutput(context, manifest, (data) => context.fs.writeFile(outResolved, data, { signal: context.signal }));
|
|
2559
|
-
options.onMetrics?.(budget.getStats());
|
|
2560
|
-
return { exitCode: 0 };
|
|
2561
|
-
}
|
|
2562
|
-
if (outputFormat === "hls" || /\.m3u8?$/i.test(outputTarget)) {
|
|
2563
|
-
const outResolved = resolvePath(context.cwd, outputTarget);
|
|
2564
|
-
const outDir = outResolved.includes("/") ? outResolved.slice(0, outResolved.lastIndexOf("/")) || "/" : "/";
|
|
2565
|
-
const isFmp4 = hlsSegmentType === "fmp4";
|
|
2566
|
-
const segExt = isFmp4 ? "m4s" : "ts";
|
|
2567
|
-
const segPlugin = registry.findByFormatName(isFmp4 ? "mp4" : "mpegts");
|
|
2568
|
-
if (!segPlugin) {
|
|
2569
|
-
throw new Error(`HLS segment format AST (${isFmp4 ? "mp4" : "mpegts"}) is not registered`);
|
|
2570
|
-
}
|
|
2571
|
-
const vTrk = workingDoc.tracks.find((t) => t.type === "video");
|
|
2572
|
-
const totalSec = Math.max(
|
|
2573
|
-
0.1,
|
|
2574
|
-
vTrk ? vTrk.duration / Math.max(1, vTrk.timescale) : workingDoc.durationSeconds || 1
|
|
2575
|
-
);
|
|
2576
|
-
const segDur = Math.max(0.2, hlsTime);
|
|
2577
|
-
const boundaries = [0];
|
|
2578
|
-
if (vTrk?.samples.length) {
|
|
2579
|
-
for (const sample of vTrk.samples) {
|
|
2580
|
-
const seconds = sample.dts / vTrk.timescale;
|
|
2581
|
-
if (sample.isKeyframe && seconds >= boundaries[boundaries.length - 1] + segDur - 1e-6 && seconds < totalSec) {
|
|
2582
|
-
boundaries.push(seconds);
|
|
2583
2920
|
}
|
|
2921
|
+
options.onMetrics?.(budget.getStats());
|
|
2922
|
+
return { exitCode: 0 };
|
|
2584
2923
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
const st = boundaries[sIdx];
|
|
2607
|
-
const en = boundaries[sIdx + 1];
|
|
2608
|
-
const actualDur = Math.max(0.04, en - st);
|
|
2609
|
-
const segmentDoc = {
|
|
2610
|
-
...workingDoc,
|
|
2611
|
-
tracks: workingDoc.tracks.map((track) => ({
|
|
2612
|
-
...track,
|
|
2613
|
-
samples: track.samples.filter((sample) => sample.dts >= st * track.timescale && sample.dts < en * track.timescale)
|
|
2614
|
-
}))
|
|
2615
|
-
};
|
|
2616
|
-
let segBytes = segPlugin.serialize(segmentDoc, {
|
|
2617
|
-
fragmented: isFmp4,
|
|
2924
|
+
const outPlugin = outputFormat ? registry.findByFormatName(outputFormat) : registry.findByFilename(outputTarget2);
|
|
2925
|
+
if (!outPlugin || !outPlugin.canMux) {
|
|
2926
|
+
const fmtDesc = outputFormat ?? outputTarget2;
|
|
2927
|
+
throw new Error(
|
|
2928
|
+
`Unable to find a suitable output format for '${fmtDesc}' (format AST not registered)`
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
{
|
|
2932
|
+
const gc = globalThis.gc;
|
|
2933
|
+
if (typeof gc === "function") {
|
|
2934
|
+
gc();
|
|
2935
|
+
gc();
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
const outExt = outputTarget2.split(".").pop()?.toLowerCase();
|
|
2939
|
+
const serializedBytes = outPlugin.serialize(workingDoc, {
|
|
2940
|
+
format: outputFormat ?? outExt,
|
|
2941
|
+
audioCodec,
|
|
2942
|
+
faststart,
|
|
2943
|
+
fragmented,
|
|
2944
|
+
metadata: metaTags,
|
|
2618
2945
|
limits: options.limits,
|
|
2619
2946
|
budget
|
|
2620
2947
|
});
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
if (sIdx === 0) {
|
|
2625
|
-
await context.fs.mkdir(outDir, { recursive: true, signal: context.signal });
|
|
2626
|
-
await writeFileOutput(context, segBytes.subarray(0, moof.offset), (data) => context.fs.writeFile(resolvePath(outDir, "init.mp4"), data, { signal: context.signal }));
|
|
2948
|
+
for (const trk of workingDoc.tracks) {
|
|
2949
|
+
if (trk.decodedVideoFrames) {
|
|
2950
|
+
for (const f of trk.decodedVideoFrames) detachBytesBuffer(f.data);
|
|
2627
2951
|
}
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
const outName = outResolved.slice(outResolved.lastIndexOf("/") + 1);
|
|
2631
|
-
const segPattern = hlsSegmentFilename ?? `${outName.includes(".") ? outName.slice(0, outName.lastIndexOf(".")) : outName}%d.${segExt}`;
|
|
2632
|
-
const formattedSeg = segPattern.replace(/%0?(\d*)d/, (_, widthDigits) => {
|
|
2633
|
-
const padLen = parseInt(widthDigits || "0", 10) || 0;
|
|
2634
|
-
return String(sIdx + (outputStartNumber ?? 0)).padStart(padLen, "0");
|
|
2635
|
-
});
|
|
2636
|
-
const fullSegPath = resolvePath(hlsSegmentFilename === void 0 ? outDir : context.cwd, formattedSeg);
|
|
2637
|
-
const relSegName = fullSegPath.startsWith(outDir === "/" ? "/" : `${outDir}/`) ? fullSegPath.slice(outDir === "/" ? 1 : outDir.length + 1) : formattedSeg;
|
|
2638
|
-
budget.checkOutputBytes(segBytes.byteLength);
|
|
2639
|
-
await context.fs.mkdir(fullSegPath.slice(0, fullSegPath.lastIndexOf("/")) || "/", { recursive: true, signal: context.signal });
|
|
2640
|
-
await writeFileOutput(context, segBytes, (data) => context.fs.writeFile(fullSegPath, data, { signal: context.signal }));
|
|
2641
|
-
if (sIdx >= firstSegment) playlistLines.push(`#EXTINF:${actualDur.toFixed(6)},`, relSegName);
|
|
2642
|
-
if (hlsDeleteSegments && hlsListSize > 0) {
|
|
2643
|
-
retainedSegments.push({ path: fullSegPath, duration: actualDur });
|
|
2644
|
-
const expiredCount = retainedSegments.length - hlsListSize;
|
|
2645
|
-
if (expiredCount > 0) {
|
|
2646
|
-
const playlistDuration = boundaries[sIdx + 1] - boundaries[sIdx + 1 - hlsListSize];
|
|
2647
|
-
const retentionDuration = playlistDuration + retainedSegments[expiredCount - 1].duration;
|
|
2648
|
-
let duration = 0;
|
|
2649
|
-
let count = 0;
|
|
2650
|
-
for (let i = expiredCount - 1; i >= 0; i--) {
|
|
2651
|
-
duration += retainedSegments[i].duration;
|
|
2652
|
-
count++;
|
|
2653
|
-
if (count >= hlsDeleteThreshold || duration >= retentionDuration) break;
|
|
2654
|
-
}
|
|
2655
|
-
for (const segment of retainedSegments.splice(0, expiredCount - count)) {
|
|
2656
|
-
await context.fs.rm(segment.path, { signal: context.signal });
|
|
2657
|
-
}
|
|
2952
|
+
if (trk.samples) {
|
|
2953
|
+
for (const s of trk.samples) detachBytesBuffer(s.data);
|
|
2658
2954
|
}
|
|
2659
2955
|
}
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
const limit = maxVideoFrames ?? Infinity;
|
|
2681
|
-
const duration = frames[0]?.durationSeconds ?? 1;
|
|
2682
|
-
const constantRate = ["auto", "-1", "cfr", "1"].includes(videoSync);
|
|
2683
|
-
let nextPts = (frames[0]?.ptsSeconds ?? 0) / duration;
|
|
2684
|
-
let idx = 0;
|
|
2685
|
-
const imgFmt = outExt2 === "jpg" || outExt2 === "jpeg" ? "jpeg" : outExt2 === "webp" ? "webp" : outExt2 === "gif" ? "gif" : outExt2 === "bmp" ? "bmp" : outExt2 === "ppm" ? "ppm" : "png";
|
|
2686
|
-
for (let sourceIndex = 0; sourceIndex < frames.length && idx < limit; sourceIndex++) {
|
|
2687
|
-
const source = frames[sourceIndex];
|
|
2688
|
-
const delta0 = source.ptsSeconds / duration - nextPts;
|
|
2689
|
-
const delta = delta0 + source.durationSeconds / duration;
|
|
2690
|
-
const copies = constantRate ? delta < -1.1 ? 0 : delta > 1.1 ? Math.round(delta) : 1 : 1;
|
|
2691
|
-
const previousCopies = constantRate && delta0 > 1.1 ? Math.max(0, Math.round(delta0 - 0.6)) : 0;
|
|
2692
|
-
if (!Number.isSafeInteger(copies)) throw new Error("Invalid output frame timestamp");
|
|
2693
|
-
for (let copy = 0; copy < copies && idx < limit; copy++, idx++, nextPts++) {
|
|
2694
|
-
budget.checkCpu();
|
|
2695
|
-
budget.checkSamples(idx + 1);
|
|
2696
|
-
budget.checkDuration((idx + 1) * duration);
|
|
2697
|
-
const f = copy < previousCopies && sourceIndex > 0 ? frames[sourceIndex - 1] : source;
|
|
2698
|
-
if (copy > 0) budget.recordFrame(f.width, f.height);
|
|
2699
|
-
const fileName = outputTarget.replace(/%0?(\d*)d/, (_, widthDigits) => {
|
|
2700
|
-
const padLen = parseInt(widthDigits || "0", 10) || 0;
|
|
2701
|
-
return String(idx + (outputStartNumber ?? 1)).padStart(padLen, "0");
|
|
2702
|
-
});
|
|
2703
|
-
const fullOutPath = resolvePath(context.cwd, fileName);
|
|
2704
|
-
const encoded = (await yieldTurn5(context.signal), context.signal?.throwIfAborted(), encodeImage(makeRgbaImg(f.width, f.height, f.data), {
|
|
2705
|
-
format: imgFmt
|
|
2706
|
-
})).data;
|
|
2707
|
-
budget.checkOutputBytes(encoded.byteLength);
|
|
2708
|
-
await writeFileOutput(context, encoded, (data) => context.fs.writeFile(fullOutPath, data, { signal: context.signal }));
|
|
2956
|
+
budget.checkOutputBytes(serializedBytes.byteLength);
|
|
2957
|
+
if (isStdout(outputTarget2)) {
|
|
2958
|
+
await writeBytes(context.stdout, serializedBytes, context.signal);
|
|
2959
|
+
} else {
|
|
2960
|
+
const fullOutPath = resolvePath(context.cwd, outputTarget2);
|
|
2961
|
+
await writeFileOutput(context, serializedBytes, (data) => context.fs.writeFile(fullOutPath, data, { signal: context.signal }));
|
|
2962
|
+
}
|
|
2963
|
+
options.onMetrics?.(budget.getStats());
|
|
2964
|
+
return { exitCode: 0 };
|
|
2965
|
+
} catch (err) {
|
|
2966
|
+
rethrowRuntimeError(context, err);
|
|
2967
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2968
|
+
await writeBytes(context.stderr, encodeUtf8(`ffmpeg: ${msg}
|
|
2969
|
+
`), context.signal);
|
|
2970
|
+
return { exitCode: 1 };
|
|
2971
|
+
} finally {
|
|
2972
|
+
const gc = globalThis.gc;
|
|
2973
|
+
if (typeof gc === "function") {
|
|
2974
|
+
gc();
|
|
2975
|
+
gc();
|
|
2709
2976
|
}
|
|
2710
2977
|
}
|
|
2711
|
-
options.onMetrics?.(budget.getStats());
|
|
2712
|
-
return { exitCode: 0 };
|
|
2713
|
-
}
|
|
2714
|
-
const outPlugin = outputFormat ? registry.findByFormatName(outputFormat) : registry.findByFilename(outputTarget);
|
|
2715
|
-
if (!outPlugin || !outPlugin.canMux) {
|
|
2716
|
-
const fmtDesc = outputFormat ?? outputTarget;
|
|
2717
|
-
throw new Error(
|
|
2718
|
-
`Unable to find a suitable output format for '${fmtDesc}' (format AST not registered)`
|
|
2719
|
-
);
|
|
2720
|
-
}
|
|
2721
|
-
{
|
|
2722
|
-
const gc = globalThis.gc;
|
|
2723
|
-
if (typeof gc === "function") {
|
|
2724
|
-
gc();
|
|
2725
|
-
gc();
|
|
2726
|
-
}
|
|
2727
|
-
}
|
|
2728
|
-
const outExt = outputTarget.split(".").pop()?.toLowerCase();
|
|
2729
|
-
const serializedBytes = outPlugin.serialize(workingDoc, {
|
|
2730
|
-
format: outputFormat ?? outExt,
|
|
2731
|
-
audioCodec,
|
|
2732
|
-
faststart,
|
|
2733
|
-
fragmented,
|
|
2734
|
-
metadata: metaTags,
|
|
2735
|
-
limits: options.limits,
|
|
2736
|
-
budget
|
|
2737
2978
|
});
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
}
|
|
2742
|
-
if (trk.samples) {
|
|
2743
|
-
for (const s of trk.samples) detachBytesBuffer(s.data);
|
|
2744
|
-
}
|
|
2745
|
-
}
|
|
2746
|
-
budget.checkOutputBytes(serializedBytes.byteLength);
|
|
2747
|
-
if (isStdout(outputTarget)) {
|
|
2748
|
-
await writeBytes(context.stdout, serializedBytes, context.signal);
|
|
2749
|
-
} else {
|
|
2750
|
-
const fullOutPath = resolvePath(context.cwd, outputTarget);
|
|
2751
|
-
await writeFileOutput(context, serializedBytes, (data) => context.fs.writeFile(fullOutPath, data, { signal: context.signal }));
|
|
2752
|
-
}
|
|
2753
|
-
options.onMetrics?.(budget.getStats());
|
|
2754
|
-
return { exitCode: 0 };
|
|
2755
|
-
} catch (err) {
|
|
2756
|
-
rethrowRuntimeError(context, err);
|
|
2757
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2758
|
-
await writeBytes(context.stderr, encodeUtf8(`ffmpeg: ${msg}
|
|
2759
|
-
`), context.signal);
|
|
2979
|
+
}
|
|
2980
|
+
if (outputs.length === 0) {
|
|
2981
|
+
await writeBytes(context.stderr, encodeUtf8("ffmpeg: must specify an output file\n"), context.signal);
|
|
2760
2982
|
return { exitCode: 1 };
|
|
2761
|
-
} finally {
|
|
2762
|
-
const gc = globalThis.gc;
|
|
2763
|
-
if (typeof gc === "function") {
|
|
2764
|
-
gc();
|
|
2765
|
-
gc();
|
|
2766
|
-
}
|
|
2767
2983
|
}
|
|
2984
|
+
for (const output of outputs) {
|
|
2985
|
+
const result = await output();
|
|
2986
|
+
if (result.exitCode !== 0) return result;
|
|
2987
|
+
}
|
|
2988
|
+
return { exitCode: 0 };
|
|
2768
2989
|
}
|
|
2769
2990
|
};
|
|
2770
2991
|
}
|