@poe-platform/safe-bash 0.1.844 → 0.1.846

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.
Files changed (43) hide show
  1. package/dist/image-ast/ast.d.ts +12 -2
  2. package/dist/image-ast/browser/ast.d.ts +12 -2
  3. package/dist/image-ast/browser/codecs/exif-metadata.d.ts +2 -0
  4. package/dist/image-ast/browser/codecs/exif.d.ts +2 -0
  5. package/dist/image-ast/browser/codecs/gif-metadata-storage.d.ts +1 -0
  6. package/dist/image-ast/browser/codecs/gif-metadata.d.ts +1 -0
  7. package/dist/image-ast/browser/codecs/gif-output-parts.d.ts +1 -1
  8. package/dist/image-ast/browser/codecs/jpeg.d.ts +1 -1
  9. package/dist/image-ast/browser/codecs/netpbm-storage.d.ts +2 -2
  10. package/dist/image-ast/browser/codecs/netpbm.d.ts +4 -4
  11. package/dist/image-ast/browser/codecs/png-storage.d.ts +9 -1
  12. package/dist/image-ast/browser/codecs/png.d.ts +1 -0
  13. package/dist/image-ast/browser/codecs/tiff-layout.d.ts +3 -2
  14. package/dist/image-ast/codecs/exif-metadata.d.ts +2 -0
  15. package/dist/image-ast/codecs/exif.d.ts +2 -0
  16. package/dist/image-ast/codecs/gif-metadata-storage.d.ts +1 -0
  17. package/dist/image-ast/codecs/gif-metadata.d.ts +1 -0
  18. package/dist/image-ast/codecs/gif-output-parts.d.ts +1 -1
  19. package/dist/image-ast/codecs/jpeg.d.ts +1 -1
  20. package/dist/image-ast/codecs/netpbm-storage.d.ts +2 -2
  21. package/dist/image-ast/codecs/netpbm.d.ts +4 -4
  22. package/dist/image-ast/codecs/png-storage.d.ts +9 -1
  23. package/dist/image-ast/codecs/png.d.ts +1 -0
  24. package/dist/image-ast/codecs/tiff-layout.d.ts +3 -2
  25. package/dist/image-ast/index.js +554 -163
  26. package/dist/mp4-ast/types.d.ts +2 -0
  27. package/dist/safe-bash/command-chunks/{chunk-LORSYIAF.js → chunk-CSJHADNR.js} +31 -7
  28. package/dist/safe-bash/command-chunks/{chunk-KSCVKF7I.js → chunk-F42YLXD4.js} +3 -3
  29. package/dist/safe-bash/command-chunks/{chunk-2EEIB5O3.js → chunk-HUE2ATJY.js} +3 -3
  30. package/dist/safe-bash/command-chunks/{chunk-3SWIXYXT.js → chunk-NBQWCUOP.js} +1 -1
  31. package/dist/safe-bash/command-chunks/{chunk-U5TQVW64.js → chunk-ZTEK2JJD.js} +1 -1
  32. package/dist/safe-bash-command-ffmpeg/index.js +535 -88
  33. package/dist/safe-bash-command-ffprobe/index.js +2 -2
  34. package/dist/safe-bash-command-ffprobe/media.js +2 -2
  35. package/dist/safe-bash-command-imagemagick/compare-file.d.ts +3 -2
  36. package/dist/safe-bash-command-imagemagick/index.d.ts +1 -0
  37. package/dist/safe-bash-command-imagemagick/index.js +895 -276
  38. package/dist/safe-bash-command-imagemagick/raw-output.d.ts +10 -0
  39. package/dist/safe-bash-command-python/executor.js +3 -3
  40. package/dist/safe-bash-command-python/index.js +3 -3
  41. package/dist/safe-bash-command-python/jspi.js +2 -2
  42. package/dist/safe-bash-command-python/worker.js +1 -1
  43. package/package.json +2 -2
@@ -23,7 +23,7 @@ import {
23
23
  sliceMp4,
24
24
  srtAst,
25
25
  webvttAst
26
- } from "../safe-bash/command-chunks/chunk-LORSYIAF.js";
26
+ } from "../safe-bash/command-chunks/chunk-CSJHADNR.js";
27
27
  import "../safe-bash/command-chunks/chunk-65DS4IOB.js";
28
28
 
29
29
  // packages/safe-bash-command-ffmpeg/src/index.ts
@@ -78,6 +78,73 @@ async function resampleAudio(channels, outputLength, rateRatio, budget, signal)
78
78
 
79
79
  // packages/safe-bash-command-ffmpeg/src/audio-tempo.ts
80
80
  import { yieldTurn as yieldTurn2 } from "../safe-bash-contracts/yield.js";
81
+ async function filterUnityTempo(channels, sampleRate, pcm16, budget, signal) {
82
+ const length = channels[0]?.length ?? 0;
83
+ const window = 2 ** Math.ceil(Math.log2(Math.max(2, Math.floor(sampleRate / 24))));
84
+ const hop = window / 2;
85
+ if (length < hop) return channels.map(() => new Float32Array(0));
86
+ const hann = Float32Array.from({ length: window }, (_, i) => (1 - Math.cos(2 * Math.PI * i / (window - 1))) / 2);
87
+ const mono = new Float32Array(length);
88
+ for (let i = 0; i < length; i++) {
89
+ if (i % 16384 === 0) {
90
+ await yieldTurn2(signal);
91
+ signal?.throwIfAborted();
92
+ budget.checkCpu();
93
+ }
94
+ for (const channel of channels) {
95
+ if (Math.abs(channel[i]) > Math.abs(mono[i])) mono[i] = channel[i];
96
+ }
97
+ }
98
+ const output = channels.map(() => []);
99
+ let previous = -hop;
100
+ let position = -hop;
101
+ let work = 0;
102
+ while (true) {
103
+ let current = previous + hop;
104
+ position += hop;
105
+ const drift = position - hop - previous;
106
+ const start = Math.min(window, Math.max(0, -drift));
107
+ const end = Math.max(0, Math.min(window - drift, window - window / 16));
108
+ let correction = -drift;
109
+ let best = -Infinity;
110
+ for (let lag = start; lag < end; lag++) {
111
+ let correlation = 0;
112
+ for (let j = Math.max(0, -current); j < Math.min(window - lag, length - current, length - previous - lag); j++) {
113
+ correlation += (mono[previous + j + lag] ?? 0) * mono[current + j];
114
+ if (++work % 16384 === 0) {
115
+ await yieldTurn2(signal);
116
+ signal?.throwIfAborted();
117
+ budget.checkCpu();
118
+ }
119
+ }
120
+ const metric = correlation * (drift + lag) * (lag - start) * (end - lag);
121
+ if (metric > best) {
122
+ best = metric;
123
+ correction = lag - hop;
124
+ }
125
+ }
126
+ current -= correction;
127
+ const count = Math.max(0, Math.min(window, length - current));
128
+ const overlap = Math.min(hop, count, length - previous - hop);
129
+ for (let c = 0; c < channels.length; c++) {
130
+ const channel = channels[c];
131
+ const dst = output[c];
132
+ for (let i = 0; i < overlap; i++) {
133
+ const a = channel[previous + hop + i] ?? 0;
134
+ const b = channel[current + i] ?? 0;
135
+ const sample = current + i < 0 ? a : Math.fround(a * hann[hop + i] + Math.fround(b * hann[i]));
136
+ dst.push(pcm16 ? Math.trunc(sample * 32768) / 32768 : sample);
137
+ }
138
+ if (current + count >= length) {
139
+ for (let i = overlap; i < count; i++) dst.push(channel[current + i] ?? 0);
140
+ }
141
+ }
142
+ budget.checkDuration((output[0]?.length ?? 0) / sampleRate);
143
+ if (current + count >= length) break;
144
+ previous = current;
145
+ }
146
+ return output.map((channel) => Float32Array.from(channel));
147
+ }
81
148
  async function stretchAudio(channels, sampleRate, tempo, budget, signal) {
82
149
  const length = channels[0]?.length ?? 0;
83
150
  if (tempo === 1 || length === 0) return channels;
@@ -275,6 +342,26 @@ import {
275
342
  resizeImage,
276
343
  rotateImage
277
344
  } from "../image-ast/portable.js";
345
+ function parseFrameRate(value) {
346
+ switch (value) {
347
+ case "ntsc":
348
+ case "qntsc":
349
+ case "sntsc":
350
+ return 3e4 / 1001;
351
+ case "pal":
352
+ case "qpal":
353
+ case "spal":
354
+ return 25;
355
+ case "film":
356
+ return 24;
357
+ case "ntsc-film":
358
+ return 24e3 / 1001;
359
+ }
360
+ const [numerator, denominator = "1", extra] = value.split("/");
361
+ if (extra !== void 0 || !numerator?.trim() || !denominator.trim()) return void 0;
362
+ const rate = Number(numerator) / Number(denominator);
363
+ return Number.isFinite(rate) && rate > 0 ? rate : void 0;
364
+ }
278
365
  function loopToOutputEnd(doc, endSeconds, budget) {
279
366
  if (endSeconds === void 0 || !Number.isFinite(endSeconds)) {
280
367
  throw new Error("-stream_loop -1 requires a finite output -t or -to");
@@ -532,6 +619,7 @@ function* lavfiSteps(spec, budget, durationOverride) {
532
619
  };
533
620
  }
534
621
  async function ensureDecodedFrames(track, movieTimescale, budget, signal, maxFrames) {
622
+ if (maxFrames === 0) return [];
535
623
  const width = track.width ?? track.codecDescriptions[0]?.width ?? 64;
536
624
  const height = track.height ?? track.codecDescriptions[0]?.height ?? 64;
537
625
  const ts = track.timescale || 9e4;
@@ -601,6 +689,9 @@ function parseFilterExpression(expression, filter, variables) {
601
689
  }
602
690
  if (name === "not" && args.length === 1) value = Number(args[0] === 0);
603
691
  else if (name === "eq" && args.length === 2) value = Number(args[0] === args[1]);
692
+ else if (name === "gte" && args.length === 2) value = Number(args[0] >= args[1]);
693
+ else if (name === "lt" && args.length === 2) value = Number(args[0] < args[1]);
694
+ else if (name === "if" && args.length === 3) value = args[0] !== 0 ? args[1] : args[2];
604
695
  else if (name === "gt" && args.length === 2) value = Number(args[0] > args[1]);
605
696
  else if (name === "mod" && args.length === 2) value = args[0] - Math.floor(args[0] / args[1]) * args[1];
606
697
  else if (name === "between" && args.length === 3) value = Number(args[0] >= args[1] && args[0] <= args[2]);
@@ -787,10 +878,7 @@ async function renderBitmapTextToRgba(rgba, width, height, text, opts = {}, sign
787
878
  }
788
879
  return out;
789
880
  }
790
- async function applyVideoFilterChain(frames, filterChainStr, budget, subtitleCues, signal) {
791
- let work = 0;
792
- let current = frames;
793
- if (current.length === 0) return current;
881
+ function parseFilterChain(filterChainStr) {
794
882
  const filters = [];
795
883
  let filter = "";
796
884
  let quoted = false;
@@ -808,7 +896,13 @@ async function applyVideoFilterChain(frames, filterChainStr, budget, subtitleCue
808
896
  }
809
897
  }
810
898
  if (filter.trim()) filters.push(filter.trim());
811
- for (const filterSpec of filters) {
899
+ return filters;
900
+ }
901
+ async function applyVideoFilterChain(frames, filterChainStr, budget, subtitleCues, signal) {
902
+ let work = 0;
903
+ let current = frames;
904
+ if (current.length === 0) return current;
905
+ for (const filterSpec of parseFilterChain(filterChainStr)) {
812
906
  if (++work % 1024 === 0) {
813
907
  await yieldTurn5(signal);
814
908
  signal?.throwIfAborted();
@@ -1397,7 +1491,7 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1397
1491
  if (arg === "-y" || arg === "-n") {
1398
1492
  if (noOverwrite !== void 0 && noOverwrite !== (arg === "-n")) return void 0;
1399
1493
  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++;
1494
+ } else if (arg === "-v" || arg === "-loglevel" || arg === "-safe" || arg === "-threads" || arg === "-pix_fmt" || arg === "-preset" || arg === "-crf" || arg === "-bf" || arg === "-b:v" || arg === "-b:a" || arg === "-bsf:v" || arg === "-bsf:a" || arg === "-tag:v" || arg === "-tag:a" || arg === "-vsync" || arg === "-fps_mode") i++;
1401
1495
  else if (arg === "-map_chapters") {
1402
1496
  const value = args[++i];
1403
1497
  chapterSource = value?.trim() ? Number(value) : NaN;
@@ -1411,8 +1505,10 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1411
1505
  if (!Number.isSafeInteger(val) || val < 0) return void 0;
1412
1506
  if (args.slice(i + 1).includes("-i")) pendingStartNumber = val;
1413
1507
  else outputStartNumber = val;
1414
- } else if (arg === "-ar") audioRate = parseInt(args[++i] ?? "0", 10) || void 0;
1415
- else if (arg === "-ac") audioChannels = parseInt(args[++i] ?? "0", 10) || void 0;
1508
+ } else if (arg === "-ar") {
1509
+ audioRate = parseInt(args[++i] ?? "0", 10) || void 0;
1510
+ if (audioRate !== void 0 && audioRate < 0) return void 0;
1511
+ } else if (arg === "-ac") audioChannels = parseInt(args[++i] ?? "0", 10) || void 0;
1416
1512
  else if (arg === "-hide_banner" || arg === "-nostdin" || arg === "-stats") continue;
1417
1513
  else if (arg === "-f") {
1418
1514
  const val = args[++i];
@@ -1433,7 +1529,8 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1433
1529
  else outputDuration = val;
1434
1530
  } else if (arg === "-stream_loop") pendingLoop = parseInt(args[++i] ?? "0", 10) || 0;
1435
1531
  else if (arg === "-r" || arg === "-framerate") {
1436
- const val = parseFloat(args[++i] ?? "25") || 25;
1532
+ const val = parseFrameRate(args[++i] ?? "");
1533
+ if (val === void 0) return void 0;
1437
1534
  if (args.slice(i + 1).includes("-i")) pendingFps = val;
1438
1535
  else outputFps = val;
1439
1536
  } else if (arg === "-s") outputSize = args[++i];
@@ -1471,7 +1568,7 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1471
1568
  else if (arg === "-vn") stripVideo = true;
1472
1569
  else if (arg === "-sn") stripSubtitles = true;
1473
1570
  else if (arg === "-shortest") shortest = true;
1474
- else if (arg === "-frames:v" || arg === "-vframes") maxVideoFrames = parseInt(args[++i] ?? "1", 10) || 1;
1571
+ else if (arg === "-frames:v" || arg === "-vframes") maxVideoFrames = parseInt(args[++i] ?? "1", 10);
1475
1572
  else if (arg === "-frames:a" || arg === "-aframes") {
1476
1573
  const value = args[++i];
1477
1574
  maxAudioFrames = value?.trim() ? Number(value) : NaN;
@@ -1483,7 +1580,7 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1483
1580
  } else if (arg === "-vf" || arg === "-filter:v") {
1484
1581
  const f = args[++i];
1485
1582
  if (f) vfFilters.push(f);
1486
- } else if (arg === "-af" || arg === "-filter:a") {
1583
+ } else if (arg === "-af" || arg === "-filter:a" || arg.startsWith("-af:") || arg.startsWith("-filter:a:")) {
1487
1584
  return void 0;
1488
1585
  } else if (arg === "-disposition" || arg.startsWith("-disposition:")) {
1489
1586
  streamDispositions.push([arg.slice("-disposition:".length), args[++i] ?? ""]);
@@ -1588,16 +1685,16 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1588
1685
  if (isNullMux) return "";
1589
1686
  if (inputs.some((inp) => isStdin(inp.path)) && !inBytes) return void 0;
1590
1687
  if (!writeFileSync || !outputTarget || outputTarget.endsWith("/") || outputTarget.endsWith("/.") || outputTarget.includes("/./") || /(?:^|\/)\.\.(?:\/|$)/u.test(outputTarget)) return void 0;
1591
- if (/%0?\d*d/.test(outputTarget)) {
1688
+ if (formatImageSequence(outputTarget, outputStartNumber ?? 1) !== void 0) {
1592
1689
  const outExt2 = outputTarget.split(".").pop()?.toLowerCase() ?? "png";
1593
1690
  const vTrack = workingDoc.tracks.find((t) => t.type === "video");
1594
1691
  if (vTrack && !vTrack.decodedVideoFrames) return void 0;
1595
1692
  const frames = vTrack?.decodedVideoFrames ?? [];
1596
- const limit = maxVideoFrames ? Math.min(frames.length, maxVideoFrames) : frames.length;
1693
+ const limit = Math.min(frames.length, maxVideoFrames ?? Infinity);
1597
1694
  const imgFmt = outExt2 === "jpg" || outExt2 === "jpeg" ? "jpeg" : outExt2 === "webp" ? "webp" : outExt2 === "gif" ? "gif" : outExt2 === "bmp" ? "bmp" : outExt2 === "ppm" ? "ppm" : "png";
1598
1695
  for (let idx = 0; idx < limit; idx++) {
1599
1696
  const f = frames[idx];
1600
- const fileName = outputTarget.replace(/%0?(\d*)d/, (_, widthDigits) => String(idx + (outputStartNumber ?? 1)).padStart(parseInt(widthDigits || "0", 10) || 0, "0"));
1697
+ const fileName = formatImageSequence(outputTarget, idx + (outputStartNumber ?? 1));
1601
1698
  const encoded = encodeImage(makeRgbaImg(f.width, f.height, f.data), { format: imgFmt }).data;
1602
1699
  if (!writeFileSync(fileName, encoded)) return void 0;
1603
1700
  }
@@ -1605,6 +1702,7 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1605
1702
  }
1606
1703
  const outPlugin = outputFormat ? registry.findByFormatName(outputFormat) : registry.findByFilename(outputTarget);
1607
1704
  if (!outPlugin || !outPlugin.canMux || outPlugin.id === "dash" || outPlugin.id === "hls") return void 0;
1705
+ if (outPlugin.id === "image2" && maxVideoFrames === 0) return "";
1608
1706
  const outExt = outputTarget.split(".").pop()?.toLowerCase();
1609
1707
  const serializedBytes = outPlugin.serialize(workingDoc, { audioCodec, format: outputFormat ?? outExt, faststart, fragmented, metadata: metaTags, budget });
1610
1708
  if (isStdout(outputTarget)) return void 0;
@@ -1614,6 +1712,81 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1614
1712
  return void 0;
1615
1713
  }
1616
1714
  }
1715
+ function formatImageSequence(pattern, number) {
1716
+ let filename = "";
1717
+ let numbered = false;
1718
+ for (let i = 0; i < pattern.length; i++) {
1719
+ if (pattern[i] !== "%") {
1720
+ filename += pattern[i];
1721
+ continue;
1722
+ }
1723
+ i++;
1724
+ if (pattern[i] === "%") {
1725
+ filename += "%";
1726
+ continue;
1727
+ }
1728
+ let width = 0;
1729
+ while (i < pattern.length && pattern[i] >= "0" && pattern[i] <= "9") {
1730
+ width = width * 10 + Number(pattern[i]);
1731
+ i++;
1732
+ }
1733
+ if (pattern[i] !== "d") return void 0;
1734
+ filename += String(number).padStart(width, "0");
1735
+ numbered = true;
1736
+ }
1737
+ return numbered ? filename : void 0;
1738
+ }
1739
+ function matchesImageGlob(pattern, name, budget) {
1740
+ const characters = Array.from(name);
1741
+ let positions = /* @__PURE__ */ new Set([0]);
1742
+ for (let i = 0; i < pattern.length; i++) {
1743
+ budget.checkCpu();
1744
+ const token = pattern[i];
1745
+ const next = /* @__PURE__ */ new Set();
1746
+ if (token === "*") {
1747
+ let start = characters.length;
1748
+ for (const p of positions) start = Math.min(start, p);
1749
+ for (let p = start; p <= characters.length; p++) next.add(p);
1750
+ } else {
1751
+ let literal = token;
1752
+ let contents;
1753
+ if (token === "\\") literal = pattern[++i] ?? "\\";
1754
+ if (token === "[") {
1755
+ let end = i + 1;
1756
+ if (pattern[end] === "!" || pattern[end] === "^") end++;
1757
+ if (pattern[end] === "]") end++;
1758
+ while (end < pattern.length && pattern[end] !== "]") end++;
1759
+ if (end < pattern.length) {
1760
+ contents = pattern.slice(i + 1, end);
1761
+ i = end;
1762
+ }
1763
+ }
1764
+ for (const p of positions) {
1765
+ budget.checkCpu();
1766
+ const character = characters[p];
1767
+ if (character === void 0) continue;
1768
+ let matches = token === "?" || character === literal;
1769
+ if (contents !== void 0) {
1770
+ const negated = contents[0] === "!" || contents[0] === "^";
1771
+ let included = false;
1772
+ for (let c = negated ? 1 : 0; c < contents.length; c++) {
1773
+ let start = contents[c];
1774
+ if (start === "\\" && c + 1 < contents.length) start = contents[++c];
1775
+ if (contents[c + 1] === "-" && c + 2 < contents.length) {
1776
+ included ||= character >= start && character <= contents[c + 2];
1777
+ c += 2;
1778
+ } else included ||= character === start;
1779
+ }
1780
+ matches = negated ? !included : included;
1781
+ }
1782
+ if (matches) next.add(p + 1);
1783
+ }
1784
+ }
1785
+ positions = next;
1786
+ if (positions.size === 0) return false;
1787
+ }
1788
+ return positions.has(characters.length);
1789
+ }
1617
1790
  function createFfmpegCommand(options = {}) {
1618
1791
  const astPlugins = options.asts ?? allMediaAsts();
1619
1792
  const registry = createMediaAstRegistry(astPlugins);
@@ -1647,14 +1820,20 @@ function createFfmpegCommand(options = {}) {
1647
1820
  }
1648
1821
  const inputs = [];
1649
1822
  let pendingFormat;
1823
+ let pendingSafe;
1650
1824
  let pendingAudioDecoder;
1651
1825
  let pendingSs;
1652
1826
  let pendingTo;
1653
1827
  let pendingDuration;
1654
1828
  let pendingLoop;
1829
+ let pendingImageLoop;
1655
1830
  let pendingFps;
1656
1831
  let pendingStartNumber;
1832
+ let pendingStartNumberRange;
1833
+ let pendingPatternType;
1657
1834
  let overwrite;
1835
+ let exitOnError = false;
1836
+ let inputReadFailed = false;
1658
1837
  let filterComplex;
1659
1838
  const outputs = [];
1660
1839
  const inputBytes = /* @__PURE__ */ new Map();
@@ -1670,6 +1849,8 @@ function createFfmpegCommand(options = {}) {
1670
1849
  let i = 0;
1671
1850
  while (i < args.length) {
1672
1851
  let outputStartNumber;
1852
+ let updateImage = false;
1853
+ let framePts = false;
1673
1854
  let outputFormat;
1674
1855
  let outputSs;
1675
1856
  let outputTo;
@@ -1715,8 +1896,19 @@ function createFfmpegCommand(options = {}) {
1715
1896
  return { exitCode: 234 };
1716
1897
  }
1717
1898
  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") {
1899
+ } else if (arg === "-xerror") {
1900
+ exitOnError = true;
1901
+ } else if (arg === "-safe") {
1902
+ const value = args[++i];
1903
+ if (value !== "0" && value !== "1") {
1904
+ await writeBytes(context.stderr, encodeUtf8("ffmpeg: -safe must be 0 or 1\n"), context.signal);
1905
+ return { exitCode: 1 };
1906
+ }
1907
+ pendingSafe = value === "1";
1908
+ } else if (arg === "-v" || arg === "-loglevel" || arg === "-threads" || arg === "-pix_fmt" || arg === "-preset" || arg === "-crf" || arg === "-bf" || arg === "-b:v" || arg === "-b:a" || arg === "-bsf:v" || arg === "-bsf:a" || arg === "-tag:v" || arg === "-tag:a") {
1719
1909
  i++;
1910
+ } else if (arg === "-update") {
1911
+ updateImage = args[++i] === "1";
1720
1912
  } else if (arg === "-vsync" || arg === "-fps_mode") {
1721
1913
  videoSync = args[++i] ?? "";
1722
1914
  } else if (arg === "-hls_time" || arg === "-seg_duration") {
@@ -1768,6 +1960,20 @@ function createFfmpegCommand(options = {}) {
1768
1960
  await writeBytes(context.stderr, encodeUtf8("ffmpeg: invalid metadata input index\n"), context.signal);
1769
1961
  return { exitCode: 1 };
1770
1962
  }
1963
+ } else if (arg === "-frame_pts") {
1964
+ const value = args[++i];
1965
+ if (value !== "0" && value !== "1") {
1966
+ await writeBytes(context.stderr, encodeUtf8("ffmpeg: invalid frame_pts\n"), context.signal);
1967
+ return { exitCode: 1 };
1968
+ }
1969
+ framePts = value === "1";
1970
+ } else if (arg === "-start_number_range") {
1971
+ const value = Number(args[++i]);
1972
+ if (!Number.isInteger(value) || value < 1 || value > 2147483647) {
1973
+ await writeBytes(context.stderr, encodeUtf8("ffmpeg: invalid start_number_range\n"), context.signal);
1974
+ return { exitCode: 1 };
1975
+ }
1976
+ pendingStartNumberRange = value;
1771
1977
  } else if (arg === "-start_number") {
1772
1978
  const val = Number(args[++i]);
1773
1979
  if (!Number.isSafeInteger(val) || val < 0) {
@@ -1778,6 +1984,10 @@ function createFfmpegCommand(options = {}) {
1778
1984
  else outputStartNumber = val;
1779
1985
  } else if (arg === "-ar") {
1780
1986
  audioRate = parseInt(args[++i] ?? "0", 10) || void 0;
1987
+ if (audioRate !== void 0 && audioRate < 0) {
1988
+ await writeBytes(context.stderr, encodeUtf8("ffmpeg: sample rate must be nonnegative\n"), context.signal);
1989
+ return { exitCode: 1 };
1990
+ }
1781
1991
  } else if (arg === "-ac") {
1782
1992
  audioChannels = parseInt(args[++i] ?? "0", 10) || void 0;
1783
1993
  } else if (arg === "-hide_banner" || arg === "-nostdin" || arg === "-stats") {
@@ -1813,10 +2023,29 @@ function createFfmpegCommand(options = {}) {
1813
2023
  } else {
1814
2024
  outputDuration = val;
1815
2025
  }
2026
+ } else if (arg === "-pattern_type") {
2027
+ const value = args[++i];
2028
+ if (value !== "glob" && value !== "none" && value !== "sequence") {
2029
+ await writeBytes(context.stderr, encodeUtf8(`ffmpeg: Invalid image2 pattern type: ${value ?? ""}
2030
+ `), context.signal);
2031
+ return { exitCode: 1 };
2032
+ }
2033
+ pendingPatternType = value;
2034
+ } else if (arg === "-loop") {
2035
+ const value = args[++i];
2036
+ if (value !== "0" && value !== "1") {
2037
+ await writeBytes(context.stderr, encodeUtf8("ffmpeg: -loop must be 0 or 1\n"), context.signal);
2038
+ return { exitCode: 1 };
2039
+ }
2040
+ pendingImageLoop = value === "1";
1816
2041
  } else if (arg === "-stream_loop") {
1817
2042
  pendingLoop = parseInt(args[++i] ?? "0", 10) || 0;
1818
2043
  } else if (arg === "-r" || arg === "-framerate") {
1819
- const val = parseFloat(args[++i] ?? "25") || 25;
2044
+ const val = parseFrameRate(args[++i] ?? "");
2045
+ if (val === void 0) {
2046
+ await writeBytes(context.stderr, encodeUtf8("ffmpeg: Invalid framerate\n"), context.signal);
2047
+ return { exitCode: 1 };
2048
+ }
1820
2049
  if (args.slice(i + 1).includes("-i")) {
1821
2050
  pendingFps = val;
1822
2051
  } else {
@@ -1835,20 +2064,28 @@ function createFfmpegCommand(options = {}) {
1835
2064
  inputs.push({
1836
2065
  path,
1837
2066
  format: pendingFormat,
2067
+ safe: pendingSafe,
1838
2068
  startSeconds: pendingSs,
1839
2069
  endSeconds: pendingTo,
1840
2070
  durationSeconds: pendingDuration,
1841
2071
  streamLoop: pendingLoop,
2072
+ imageLoop: pendingImageLoop,
1842
2073
  fps: pendingFps,
1843
- startNumber: pendingStartNumber
2074
+ startNumber: pendingStartNumber,
2075
+ patternType: pendingPatternType,
2076
+ startNumberRange: pendingStartNumberRange
1844
2077
  });
1845
2078
  pendingFormat = void 0;
2079
+ pendingSafe = void 0;
1846
2080
  pendingSs = void 0;
1847
2081
  pendingTo = void 0;
1848
2082
  pendingDuration = void 0;
1849
2083
  pendingLoop = void 0;
2084
+ pendingImageLoop = void 0;
1850
2085
  pendingFps = void 0;
1851
2086
  pendingStartNumber = void 0;
2087
+ pendingStartNumberRange = void 0;
2088
+ pendingPatternType = void 0;
1852
2089
  } else if (arg === "-c" || arg === "-codec") {
1853
2090
  const val = args[++i];
1854
2091
  videoCodec = val;
@@ -1870,7 +2107,7 @@ function createFfmpegCommand(options = {}) {
1870
2107
  } else if (arg === "-shortest") {
1871
2108
  shortest = true;
1872
2109
  } else if (arg === "-frames:v" || arg === "-vframes") {
1873
- maxVideoFrames = parseInt(args[++i] ?? "1", 10) || 1;
2110
+ maxVideoFrames = parseInt(args[++i] ?? "1", 10);
1874
2111
  } else if (arg === "-frames:a" || arg === "-aframes") {
1875
2112
  const value = args[++i];
1876
2113
  maxAudioFrames = value?.trim() ? Number(value) : NaN;
@@ -1890,9 +2127,14 @@ function createFfmpegCommand(options = {}) {
1890
2127
  } else if (arg === "-vf" || arg === "-filter:v") {
1891
2128
  const f = args[++i];
1892
2129
  if (f) vfFilters.push(f);
1893
- } else if (arg === "-af" || arg === "-filter:a") {
2130
+ } else if (arg === "-af" || arg === "-filter:a" || arg.startsWith("-af:") || arg.startsWith("-filter:a:")) {
1894
2131
  const f = args[++i];
1895
- if (f) afFilters.push(f);
2132
+ const selector = arg.startsWith("-af:") ? arg.slice(4) : arg.startsWith("-filter:a:") ? arg.slice(10) : void 0;
2133
+ const ordinal = selector === void 0 ? void 0 : Number(selector);
2134
+ if (selector !== void 0 && (!selector || !Array.from(selector).every((char) => char >= "0" && char <= "9") || !Number.isSafeInteger(ordinal))) {
2135
+ throw new Error(`Unsupported audio stream specifier: ${selector}`);
2136
+ }
2137
+ if (f) afFilters.push({ ordinal, expression: f });
1896
2138
  } else if (arg === "-filter_complex" || arg === "-lavfi") {
1897
2139
  filterComplex = args[++i];
1898
2140
  } else if (arg === "-disposition" || arg.startsWith("-disposition:")) {
@@ -1985,16 +2227,41 @@ function createFfmpegCommand(options = {}) {
1985
2227
  }
1986
2228
  return concatMp4(docs, { limits: options.limits, budget });
1987
2229
  }
1988
- if (/%0?\d*d/.test(filePath)) {
2230
+ let globPaths;
2231
+ if (input?.patternType === "glob") {
2232
+ globPaths = ["/"];
2233
+ for (const component of resolvePath(context.cwd, filePath).split("/").filter(Boolean)) {
2234
+ const matches = [];
2235
+ for (const directory of globPaths) {
2236
+ budget.checkCpu();
2237
+ context.signal.throwIfAborted();
2238
+ let entries;
2239
+ try {
2240
+ entries = await context.fs.readdir(directory, { signal: context.signal });
2241
+ } catch (error) {
2242
+ rethrowRuntimeError(context, error);
2243
+ if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) continue;
2244
+ throw error;
2245
+ }
2246
+ for (const entry of entries) {
2247
+ budget.checkCpu();
2248
+ if (entry.name.startsWith(".") && !component.startsWith(".") && !component.startsWith("\\.")) continue;
2249
+ if (matchesImageGlob(component, entry.name, budget)) matches.push(resolvePath(directory, entry.name));
2250
+ }
2251
+ }
2252
+ globPaths = matches;
2253
+ }
2254
+ globPaths.sort();
2255
+ }
2256
+ if (globPaths || input?.patternType !== "none" && formatImageSequence(filePath, input?.startNumber ?? 0) !== void 0) {
1989
2257
  const frames = [];
1990
2258
  const fps = fpsHint ?? 25;
1991
- for (let idx = input?.startNumber ?? 0; ; idx++) {
2259
+ const startNumber = input?.startNumber ?? 0;
2260
+ const startNumberRange = input?.startNumberRange ?? 5;
2261
+ for (let idx = globPaths ? 0 : startNumber; !globPaths || idx < globPaths.length; idx++) {
1992
2262
  budget.checkCpu();
1993
2263
  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
- });
2264
+ const formatted = globPaths ? globPaths[idx] : formatImageSequence(filePath, idx);
1998
2265
  const resolved = resolvePath(context.cwd, formatted);
1999
2266
  let imgBytes;
2000
2267
  try {
@@ -2002,7 +2269,8 @@ function createFfmpegCommand(options = {}) {
2002
2269
  } catch (error) {
2003
2270
  rethrowRuntimeError(context, error);
2004
2271
  if (error.code !== "ENOENT") throw error;
2005
- if (idx === 0 && input?.startNumber === void 0) continue;
2272
+ if (globPaths) throw error;
2273
+ if (frames.length === 0 && idx - startNumber < startNumberRange - 1) continue;
2006
2274
  break;
2007
2275
  }
2008
2276
  {
@@ -2041,27 +2309,76 @@ function createFfmpegCommand(options = {}) {
2041
2309
  detachBytesBuffer(synthBytes);
2042
2310
  return {
2043
2311
  ...base,
2312
+ duration: Math.round(frames.length / fps * base.timescale),
2313
+ durationSeconds: frames.length / fps,
2044
2314
  tracks: base.tracks.map(
2045
- (t) => t.type === "video" ? { ...t, width: first.width, height: first.height, samples: [], decodedVideoFrames: frames } : t
2315
+ (t) => t.type === "video" ? { ...t, width: first.width, height: first.height, duration: Math.round(frames.length / fps * t.timescale), samples: [], decodedVideoFrames: frames } : t
2046
2316
  )
2047
2317
  };
2048
2318
  }
2049
2319
  const rawBytes = await readInput(filePath);
2050
- if (explicitFormat === "concat") {
2320
+ const concatSignature = "ffconcat version 1.0";
2321
+ if (explicitFormat === "concat" || explicitFormat === void 0 && new TextDecoder("utf-8", { ignoreBOM: true }).decode(rawBytes.subarray(0, concatSignature.length)) === concatSignature) {
2051
2322
  const listText = new TextDecoder().decode(rawBytes);
2052
2323
  const listDir = filePath === "-" ? context.cwd : dirnameOf(resolvePath(context.cwd, filePath));
2053
2324
  const concatDocs = [];
2325
+ const startTimesSeconds = [];
2326
+ let nextStartSeconds = 0;
2327
+ const segments = [];
2054
2328
  let currentInpoint;
2055
2329
  let currentOutpoint;
2056
2330
  let currentDuration;
2057
2331
  const lines = listText.split(/\r?\n/);
2058
2332
  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);
2333
+ const line = lines[l].trimStart();
2334
+ if (!line || line.startsWith("#")) continue;
2335
+ const keyword = line.split(/[ \t]/, 1)[0];
2336
+ if (keyword === "option" && input?.safe !== false) {
2337
+ throw new Error(`Line ${l + 1}: option not allowed if safe`);
2338
+ }
2339
+ if (keyword === "ffconcat") {
2340
+ const [, declaration, version] = line.split(" ").join(" ").split(" ").filter(Boolean);
2341
+ if (declaration !== "version" || version !== "1.0") {
2342
+ throw new Error(`Line ${l + 1}: invalid version`);
2343
+ }
2344
+ continue;
2345
+ }
2346
+ if (segments.length === 0) {
2347
+ const directive = ["duration", "inpoint", "outpoint"].find(
2348
+ (name) => line === name || line.startsWith(`${name} `) || line.startsWith(`${name} `)
2349
+ );
2350
+ if (directive) throw new Error(`Line ${l + 1}: ${directive} without file`);
2351
+ }
2352
+ if (keyword === "file") {
2353
+ let target2 = "";
2354
+ let protectedEnd = 0;
2355
+ let cursor = keyword.length;
2356
+ const whitespace = " \r\n";
2357
+ while (cursor < line.length && whitespace.includes(line[cursor])) cursor++;
2358
+ while (cursor < line.length && !whitespace.includes(line[cursor])) {
2359
+ const char = line[cursor++];
2360
+ if (char === "\\" && cursor < line.length) {
2361
+ target2 += line[cursor++];
2362
+ protectedEnd = target2.length;
2363
+ } else if (char === "'") {
2364
+ while (cursor < line.length && line[cursor] !== "'") target2 += line[cursor++];
2365
+ if (cursor < line.length) {
2366
+ cursor++;
2367
+ protectedEnd = target2.length;
2368
+ }
2369
+ } else {
2370
+ target2 += char;
2371
+ }
2372
+ }
2373
+ while (target2.length > protectedEnd && whitespace.includes(target2[target2.length - 1])) {
2374
+ target2 = target2.slice(0, -1);
2375
+ }
2376
+ if (input?.safe !== false && target2.split("/").some(
2377
+ (component) => !component || component.startsWith(".") || Array.from(component).some(
2378
+ (char) => !(char >= "a" && char <= "z") && !(char >= "A" && char <= "Z") && !(char >= "0" && char <= "9") && char !== "_" && char !== "-" && char !== "."
2379
+ )
2380
+ )) {
2381
+ throw new Error(`Unsafe file name '${target2}'`);
2065
2382
  }
2066
2383
  const resolvedItem = resolvePath(listDir, target2);
2067
2384
  currentInpoint = void 0;
@@ -2069,32 +2386,66 @@ function createFfmpegCommand(options = {}) {
2069
2386
  currentDuration = void 0;
2070
2387
  while (l + 1 < lines.length) {
2071
2388
  const nextLine = lines[l + 1].trim();
2072
- if (nextLine.startsWith("inpoint ")) {
2073
- currentInpoint = parseFfmpegTimestamp(nextLine.slice(8));
2389
+ if (!nextLine || nextLine.startsWith("#")) {
2074
2390
  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));
2391
+ } else if (["inpoint", "outpoint", "duration"].includes(nextLine.split(/[ \t]/, 1)[0])) {
2392
+ const directive = nextLine.split(/[ \t]/, 1)[0];
2393
+ const value = nextLine.slice(directive.length).trim();
2394
+ if (!/^-?(?:(?:\d+(?:\.\d*)?|\.\d+)(?:s|ms|us)?|(?:\d+:)?[0-5]?\d:[0-5]?\d(?:\.\d*)?)$/.test(value)) {
2395
+ throw new Error(`Line ${l + 2}: invalid ${directive} '${value}'`);
2396
+ }
2397
+ const seconds = value.startsWith("-") ? -parseFfmpegTimestamp(value.slice(1)) : parseFfmpegTimestamp(value);
2398
+ if (!Number.isFinite(seconds) || Math.abs(seconds) >= 2 ** 63 / 1e6) {
2399
+ throw new Error(`Line ${l + 2}: invalid ${directive} '${value}'`);
2400
+ }
2401
+ if (directive === "inpoint") currentInpoint = seconds;
2402
+ else if (directive === "outpoint") currentOutpoint = seconds;
2403
+ else currentDuration = seconds;
2080
2404
  l++;
2081
2405
  } else {
2082
2406
  break;
2083
2407
  }
2084
2408
  }
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
- });
2409
+ if (currentInpoint !== void 0 && currentOutpoint !== void 0 && currentOutpoint < currentInpoint) {
2410
+ throw new Error(`${resolvedItem}: outpoint must not precede inpoint`);
2092
2411
  }
2093
- concatDocs.push(itemDoc);
2412
+ segments.push({ path: resolvedItem, inpoint: currentInpoint, outpoint: currentOutpoint, duration: currentDuration });
2413
+ } else {
2414
+ throw new Error(`Line ${l + 1}: unknown keyword '${keyword}'`);
2094
2415
  }
2095
2416
  }
2096
- budget.checkConcatInputs(concatDocs.length);
2097
- return concatMp4(concatDocs, { limits: options.limits, budget });
2417
+ budget.checkConcatInputs(segments.length);
2418
+ for (const segment of segments) {
2419
+ let itemDoc;
2420
+ try {
2421
+ itemDoc = await loadSingleDocument(segment.path);
2422
+ } catch (error) {
2423
+ rethrowRuntimeError(context, error);
2424
+ if (error.code !== "ENOENT" || !concatDocs.some((doc) => doc.tracks.some((track) => track.samples.length > 0))) throw error;
2425
+ await writeBytes(context.stderr, encodeUtf8(`ffmpeg: Impossible to open '${segment.path}': No such file or directory
2426
+ `), context.signal);
2427
+ inputReadFailed = true;
2428
+ break;
2429
+ }
2430
+ let sliceEnd = segment.outpoint;
2431
+ const wavTrack = itemDoc.containerFormat === "wav" ? itemDoc.tracks[0] : void 0;
2432
+ const packetFrames = wavTrack?.samples[0]?.duration;
2433
+ if (sliceEnd !== void 0 && wavTrack && packetFrames) {
2434
+ const start = Math.max(0, segment.inpoint ?? 0);
2435
+ const packets = Math.ceil(Math.max(0, sliceEnd - start) * wavTrack.timescale / packetFrames);
2436
+ sliceEnd = start + packets * packetFrames / wavTrack.timescale;
2437
+ }
2438
+ if (segment.inpoint !== void 0 || segment.outpoint !== void 0) {
2439
+ itemDoc = sliceMp4(itemDoc, {
2440
+ startSeconds: segment.inpoint,
2441
+ endSeconds: sliceEnd
2442
+ });
2443
+ }
2444
+ startTimesSeconds.push(nextStartSeconds);
2445
+ nextStartSeconds += segment.duration ?? itemDoc.durationSeconds;
2446
+ concatDocs.push(itemDoc);
2447
+ }
2448
+ return concatMp4(concatDocs, { startTimesSeconds, limits: options.limits, budget });
2098
2449
  }
2099
2450
  const plugin = explicitFormat !== void 0 ? registry.findByFormatName(explicitFormat) : registry.detect(rawBytes, filePath);
2100
2451
  if (!plugin || !plugin.canDemux) {
@@ -2115,6 +2466,38 @@ function createFfmpegCommand(options = {}) {
2115
2466
  await yieldTurn5(context.signal);
2116
2467
  context.signal.throwIfAborted();
2117
2468
  let doc = await loadSingleDocument(inp.path, inp.format, inp.fps, inp);
2469
+ if (inp.imageLoop) {
2470
+ const fps = inp.fps ?? 25;
2471
+ const endSeconds = Math.min(
2472
+ inp.durationSeconds !== void 0 ? (inp.startSeconds ?? 0) + inp.durationSeconds : inp.endSeconds ?? Infinity,
2473
+ (inp.startSeconds ?? 0) + (outputDuration !== void 0 ? (outputSs ?? 0) + outputDuration : outputTo ?? Infinity),
2474
+ maxVideoFrames !== void 0 ? (inp.startSeconds ?? 0) + (outputSs ?? 0) + maxVideoFrames / (outputFps ?? fps) : Infinity
2475
+ );
2476
+ if (!Number.isFinite(endSeconds)) {
2477
+ throw new Error("-loop 1 requires a finite frame count or duration");
2478
+ }
2479
+ budget.checkDuration(endSeconds);
2480
+ const frameCount = Math.max(0, Math.ceil(endSeconds * fps));
2481
+ doc = {
2482
+ ...doc,
2483
+ duration: Math.round(frameCount / fps * doc.timescale),
2484
+ durationSeconds: frameCount / fps,
2485
+ tracks: await mapWork(doc.tracks, async (track) => {
2486
+ if (track.type !== "video") return track;
2487
+ const source = track.decodedVideoFrames;
2488
+ if (!source?.length) throw new Error("-loop requires decoded image input");
2489
+ const frames = [];
2490
+ for (let index = 0; index < frameCount; index++) {
2491
+ if (index % 64 === 0) await yieldTurn5(context.signal);
2492
+ context.signal.throwIfAborted();
2493
+ const frame = source[index % source.length];
2494
+ budget.recordFrame(frame.width, frame.height);
2495
+ frames.push({ ...frame, ptsSeconds: index / fps, durationSeconds: 1 / fps });
2496
+ }
2497
+ return { ...track, duration: Math.round(frameCount / fps * track.timescale), samples: [], decodedVideoFrames: frames };
2498
+ }, context.signal)
2499
+ };
2500
+ }
2118
2501
  if (inp.startSeconds !== void 0 || inp.endSeconds !== void 0 || inp.durationSeconds !== void 0) {
2119
2502
  doc = sliceMp4(doc, {
2120
2503
  startSeconds: inp.startSeconds,
@@ -2430,7 +2813,8 @@ To ignore this, add a trailing '?' to the map.
2430
2813
  }, context.signal);
2431
2814
  }
2432
2815
  validateAudioEncoder(audioCodec, workingDoc, astPlugins);
2433
- if (audioCodec === "copy" && afFilters.length > 0 && workingDoc.tracks.some((track) => track.type === "audio")) {
2816
+ const audioTrackCount = workingDoc.tracks.filter((track) => track.type === "audio").length;
2817
+ if (audioCodec === "copy" && audioTrackCount > 0 && afFilters.some((filter) => filter.ordinal === void 0 || filter.ordinal < audioTrackCount)) {
2434
2818
  await writeBytes(context.stderr, encodeUtf8("ffmpeg: Filtering and streamcopy cannot be used together for audio.\n"), context.signal);
2435
2819
  return { exitCode: 218 };
2436
2820
  }
@@ -2538,60 +2922,117 @@ To ignore this, add a trailing '?' to the map.
2538
2922
  workingDoc = limitAudioCopyPackets(workingDoc, maxAudioFrames);
2539
2923
  }
2540
2924
  if ((audioRate !== void 0 || audioChannels !== void 0 || afFilters.length > 0) && audioCodec !== "copy") {
2925
+ let audioOrdinal = 0;
2541
2926
  workingDoc = {
2542
2927
  ...workingDoc,
2543
2928
  tracks: await mapWork(workingDoc.tracks, async (t) => {
2544
2929
  let work = 0;
2545
2930
  if (t.type !== "audio") return t;
2931
+ const ordinal = audioOrdinal++;
2932
+ const filter = afFilters.filter((filter2) => filter2.ordinal === void 0 || filter2.ordinal === ordinal).at(-1);
2933
+ if (audioRate === void 0 && audioChannels === void 0 && !filter) return t;
2546
2934
  const audio = t.decodedAudio ?? (t.codecDescriptions[0]?.codecName === "aac" ? decodeAacTrack(workingDoc, t) : void 0);
2547
2935
  if (!audio) throw new Error("Audio filtering requires decoded PCM audio");
2548
2936
  const origRate = audio.sampleRate;
2549
2937
  const origCh = audio.channels;
2550
2938
  const targetRate = audioRate ?? origRate;
2551
- const targetCh = audioChannels ?? origCh;
2939
+ const targetCh = audioChannels === -1 ? origCh : audioChannels ?? origCh;
2552
2940
  const channelMask = audio.channelMask;
2553
2941
  const surroundMono = origCh === 6 && targetCh === 1;
2554
2942
  const monoStereo = origCh === 1 && (targetCh === 2 || targetCh === 3);
2943
+ const pcm16MonoStereo = monoStereo && !filter && targetRate === origRate && t.codecDescriptions[0]?.codecName === "pcm_s16le" && (audioCodec === "pcm_s16le" || audioCodec === void 0 && (outputFormat ?? outputTarget2.split(".").pop()?.toLowerCase()) === "wav");
2944
+ const monoSurround = origCh === 1 && targetCh >= 4 && targetCh <= 8;
2555
2945
  if (targetCh !== origCh && origCh > 2 && (!surroundMono || channelMask && channelMask !== 63 && channelMask !== 1551)) {
2556
2946
  throw new Error(`Unsupported audio channel layout conversion: ${origCh} channels (mask ${channelMask ?? 0}) to ${targetCh}`);
2557
2947
  }
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(",")) {
2948
+ const volFactor = t.volume ?? 1;
2949
+ let stretched = audio.channelData;
2950
+ if (filter) {
2951
+ for (const item of parseFilterChain(filter.expression)) {
2568
2952
  if (++work % 1024 === 0) {
2569
2953
  await yieldTurn5(context.signal);
2570
2954
  context.signal?.throwIfAborted();
2571
2955
  }
2572
- const trimmed = item.trim();
2573
- if (trimmed.startsWith("volume=")) {
2574
- const volSpec = trimmed.slice("volume=".length).trim();
2956
+ let trimmed = item.trim();
2957
+ const equals = trimmed.indexOf("=");
2958
+ const instance = trimmed.indexOf("@");
2959
+ if (instance >= 0 && (equals < 0 || instance < equals)) {
2960
+ trimmed = trimmed.slice(0, instance) + (equals < 0 ? "" : trimmed.slice(equals));
2961
+ }
2962
+ if (trimmed === "anull") continue;
2963
+ if (trimmed === "volume" || trimmed.startsWith("volume=")) {
2964
+ const argument = trimmed === "volume" ? "1" : trimmed.slice("volume=".length).trim();
2965
+ let volSpec = "1";
2966
+ let precision = "float";
2967
+ let evaluation = "once";
2968
+ let enable;
2969
+ for (const [index, option] of argument.split(":").entries()) {
2970
+ const separator = option.indexOf("=");
2971
+ const key = separator < 0 ? ["volume", "precision", "eval"][index] ?? "" : option.slice(0, separator).trim();
2972
+ const value = separator < 0 ? option.trim() : option.slice(separator + 1).trim();
2973
+ if (key === "volume") volSpec = value;
2974
+ else if (key === "precision") precision = value;
2975
+ else if (key === "eval") evaluation = value;
2976
+ else if (key === "enable") enable = value;
2977
+ else throw new Error(`volume: unsupported option ${key}`);
2978
+ }
2979
+ if (!["fixed", "float", "double"].includes(precision)) throw new Error("volume: invalid precision");
2980
+ if (evaluation !== "once" && evaluation !== "frame") throw new Error("volume: invalid eval");
2575
2981
  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) {
2982
+ const output = stretched.map((channel) => new Float32Array(channel.length));
2983
+ const length = stretched[0]?.length ?? 0;
2984
+ const sampleScale = t.codecDescriptions[0]?.codecName === "pcm_s16le" ? 32768 : 2147483648;
2985
+ for (let start = 0, n = 0; start < length; n++) {
2986
+ const packet = t.samples[n];
2987
+ const count = packet ? Math.max(1, Math.round(packet.duration * origRate / t.timescale)) : 1024;
2988
+ const end = evaluation === "once" && enable === void 0 ? length : Math.min(length, start + count);
2989
+ const variables = { sample_rate: origRate, nb_channels: origCh, tb: 1 / origRate };
2990
+ if (evaluation === "frame") Object.assign(variables, {
2991
+ n,
2992
+ t: packet ? packet.pts / t.timescale : start / origRate,
2993
+ pts: packet ? packet.pts * origRate / t.timescale : start,
2994
+ nb_samples: end - start,
2995
+ nb_consumed_samples: start
2996
+ });
2997
+ const gain = parseFilterExpression(decibels ? volSpec.slice(0, -2) : volSpec, "volume", variables);
2998
+ const factor = decibels ? Math.pow(10, gain / 20) : gain;
2999
+ if (!Number.isFinite(factor)) throw new Error("volume: invalid gain");
3000
+ const fixedGain = Math.floor(factor * 256 + 0.5);
3001
+ const enabled = enable === void 0 || parseFilterExpression(enable, "volume enable", {
3002
+ n,
3003
+ t: packet ? packet.pts / t.timescale : start / origRate
3004
+ }) !== 0;
3005
+ for (let c = 0; c < stretched.length; c++) {
3006
+ const channel = stretched[c];
3007
+ for (let i2 = start; i2 < end; i2++) {
3008
+ if (++work % 1024 === 0) {
3009
+ await yieldTurn5(context.signal);
3010
+ context.signal?.throwIfAborted();
3011
+ }
3012
+ output[c][i2] = !enabled ? channel[i2] : precision === "fixed" ? Math.max(-sampleScale, Math.min(
3013
+ sampleScale - 1,
3014
+ Math.floor((channel[i2] * sampleScale * fixedGain + 128) / 256)
3015
+ )) / sampleScale : channel[i2] * (precision === "float" ? Math.fround(factor) : factor);
3016
+ }
3017
+ }
3018
+ start = end;
3019
+ }
3020
+ stretched = output;
3021
+ } else if (trimmed === "atempo" || trimmed.startsWith("atempo=")) {
3022
+ const argument = trimmed === "atempo" ? "1" : trimmed.slice("atempo=".length).trim();
3023
+ const tempo = parseFilterExpression(argument.startsWith("tempo=") ? argument.slice("tempo=".length) : argument, "atempo", {});
3024
+ if (tempo < 0.5 || tempo > 100) {
2583
3025
  throw new Error("atempo must be in the range [0.5, 100]");
2584
3026
  }
2585
- tempoFactor *= tempo;
3027
+ stretched = tempo === 1 ? await filterUnityTempo(stretched, origRate, t.codecDescriptions[0]?.codecName === "pcm_s16le", budget, context.signal) : await stretchAudio(stretched, origRate, tempo, budget, context.signal);
2586
3028
  } else if (trimmed === "areverse") {
2587
- reverseAudio = !reverseAudio;
3029
+ stretched = await mapWork(stretched, async (channel) => channel.slice().reverse(), context.signal);
2588
3030
  } else {
2589
3031
  throw new Error(`Unsupported audio filter: ${trimmed}`);
2590
3032
  }
2591
3033
  }
2592
3034
  }
2593
- const targetSamples = Math.max(0, Math.round(durSec / tempoFactor * targetRate));
2594
- let stretched = await stretchAudio(audio.channelData, origRate, tempoFactor, budget, context.signal);
3035
+ const targetSamples = Math.max(0, Math.round((stretched[0]?.length ?? 0) * targetRate / origRate));
2595
3036
  if (targetRate !== origRate) stretched = await resampleAudio(stretched, targetSamples, origRate / targetRate, budget, context.signal);
2596
3037
  const newChannelData = [];
2597
3038
  for (let c = 0; c < targetCh; c++) {
@@ -2600,7 +3041,7 @@ To ignore this, add a trailing '?' to the map.
2600
3041
  context.signal?.throwIfAborted();
2601
3042
  }
2602
3043
  const dst = new Float32Array(targetSamples);
2603
- const srcCh = monoStereo && c >= 2 ? void 0 : stretched[origCh === 1 ? 0 : c];
3044
+ const srcCh = monoStereo && c >= 2 || monoSurround && c !== 2 ? void 0 : stretched[origCh === 1 ? 0 : c];
2604
3045
  const rightCh = origCh === 2 && targetCh === 1 ? stretched[1] : void 0;
2605
3046
  if (srcCh && srcCh.length > 0) {
2606
3047
  for (let i2 = 0; i2 < targetSamples; i2++) {
@@ -2610,10 +3051,10 @@ To ignore this, add a trailing '?' to the map.
2610
3051
  }
2611
3052
  const srcIdx = Math.min(srcCh.length - 1, i2);
2612
3053
  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)));
3054
+ const mixed = pcm16MonoStereo ? Math.floor((sample * 32768 * 23170 + 16384) / 32768) / 32768 : sample * (monoStereo ? Math.SQRT1_2 : 1);
3055
+ dst[i2] = Math.max(-1, Math.min(1, mixed * volFactor));
2614
3056
  }
2615
3057
  }
2616
- if (reverseAudio) dst.reverse();
2617
3058
  newChannelData.push(dst);
2618
3059
  }
2619
3060
  const nextDescs = t.codecDescriptions.map((d) => ({
@@ -2788,10 +3229,15 @@ To ignore this, add a trailing '?' to the map.
2788
3229
  options.onMetrics?.(budget.getStats());
2789
3230
  return { exitCode: 0 };
2790
3231
  }
3232
+ if (imageOutput && maxVideoFrames === 0) {
3233
+ options.onMetrics?.(budget.getStats());
3234
+ return { exitCode: 0 };
3235
+ }
2791
3236
  if (imageOutput && !workingDoc.tracks.some((track) => track.type === "video" && track.decodedVideoFrames?.length)) {
2792
3237
  throw new Error("No video frame in the requested presentation interval");
2793
3238
  }
2794
- if (/%0?\d*d/.test(outputTarget2)) {
3239
+ const numberedImageOutput = formatImageSequence(outputTarget2, outputStartNumber ?? 1) !== void 0;
3240
+ if (imageOutput && !isStdout(outputTarget2) || numberedImageOutput) {
2795
3241
  const outExt2 = outputTarget2.split(".").pop()?.toLowerCase() ?? "png";
2796
3242
  const imgPlugin = registry.findByFilename(`x.${outExt2}`);
2797
3243
  if (!imgPlugin || !imgPlugin.canMux) {
@@ -2813,15 +3259,16 @@ To ignore this, add a trailing '?' to the map.
2813
3259
  const previousCopies = constantRate && delta0 > 1.1 ? Math.max(0, Math.round(delta0 - 0.6)) : 0;
2814
3260
  if (!Number.isSafeInteger(copies)) throw new Error("Invalid output frame timestamp");
2815
3261
  for (let copy = 0; copy < copies && idx < limit; copy++, idx++, nextPts++) {
3262
+ if (idx > 0 && !numberedImageOutput && !updateImage) {
3263
+ throw new Error("Cannot write more than one file with the same name. Use a numbered pattern or -update 1.");
3264
+ }
2816
3265
  budget.checkCpu();
2817
3266
  budget.checkSamples(idx + 1);
2818
3267
  budget.checkDuration((idx + 1) * duration);
2819
3268
  const f = copy < previousCopies && sourceIndex > 0 ? frames[sourceIndex - 1] : source;
2820
3269
  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
- });
3270
+ const number = framePts ? Math.round(constantRate ? nextPts : source.ptsSeconds / duration) : idx + (outputStartNumber ?? 1);
3271
+ const fileName = updateImage ? outputTarget2 : formatImageSequence(outputTarget2, number) ?? outputTarget2;
2825
3272
  const fullOutPath = resolvePath(context.cwd, fileName);
2826
3273
  const encoded = (await yieldTurn5(context.signal), context.signal?.throwIfAborted(), encodeImage(makeRgbaImg(f.width, f.height, f.data), {
2827
3274
  format: imgFmt
@@ -2897,7 +3344,7 @@ To ignore this, add a trailing '?' to the map.
2897
3344
  const result = await output();
2898
3345
  if (result.exitCode !== 0) return result;
2899
3346
  }
2900
- return { exitCode: 0 };
3347
+ return { exitCode: exitOnError && inputReadFailed ? 1 : 0 };
2901
3348
  }
2902
3349
  };
2903
3350
  }