@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.
- package/dist/image-ast/ast.d.ts +12 -2
- package/dist/image-ast/browser/ast.d.ts +12 -2
- package/dist/image-ast/browser/codecs/exif-metadata.d.ts +2 -0
- package/dist/image-ast/browser/codecs/exif.d.ts +2 -0
- package/dist/image-ast/browser/codecs/gif-metadata-storage.d.ts +1 -0
- package/dist/image-ast/browser/codecs/gif-metadata.d.ts +1 -0
- package/dist/image-ast/browser/codecs/gif-output-parts.d.ts +1 -1
- package/dist/image-ast/browser/codecs/jpeg.d.ts +1 -1
- package/dist/image-ast/browser/codecs/netpbm-storage.d.ts +2 -2
- package/dist/image-ast/browser/codecs/netpbm.d.ts +4 -4
- package/dist/image-ast/browser/codecs/png-storage.d.ts +9 -1
- package/dist/image-ast/browser/codecs/png.d.ts +1 -0
- package/dist/image-ast/browser/codecs/tiff-layout.d.ts +3 -2
- package/dist/image-ast/codecs/exif-metadata.d.ts +2 -0
- package/dist/image-ast/codecs/exif.d.ts +2 -0
- package/dist/image-ast/codecs/gif-metadata-storage.d.ts +1 -0
- package/dist/image-ast/codecs/gif-metadata.d.ts +1 -0
- package/dist/image-ast/codecs/gif-output-parts.d.ts +1 -1
- package/dist/image-ast/codecs/jpeg.d.ts +1 -1
- package/dist/image-ast/codecs/netpbm-storage.d.ts +2 -2
- package/dist/image-ast/codecs/netpbm.d.ts +4 -4
- package/dist/image-ast/codecs/png-storage.d.ts +9 -1
- package/dist/image-ast/codecs/png.d.ts +1 -0
- package/dist/image-ast/codecs/tiff-layout.d.ts +3 -2
- package/dist/image-ast/index.js +554 -163
- package/dist/mp4-ast/types.d.ts +2 -0
- package/dist/safe-bash/command-chunks/{chunk-CMGVQM4Y.js → chunk-CSJHADNR.js} +17 -3
- package/dist/safe-bash/command-chunks/{chunk-KSCVKF7I.js → chunk-F42YLXD4.js} +3 -3
- package/dist/safe-bash/command-chunks/{chunk-2EEIB5O3.js → chunk-HUE2ATJY.js} +3 -3
- package/dist/safe-bash/command-chunks/{chunk-4KM2VJRB.js → chunk-NBQWCUOP.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-U5TQVW64.js → chunk-ZTEK2JJD.js} +1 -1
- package/dist/safe-bash-command-ffmpeg/index.js +433 -74
- package/dist/safe-bash-command-ffprobe/index.js +2 -2
- package/dist/safe-bash-command-ffprobe/media.js +2 -2
- package/dist/safe-bash-command-imagemagick/compare-file.d.ts +3 -2
- package/dist/safe-bash-command-imagemagick/index.js +835 -247
- package/dist/safe-bash-command-imagemagick/raw-output.d.ts +10 -0
- package/dist/safe-bash-command-python/executor.js +3 -3
- package/dist/safe-bash-command-python/index.js +3 -3
- package/dist/safe-bash-command-python/jspi.js +2 -2
- package/dist/safe-bash-command-python/worker.js +1 -1
- package/package.json +2 -2
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
sliceMp4,
|
|
24
24
|
srtAst,
|
|
25
25
|
webvttAst
|
|
26
|
-
} from "../safe-bash/command-chunks/chunk-
|
|
26
|
+
} from "../safe-bash/command-chunks/chunk-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
|
-
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
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 (
|
|
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 =
|
|
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
|
|
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 === "-
|
|
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 =
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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].
|
|
2138
|
-
if (!line || line.startsWith("#")
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
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("
|
|
2151
|
-
currentInpoint = parseFfmpegTimestamp(nextLine.slice(8));
|
|
2389
|
+
if (!nextLine || nextLine.startsWith("#")) {
|
|
2152
2390
|
l++;
|
|
2153
|
-
} else if (nextLine.
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
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
|
-
|
|
2164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
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
|
|
2656
|
-
const
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
const
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
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
|
-
|
|
2668
|
-
}
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
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
|
-
|
|
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
|
|
2910
|
-
|
|
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
|
}
|