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