node-av-lgpl 0.0.0 → 7.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +22 -0
- package/README.md +711 -0
- package/dist/api/audio-frame-buffer.d.ts +636 -0
- package/dist/api/audio-frame-buffer.js +1027 -0
- package/dist/api/audio-frame-buffer.js.map +1 -0
- package/dist/api/bitstream-filter.d.ts +938 -0
- package/dist/api/bitstream-filter.js +1427 -0
- package/dist/api/bitstream-filter.js.map +1 -0
- package/dist/api/constants.d.ts +44 -0
- package/dist/api/constants.js +45 -0
- package/dist/api/constants.js.map +1 -0
- package/dist/api/data/test_av1.ivf +0 -0
- package/dist/api/data/test_h264.h264 +0 -0
- package/dist/api/data/test_hevc.h265 +0 -0
- package/dist/api/data/test_mjpeg.mjpeg +0 -0
- package/dist/api/data/test_vp8.ivf +0 -0
- package/dist/api/data/test_vp9.ivf +0 -0
- package/dist/api/decoder.d.ts +1413 -0
- package/dist/api/decoder.js +2367 -0
- package/dist/api/decoder.js.map +1 -0
- package/dist/api/demuxer.d.ts +1295 -0
- package/dist/api/demuxer.js +2344 -0
- package/dist/api/demuxer.js.map +1 -0
- package/dist/api/device.d.ts +589 -0
- package/dist/api/device.js +964 -0
- package/dist/api/device.js.map +1 -0
- package/dist/api/encoder-pool.d.ts +262 -0
- package/dist/api/encoder-pool.js +397 -0
- package/dist/api/encoder-pool.js.map +1 -0
- package/dist/api/encoder.d.ts +1509 -0
- package/dist/api/encoder.js +2619 -0
- package/dist/api/encoder.js.map +1 -0
- package/dist/api/filter-complex.d.ts +828 -0
- package/dist/api/filter-complex.js +1661 -0
- package/dist/api/filter-complex.js.map +1 -0
- package/dist/api/filter-presets.d.ts +800 -0
- package/dist/api/filter-presets.js +1554 -0
- package/dist/api/filter-presets.js.map +1 -0
- package/dist/api/filter.d.ts +1339 -0
- package/dist/api/filter.js +2075 -0
- package/dist/api/filter.js.map +1 -0
- package/dist/api/fmp4-stream.d.ts +797 -0
- package/dist/api/fmp4-stream.js +1428 -0
- package/dist/api/fmp4-stream.js.map +1 -0
- package/dist/api/hardware.d.ts +677 -0
- package/dist/api/hardware.js +1300 -0
- package/dist/api/hardware.js.map +1 -0
- package/dist/api/index.d.ts +21 -0
- package/dist/api/index.js +38 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/io-stream.d.ts +308 -0
- package/dist/api/io-stream.js +358 -0
- package/dist/api/io-stream.js.map +1 -0
- package/dist/api/muxer.d.ts +1311 -0
- package/dist/api/muxer.js +2672 -0
- package/dist/api/muxer.js.map +1 -0
- package/dist/api/pipeline.d.ts +801 -0
- package/dist/api/pipeline.js +1344 -0
- package/dist/api/pipeline.js.map +1 -0
- package/dist/api/probe.d.ts +128 -0
- package/dist/api/probe.js +227 -0
- package/dist/api/probe.js.map +1 -0
- package/dist/api/rtp-stream.d.ts +497 -0
- package/dist/api/rtp-stream.js +1131 -0
- package/dist/api/rtp-stream.js.map +1 -0
- package/dist/api/scaler.d.ts +561 -0
- package/dist/api/scaler.js +918 -0
- package/dist/api/scaler.js.map +1 -0
- package/dist/api/types.d.ts +17 -0
- package/dist/api/types.js +2 -0
- package/dist/api/types.js.map +1 -0
- package/dist/api/utilities/async-queue.d.ts +149 -0
- package/dist/api/utilities/async-queue.js +254 -0
- package/dist/api/utilities/async-queue.js.map +1 -0
- package/dist/api/utilities/audio-sample.d.ts +117 -0
- package/dist/api/utilities/audio-sample.js +112 -0
- package/dist/api/utilities/audio-sample.js.map +1 -0
- package/dist/api/utilities/audio-transcode.d.ts +20 -0
- package/dist/api/utilities/audio-transcode.js +27 -0
- package/dist/api/utilities/audio-transcode.js.map +1 -0
- package/dist/api/utilities/channel-layout.d.ts +76 -0
- package/dist/api/utilities/channel-layout.js +80 -0
- package/dist/api/utilities/channel-layout.js.map +1 -0
- package/dist/api/utilities/codec-format.d.ts +87 -0
- package/dist/api/utilities/codec-format.js +117 -0
- package/dist/api/utilities/codec-format.js.map +1 -0
- package/dist/api/utilities/context-options.d.ts +25 -0
- package/dist/api/utilities/context-options.js +23 -0
- package/dist/api/utilities/context-options.js.map +1 -0
- package/dist/api/utilities/electron-shared-texture.d.ts +368 -0
- package/dist/api/utilities/electron-shared-texture.js +540 -0
- package/dist/api/utilities/electron-shared-texture.js.map +1 -0
- package/dist/api/utilities/image.d.ts +207 -0
- package/dist/api/utilities/image.js +213 -0
- package/dist/api/utilities/image.js.map +1 -0
- package/dist/api/utilities/index.d.ts +14 -0
- package/dist/api/utilities/index.js +27 -0
- package/dist/api/utilities/index.js.map +1 -0
- package/dist/api/utilities/live-decoding.d.ts +15 -0
- package/dist/api/utilities/live-decoding.js +32 -0
- package/dist/api/utilities/live-decoding.js.map +1 -0
- package/dist/api/utilities/media-type.d.ts +49 -0
- package/dist/api/utilities/media-type.js +53 -0
- package/dist/api/utilities/media-type.js.map +1 -0
- package/dist/api/utilities/packet-age-watchdog.d.ts +198 -0
- package/dist/api/utilities/packet-age-watchdog.js +594 -0
- package/dist/api/utilities/packet-age-watchdog.js.map +1 -0
- package/dist/api/utilities/pixel-format.d.ts +89 -0
- package/dist/api/utilities/pixel-format.js +97 -0
- package/dist/api/utilities/pixel-format.js.map +1 -0
- package/dist/api/utilities/sample-format.d.ts +129 -0
- package/dist/api/utilities/sample-format.js +141 -0
- package/dist/api/utilities/sample-format.js.map +1 -0
- package/dist/api/utilities/scale-size.d.ts +51 -0
- package/dist/api/utilities/scale-size.js +55 -0
- package/dist/api/utilities/scale-size.js.map +1 -0
- package/dist/api/utilities/scheduler.d.ts +138 -0
- package/dist/api/utilities/scheduler.js +98 -0
- package/dist/api/utilities/scheduler.js.map +1 -0
- package/dist/api/utilities/streaming.d.ts +186 -0
- package/dist/api/utilities/streaming.js +309 -0
- package/dist/api/utilities/streaming.js.map +1 -0
- package/dist/api/utilities/timestamp.d.ts +193 -0
- package/dist/api/utilities/timestamp.js +206 -0
- package/dist/api/utilities/timestamp.js.map +1 -0
- package/dist/api/utilities/video-transcode.d.ts +218 -0
- package/dist/api/utilities/video-transcode.js +624 -0
- package/dist/api/utilities/video-transcode.js.map +1 -0
- package/dist/api/utilities/whisper-model.d.ts +330 -0
- package/dist/api/utilities/whisper-model.js +587 -0
- package/dist/api/utilities/whisper-model.js.map +1 -0
- package/dist/api/utils.d.ts +19 -0
- package/dist/api/utils.js +39 -0
- package/dist/api/utils.js.map +1 -0
- package/dist/api/webrtc-stream.d.ts +294 -0
- package/dist/api/webrtc-stream.js +546 -0
- package/dist/api/webrtc-stream.js.map +1 -0
- package/dist/api/whisper.d.ts +326 -0
- package/dist/api/whisper.js +402 -0
- package/dist/api/whisper.js.map +1 -0
- package/dist/constants/bsf-options.d.ts +364 -0
- package/dist/constants/bsf-options.js +7 -0
- package/dist/constants/bsf-options.js.map +1 -0
- package/dist/constants/channel-layouts.d.ts +53 -0
- package/dist/constants/channel-layouts.js +57 -0
- package/dist/constants/channel-layouts.js.map +1 -0
- package/dist/constants/constants.d.ts +2435 -0
- package/dist/constants/constants.js +1989 -0
- package/dist/constants/constants.js.map +1 -0
- package/dist/constants/decoders.d.ts +651 -0
- package/dist/constants/decoders.js +641 -0
- package/dist/constants/decoders.js.map +1 -0
- package/dist/constants/encoders.d.ts +314 -0
- package/dist/constants/encoders.js +309 -0
- package/dist/constants/encoders.js.map +1 -0
- package/dist/constants/filter-options.d.ts +12099 -0
- package/dist/constants/filter-options.js +7 -0
- package/dist/constants/filter-options.js.map +1 -0
- package/dist/constants/format-options.d.ts +4319 -0
- package/dist/constants/format-options.js +7 -0
- package/dist/constants/format-options.js.map +1 -0
- package/dist/constants/formats.d.ts +18 -0
- package/dist/constants/formats.js +7 -0
- package/dist/constants/formats.js.map +1 -0
- package/dist/constants/hardware.d.ts +26 -0
- package/dist/constants/hardware.js +27 -0
- package/dist/constants/hardware.js.map +1 -0
- package/dist/constants/index.d.ts +10 -0
- package/dist/constants/index.js +6 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/constants/options.d.ts +6217 -0
- package/dist/constants/options.js +7 -0
- package/dist/constants/options.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +101 -0
- package/dist/ffmpeg/index.js +161 -0
- package/dist/ffmpeg/index.js.map +1 -0
- package/dist/ffmpeg/version.d.ts +6 -0
- package/dist/ffmpeg/version.js +7 -0
- package/dist/ffmpeg/version.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/audio-fifo.d.ts +399 -0
- package/dist/lib/audio-fifo.js +431 -0
- package/dist/lib/audio-fifo.js.map +1 -0
- package/dist/lib/binding.d.ts +242 -0
- package/dist/lib/binding.js +102 -0
- package/dist/lib/binding.js.map +1 -0
- package/dist/lib/bitstream-filter-context.d.ts +381 -0
- package/dist/lib/bitstream-filter-context.js +443 -0
- package/dist/lib/bitstream-filter-context.js.map +1 -0
- package/dist/lib/bitstream-filter.d.ts +140 -0
- package/dist/lib/bitstream-filter.js +154 -0
- package/dist/lib/bitstream-filter.js.map +1 -0
- package/dist/lib/codec-context.d.ts +1069 -0
- package/dist/lib/codec-context.js +1336 -0
- package/dist/lib/codec-context.js.map +1 -0
- package/dist/lib/codec-parameters.d.ts +664 -0
- package/dist/lib/codec-parameters.js +813 -0
- package/dist/lib/codec-parameters.js.map +1 -0
- package/dist/lib/codec-parser.d.ts +201 -0
- package/dist/lib/codec-parser.js +213 -0
- package/dist/lib/codec-parser.js.map +1 -0
- package/dist/lib/codec.d.ts +617 -0
- package/dist/lib/codec.js +746 -0
- package/dist/lib/codec.js.map +1 -0
- package/dist/lib/device.d.ts +294 -0
- package/dist/lib/device.js +326 -0
- package/dist/lib/device.js.map +1 -0
- package/dist/lib/dictionary.d.ts +333 -0
- package/dist/lib/dictionary.js +372 -0
- package/dist/lib/dictionary.js.map +1 -0
- package/dist/lib/error.d.ts +311 -0
- package/dist/lib/error.js +395 -0
- package/dist/lib/error.js.map +1 -0
- package/dist/lib/fifo.d.ts +416 -0
- package/dist/lib/fifo.js +453 -0
- package/dist/lib/fifo.js.map +1 -0
- package/dist/lib/filter-context.d.ts +714 -0
- package/dist/lib/filter-context.js +783 -0
- package/dist/lib/filter-context.js.map +1 -0
- package/dist/lib/filter-graph-segment.d.ts +161 -0
- package/dist/lib/filter-graph-segment.js +172 -0
- package/dist/lib/filter-graph-segment.js.map +1 -0
- package/dist/lib/filter-graph.d.ts +641 -0
- package/dist/lib/filter-graph.js +704 -0
- package/dist/lib/filter-graph.js.map +1 -0
- package/dist/lib/filter-inout.d.ts +202 -0
- package/dist/lib/filter-inout.js +261 -0
- package/dist/lib/filter-inout.js.map +1 -0
- package/dist/lib/filter.d.ts +243 -0
- package/dist/lib/filter.js +272 -0
- package/dist/lib/filter.js.map +1 -0
- package/dist/lib/format-context.d.ts +1335 -0
- package/dist/lib/format-context.js +1455 -0
- package/dist/lib/format-context.js.map +1 -0
- package/dist/lib/frame-utils.d.ts +116 -0
- package/dist/lib/frame-utils.js +98 -0
- package/dist/lib/frame-utils.js.map +1 -0
- package/dist/lib/frame.d.ts +1315 -0
- package/dist/lib/frame.js +1535 -0
- package/dist/lib/frame.js.map +1 -0
- package/dist/lib/hardware-device-context.d.ts +362 -0
- package/dist/lib/hardware-device-context.js +383 -0
- package/dist/lib/hardware-device-context.js.map +1 -0
- package/dist/lib/hardware-frames-context.d.ts +418 -0
- package/dist/lib/hardware-frames-context.js +471 -0
- package/dist/lib/hardware-frames-context.js.map +1 -0
- package/dist/lib/index.d.ts +35 -0
- package/dist/lib/index.js +60 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/input-format.d.ts +249 -0
- package/dist/lib/input-format.js +306 -0
- package/dist/lib/input-format.js.map +1 -0
- package/dist/lib/io-context.d.ts +698 -0
- package/dist/lib/io-context.js +771 -0
- package/dist/lib/io-context.js.map +1 -0
- package/dist/lib/log.d.ts +174 -0
- package/dist/lib/log.js +184 -0
- package/dist/lib/log.js.map +1 -0
- package/dist/lib/native-types.d.ts +1024 -0
- package/dist/lib/native-types.js +2 -0
- package/dist/lib/native-types.js.map +1 -0
- package/dist/lib/option.d.ts +953 -0
- package/dist/lib/option.js +1607 -0
- package/dist/lib/option.js.map +1 -0
- package/dist/lib/output-format.d.ts +222 -0
- package/dist/lib/output-format.js +259 -0
- package/dist/lib/output-format.js.map +1 -0
- package/dist/lib/packet.d.ts +521 -0
- package/dist/lib/packet.js +613 -0
- package/dist/lib/packet.js.map +1 -0
- package/dist/lib/rational.d.ts +251 -0
- package/dist/lib/rational.js +278 -0
- package/dist/lib/rational.js.map +1 -0
- package/dist/lib/software-resample-context.d.ts +552 -0
- package/dist/lib/software-resample-context.js +592 -0
- package/dist/lib/software-resample-context.js.map +1 -0
- package/dist/lib/software-scale-context.d.ts +344 -0
- package/dist/lib/software-scale-context.js +366 -0
- package/dist/lib/software-scale-context.js.map +1 -0
- package/dist/lib/stream.d.ts +383 -0
- package/dist/lib/stream.js +524 -0
- package/dist/lib/stream.js.map +1 -0
- package/dist/lib/sync-queue.d.ts +179 -0
- package/dist/lib/sync-queue.js +197 -0
- package/dist/lib/sync-queue.js.map +1 -0
- package/dist/lib/types.d.ts +34 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/utilities.d.ts +1198 -0
- package/dist/lib/utilities.js +1302 -0
- package/dist/lib/utilities.js.map +1 -0
- package/dist/utils/electron.d.ts +49 -0
- package/dist/utils/electron.js +63 -0
- package/dist/utils/electron.js.map +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/libc.d.ts +11 -0
- package/dist/utils/libc.js +35 -0
- package/dist/utils/libc.js.map +1 -0
- package/dist/utils/variant.d.ts +26 -0
- package/dist/utils/variant.js +31 -0
- package/dist/utils/variant.js.map +1 -0
- package/dist/webrtc/index.d.ts +4 -0
- package/dist/webrtc/index.js +7 -0
- package/dist/webrtc/index.js.map +1 -0
- package/install/check.js +124 -0
- package/install/ffmpeg.js +66 -0
- package/package.json +165 -3
|
@@ -0,0 +1,2344 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
import { createSocket } from 'dgram';
|
|
54
|
+
import { closeSync, openSync, readSync } from 'fs';
|
|
55
|
+
import { open } from 'fs/promises';
|
|
56
|
+
import { Readable } from 'node:stream';
|
|
57
|
+
import { resolve } from 'path';
|
|
58
|
+
import { AV_NOPTS_VALUE, AV_PIX_FMT_NONE, AV_ROUND_NEAR_INF, AV_ROUND_PASS_MINMAX, AV_TIME_BASE, AV_TIME_BASE_Q, AVERROR_EOF, AVERROR_EXIT, AVFLAG_NONE, AVFMT_FLAG_CUSTOM_IO, AVFMT_FLAG_NONBLOCK, AVFMT_TS_DISCONT, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, AVSEEK_CUR, AVSEEK_END, AVSEEK_SET, } from '../constants/constants.js';
|
|
59
|
+
import { Dictionary } from '../lib/dictionary.js';
|
|
60
|
+
import { AVERROR_EAGAIN, FFmpegError } from '../lib/error.js';
|
|
61
|
+
import { FormatContext } from '../lib/format-context.js';
|
|
62
|
+
import { InputFormat } from '../lib/input-format.js';
|
|
63
|
+
import { IOContext } from '../lib/io-context.js';
|
|
64
|
+
import { Packet } from '../lib/packet.js';
|
|
65
|
+
import { Rational } from '../lib/rational.js';
|
|
66
|
+
import { avGetPixFmtName, avGetSampleFmtName, avRescaleQ, avRescaleQRnd, dtsPredict as nativeDtsPredict } from '../lib/utilities.js';
|
|
67
|
+
import { DELTA_THRESHOLD, DTS_ERROR_THRESHOLD, IO_BUFFER_SIZE, MAX_INPUT_QUEUE_SIZE } from './constants.js';
|
|
68
|
+
import { IOStream } from './io-stream.js';
|
|
69
|
+
import { StreamingUtils } from './utilities/streaming.js';
|
|
70
|
+
// Scratch buffer for Atomics.wait(), created on first use: Electron renderers
|
|
71
|
+
// without cross-origin isolation have no SharedArrayBuffer, so creating it at
|
|
72
|
+
// import time would make the whole module fail to load there.
|
|
73
|
+
let syncSleepSignal;
|
|
74
|
+
/**
|
|
75
|
+
* Block the calling thread without spinning the CPU.
|
|
76
|
+
*
|
|
77
|
+
* Gives packetsSync a blocking sleep for EAGAIN retries (mirrors FFmpeg CLI's av_usleep()).
|
|
78
|
+
*
|
|
79
|
+
* @param ms - Sleep duration in milliseconds
|
|
80
|
+
*
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
function sleepSync(ms) {
|
|
84
|
+
syncSleepSignal ??= new Int32Array(new SharedArrayBuffer(4));
|
|
85
|
+
Atomics.wait(syncSleepSignal, 0, 0, ms);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Keep a timer from holding the process open.
|
|
89
|
+
*
|
|
90
|
+
* Electron renderers use browser timers, where setTimeout returns a number without unref().
|
|
91
|
+
*
|
|
92
|
+
* @param timer - Timer handle returned by setTimeout
|
|
93
|
+
*
|
|
94
|
+
* @internal
|
|
95
|
+
*/
|
|
96
|
+
function unrefTimer(timer) {
|
|
97
|
+
if (typeof timer === 'object') {
|
|
98
|
+
timer.unref();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* High-level demuxer for reading and demuxing media files.
|
|
103
|
+
*
|
|
104
|
+
* Provides simplified access to media streams, packets, and metadata.
|
|
105
|
+
* Handles file opening, format detection, and stream information extraction.
|
|
106
|
+
* Supports files, URLs, buffers, and raw data input with automatic cleanup.
|
|
107
|
+
* Essential component for media processing pipelines and transcoding.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* import { Demuxer } from 'node-av/api';
|
|
112
|
+
*
|
|
113
|
+
* // Open media file
|
|
114
|
+
* await using input = await Demuxer.open('video.mp4');
|
|
115
|
+
* console.log(`Format: ${input.formatName}`);
|
|
116
|
+
* console.log(`Duration: ${input.duration}s`);
|
|
117
|
+
*
|
|
118
|
+
* // Process packets
|
|
119
|
+
* for await (const packet of input.packets()) {
|
|
120
|
+
* console.log(`Packet from stream ${packet.streamIndex}`);
|
|
121
|
+
* packet.free();
|
|
122
|
+
* }
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* // From buffer
|
|
128
|
+
* const buffer = await fs.readFile('video.mp4');
|
|
129
|
+
* await using input = await Demuxer.open(buffer);
|
|
130
|
+
*
|
|
131
|
+
* // Access streams
|
|
132
|
+
* const videoStream = input.video();
|
|
133
|
+
* const audioStream = input.audio();
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* @see {@link Muxer} For writing media files
|
|
137
|
+
* @see {@link Decoder} For decoding packets to frames
|
|
138
|
+
* @see {@link FormatContext} For low-level API
|
|
139
|
+
*/
|
|
140
|
+
export class Demuxer {
|
|
141
|
+
formatContext;
|
|
142
|
+
_streams = [];
|
|
143
|
+
ioContext;
|
|
144
|
+
externalIO = false; // ioContext was supplied by the caller - may own a protocol handle from open2()
|
|
145
|
+
isClosed = false;
|
|
146
|
+
options;
|
|
147
|
+
// Timestamp processing state (per-stream)
|
|
148
|
+
streamStates = new Map();
|
|
149
|
+
// Timestamp discontinuity tracking (global)
|
|
150
|
+
tsOffsetDiscont = 0n;
|
|
151
|
+
lastTs = AV_NOPTS_VALUE;
|
|
152
|
+
// Demux manager for handling multiple parallel packet generators
|
|
153
|
+
activeGenerators = 0;
|
|
154
|
+
demuxThread = null;
|
|
155
|
+
packetQueues = new Map(); // streamIndex or 'all' -> queue
|
|
156
|
+
packetQueueConsumers = new Map(); // active consumer count per queue key
|
|
157
|
+
queueResolvers = new Map(); // Promise resolvers for waiting consumers (multiple per key)
|
|
158
|
+
drainResolver = null; // Promise resolver for the demux thread parked on a full queue
|
|
159
|
+
demuxThreadActive = false;
|
|
160
|
+
demuxEof = false;
|
|
161
|
+
lastError = null; // read error that ended the demux loop (null = clean EOF)
|
|
162
|
+
signal;
|
|
163
|
+
signalCleanup;
|
|
164
|
+
/**
|
|
165
|
+
* @param formatContext - Opened format context
|
|
166
|
+
*
|
|
167
|
+
* @param options - Media input options
|
|
168
|
+
*
|
|
169
|
+
* @param ioContext - Optional IO context for custom I/O (e.g., from Buffer)
|
|
170
|
+
*
|
|
171
|
+
* @param externalIO - Whether ioContext was supplied by the caller instead of built here
|
|
172
|
+
*
|
|
173
|
+
* @internal
|
|
174
|
+
*/
|
|
175
|
+
constructor(formatContext, options, ioContext, externalIO = false) {
|
|
176
|
+
this.formatContext = formatContext;
|
|
177
|
+
this.ioContext = ioContext;
|
|
178
|
+
this.externalIO = externalIO;
|
|
179
|
+
this._streams = formatContext.streams ?? [];
|
|
180
|
+
this.options = options;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Probe media format without fully opening the file.
|
|
184
|
+
*
|
|
185
|
+
* Detects format by analyzing file headers and content.
|
|
186
|
+
* Useful for format validation before processing.
|
|
187
|
+
*
|
|
188
|
+
* Direct mapping to av_probe_input_format().
|
|
189
|
+
*
|
|
190
|
+
* @param input - File path or buffer to probe
|
|
191
|
+
*
|
|
192
|
+
* @returns Format information or null if unrecognized
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* const info = await Demuxer.probeFormat('video.mp4');
|
|
197
|
+
* if (info) {
|
|
198
|
+
* console.log(`Format: ${info.format}`);
|
|
199
|
+
* console.log(`Confidence: ${info.confidence}%`);
|
|
200
|
+
* }
|
|
201
|
+
* ```
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```typescript
|
|
205
|
+
* // Probe from buffer
|
|
206
|
+
* const buffer = await fs.readFile('video.webm');
|
|
207
|
+
* const info = await Demuxer.probeFormat(buffer);
|
|
208
|
+
* console.log(`MIME type: ${info?.mimeType}`);
|
|
209
|
+
* ```
|
|
210
|
+
*
|
|
211
|
+
* @see {@link InputFormat.probe} For low-level probing
|
|
212
|
+
*/
|
|
213
|
+
static async probeFormat(input) {
|
|
214
|
+
try {
|
|
215
|
+
if (Buffer.isBuffer(input)) {
|
|
216
|
+
// Probe from buffer
|
|
217
|
+
const format = InputFormat.probe(input);
|
|
218
|
+
if (!format) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
format: format.name ?? 'unknown',
|
|
223
|
+
longName: format.longName ?? undefined,
|
|
224
|
+
extensions: format.extensions ?? undefined,
|
|
225
|
+
mimeType: format.mimeType ?? undefined,
|
|
226
|
+
confidence: 100, // Direct probe always has high confidence
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
// For files, read first part and probe
|
|
231
|
+
let fileHandle;
|
|
232
|
+
try {
|
|
233
|
+
fileHandle = await open(input, 'r');
|
|
234
|
+
// Read first 64KB for probing
|
|
235
|
+
const buffer = Buffer.alloc(65536);
|
|
236
|
+
const { bytesRead } = await fileHandle.read(buffer, 0, 65536, 0);
|
|
237
|
+
const probeBuffer = buffer.subarray(0, bytesRead);
|
|
238
|
+
const format = InputFormat.probe(probeBuffer, input);
|
|
239
|
+
if (!format) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
format: format.name ?? 'unknown',
|
|
244
|
+
longName: format.longName ?? undefined,
|
|
245
|
+
extensions: format.extensions ?? undefined,
|
|
246
|
+
mimeType: format.mimeType ?? undefined,
|
|
247
|
+
confidence: 90, // File-based probe with filename hint
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// If file reading fails, return null
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
await fileHandle?.close();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Probe media format without fully opening the file synchronously.
|
|
265
|
+
* Synchronous version of probeFormat.
|
|
266
|
+
*
|
|
267
|
+
* Detects format by analyzing file headers and content.
|
|
268
|
+
* Useful for format validation before processing.
|
|
269
|
+
*
|
|
270
|
+
* Direct mapping to av_probe_input_format().
|
|
271
|
+
*
|
|
272
|
+
* @param input - File path or buffer to probe
|
|
273
|
+
*
|
|
274
|
+
* @returns Format information or null if unrecognized
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```typescript
|
|
278
|
+
* const info = Demuxer.probeFormatSync('video.mp4');
|
|
279
|
+
* if (info) {
|
|
280
|
+
* console.log(`Format: ${info.format}`);
|
|
281
|
+
* console.log(`Confidence: ${info.confidence}%`);
|
|
282
|
+
* }
|
|
283
|
+
* ```
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* ```typescript
|
|
287
|
+
* // Probe from buffer
|
|
288
|
+
* const buffer = fs.readFileSync('video.webm');
|
|
289
|
+
* const info = Demuxer.probeFormatSync(buffer);
|
|
290
|
+
* console.log(`MIME type: ${info?.mimeType}`);
|
|
291
|
+
* ```
|
|
292
|
+
*
|
|
293
|
+
* @see {@link probeFormat} For async version
|
|
294
|
+
*/
|
|
295
|
+
static probeFormatSync(input) {
|
|
296
|
+
try {
|
|
297
|
+
if (Buffer.isBuffer(input)) {
|
|
298
|
+
// Probe from buffer
|
|
299
|
+
const format = InputFormat.probe(input);
|
|
300
|
+
if (!format) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
format: format.name ?? 'unknown',
|
|
305
|
+
longName: format.longName ?? undefined,
|
|
306
|
+
extensions: format.extensions ?? undefined,
|
|
307
|
+
mimeType: format.mimeType ?? undefined,
|
|
308
|
+
confidence: 100, // Direct probe always has high confidence
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
// For files, read first part and probe
|
|
313
|
+
let fd;
|
|
314
|
+
try {
|
|
315
|
+
fd = openSync(input, 'r');
|
|
316
|
+
// Read first 64KB for probing
|
|
317
|
+
const buffer = Buffer.alloc(65536);
|
|
318
|
+
const bytesRead = readSync(fd, buffer, 0, 65536, 0);
|
|
319
|
+
const probeBuffer = buffer.subarray(0, bytesRead);
|
|
320
|
+
const format = InputFormat.probe(probeBuffer, input);
|
|
321
|
+
if (!format) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
format: format.name ?? 'unknown',
|
|
326
|
+
longName: format.longName ?? undefined,
|
|
327
|
+
extensions: format.extensions ?? undefined,
|
|
328
|
+
mimeType: format.mimeType ?? undefined,
|
|
329
|
+
confidence: 90, // File-based probe with filename hint
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// If file reading fails, return null
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
if (fd !== undefined)
|
|
338
|
+
closeSync(fd);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
static async open(input, options = {}) {
|
|
347
|
+
// Check if input is raw data
|
|
348
|
+
if (typeof input === 'object' && 'type' in input && ('width' in input || 'sampleRate' in input)) {
|
|
349
|
+
// Build options for raw data
|
|
350
|
+
const rawOptions = {
|
|
351
|
+
bufferSize: options.bufferSize,
|
|
352
|
+
format: options.format ?? (input.type === 'video' ? 'rawvideo' : 's16le'),
|
|
353
|
+
options: {
|
|
354
|
+
...options.options,
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
if (input.type === 'video') {
|
|
358
|
+
rawOptions.options = {
|
|
359
|
+
...rawOptions.options,
|
|
360
|
+
video_size: `${input.width}x${input.height}`,
|
|
361
|
+
pixel_format: avGetPixFmtName(input.pixelFormat) ?? 'yuv420p',
|
|
362
|
+
framerate: new Rational(input.frameRate.num, input.frameRate.den).toString(),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
rawOptions.options = {
|
|
367
|
+
...rawOptions.options,
|
|
368
|
+
sample_rate: input.sampleRate,
|
|
369
|
+
channels: input.channels,
|
|
370
|
+
sample_fmt: avGetSampleFmtName(input.sampleFormat) ?? 's16le',
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
input = input.input;
|
|
374
|
+
options = rawOptions;
|
|
375
|
+
}
|
|
376
|
+
// Original implementation for non-raw data
|
|
377
|
+
const formatContext = new FormatContext();
|
|
378
|
+
let ioContext;
|
|
379
|
+
let externalIO = false; // caller-supplied context - released via dispose, not freeContext
|
|
380
|
+
let optionsDict = null;
|
|
381
|
+
let inputFormat = null;
|
|
382
|
+
// Abort during the open phase: avformat_open_input()/find_stream_info()
|
|
383
|
+
// block for seconds on an unresponsive network source, so the signal must
|
|
384
|
+
// fire the native interrupt callback to unblock them.
|
|
385
|
+
const onOpenAbort = () => formatContext.interrupt();
|
|
386
|
+
if (options.signal) {
|
|
387
|
+
options.signal.throwIfAborted();
|
|
388
|
+
options.signal.addEventListener('abort', onOpenAbort, { once: true });
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
// Create options dictionary if options are provided
|
|
392
|
+
if (options.options) {
|
|
393
|
+
optionsDict = Dictionary.fromObject(options.options);
|
|
394
|
+
}
|
|
395
|
+
// Find input format if specified
|
|
396
|
+
if (options.format) {
|
|
397
|
+
inputFormat = InputFormat.findInputFormat(options.format);
|
|
398
|
+
if (!inputFormat) {
|
|
399
|
+
throw new Error(`Input format '${options.format}' not found`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (typeof input === 'string') {
|
|
403
|
+
// File path or URL - resolve relative paths to absolute
|
|
404
|
+
// Skip path resolution for device inputs
|
|
405
|
+
// Check if it's a URL (starts with protocol://) or a file path
|
|
406
|
+
const isUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(input);
|
|
407
|
+
const resolvedInput = isUrl || inputFormat ? input : resolve(input);
|
|
408
|
+
const ret = await formatContext.openInput(resolvedInput, inputFormat, optionsDict);
|
|
409
|
+
FFmpegError.throwIfError(ret, 'Failed to open input');
|
|
410
|
+
formatContext.setFlags(AVFMT_FLAG_NONBLOCK);
|
|
411
|
+
}
|
|
412
|
+
else if (Buffer.isBuffer(input)) {
|
|
413
|
+
// Validate buffer is not empty
|
|
414
|
+
if (input.length === 0) {
|
|
415
|
+
throw new Error('Cannot open media from empty buffer');
|
|
416
|
+
}
|
|
417
|
+
// From buffer - allocate context first for custom I/O
|
|
418
|
+
formatContext.allocContext();
|
|
419
|
+
ioContext = IOStream.create(input, { bufferSize: options.bufferSize });
|
|
420
|
+
formatContext.pb = ioContext;
|
|
421
|
+
const ret = await formatContext.openInput('', inputFormat, optionsDict);
|
|
422
|
+
FFmpegError.throwIfError(ret, 'Failed to open input from buffer');
|
|
423
|
+
}
|
|
424
|
+
else if (input instanceof IOContext) {
|
|
425
|
+
if (!options.format) {
|
|
426
|
+
throw new Error('Format must be specified for native IOContext input');
|
|
427
|
+
}
|
|
428
|
+
formatContext.allocContext();
|
|
429
|
+
ioContext = input;
|
|
430
|
+
externalIO = true;
|
|
431
|
+
formatContext.pb = ioContext;
|
|
432
|
+
formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
433
|
+
const ret = await formatContext.openInput('', inputFormat, optionsDict);
|
|
434
|
+
FFmpegError.throwIfError(ret, 'Failed to open input from native IOContext');
|
|
435
|
+
}
|
|
436
|
+
else if (input instanceof Readable) {
|
|
437
|
+
// Readable stream - format is required
|
|
438
|
+
if (!options.format) {
|
|
439
|
+
throw new Error('Format must be specified for Readable stream input');
|
|
440
|
+
}
|
|
441
|
+
formatContext.allocContext();
|
|
442
|
+
ioContext = IOStream.create(input, { bufferSize: options.bufferSize });
|
|
443
|
+
formatContext.pb = ioContext;
|
|
444
|
+
formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
445
|
+
const ret = await formatContext.openInput('', inputFormat, optionsDict);
|
|
446
|
+
FFmpegError.throwIfError(ret, 'Failed to open input from Readable stream');
|
|
447
|
+
}
|
|
448
|
+
else if (typeof input === 'object' && 'read' in input) {
|
|
449
|
+
// Custom I/O with callbacks - format is required
|
|
450
|
+
if (!options.format) {
|
|
451
|
+
throw new Error('Format must be specified for custom I/O');
|
|
452
|
+
}
|
|
453
|
+
// Allocate context first for custom I/O
|
|
454
|
+
formatContext.allocContext();
|
|
455
|
+
// Setup custom I/O with callbacks
|
|
456
|
+
ioContext = new IOContext();
|
|
457
|
+
ioContext.allocContextWithCallbacks(options.bufferSize ?? IO_BUFFER_SIZE, 0, input.read, null, input.seek);
|
|
458
|
+
formatContext.pb = ioContext;
|
|
459
|
+
formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
460
|
+
const ret = await formatContext.openInput('', inputFormat, optionsDict);
|
|
461
|
+
FFmpegError.throwIfError(ret, 'Failed to open input from custom I/O');
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
throw new TypeError('Invalid input type. Expected file path, URL, Buffer, IOContext, Readable, or IOInputCallbacks');
|
|
465
|
+
}
|
|
466
|
+
// Find stream information
|
|
467
|
+
if (!options.skipStreamInfo) {
|
|
468
|
+
const ret = await formatContext.findStreamInfo(null);
|
|
469
|
+
FFmpegError.throwIfError(ret, 'Failed to find stream info');
|
|
470
|
+
// Try to parse extradata for video streams with missing dimensions
|
|
471
|
+
for (const stream of formatContext.streams ?? []) {
|
|
472
|
+
if (stream.codecpar.codecType === AVMEDIA_TYPE_VIDEO) {
|
|
473
|
+
const dimensionsMissing = stream.codecpar.width === 0 || stream.codecpar.height === 0;
|
|
474
|
+
const invalidFormat = stream.codecpar.format === AV_PIX_FMT_NONE;
|
|
475
|
+
const invalidRate = stream.codecpar.frameRate.num === 0 || stream.codecpar.frameRate.den === 0;
|
|
476
|
+
const needsParsing = dimensionsMissing || invalidFormat || invalidRate;
|
|
477
|
+
if (needsParsing && stream.codecpar.extradataSize > 0) {
|
|
478
|
+
stream.codecpar.parseExtradata();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
options.configure?.(formatContext);
|
|
484
|
+
// Determine buffer size
|
|
485
|
+
let bufferSize = options.bufferSize ?? IO_BUFFER_SIZE;
|
|
486
|
+
if (!ioContext && formatContext.iformat && formatContext.pb) {
|
|
487
|
+
// Check if this is a streaming input (like RTSP, HTTP, etc.)
|
|
488
|
+
const isStreaming = formatContext.pb.seekable === 0;
|
|
489
|
+
if (isStreaming) {
|
|
490
|
+
bufferSize *= 2; // double buffer size for streaming inputs
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// Apply defaults to options
|
|
494
|
+
const fullOptions = {
|
|
495
|
+
bufferSize,
|
|
496
|
+
format: options.format ?? '',
|
|
497
|
+
skipStreamInfo: options.skipStreamInfo ?? false,
|
|
498
|
+
startWithKeyframe: options.startWithKeyframe ?? false,
|
|
499
|
+
dtsDeltaThreshold: options.dtsDeltaThreshold ?? DELTA_THRESHOLD,
|
|
500
|
+
dtsErrorThreshold: options.dtsErrorThreshold ?? DTS_ERROR_THRESHOLD,
|
|
501
|
+
copyTs: options.copyTs ?? false,
|
|
502
|
+
options: options.options ?? {},
|
|
503
|
+
};
|
|
504
|
+
const demuxer = new Demuxer(formatContext, fullOptions, ioContext, externalIO);
|
|
505
|
+
if (options.signal) {
|
|
506
|
+
const signal = options.signal;
|
|
507
|
+
signal.removeEventListener('abort', onOpenAbort);
|
|
508
|
+
demuxer.signal = signal;
|
|
509
|
+
// An abort that raced the successful open must not hand out a usable
|
|
510
|
+
// demuxer - tear it down and surface the abort instead.
|
|
511
|
+
if (signal.aborted) {
|
|
512
|
+
await demuxer.close();
|
|
513
|
+
signal.throwIfAborted();
|
|
514
|
+
}
|
|
515
|
+
// Keep abort -> interrupt wired for the demuxer's lifetime so a
|
|
516
|
+
// blocked av_read_frame() unwinds too; close() removes the listener.
|
|
517
|
+
const onAbort = () => {
|
|
518
|
+
demuxer.demuxThreadActive = false;
|
|
519
|
+
demuxer.interrupt();
|
|
520
|
+
};
|
|
521
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
522
|
+
demuxer.signalCleanup = () => signal.removeEventListener('abort', onAbort);
|
|
523
|
+
}
|
|
524
|
+
return demuxer;
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
options.signal?.removeEventListener('abort', onOpenAbort);
|
|
528
|
+
// Clean up only on error
|
|
529
|
+
if (ioContext) {
|
|
530
|
+
// Clear the pb reference first
|
|
531
|
+
formatContext.pb = null;
|
|
532
|
+
// Release the IOContext (for both custom I/O and buffer-based I/O).
|
|
533
|
+
// A caller-supplied context may own a protocol handle from open2(),
|
|
534
|
+
// which only dispose closes - see close().
|
|
535
|
+
if (externalIO) {
|
|
536
|
+
await ioContext[Symbol.asyncDispose]();
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
ioContext.freeContext();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// Clean up FormatContext
|
|
543
|
+
await formatContext.closeInput();
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
finally {
|
|
547
|
+
// Clean up options dictionary
|
|
548
|
+
if (optionsDict) {
|
|
549
|
+
optionsDict.free();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
static openSync(input, options = {}) {
|
|
554
|
+
// Check if input is raw data
|
|
555
|
+
if (typeof input === 'object' && 'type' in input && ('width' in input || 'sampleRate' in input)) {
|
|
556
|
+
// Build options for raw data
|
|
557
|
+
const rawOptions = {
|
|
558
|
+
bufferSize: options.bufferSize,
|
|
559
|
+
format: options.format ?? (input.type === 'video' ? 'rawvideo' : 's16le'),
|
|
560
|
+
options: {
|
|
561
|
+
...options.options,
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
if (input.type === 'video') {
|
|
565
|
+
rawOptions.options = {
|
|
566
|
+
...rawOptions.options,
|
|
567
|
+
video_size: `${input.width}x${input.height}`,
|
|
568
|
+
pixel_format: avGetPixFmtName(input.pixelFormat) ?? 'yuv420p',
|
|
569
|
+
framerate: new Rational(input.frameRate.num, input.frameRate.den).toString(),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
rawOptions.options = {
|
|
574
|
+
...rawOptions.options,
|
|
575
|
+
sample_rate: input.sampleRate,
|
|
576
|
+
channels: input.channels,
|
|
577
|
+
sample_fmt: avGetSampleFmtName(input.sampleFormat) ?? 's16le',
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
input = input.input;
|
|
581
|
+
options = rawOptions;
|
|
582
|
+
}
|
|
583
|
+
// Original implementation for non-raw data
|
|
584
|
+
const formatContext = new FormatContext();
|
|
585
|
+
let ioContext;
|
|
586
|
+
let externalIO = false; // caller-supplied context - released via dispose, not freeContext
|
|
587
|
+
let optionsDict = null;
|
|
588
|
+
let inputFormat = null;
|
|
589
|
+
try {
|
|
590
|
+
// Create options dictionary if options are provided
|
|
591
|
+
if (options.options) {
|
|
592
|
+
optionsDict = Dictionary.fromObject(options.options);
|
|
593
|
+
}
|
|
594
|
+
// Find input format if specified
|
|
595
|
+
if (options.format) {
|
|
596
|
+
inputFormat = InputFormat.findInputFormat(options.format);
|
|
597
|
+
if (!inputFormat) {
|
|
598
|
+
throw new Error(`Input format '${options.format}' not found`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (typeof input === 'string') {
|
|
602
|
+
// File path or URL - resolve relative paths to absolute
|
|
603
|
+
// Skip path resolution for device inputs
|
|
604
|
+
// Check if it's a URL (starts with protocol://) or a file path
|
|
605
|
+
const isUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(input);
|
|
606
|
+
const resolvedInput = isUrl || inputFormat ? input : resolve(input);
|
|
607
|
+
const ret = formatContext.openInputSync(resolvedInput, inputFormat, optionsDict);
|
|
608
|
+
FFmpegError.throwIfError(ret, 'Failed to open input');
|
|
609
|
+
formatContext.setFlags(AVFMT_FLAG_NONBLOCK);
|
|
610
|
+
}
|
|
611
|
+
else if (Buffer.isBuffer(input)) {
|
|
612
|
+
// Validate buffer is not empty
|
|
613
|
+
if (input.length === 0) {
|
|
614
|
+
throw new Error('Cannot open media from empty buffer');
|
|
615
|
+
}
|
|
616
|
+
// From buffer - allocate context first for custom I/O
|
|
617
|
+
formatContext.allocContext();
|
|
618
|
+
ioContext = IOStream.create(input, { bufferSize: options.bufferSize });
|
|
619
|
+
formatContext.pb = ioContext;
|
|
620
|
+
const ret = formatContext.openInputSync('', inputFormat, optionsDict);
|
|
621
|
+
FFmpegError.throwIfError(ret, 'Failed to open input from buffer');
|
|
622
|
+
}
|
|
623
|
+
else if (input instanceof IOContext) {
|
|
624
|
+
if (!options.format) {
|
|
625
|
+
throw new Error('Format must be specified for native IOContext input');
|
|
626
|
+
}
|
|
627
|
+
formatContext.allocContext();
|
|
628
|
+
ioContext = input;
|
|
629
|
+
externalIO = true;
|
|
630
|
+
formatContext.pb = ioContext;
|
|
631
|
+
formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
632
|
+
const ret = formatContext.openInputSync('', inputFormat, optionsDict);
|
|
633
|
+
FFmpegError.throwIfError(ret, 'Failed to open input from native IOContext');
|
|
634
|
+
}
|
|
635
|
+
else if (input instanceof Readable) {
|
|
636
|
+
// Readable stream - format is required
|
|
637
|
+
if (!options.format) {
|
|
638
|
+
throw new Error('Format must be specified for Readable stream input');
|
|
639
|
+
}
|
|
640
|
+
formatContext.allocContext();
|
|
641
|
+
ioContext = IOStream.create(input, { bufferSize: options.bufferSize });
|
|
642
|
+
formatContext.pb = ioContext;
|
|
643
|
+
formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
644
|
+
const ret = formatContext.openInputSync('', inputFormat, optionsDict);
|
|
645
|
+
FFmpegError.throwIfError(ret, 'Failed to open input from Readable stream');
|
|
646
|
+
}
|
|
647
|
+
else if (typeof input === 'object' && 'read' in input) {
|
|
648
|
+
// Custom I/O with callbacks - format is required
|
|
649
|
+
if (!options.format) {
|
|
650
|
+
throw new Error('Format must be specified for custom I/O');
|
|
651
|
+
}
|
|
652
|
+
// Allocate context first for custom I/O
|
|
653
|
+
formatContext.allocContext();
|
|
654
|
+
// Setup custom I/O with callbacks
|
|
655
|
+
ioContext = new IOContext();
|
|
656
|
+
ioContext.allocContextWithCallbacks(options.bufferSize ?? IO_BUFFER_SIZE, 0, input.read, null, input.seek);
|
|
657
|
+
formatContext.pb = ioContext;
|
|
658
|
+
formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
659
|
+
const ret = formatContext.openInputSync('', inputFormat, optionsDict);
|
|
660
|
+
FFmpegError.throwIfError(ret, 'Failed to open input from custom I/O');
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
throw new TypeError('Invalid input type. Expected file path, URL, Buffer, IOContext, Readable, or IOInputCallbacks');
|
|
664
|
+
}
|
|
665
|
+
// Find stream information
|
|
666
|
+
if (!options.skipStreamInfo) {
|
|
667
|
+
const ret = formatContext.findStreamInfoSync(null);
|
|
668
|
+
FFmpegError.throwIfError(ret, 'Failed to find stream info');
|
|
669
|
+
}
|
|
670
|
+
options.configure?.(formatContext);
|
|
671
|
+
// Determine buffer size
|
|
672
|
+
let bufferSize = options.bufferSize ?? IO_BUFFER_SIZE;
|
|
673
|
+
if (!ioContext && formatContext.iformat && formatContext.pb) {
|
|
674
|
+
// Check if this is a streaming input (like RTSP, HTTP, etc.)
|
|
675
|
+
const isStreaming = formatContext.pb.seekable === 0;
|
|
676
|
+
if (isStreaming) {
|
|
677
|
+
bufferSize *= 2; // double buffer size for streaming inputs
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// Apply defaults to options
|
|
681
|
+
const fullOptions = {
|
|
682
|
+
bufferSize,
|
|
683
|
+
format: options.format ?? '',
|
|
684
|
+
skipStreamInfo: options.skipStreamInfo ?? false,
|
|
685
|
+
startWithKeyframe: options.startWithKeyframe ?? false,
|
|
686
|
+
dtsDeltaThreshold: options.dtsDeltaThreshold ?? DELTA_THRESHOLD,
|
|
687
|
+
dtsErrorThreshold: options.dtsErrorThreshold ?? DTS_ERROR_THRESHOLD,
|
|
688
|
+
copyTs: options.copyTs ?? false,
|
|
689
|
+
options: options.options ?? {},
|
|
690
|
+
};
|
|
691
|
+
const demuxer = new Demuxer(formatContext, fullOptions, ioContext, externalIO);
|
|
692
|
+
if (options.signal) {
|
|
693
|
+
options.signal.throwIfAborted();
|
|
694
|
+
demuxer.signal = options.signal;
|
|
695
|
+
}
|
|
696
|
+
return demuxer;
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
// Clean up only on error
|
|
700
|
+
if (ioContext) {
|
|
701
|
+
// Clear the pb reference first
|
|
702
|
+
formatContext.pb = null;
|
|
703
|
+
// Release the IOContext (see open() for why dispose)
|
|
704
|
+
if (externalIO) {
|
|
705
|
+
ioContext[Symbol.dispose]();
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
ioContext.freeContext();
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// Clean up FormatContext
|
|
712
|
+
formatContext.closeInputSync();
|
|
713
|
+
throw error;
|
|
714
|
+
}
|
|
715
|
+
finally {
|
|
716
|
+
// Clean up options dictionary
|
|
717
|
+
if (optionsDict) {
|
|
718
|
+
optionsDict.free();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Open RTP/SRTP input stream via localhost UDP.
|
|
724
|
+
*
|
|
725
|
+
* Creates a Demuxer from SDP string received via UDP socket.
|
|
726
|
+
* Opens UDP socket and configures FFmpeg to receive and parse RTP packets.
|
|
727
|
+
*
|
|
728
|
+
* @param sdpContent - SDP content string describing the RTP stream
|
|
729
|
+
*
|
|
730
|
+
* @throws {Error} If SDP parsing or socket setup fails
|
|
731
|
+
*
|
|
732
|
+
* @throws {FFmpegError} If FFmpeg operations fail
|
|
733
|
+
*
|
|
734
|
+
* @returns Promise with Demuxer, sendPacket function and cleanup
|
|
735
|
+
*
|
|
736
|
+
* @example
|
|
737
|
+
* ```typescript
|
|
738
|
+
* import { Demuxer, StreamingUtils } from 'node-av/api';
|
|
739
|
+
* import { AV_CODEC_ID_OPUS } from 'node-av/constants';
|
|
740
|
+
*
|
|
741
|
+
* // Generate SDP for SRTP encrypted Opus
|
|
742
|
+
* const sdp = StreamingUtils.createRTPInputSDP([{
|
|
743
|
+
* port: 5004,
|
|
744
|
+
* codecId: AV_CODEC_ID_OPUS,
|
|
745
|
+
* payloadType: 111,
|
|
746
|
+
* clockRate: 16000,
|
|
747
|
+
* channels: 1,
|
|
748
|
+
* srtp: { key: srtpKey, salt: srtpSalt }
|
|
749
|
+
* }]);
|
|
750
|
+
*
|
|
751
|
+
* // Open RTP input
|
|
752
|
+
* const { input, sendPacket, close } = await Demuxer.openSDP(sdp);
|
|
753
|
+
*
|
|
754
|
+
* // Route encrypted RTP packets from network
|
|
755
|
+
* socket.on('message', (msg) => sendPacket(msg));
|
|
756
|
+
*
|
|
757
|
+
* // Decode audio
|
|
758
|
+
* const decoder = await Decoder.create(input.audio()!);
|
|
759
|
+
* for await (const packet of input.packets()) {
|
|
760
|
+
* const frame = await decoder.decode(packet);
|
|
761
|
+
* // Process frame...
|
|
762
|
+
* }
|
|
763
|
+
*
|
|
764
|
+
* // Cleanup
|
|
765
|
+
* await close();
|
|
766
|
+
* ```
|
|
767
|
+
*
|
|
768
|
+
* @see {@link StreamingUtils.createInputSDP} to generate SDP content.
|
|
769
|
+
*/
|
|
770
|
+
static async openSDP(sdpContent) {
|
|
771
|
+
// Extract all ports from SDP (supports multi-stream: video + audio)
|
|
772
|
+
const ports = StreamingUtils.extractPortsFromSDP(sdpContent);
|
|
773
|
+
if (ports.length === 0) {
|
|
774
|
+
throw new Error('Failed to extract any ports from SDP content');
|
|
775
|
+
}
|
|
776
|
+
// Convert SDP to buffer for custom I/O
|
|
777
|
+
const sdpBuffer = Buffer.from(sdpContent);
|
|
778
|
+
let position = 0;
|
|
779
|
+
// Create custom I/O callbacks for SDP content
|
|
780
|
+
const callbacks = {
|
|
781
|
+
read: (size) => {
|
|
782
|
+
if (position >= sdpBuffer.length) {
|
|
783
|
+
return null; // EOF
|
|
784
|
+
}
|
|
785
|
+
const chunk = sdpBuffer.subarray(position, Math.min(position + size, sdpBuffer.length));
|
|
786
|
+
position += chunk.length;
|
|
787
|
+
return chunk;
|
|
788
|
+
},
|
|
789
|
+
seek: (offset, whence) => {
|
|
790
|
+
const offsetNum = Number(offset);
|
|
791
|
+
if (whence === AVSEEK_SET) {
|
|
792
|
+
position = offsetNum;
|
|
793
|
+
}
|
|
794
|
+
else if (whence === AVSEEK_CUR) {
|
|
795
|
+
position += offsetNum;
|
|
796
|
+
}
|
|
797
|
+
else if (whence === AVSEEK_END) {
|
|
798
|
+
position = sdpBuffer.length + offsetNum;
|
|
799
|
+
}
|
|
800
|
+
return position;
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
// Create UDP socket for sending packets to FFmpeg
|
|
804
|
+
const udpSocket = createSocket('udp4');
|
|
805
|
+
try {
|
|
806
|
+
// Open Demuxer with SDP format using custom I/O
|
|
807
|
+
const input = await Demuxer.open(callbacks, {
|
|
808
|
+
format: 'sdp',
|
|
809
|
+
skipStreamInfo: true,
|
|
810
|
+
options: {
|
|
811
|
+
protocol_whitelist: 'pipe,udp,rtp,file,crypto',
|
|
812
|
+
listen_timeout: -1,
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
const sendPacket = (rtpPacket, streamIndex = 0) => {
|
|
816
|
+
const port = ports[streamIndex];
|
|
817
|
+
if (!port) {
|
|
818
|
+
throw new Error(`No port found for stream index ${streamIndex}. Available streams: ${ports.length}`);
|
|
819
|
+
}
|
|
820
|
+
const data = Buffer.isBuffer(rtpPacket) ? rtpPacket : rtpPacket.serialize();
|
|
821
|
+
udpSocket.send(data, port, '127.0.0.1');
|
|
822
|
+
};
|
|
823
|
+
const close = async () => {
|
|
824
|
+
await input.close();
|
|
825
|
+
udpSocket.close();
|
|
826
|
+
};
|
|
827
|
+
const closeSync = () => {
|
|
828
|
+
input.closeSync();
|
|
829
|
+
udpSocket.close();
|
|
830
|
+
};
|
|
831
|
+
return { input, sendPacket, close, closeSync };
|
|
832
|
+
}
|
|
833
|
+
catch (error) {
|
|
834
|
+
// Cleanup on error
|
|
835
|
+
udpSocket.close();
|
|
836
|
+
throw error;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Open RTP/SRTP input stream via localhost UDP synchronously.
|
|
841
|
+
* Synchronous version of openSDP.
|
|
842
|
+
*
|
|
843
|
+
* Creates a Demuxer from SDP string received via UDP socket.
|
|
844
|
+
* Opens UDP socket and configures FFmpeg to receive and parse RTP packets.
|
|
845
|
+
*
|
|
846
|
+
* @param sdpContent - SDP content string describing the RTP stream
|
|
847
|
+
*
|
|
848
|
+
* @throws {Error} If SDP parsing or socket setup fails
|
|
849
|
+
*
|
|
850
|
+
* @throws {FFmpegError} If FFmpeg operations fail
|
|
851
|
+
*
|
|
852
|
+
* @returns Object with Demuxer, sendPacket function and cleanup
|
|
853
|
+
*
|
|
854
|
+
* @example
|
|
855
|
+
* ```typescript
|
|
856
|
+
* import { Demuxer, StreamingUtils } from 'node-av/api';
|
|
857
|
+
* import { AV_CODEC_ID_OPUS } from 'node-av/constants';
|
|
858
|
+
*
|
|
859
|
+
* // Generate SDP for SRTP encrypted Opus
|
|
860
|
+
* const sdp = StreamingUtils.createRTPInputSDP([{
|
|
861
|
+
* port: 5004,
|
|
862
|
+
* codecId: AV_CODEC_ID_OPUS,
|
|
863
|
+
* payloadType: 111,
|
|
864
|
+
* clockRate: 16000,
|
|
865
|
+
* channels: 1,
|
|
866
|
+
* srtp: { key: srtpKey, salt: srtpSalt }
|
|
867
|
+
* }]);
|
|
868
|
+
*
|
|
869
|
+
* // Open RTP input
|
|
870
|
+
* const { input, sendPacket, closeSync } = Demuxer.openSDPSync(sdp);
|
|
871
|
+
*
|
|
872
|
+
* // Route encrypted RTP packets from network
|
|
873
|
+
* socket.on('message', (msg) => sendPacket(msg));
|
|
874
|
+
*
|
|
875
|
+
* // Decode audio
|
|
876
|
+
* const decoder = await Decoder.create(input.audio()!);
|
|
877
|
+
* for await (const packet of input.packets()) {
|
|
878
|
+
* const frame = await decoder.decode(packet);
|
|
879
|
+
* // Process frame...
|
|
880
|
+
* }
|
|
881
|
+
*
|
|
882
|
+
* // Cleanup synchronously
|
|
883
|
+
* closeSync();
|
|
884
|
+
* ```
|
|
885
|
+
*
|
|
886
|
+
* @see {@link StreamingUtils.createInputSDP} to generate SDP content.
|
|
887
|
+
* @see {@link openSDP} For async version
|
|
888
|
+
*/
|
|
889
|
+
static openSDPSync(sdpContent) {
|
|
890
|
+
// Extract all ports from SDP (supports multi-stream: video + audio)
|
|
891
|
+
const ports = StreamingUtils.extractPortsFromSDP(sdpContent);
|
|
892
|
+
if (ports.length === 0) {
|
|
893
|
+
throw new Error('Failed to extract any ports from SDP content');
|
|
894
|
+
}
|
|
895
|
+
// Convert SDP to buffer for custom I/O
|
|
896
|
+
const sdpBuffer = Buffer.from(sdpContent);
|
|
897
|
+
let position = 0;
|
|
898
|
+
// Create custom I/O callbacks for SDP content
|
|
899
|
+
const callbacks = {
|
|
900
|
+
read: (size) => {
|
|
901
|
+
if (position >= sdpBuffer.length) {
|
|
902
|
+
return null; // EOF
|
|
903
|
+
}
|
|
904
|
+
const chunk = sdpBuffer.subarray(position, Math.min(position + size, sdpBuffer.length));
|
|
905
|
+
position += chunk.length;
|
|
906
|
+
return chunk;
|
|
907
|
+
},
|
|
908
|
+
seek: (offset, whence) => {
|
|
909
|
+
const offsetNum = Number(offset);
|
|
910
|
+
if (whence === AVSEEK_SET) {
|
|
911
|
+
position = offsetNum;
|
|
912
|
+
}
|
|
913
|
+
else if (whence === AVSEEK_CUR) {
|
|
914
|
+
position += offsetNum;
|
|
915
|
+
}
|
|
916
|
+
else if (whence === AVSEEK_END) {
|
|
917
|
+
position = sdpBuffer.length + offsetNum;
|
|
918
|
+
}
|
|
919
|
+
return position;
|
|
920
|
+
},
|
|
921
|
+
};
|
|
922
|
+
// Create UDP socket for sending packets to FFmpeg
|
|
923
|
+
const udpSocket = createSocket('udp4');
|
|
924
|
+
try {
|
|
925
|
+
// Open Demuxer with SDP format using custom I/O
|
|
926
|
+
const input = Demuxer.openSync(callbacks, {
|
|
927
|
+
format: 'sdp',
|
|
928
|
+
skipStreamInfo: true,
|
|
929
|
+
options: {
|
|
930
|
+
protocol_whitelist: 'pipe,udp,rtp,file,crypto',
|
|
931
|
+
listen_timeout: -1,
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
const sendPacket = (rtpPacket, streamIndex = 0) => {
|
|
935
|
+
const port = ports[streamIndex];
|
|
936
|
+
if (!port) {
|
|
937
|
+
throw new Error(`No port found for stream index ${streamIndex}. Available streams: ${ports.length}`);
|
|
938
|
+
}
|
|
939
|
+
const data = Buffer.isBuffer(rtpPacket) ? rtpPacket : rtpPacket.serialize();
|
|
940
|
+
udpSocket.send(data, port, '127.0.0.1');
|
|
941
|
+
};
|
|
942
|
+
const close = async () => {
|
|
943
|
+
await input.close();
|
|
944
|
+
udpSocket.close();
|
|
945
|
+
};
|
|
946
|
+
const closeSync = () => {
|
|
947
|
+
input.closeSync();
|
|
948
|
+
udpSocket.close();
|
|
949
|
+
};
|
|
950
|
+
return { input, sendPacket, close, closeSync };
|
|
951
|
+
}
|
|
952
|
+
catch (error) {
|
|
953
|
+
// Cleanup on error
|
|
954
|
+
udpSocket.close();
|
|
955
|
+
throw error;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Check if input is open.
|
|
960
|
+
*
|
|
961
|
+
* @example
|
|
962
|
+
* ```typescript
|
|
963
|
+
* if (!input.isInputOpen) {
|
|
964
|
+
* console.log('Input is not open');
|
|
965
|
+
* }
|
|
966
|
+
* ```
|
|
967
|
+
*/
|
|
968
|
+
get isInputOpen() {
|
|
969
|
+
return !this.isClosed;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Get all streams in the media.
|
|
973
|
+
*
|
|
974
|
+
* @example
|
|
975
|
+
* ```typescript
|
|
976
|
+
* for (const stream of input.streams) {
|
|
977
|
+
* console.log(`Stream ${stream.index}: ${stream.codecpar.codecType}`);
|
|
978
|
+
* }
|
|
979
|
+
* ```
|
|
980
|
+
*/
|
|
981
|
+
get streams() {
|
|
982
|
+
return this._streams;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Get media duration in seconds.
|
|
986
|
+
*
|
|
987
|
+
* Returns 0 if duration is unknown or not available or input is closed.
|
|
988
|
+
*
|
|
989
|
+
* @example
|
|
990
|
+
* ```typescript
|
|
991
|
+
* console.log(`Duration: ${input.duration} seconds`);
|
|
992
|
+
* ```
|
|
993
|
+
*/
|
|
994
|
+
get duration() {
|
|
995
|
+
if (this.isClosed) {
|
|
996
|
+
return 0;
|
|
997
|
+
}
|
|
998
|
+
const duration = this.formatContext.duration;
|
|
999
|
+
if (!duration || duration <= 0) {
|
|
1000
|
+
return 0;
|
|
1001
|
+
}
|
|
1002
|
+
// Convert from AV_TIME_BASE (microseconds) to seconds
|
|
1003
|
+
return Number(duration) / 1000000;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Get media start time in seconds.
|
|
1007
|
+
*
|
|
1008
|
+
* For device inputs (e.g., avfoundation), this reflects the system uptime
|
|
1009
|
+
* at capture start. Pass this value to `Muxer.open()` via the `startTime`
|
|
1010
|
+
* option to produce correct output timestamps.
|
|
1011
|
+
*
|
|
1012
|
+
* Returns 0 if start time is unknown or not available or input is closed.
|
|
1013
|
+
*
|
|
1014
|
+
* @example
|
|
1015
|
+
* ```typescript
|
|
1016
|
+
* await using input = await Demuxer.open(source);
|
|
1017
|
+
* await using output = await Muxer.open('output.mp4', { startTime: input.startTime });
|
|
1018
|
+
* ```
|
|
1019
|
+
*/
|
|
1020
|
+
get startTime() {
|
|
1021
|
+
if (this.isClosed) {
|
|
1022
|
+
return 0;
|
|
1023
|
+
}
|
|
1024
|
+
const startTime = this.formatContext.startTime;
|
|
1025
|
+
if (!startTime || startTime <= 0n) {
|
|
1026
|
+
return 0;
|
|
1027
|
+
}
|
|
1028
|
+
// Convert from AV_TIME_BASE (microseconds) to seconds
|
|
1029
|
+
return Number(startTime) / 1000000;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Get media bitrate in kilobits per second.
|
|
1033
|
+
*
|
|
1034
|
+
* Returns 0 if bitrate is unknown or not available or input is closed.
|
|
1035
|
+
*
|
|
1036
|
+
* @example
|
|
1037
|
+
* ```typescript
|
|
1038
|
+
* console.log(`Bitrate: ${input.bitRate} kbps`);
|
|
1039
|
+
* ```
|
|
1040
|
+
*/
|
|
1041
|
+
get bitRate() {
|
|
1042
|
+
if (this.isClosed) {
|
|
1043
|
+
return 0;
|
|
1044
|
+
}
|
|
1045
|
+
const bitrate = this.formatContext.bitRate;
|
|
1046
|
+
if (!bitrate || bitrate <= 0) {
|
|
1047
|
+
return 0;
|
|
1048
|
+
}
|
|
1049
|
+
// Convert from bits per second to kilobits per second
|
|
1050
|
+
return Number(bitrate) / 1000;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Get media metadata.
|
|
1054
|
+
*
|
|
1055
|
+
* Returns all metadata tags as key-value pairs.
|
|
1056
|
+
*
|
|
1057
|
+
* @example
|
|
1058
|
+
* ```typescript
|
|
1059
|
+
* const metadata = input.metadata;
|
|
1060
|
+
* console.log(`Title: ${metadata.title}`);
|
|
1061
|
+
* console.log(`Artist: ${metadata.artist}`);
|
|
1062
|
+
* ```
|
|
1063
|
+
*/
|
|
1064
|
+
get metadata() {
|
|
1065
|
+
if (this.isClosed) {
|
|
1066
|
+
return {};
|
|
1067
|
+
}
|
|
1068
|
+
return this.formatContext.metadata?.getAll() ?? {};
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Get format name.
|
|
1072
|
+
*
|
|
1073
|
+
* Returns 'unknown' if input is closed or format is not available.
|
|
1074
|
+
*
|
|
1075
|
+
* @example
|
|
1076
|
+
* ```typescript
|
|
1077
|
+
* console.log(`Format: ${input.formatName}`); // "mov,mp4,m4a,3gp,3g2,mj2"
|
|
1078
|
+
* ```
|
|
1079
|
+
*/
|
|
1080
|
+
get formatName() {
|
|
1081
|
+
if (this.isClosed) {
|
|
1082
|
+
return 'unknown';
|
|
1083
|
+
}
|
|
1084
|
+
return this.formatContext.iformat?.name ?? 'unknown';
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Get format long name.
|
|
1088
|
+
*
|
|
1089
|
+
* Returns 'Unknown Format' if input is closed or format is not available.
|
|
1090
|
+
*
|
|
1091
|
+
* @example
|
|
1092
|
+
* ```typescript
|
|
1093
|
+
* console.log(`Format: ${input.formatLongName}`); // "QuickTime / MOV"
|
|
1094
|
+
* ```
|
|
1095
|
+
*/
|
|
1096
|
+
get formatLongName() {
|
|
1097
|
+
if (this.isClosed) {
|
|
1098
|
+
return 'Unknown Format';
|
|
1099
|
+
}
|
|
1100
|
+
return this.formatContext.iformat?.longName ?? 'Unknown Format';
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Get MIME type of the input format.
|
|
1104
|
+
*
|
|
1105
|
+
* Returns null if input is closed or format is not available.
|
|
1106
|
+
*
|
|
1107
|
+
* @example
|
|
1108
|
+
* ```typescript
|
|
1109
|
+
* console.log(`MIME Type: ${input.mimeType}`); // "video/mp4"
|
|
1110
|
+
* ```
|
|
1111
|
+
*/
|
|
1112
|
+
get mimeType() {
|
|
1113
|
+
if (this.isClosed) {
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
return this.formatContext.iformat?.mimeType ?? null;
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Get input stream by index.
|
|
1120
|
+
*
|
|
1121
|
+
* Returns the stream at the specified index.
|
|
1122
|
+
*
|
|
1123
|
+
* @param index - Stream index
|
|
1124
|
+
*
|
|
1125
|
+
* @returns Stream or undefined if index is invalid
|
|
1126
|
+
*
|
|
1127
|
+
* @example
|
|
1128
|
+
* ```typescript
|
|
1129
|
+
* const input = await Demuxer.open('input.mp4');
|
|
1130
|
+
*
|
|
1131
|
+
* // Get the input stream to inspect codec parameters
|
|
1132
|
+
* const stream = input.getStream(1); // Get stream at index 1
|
|
1133
|
+
* if (stream) {
|
|
1134
|
+
* console.log(`Input codec: ${stream.codecpar.codecId}`);
|
|
1135
|
+
* }
|
|
1136
|
+
* ```
|
|
1137
|
+
*
|
|
1138
|
+
* @see {@link video} For getting video streams
|
|
1139
|
+
* @see {@link audio} For getting audio streams
|
|
1140
|
+
*/
|
|
1141
|
+
getStream(index) {
|
|
1142
|
+
const streams = this.formatContext.streams;
|
|
1143
|
+
if (!streams || index < 0 || index >= streams.length) {
|
|
1144
|
+
return undefined;
|
|
1145
|
+
}
|
|
1146
|
+
return streams[index];
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Get video stream by index.
|
|
1150
|
+
*
|
|
1151
|
+
* Returns the nth video stream (0-based index).
|
|
1152
|
+
* Returns undefined if stream doesn't exist.
|
|
1153
|
+
*
|
|
1154
|
+
* @param index - Video stream index (default: 0)
|
|
1155
|
+
*
|
|
1156
|
+
* @returns Video stream or undefined
|
|
1157
|
+
*
|
|
1158
|
+
* @example
|
|
1159
|
+
* ```typescript
|
|
1160
|
+
* const videoStream = input.video();
|
|
1161
|
+
* if (videoStream) {
|
|
1162
|
+
* console.log(`Video: ${videoStream.codecpar.width}x${videoStream.codecpar.height}`);
|
|
1163
|
+
* }
|
|
1164
|
+
* ```
|
|
1165
|
+
*
|
|
1166
|
+
* @example
|
|
1167
|
+
* ```typescript
|
|
1168
|
+
* // Get second video stream
|
|
1169
|
+
* const secondVideo = input.video(1);
|
|
1170
|
+
* ```
|
|
1171
|
+
*
|
|
1172
|
+
* @see {@link audio} For audio streams
|
|
1173
|
+
* @see {@link findBestStream} For automatic selection
|
|
1174
|
+
*/
|
|
1175
|
+
video(index = 0) {
|
|
1176
|
+
const streams = this._streams.filter((s) => s.codecpar.codecType === AVMEDIA_TYPE_VIDEO);
|
|
1177
|
+
return streams[index];
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Get audio stream by index.
|
|
1181
|
+
*
|
|
1182
|
+
* Returns the nth audio stream (0-based index).
|
|
1183
|
+
* Returns undefined if stream doesn't exist.
|
|
1184
|
+
*
|
|
1185
|
+
* @param index - Audio stream index (default: 0)
|
|
1186
|
+
*
|
|
1187
|
+
* @returns Audio stream or undefined
|
|
1188
|
+
*
|
|
1189
|
+
* @example
|
|
1190
|
+
* ```typescript
|
|
1191
|
+
* const audioStream = input.audio();
|
|
1192
|
+
* if (audioStream) {
|
|
1193
|
+
* console.log(`Audio: ${audioStream.codecpar.sampleRate}Hz`);
|
|
1194
|
+
* }
|
|
1195
|
+
* ```
|
|
1196
|
+
*
|
|
1197
|
+
* @example
|
|
1198
|
+
* ```typescript
|
|
1199
|
+
* // Get second audio stream
|
|
1200
|
+
* const secondAudio = input.audio(1);
|
|
1201
|
+
* ```
|
|
1202
|
+
*
|
|
1203
|
+
* @see {@link video} For video streams
|
|
1204
|
+
* @see {@link findBestStream} For automatic selection
|
|
1205
|
+
*/
|
|
1206
|
+
audio(index = 0) {
|
|
1207
|
+
const streams = this._streams.filter((s) => s.codecpar.codecType === AVMEDIA_TYPE_AUDIO);
|
|
1208
|
+
return streams[index];
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Get input format details.
|
|
1212
|
+
*
|
|
1213
|
+
* Returns null if input is closed or format is not available.
|
|
1214
|
+
*
|
|
1215
|
+
* @returns Input format or null
|
|
1216
|
+
*
|
|
1217
|
+
* @example
|
|
1218
|
+
* ```typescript
|
|
1219
|
+
* const inputFormat = input.inputFormat;
|
|
1220
|
+
* if (inputFormat) {
|
|
1221
|
+
* console.log(`Input Format: ${inputFormat.name}`);
|
|
1222
|
+
* }
|
|
1223
|
+
* ```
|
|
1224
|
+
*/
|
|
1225
|
+
inputFormat() {
|
|
1226
|
+
return this.formatContext.iformat;
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Find the best stream of a given type.
|
|
1230
|
+
*
|
|
1231
|
+
* Uses FFmpeg's stream selection algorithm.
|
|
1232
|
+
* Considers codec support, default flags, and quality.
|
|
1233
|
+
*
|
|
1234
|
+
* Direct mapping to av_find_best_stream().
|
|
1235
|
+
*
|
|
1236
|
+
* @param type - Media type to find
|
|
1237
|
+
*
|
|
1238
|
+
* @returns Best stream or undefined if not found or input is closed
|
|
1239
|
+
*
|
|
1240
|
+
* @example
|
|
1241
|
+
* ```typescript
|
|
1242
|
+
* import { AVMEDIA_TYPE_VIDEO } from 'node-av/constants';
|
|
1243
|
+
*
|
|
1244
|
+
* const bestVideo = input.findBestStream(AVMEDIA_TYPE_VIDEO);
|
|
1245
|
+
* if (bestVideo) {
|
|
1246
|
+
* const decoder = await Decoder.create(bestVideo);
|
|
1247
|
+
* }
|
|
1248
|
+
* ```
|
|
1249
|
+
*
|
|
1250
|
+
* @see {@link video} For direct video stream access
|
|
1251
|
+
* @see {@link audio} For direct audio stream access
|
|
1252
|
+
*/
|
|
1253
|
+
findBestStream(type) {
|
|
1254
|
+
if (this.isClosed) {
|
|
1255
|
+
return undefined;
|
|
1256
|
+
}
|
|
1257
|
+
const bestStreamIndex = this.formatContext.findBestStream(type);
|
|
1258
|
+
return this._streams.find((s) => s.index === bestStreamIndex);
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Read packets from media as async generator.
|
|
1262
|
+
*
|
|
1263
|
+
* Yields demuxed packets for processing.
|
|
1264
|
+
* Automatically handles packet memory management.
|
|
1265
|
+
* Optionally filters packets by stream index.
|
|
1266
|
+
*
|
|
1267
|
+
* **Supports parallel generators**: Multiple `packets()` iterators can run concurrently.
|
|
1268
|
+
* When multiple generators are active, an internal demux thread automatically handles
|
|
1269
|
+
* packet distribution to avoid race conditions.
|
|
1270
|
+
*
|
|
1271
|
+
* Direct mapping to av_read_frame().
|
|
1272
|
+
*
|
|
1273
|
+
* @param index - Optional stream index to filter
|
|
1274
|
+
*
|
|
1275
|
+
* @yields {Packet} Demuxed packets (must be freed by caller)
|
|
1276
|
+
*
|
|
1277
|
+
* @throws {FFmpegError} If reading fails with an error other than EOF (e.g. I/O failure
|
|
1278
|
+
* or a dropped network source) - only a real end-of-file ends the stream cleanly
|
|
1279
|
+
*
|
|
1280
|
+
* @throws {Error} If packet cloning fails
|
|
1281
|
+
*
|
|
1282
|
+
* @example
|
|
1283
|
+
* ```typescript
|
|
1284
|
+
* // Read all packets
|
|
1285
|
+
* for await (const packet of input.packets()) {
|
|
1286
|
+
* console.log(`Packet: stream=${packet.streamIndex}, pts=${packet.pts}`);
|
|
1287
|
+
* packet.free();
|
|
1288
|
+
* }
|
|
1289
|
+
* ```
|
|
1290
|
+
*
|
|
1291
|
+
* @example
|
|
1292
|
+
* ```typescript
|
|
1293
|
+
* // Read only video packets
|
|
1294
|
+
* const videoStream = input.video();
|
|
1295
|
+
* for await (const packet of input.packets(videoStream.index)) {
|
|
1296
|
+
* // Process video packet
|
|
1297
|
+
* packet.free();
|
|
1298
|
+
* }
|
|
1299
|
+
* ```
|
|
1300
|
+
*
|
|
1301
|
+
* @example
|
|
1302
|
+
* ```typescript
|
|
1303
|
+
* // Parallel processing of video and audio streams
|
|
1304
|
+
* const videoGen = input.packets(videoStream.index);
|
|
1305
|
+
* const audioGen = input.packets(audioStream.index);
|
|
1306
|
+
*
|
|
1307
|
+
* await Promise.all([
|
|
1308
|
+
* (async () => {
|
|
1309
|
+
* for await (const packet of videoGen) {
|
|
1310
|
+
* // Process video
|
|
1311
|
+
* packet.free();
|
|
1312
|
+
* }
|
|
1313
|
+
* })(),
|
|
1314
|
+
* (async () => {
|
|
1315
|
+
* for await (const packet of audioGen) {
|
|
1316
|
+
* // Process audio
|
|
1317
|
+
* packet.free();
|
|
1318
|
+
* }
|
|
1319
|
+
* })()
|
|
1320
|
+
* ]);
|
|
1321
|
+
* ```
|
|
1322
|
+
*
|
|
1323
|
+
* @see {@link Decoder.frames} For decoding packets
|
|
1324
|
+
*/
|
|
1325
|
+
async *packets(index) {
|
|
1326
|
+
// Register this generator
|
|
1327
|
+
this.activeGenerators++;
|
|
1328
|
+
const queueKey = index ?? 'all';
|
|
1329
|
+
// Initialize queue for this generator and count it as a consumer of the key.
|
|
1330
|
+
if (!this.packetQueues.has(queueKey)) {
|
|
1331
|
+
this.packetQueues.set(queueKey, []);
|
|
1332
|
+
}
|
|
1333
|
+
this.packetQueueConsumers.set(queueKey, (this.packetQueueConsumers.get(queueKey) ?? 0) + 1);
|
|
1334
|
+
// Always start demux thread (handles single and multiple generators)
|
|
1335
|
+
this.startDemuxThread();
|
|
1336
|
+
try {
|
|
1337
|
+
let hasSeenKeyframe = !this.options.startWithKeyframe;
|
|
1338
|
+
// Read from queue (demux thread is handling av_read_frame)
|
|
1339
|
+
const queue = this.packetQueues.get(queueKey);
|
|
1340
|
+
while (!this.isClosed) {
|
|
1341
|
+
if (this.signal?.aborted) {
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
// Try to get packet from queue
|
|
1345
|
+
let packet = queue.shift();
|
|
1346
|
+
if (packet) {
|
|
1347
|
+
this.wakeDemuxThread();
|
|
1348
|
+
}
|
|
1349
|
+
// If queue is empty, wait for next packet
|
|
1350
|
+
if (!packet) {
|
|
1351
|
+
// Queue drained - check for end of stream. Buffered packets always
|
|
1352
|
+
// drain before EOF/error is honored, so successfully read data is
|
|
1353
|
+
// never dropped.
|
|
1354
|
+
if (this.demuxEof) {
|
|
1355
|
+
// The read loop ended with a real error (not EOF): propagate it
|
|
1356
|
+
// instead of ending the stream silently - a dead live source must
|
|
1357
|
+
// be distinguishable from a finished file.
|
|
1358
|
+
if (this.lastError) {
|
|
1359
|
+
throw this.lastError;
|
|
1360
|
+
}
|
|
1361
|
+
break; // End of stream
|
|
1362
|
+
}
|
|
1363
|
+
// Create promise and register resolver. Multiple consumers may wait
|
|
1364
|
+
// on the same queue key concurrently, so resolvers are collected per
|
|
1365
|
+
// key - a single slot would be overwritten by the second consumer,
|
|
1366
|
+
// parking the first one forever (whole-demuxer deadlock).
|
|
1367
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
1368
|
+
const resolvers = this.queueResolvers.get(queueKey);
|
|
1369
|
+
if (resolvers) {
|
|
1370
|
+
resolvers.push(resolve);
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
this.queueResolvers.set(queueKey, [resolve]);
|
|
1374
|
+
}
|
|
1375
|
+
// Wait for demux thread to add packet
|
|
1376
|
+
await promise;
|
|
1377
|
+
// Check for abort after wakeup
|
|
1378
|
+
if (this.signal?.aborted) {
|
|
1379
|
+
break;
|
|
1380
|
+
}
|
|
1381
|
+
packet = queue.shift();
|
|
1382
|
+
if (!packet) {
|
|
1383
|
+
// Woken without a packet for us - either EOF/error or another
|
|
1384
|
+
// consumer grabbed it. Loop back and re-evaluate.
|
|
1385
|
+
if (this.demuxEof) {
|
|
1386
|
+
if (this.lastError) {
|
|
1387
|
+
throw this.lastError;
|
|
1388
|
+
}
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
this.wakeDemuxThread();
|
|
1394
|
+
}
|
|
1395
|
+
// Apply keyframe filtering if needed
|
|
1396
|
+
if (!hasSeenKeyframe) {
|
|
1397
|
+
const stream = this._streams[packet.streamIndex];
|
|
1398
|
+
const isVideoStream = stream?.codecpar.codecType === AVMEDIA_TYPE_VIDEO;
|
|
1399
|
+
if (isVideoStream && packet.isKeyframe) {
|
|
1400
|
+
hasSeenKeyframe = true;
|
|
1401
|
+
}
|
|
1402
|
+
else if (isVideoStream && !packet.isKeyframe) {
|
|
1403
|
+
packet.free();
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
yield packet;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
finally {
|
|
1411
|
+
// Unregister this generator
|
|
1412
|
+
this.activeGenerators--;
|
|
1413
|
+
// Drop this consumer from its queue key. When the last consumer of a key
|
|
1414
|
+
// leaves, remove the queue so the demux thread stops treating it as a
|
|
1415
|
+
// backpressure target - otherwise a full queue whose consumer has already
|
|
1416
|
+
// stopped would block the read loop forever and starve the other streams
|
|
1417
|
+
// (teardown deadlock with multi-stream pipelines). Free any buffered packets.
|
|
1418
|
+
const remaining = (this.packetQueueConsumers.get(queueKey) ?? 1) - 1;
|
|
1419
|
+
if (remaining <= 0) {
|
|
1420
|
+
this.packetQueueConsumers.delete(queueKey);
|
|
1421
|
+
const abandoned = this.packetQueues.get(queueKey);
|
|
1422
|
+
this.packetQueues.delete(queueKey);
|
|
1423
|
+
if (abandoned) {
|
|
1424
|
+
for (const p of abandoned) {
|
|
1425
|
+
p.free();
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
// The demux thread may be waiting on this queue, so wake it to check the remaining ones.
|
|
1429
|
+
this.wakeDemuxThread();
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
this.packetQueueConsumers.set(queueKey, remaining);
|
|
1433
|
+
}
|
|
1434
|
+
// Stop demux thread if no more generators
|
|
1435
|
+
if (this.activeGenerators === 0) {
|
|
1436
|
+
await this.stopDemuxThread();
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
// Only reached when the loop ended naturally (EOF, abort, close). A consumer
|
|
1440
|
+
// break resumes inside the finally above and completes the generator there -
|
|
1441
|
+
// yielding from the finally would suspend forever inside iterator.return(),
|
|
1442
|
+
// violating the iterator protocol.
|
|
1443
|
+
this.signal?.throwIfAborted();
|
|
1444
|
+
yield null; // Signal EOF
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Read packets from media as generator synchronously.
|
|
1448
|
+
* Synchronous version of packets.
|
|
1449
|
+
*
|
|
1450
|
+
* Yields demuxed packets for processing.
|
|
1451
|
+
* Automatically handles packet memory management.
|
|
1452
|
+
* Optionally filters packets by stream index.
|
|
1453
|
+
*
|
|
1454
|
+
* Direct mapping to av_read_frame().
|
|
1455
|
+
*
|
|
1456
|
+
* @param index - Optional stream index to filter
|
|
1457
|
+
*
|
|
1458
|
+
* @yields {Packet} Demuxed packets (must be freed by caller)
|
|
1459
|
+
*
|
|
1460
|
+
* @throws {FFmpegError} If reading fails with an error other than EOF (e.g. I/O failure
|
|
1461
|
+
* or a dropped network source) - only a real end-of-file ends the stream cleanly
|
|
1462
|
+
*
|
|
1463
|
+
* @throws {Error} If packet cloning fails
|
|
1464
|
+
*
|
|
1465
|
+
* @example
|
|
1466
|
+
* ```typescript
|
|
1467
|
+
* // Read all packets
|
|
1468
|
+
* for (const packet of input.packetsSync()) {
|
|
1469
|
+
* console.log(`Packet: stream=${packet.streamIndex}, pts=${packet.pts}`);
|
|
1470
|
+
* packet.free();
|
|
1471
|
+
* }
|
|
1472
|
+
* ```
|
|
1473
|
+
*
|
|
1474
|
+
* @example
|
|
1475
|
+
* ```typescript
|
|
1476
|
+
* // Read only video packets
|
|
1477
|
+
* const videoStream = input.video();
|
|
1478
|
+
* for (const packet of input.packetsSync(videoStream.index)) {
|
|
1479
|
+
* // Process video packet
|
|
1480
|
+
* packet.free();
|
|
1481
|
+
* }
|
|
1482
|
+
* ```
|
|
1483
|
+
*
|
|
1484
|
+
* @see {@link packets} For async version
|
|
1485
|
+
*/
|
|
1486
|
+
*packetsSync(index) {
|
|
1487
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
1488
|
+
try {
|
|
1489
|
+
const packet = __addDisposableResource(env_1, new Packet(), false);
|
|
1490
|
+
packet.alloc();
|
|
1491
|
+
let hasSeenKeyframe = !this.options.startWithKeyframe;
|
|
1492
|
+
while (!this.isClosed) {
|
|
1493
|
+
const ret = this.formatContext.readFrameSync(packet);
|
|
1494
|
+
if (ret < 0) {
|
|
1495
|
+
// No data available yet (live device capture) - blocking retry
|
|
1496
|
+
if (ret === AVERROR_EAGAIN) {
|
|
1497
|
+
sleepSync(10);
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
// Real read errors propagate; end-of-file and a deliberate
|
|
1501
|
+
// interrupt() (AVERROR_EXIT) end cleanly
|
|
1502
|
+
if (!FFmpegError.is(ret, AVERROR_EOF) && !FFmpegError.is(ret, AVERROR_EXIT)) {
|
|
1503
|
+
throw new FFmpegError(ret);
|
|
1504
|
+
}
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
// Get stream for timestamp processing
|
|
1508
|
+
const stream = this._streams[packet.streamIndex];
|
|
1509
|
+
if (stream) {
|
|
1510
|
+
// Set packet timebase to stream timebase
|
|
1511
|
+
// This must be done BEFORE any timestamp processing
|
|
1512
|
+
packet.timeBase = stream.timeBase;
|
|
1513
|
+
// Apply timestamp processing
|
|
1514
|
+
// 1. PTS wrap-around correction
|
|
1515
|
+
this.ptsWrapAroundCorrection(packet, stream);
|
|
1516
|
+
// 2. Timestamp discontinuity processing
|
|
1517
|
+
this.timestampDiscontinuityProcess(packet, stream);
|
|
1518
|
+
// 3. DTS prediction/update
|
|
1519
|
+
this.dtsPredict(packet, stream);
|
|
1520
|
+
}
|
|
1521
|
+
if (index === undefined || packet.streamIndex === index) {
|
|
1522
|
+
// If startWithKeyframe is enabled, skip packets until we see a keyframe
|
|
1523
|
+
// Only apply to video streams - audio packets should always pass through
|
|
1524
|
+
if (!hasSeenKeyframe) {
|
|
1525
|
+
const stream = this._streams[packet.streamIndex];
|
|
1526
|
+
const isVideoStream = stream?.codecpar.codecType === AVMEDIA_TYPE_VIDEO;
|
|
1527
|
+
if (isVideoStream && packet.isKeyframe) {
|
|
1528
|
+
hasSeenKeyframe = true;
|
|
1529
|
+
}
|
|
1530
|
+
else if (isVideoStream && !packet.isKeyframe) {
|
|
1531
|
+
// Skip video P-frames until first keyframe
|
|
1532
|
+
packet.unref();
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
// Non-video streams (audio, etc.) always pass through
|
|
1536
|
+
}
|
|
1537
|
+
// Clone the packet for the user
|
|
1538
|
+
// This creates a new Packet object that shares the same data buffer
|
|
1539
|
+
// through reference counting. The data won't be freed until both
|
|
1540
|
+
// the original and the clone are unreferenced.
|
|
1541
|
+
const cloned = packet.clone();
|
|
1542
|
+
if (!cloned) {
|
|
1543
|
+
throw new Error('Failed to clone packet (out of memory)');
|
|
1544
|
+
}
|
|
1545
|
+
yield cloned;
|
|
1546
|
+
}
|
|
1547
|
+
// Unreference the original packet's data buffer
|
|
1548
|
+
// This allows us to reuse the packet object for the next readFrame()
|
|
1549
|
+
// The data itself is still alive because the clone has a reference
|
|
1550
|
+
packet.unref();
|
|
1551
|
+
}
|
|
1552
|
+
// Signal EOF
|
|
1553
|
+
yield null;
|
|
1554
|
+
}
|
|
1555
|
+
catch (e_1) {
|
|
1556
|
+
env_1.error = e_1;
|
|
1557
|
+
env_1.hasError = true;
|
|
1558
|
+
}
|
|
1559
|
+
finally {
|
|
1560
|
+
__disposeResources(env_1);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Seek to timestamp in media.
|
|
1565
|
+
*
|
|
1566
|
+
* Seeks to the specified position in seconds.
|
|
1567
|
+
* Can seek in specific stream or globally.
|
|
1568
|
+
*
|
|
1569
|
+
* Direct mapping to av_seek_frame().
|
|
1570
|
+
*
|
|
1571
|
+
* @param timestamp - Target position in seconds
|
|
1572
|
+
*
|
|
1573
|
+
* @param streamIndex - Stream index or -1 for global (default: -1)
|
|
1574
|
+
*
|
|
1575
|
+
* @param flags - Seek flags (default: AVFLAG_NONE)
|
|
1576
|
+
*
|
|
1577
|
+
* @returns 0 on success, negative on error
|
|
1578
|
+
*
|
|
1579
|
+
* @throws {Error} If input is closed
|
|
1580
|
+
*
|
|
1581
|
+
* @example
|
|
1582
|
+
* ```typescript
|
|
1583
|
+
* // Seek to 30 seconds
|
|
1584
|
+
* const ret = await input.seek(30);
|
|
1585
|
+
* FFmpegError.throwIfError(ret, 'seek failed');
|
|
1586
|
+
* ```
|
|
1587
|
+
*
|
|
1588
|
+
* @example
|
|
1589
|
+
* ```typescript
|
|
1590
|
+
* import { AVSEEK_FLAG_BACKWARD } from 'node-av/constants';
|
|
1591
|
+
*
|
|
1592
|
+
* // Seek to keyframe before 60 seconds
|
|
1593
|
+
* await input.seek(60, -1, AVSEEK_FLAG_BACKWARD);
|
|
1594
|
+
* ```
|
|
1595
|
+
*
|
|
1596
|
+
* @see {@link AVSeekFlag} For seek flags
|
|
1597
|
+
*/
|
|
1598
|
+
async seek(timestamp, streamIndex = -1, flags = AVFLAG_NONE) {
|
|
1599
|
+
this.signal?.throwIfAborted();
|
|
1600
|
+
if (this.isClosed) {
|
|
1601
|
+
throw new Error('Cannot seek on closed input');
|
|
1602
|
+
}
|
|
1603
|
+
// Convert seconds to AV_TIME_BASE
|
|
1604
|
+
const ts = BigInt(Math.floor(timestamp * 1000000));
|
|
1605
|
+
return this.formatContext.seekFrame(streamIndex, ts, flags);
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Seek to timestamp in media synchronously.
|
|
1609
|
+
* Synchronous version of seek.
|
|
1610
|
+
*
|
|
1611
|
+
* Seeks to the specified position in seconds.
|
|
1612
|
+
* Can seek in specific stream or globally.
|
|
1613
|
+
*
|
|
1614
|
+
* Direct mapping to av_seek_frame().
|
|
1615
|
+
*
|
|
1616
|
+
* @param timestamp - Target position in seconds
|
|
1617
|
+
*
|
|
1618
|
+
* @param streamIndex - Stream index or -1 for global (default: -1)
|
|
1619
|
+
*
|
|
1620
|
+
* @param flags - Seek flags (default: AVFLAG_NONE)
|
|
1621
|
+
*
|
|
1622
|
+
* @returns 0 on success, negative on error
|
|
1623
|
+
*
|
|
1624
|
+
* @throws {Error} If input is closed
|
|
1625
|
+
*
|
|
1626
|
+
* @example
|
|
1627
|
+
* ```typescript
|
|
1628
|
+
* // Seek to 30 seconds
|
|
1629
|
+
* const ret = input.seekSync(30);
|
|
1630
|
+
* FFmpegError.throwIfError(ret, 'seek failed');
|
|
1631
|
+
* ```
|
|
1632
|
+
*
|
|
1633
|
+
* @example
|
|
1634
|
+
* ```typescript
|
|
1635
|
+
* import { AVSEEK_FLAG_BACKWARD } from 'node-av/constants';
|
|
1636
|
+
*
|
|
1637
|
+
* // Seek to keyframe before 60 seconds
|
|
1638
|
+
* input.seekSync(60, -1, AVSEEK_FLAG_BACKWARD);
|
|
1639
|
+
* ```
|
|
1640
|
+
*
|
|
1641
|
+
* @see {@link seek} For async version
|
|
1642
|
+
*/
|
|
1643
|
+
seekSync(timestamp, streamIndex = -1, flags = AVFLAG_NONE) {
|
|
1644
|
+
if (this.isClosed) {
|
|
1645
|
+
throw new Error('Cannot seek on closed input');
|
|
1646
|
+
}
|
|
1647
|
+
// Convert seconds to AV_TIME_BASE
|
|
1648
|
+
const ts = BigInt(Math.floor(timestamp * 1000000));
|
|
1649
|
+
return this.formatContext.seekFrameSync(streamIndex, ts, flags);
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Wake the demux thread if it is parked waiting for a full queue to drain.
|
|
1653
|
+
*
|
|
1654
|
+
* @internal
|
|
1655
|
+
*/
|
|
1656
|
+
wakeDemuxThread() {
|
|
1657
|
+
const resolve = this.drainResolver;
|
|
1658
|
+
if (resolve) {
|
|
1659
|
+
this.drainResolver = null;
|
|
1660
|
+
resolve();
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Interrupt a blocking read without closing the demuxer.
|
|
1665
|
+
*
|
|
1666
|
+
* Aborts any in-progress `av_read_frame()` (e.g. on a quiet RTSP/network source
|
|
1667
|
+
* or a stalled Readable/custom I/O source that is waiting for data) and signals
|
|
1668
|
+
* end-of-stream to packet consumers, so an active pipeline can drain and finish.
|
|
1669
|
+
* The demuxer is not freed and its streams stay valid - call {@link close}
|
|
1670
|
+
* afterwards to release resources.
|
|
1671
|
+
*
|
|
1672
|
+
* Mainly used by pipeline teardown: a blocking read only unblocks on close, but
|
|
1673
|
+
* closing while the pipeline is still draining would free the context too early,
|
|
1674
|
+
* so the read is interrupted first and the input closed once processing settles.
|
|
1675
|
+
*
|
|
1676
|
+
* @example
|
|
1677
|
+
* ```typescript
|
|
1678
|
+
* control.stop();
|
|
1679
|
+
* input.interrupt(); // unblock a stalled read so completion can resolve
|
|
1680
|
+
* await control.completion;
|
|
1681
|
+
* await input.close();
|
|
1682
|
+
* ```
|
|
1683
|
+
*
|
|
1684
|
+
* @see {@link close} To close and free the demuxer
|
|
1685
|
+
*/
|
|
1686
|
+
interrupt() {
|
|
1687
|
+
if (this.isClosed) {
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
// Abort the native blocking read so readFrame() returns immediately.
|
|
1691
|
+
this.formatContext.interrupt();
|
|
1692
|
+
// Signal EOF to consumers and wake any generator parked on an empty queue.
|
|
1693
|
+
this.demuxEof = true;
|
|
1694
|
+
this.wakeDemuxThread();
|
|
1695
|
+
for (const resolvers of this.queueResolvers.values()) {
|
|
1696
|
+
for (const resolve of resolvers) {
|
|
1697
|
+
resolve();
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
this.queueResolvers.clear();
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Start the internal demux thread for handling multiple parallel packet generators.
|
|
1704
|
+
* This thread reads packets from the format context and distributes them to queues.
|
|
1705
|
+
*
|
|
1706
|
+
* @internal
|
|
1707
|
+
*/
|
|
1708
|
+
startDemuxThread() {
|
|
1709
|
+
if (this.demuxThreadActive || this.demuxThread) {
|
|
1710
|
+
return; // Already running
|
|
1711
|
+
}
|
|
1712
|
+
this.demuxThreadActive = true;
|
|
1713
|
+
if (this.signal && !this.signalCleanup) {
|
|
1714
|
+
const handler = () => {
|
|
1715
|
+
this.demuxThreadActive = false;
|
|
1716
|
+
// Abort the native blocking read too - setting the flag alone leaves the
|
|
1717
|
+
// demux thread parked in av_read_frame() until data happens to arrive.
|
|
1718
|
+
// interrupt() also signals EOF and wakes the resolvers below.
|
|
1719
|
+
this.interrupt();
|
|
1720
|
+
};
|
|
1721
|
+
this.signal.addEventListener('abort', handler, { once: true });
|
|
1722
|
+
this.signalCleanup = () => this.signal?.removeEventListener('abort', handler);
|
|
1723
|
+
}
|
|
1724
|
+
this.demuxThread = (async () => {
|
|
1725
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
1726
|
+
try {
|
|
1727
|
+
const packet = __addDisposableResource(env_2, new Packet(), false);
|
|
1728
|
+
packet.alloc();
|
|
1729
|
+
while (this.demuxThreadActive && !this.isClosed) {
|
|
1730
|
+
// Read next packet
|
|
1731
|
+
const ret = await this.formatContext.readFrame(packet);
|
|
1732
|
+
// IMPORTANT: Check isClosed again after await - the demuxer may have been
|
|
1733
|
+
// closed while we were waiting for readFrame(). If closed, the native
|
|
1734
|
+
// AVStreams have been freed and accessing them would cause use-after-free!
|
|
1735
|
+
if (this.isClosed) {
|
|
1736
|
+
break;
|
|
1737
|
+
}
|
|
1738
|
+
if (ret < 0) {
|
|
1739
|
+
// EAGAIN means no data available yet (common with live device capture)
|
|
1740
|
+
// Retry after a short delay instead of treating as EOF
|
|
1741
|
+
// Matches FFmpeg CLI behavior: av_usleep(10000) in ffmpeg_demux.c
|
|
1742
|
+
if (ret === AVERROR_EAGAIN) {
|
|
1743
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
// Anything but a genuine end-of-file is a real read error (I/O
|
|
1747
|
+
// failure, dropped network source, ...). Store it so consumers throw
|
|
1748
|
+
// instead of seeing a clean EOF - matching FFmpeg CLI, where a read
|
|
1749
|
+
// error ends processing with a failure, not a normal finish.
|
|
1750
|
+
// AVERROR_EXIT is exempt: it is how interrupt() aborts a blocking
|
|
1751
|
+
// read on purpose (pipeline stop/teardown), not a source failure.
|
|
1752
|
+
if (!FFmpegError.is(ret, AVERROR_EOF) && !FFmpegError.is(ret, AVERROR_EXIT)) {
|
|
1753
|
+
this.lastError = new FFmpegError(ret);
|
|
1754
|
+
}
|
|
1755
|
+
// End of stream (or error) - notify all waiting consumers
|
|
1756
|
+
this.demuxEof = true;
|
|
1757
|
+
for (const resolvers of this.queueResolvers.values()) {
|
|
1758
|
+
for (const resolve of resolvers) {
|
|
1759
|
+
resolve();
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
this.queueResolvers.clear();
|
|
1763
|
+
break;
|
|
1764
|
+
}
|
|
1765
|
+
// Get stream for timestamp processing
|
|
1766
|
+
const stream = this._streams[packet.streamIndex];
|
|
1767
|
+
if (stream) {
|
|
1768
|
+
packet.timeBase = stream.timeBase;
|
|
1769
|
+
this.ptsWrapAroundCorrection(packet, stream);
|
|
1770
|
+
this.timestampDiscontinuityProcess(packet, stream);
|
|
1771
|
+
this.dtsPredict(packet, stream);
|
|
1772
|
+
}
|
|
1773
|
+
// Find which queues need this packet. Select by existence, not by
|
|
1774
|
+
// fullness: each consumer registers a queue for its stream and needs
|
|
1775
|
+
// EVERY packet of that stream. Only streams with no registered consumer
|
|
1776
|
+
// (e.g. a subtitle track absent from the pipeline) are skipped here.
|
|
1777
|
+
const allQueue = this.packetQueues.get('all');
|
|
1778
|
+
const streamQueue = this.packetQueues.get(packet.streamIndex);
|
|
1779
|
+
const targetQueues = [];
|
|
1780
|
+
if (allQueue) {
|
|
1781
|
+
targetQueues.push({ queue: allQueue, key: 'all', event: 'packet-all' });
|
|
1782
|
+
}
|
|
1783
|
+
// Only add stream queue if it's different from 'all' queue
|
|
1784
|
+
if (streamQueue && streamQueue !== allQueue) {
|
|
1785
|
+
targetQueues.push({ queue: streamQueue, key: packet.streamIndex, event: `packet-${packet.streamIndex}` });
|
|
1786
|
+
}
|
|
1787
|
+
if (targetQueues.length === 0) {
|
|
1788
|
+
// No consumer for this stream, skip it
|
|
1789
|
+
packet.unref();
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
// Backpressure: if a target queue is full, wait until the consumer drains
|
|
1793
|
+
// it instead of dropping the packet. Dropping loses reference frames and
|
|
1794
|
+
// corrupts decoding (e.g. H.264 "missing reference picture") - the bug
|
|
1795
|
+
// that surfaced with multi-stream pipelines where one stream (video)
|
|
1796
|
+
// decodes slower than the other (audio). The read loop is paced by the
|
|
1797
|
+
// slowest consumer; faster consumers simply idle on an empty queue.
|
|
1798
|
+
//
|
|
1799
|
+
// Only wait on queues whose consumer is still live: when a generator stops
|
|
1800
|
+
// it removes its queue from packetQueues, so `get(key) !== target.queue`
|
|
1801
|
+
// means that consumer is gone - skip it instead of blocking forever, which
|
|
1802
|
+
// would starve the other streams and deadlock teardown.
|
|
1803
|
+
let abandoned = false;
|
|
1804
|
+
while (this.demuxThreadActive && !this.isClosed) {
|
|
1805
|
+
let queueFull = false;
|
|
1806
|
+
let anyLive = false;
|
|
1807
|
+
for (const target of targetQueues) {
|
|
1808
|
+
if (this.packetQueues.get(target.key) !== target.queue) {
|
|
1809
|
+
continue; // consumer gone - do not block on its queue
|
|
1810
|
+
}
|
|
1811
|
+
anyLive = true;
|
|
1812
|
+
if (target.queue.length >= MAX_INPUT_QUEUE_SIZE) {
|
|
1813
|
+
queueFull = true;
|
|
1814
|
+
break;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
if (!anyLive) {
|
|
1818
|
+
// Every target's consumer has stopped - drop this packet.
|
|
1819
|
+
abandoned = true;
|
|
1820
|
+
break;
|
|
1821
|
+
}
|
|
1822
|
+
if (!queueFull) {
|
|
1823
|
+
break;
|
|
1824
|
+
}
|
|
1825
|
+
// Wait until a consumer frees a slot or the demuxer shuts down. Polling here would keep a CPU core busy.
|
|
1826
|
+
const { promise: drained, resolve: onDrain } = Promise.withResolvers();
|
|
1827
|
+
this.drainResolver = onDrain;
|
|
1828
|
+
await drained;
|
|
1829
|
+
}
|
|
1830
|
+
if (abandoned || !this.demuxThreadActive || this.isClosed) {
|
|
1831
|
+
packet.unref();
|
|
1832
|
+
if (abandoned) {
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
break;
|
|
1836
|
+
}
|
|
1837
|
+
// Distribute to every live target queue. Each gets its own clone, which
|
|
1838
|
+
// shares the data buffer via reference counting. Skip any consumer that
|
|
1839
|
+
// vanished during the backpressure wait so its clone is not leaked into
|
|
1840
|
+
// an orphaned queue.
|
|
1841
|
+
for (const target of targetQueues) {
|
|
1842
|
+
if (this.packetQueues.get(target.key) !== target.queue) {
|
|
1843
|
+
continue; // consumer gone
|
|
1844
|
+
}
|
|
1845
|
+
const clone = packet.clone();
|
|
1846
|
+
if (!clone) {
|
|
1847
|
+
throw new Error('Failed to clone packet in demux thread (out of memory)');
|
|
1848
|
+
}
|
|
1849
|
+
target.queue.push(clone);
|
|
1850
|
+
// Wake every consumer waiting on this key - only one gets the packet,
|
|
1851
|
+
// the others re-park, but a lost wakeup would deadlock them.
|
|
1852
|
+
const resolvers = this.queueResolvers.get(target.key);
|
|
1853
|
+
if (resolvers) {
|
|
1854
|
+
this.queueResolvers.delete(target.key);
|
|
1855
|
+
for (const resolve of resolvers) {
|
|
1856
|
+
resolve();
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
packet.unref();
|
|
1861
|
+
}
|
|
1862
|
+
this.demuxThreadActive = false;
|
|
1863
|
+
}
|
|
1864
|
+
catch (e_2) {
|
|
1865
|
+
env_2.error = e_2;
|
|
1866
|
+
env_2.hasError = true;
|
|
1867
|
+
}
|
|
1868
|
+
finally {
|
|
1869
|
+
__disposeResources(env_2);
|
|
1870
|
+
}
|
|
1871
|
+
})();
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Stop the internal demux thread.
|
|
1875
|
+
*
|
|
1876
|
+
* @internal
|
|
1877
|
+
*/
|
|
1878
|
+
async stopDemuxThread() {
|
|
1879
|
+
this.demuxThreadActive = false;
|
|
1880
|
+
this.demuxEof = true;
|
|
1881
|
+
this.wakeDemuxThread();
|
|
1882
|
+
// Wake up any waiting generators
|
|
1883
|
+
for (const resolvers of this.queueResolvers.values()) {
|
|
1884
|
+
for (const resolve of resolvers) {
|
|
1885
|
+
resolve();
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
this.queueResolvers.clear();
|
|
1889
|
+
// Wait for demux thread with timeout to avoid hanging on blocked reads
|
|
1890
|
+
if (this.demuxThread) {
|
|
1891
|
+
const threadPromise = this.demuxThread;
|
|
1892
|
+
let timer;
|
|
1893
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
1894
|
+
timer = setTimeout(resolve, 2000);
|
|
1895
|
+
unrefTimer(timer);
|
|
1896
|
+
});
|
|
1897
|
+
await Promise.race([threadPromise, timeoutPromise]);
|
|
1898
|
+
clearTimeout(timer);
|
|
1899
|
+
this.demuxThread = null;
|
|
1900
|
+
}
|
|
1901
|
+
// Clear all queues
|
|
1902
|
+
for (const queue of this.packetQueues.values()) {
|
|
1903
|
+
for (const packet of queue) {
|
|
1904
|
+
packet.free();
|
|
1905
|
+
}
|
|
1906
|
+
queue.length = 0;
|
|
1907
|
+
}
|
|
1908
|
+
this.packetQueues.clear();
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Get or create stream state for timestamp processing.
|
|
1912
|
+
*
|
|
1913
|
+
* @param streamIndex - Stream index
|
|
1914
|
+
*
|
|
1915
|
+
* @returns Stream state
|
|
1916
|
+
*
|
|
1917
|
+
* @internal
|
|
1918
|
+
*/
|
|
1919
|
+
getStreamState(streamIndex) {
|
|
1920
|
+
let state = this.streamStates.get(streamIndex);
|
|
1921
|
+
if (!state) {
|
|
1922
|
+
state = {
|
|
1923
|
+
wrapCorrectionDone: false,
|
|
1924
|
+
sawFirstTs: false,
|
|
1925
|
+
firstDts: AV_NOPTS_VALUE,
|
|
1926
|
+
nextDts: AV_NOPTS_VALUE,
|
|
1927
|
+
dts: AV_NOPTS_VALUE,
|
|
1928
|
+
};
|
|
1929
|
+
this.streamStates.set(streamIndex, state);
|
|
1930
|
+
}
|
|
1931
|
+
return state;
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* PTS Wrap-Around Correction.
|
|
1935
|
+
*
|
|
1936
|
+
* Based on FFmpeg's ts_fixup().
|
|
1937
|
+
*
|
|
1938
|
+
* Corrects timestamp wrap-around for streams with limited timestamp bits.
|
|
1939
|
+
* DVB streams typically use 31-bit timestamps that wrap around.
|
|
1940
|
+
* Without correction, timestamps become negative causing playback errors.
|
|
1941
|
+
*
|
|
1942
|
+
* Handles:
|
|
1943
|
+
* - Detects wrap-around based on pts_wrap_bits from stream
|
|
1944
|
+
* - Applies correction once per stream
|
|
1945
|
+
* - Corrects both PTS and DTS
|
|
1946
|
+
*
|
|
1947
|
+
* @param packet - Packet to correct
|
|
1948
|
+
*
|
|
1949
|
+
* @param stream - Stream metadata
|
|
1950
|
+
*
|
|
1951
|
+
* @internal
|
|
1952
|
+
*/
|
|
1953
|
+
ptsWrapAroundCorrection(packet, stream) {
|
|
1954
|
+
const state = this.getStreamState(packet.streamIndex);
|
|
1955
|
+
// Already corrected or no wrap bits configured
|
|
1956
|
+
if (state.wrapCorrectionDone || stream.ptsWrapBits >= 64) {
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
const startTime = this.formatContext.startTime;
|
|
1960
|
+
if (startTime === AV_NOPTS_VALUE) {
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
const ptsWrapBits = stream.ptsWrapBits;
|
|
1964
|
+
// Rescale start_time to packet's timebase
|
|
1965
|
+
// Note: packet.timeBase was set to stream.timeBase in packets() generator
|
|
1966
|
+
const stime = avRescaleQ(startTime, AV_TIME_BASE_Q, packet.timeBase);
|
|
1967
|
+
const stime2 = stime + (1n << BigInt(ptsWrapBits));
|
|
1968
|
+
state.wrapCorrectionDone = true;
|
|
1969
|
+
const wrapThreshold = stime + (1n << BigInt(ptsWrapBits - 1));
|
|
1970
|
+
// Check DTS for wrap-around
|
|
1971
|
+
if (stime2 > stime && packet.dts !== AV_NOPTS_VALUE && packet.dts > wrapThreshold) {
|
|
1972
|
+
packet.dts -= 1n << BigInt(ptsWrapBits);
|
|
1973
|
+
state.wrapCorrectionDone = false; // May wrap again
|
|
1974
|
+
}
|
|
1975
|
+
// Check PTS for wrap-around
|
|
1976
|
+
if (stime2 > stime && packet.pts !== AV_NOPTS_VALUE && packet.pts > wrapThreshold) {
|
|
1977
|
+
packet.pts -= 1n << BigInt(ptsWrapBits);
|
|
1978
|
+
state.wrapCorrectionDone = false; // May wrap again
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* DTS Prediction and Update.
|
|
1983
|
+
*
|
|
1984
|
+
* Based on FFmpeg's ist_dts_update().
|
|
1985
|
+
*
|
|
1986
|
+
* Predicts next expected DTS for frame ordering validation and discontinuity detection.
|
|
1987
|
+
* Uses codec-specific logic:
|
|
1988
|
+
* - Audio: Based on sample_rate and frame_size
|
|
1989
|
+
* - Video: Based on framerate or duration
|
|
1990
|
+
*
|
|
1991
|
+
* Handles:
|
|
1992
|
+
* - First timestamp initialization
|
|
1993
|
+
* - Codec-specific duration calculation
|
|
1994
|
+
* - DTS sequence tracking
|
|
1995
|
+
*
|
|
1996
|
+
* @param packet - Packet to process
|
|
1997
|
+
*
|
|
1998
|
+
* @param stream - Stream metadata
|
|
1999
|
+
*
|
|
2000
|
+
* @internal
|
|
2001
|
+
*/
|
|
2002
|
+
dtsPredict(packet, stream) {
|
|
2003
|
+
const state = this.getStreamState(packet.streamIndex);
|
|
2004
|
+
// Call native implementation with native objects
|
|
2005
|
+
const newState = nativeDtsPredict(packet, stream, {
|
|
2006
|
+
sawFirstTs: state.sawFirstTs,
|
|
2007
|
+
dts: state.dts,
|
|
2008
|
+
nextDts: state.nextDts,
|
|
2009
|
+
firstDts: state.firstDts,
|
|
2010
|
+
});
|
|
2011
|
+
// Update state with results
|
|
2012
|
+
state.sawFirstTs = newState.sawFirstTs;
|
|
2013
|
+
state.dts = newState.dts;
|
|
2014
|
+
state.nextDts = newState.nextDts;
|
|
2015
|
+
state.firstDts = newState.firstDts;
|
|
2016
|
+
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Timestamp Discontinuity Detection.
|
|
2019
|
+
*
|
|
2020
|
+
* Based on FFmpeg's ts_discontinuity_detect().
|
|
2021
|
+
*
|
|
2022
|
+
* Detects and corrects timestamp discontinuities in streams.
|
|
2023
|
+
* Handles two cases:
|
|
2024
|
+
* - Discontinuous formats (MPEG-TS): Apply offset correction
|
|
2025
|
+
* - Continuous formats (MP4): Mark timestamps as invalid
|
|
2026
|
+
*
|
|
2027
|
+
* Handles:
|
|
2028
|
+
* - Format-specific discontinuity handling (AVFMT_TS_DISCONT flag)
|
|
2029
|
+
* - PTS wrap-around detection for streams with limited timestamp bits
|
|
2030
|
+
* - Intra-stream discontinuity detection
|
|
2031
|
+
* - Inter-stream discontinuity detection
|
|
2032
|
+
* - Offset accumulation and application
|
|
2033
|
+
* - copyTs mode with selective correction
|
|
2034
|
+
*
|
|
2035
|
+
* @param packet - Packet to check for discontinuities
|
|
2036
|
+
*
|
|
2037
|
+
* @param stream - Stream metadata
|
|
2038
|
+
*
|
|
2039
|
+
* @internal
|
|
2040
|
+
*/
|
|
2041
|
+
timestampDiscontinuityDetect(packet, stream) {
|
|
2042
|
+
const state = this.getStreamState(packet.streamIndex);
|
|
2043
|
+
const inputFormat = this.formatContext.iformat;
|
|
2044
|
+
// Check if format declares timestamp discontinuities
|
|
2045
|
+
const fmtIsDiscont = !!(inputFormat && inputFormat.flags & AVFMT_TS_DISCONT);
|
|
2046
|
+
// Disable correction when copyTs is enabled
|
|
2047
|
+
let disableDiscontinuityCorrection = this.options.copyTs;
|
|
2048
|
+
// Rescale packet DTS to AV_TIME_BASE for comparison
|
|
2049
|
+
const pktDts = avRescaleQRnd(packet.dts, packet.timeBase, AV_TIME_BASE_Q, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
|
|
2050
|
+
// PTS wrap-around detection
|
|
2051
|
+
// Only applies when copyTs is enabled and stream has limited timestamp bits
|
|
2052
|
+
if (this.options.copyTs && state.nextDts !== AV_NOPTS_VALUE && fmtIsDiscont && stream.ptsWrapBits < 60) {
|
|
2053
|
+
// Calculate wrapped DTS by adding 2^pts_wrap_bits to packet DTS
|
|
2054
|
+
const wrapDts = avRescaleQRnd(packet.dts + (1n << BigInt(stream.ptsWrapBits)), packet.timeBase, AV_TIME_BASE_Q, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
|
|
2055
|
+
// If wrapped DTS is closer to predicted nextDts, enable correction
|
|
2056
|
+
const wrapDelta = wrapDts > state.nextDts ? wrapDts - state.nextDts : state.nextDts - wrapDts;
|
|
2057
|
+
const normalDelta = pktDts > state.nextDts ? pktDts - state.nextDts : state.nextDts - pktDts;
|
|
2058
|
+
if (wrapDelta < normalDelta / 10n) {
|
|
2059
|
+
disableDiscontinuityCorrection = false;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
// Intra-stream discontinuity detection
|
|
2063
|
+
if (state.nextDts !== AV_NOPTS_VALUE && !disableDiscontinuityCorrection) {
|
|
2064
|
+
const delta = pktDts - state.nextDts;
|
|
2065
|
+
if (fmtIsDiscont) {
|
|
2066
|
+
// Discontinuous format (e.g., MPEG-TS) - apply offset correction
|
|
2067
|
+
const threshold = BigInt(this.options.dtsDeltaThreshold) * BigInt(AV_TIME_BASE);
|
|
2068
|
+
if (delta > threshold || delta < -threshold || pktDts + BigInt(AV_TIME_BASE) / 10n < state.dts) {
|
|
2069
|
+
this.tsOffsetDiscont -= delta;
|
|
2070
|
+
// Apply correction to packet
|
|
2071
|
+
const deltaInPktTb = avRescaleQ(delta, AV_TIME_BASE_Q, packet.timeBase);
|
|
2072
|
+
packet.dts -= deltaInPktTb;
|
|
2073
|
+
if (packet.pts !== AV_NOPTS_VALUE) {
|
|
2074
|
+
packet.pts -= deltaInPktTb;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
else {
|
|
2079
|
+
// Continuous format (e.g., MP4) - mark invalid timestamps
|
|
2080
|
+
const threshold = BigInt(this.options.dtsErrorThreshold) * BigInt(AV_TIME_BASE);
|
|
2081
|
+
// Check DTS
|
|
2082
|
+
if (delta > threshold || delta < -threshold) {
|
|
2083
|
+
packet.dts = AV_NOPTS_VALUE;
|
|
2084
|
+
}
|
|
2085
|
+
// Check PTS
|
|
2086
|
+
if (packet.pts !== AV_NOPTS_VALUE) {
|
|
2087
|
+
const pktPts = avRescaleQ(packet.pts, packet.timeBase, AV_TIME_BASE_Q);
|
|
2088
|
+
const ptsDelta = pktPts - state.nextDts;
|
|
2089
|
+
if (ptsDelta > threshold || ptsDelta < -threshold) {
|
|
2090
|
+
packet.pts = AV_NOPTS_VALUE;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
else if (state.nextDts === AV_NOPTS_VALUE && !this.options.copyTs && fmtIsDiscont && this.lastTs !== AV_NOPTS_VALUE) {
|
|
2096
|
+
// Inter-stream discontinuity detection
|
|
2097
|
+
const delta = pktDts - this.lastTs;
|
|
2098
|
+
const threshold = BigInt(this.options.dtsDeltaThreshold) * BigInt(AV_TIME_BASE);
|
|
2099
|
+
if (delta > threshold || delta < -threshold) {
|
|
2100
|
+
this.tsOffsetDiscont -= delta;
|
|
2101
|
+
// Apply correction to packet
|
|
2102
|
+
const deltaInPktTb = avRescaleQ(delta, AV_TIME_BASE_Q, packet.timeBase);
|
|
2103
|
+
packet.dts -= deltaInPktTb;
|
|
2104
|
+
if (packet.pts !== AV_NOPTS_VALUE) {
|
|
2105
|
+
packet.pts -= deltaInPktTb;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
// Update last timestamp
|
|
2110
|
+
this.lastTs = avRescaleQ(packet.dts, packet.timeBase, AV_TIME_BASE_Q);
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* Timestamp Discontinuity Processing - main entry point.
|
|
2114
|
+
*
|
|
2115
|
+
* Based on FFmpeg's ts_discontinuity_process().
|
|
2116
|
+
*
|
|
2117
|
+
* Applies accumulated discontinuity offset and detects new discontinuities.
|
|
2118
|
+
* Must be called for every packet before other timestamp processing.
|
|
2119
|
+
*
|
|
2120
|
+
* Handles:
|
|
2121
|
+
* - Applying previously-detected offset to all streams
|
|
2122
|
+
* - Detecting new discontinuities for audio/video streams
|
|
2123
|
+
*
|
|
2124
|
+
* @param packet - Packet to process
|
|
2125
|
+
*
|
|
2126
|
+
* @param stream - Stream metadata
|
|
2127
|
+
*
|
|
2128
|
+
* @internal
|
|
2129
|
+
*/
|
|
2130
|
+
timestampDiscontinuityProcess(packet, stream) {
|
|
2131
|
+
// Apply previously-detected discontinuity offset
|
|
2132
|
+
// This applies to ALL streams, not just audio/video
|
|
2133
|
+
const offset = avRescaleQ(this.tsOffsetDiscont, AV_TIME_BASE_Q, packet.timeBase);
|
|
2134
|
+
if (packet.dts !== AV_NOPTS_VALUE) {
|
|
2135
|
+
packet.dts += offset;
|
|
2136
|
+
}
|
|
2137
|
+
if (packet.pts !== AV_NOPTS_VALUE) {
|
|
2138
|
+
packet.pts += offset;
|
|
2139
|
+
}
|
|
2140
|
+
// Detect new timestamp discontinuities for audio/video
|
|
2141
|
+
const par = stream.codecpar;
|
|
2142
|
+
if ((par.codecType === AVMEDIA_TYPE_VIDEO || par.codecType === AVMEDIA_TYPE_AUDIO) && packet.dts !== AV_NOPTS_VALUE) {
|
|
2143
|
+
this.timestampDiscontinuityDetect(packet, stream);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Close demuxer and free resources.
|
|
2148
|
+
*
|
|
2149
|
+
* Releases format context and I/O context.
|
|
2150
|
+
* A read waiting for a stalled Readable or custom I/O source is abandoned,
|
|
2151
|
+
* so closing never waits for the source to deliver.
|
|
2152
|
+
* Safe to call multiple times.
|
|
2153
|
+
* Automatically called by Symbol.asyncDispose.
|
|
2154
|
+
*
|
|
2155
|
+
* Direct mapping to avformat_close_input().
|
|
2156
|
+
*
|
|
2157
|
+
* @example
|
|
2158
|
+
* ```typescript
|
|
2159
|
+
* const input = await Demuxer.open('video.mp4');
|
|
2160
|
+
* try {
|
|
2161
|
+
* // Use input
|
|
2162
|
+
* } finally {
|
|
2163
|
+
* await input.close();
|
|
2164
|
+
* }
|
|
2165
|
+
* ```
|
|
2166
|
+
*
|
|
2167
|
+
* @see {@link Symbol.asyncDispose} For automatic cleanup
|
|
2168
|
+
*/
|
|
2169
|
+
async close() {
|
|
2170
|
+
if (this.isClosed) {
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
this.isClosed = true;
|
|
2174
|
+
// Clean up abort signal listener
|
|
2175
|
+
this.signalCleanup?.();
|
|
2176
|
+
this.signalCleanup = undefined;
|
|
2177
|
+
// Signal demux thread to stop FIRST
|
|
2178
|
+
this.demuxThreadActive = false;
|
|
2179
|
+
// Set EOF flag so generators know to exit
|
|
2180
|
+
this.demuxEof = true;
|
|
2181
|
+
this.wakeDemuxThread();
|
|
2182
|
+
// Wake up all waiting generators BEFORE closing format context
|
|
2183
|
+
// This ensures generators can exit cleanly even if readFrame() is blocking
|
|
2184
|
+
for (const resolvers of this.queueResolvers.values()) {
|
|
2185
|
+
for (const resolve of resolvers) {
|
|
2186
|
+
resolve();
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
this.queueResolvers.clear();
|
|
2190
|
+
// Close FormatContext - this may interrupt blocking readFrame(). A custom
|
|
2191
|
+
// pb is detached inside, after the reader thread stopped: nulling it from
|
|
2192
|
+
// here would pull it out from under a running av_read_frame()
|
|
2193
|
+
await this.formatContext.closeInput(!!this.ioContext);
|
|
2194
|
+
// Wait for demux thread with timeout to avoid hanging on blocked reads
|
|
2195
|
+
if (this.demuxThread) {
|
|
2196
|
+
const threadPromise = this.demuxThread;
|
|
2197
|
+
let timer;
|
|
2198
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
2199
|
+
timer = setTimeout(resolve, 2000);
|
|
2200
|
+
unrefTimer(timer);
|
|
2201
|
+
});
|
|
2202
|
+
await Promise.race([threadPromise, timeoutPromise]);
|
|
2203
|
+
clearTimeout(timer);
|
|
2204
|
+
this.demuxThread = null;
|
|
2205
|
+
}
|
|
2206
|
+
// Clean up packet queues
|
|
2207
|
+
for (const queue of this.packetQueues.values()) {
|
|
2208
|
+
for (const packet of queue) {
|
|
2209
|
+
packet.free();
|
|
2210
|
+
}
|
|
2211
|
+
queue.length = 0;
|
|
2212
|
+
}
|
|
2213
|
+
this.packetQueues.clear();
|
|
2214
|
+
this.packetQueueConsumers.clear();
|
|
2215
|
+
// NOW we can safely release the IOContext. Contexts we built ourselves are
|
|
2216
|
+
// callback-backed and go through freeContext(), which IOStream hooks to
|
|
2217
|
+
// detach its stream listeners. A caller-supplied context may instead own a
|
|
2218
|
+
// protocol handle from open2(); dispose picks avio_closep() for those, as
|
|
2219
|
+
// freeing them alone leaks the handle and keeps the file locked on Windows.
|
|
2220
|
+
if (this.ioContext) {
|
|
2221
|
+
if (this.externalIO) {
|
|
2222
|
+
await this.ioContext[Symbol.asyncDispose]();
|
|
2223
|
+
}
|
|
2224
|
+
else {
|
|
2225
|
+
this.ioContext.freeContext();
|
|
2226
|
+
}
|
|
2227
|
+
this.ioContext = undefined;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Close demuxer and free resources synchronously.
|
|
2232
|
+
* Synchronous version of close.
|
|
2233
|
+
*
|
|
2234
|
+
* Releases format context and I/O context.
|
|
2235
|
+
* A read waiting for a stalled Readable or custom I/O source is abandoned.
|
|
2236
|
+
* Safe to call multiple times.
|
|
2237
|
+
* Automatically called by Symbol.dispose.
|
|
2238
|
+
*
|
|
2239
|
+
* Direct mapping to avformat_close_input().
|
|
2240
|
+
*
|
|
2241
|
+
* @example
|
|
2242
|
+
* ```typescript
|
|
2243
|
+
* const input = Demuxer.openSync('video.mp4');
|
|
2244
|
+
* try {
|
|
2245
|
+
* // Use input
|
|
2246
|
+
* } finally {
|
|
2247
|
+
* input.closeSync();
|
|
2248
|
+
* }
|
|
2249
|
+
* ```
|
|
2250
|
+
*
|
|
2251
|
+
* @see {@link close} For async version
|
|
2252
|
+
*/
|
|
2253
|
+
closeSync() {
|
|
2254
|
+
if (this.isClosed) {
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
this.isClosed = true;
|
|
2258
|
+
// Clean up abort signal listener
|
|
2259
|
+
this.signalCleanup?.();
|
|
2260
|
+
this.signalCleanup = undefined;
|
|
2261
|
+
// Stop the demux thread and wake generators parked on an empty queue: they
|
|
2262
|
+
// resume once this returns and end, as in close()
|
|
2263
|
+
this.demuxThreadActive = false;
|
|
2264
|
+
this.demuxEof = true;
|
|
2265
|
+
this.wakeDemuxThread();
|
|
2266
|
+
for (const resolvers of this.queueResolvers.values()) {
|
|
2267
|
+
for (const resolve of resolvers) {
|
|
2268
|
+
resolve();
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
this.queueResolvers.clear();
|
|
2272
|
+
// Close FormatContext; a custom pb is detached inside so it is not closed with the input
|
|
2273
|
+
this.formatContext.closeInputSync(!!this.ioContext);
|
|
2274
|
+
for (const queue of this.packetQueues.values()) {
|
|
2275
|
+
for (const packet of queue) {
|
|
2276
|
+
packet.free();
|
|
2277
|
+
}
|
|
2278
|
+
queue.length = 0;
|
|
2279
|
+
}
|
|
2280
|
+
this.packetQueues.clear();
|
|
2281
|
+
this.packetQueueConsumers.clear();
|
|
2282
|
+
// NOW we can safely release the IOContext (see close() for why dispose)
|
|
2283
|
+
if (this.ioContext) {
|
|
2284
|
+
if (this.externalIO) {
|
|
2285
|
+
this.ioContext[Symbol.dispose]();
|
|
2286
|
+
}
|
|
2287
|
+
else {
|
|
2288
|
+
this.ioContext.freeContext();
|
|
2289
|
+
}
|
|
2290
|
+
this.ioContext = undefined;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
/**
|
|
2294
|
+
* Get underlying format context.
|
|
2295
|
+
*
|
|
2296
|
+
* Returns the internal format context for advanced operations.
|
|
2297
|
+
*
|
|
2298
|
+
* @returns Format context
|
|
2299
|
+
*
|
|
2300
|
+
* @internal
|
|
2301
|
+
*/
|
|
2302
|
+
getFormatContext() {
|
|
2303
|
+
return this.formatContext;
|
|
2304
|
+
}
|
|
2305
|
+
/**
|
|
2306
|
+
* Dispose of demuxer.
|
|
2307
|
+
*
|
|
2308
|
+
* Implements AsyncDisposable interface for automatic cleanup.
|
|
2309
|
+
* Equivalent to calling close().
|
|
2310
|
+
*
|
|
2311
|
+
* @example
|
|
2312
|
+
* ```typescript
|
|
2313
|
+
* {
|
|
2314
|
+
* await using input = await Demuxer.open('video.mp4');
|
|
2315
|
+
* // Process media...
|
|
2316
|
+
* } // Automatically closed
|
|
2317
|
+
* ```
|
|
2318
|
+
*
|
|
2319
|
+
* @see {@link close} For manual cleanup
|
|
2320
|
+
*/
|
|
2321
|
+
async [Symbol.asyncDispose]() {
|
|
2322
|
+
await this.close();
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* Dispose of demuxer synchronously.
|
|
2326
|
+
*
|
|
2327
|
+
* Implements Disposable interface for automatic cleanup.
|
|
2328
|
+
* Equivalent to calling closeSync().
|
|
2329
|
+
*
|
|
2330
|
+
* @example
|
|
2331
|
+
* ```typescript
|
|
2332
|
+
* {
|
|
2333
|
+
* using input = Demuxer.openSync('video.mp4');
|
|
2334
|
+
* // Process media...
|
|
2335
|
+
* } // Automatically closed
|
|
2336
|
+
* ```
|
|
2337
|
+
*
|
|
2338
|
+
* @see {@link closeSync} For manual cleanup
|
|
2339
|
+
*/
|
|
2340
|
+
[Symbol.dispose]() {
|
|
2341
|
+
this.closeSync();
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
//# sourceMappingURL=demuxer.js.map
|