@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.
- 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-LORSYIAF.js → chunk-CSJHADNR.js} +31 -7
- 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-3SWIXYXT.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 +535 -88
- 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.d.ts +1 -0
- package/dist/safe-bash-command-imagemagick/index.js +895 -276
- 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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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")
|
|
1415
|
-
|
|
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 =
|
|
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)
|
|
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 (
|
|
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 =
|
|
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
|
|
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 === "-
|
|
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 =
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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].
|
|
2060
|
-
if (!line || line.startsWith("#")
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
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("
|
|
2073
|
-
currentInpoint = parseFfmpegTimestamp(nextLine.slice(8));
|
|
2389
|
+
if (!nextLine || nextLine.startsWith("#")) {
|
|
2074
2390
|
l++;
|
|
2075
|
-
} else if (nextLine.
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
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
|
-
|
|
2086
|
-
|
|
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
|
-
|
|
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(
|
|
2097
|
-
|
|
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
|
-
|
|
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
|
|
2559
|
-
let
|
|
2560
|
-
|
|
2561
|
-
|
|
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
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
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
|
|
2577
|
-
const
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2822
|
-
|
|
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
|
}
|