@poe-platform/safe-bash 0.1.843 → 0.1.845

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