@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.
@@ -23,7 +23,7 @@ import {
23
23
  sliceMp4,
24
24
  srtAst,
25
25
  webvttAst
26
- } from "../safe-bash/command-chunks/chunk-SBBMNLD6.js";
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 = true;
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") noOverwrite = false;
1364
- else if (arg === "-n") noOverwrite = true;
1365
- 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++;
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") audioCodec = args[++i];
1428
- else if (arg === "-c:s" || arg === "-scodec" || arg === "-codec:s") i++;
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") i++;
1435
- else if (arg === "-movflags") {
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 outputTarget = arg;
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, inp.format);
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 && inp.streamLoop > 0) {
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 readInput = createInputReader(context, budget);
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 outputStartNumber;
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
- let chapterSource;
1636
- let metadataSource;
1637
- const metadata = {};
1638
- const streamLanguages = [];
1639
- const streamDispositions = [];
1640
- let outputTarget;
1641
- for (let i = 0; i < args.length; i++) {
1642
- const arg = args[i];
1643
- if (arg === "-y") {
1644
- overwrite = true;
1645
- } else if (arg === "-n") {
1646
- overwrite = false;
1647
- } 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") {
1648
- i++;
1649
- } else if (arg === "-vsync" || arg === "-fps_mode") {
1650
- videoSync = args[++i] ?? "";
1651
- } else if (arg === "-hls_time" || arg === "-seg_duration") {
1652
- hlsTime = parseFloat(args[++i] ?? "2") || 2;
1653
- } else if (arg === "-hls_segment_filename") {
1654
- hlsSegmentFilename = args[++i];
1655
- } else if (arg === "-hls_segment_type") {
1656
- hlsSegmentType = args[++i];
1657
- } else if (arg === "-hls_list_size") {
1658
- const value = args[++i];
1659
- hlsListSize = value?.trim() ? Number(value) : NaN;
1660
- if (!Number.isSafeInteger(hlsListSize) || hlsListSize < 0) {
1661
- await writeBytes(context.stderr, encodeUtf8("ffmpeg: invalid hls_list_size\n"), context.signal);
1662
- return { exitCode: 1 };
1663
- }
1664
- } else if (arg === "-hls_flags") {
1665
- const value = args[++i] ?? "";
1666
- if (!value.startsWith("+") && !value.startsWith("-")) {
1667
- hlsDeleteSegments = false;
1668
- hlsOmitEndlist = false;
1669
- hlsIndependentSegments = false;
1670
- }
1671
- for (const flag of value.replaceAll("-", "+-").split("+")) {
1672
- if (flag === "delete_segments") hlsDeleteSegments = true;
1673
- if (flag === "-delete_segments") hlsDeleteSegments = false;
1674
- if (flag === "independent_segments") hlsIndependentSegments = true;
1675
- if (flag === "-independent_segments") hlsIndependentSegments = false;
1676
- if (flag === "omit_endlist") hlsOmitEndlist = true;
1677
- if (flag === "-omit_endlist") hlsOmitEndlist = false;
1678
- }
1679
- } else if (arg === "-hls_delete_threshold") {
1680
- const value = args[++i];
1681
- hlsDeleteThreshold = value?.trim() ? Number(value) : NaN;
1682
- if (!Number.isSafeInteger(hlsDeleteThreshold) || hlsDeleteThreshold < 1) {
1683
- await writeBytes(context.stderr, encodeUtf8("ffmpeg: invalid hls_delete_threshold\n"), context.signal);
1684
- return { exitCode: 1 };
1685
- }
1686
- } else if (arg === "-map_chapters") {
1687
- const value = args[++i];
1688
- chapterSource = value?.trim() ? Number(value) : NaN;
1689
- if (!Number.isSafeInteger(chapterSource)) {
1690
- await writeBytes(context.stderr, encodeUtf8("ffmpeg: invalid chapter input index\n"), context.signal);
1691
- return { exitCode: 1 };
1692
- }
1693
- } else if (arg === "-map_metadata") {
1694
- const value = args[++i];
1695
- metadataSource = value?.trim() ? Number(value) : NaN;
1696
- if (!Number.isSafeInteger(metadataSource)) {
1697
- await writeBytes(context.stderr, encodeUtf8("ffmpeg: invalid metadata input index\n"), context.signal);
1698
- return { exitCode: 1 };
1699
- }
1700
- } else if (arg === "-start_number") {
1701
- const val = Number(args[++i]);
1702
- if (!Number.isSafeInteger(val) || val < 0) {
1703
- await writeBytes(context.stderr, encodeUtf8("ffmpeg: invalid start_number\n"), context.signal);
1704
- return { exitCode: 1 };
1705
- }
1706
- if (args.slice(i + 1).includes("-i")) pendingStartNumber = val;
1707
- else outputStartNumber = val;
1708
- } else if (arg === "-ar") {
1709
- audioRate = parseInt(args[++i] ?? "0", 10) || void 0;
1710
- } else if (arg === "-ac") {
1711
- audioChannels = parseInt(args[++i] ?? "0", 10) || void 0;
1712
- } else if (arg === "-hide_banner" || arg === "-nostdin" || arg === "-stats") {
1713
- } else if (arg === "-f") {
1714
- const val = args[++i];
1715
- if (inputs.length === 0 || i + 1 < args.length && args.slice(i + 1).includes("-i")) {
1716
- pendingFormat = val;
1717
- } else {
1718
- outputFormat = val;
1719
- }
1720
- } else if (arg === "-ss") {
1721
- const val = parseFfmpegTimestamp(args[++i] ?? "0");
1722
- if (args.slice(i + 1).includes("-i")) {
1723
- pendingSs = val;
1724
- } else {
1725
- outputSs = val;
1726
- }
1727
- } else if (arg === "-to") {
1728
- const val = parseFfmpegTimestamp(args[++i] ?? "0");
1729
- if (args.slice(i + 1).includes("-i")) {
1730
- pendingTo = val;
1731
- } else {
1732
- outputTo = val;
1733
- }
1734
- } else if (arg === "-t") {
1735
- const val = parseFfmpegTimestamp(args[++i] ?? "0");
1736
- if (args.slice(i + 1).includes("-i")) {
1737
- pendingDuration = val;
1738
- } else {
1739
- outputDuration = val;
1740
- }
1741
- } else if (arg === "-stream_loop") {
1742
- pendingLoop = parseInt(args[++i] ?? "0", 10) || 0;
1743
- } else if (arg === "-r" || arg === "-framerate") {
1744
- const val = parseFloat(args[++i] ?? "25") || 25;
1745
- if (args.slice(i + 1).includes("-i")) {
1746
- pendingFps = val;
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
- outputFps = val;
1749
- }
1750
- } else if (arg === "-s") {
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
- } else if (arg === "-map") {
1800
- const m = args[++i];
1801
- if (m) maps.push(m);
1802
- } else if (arg === "-vf" || arg === "-filter:v") {
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
- if (inputs.length === 0) {
1830
- await writeBytes(
1831
- context.stderr,
1832
- encodeUtf8("ffmpeg: must specify at least one input (-i)\n"),
1833
- context.signal
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
- const loadSingleDocument = async (filePath, explicitFormat, fpsHint, input) => {
1867
- if (explicitFormat === "lavfi") {
1868
- if (features.lavfiSources === false) {
1869
- throw new Error("lavfi synthetic sources are disabled by consumer feature configuration");
1870
- }
1871
- 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));
1872
- let step = steps.next();
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
- if (filePath.startsWith("concat:")) {
1881
- const parts = filePath.slice("concat:".length).split("|").map((x) => x.trim()).filter(Boolean);
1882
- budget.checkConcatInputs(parts.length);
1883
- const docs = [];
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
- return concatMp4(docs, { limits: options.limits, budget });
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
- imgBytes = await readInput(resolved);
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
- const decoded = decodeImage(imgBytes);
1911
- detachBytesBuffer(imgBytes);
1912
- if (decoded.width * decoded.height >= 16384) {
1913
- const gc = globalThis.gc;
1914
- if (typeof gc === "function") {
1915
- gc();
1916
- gc();
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
- budget.recordFrame(decoded.width, decoded.height);
1920
- frames.push({
1921
- width: decoded.width,
1922
- height: decoded.height,
1923
- data: decoded.data,
1924
- ptsSeconds: frames.length / fps,
1925
- durationSeconds: 1 / fps,
1926
- keyframe: true
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
- if (frames.length === 0) {
1931
- throw new Error(`${filePath}: No matching image sequence files found`);
1932
- }
1933
- const first = frames[0];
1934
- const synthBytes = (await yieldTurn5(context.signal), context.signal?.throwIfAborted(), createSyntheticMp4({
1935
- width: 16,
1936
- height: 16,
1937
- fps,
1938
- frameCount: 1,
1939
- includeAudio: false
1940
- }));
1941
- const base = parseMp4(synthBytes);
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
- const resolvedItem = resolvePath(listDir, target);
1968
- currentInpoint = void 0;
1969
- currentOutpoint = void 0;
1970
- currentDuration = void 0;
1971
- while (l + 1 < lines.length) {
1972
- const nextLine = lines[l + 1].trim();
1973
- if (nextLine.startsWith("inpoint ")) {
1974
- currentInpoint = parseFfmpegTimestamp(nextLine.slice(8));
1975
- l++;
1976
- } else if (nextLine.startsWith("outpoint ")) {
1977
- currentOutpoint = parseFfmpegTimestamp(nextLine.slice(9));
1978
- l++;
1979
- } else if (nextLine.startsWith("duration ")) {
1980
- currentDuration = parseFfmpegTimestamp(nextLine.slice(9));
1981
- l++;
1982
- } else {
1983
- break;
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
- let itemDoc = await loadSingleDocument(resolvedItem);
1987
- if (currentInpoint !== void 0 || currentOutpoint !== void 0 || currentDuration !== void 0) {
1988
- itemDoc = sliceMp4(itemDoc, {
1989
- startSeconds: currentInpoint,
1990
- endSeconds: currentOutpoint,
1991
- durationSeconds: currentDuration
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
- budget.checkConcatInputs(concatDocs.length);
1998
- return concatMp4(concatDocs, { limits: options.limits, budget });
1999
- }
2000
- const plugin = registry.detect(rawBytes, filePath, explicitFormat);
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 (/concat=[^;[]*\bv=0\b/.test(filterComplex)) {
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.filter((t) => t.type !== "video")
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
- } else if (filterComplex.includes("xfade") && loadedDocs.length >= 2) {
2052
- const track0 = loadedDocs[0].tracks.find((t) => t.type === "video");
2053
- const track1 = loadedDocs[1].tracks.find((t) => t.type === "video");
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 timebase0 = inputs[0].format === "lavfi" ? f0[0].durationSeconds : 1 / track0.timescale;
2071
- const timebase1 = inputs[1].format === "lavfi" ? f1[0].durationSeconds : 1 / track1.timescale;
2072
- if (timebase0 !== timebase1) {
2073
- throw new Error(`xfade input timebase ${timebase0} does not match ${timebase1}`);
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
- const xfadeSpec = filterComplex.slice(filterComplex.indexOf("xfade") + "xfade".length).split("[")[0].split(",")[0].split(";")[0].trim();
2077
- const positional = (xfadeSpec.startsWith("=") ? xfadeSpec.slice(1) : "").split(":");
2078
- const named = {};
2079
- for (const part of positional) {
2080
- const eqIdx = part.indexOf("=");
2081
- if (eqIdx >= 0) named[part.slice(0, eqIdx).trim()] = part.slice(eqIdx + 1).trim();
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
- const transition = named.transition ?? (positional[0].includes("=") ? "fade" : positional[0] || "fade");
2084
- if (transition !== "fade" && transition !== "wipeleft") {
2085
- throw new Error(`Unsupported xfade transition: ${transition}`);
2537
+ if (audioCodec === "copy" && maxAudioFrames !== void 0) {
2538
+ workingDoc = limitAudioCopyPackets(workingDoc, maxAudioFrames);
2086
2539
  }
2087
- const fadeDur = parseFloat(named.duration ?? "0.5") || 0.5;
2088
- const fps = f0[0]?.durationSeconds ? Math.round(1 / f0[0].durationSeconds) : 25;
2089
- const overlapFrames = Math.max(1, Math.min(f0.length, f1.length, Math.round(fadeDur * fps)));
2090
- const offsetSec = named.offset !== void 0 ? parseFloat(named.offset) : Math.max(0, (f0.length - overlapFrames) / fps);
2091
- const preCount = Math.max(0, Math.min(f0.length - overlapFrames, Math.round(offsetSec * fps)));
2092
- const outFrames = [];
2093
- let cursor = 0;
2094
- for (let i = 0; i < preCount; i++) {
2095
- const f = f0[i];
2096
- outFrames.push({ ...f, ptsSeconds: cursor });
2097
- cursor += f.durationSeconds;
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
- for (let k = 0; k < overlapFrames; k++) {
2100
- const a = f0[Math.min(f0.length - 1, preCount + k)];
2101
- const b = f1[Math.min(f1.length - 1, k)];
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
- outFrames.push({
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
- for (let j = overlapFrames; j < f1.length; j++) {
2122
- const b = f1[j];
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
- workingDoc = {
2127
- ...loadedDocs[0],
2128
- durationSeconds: cursor,
2129
- tracks: loadedDocs[0].tracks.map(
2130
- (t) => t.type === "video" ? { ...t, samples: [], decodedVideoFrames: outFrames } : t
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
- ...loadedDocs[0],
2182
- tracks: loadedDocs[0].tracks.map(
2183
- (t) => t.type === "video" ? {
2184
- ...t,
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
- } else if (filterComplex.includes("overlay") && loadedDocs.length >= 2) {
2193
- const f0 = await ensureDecodedFrames(
2194
- loadedDocs[0].tracks.find((t) => t.type === "video"),
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
- const xExpression = named.x ?? (positional[0] && !positional[0].includes("=") ? positional[0] : "0");
2213
- const yExpression = named.y ?? (positional[1] && !positional[1].includes("=") ? positional[1] : "0");
2214
- const format = named.format ?? "yuv420";
2215
- const horizontalGrid = format.startsWith("yuv420") || format.startsWith("yuv422") ? 2 : 1;
2216
- const verticalGrid = format.startsWith("yuv420") ? 2 : 1;
2217
- const overlaid = f0.map((a, idx) => {
2218
- const b = f1[Math.min(idx, f1.length - 1)];
2219
- if (!b) return a;
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
- return { ...a, data: outData };
2250
- });
2251
- workingDoc = {
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
- workingDoc = await muxMp4Cooperatively(
2305
- [{ ...loadedDocs[0], tracks: filteredTracks }],
2306
- { stripAudio, stripVideo, stripSubtitles, shortest, rotation },
2307
- context.signal
2308
- );
2309
- } else {
2310
- workingDoc = await muxMp4Cooperatively(selectAutomaticAudio(loadedDocs), {
2311
- stripAudio,
2312
- stripVideo,
2313
- stripSubtitles,
2314
- shortest,
2315
- rotation
2316
- }, context.signal);
2317
- }
2318
- if (outputSs !== void 0 || outputTo !== void 0 || outputDuration !== void 0) {
2319
- workingDoc = sliceMp4(workingDoc, {
2320
- startSeconds: outputSs,
2321
- endSeconds: outputDuration === void 0 ? outputTo : void 0,
2322
- durationSeconds: outputDuration
2323
- });
2324
- }
2325
- if (outputSize && videoCodec !== "copy") {
2326
- const [w, h] = outputSize.toLowerCase().split("x");
2327
- if (w && h) vfFilters.push(`scale=${w}:${h}`);
2328
- }
2329
- if (outputFps && videoCodec !== "copy") {
2330
- vfFilters.push(`fps=${outputFps}`);
2331
- }
2332
- const imageOutput = (outputFormat ? registry.findByFormatName(outputFormat) : registry.findByFilename(outputTarget))?.id === "image2";
2333
- if (imageOutput || maxVideoFrames !== void 0 && videoCodec !== "copy") {
2334
- workingDoc = {
2335
- ...workingDoc,
2336
- tracks: await mapWork(workingDoc.tracks, async (track) => {
2337
- if (track.type !== "video") return track;
2338
- const limit = vfFilters.length === 0 ? maxVideoFrames : void 0;
2339
- const frames = await ensureDecodedFrames(track, workingDoc.timescale, budget, context.signal, limit);
2340
- return { ...track, samples: [], decodedVideoFrames: frames };
2341
- }, context.signal)
2342
- };
2343
- }
2344
- if (vfFilters.length > 0) {
2345
- if (features.videoTranscode === false || features.filterGraph === false) {
2346
- throw new Error("Video transcoding / filtergraph is disabled by consumer feature configuration");
2347
- }
2348
- const combinedChain = vfFilters.join(",");
2349
- let loadedSubtitleCues;
2350
- const subMatch = /subtitles=(?:filename=)?['"]?([^:'",]+)['"]?/.exec(combinedChain);
2351
- if (subMatch?.[1]) {
2352
- const subPath = resolvePath(context.cwd, subMatch[1]);
2353
- const subBytes = await readInput(subPath);
2354
- const subFmt = subPath.toLowerCase().endsWith(".vtt") ? "webvtt" : "srt";
2355
- const subDoc = parseSubtitleDocument(subBytes, subFmt);
2356
- const st = subDoc.tracks.find((t) => t.type === "subtitle");
2357
- if (st) {
2358
- const sts = st.timescale || 1e3;
2359
- loadedSubtitleCues = st.samples.map((s) => ({
2360
- startSec: s.pts / sts,
2361
- endSec: (s.pts + s.duration) / sts,
2362
- text: decodeUtf8(s.data).trim()
2363
- }));
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 offsetTicks = Math.round(presentationOffset * t.timescale);
2379
- const decoded = await ensureDecodedFrames(t, workingDoc.timescale, budget, context.signal);
2380
- const displayed = decoded.map((frame) => ({ ...frame, ptsSeconds: (Math.round(frame.ptsSeconds * t.timescale) - offsetTicks) / t.timescale }));
2381
- const filtered = await applyVideoFilterChain(displayed, combinedChain, budget, loadedSubtitleCues, context.signal);
2382
- const first = filtered[0];
2383
- return {
2384
- ...t,
2385
- width: first?.width ?? t.width,
2386
- height: first?.height ?? t.height,
2387
- samples: [],
2388
- decodedVideoFrames: filtered.map((frame) => ({ ...frame, ptsSeconds: (Math.round(frame.ptsSeconds * t.timescale) + offsetTicks) / t.timescale }))
2389
- };
2390
- }, context.signal)
2391
- };
2392
- }
2393
- if (maxVideoFrames !== void 0 && !outputTarget.includes("%")) {
2394
- workingDoc = {
2395
- ...workingDoc,
2396
- tracks: workingDoc.tracks.map((t) => {
2397
- if (t.type !== "video") return t;
2398
- if (t.decodedVideoFrames && t.decodedVideoFrames.length > maxVideoFrames) {
2399
- return {
2400
- ...t,
2401
- samples: [],
2402
- decodedVideoFrames: t.decodedVideoFrames.slice(0, maxVideoFrames)
2403
- };
2404
- }
2405
- if (t.samples.length > maxVideoFrames) {
2406
- const slicedSamples = t.samples.slice(0, maxVideoFrames);
2407
- const dur = slicedSamples.reduce((acc, s) => acc + s.duration, 0);
2408
- return {
2409
- ...t,
2410
- duration: dur,
2411
- samples: slicedSamples
2412
- };
2413
- }
2414
- return t;
2415
- })
2416
- };
2417
- }
2418
- if ((audioRate !== void 0 || audioChannels !== void 0 || afFilters.length > 0) && audioCodec !== "copy") {
2419
- workingDoc = {
2420
- ...workingDoc,
2421
- tracks: await mapWork(workingDoc.tracks, async (t) => {
2422
- let work = 0;
2423
- if (t.type !== "audio") return t;
2424
- const audio = t.decodedAudio ?? (t.codecDescriptions[0]?.codecName === "aac" ? decodeAacTrack(workingDoc, t) : void 0);
2425
- if (!audio) throw new Error("Audio filtering requires decoded PCM audio");
2426
- const origRate = audio.sampleRate;
2427
- const origCh = audio.channels;
2428
- const targetRate = audioRate ?? origRate;
2429
- const targetCh = audioChannels ?? origCh;
2430
- const channelMask = audio.channelMask;
2431
- const surroundMono = origCh === 6 && targetCh === 1;
2432
- const monoStereo = origCh === 1 && (targetCh === 2 || targetCh === 3);
2433
- if (targetCh !== origCh && origCh > 2 && (!surroundMono || channelMask && channelMask !== 63 && channelMask !== 1551)) {
2434
- throw new Error(`Unsupported audio channel layout conversion: ${origCh} channels (mask ${channelMask ?? 0}) to ${targetCh}`);
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
- const targetSamples = Math.max(0, Math.round(durSec / tempoFactor * targetRate));
2472
- let stretched = await stretchAudio(audio.channelData, origRate, tempoFactor, budget, context.signal);
2473
- if (targetRate !== origRate) stretched = await resampleAudio(stretched, targetSamples, origRate / targetRate, budget, context.signal);
2474
- const newChannelData = [];
2475
- for (let c = 0; c < targetCh; c++) {
2476
- if (++work % 1024 === 0) {
2477
- await yieldTurn5(context.signal);
2478
- context.signal?.throwIfAborted();
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 dst = new Float32Array(targetSamples);
2481
- const srcCh = monoStereo && c >= 2 ? void 0 : stretched[origCh === 1 ? 0 : c];
2482
- const rightCh = origCh === 2 && targetCh === 1 ? stretched[1] : void 0;
2483
- if (srcCh && srcCh.length > 0) {
2484
- for (let i = 0; i < targetSamples; i++) {
2485
- if (++work % 1024 === 0) {
2486
- await yieldTurn5(context.signal);
2487
- context.signal?.throwIfAborted();
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
- const nextDescs = t.codecDescriptions.map((d) => ({
2498
- ...d,
2499
- sampleRate: targetRate,
2500
- channels: targetCh
2501
- }));
2502
- return {
2503
- ...t,
2504
- timescale: targetRate,
2505
- duration: targetSamples,
2506
- volume: volFactor,
2507
- codecDescriptions: nextDescs,
2508
- samples: [],
2509
- decodedAudio: {
2510
- sampleRate: targetRate,
2511
- channels: targetCh,
2512
- channelData: newChannelData,
2513
- ...targetCh === origCh ? { channelMask } : {}
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
- } else {
2586
- for (let seconds = segDur; seconds < totalSec - 1e-6; seconds += segDur) boundaries.push(seconds);
2587
- }
2588
- boundaries.push(totalSec);
2589
- let targetDuration = 0;
2590
- for (let i = 1; i < boundaries.length; i++) targetDuration = Math.max(targetDuration, boundaries[i] - boundaries[i - 1]);
2591
- const firstSegment = hlsListSize === 0 ? 0 : Math.max(0, boundaries.length - 1 - hlsListSize);
2592
- const independentSegments = hlsIndependentSegments && !!vTrk && workingDoc.tracks.filter((track) => track.type === "video").every((track) => boundaries.slice(firstSegment, -1).every((start, index) => {
2593
- const first = track.samples.find((sample) => sample.dts >= start * track.timescale);
2594
- return first?.isKeyframe && first.dts < boundaries[firstSegment + index + 1] * track.timescale;
2595
- }));
2596
- const playlistLines = [
2597
- "#EXTM3U",
2598
- isFmp4 ? "#EXT-X-VERSION:7" : independentSegments ? "#EXT-X-VERSION:6" : "#EXT-X-VERSION:3",
2599
- `#EXT-X-TARGETDURATION:${Math.ceil(targetDuration)}`,
2600
- `#EXT-X-MEDIA-SEQUENCE:${(outputStartNumber ?? 0) + firstSegment}`
2601
- ];
2602
- if (independentSegments) playlistLines.push("#EXT-X-INDEPENDENT-SEGMENTS");
2603
- if (isFmp4) playlistLines.push('#EXT-X-MAP:URI="init.mp4"');
2604
- const retainedSegments = [];
2605
- for (let sIdx = 0; sIdx < boundaries.length - 1; sIdx++) {
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
- if (isFmp4) {
2622
- const moof = parseMp4Boxes(segBytes, 0, 0, budget).find((box) => box.type === "moof");
2623
- if (!moof) throw new Error("HLS fMP4 output requires fragmented MP4 samples");
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
- segBytes = segBytes.subarray(moof.offset);
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
- if (!hlsOmitEndlist) playlistLines.push("#EXT-X-ENDLIST");
2662
- playlistLines.push("");
2663
- const m3u8Bytes = encodeUtf8(playlistLines.join("\n"));
2664
- await context.fs.mkdir(outDir, { recursive: true, signal: context.signal });
2665
- await writeFileOutput(context, m3u8Bytes, (data) => context.fs.writeFile(outResolved, data, { signal: context.signal }));
2666
- options.onMetrics?.(budget.getStats());
2667
- return { exitCode: 0 };
2668
- }
2669
- if (imageOutput && !workingDoc.tracks.some((track) => track.type === "video" && track.decodedVideoFrames?.length)) {
2670
- throw new Error("No video frame in the requested presentation interval");
2671
- }
2672
- if (/%0?\d*d/.test(outputTarget)) {
2673
- const outExt2 = outputTarget.split(".").pop()?.toLowerCase() ?? "png";
2674
- const imgPlugin = registry.findByFilename(`x.${outExt2}`);
2675
- if (!imgPlugin || !imgPlugin.canMux) {
2676
- throw new Error(`Unsupported output format for '${outputTarget}' (AST not registered)`);
2677
- }
2678
- const vTrack = workingDoc.tracks.find((t) => t.type === "video");
2679
- const frames = vTrack ? await ensureDecodedFrames(vTrack, workingDoc.timescale, budget, context.signal) : [];
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
- for (const trk of workingDoc.tracks) {
2739
- if (trk.decodedVideoFrames) {
2740
- for (const f of trk.decodedVideoFrames) detachBytesBuffer(f.data);
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
  }