@poe-platform/safe-bash 0.1.845 → 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 (42) 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-CMGVQM4Y.js → chunk-CSJHADNR.js} +17 -3
  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-4KM2VJRB.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 +433 -74
  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.js +835 -247
  37. package/dist/safe-bash-command-imagemagick/raw-output.d.ts +10 -0
  38. package/dist/safe-bash-command-python/executor.js +3 -3
  39. package/dist/safe-bash-command-python/index.js +3 -3
  40. package/dist/safe-bash-command-python/jspi.js +2 -2
  41. package/dist/safe-bash-command-python/worker.js +1 -1
  42. 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-CMGVQM4Y.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
@@ -342,6 +342,26 @@ import {
342
342
  resizeImage,
343
343
  rotateImage
344
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
+ }
345
365
  function loopToOutputEnd(doc, endSeconds, budget) {
346
366
  if (endSeconds === void 0 || !Number.isFinite(endSeconds)) {
347
367
  throw new Error("-stream_loop -1 requires a finite output -t or -to");
@@ -599,6 +619,7 @@ function* lavfiSteps(spec, budget, durationOverride) {
599
619
  };
600
620
  }
601
621
  async function ensureDecodedFrames(track, movieTimescale, budget, signal, maxFrames) {
622
+ if (maxFrames === 0) return [];
602
623
  const width = track.width ?? track.codecDescriptions[0]?.width ?? 64;
603
624
  const height = track.height ?? track.codecDescriptions[0]?.height ?? 64;
604
625
  const ts = track.timescale || 9e4;
@@ -668,6 +689,9 @@ function parseFilterExpression(expression, filter, variables) {
668
689
  }
669
690
  if (name === "not" && args.length === 1) value = Number(args[0] === 0);
670
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];
671
695
  else if (name === "gt" && args.length === 2) value = Number(args[0] > args[1]);
672
696
  else if (name === "mod" && args.length === 2) value = args[0] - Math.floor(args[0] / args[1]) * args[1];
673
697
  else if (name === "between" && args.length === 3) value = Number(args[0] >= args[1] && args[0] <= args[2]);
@@ -854,10 +878,7 @@ async function renderBitmapTextToRgba(rgba, width, height, text, opts = {}, sign
854
878
  }
855
879
  return out;
856
880
  }
857
- async function applyVideoFilterChain(frames, filterChainStr, budget, subtitleCues, signal) {
858
- let work = 0;
859
- let current = frames;
860
- if (current.length === 0) return current;
881
+ function parseFilterChain(filterChainStr) {
861
882
  const filters = [];
862
883
  let filter = "";
863
884
  let quoted = false;
@@ -875,7 +896,13 @@ async function applyVideoFilterChain(frames, filterChainStr, budget, subtitleCue
875
896
  }
876
897
  }
877
898
  if (filter.trim()) filters.push(filter.trim());
878
- 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)) {
879
906
  if (++work % 1024 === 0) {
880
907
  await yieldTurn5(signal);
881
908
  signal?.throwIfAborted();
@@ -1464,7 +1491,7 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1464
1491
  if (arg === "-y" || arg === "-n") {
1465
1492
  if (noOverwrite !== void 0 && noOverwrite !== (arg === "-n")) return void 0;
1466
1493
  noOverwrite = arg === "-n";
1467
- } else if (arg === "-v" || arg === "-loglevel" || arg === "-safe" || arg === "-threads" || arg === "-pix_fmt" || arg === "-preset" || arg === "-crf" || arg === "-b:v" || arg === "-b:a" || arg === "-bsf:v" || arg === "-bsf:a" || arg === "-tag:v" || arg === "-tag:a" || arg === "-vsync" || arg === "-fps_mode") i++;
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++;
1468
1495
  else if (arg === "-map_chapters") {
1469
1496
  const value = args[++i];
1470
1497
  chapterSource = value?.trim() ? Number(value) : NaN;
@@ -1502,7 +1529,8 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1502
1529
  else outputDuration = val;
1503
1530
  } else if (arg === "-stream_loop") pendingLoop = parseInt(args[++i] ?? "0", 10) || 0;
1504
1531
  else if (arg === "-r" || arg === "-framerate") {
1505
- const val = parseFloat(args[++i] ?? "25") || 25;
1532
+ const val = parseFrameRate(args[++i] ?? "");
1533
+ if (val === void 0) return void 0;
1506
1534
  if (args.slice(i + 1).includes("-i")) pendingFps = val;
1507
1535
  else outputFps = val;
1508
1536
  } else if (arg === "-s") outputSize = args[++i];
@@ -1540,7 +1568,7 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1540
1568
  else if (arg === "-vn") stripVideo = true;
1541
1569
  else if (arg === "-sn") stripSubtitles = true;
1542
1570
  else if (arg === "-shortest") shortest = true;
1543
- 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);
1544
1572
  else if (arg === "-frames:a" || arg === "-aframes") {
1545
1573
  const value = args[++i];
1546
1574
  maxAudioFrames = value?.trim() ? Number(value) : NaN;
@@ -1657,16 +1685,16 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1657
1685
  if (isNullMux) return "";
1658
1686
  if (inputs.some((inp) => isStdin(inp.path)) && !inBytes) return void 0;
1659
1687
  if (!writeFileSync || !outputTarget || outputTarget.endsWith("/") || outputTarget.endsWith("/.") || outputTarget.includes("/./") || /(?:^|\/)\.\.(?:\/|$)/u.test(outputTarget)) return void 0;
1660
- if (/%0?\d*d/.test(outputTarget)) {
1688
+ if (formatImageSequence(outputTarget, outputStartNumber ?? 1) !== void 0) {
1661
1689
  const outExt2 = outputTarget.split(".").pop()?.toLowerCase() ?? "png";
1662
1690
  const vTrack = workingDoc.tracks.find((t) => t.type === "video");
1663
1691
  if (vTrack && !vTrack.decodedVideoFrames) return void 0;
1664
1692
  const frames = vTrack?.decodedVideoFrames ?? [];
1665
- const limit = maxVideoFrames ? Math.min(frames.length, maxVideoFrames) : frames.length;
1693
+ const limit = Math.min(frames.length, maxVideoFrames ?? Infinity);
1666
1694
  const imgFmt = outExt2 === "jpg" || outExt2 === "jpeg" ? "jpeg" : outExt2 === "webp" ? "webp" : outExt2 === "gif" ? "gif" : outExt2 === "bmp" ? "bmp" : outExt2 === "ppm" ? "ppm" : "png";
1667
1695
  for (let idx = 0; idx < limit; idx++) {
1668
1696
  const f = frames[idx];
1669
- 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));
1670
1698
  const encoded = encodeImage(makeRgbaImg(f.width, f.height, f.data), { format: imgFmt }).data;
1671
1699
  if (!writeFileSync(fileName, encoded)) return void 0;
1672
1700
  }
@@ -1674,6 +1702,7 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1674
1702
  }
1675
1703
  const outPlugin = outputFormat ? registry.findByFormatName(outputFormat) : registry.findByFilename(outputTarget);
1676
1704
  if (!outPlugin || !outPlugin.canMux || outPlugin.id === "dash" || outPlugin.id === "hls") return void 0;
1705
+ if (outPlugin.id === "image2" && maxVideoFrames === 0) return "";
1677
1706
  const outExt = outputTarget.split(".").pop()?.toLowerCase();
1678
1707
  const serializedBytes = outPlugin.serialize(workingDoc, { audioCodec, format: outputFormat ?? outExt, faststart, fragmented, metadata: metaTags, budget });
1679
1708
  if (isStdout(outputTarget)) return void 0;
@@ -1683,6 +1712,81 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1683
1712
  return void 0;
1684
1713
  }
1685
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
+ }
1686
1790
  function createFfmpegCommand(options = {}) {
1687
1791
  const astPlugins = options.asts ?? allMediaAsts();
1688
1792
  const registry = createMediaAstRegistry(astPlugins);
@@ -1716,14 +1820,20 @@ function createFfmpegCommand(options = {}) {
1716
1820
  }
1717
1821
  const inputs = [];
1718
1822
  let pendingFormat;
1823
+ let pendingSafe;
1719
1824
  let pendingAudioDecoder;
1720
1825
  let pendingSs;
1721
1826
  let pendingTo;
1722
1827
  let pendingDuration;
1723
1828
  let pendingLoop;
1829
+ let pendingImageLoop;
1724
1830
  let pendingFps;
1725
1831
  let pendingStartNumber;
1832
+ let pendingStartNumberRange;
1833
+ let pendingPatternType;
1726
1834
  let overwrite;
1835
+ let exitOnError = false;
1836
+ let inputReadFailed = false;
1727
1837
  let filterComplex;
1728
1838
  const outputs = [];
1729
1839
  const inputBytes = /* @__PURE__ */ new Map();
@@ -1739,6 +1849,8 @@ function createFfmpegCommand(options = {}) {
1739
1849
  let i = 0;
1740
1850
  while (i < args.length) {
1741
1851
  let outputStartNumber;
1852
+ let updateImage = false;
1853
+ let framePts = false;
1742
1854
  let outputFormat;
1743
1855
  let outputSs;
1744
1856
  let outputTo;
@@ -1784,8 +1896,19 @@ function createFfmpegCommand(options = {}) {
1784
1896
  return { exitCode: 234 };
1785
1897
  }
1786
1898
  overwrite = arg === "-y";
1787
- } else if (arg === "-v" || arg === "-loglevel" || arg === "-safe" || arg === "-threads" || arg === "-pix_fmt" || arg === "-preset" || arg === "-crf" || arg === "-b:v" || arg === "-b:a" || arg === "-bsf:v" || arg === "-bsf:a" || arg === "-tag:v" || arg === "-tag:a" || arg === "-update" || arg === "-loop") {
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") {
1788
1909
  i++;
1910
+ } else if (arg === "-update") {
1911
+ updateImage = args[++i] === "1";
1789
1912
  } else if (arg === "-vsync" || arg === "-fps_mode") {
1790
1913
  videoSync = args[++i] ?? "";
1791
1914
  } else if (arg === "-hls_time" || arg === "-seg_duration") {
@@ -1837,6 +1960,20 @@ function createFfmpegCommand(options = {}) {
1837
1960
  await writeBytes(context.stderr, encodeUtf8("ffmpeg: invalid metadata input index\n"), context.signal);
1838
1961
  return { exitCode: 1 };
1839
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;
1840
1977
  } else if (arg === "-start_number") {
1841
1978
  const val = Number(args[++i]);
1842
1979
  if (!Number.isSafeInteger(val) || val < 0) {
@@ -1886,10 +2023,29 @@ function createFfmpegCommand(options = {}) {
1886
2023
  } else {
1887
2024
  outputDuration = val;
1888
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";
1889
2041
  } else if (arg === "-stream_loop") {
1890
2042
  pendingLoop = parseInt(args[++i] ?? "0", 10) || 0;
1891
2043
  } else if (arg === "-r" || arg === "-framerate") {
1892
- 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
+ }
1893
2049
  if (args.slice(i + 1).includes("-i")) {
1894
2050
  pendingFps = val;
1895
2051
  } else {
@@ -1908,20 +2064,28 @@ function createFfmpegCommand(options = {}) {
1908
2064
  inputs.push({
1909
2065
  path,
1910
2066
  format: pendingFormat,
2067
+ safe: pendingSafe,
1911
2068
  startSeconds: pendingSs,
1912
2069
  endSeconds: pendingTo,
1913
2070
  durationSeconds: pendingDuration,
1914
2071
  streamLoop: pendingLoop,
2072
+ imageLoop: pendingImageLoop,
1915
2073
  fps: pendingFps,
1916
- startNumber: pendingStartNumber
2074
+ startNumber: pendingStartNumber,
2075
+ patternType: pendingPatternType,
2076
+ startNumberRange: pendingStartNumberRange
1917
2077
  });
1918
2078
  pendingFormat = void 0;
2079
+ pendingSafe = void 0;
1919
2080
  pendingSs = void 0;
1920
2081
  pendingTo = void 0;
1921
2082
  pendingDuration = void 0;
1922
2083
  pendingLoop = void 0;
2084
+ pendingImageLoop = void 0;
1923
2085
  pendingFps = void 0;
1924
2086
  pendingStartNumber = void 0;
2087
+ pendingStartNumberRange = void 0;
2088
+ pendingPatternType = void 0;
1925
2089
  } else if (arg === "-c" || arg === "-codec") {
1926
2090
  const val = args[++i];
1927
2091
  videoCodec = val;
@@ -1943,7 +2107,7 @@ function createFfmpegCommand(options = {}) {
1943
2107
  } else if (arg === "-shortest") {
1944
2108
  shortest = true;
1945
2109
  } else if (arg === "-frames:v" || arg === "-vframes") {
1946
- maxVideoFrames = parseInt(args[++i] ?? "1", 10) || 1;
2110
+ maxVideoFrames = parseInt(args[++i] ?? "1", 10);
1947
2111
  } else if (arg === "-frames:a" || arg === "-aframes") {
1948
2112
  const value = args[++i];
1949
2113
  maxAudioFrames = value?.trim() ? Number(value) : NaN;
@@ -2063,16 +2227,41 @@ function createFfmpegCommand(options = {}) {
2063
2227
  }
2064
2228
  return concatMp4(docs, { limits: options.limits, budget });
2065
2229
  }
2066
- 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) {
2067
2257
  const frames = [];
2068
2258
  const fps = fpsHint ?? 25;
2069
- 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++) {
2070
2262
  budget.checkCpu();
2071
2263
  context.signal.throwIfAborted();
2072
- const formatted = filePath.replace(/%0?(\d*)d/, (_, widthDigits) => {
2073
- const padLen = parseInt(widthDigits || "0", 10) || 0;
2074
- return String(idx).padStart(padLen, "0");
2075
- });
2264
+ const formatted = globPaths ? globPaths[idx] : formatImageSequence(filePath, idx);
2076
2265
  const resolved = resolvePath(context.cwd, formatted);
2077
2266
  let imgBytes;
2078
2267
  try {
@@ -2080,7 +2269,8 @@ function createFfmpegCommand(options = {}) {
2080
2269
  } catch (error) {
2081
2270
  rethrowRuntimeError(context, error);
2082
2271
  if (error.code !== "ENOENT") throw error;
2083
- if (idx === 0 && input?.startNumber === void 0) continue;
2272
+ if (globPaths) throw error;
2273
+ if (frames.length === 0 && idx - startNumber < startNumberRange - 1) continue;
2084
2274
  break;
2085
2275
  }
2086
2276
  {
@@ -2119,27 +2309,76 @@ function createFfmpegCommand(options = {}) {
2119
2309
  detachBytesBuffer(synthBytes);
2120
2310
  return {
2121
2311
  ...base,
2312
+ duration: Math.round(frames.length / fps * base.timescale),
2313
+ durationSeconds: frames.length / fps,
2122
2314
  tracks: base.tracks.map(
2123
- (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
2124
2316
  )
2125
2317
  };
2126
2318
  }
2127
2319
  const rawBytes = await readInput(filePath);
2128
- 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) {
2129
2322
  const listText = new TextDecoder().decode(rawBytes);
2130
2323
  const listDir = filePath === "-" ? context.cwd : dirnameOf(resolvePath(context.cwd, filePath));
2131
2324
  const concatDocs = [];
2325
+ const startTimesSeconds = [];
2326
+ let nextStartSeconds = 0;
2327
+ const segments = [];
2132
2328
  let currentInpoint;
2133
2329
  let currentOutpoint;
2134
2330
  let currentDuration;
2135
2331
  const lines = listText.split(/\r?\n/);
2136
2332
  for (let l = 0; l < lines.length; l++) {
2137
- const line = lines[l].trim();
2138
- if (!line || line.startsWith("#") || line.startsWith("ffconcat")) continue;
2139
- if (line.startsWith("file ")) {
2140
- let target2 = line.slice(5).trim();
2141
- if (target2.startsWith("'") && target2.endsWith("'") || target2.startsWith('"') && target2.endsWith('"')) {
2142
- target2 = target2.slice(1, -1);
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}'`);
2143
2382
  }
2144
2383
  const resolvedItem = resolvePath(listDir, target2);
2145
2384
  currentInpoint = void 0;
@@ -2147,32 +2386,66 @@ function createFfmpegCommand(options = {}) {
2147
2386
  currentDuration = void 0;
2148
2387
  while (l + 1 < lines.length) {
2149
2388
  const nextLine = lines[l + 1].trim();
2150
- if (nextLine.startsWith("inpoint ")) {
2151
- currentInpoint = parseFfmpegTimestamp(nextLine.slice(8));
2389
+ if (!nextLine || nextLine.startsWith("#")) {
2152
2390
  l++;
2153
- } else if (nextLine.startsWith("outpoint ")) {
2154
- currentOutpoint = parseFfmpegTimestamp(nextLine.slice(9));
2155
- l++;
2156
- } else if (nextLine.startsWith("duration ")) {
2157
- currentDuration = parseFfmpegTimestamp(nextLine.slice(9));
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;
2158
2404
  l++;
2159
2405
  } else {
2160
2406
  break;
2161
2407
  }
2162
2408
  }
2163
- let itemDoc = await loadSingleDocument(resolvedItem);
2164
- if (currentInpoint !== void 0 || currentOutpoint !== void 0 || currentDuration !== void 0) {
2165
- itemDoc = sliceMp4(itemDoc, {
2166
- startSeconds: currentInpoint,
2167
- endSeconds: currentOutpoint,
2168
- durationSeconds: currentDuration
2169
- });
2409
+ if (currentInpoint !== void 0 && currentOutpoint !== void 0 && currentOutpoint < currentInpoint) {
2410
+ throw new Error(`${resolvedItem}: outpoint must not precede inpoint`);
2170
2411
  }
2171
- 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}'`);
2415
+ }
2416
+ }
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;
2172
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);
2173
2447
  }
2174
- budget.checkConcatInputs(concatDocs.length);
2175
- return concatMp4(concatDocs, { limits: options.limits, budget });
2448
+ return concatMp4(concatDocs, { startTimesSeconds, limits: options.limits, budget });
2176
2449
  }
2177
2450
  const plugin = explicitFormat !== void 0 ? registry.findByFormatName(explicitFormat) : registry.detect(rawBytes, filePath);
2178
2451
  if (!plugin || !plugin.canDemux) {
@@ -2193,6 +2466,38 @@ function createFfmpegCommand(options = {}) {
2193
2466
  await yieldTurn5(context.signal);
2194
2467
  context.signal.throwIfAborted();
2195
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
+ }
2196
2501
  if (inp.startSeconds !== void 0 || inp.endSeconds !== void 0 || inp.durationSeconds !== void 0) {
2197
2502
  doc = sliceMp4(doc, {
2198
2503
  startSeconds: inp.startSeconds,
@@ -2631,7 +2936,7 @@ To ignore this, add a trailing '?' to the map.
2631
2936
  const origRate = audio.sampleRate;
2632
2937
  const origCh = audio.channels;
2633
2938
  const targetRate = audioRate ?? origRate;
2634
- const targetCh = audioChannels ?? origCh;
2939
+ const targetCh = audioChannels === -1 ? origCh : audioChannels ?? origCh;
2635
2940
  const channelMask = audio.channelMask;
2636
2941
  const surroundMono = origCh === 6 && targetCh === 1;
2637
2942
  const monoStereo = origCh === 1 && (targetCh === 2 || targetCh === 3);
@@ -2643,32 +2948,80 @@ To ignore this, add a trailing '?' to the map.
2643
2948
  const volFactor = t.volume ?? 1;
2644
2949
  let stretched = audio.channelData;
2645
2950
  if (filter) {
2646
- for (const item of filter.expression.split(",")) {
2951
+ for (const item of parseFilterChain(filter.expression)) {
2647
2952
  if (++work % 1024 === 0) {
2648
2953
  await yieldTurn5(context.signal);
2649
2954
  context.signal?.throwIfAborted();
2650
2955
  }
2651
- const trimmed = item.trim();
2652
- if (trimmed.startsWith("volume=")) {
2653
- 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");
2654
2981
  const decibels = volSpec.endsWith("dB");
2655
- const gain = parseFilterExpression(decibels ? volSpec.slice(0, -2) : volSpec, "volume", {});
2656
- const factor = decibels ? Math.pow(10, gain / 20) : gain;
2657
- if (!Number.isFinite(factor)) throw new Error("volume: invalid gain");
2658
- stretched = await mapWork(stretched, async (channel) => {
2659
- const output = new Float32Array(channel.length);
2660
- for (let i2 = 0; i2 < channel.length; i2++) {
2661
- if (++work % 1024 === 0) {
2662
- await yieldTurn5(context.signal);
2663
- context.signal?.throwIfAborted();
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);
2664
3016
  }
2665
- output[i2] = channel[i2] * factor;
2666
3017
  }
2667
- return output;
2668
- }, context.signal);
2669
- } else if (trimmed.startsWith("atempo=")) {
2670
- const tempo = Number(trimmed.slice(7));
2671
- if (!Number.isFinite(tempo) || tempo < 0.5 || tempo > 100) {
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) {
2672
3025
  throw new Error("atempo must be in the range [0.5, 100]");
2673
3026
  }
2674
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);
@@ -2876,10 +3229,15 @@ To ignore this, add a trailing '?' to the map.
2876
3229
  options.onMetrics?.(budget.getStats());
2877
3230
  return { exitCode: 0 };
2878
3231
  }
3232
+ if (imageOutput && maxVideoFrames === 0) {
3233
+ options.onMetrics?.(budget.getStats());
3234
+ return { exitCode: 0 };
3235
+ }
2879
3236
  if (imageOutput && !workingDoc.tracks.some((track) => track.type === "video" && track.decodedVideoFrames?.length)) {
2880
3237
  throw new Error("No video frame in the requested presentation interval");
2881
3238
  }
2882
- if (/%0?\d*d/.test(outputTarget2)) {
3239
+ const numberedImageOutput = formatImageSequence(outputTarget2, outputStartNumber ?? 1) !== void 0;
3240
+ if (imageOutput && !isStdout(outputTarget2) || numberedImageOutput) {
2883
3241
  const outExt2 = outputTarget2.split(".").pop()?.toLowerCase() ?? "png";
2884
3242
  const imgPlugin = registry.findByFilename(`x.${outExt2}`);
2885
3243
  if (!imgPlugin || !imgPlugin.canMux) {
@@ -2901,15 +3259,16 @@ To ignore this, add a trailing '?' to the map.
2901
3259
  const previousCopies = constantRate && delta0 > 1.1 ? Math.max(0, Math.round(delta0 - 0.6)) : 0;
2902
3260
  if (!Number.isSafeInteger(copies)) throw new Error("Invalid output frame timestamp");
2903
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
+ }
2904
3265
  budget.checkCpu();
2905
3266
  budget.checkSamples(idx + 1);
2906
3267
  budget.checkDuration((idx + 1) * duration);
2907
3268
  const f = copy < previousCopies && sourceIndex > 0 ? frames[sourceIndex - 1] : source;
2908
3269
  if (copy > 0) budget.recordFrame(f.width, f.height);
2909
- const fileName = outputTarget2.replace(/%0?(\d*)d/, (_, widthDigits) => {
2910
- const padLen = parseInt(widthDigits || "0", 10) || 0;
2911
- return String(idx + (outputStartNumber ?? 1)).padStart(padLen, "0");
2912
- });
3270
+ const number = framePts ? Math.round(constantRate ? nextPts : source.ptsSeconds / duration) : idx + (outputStartNumber ?? 1);
3271
+ const fileName = updateImage ? outputTarget2 : formatImageSequence(outputTarget2, number) ?? outputTarget2;
2913
3272
  const fullOutPath = resolvePath(context.cwd, fileName);
2914
3273
  const encoded = (await yieldTurn5(context.signal), context.signal?.throwIfAborted(), encodeImage(makeRgbaImg(f.width, f.height, f.data), {
2915
3274
  format: imgFmt
@@ -2985,7 +3344,7 @@ To ignore this, add a trailing '?' to the map.
2985
3344
  const result = await output();
2986
3345
  if (result.exitCode !== 0) return result;
2987
3346
  }
2988
- return { exitCode: 0 };
3347
+ return { exitCode: exitOnError && inputReadFailed ? 1 : 0 };
2989
3348
  }
2990
3349
  };
2991
3350
  }