node-av 3.1.3 → 4.0.0
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/README.md +65 -52
- package/binding.gyp +4 -0
- package/dist/api/audio-frame-buffer.d.ts +201 -0
- package/dist/api/audio-frame-buffer.js +275 -0
- package/dist/api/audio-frame-buffer.js.map +1 -0
- package/dist/api/bitstream-filter.d.ts +319 -78
- package/dist/api/bitstream-filter.js +680 -151
- package/dist/api/bitstream-filter.js.map +1 -1
- 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_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 +279 -17
- package/dist/api/decoder.js +998 -209
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/{media-input.d.ts → demuxer.d.ts} +294 -44
- package/dist/api/demuxer.js +1968 -0
- package/dist/api/demuxer.js.map +1 -0
- package/dist/api/encoder.d.ts +308 -50
- package/dist/api/encoder.js +1133 -111
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/filter-presets.d.ts +12 -5
- package/dist/api/filter-presets.js +21 -7
- package/dist/api/filter-presets.js.map +1 -1
- package/dist/api/filter.d.ts +406 -40
- package/dist/api/filter.js +966 -139
- package/dist/api/filter.js.map +1 -1
- package/dist/api/{fmp4.d.ts → fmp4-stream.d.ts} +141 -140
- package/dist/api/fmp4-stream.js +539 -0
- package/dist/api/fmp4-stream.js.map +1 -0
- package/dist/api/hardware.d.ts +58 -6
- package/dist/api/hardware.js +127 -11
- package/dist/api/hardware.js.map +1 -1
- package/dist/api/index.d.ts +6 -4
- package/dist/api/index.js +14 -8
- package/dist/api/index.js.map +1 -1
- package/dist/api/io-stream.d.ts +3 -3
- package/dist/api/io-stream.js +5 -4
- package/dist/api/io-stream.js.map +1 -1
- package/dist/api/{media-output.d.ts → muxer.d.ts} +274 -60
- package/dist/api/muxer.js +1934 -0
- package/dist/api/muxer.js.map +1 -0
- package/dist/api/pipeline.d.ts +77 -29
- package/dist/api/pipeline.js +435 -425
- package/dist/api/pipeline.js.map +1 -1
- package/dist/api/rtp-stream.d.ts +312 -0
- package/dist/api/rtp-stream.js +630 -0
- package/dist/api/rtp-stream.js.map +1 -0
- package/dist/api/types.d.ts +476 -55
- package/dist/api/utilities/async-queue.d.ts +91 -0
- package/dist/api/utilities/async-queue.js +162 -0
- package/dist/api/utilities/async-queue.js.map +1 -0
- package/dist/api/utilities/audio-sample.d.ts +1 -1
- package/dist/api/utilities/image.d.ts +1 -1
- package/dist/api/utilities/index.d.ts +2 -0
- package/dist/api/utilities/index.js +4 -0
- package/dist/api/utilities/index.js.map +1 -1
- package/dist/api/utilities/media-type.d.ts +1 -1
- package/dist/api/utilities/pixel-format.d.ts +1 -1
- package/dist/api/utilities/sample-format.d.ts +1 -1
- package/dist/api/utilities/scheduler.d.ts +169 -0
- package/dist/api/utilities/scheduler.js +136 -0
- package/dist/api/utilities/scheduler.js.map +1 -0
- package/dist/api/utilities/streaming.d.ts +74 -15
- package/dist/api/utilities/streaming.js +170 -12
- package/dist/api/utilities/streaming.js.map +1 -1
- package/dist/api/utilities/timestamp.d.ts +1 -1
- package/dist/api/webrtc-stream.d.ts +288 -0
- package/dist/api/webrtc-stream.js +440 -0
- package/dist/api/webrtc-stream.js.map +1 -0
- package/dist/constants/constants.d.ts +51 -1
- package/dist/constants/constants.js +47 -1
- package/dist/constants/constants.js.map +1 -1
- package/dist/constants/encoders.d.ts +2 -1
- package/dist/constants/encoders.js +4 -3
- package/dist/constants/encoders.js.map +1 -1
- 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 +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/lib/binding.d.ts +19 -8
- package/dist/lib/binding.js.map +1 -1
- package/dist/lib/codec-context.d.ts +87 -0
- package/dist/lib/codec-context.js +125 -4
- package/dist/lib/codec-context.js.map +1 -1
- package/dist/lib/codec-parameters.d.ts +183 -1
- package/dist/lib/codec-parameters.js +209 -0
- package/dist/lib/codec-parameters.js.map +1 -1
- package/dist/lib/codec-parser.d.ts +23 -0
- package/dist/lib/codec-parser.js +25 -0
- package/dist/lib/codec-parser.js.map +1 -1
- package/dist/lib/codec.d.ts +26 -4
- package/dist/lib/codec.js +35 -0
- package/dist/lib/codec.js.map +1 -1
- package/dist/lib/dictionary.js +1 -0
- package/dist/lib/dictionary.js.map +1 -1
- package/dist/lib/error.js +1 -1
- package/dist/lib/error.js.map +1 -1
- package/dist/lib/filter-context.d.ts +52 -11
- package/dist/lib/filter-context.js +56 -12
- package/dist/lib/filter-context.js.map +1 -1
- package/dist/lib/filter-graph.d.ts +9 -0
- package/dist/lib/filter-graph.js +13 -0
- package/dist/lib/filter-graph.js.map +1 -1
- package/dist/lib/filter.d.ts +21 -0
- package/dist/lib/filter.js +28 -0
- package/dist/lib/filter.js.map +1 -1
- package/dist/lib/format-context.d.ts +48 -14
- package/dist/lib/format-context.js +76 -7
- package/dist/lib/format-context.js.map +1 -1
- package/dist/lib/frame.d.ts +168 -0
- package/dist/lib/frame.js +212 -0
- package/dist/lib/frame.js.map +1 -1
- package/dist/lib/hardware-device-context.d.ts +3 -2
- package/dist/lib/hardware-device-context.js.map +1 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/input-format.d.ts +21 -0
- package/dist/lib/input-format.js +42 -2
- package/dist/lib/input-format.js.map +1 -1
- package/dist/lib/native-types.d.ts +48 -26
- package/dist/lib/option.d.ts +25 -13
- package/dist/lib/option.js +28 -0
- package/dist/lib/option.js.map +1 -1
- package/dist/lib/output-format.d.ts +22 -1
- package/dist/lib/output-format.js +28 -0
- package/dist/lib/output-format.js.map +1 -1
- package/dist/lib/packet.d.ts +35 -0
- package/dist/lib/packet.js +52 -2
- package/dist/lib/packet.js.map +1 -1
- package/dist/lib/stream.d.ts +126 -0
- package/dist/lib/stream.js +188 -5
- package/dist/lib/stream.js.map +1 -1
- 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 +27 -1
- package/dist/lib/utilities.d.ts +281 -53
- package/dist/lib/utilities.js +298 -55
- package/dist/lib/utilities.js.map +1 -1
- package/package.json +20 -19
- package/dist/api/fmp4.js +0 -710
- package/dist/api/fmp4.js.map +0 -1
- package/dist/api/media-input.js +0 -1075
- package/dist/api/media-input.js.map +0 -1
- package/dist/api/media-output.js +0 -1040
- package/dist/api/media-output.js.map +0 -1
- package/dist/api/webrtc.d.ts +0 -664
- package/dist/api/webrtc.js +0 -1132
- package/dist/api/webrtc.js.map +0 -1
package/dist/api/decoder.js
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 { AV_CODEC_FLAG_COPY_OPAQUE, AV_FRAME_FLAG_CORRUPT, AV_NOPTS_VALUE, AV_ROUND_UP, AVERROR_EAGAIN, AVERROR_EOF, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, INT_MAX, } from '../constants/constants.js';
|
|
54
|
+
import { CodecContext } from '../lib/codec-context.js';
|
|
55
|
+
import { Codec } from '../lib/codec.js';
|
|
56
|
+
import { Dictionary } from '../lib/dictionary.js';
|
|
57
|
+
import { FFmpegError } from '../lib/error.js';
|
|
58
|
+
import { Frame } from '../lib/frame.js';
|
|
59
|
+
import { Rational } from '../lib/rational.js';
|
|
60
|
+
import { avGcd, avInvQ, avMulQ, avRescaleDelta, avRescaleQ, avRescaleQRnd } from '../lib/utilities.js';
|
|
61
|
+
import { FRAME_THREAD_QUEUE_SIZE, PACKET_THREAD_QUEUE_SIZE } from './constants.js';
|
|
62
|
+
import { AsyncQueue } from './utilities/async-queue.js';
|
|
63
|
+
import { Scheduler } from './utilities/scheduler.js';
|
|
3
64
|
/**
|
|
4
65
|
* High-level decoder for audio and video streams.
|
|
5
66
|
*
|
|
@@ -10,10 +71,10 @@ import { Codec, CodecContext, Dictionary, FFmpegError, Frame } from '../lib/inde
|
|
|
10
71
|
*
|
|
11
72
|
* @example
|
|
12
73
|
* ```typescript
|
|
13
|
-
* import {
|
|
74
|
+
* import { Demuxer, Decoder } from 'node-av/api';
|
|
14
75
|
*
|
|
15
76
|
* // Open media and create decoder
|
|
16
|
-
* await using input = await
|
|
77
|
+
* await using input = await Demuxer.open('video.mp4');
|
|
17
78
|
* using decoder = await Decoder.create(input.video());
|
|
18
79
|
*
|
|
19
80
|
* // Decode frames
|
|
@@ -39,7 +100,7 @@ import { Codec, CodecContext, Dictionary, FFmpegError, Frame } from '../lib/inde
|
|
|
39
100
|
* ```
|
|
40
101
|
*
|
|
41
102
|
* @see {@link Encoder} For encoding frames to packets
|
|
42
|
-
* @see {@link
|
|
103
|
+
* @see {@link Demuxer} For reading media files
|
|
43
104
|
* @see {@link HardwareContext} For GPU acceleration
|
|
44
105
|
*/
|
|
45
106
|
export class Decoder {
|
|
@@ -50,6 +111,19 @@ export class Decoder {
|
|
|
50
111
|
initialized = true;
|
|
51
112
|
isClosed = false;
|
|
52
113
|
options;
|
|
114
|
+
// Frame tracking for PTS/duration estimation
|
|
115
|
+
lastFramePts = AV_NOPTS_VALUE;
|
|
116
|
+
lastFrameDurationEst = 0n;
|
|
117
|
+
lastFrameTb;
|
|
118
|
+
// Audio-specific frame tracking
|
|
119
|
+
lastFrameSampleRate = 0;
|
|
120
|
+
lastFilterInRescaleDelta = AV_NOPTS_VALUE;
|
|
121
|
+
// Worker pattern for push-based processing
|
|
122
|
+
inputQueue;
|
|
123
|
+
outputQueue;
|
|
124
|
+
workerPromise = null;
|
|
125
|
+
nextComponent = null;
|
|
126
|
+
pipeToPromise = null;
|
|
53
127
|
/**
|
|
54
128
|
* @param codecContext - Configured codec context
|
|
55
129
|
*
|
|
@@ -70,71 +144,67 @@ export class Decoder {
|
|
|
70
144
|
this.options = options;
|
|
71
145
|
this.frame = new Frame();
|
|
72
146
|
this.frame.alloc();
|
|
147
|
+
this.lastFrameTb = new Rational(0, 1);
|
|
148
|
+
this.inputQueue = new AsyncQueue(PACKET_THREAD_QUEUE_SIZE);
|
|
149
|
+
this.outputQueue = new AsyncQueue(FRAME_THREAD_QUEUE_SIZE);
|
|
73
150
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
* import { MediaInput, Decoder } from 'node-av/api';
|
|
94
|
-
*
|
|
95
|
-
* await using input = await MediaInput.open('video.mp4');
|
|
96
|
-
* using decoder = await Decoder.create(input.video());
|
|
97
|
-
* ```
|
|
98
|
-
*
|
|
99
|
-
* @example
|
|
100
|
-
* ```typescript
|
|
101
|
-
* using decoder = await Decoder.create(stream, {
|
|
102
|
-
* threads: 4,
|
|
103
|
-
* options: {
|
|
104
|
-
* 'refcounted_frames': '1',
|
|
105
|
-
* 'skip_frame': 'nonkey' // Only decode keyframes
|
|
106
|
-
* }
|
|
107
|
-
* });
|
|
108
|
-
* ```
|
|
109
|
-
*
|
|
110
|
-
* @example
|
|
111
|
-
* ```typescript
|
|
112
|
-
* const hw = HardwareContext.auto();
|
|
113
|
-
* using decoder = await Decoder.create(stream, {
|
|
114
|
-
* hardware: hw,
|
|
115
|
-
* threads: 0 // Auto-detect thread count
|
|
116
|
-
* exitOnError: false // Continue on decode errors (default: true)
|
|
117
|
-
* });
|
|
118
|
-
* ```
|
|
119
|
-
*
|
|
120
|
-
* @see {@link HardwareContext} For GPU acceleration setup
|
|
121
|
-
* @see {@link DecoderOptions} For configuration options
|
|
122
|
-
*/
|
|
123
|
-
static async create(stream, options = {}) {
|
|
151
|
+
static async create(stream, optionsOrCodec, maybeOptions) {
|
|
152
|
+
// Parse arguments
|
|
153
|
+
let options = {};
|
|
154
|
+
let explicitCodec;
|
|
155
|
+
if (optionsOrCodec !== undefined) {
|
|
156
|
+
// Check if first argument is a codec or options
|
|
157
|
+
if (typeof optionsOrCodec === 'string' || // FFDecoderCodec
|
|
158
|
+
typeof optionsOrCodec === 'number' || // AVCodecID
|
|
159
|
+
optionsOrCodec instanceof Codec // Codec instance
|
|
160
|
+
) {
|
|
161
|
+
// First argument is a codec
|
|
162
|
+
explicitCodec = optionsOrCodec;
|
|
163
|
+
options = maybeOptions ?? {};
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
// First argument is options
|
|
167
|
+
options = optionsOrCodec;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
124
170
|
let codec = null;
|
|
125
|
-
// If
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
171
|
+
// If explicit codec provided, use it
|
|
172
|
+
if (explicitCodec !== undefined) {
|
|
173
|
+
if (typeof explicitCodec === 'object' && 'id' in explicitCodec) {
|
|
174
|
+
// Already a Codec instance
|
|
175
|
+
codec = explicitCodec;
|
|
176
|
+
}
|
|
177
|
+
else if (typeof explicitCodec === 'string') {
|
|
178
|
+
// FFDecoderCodec string
|
|
179
|
+
codec = Codec.findDecoderByName(explicitCodec);
|
|
180
|
+
if (!codec) {
|
|
181
|
+
throw new Error(`Decoder '${explicitCodec}' not found`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// AVCodecID number
|
|
186
|
+
codec = Codec.findDecoder(explicitCodec);
|
|
187
|
+
if (!codec) {
|
|
188
|
+
throw new Error(`Decoder not found for codec ID ${explicitCodec}`);
|
|
189
|
+
}
|
|
131
190
|
}
|
|
132
191
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
192
|
+
else {
|
|
193
|
+
// No explicit codec - use auto-detection logic
|
|
194
|
+
// If hardware acceleration requested, try to find hardware decoder first
|
|
195
|
+
if (options.hardware) {
|
|
196
|
+
codec = options.hardware.getDecoderCodec(stream.codecpar.codecId);
|
|
197
|
+
if (!codec) {
|
|
198
|
+
// No hardware decoder available, fall back to software
|
|
199
|
+
options.hardware = undefined;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// If no hardware decoder or no hardware requested, use software decoder
|
|
136
203
|
if (!codec) {
|
|
137
|
-
|
|
204
|
+
codec = Codec.findDecoder(stream.codecpar.codecId);
|
|
205
|
+
if (!codec) {
|
|
206
|
+
throw new Error(`Decoder not found for codec ${stream.codecpar.codecId}`);
|
|
207
|
+
}
|
|
138
208
|
}
|
|
139
209
|
}
|
|
140
210
|
// Allocate and configure codec context
|
|
@@ -148,16 +218,14 @@ export class Decoder {
|
|
|
148
218
|
}
|
|
149
219
|
// Set packet time base
|
|
150
220
|
codecContext.pktTimebase = stream.timeBase;
|
|
151
|
-
// Apply options
|
|
152
|
-
if (options.threads !== undefined) {
|
|
153
|
-
codecContext.threadCount = options.threads;
|
|
154
|
-
}
|
|
155
221
|
// Check if this decoder supports hardware acceleration
|
|
156
222
|
// Only apply hardware acceleration if the decoder supports it
|
|
157
223
|
// Silently ignore hardware for software decoders
|
|
158
224
|
const isHWDecoder = codec.isHardwareAcceleratedDecoder();
|
|
159
225
|
if (isHWDecoder && options.hardware) {
|
|
160
226
|
codecContext.hwDeviceCtx = options.hardware.deviceContext;
|
|
227
|
+
// Set hardware pixel format
|
|
228
|
+
codecContext.setHardwarePixelFormat(options.hardware.devicePixelFormat);
|
|
161
229
|
// Set extra_hw_frames if specified
|
|
162
230
|
if (options.extraHWFrames !== undefined && options.extraHWFrames > 0) {
|
|
163
231
|
codecContext.extraHWFrames = options.extraHWFrames;
|
|
@@ -167,6 +235,8 @@ export class Decoder {
|
|
|
167
235
|
options.hardware = undefined;
|
|
168
236
|
}
|
|
169
237
|
options.exitOnError = options.exitOnError ?? true;
|
|
238
|
+
// Enable COPY_OPAQUE flag to copy packet.opaque to frame.opaque
|
|
239
|
+
codecContext.setFlags(AV_CODEC_FLAG_COPY_OPAQUE);
|
|
170
240
|
const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
|
|
171
241
|
// Open codec
|
|
172
242
|
const openRet = await codecContext.open2(codec, opts);
|
|
@@ -174,71 +244,76 @@ export class Decoder {
|
|
|
174
244
|
codecContext.freeContext();
|
|
175
245
|
FFmpegError.throwIfError(openRet, 'Failed to open codec');
|
|
176
246
|
}
|
|
247
|
+
// Adjust extra_hw_frames for queuing
|
|
248
|
+
// This is done AFTER open2 because the decoder validates extra_hw_frames during open
|
|
249
|
+
if (isHWDecoder && options.hardware) {
|
|
250
|
+
const currentExtraFrames = codecContext.extraHWFrames;
|
|
251
|
+
if (currentExtraFrames >= 0) {
|
|
252
|
+
codecContext.extraHWFrames = currentExtraFrames + FRAME_THREAD_QUEUE_SIZE;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
codecContext.extraHWFrames = 1;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
177
258
|
return new Decoder(codecContext, codec, stream, options);
|
|
178
259
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
* ```typescript
|
|
199
|
-
* import { MediaInput, Decoder } from 'node-av/api';
|
|
200
|
-
*
|
|
201
|
-
* await using input = await MediaInput.open('video.mp4');
|
|
202
|
-
* using decoder = await Decoder.create(input.video());
|
|
203
|
-
* ```
|
|
204
|
-
*
|
|
205
|
-
* @example
|
|
206
|
-
* ```typescript
|
|
207
|
-
* using decoder = await Decoder.create(stream, {
|
|
208
|
-
* threads: 4,
|
|
209
|
-
* options: {
|
|
210
|
-
* 'refcounted_frames': '1',
|
|
211
|
-
* 'skip_frame': 'nonkey' // Only decode keyframes
|
|
212
|
-
* }
|
|
213
|
-
* });
|
|
214
|
-
* ```
|
|
215
|
-
*
|
|
216
|
-
* @example
|
|
217
|
-
* ```typescript
|
|
218
|
-
* const hw = HardwareContext.auto();
|
|
219
|
-
* using decoder = await Decoder.create(stream, {
|
|
220
|
-
* hardware: hw,
|
|
221
|
-
* threads: 0 // Auto-detect thread count
|
|
222
|
-
* });
|
|
223
|
-
* ```
|
|
224
|
-
*
|
|
225
|
-
* @see {@link create} For async version
|
|
226
|
-
*/
|
|
227
|
-
static createSync(stream, options = {}) {
|
|
260
|
+
static createSync(stream, optionsOrCodec, maybeOptions) {
|
|
261
|
+
// Parse arguments
|
|
262
|
+
let options = {};
|
|
263
|
+
let explicitCodec;
|
|
264
|
+
if (optionsOrCodec !== undefined) {
|
|
265
|
+
// Check if first argument is a codec or options
|
|
266
|
+
if (typeof optionsOrCodec === 'string' || // FFDecoderCodec
|
|
267
|
+
typeof optionsOrCodec === 'number' || // AVCodecID
|
|
268
|
+
optionsOrCodec instanceof Codec // Codec instance
|
|
269
|
+
) {
|
|
270
|
+
// First argument is a codec
|
|
271
|
+
explicitCodec = optionsOrCodec;
|
|
272
|
+
options = maybeOptions ?? {};
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// First argument is options
|
|
276
|
+
options = optionsOrCodec;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
228
279
|
let codec = null;
|
|
229
|
-
// If
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
280
|
+
// If explicit codec provided, use it
|
|
281
|
+
if (explicitCodec !== undefined) {
|
|
282
|
+
if (typeof explicitCodec === 'object' && 'id' in explicitCodec) {
|
|
283
|
+
// Already a Codec instance
|
|
284
|
+
codec = explicitCodec;
|
|
285
|
+
}
|
|
286
|
+
else if (typeof explicitCodec === 'string') {
|
|
287
|
+
// FFDecoderCodec string
|
|
288
|
+
codec = Codec.findDecoderByName(explicitCodec);
|
|
289
|
+
if (!codec) {
|
|
290
|
+
throw new Error(`Decoder '${explicitCodec}' not found`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// AVCodecID number
|
|
295
|
+
codec = Codec.findDecoder(explicitCodec);
|
|
296
|
+
if (!codec) {
|
|
297
|
+
throw new Error(`Decoder not found for codec ID ${explicitCodec}`);
|
|
298
|
+
}
|
|
235
299
|
}
|
|
236
300
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
301
|
+
else {
|
|
302
|
+
// No explicit codec - use auto-detection logic
|
|
303
|
+
// If hardware acceleration requested, try to find hardware decoder first
|
|
304
|
+
if (options.hardware) {
|
|
305
|
+
codec = options.hardware.getDecoderCodec(stream.codecpar.codecId);
|
|
306
|
+
if (!codec) {
|
|
307
|
+
// No hardware decoder available, fall back to software
|
|
308
|
+
options.hardware = undefined;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// If no hardware decoder or no hardware requested, use software decoder
|
|
240
312
|
if (!codec) {
|
|
241
|
-
|
|
313
|
+
codec = Codec.findDecoder(stream.codecpar.codecId);
|
|
314
|
+
if (!codec) {
|
|
315
|
+
throw new Error(`Decoder not found for codec ${stream.codecpar.codecId}`);
|
|
316
|
+
}
|
|
242
317
|
}
|
|
243
318
|
}
|
|
244
319
|
// Allocate and configure codec context
|
|
@@ -252,16 +327,14 @@ export class Decoder {
|
|
|
252
327
|
}
|
|
253
328
|
// Set packet time base
|
|
254
329
|
codecContext.pktTimebase = stream.timeBase;
|
|
255
|
-
// Apply options
|
|
256
|
-
if (options.threads !== undefined) {
|
|
257
|
-
codecContext.threadCount = options.threads;
|
|
258
|
-
}
|
|
259
330
|
// Check if this decoder supports hardware acceleration
|
|
260
331
|
// Only apply hardware acceleration if the decoder supports it
|
|
261
332
|
// Silently ignore hardware for software decoders
|
|
262
333
|
const isHWDecoder = codec.isHardwareAcceleratedDecoder();
|
|
263
334
|
if (isHWDecoder && options.hardware) {
|
|
264
335
|
codecContext.hwDeviceCtx = options.hardware.deviceContext;
|
|
336
|
+
// Set hardware pixel format and get_format callback
|
|
337
|
+
codecContext.setHardwarePixelFormat(options.hardware.devicePixelFormat);
|
|
265
338
|
// Set extra_hw_frames if specified
|
|
266
339
|
if (options.extraHWFrames !== undefined && options.extraHWFrames > 0) {
|
|
267
340
|
codecContext.extraHWFrames = options.extraHWFrames;
|
|
@@ -270,6 +343,9 @@ export class Decoder {
|
|
|
270
343
|
else {
|
|
271
344
|
options.hardware = undefined;
|
|
272
345
|
}
|
|
346
|
+
options.exitOnError = options.exitOnError ?? true;
|
|
347
|
+
// Enable COPY_OPAQUE flag to copy packet.opaque to frame.opaque
|
|
348
|
+
// codecContext.setFlags(AV_CODEC_FLAG_COPY_OPAQUE);
|
|
273
349
|
const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
|
|
274
350
|
// Open codec synchronously
|
|
275
351
|
const openRet = codecContext.open2Sync(codec, opts);
|
|
@@ -277,6 +353,17 @@ export class Decoder {
|
|
|
277
353
|
codecContext.freeContext();
|
|
278
354
|
FFmpegError.throwIfError(openRet, 'Failed to open codec');
|
|
279
355
|
}
|
|
356
|
+
// Adjust extra_hw_frames for queuing
|
|
357
|
+
// This is done AFTER open2 because the decoder validates extra_hw_frames during open
|
|
358
|
+
if (isHWDecoder && options.hardware) {
|
|
359
|
+
const currentExtraFrames = codecContext.extraHWFrames;
|
|
360
|
+
if (currentExtraFrames >= 0) {
|
|
361
|
+
codecContext.extraHWFrames = currentExtraFrames + FRAME_THREAD_QUEUE_SIZE;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
codecContext.extraHWFrames = 1;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
280
367
|
return new Decoder(codecContext, codec, stream, options);
|
|
281
368
|
}
|
|
282
369
|
/**
|
|
@@ -351,6 +438,10 @@ export class Decoder {
|
|
|
351
438
|
* Handles internal buffering - may return null if more packets needed.
|
|
352
439
|
* Automatically manages decoder state and error recovery.
|
|
353
440
|
*
|
|
441
|
+
* **Note**: This method receives only ONE frame per call.
|
|
442
|
+
* A single packet can produce multiple frames (e.g., packed B-frames, codec buffering).
|
|
443
|
+
* To receive all frames from a packet, use {@link decodeAll} or {@link frames} instead.
|
|
444
|
+
*
|
|
354
445
|
* Direct mapping to avcodec_send_packet() and avcodec_receive_frame().
|
|
355
446
|
*
|
|
356
447
|
* @param packet - Compressed packet to decode
|
|
@@ -382,29 +473,42 @@ export class Decoder {
|
|
|
382
473
|
* }
|
|
383
474
|
* ```
|
|
384
475
|
*
|
|
476
|
+
* @see {@link decodeAll} For multiple frame decoding
|
|
385
477
|
* @see {@link frames} For automatic packet iteration
|
|
386
478
|
* @see {@link flush} For end-of-stream handling
|
|
479
|
+
* @see {@link decodeSync} For synchronous version
|
|
387
480
|
*/
|
|
388
481
|
async decode(packet) {
|
|
389
482
|
if (this.isClosed) {
|
|
390
483
|
return null;
|
|
391
484
|
}
|
|
485
|
+
// Skip 0-sized packets
|
|
486
|
+
if (packet.size === 0) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
392
489
|
// Send packet to decoder
|
|
393
490
|
const sendRet = await this.codecContext.sendPacket(packet);
|
|
394
|
-
|
|
395
|
-
|
|
491
|
+
// Handle EAGAIN: decoder buffer is full, need to read frames first
|
|
492
|
+
// Unlike FFmpeg CLI which reads ALL frames in a loop, our decode() returns
|
|
493
|
+
// only one frame at a time. This means the decoder can still have frames
|
|
494
|
+
// from previous packets when we try to send a new packet.
|
|
495
|
+
if (sendRet === AVERROR_EAGAIN) {
|
|
496
|
+
// Decoder buffer full, receive a frame first
|
|
396
497
|
const frame = await this.receive();
|
|
397
498
|
if (frame) {
|
|
398
499
|
return frame;
|
|
399
500
|
}
|
|
400
|
-
// If
|
|
401
|
-
|
|
402
|
-
|
|
501
|
+
// If receive() returned null, this is unexpected - treat as decoder bug
|
|
502
|
+
throw new Error('Decoder returned EAGAIN on send but no frame available - decoder bug');
|
|
503
|
+
}
|
|
504
|
+
// Handle other send errors (matches FFmpeg's error handling)
|
|
505
|
+
if (sendRet < 0 && sendRet !== AVERROR_EOF) {
|
|
506
|
+
if (this.options.exitOnError) {
|
|
507
|
+
FFmpegError.throwIfError(sendRet, 'Failed to send packet to decoder');
|
|
403
508
|
}
|
|
404
509
|
}
|
|
405
510
|
// Try to receive frame
|
|
406
|
-
|
|
407
|
-
return frame;
|
|
511
|
+
return await this.receive();
|
|
408
512
|
}
|
|
409
513
|
/**
|
|
410
514
|
* Decode a packet to frame synchronously.
|
|
@@ -414,6 +518,10 @@ export class Decoder {
|
|
|
414
518
|
* Handles decoder buffering and error conditions.
|
|
415
519
|
* May return null if decoder needs more data.
|
|
416
520
|
*
|
|
521
|
+
* **Note**: This method receives only ONE frame per call.
|
|
522
|
+
* A single packet can produce multiple frames (e.g., packed B-frames, codec buffering).
|
|
523
|
+
* To receive all frames from a packet, use {@link decodeAllSync} or {@link framesSync} instead.
|
|
524
|
+
*
|
|
417
525
|
* @param packet - Compressed packet to decode
|
|
418
526
|
*
|
|
419
527
|
* @returns Decoded frame or null if more data needed or decoder is closed
|
|
@@ -428,28 +536,179 @@ export class Decoder {
|
|
|
428
536
|
* }
|
|
429
537
|
* ```
|
|
430
538
|
*
|
|
539
|
+
* @see {@link decodeAllSync} For multiple frame decoding
|
|
540
|
+
* @see {@link framesSync} For automatic packet iteration
|
|
541
|
+
* @see {@link flushSync} For end-of-stream handling
|
|
431
542
|
* @see {@link decode} For async version
|
|
432
543
|
*/
|
|
433
544
|
decodeSync(packet) {
|
|
434
545
|
if (this.isClosed) {
|
|
435
546
|
return null;
|
|
436
547
|
}
|
|
548
|
+
// Skip 0-sized packets
|
|
549
|
+
if (packet.size === 0) {
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
437
552
|
// Send packet to decoder
|
|
438
553
|
const sendRet = this.codecContext.sendPacketSync(packet);
|
|
439
|
-
|
|
440
|
-
|
|
554
|
+
// Handle EAGAIN: decoder buffer is full, need to read frames first
|
|
555
|
+
// Unlike FFmpeg CLI which reads ALL frames in a loop, our decode() returns
|
|
556
|
+
// only one frame at a time. This means the decoder can still have frames
|
|
557
|
+
// from previous packets when we try to send a new packet.
|
|
558
|
+
if (sendRet === AVERROR_EAGAIN) {
|
|
559
|
+
// Decoder buffer full, receive a frame first
|
|
441
560
|
const frame = this.receiveSync();
|
|
442
561
|
if (frame) {
|
|
443
562
|
return frame;
|
|
444
563
|
}
|
|
445
|
-
// If
|
|
446
|
-
|
|
447
|
-
|
|
564
|
+
// If receive() returned null, this is unexpected - treat as decoder bug
|
|
565
|
+
throw new Error('Decoder returned EAGAIN on send but no frame available - decoder bug');
|
|
566
|
+
}
|
|
567
|
+
// Handle other send errors
|
|
568
|
+
if (sendRet < 0 && sendRet !== AVERROR_EOF) {
|
|
569
|
+
if (this.options.exitOnError) {
|
|
570
|
+
FFmpegError.throwIfError(sendRet, 'Failed to send packet to decoder');
|
|
448
571
|
}
|
|
572
|
+
// exitOnError=false: Continue to receive
|
|
449
573
|
}
|
|
450
574
|
// Try to receive frame
|
|
451
|
-
|
|
452
|
-
|
|
575
|
+
return this.receiveSync();
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Decode a packet to frames.
|
|
579
|
+
*
|
|
580
|
+
* Sends a packet to the decoder and receives all available decoded frames.
|
|
581
|
+
* Returns array of frames - may be empty if decoder needs more data.
|
|
582
|
+
* One packet can produce zero, one, or multiple frames depending on codec.
|
|
583
|
+
* Automatically manages decoder state and error recovery.
|
|
584
|
+
*
|
|
585
|
+
* Direct mapping to avcodec_send_packet() and avcodec_receive_frame().
|
|
586
|
+
*
|
|
587
|
+
* @param packet - Compressed packet to decode
|
|
588
|
+
*
|
|
589
|
+
* @returns Array of decoded frames (empty if more data needed or decoder is closed)
|
|
590
|
+
*
|
|
591
|
+
* @throws {FFmpegError} If decoding fails
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* ```typescript
|
|
595
|
+
* const frames = await decoder.decodeAll(packet);
|
|
596
|
+
* for (const frame of frames) {
|
|
597
|
+
* console.log(`Decoded frame with PTS: ${frame.pts}`);
|
|
598
|
+
* frame.free();
|
|
599
|
+
* }
|
|
600
|
+
* ```
|
|
601
|
+
*
|
|
602
|
+
* @example
|
|
603
|
+
* ```typescript
|
|
604
|
+
* for await (const packet of input.packets()) {
|
|
605
|
+
* if (packet.streamIndex === decoder.getStream().index) {
|
|
606
|
+
* const frames = await decoder.decodeAll(packet);
|
|
607
|
+
* for (const frame of frames) {
|
|
608
|
+
* await processFrame(frame);
|
|
609
|
+
* frame.free();
|
|
610
|
+
* }
|
|
611
|
+
* }
|
|
612
|
+
* packet.free();
|
|
613
|
+
* }
|
|
614
|
+
* ```
|
|
615
|
+
*
|
|
616
|
+
* @see {@link decode} For single packet decoding
|
|
617
|
+
* @see {@link frames} For automatic packet iteration
|
|
618
|
+
* @see {@link flush} For end-of-stream handling
|
|
619
|
+
* @see {@link decodeAllSync} For synchronous version
|
|
620
|
+
*/
|
|
621
|
+
async decodeAll(packet) {
|
|
622
|
+
if (this.isClosed) {
|
|
623
|
+
return [];
|
|
624
|
+
}
|
|
625
|
+
// Skip 0-sized packets
|
|
626
|
+
if (packet.size === 0) {
|
|
627
|
+
return [];
|
|
628
|
+
}
|
|
629
|
+
// Send packet to decoder
|
|
630
|
+
const sendRet = await this.codecContext.sendPacket(packet);
|
|
631
|
+
// EAGAIN during send_packet is a decoder bug (FFmpeg treats this as AVERROR_BUG)
|
|
632
|
+
// We read all decoded frames with receive() until done, so decoder should never be full
|
|
633
|
+
if (sendRet === AVERROR_EAGAIN) {
|
|
634
|
+
throw new Error('Decoder returned EAGAIN on send - this is a decoder bug');
|
|
635
|
+
}
|
|
636
|
+
// Handle send errors
|
|
637
|
+
if (sendRet < 0 && sendRet !== AVERROR_EOF) {
|
|
638
|
+
if (this.options.exitOnError) {
|
|
639
|
+
FFmpegError.throwIfError(sendRet, 'Failed to send packet to decoder');
|
|
640
|
+
}
|
|
641
|
+
// exitOnError=false: Continue to receive loop to drain any buffered frames
|
|
642
|
+
}
|
|
643
|
+
// Receive all available frames
|
|
644
|
+
const frames = [];
|
|
645
|
+
while (true) {
|
|
646
|
+
const remaining = await this.receive();
|
|
647
|
+
if (!remaining)
|
|
648
|
+
break;
|
|
649
|
+
frames.push(remaining);
|
|
650
|
+
}
|
|
651
|
+
return frames;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Decode a packet to frames synchronously.
|
|
655
|
+
* Synchronous version of decodeAll.
|
|
656
|
+
*
|
|
657
|
+
* Sends packet to decoder and receives all available decoded frames.
|
|
658
|
+
* Returns array of frames - may be empty if decoder needs more data.
|
|
659
|
+
* One packet can produce zero, one, or multiple frames depending on codec.
|
|
660
|
+
*
|
|
661
|
+
* @param packet - Compressed packet to decode
|
|
662
|
+
*
|
|
663
|
+
* @returns Array of decoded frames (empty if more data needed or decoder is closed)
|
|
664
|
+
*
|
|
665
|
+
* @throws {FFmpegError} If decoding fails
|
|
666
|
+
*
|
|
667
|
+
* @example
|
|
668
|
+
* ```typescript
|
|
669
|
+
* const frames = decoder.decodeAllSync(packet);
|
|
670
|
+
* for (const frame of frames) {
|
|
671
|
+
* console.log(`Decoded: ${frame.width}x${frame.height}`);
|
|
672
|
+
* frame.free();
|
|
673
|
+
* }
|
|
674
|
+
* ```
|
|
675
|
+
*
|
|
676
|
+
* @see {@link decodeSync} For single packet decoding
|
|
677
|
+
* @see {@link framesSync} For automatic packet iteration
|
|
678
|
+
* @see {@link flushSync} For end-of-stream handling
|
|
679
|
+
* @see {@link decodeAll} For async version
|
|
680
|
+
*/
|
|
681
|
+
decodeAllSync(packet) {
|
|
682
|
+
if (this.isClosed) {
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
// Skip 0-sized packets
|
|
686
|
+
if (packet.size === 0) {
|
|
687
|
+
return [];
|
|
688
|
+
}
|
|
689
|
+
// Send packet to decoder
|
|
690
|
+
const sendRet = this.codecContext.sendPacketSync(packet);
|
|
691
|
+
// EAGAIN during send_packet is a decoder bug (FFmpeg treats this as AVERROR_BUG)
|
|
692
|
+
// We read all decoded frames with receive() until done, so decoder should never be full
|
|
693
|
+
if (sendRet === AVERROR_EAGAIN) {
|
|
694
|
+
throw new Error('Decoder returned EAGAIN on send - this is a decoder bug');
|
|
695
|
+
}
|
|
696
|
+
// Handle send errors
|
|
697
|
+
if (sendRet < 0 && sendRet !== AVERROR_EOF) {
|
|
698
|
+
if (this.options.exitOnError) {
|
|
699
|
+
FFmpegError.throwIfError(sendRet, 'Failed to send packet to decoder');
|
|
700
|
+
}
|
|
701
|
+
// exitOnError=false: Continue to receive loop to drain any buffered frames
|
|
702
|
+
}
|
|
703
|
+
// Receive all available frames
|
|
704
|
+
const frames = [];
|
|
705
|
+
while (true) {
|
|
706
|
+
const remaining = this.receiveSync();
|
|
707
|
+
if (!remaining)
|
|
708
|
+
break;
|
|
709
|
+
frames.push(remaining);
|
|
710
|
+
}
|
|
711
|
+
return frames;
|
|
453
712
|
}
|
|
454
713
|
/**
|
|
455
714
|
* Decode packet stream to frame stream.
|
|
@@ -469,7 +728,7 @@ export class Decoder {
|
|
|
469
728
|
*
|
|
470
729
|
* @example
|
|
471
730
|
* ```typescript
|
|
472
|
-
* await using input = await
|
|
731
|
+
* await using input = await Demuxer.open('video.mp4');
|
|
473
732
|
* using decoder = await Decoder.create(input.video());
|
|
474
733
|
*
|
|
475
734
|
* for await (const frame of decoder.frames(input.packets())) {
|
|
@@ -503,33 +762,77 @@ export class Decoder {
|
|
|
503
762
|
* ```
|
|
504
763
|
*
|
|
505
764
|
* @see {@link decode} For single packet decoding
|
|
506
|
-
* @see {@link
|
|
765
|
+
* @see {@link Demuxer.packets} For packet source
|
|
766
|
+
* @see {@link framesSync} For sync version
|
|
507
767
|
*/
|
|
508
768
|
async *frames(packets) {
|
|
509
|
-
|
|
510
|
-
|
|
769
|
+
for await (const packet_1 of packets) {
|
|
770
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
511
771
|
try {
|
|
772
|
+
const packet = __addDisposableResource(env_1, packet_1, false);
|
|
773
|
+
// Handle EOF signal
|
|
774
|
+
if (packet === null) {
|
|
775
|
+
// Flush decoder
|
|
776
|
+
await this.flush();
|
|
777
|
+
while (true) {
|
|
778
|
+
const remaining = await this.receive();
|
|
779
|
+
if (!remaining)
|
|
780
|
+
break;
|
|
781
|
+
yield remaining;
|
|
782
|
+
}
|
|
783
|
+
// Signal EOF and stop processing
|
|
784
|
+
yield null;
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
512
787
|
// Only process packets for our stream
|
|
513
788
|
if (packet.streamIndex === this.stream.index) {
|
|
514
|
-
|
|
515
|
-
|
|
789
|
+
if (this.isClosed) {
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
// Skip 0-sized packets
|
|
793
|
+
if (packet.size === 0) {
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
// Send packet to decoder
|
|
797
|
+
const sendRet = await this.codecContext.sendPacket(packet);
|
|
798
|
+
// EAGAIN during send_packet is a decoder bug
|
|
799
|
+
// We read all decoded frames with receive() until done, so decoder should never be full
|
|
800
|
+
if (sendRet === AVERROR_EAGAIN) {
|
|
801
|
+
throw new Error('Decoder returned EAGAIN but no frame available');
|
|
802
|
+
}
|
|
803
|
+
if (sendRet < 0 && sendRet !== AVERROR_EOF) {
|
|
804
|
+
if (this.options.exitOnError) {
|
|
805
|
+
FFmpegError.throwIfError(sendRet, 'Failed to send packet');
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
// Receive ALL available frames immediately
|
|
809
|
+
// This ensures frames are yielded ASAP without latency
|
|
810
|
+
while (true) {
|
|
811
|
+
const frame = await this.receive();
|
|
812
|
+
if (!frame)
|
|
813
|
+
break; // EAGAIN or EOF
|
|
516
814
|
yield frame;
|
|
517
815
|
}
|
|
518
816
|
}
|
|
519
817
|
}
|
|
818
|
+
catch (e_1) {
|
|
819
|
+
env_1.error = e_1;
|
|
820
|
+
env_1.hasError = true;
|
|
821
|
+
}
|
|
520
822
|
finally {
|
|
521
|
-
|
|
522
|
-
packet.free();
|
|
823
|
+
__disposeResources(env_1);
|
|
523
824
|
}
|
|
524
825
|
}
|
|
525
|
-
// Flush decoder after all packets
|
|
826
|
+
// Flush decoder after all packets (fallback if no null was sent)
|
|
526
827
|
await this.flush();
|
|
527
|
-
while (
|
|
828
|
+
while (true) {
|
|
528
829
|
const remaining = await this.receive();
|
|
529
830
|
if (!remaining)
|
|
530
831
|
break;
|
|
531
832
|
yield remaining;
|
|
532
833
|
}
|
|
834
|
+
// Signal EOF
|
|
835
|
+
yield null;
|
|
533
836
|
}
|
|
534
837
|
/**
|
|
535
838
|
* Decode packet stream to frame stream synchronously.
|
|
@@ -555,33 +858,78 @@ export class Decoder {
|
|
|
555
858
|
* }
|
|
556
859
|
* ```
|
|
557
860
|
*
|
|
861
|
+
* @see {@link decodeSync} For single packet decoding
|
|
862
|
+
* @see {@link Demuxer.packetsSync} For packet source
|
|
558
863
|
* @see {@link frames} For async version
|
|
559
864
|
*/
|
|
560
865
|
*framesSync(packets) {
|
|
561
|
-
|
|
562
|
-
|
|
866
|
+
for (const packet_2 of packets) {
|
|
867
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
563
868
|
try {
|
|
869
|
+
const packet = __addDisposableResource(env_2, packet_2, false);
|
|
870
|
+
// Handle EOF signal
|
|
871
|
+
if (packet === null) {
|
|
872
|
+
// Flush decoder
|
|
873
|
+
this.flushSync();
|
|
874
|
+
while (true) {
|
|
875
|
+
const remaining = this.receiveSync();
|
|
876
|
+
if (!remaining)
|
|
877
|
+
break;
|
|
878
|
+
yield remaining;
|
|
879
|
+
}
|
|
880
|
+
// Signal EOF and stop processing
|
|
881
|
+
yield null;
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
564
884
|
// Only process packets for our stream
|
|
565
885
|
if (packet.streamIndex === this.stream.index) {
|
|
566
|
-
|
|
567
|
-
|
|
886
|
+
if (this.isClosed) {
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
// Skip 0-sized packets
|
|
890
|
+
if (packet.size === 0) {
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
// Send packet to decoder
|
|
894
|
+
const sendRet = this.codecContext.sendPacketSync(packet);
|
|
895
|
+
// EAGAIN during send_packet is a decoder bug
|
|
896
|
+
// We read all decoded frames with receive() until done, so decoder should never be full
|
|
897
|
+
if (sendRet === AVERROR_EAGAIN) {
|
|
898
|
+
throw new Error('Decoder returned EAGAIN but no frame available');
|
|
899
|
+
}
|
|
900
|
+
if (sendRet < 0 && sendRet !== AVERROR_EOF) {
|
|
901
|
+
if (this.options.exitOnError) {
|
|
902
|
+
FFmpegError.throwIfError(sendRet, 'Failed to send packet');
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
// Receive ALL available frames immediately
|
|
906
|
+
// This ensures frames are yielded ASAP without latency
|
|
907
|
+
while (true) {
|
|
908
|
+
const frame = this.receiveSync();
|
|
909
|
+
if (!frame)
|
|
910
|
+
break; // EAGAIN or EOF
|
|
568
911
|
yield frame;
|
|
569
912
|
}
|
|
570
913
|
}
|
|
571
914
|
}
|
|
915
|
+
catch (e_2) {
|
|
916
|
+
env_2.error = e_2;
|
|
917
|
+
env_2.hasError = true;
|
|
918
|
+
}
|
|
572
919
|
finally {
|
|
573
|
-
|
|
574
|
-
packet.free();
|
|
920
|
+
__disposeResources(env_2);
|
|
575
921
|
}
|
|
576
922
|
}
|
|
577
|
-
// Flush decoder after all packets
|
|
923
|
+
// Flush decoder after all packets (fallback if no null was sent)
|
|
578
924
|
this.flushSync();
|
|
579
|
-
while (
|
|
925
|
+
while (true) {
|
|
580
926
|
const remaining = this.receiveSync();
|
|
581
927
|
if (!remaining)
|
|
582
928
|
break;
|
|
583
929
|
yield remaining;
|
|
584
930
|
}
|
|
931
|
+
// Signal EOF
|
|
932
|
+
yield null;
|
|
585
933
|
}
|
|
586
934
|
/**
|
|
587
935
|
* Flush decoder and signal end-of-stream.
|
|
@@ -609,6 +957,7 @@ export class Decoder {
|
|
|
609
957
|
*
|
|
610
958
|
* @see {@link flushFrames} For convenient async iteration
|
|
611
959
|
* @see {@link receive} For getting buffered frames
|
|
960
|
+
* @see {@link flushSync} For synchronous version
|
|
612
961
|
*/
|
|
613
962
|
async flush() {
|
|
614
963
|
if (this.isClosed) {
|
|
@@ -642,6 +991,8 @@ export class Decoder {
|
|
|
642
991
|
* }
|
|
643
992
|
* ```
|
|
644
993
|
*
|
|
994
|
+
* @see {@link flushFramesSync} For convenient sync iteration
|
|
995
|
+
* @see {@link receiveSync} For getting buffered frames
|
|
645
996
|
* @see {@link flush} For async version
|
|
646
997
|
*/
|
|
647
998
|
flushSync() {
|
|
@@ -675,15 +1026,18 @@ export class Decoder {
|
|
|
675
1026
|
* }
|
|
676
1027
|
* ```
|
|
677
1028
|
*
|
|
1029
|
+
* @see {@link decode} For sending packets and receiving frames
|
|
678
1030
|
* @see {@link flush} For signaling end-of-stream
|
|
679
|
-
* @see {@link
|
|
1031
|
+
* @see {@link flushFramesSync} For synchronous version
|
|
680
1032
|
*/
|
|
681
1033
|
async *flushFrames() {
|
|
682
1034
|
// Send flush signal
|
|
683
1035
|
await this.flush();
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1036
|
+
while (true) {
|
|
1037
|
+
const remaining = await this.receive();
|
|
1038
|
+
if (!remaining)
|
|
1039
|
+
break;
|
|
1040
|
+
yield remaining;
|
|
687
1041
|
}
|
|
688
1042
|
}
|
|
689
1043
|
/**
|
|
@@ -706,14 +1060,18 @@ export class Decoder {
|
|
|
706
1060
|
* }
|
|
707
1061
|
* ```
|
|
708
1062
|
*
|
|
1063
|
+
* @see {@link decodeSync} For sending packets and receiving frames
|
|
1064
|
+
* @see {@link flushSync} For signaling end-of-stream
|
|
709
1065
|
* @see {@link flushFrames} For async version
|
|
710
1066
|
*/
|
|
711
1067
|
*flushFramesSync() {
|
|
712
1068
|
// Send flush signal
|
|
713
1069
|
this.flushSync();
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1070
|
+
while (true) {
|
|
1071
|
+
const remaining = this.receiveSync();
|
|
1072
|
+
if (!remaining)
|
|
1073
|
+
break;
|
|
1074
|
+
yield remaining;
|
|
717
1075
|
}
|
|
718
1076
|
}
|
|
719
1077
|
/**
|
|
@@ -751,29 +1109,50 @@ export class Decoder {
|
|
|
751
1109
|
*
|
|
752
1110
|
* @see {@link decode} For sending packets and receiving frames
|
|
753
1111
|
* @see {@link flush} For signaling end-of-stream
|
|
1112
|
+
* @see {@link receiveSync} For synchronous version
|
|
754
1113
|
*/
|
|
755
1114
|
async receive() {
|
|
756
|
-
//
|
|
757
|
-
this.
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1115
|
+
// When exitOnError=false, continue on errors until we get a frame or EAGAIN/EOF
|
|
1116
|
+
while (!this.isClosed) {
|
|
1117
|
+
// Clear previous frame data
|
|
1118
|
+
this.frame.unref();
|
|
1119
|
+
const ret = await this.codecContext.receiveFrame(this.frame);
|
|
1120
|
+
if (ret === 0) {
|
|
1121
|
+
// Set frame time_base to decoder's packet timebase
|
|
1122
|
+
this.frame.timeBase = this.codecContext.pktTimebase;
|
|
1123
|
+
// Check for corrupt frame
|
|
1124
|
+
if (this.frame.decodeErrorFlags || this.frame.hasFlags(AV_FRAME_FLAG_CORRUPT)) {
|
|
1125
|
+
if (this.options.exitOnError) {
|
|
1126
|
+
throw new Error('Corrupt decoded frame detected');
|
|
1127
|
+
}
|
|
1128
|
+
// exitOnError=false: skip corrupt frame, continue to next
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
// Handles PTS assignment, duration estimation, and frame tracking
|
|
1132
|
+
if (this.codecContext.codecType === AVMEDIA_TYPE_VIDEO) {
|
|
1133
|
+
this.processVideoFrame(this.frame);
|
|
1134
|
+
}
|
|
1135
|
+
// Handles timestamp extrapolation, sample rate changes, and duration calculation
|
|
1136
|
+
if (this.codecContext.codecType === AVMEDIA_TYPE_AUDIO) {
|
|
1137
|
+
this.processAudioFrame(this.frame);
|
|
1138
|
+
}
|
|
1139
|
+
// Got a frame, clone it for the user
|
|
1140
|
+
return this.frame.clone();
|
|
1141
|
+
}
|
|
1142
|
+
else if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
|
|
1143
|
+
// Need more data or end of stream
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
// Error during receive
|
|
1148
|
+
if (this.options.exitOnError) {
|
|
1149
|
+
FFmpegError.throwIfError(ret, 'Failed to receive frame');
|
|
1150
|
+
}
|
|
1151
|
+
// exitOnError=false: continue to next frame
|
|
1152
|
+
continue;
|
|
774
1153
|
}
|
|
775
|
-
return null;
|
|
776
1154
|
}
|
|
1155
|
+
return null;
|
|
777
1156
|
}
|
|
778
1157
|
/**
|
|
779
1158
|
* Receive frame from decoder synchronously.
|
|
@@ -809,30 +1188,72 @@ export class Decoder {
|
|
|
809
1188
|
* }
|
|
810
1189
|
* ```
|
|
811
1190
|
*
|
|
1191
|
+
* @see {@link decodeSync} For sending packets and receiving frames
|
|
1192
|
+
* @see {@link flushSync} For signaling end-of-stream
|
|
812
1193
|
* @see {@link receive} For async version
|
|
813
1194
|
*/
|
|
814
1195
|
receiveSync() {
|
|
815
|
-
//
|
|
816
|
-
this.
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1196
|
+
// When exitOnError=false, continue on errors until we get a frame or EAGAIN/EOF
|
|
1197
|
+
while (!this.isClosed) {
|
|
1198
|
+
// Clear previous frame data
|
|
1199
|
+
this.frame.unref();
|
|
1200
|
+
const ret = this.codecContext.receiveFrameSync(this.frame);
|
|
1201
|
+
if (ret === 0) {
|
|
1202
|
+
// Set frame time_base to decoder's packet timebase
|
|
1203
|
+
this.frame.timeBase = this.codecContext.pktTimebase;
|
|
1204
|
+
// Check for corrupt frame
|
|
1205
|
+
if (this.frame.decodeErrorFlags || this.frame.hasFlags(AV_FRAME_FLAG_CORRUPT)) {
|
|
1206
|
+
if (this.options.exitOnError) {
|
|
1207
|
+
throw new Error('Corrupt decoded frame detected');
|
|
1208
|
+
}
|
|
1209
|
+
// exitOnError=false: skip corrupt frame, continue to next
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
// Process video frame
|
|
1213
|
+
// Handles PTS assignment, duration estimation, and frame tracking
|
|
1214
|
+
if (this.codecContext.codecType === AVMEDIA_TYPE_VIDEO) {
|
|
1215
|
+
this.processVideoFrame(this.frame);
|
|
1216
|
+
}
|
|
1217
|
+
// Process audio frame
|
|
1218
|
+
// Handles timestamp extrapolation, sample rate changes, and duration calculation
|
|
1219
|
+
if (this.codecContext.codecType === AVMEDIA_TYPE_AUDIO) {
|
|
1220
|
+
this.processAudioFrame(this.frame);
|
|
1221
|
+
}
|
|
1222
|
+
// Got a frame, clone it for the user
|
|
1223
|
+
return this.frame.clone();
|
|
1224
|
+
}
|
|
1225
|
+
else if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
|
|
1226
|
+
// Need more data or end of stream
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
else {
|
|
1230
|
+
// Error during receive
|
|
1231
|
+
if (this.options.exitOnError) {
|
|
1232
|
+
FFmpegError.throwIfError(ret, 'Failed to receive frame');
|
|
1233
|
+
}
|
|
1234
|
+
// exitOnError=false: continue to next frame
|
|
1235
|
+
continue;
|
|
833
1236
|
}
|
|
834
|
-
return null;
|
|
835
1237
|
}
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
pipeTo(target) {
|
|
1241
|
+
const t = target;
|
|
1242
|
+
// Store reference to next component for flush propagation
|
|
1243
|
+
this.nextComponent = t;
|
|
1244
|
+
// Start worker if not already running
|
|
1245
|
+
this.workerPromise ??= this.runWorker();
|
|
1246
|
+
// Start pipe task: decoder.outputQueue -> target.inputQueue (via target.send)
|
|
1247
|
+
this.pipeToPromise = (async () => {
|
|
1248
|
+
while (true) {
|
|
1249
|
+
const frame = await this.receiveFromQueue();
|
|
1250
|
+
if (!frame)
|
|
1251
|
+
break;
|
|
1252
|
+
await t.sendToQueue(frame);
|
|
1253
|
+
}
|
|
1254
|
+
})();
|
|
1255
|
+
// Return scheduler for chaining (target is now the last component)
|
|
1256
|
+
return new Scheduler(this, t);
|
|
836
1257
|
}
|
|
837
1258
|
/**
|
|
838
1259
|
* Close decoder and free resources.
|
|
@@ -858,6 +1279,8 @@ export class Decoder {
|
|
|
858
1279
|
return;
|
|
859
1280
|
}
|
|
860
1281
|
this.isClosed = true;
|
|
1282
|
+
this.inputQueue?.close();
|
|
1283
|
+
this.outputQueue?.close();
|
|
861
1284
|
this.frame.free();
|
|
862
1285
|
this.codecContext.freeContext();
|
|
863
1286
|
this.initialized = false;
|
|
@@ -908,6 +1331,372 @@ export class Decoder {
|
|
|
908
1331
|
getCodecContext() {
|
|
909
1332
|
return !this.isClosed && this.initialized ? this.codecContext : null;
|
|
910
1333
|
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Worker loop for push-based processing.
|
|
1336
|
+
*
|
|
1337
|
+
* @internal
|
|
1338
|
+
*/
|
|
1339
|
+
async runWorker() {
|
|
1340
|
+
try {
|
|
1341
|
+
// Outer loop - receive packets
|
|
1342
|
+
while (!this.inputQueue.isClosed) {
|
|
1343
|
+
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
1344
|
+
try {
|
|
1345
|
+
const packet = __addDisposableResource(env_3, await this.inputQueue.receive(), false);
|
|
1346
|
+
if (!packet)
|
|
1347
|
+
break;
|
|
1348
|
+
// Skip packets for other streams
|
|
1349
|
+
if (packet.streamIndex !== this.stream.index) {
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
if (packet.size === 0) {
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
// Send packet to decoder
|
|
1356
|
+
const sendRet = await this.codecContext.sendPacket(packet);
|
|
1357
|
+
// EAGAIN during send_packet is a decoder bug
|
|
1358
|
+
// We read all decoded frames with receive() until done, so decoder should never be full
|
|
1359
|
+
if (sendRet === AVERROR_EAGAIN) {
|
|
1360
|
+
throw new Error('Decoder returned EAGAIN but no frame available');
|
|
1361
|
+
}
|
|
1362
|
+
if (sendRet < 0 && sendRet !== AVERROR_EOF) {
|
|
1363
|
+
if (this.options.exitOnError) {
|
|
1364
|
+
FFmpegError.throwIfError(sendRet, 'Failed to send packet');
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
// Receive ALL available frames immediately
|
|
1368
|
+
// This ensures frames are yielded ASAP without latency
|
|
1369
|
+
while (!this.outputQueue.isClosed) {
|
|
1370
|
+
const frame = await this.receive();
|
|
1371
|
+
if (!frame)
|
|
1372
|
+
break; // EAGAIN or EOF
|
|
1373
|
+
await this.outputQueue.send(frame);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
catch (e_3) {
|
|
1377
|
+
env_3.error = e_3;
|
|
1378
|
+
env_3.hasError = true;
|
|
1379
|
+
}
|
|
1380
|
+
finally {
|
|
1381
|
+
__disposeResources(env_3);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
// Flush decoder at end
|
|
1385
|
+
await this.flush();
|
|
1386
|
+
while (!this.outputQueue.isClosed) {
|
|
1387
|
+
const frame = await this.receive();
|
|
1388
|
+
if (!frame)
|
|
1389
|
+
break;
|
|
1390
|
+
await this.outputQueue.send(frame);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
catch {
|
|
1394
|
+
// Ignore ?
|
|
1395
|
+
}
|
|
1396
|
+
finally {
|
|
1397
|
+
// Close output queue when done
|
|
1398
|
+
this.outputQueue?.close();
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Send packet to input queue.
|
|
1403
|
+
*
|
|
1404
|
+
* @param packet - Packet to send
|
|
1405
|
+
*
|
|
1406
|
+
* @internal
|
|
1407
|
+
*/
|
|
1408
|
+
async sendToQueue(packet) {
|
|
1409
|
+
await this.inputQueue.send(packet);
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Receive frame from output queue.
|
|
1413
|
+
*
|
|
1414
|
+
* @returns Frame from output queue or null if closed
|
|
1415
|
+
*
|
|
1416
|
+
* @internal
|
|
1417
|
+
*/
|
|
1418
|
+
async receiveFromQueue() {
|
|
1419
|
+
return await this.outputQueue.receive();
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Flush the entire filter pipeline.
|
|
1423
|
+
*
|
|
1424
|
+
* Propagates flush through worker, output queue, and next component.
|
|
1425
|
+
*
|
|
1426
|
+
* @internal
|
|
1427
|
+
*/
|
|
1428
|
+
async flushPipeline() {
|
|
1429
|
+
// Close input queue to signal end of stream to worker
|
|
1430
|
+
this.inputQueue.close();
|
|
1431
|
+
// Wait for worker to finish processing all packets (if exists)
|
|
1432
|
+
if (this.workerPromise) {
|
|
1433
|
+
await this.workerPromise;
|
|
1434
|
+
}
|
|
1435
|
+
// Flush decoder at end
|
|
1436
|
+
await this.flush();
|
|
1437
|
+
// Send all flushed frames to output queue
|
|
1438
|
+
while (true) {
|
|
1439
|
+
const frame = await this.receive();
|
|
1440
|
+
if (!frame)
|
|
1441
|
+
break;
|
|
1442
|
+
await this.outputQueue.send(frame);
|
|
1443
|
+
}
|
|
1444
|
+
// Close output queue to signal end of stream to pipeTo() task
|
|
1445
|
+
this.outputQueue.close();
|
|
1446
|
+
// Wait for pipeTo() task to finish processing all frames (if exists)
|
|
1447
|
+
if (this.pipeToPromise) {
|
|
1448
|
+
await this.pipeToPromise;
|
|
1449
|
+
}
|
|
1450
|
+
// Then propagate flush to next component
|
|
1451
|
+
if (this.nextComponent) {
|
|
1452
|
+
await this.nextComponent.flushPipeline();
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Estimate video frame duration.
|
|
1457
|
+
*
|
|
1458
|
+
* Implements FFmpeg CLI's video_duration_estimate() logic.
|
|
1459
|
+
* Uses multiple heuristics to determine frame duration when not explicitly available:
|
|
1460
|
+
* 1. Frame duration from container (if reliable)
|
|
1461
|
+
* 2. Duration from codec framerate
|
|
1462
|
+
* 3. PTS difference between frames
|
|
1463
|
+
* 4. Stream framerate
|
|
1464
|
+
* 5. Last frame's estimated duration
|
|
1465
|
+
*
|
|
1466
|
+
* @param frame - Frame to estimate duration for
|
|
1467
|
+
*
|
|
1468
|
+
* @returns Estimated duration in frame's timebase units
|
|
1469
|
+
*
|
|
1470
|
+
* @internal
|
|
1471
|
+
*/
|
|
1472
|
+
estimateVideoDuration(frame) {
|
|
1473
|
+
// Difference between this and last frame's timestamps
|
|
1474
|
+
const tsDiff = frame.pts !== AV_NOPTS_VALUE && this.lastFramePts !== AV_NOPTS_VALUE ? frame.pts - this.lastFramePts : -1n;
|
|
1475
|
+
// Frame duration is unreliable (typically guessed by lavf) when it is equal
|
|
1476
|
+
// to 1 and the actual duration of the last frame is more than 2x larger
|
|
1477
|
+
const durationUnreliable = frame.duration === 1n && tsDiff > 2n * frame.duration;
|
|
1478
|
+
// Prefer frame duration for containers with timestamps
|
|
1479
|
+
if (frame.duration > 0n && !durationUnreliable) {
|
|
1480
|
+
return frame.duration;
|
|
1481
|
+
}
|
|
1482
|
+
// Calculate codec duration from framerate
|
|
1483
|
+
let codecDuration = 0n;
|
|
1484
|
+
const framerate = this.codecContext.framerate;
|
|
1485
|
+
if (framerate && framerate.den > 0 && framerate.num > 0) {
|
|
1486
|
+
const fields = (frame.repeatPict ?? 0) + 2;
|
|
1487
|
+
const fieldRate = avMulQ(framerate, { num: 2, den: 1 });
|
|
1488
|
+
codecDuration = avRescaleQ(fields, avInvQ(fieldRate), frame.timeBase);
|
|
1489
|
+
}
|
|
1490
|
+
// When timestamps are available, repeat last frame's actual duration
|
|
1491
|
+
if (tsDiff > 0n) {
|
|
1492
|
+
return tsDiff;
|
|
1493
|
+
}
|
|
1494
|
+
// Try frame/codec duration
|
|
1495
|
+
if (frame.duration > 0n) {
|
|
1496
|
+
return frame.duration;
|
|
1497
|
+
}
|
|
1498
|
+
if (codecDuration > 0n) {
|
|
1499
|
+
return codecDuration;
|
|
1500
|
+
}
|
|
1501
|
+
// Try stream framerate
|
|
1502
|
+
const streamFramerate = this.stream.avgFrameRate ?? this.stream.rFrameRate;
|
|
1503
|
+
if (streamFramerate && streamFramerate.num > 0 && streamFramerate.den > 0) {
|
|
1504
|
+
const d = avRescaleQ(1, avInvQ(streamFramerate), frame.timeBase);
|
|
1505
|
+
if (d > 0n) {
|
|
1506
|
+
return d;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
// Last resort is last frame's estimated duration, and 1
|
|
1510
|
+
return this.lastFrameDurationEst > 0n ? this.lastFrameDurationEst : 1n;
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Process video frame after decoding.
|
|
1514
|
+
*
|
|
1515
|
+
* Implements FFmpeg CLI's video_frame_process() logic.
|
|
1516
|
+
* Handles:
|
|
1517
|
+
* - Hardware frame transfer to software format
|
|
1518
|
+
* - PTS assignment from best_effort_timestamp
|
|
1519
|
+
* - PTS extrapolation when missing
|
|
1520
|
+
* - Duration estimation
|
|
1521
|
+
* - Frame tracking for next frame
|
|
1522
|
+
*
|
|
1523
|
+
* @param frame - Decoded frame to process
|
|
1524
|
+
*
|
|
1525
|
+
* @internal
|
|
1526
|
+
*/
|
|
1527
|
+
processVideoFrame(frame) {
|
|
1528
|
+
// Hardware acceleration retrieve
|
|
1529
|
+
// If hwaccel_output_format is set and frame is in hardware format, transfer to software format
|
|
1530
|
+
if (this.options.hwaccelOutputFormat !== undefined && frame.isHwFrame()) {
|
|
1531
|
+
const swFrame = new Frame();
|
|
1532
|
+
swFrame.alloc();
|
|
1533
|
+
swFrame.format = this.options.hwaccelOutputFormat;
|
|
1534
|
+
// Transfer data from hardware to software frame
|
|
1535
|
+
const ret = frame.hwframeTransferDataSync(swFrame, 0);
|
|
1536
|
+
if (ret < 0) {
|
|
1537
|
+
swFrame.free();
|
|
1538
|
+
if (this.options.exitOnError) {
|
|
1539
|
+
FFmpegError.throwIfError(ret, 'Failed to transfer hardware frame data');
|
|
1540
|
+
}
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
// Copy properties from hw frame to sw frame
|
|
1544
|
+
swFrame.copyProps(frame);
|
|
1545
|
+
// Replace frame with software version (unref old, move ref)
|
|
1546
|
+
frame.unref();
|
|
1547
|
+
const refRet = frame.ref(swFrame);
|
|
1548
|
+
swFrame.free();
|
|
1549
|
+
if (refRet < 0) {
|
|
1550
|
+
if (this.options.exitOnError) {
|
|
1551
|
+
FFmpegError.throwIfError(refRet, 'Failed to reference software frame');
|
|
1552
|
+
}
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
// Set PTS from best_effort_timestamp
|
|
1557
|
+
frame.pts = frame.bestEffortTimestamp;
|
|
1558
|
+
// DECODER_FLAG_FRAMERATE_FORCED: Ignores all timestamps and generates constant framerate
|
|
1559
|
+
if (this.options.forcedFramerate) {
|
|
1560
|
+
frame.pts = AV_NOPTS_VALUE;
|
|
1561
|
+
frame.duration = 1n;
|
|
1562
|
+
const invFramerate = avInvQ(this.options.forcedFramerate);
|
|
1563
|
+
frame.timeBase = new Rational(invFramerate.num, invFramerate.den);
|
|
1564
|
+
}
|
|
1565
|
+
// No timestamp available - extrapolate from previous frame duration
|
|
1566
|
+
if (frame.pts === AV_NOPTS_VALUE) {
|
|
1567
|
+
frame.pts = this.lastFramePts === AV_NOPTS_VALUE ? 0n : this.lastFramePts + this.lastFrameDurationEst;
|
|
1568
|
+
}
|
|
1569
|
+
// Update timestamp history
|
|
1570
|
+
this.lastFrameDurationEst = this.estimateVideoDuration(frame);
|
|
1571
|
+
this.lastFramePts = frame.pts;
|
|
1572
|
+
this.lastFrameTb = new Rational(frame.timeBase.num, frame.timeBase.den);
|
|
1573
|
+
// SAR override
|
|
1574
|
+
if (this.options.sarOverride) {
|
|
1575
|
+
frame.sampleAspectRatio = new Rational(this.options.sarOverride.num, this.options.sarOverride.den);
|
|
1576
|
+
}
|
|
1577
|
+
// Apply cropping
|
|
1578
|
+
if (this.options.applyCropping) {
|
|
1579
|
+
const ret = frame.applyCropping(1); // AV_FRAME_CROP_UNALIGNED = 1
|
|
1580
|
+
if (ret < 0) {
|
|
1581
|
+
if (this.options.exitOnError) {
|
|
1582
|
+
FFmpegError.throwIfError(ret, 'Error applying decoder cropping');
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Audio samplerate update - handles sample rate changes.
|
|
1589
|
+
*
|
|
1590
|
+
* Based on FFmpeg's audio_samplerate_update().
|
|
1591
|
+
*
|
|
1592
|
+
* On sample rate change, chooses a new internal timebase that can represent
|
|
1593
|
+
* timestamps from all sample rates seen so far. Uses GCD to find minimal
|
|
1594
|
+
* common timebase, with fallback to LCM of common sample rates (28224000).
|
|
1595
|
+
*
|
|
1596
|
+
* Handles:
|
|
1597
|
+
* - Sample rate change detection
|
|
1598
|
+
* - Timebase calculation via GCD
|
|
1599
|
+
* - Overflow detection and fallback
|
|
1600
|
+
* - Frame timebase optimization
|
|
1601
|
+
* - Rescaling existing timestamps
|
|
1602
|
+
*
|
|
1603
|
+
* @param frame - Audio frame to process
|
|
1604
|
+
*
|
|
1605
|
+
* @returns Timebase to use for this frame
|
|
1606
|
+
*
|
|
1607
|
+
* @internal
|
|
1608
|
+
*/
|
|
1609
|
+
audioSamplerateUpdate(frame) {
|
|
1610
|
+
const prev = this.lastFrameTb.den;
|
|
1611
|
+
const sr = frame.sampleRate;
|
|
1612
|
+
// No change - return existing timebase
|
|
1613
|
+
if (frame.sampleRate === this.lastFrameSampleRate) {
|
|
1614
|
+
return this.lastFrameTb;
|
|
1615
|
+
}
|
|
1616
|
+
// Calculate GCD to find minimal common timebase
|
|
1617
|
+
const gcd = avGcd(prev, sr);
|
|
1618
|
+
let tbNew;
|
|
1619
|
+
// Check for overflow
|
|
1620
|
+
if (Number(prev) / Number(gcd) >= INT_MAX / sr) {
|
|
1621
|
+
// LCM of 192000, 44100 - represents all common sample rates
|
|
1622
|
+
tbNew = { num: 1, den: 28224000 };
|
|
1623
|
+
}
|
|
1624
|
+
else {
|
|
1625
|
+
// Normal case
|
|
1626
|
+
tbNew = { num: 1, den: (Number(prev) / Number(gcd)) * sr };
|
|
1627
|
+
}
|
|
1628
|
+
// Keep frame's timebase if strictly better
|
|
1629
|
+
// "Strictly better" means: num=1, den > tbNew.den, and tbNew.den divides den evenly
|
|
1630
|
+
if (frame.timeBase.num === 1 && frame.timeBase.den > tbNew.den && frame.timeBase.den % tbNew.den === 0) {
|
|
1631
|
+
tbNew = { num: frame.timeBase.num, den: frame.timeBase.den };
|
|
1632
|
+
}
|
|
1633
|
+
// Rescale existing timestamps to new timebase
|
|
1634
|
+
if (this.lastFramePts !== AV_NOPTS_VALUE) {
|
|
1635
|
+
this.lastFramePts = avRescaleQ(this.lastFramePts, this.lastFrameTb, tbNew);
|
|
1636
|
+
}
|
|
1637
|
+
this.lastFrameDurationEst = avRescaleQ(this.lastFrameDurationEst, this.lastFrameTb, tbNew);
|
|
1638
|
+
this.lastFrameTb = new Rational(tbNew.num, tbNew.den);
|
|
1639
|
+
this.lastFrameSampleRate = frame.sampleRate;
|
|
1640
|
+
return this.lastFrameTb;
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Audio timestamp processing - handles audio frame timestamps.
|
|
1644
|
+
*
|
|
1645
|
+
* Based on FFmpeg's audio_ts_process().
|
|
1646
|
+
*
|
|
1647
|
+
* Processes audio frame timestamps with:
|
|
1648
|
+
* - Sample rate change handling via audioSamplerateUpdate()
|
|
1649
|
+
* - PTS extrapolation when missing (pts_pred)
|
|
1650
|
+
* - Gap detection (resets av_rescale_delta state)
|
|
1651
|
+
* - Smooth timestamp conversion via av_rescale_delta
|
|
1652
|
+
* - Duration calculation from nb_samples
|
|
1653
|
+
* - Conversion to filtering timebase {1, sample_rate}
|
|
1654
|
+
*
|
|
1655
|
+
* Handles:
|
|
1656
|
+
* - Dynamic sample rate changes
|
|
1657
|
+
* - Missing timestamps (AV_NOPTS_VALUE)
|
|
1658
|
+
* - Timestamp gaps/discontinuities
|
|
1659
|
+
* - Sample-accurate timestamp generation
|
|
1660
|
+
* - Frame duration calculation
|
|
1661
|
+
*
|
|
1662
|
+
* @param frame - Decoded audio frame to process
|
|
1663
|
+
*
|
|
1664
|
+
* @internal
|
|
1665
|
+
*/
|
|
1666
|
+
processAudioFrame(frame) {
|
|
1667
|
+
// Filtering timebase is always {1, sample_rate} for audio
|
|
1668
|
+
const tbFilter = { num: 1, den: frame.sampleRate };
|
|
1669
|
+
// Handle sample rate change - updates internal timebase
|
|
1670
|
+
const tb = this.audioSamplerateUpdate(frame);
|
|
1671
|
+
// Predict next PTS based on last frame + duration
|
|
1672
|
+
const ptsPred = this.lastFramePts === AV_NOPTS_VALUE ? 0n : this.lastFramePts + this.lastFrameDurationEst;
|
|
1673
|
+
// No timestamp - use predicted value
|
|
1674
|
+
if (frame.pts === AV_NOPTS_VALUE) {
|
|
1675
|
+
frame.pts = ptsPred;
|
|
1676
|
+
frame.timeBase = new Rational(tb.num, tb.den);
|
|
1677
|
+
}
|
|
1678
|
+
else if (this.lastFramePts !== AV_NOPTS_VALUE) {
|
|
1679
|
+
// Detect timestamp gap - compare with predicted timestamp
|
|
1680
|
+
const ptsPredInFrameTb = avRescaleQRnd(ptsPred, tb, frame.timeBase, AV_ROUND_UP);
|
|
1681
|
+
if (frame.pts > ptsPredInFrameTb) {
|
|
1682
|
+
// Gap detected - reset rescale_delta state for smooth conversion
|
|
1683
|
+
this.lastFilterInRescaleDelta = AV_NOPTS_VALUE;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
// Smooth timestamp conversion with av_rescale_delta
|
|
1687
|
+
// This maintains fractional sample accuracy across timebase conversions
|
|
1688
|
+
// avRescaleDelta modifies lastRef in place (simulates C's &last_filter_in_rescale_delta)
|
|
1689
|
+
const lastRef = { value: this.lastFilterInRescaleDelta };
|
|
1690
|
+
frame.pts = avRescaleDelta(frame.timeBase, frame.pts, tb, frame.nbSamples, lastRef, tb);
|
|
1691
|
+
this.lastFilterInRescaleDelta = lastRef.value;
|
|
1692
|
+
// Update frame tracking
|
|
1693
|
+
this.lastFramePts = frame.pts;
|
|
1694
|
+
this.lastFrameDurationEst = avRescaleQ(BigInt(frame.nbSamples), tbFilter, tb);
|
|
1695
|
+
// Convert to filtering timebase
|
|
1696
|
+
frame.pts = avRescaleQ(frame.pts, tb, tbFilter);
|
|
1697
|
+
frame.duration = BigInt(frame.nbSamples);
|
|
1698
|
+
frame.timeBase = new Rational(tbFilter.num, tbFilter.den);
|
|
1699
|
+
}
|
|
911
1700
|
/**
|
|
912
1701
|
* Dispose of decoder.
|
|
913
1702
|
*
|