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