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/filter.js
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
+
/* eslint-disable @stylistic/indent-binary-ops */
|
|
54
|
+
import { AV_BUFFERSRC_FLAG_PUSH, AVERROR_EAGAIN, AVERROR_EOF, AVFILTER_FLAG_HWDEVICE } from '../constants/constants.js';
|
|
55
|
+
import { FFmpegError } from '../lib/error.js';
|
|
56
|
+
import { FilterGraph } from '../lib/filter-graph.js';
|
|
57
|
+
import { FilterInOut } from '../lib/filter-inout.js';
|
|
58
|
+
import { Filter } from '../lib/filter.js';
|
|
59
|
+
import { Frame } from '../lib/frame.js';
|
|
60
|
+
import { Rational } from '../lib/rational.js';
|
|
61
|
+
import { avGetSampleFmtName, avInvQ, avRescaleQ } from '../lib/utilities.js';
|
|
62
|
+
import { FRAME_THREAD_QUEUE_SIZE } from './constants.js';
|
|
63
|
+
import { AsyncQueue } from './utilities/async-queue.js';
|
|
64
|
+
import { Scheduler } from './utilities/scheduler.js';
|
|
4
65
|
/**
|
|
5
66
|
* High-level filter API for audio and video processing.
|
|
6
67
|
*
|
|
@@ -44,8 +105,20 @@ export class FilterAPI {
|
|
|
44
105
|
options;
|
|
45
106
|
buffersrcCtx = null;
|
|
46
107
|
buffersinkCtx = null;
|
|
108
|
+
frame = new Frame(); // Reusable frame for receive operations
|
|
109
|
+
initializePromise = null;
|
|
47
110
|
initialized = false;
|
|
48
111
|
isClosed = false;
|
|
112
|
+
// Auto-calculated timeBase from first frame
|
|
113
|
+
calculatedTimeBase = null;
|
|
114
|
+
// Track last frame properties for change detection (for dropOnChange/allowReinit)
|
|
115
|
+
lastFrameProps = null;
|
|
116
|
+
// Worker pattern for push-based processing
|
|
117
|
+
inputQueue;
|
|
118
|
+
outputQueue;
|
|
119
|
+
workerPromise = null;
|
|
120
|
+
nextComponent = null;
|
|
121
|
+
pipeToPromise = null;
|
|
49
122
|
/**
|
|
50
123
|
* @param graph - Filter graph instance
|
|
51
124
|
*
|
|
@@ -59,50 +132,58 @@ export class FilterAPI {
|
|
|
59
132
|
this.graph = graph;
|
|
60
133
|
this.description = description;
|
|
61
134
|
this.options = options;
|
|
135
|
+
this.inputQueue = new AsyncQueue(FRAME_THREAD_QUEUE_SIZE);
|
|
136
|
+
this.outputQueue = new AsyncQueue(FRAME_THREAD_QUEUE_SIZE);
|
|
62
137
|
}
|
|
63
138
|
/**
|
|
64
139
|
* Create a filter with specified description and configuration.
|
|
65
140
|
*
|
|
66
141
|
* Creates and allocates filter graph immediately.
|
|
67
142
|
* Filter configuration is completed on first frame with frame properties.
|
|
143
|
+
* TimeBase is automatically calculated from first frame based on CFR option.
|
|
68
144
|
* Hardware frames context is automatically detected from input frames.
|
|
69
145
|
*
|
|
70
146
|
* Direct mapping to avfilter_graph_parse_ptr() and avfilter_graph_config().
|
|
71
147
|
*
|
|
72
148
|
* @param description - Filter graph description
|
|
73
149
|
*
|
|
74
|
-
* @param options - Filter options
|
|
150
|
+
* @param options - Filter options
|
|
75
151
|
*
|
|
76
152
|
* @returns Configured filter instance
|
|
77
153
|
*
|
|
154
|
+
* @throws {Error} If cfr=true but framerate is not set
|
|
155
|
+
*
|
|
78
156
|
* @example
|
|
79
157
|
* ```typescript
|
|
80
|
-
* // Simple video filter
|
|
81
|
-
* const filter = FilterAPI.create('scale=640:480'
|
|
82
|
-
* timeBase: video.timeBase
|
|
83
|
-
* });
|
|
158
|
+
* // Simple video filter (VFR mode, auto timeBase)
|
|
159
|
+
* const filter = FilterAPI.create('scale=640:480');
|
|
84
160
|
* ```
|
|
85
161
|
*
|
|
86
162
|
* @example
|
|
87
163
|
* ```typescript
|
|
88
|
-
* //
|
|
89
|
-
* const filter = FilterAPI.create('
|
|
90
|
-
*
|
|
164
|
+
* // CFR mode with constant framerate
|
|
165
|
+
* const filter = FilterAPI.create('scale=1920:1080', {
|
|
166
|
+
* cfr: true,
|
|
167
|
+
* framerate: { num: 25, den: 1 }
|
|
91
168
|
* });
|
|
92
169
|
* ```
|
|
93
170
|
*
|
|
94
171
|
* @example
|
|
95
172
|
* ```typescript
|
|
96
|
-
* // Audio filter
|
|
97
|
-
* const filter = FilterAPI.create('
|
|
98
|
-
*
|
|
173
|
+
* // Audio filter with resampling
|
|
174
|
+
* const filter = FilterAPI.create('aformat=sample_fmts=s16:sample_rates=44100', {
|
|
175
|
+
* audioResampleOpts: 'async=1'
|
|
99
176
|
* });
|
|
100
177
|
* ```
|
|
101
178
|
*
|
|
102
179
|
* @see {@link process} For frame processing
|
|
103
180
|
* @see {@link FilterOptions} For configuration options
|
|
104
181
|
*/
|
|
105
|
-
static create(description, options) {
|
|
182
|
+
static create(description, options = {}) {
|
|
183
|
+
// Validate options: CFR requires framerate
|
|
184
|
+
if (options.cfr && !options.framerate) {
|
|
185
|
+
throw new Error('cfr=true requires framerate to be set');
|
|
186
|
+
}
|
|
106
187
|
// Create graph
|
|
107
188
|
const graph = new FilterGraph();
|
|
108
189
|
graph.alloc();
|
|
@@ -147,6 +228,261 @@ export class FilterAPI {
|
|
|
147
228
|
get isFilterInitialized() {
|
|
148
229
|
return this.initialized;
|
|
149
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Get buffersink filter context.
|
|
233
|
+
*
|
|
234
|
+
* Provides access to the buffersink filter context for advanced operations.
|
|
235
|
+
* Returns null if filter is not initialized.
|
|
236
|
+
*
|
|
237
|
+
* @returns Buffersink context or null
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```typescript
|
|
241
|
+
* const sink = filter.buffersink;
|
|
242
|
+
* if (sink) {
|
|
243
|
+
* const fr = sink.buffersinkGetFrameRate();
|
|
244
|
+
* console.log(`Output frame rate: ${fr.num}/${fr.den}`);
|
|
245
|
+
* }
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
get buffersink() {
|
|
249
|
+
return this.buffersinkCtx;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Output frame rate from filter graph.
|
|
253
|
+
*
|
|
254
|
+
* Returns the frame rate determined by the filter graph output.
|
|
255
|
+
* Matches FFmpeg CLI's av_buffersink_get_frame_rate() behavior.
|
|
256
|
+
* Returns null if filter is not initialized or frame rate is not set.
|
|
257
|
+
*
|
|
258
|
+
* Direct mapping to av_buffersink_get_frame_rate().
|
|
259
|
+
*
|
|
260
|
+
* @returns Frame rate or null if not available
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```typescript
|
|
264
|
+
* const frameRate = filter.frameRate;
|
|
265
|
+
* if (frameRate) {
|
|
266
|
+
* console.log(`Filter output: ${frameRate.num}/${frameRate.den} fps`);
|
|
267
|
+
* }
|
|
268
|
+
* ```
|
|
269
|
+
*
|
|
270
|
+
* @see {@link timeBase} For output timebase
|
|
271
|
+
*/
|
|
272
|
+
get frameRate() {
|
|
273
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
const fr = this.buffersinkCtx.buffersinkGetFrameRate();
|
|
277
|
+
// Return null if frame rate is not set (0/0 or 0/1)
|
|
278
|
+
if (fr.num <= 0 || fr.den <= 0) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
return fr;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Output time base from filter graph.
|
|
285
|
+
*
|
|
286
|
+
* Returns the time base of the buffersink output.
|
|
287
|
+
* Matches FFmpeg CLI's av_buffersink_get_time_base() behavior.
|
|
288
|
+
*
|
|
289
|
+
* Direct mapping to av_buffersink_get_time_base().
|
|
290
|
+
*
|
|
291
|
+
* @returns Time base or null if not initialized
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* const timeBase = filter.timeBase;
|
|
296
|
+
* if (timeBase) {
|
|
297
|
+
* console.log(`Filter timebase: ${timeBase.num}/${timeBase.den}`);
|
|
298
|
+
* }
|
|
299
|
+
* ```
|
|
300
|
+
*
|
|
301
|
+
* @see {@link frameRate} For output frame rate
|
|
302
|
+
*/
|
|
303
|
+
get timeBase() {
|
|
304
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
return this.buffersinkCtx.buffersinkGetTimeBase();
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Output format from filter graph.
|
|
311
|
+
*
|
|
312
|
+
* Returns the pixel format (video) or sample format (audio) of the buffersink output.
|
|
313
|
+
* Matches FFmpeg CLI's av_buffersink_get_format() behavior.
|
|
314
|
+
*
|
|
315
|
+
* Direct mapping to av_buffersink_get_format().
|
|
316
|
+
*
|
|
317
|
+
* @returns Pixel format or sample format, or null if not initialized
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```typescript
|
|
321
|
+
* const format = filter.format;
|
|
322
|
+
* if (format !== null) {
|
|
323
|
+
* console.log(`Filter output format: ${format}`);
|
|
324
|
+
* }
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
get format() {
|
|
328
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
return this.buffersinkCtx.buffersinkGetFormat();
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Output dimensions from filter graph (video only).
|
|
335
|
+
*
|
|
336
|
+
* Returns the width and height of the buffersink output.
|
|
337
|
+
* Matches FFmpeg CLI's av_buffersink_get_w() and av_buffersink_get_h() behavior.
|
|
338
|
+
* Only meaningful for video filters.
|
|
339
|
+
*
|
|
340
|
+
* Direct mapping to av_buffersink_get_w() and av_buffersink_get_h().
|
|
341
|
+
*
|
|
342
|
+
* @returns Dimensions object or null if not initialized
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* ```typescript
|
|
346
|
+
* const dims = filter.dimensions;
|
|
347
|
+
* if (dims) {
|
|
348
|
+
* console.log(`Filter output: ${dims.width}x${dims.height}`);
|
|
349
|
+
* }
|
|
350
|
+
* ```
|
|
351
|
+
*/
|
|
352
|
+
get dimensions() {
|
|
353
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
width: this.buffersinkCtx.buffersinkGetWidth(),
|
|
358
|
+
height: this.buffersinkCtx.buffersinkGetHeight(),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Output sample rate from filter graph (audio only).
|
|
363
|
+
*
|
|
364
|
+
* Returns the sample rate of the buffersink output.
|
|
365
|
+
* Matches FFmpeg CLI's av_buffersink_get_sample_rate() behavior.
|
|
366
|
+
* Only meaningful for audio filters.
|
|
367
|
+
*
|
|
368
|
+
* Direct mapping to av_buffersink_get_sample_rate().
|
|
369
|
+
*
|
|
370
|
+
* @returns Sample rate or null if not initialized
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```typescript
|
|
374
|
+
* const sampleRate = filter.sampleRate;
|
|
375
|
+
* if (sampleRate) {
|
|
376
|
+
* console.log(`Filter output sample rate: ${sampleRate} Hz`);
|
|
377
|
+
* }
|
|
378
|
+
* ```
|
|
379
|
+
*/
|
|
380
|
+
get sampleRate() {
|
|
381
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
return this.buffersinkCtx.buffersinkGetSampleRate();
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Output channel layout from filter graph (audio only).
|
|
388
|
+
*
|
|
389
|
+
* Returns the channel layout of the buffersink output.
|
|
390
|
+
* Matches FFmpeg CLI's av_buffersink_get_ch_layout() behavior.
|
|
391
|
+
* Only meaningful for audio filters.
|
|
392
|
+
*
|
|
393
|
+
* Direct mapping to av_buffersink_get_ch_layout().
|
|
394
|
+
*
|
|
395
|
+
* @returns Channel layout or null if not initialized
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* ```typescript
|
|
399
|
+
* const layout = filter.channelLayout;
|
|
400
|
+
* if (layout) {
|
|
401
|
+
* console.log(`Filter output channels: ${layout.nbChannels}`);
|
|
402
|
+
* }
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
405
|
+
get channelLayout() {
|
|
406
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
return this.buffersinkCtx.buffersinkGetChannelLayout();
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Output color space from filter graph (video only).
|
|
413
|
+
*
|
|
414
|
+
* Returns the color space of the buffersink output.
|
|
415
|
+
* Matches FFmpeg CLI's av_buffersink_get_colorspace() behavior.
|
|
416
|
+
* Only meaningful for video filters.
|
|
417
|
+
*
|
|
418
|
+
* Direct mapping to av_buffersink_get_colorspace().
|
|
419
|
+
*
|
|
420
|
+
* @returns Color space or null if not initialized
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* ```typescript
|
|
424
|
+
* const colorSpace = filter.colorSpace;
|
|
425
|
+
* if (colorSpace !== null) {
|
|
426
|
+
* console.log(`Filter output color space: ${colorSpace}`);
|
|
427
|
+
* }
|
|
428
|
+
* ```
|
|
429
|
+
*/
|
|
430
|
+
get colorSpace() {
|
|
431
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
return this.buffersinkCtx.buffersinkGetColorspace();
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Output color range from filter graph (video only).
|
|
438
|
+
*
|
|
439
|
+
* Returns the color range of the buffersink output.
|
|
440
|
+
* Matches FFmpeg CLI's av_buffersink_get_color_range() behavior.
|
|
441
|
+
* Only meaningful for video filters.
|
|
442
|
+
*
|
|
443
|
+
* Direct mapping to av_buffersink_get_color_range().
|
|
444
|
+
*
|
|
445
|
+
* @returns Color range or null if not initialized
|
|
446
|
+
*
|
|
447
|
+
* @example
|
|
448
|
+
* ```typescript
|
|
449
|
+
* const colorRange = filter.colorRange;
|
|
450
|
+
* if (colorRange !== null) {
|
|
451
|
+
* console.log(`Filter output color range: ${colorRange}`);
|
|
452
|
+
* }
|
|
453
|
+
* ```
|
|
454
|
+
*/
|
|
455
|
+
get colorRange() {
|
|
456
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
return this.buffersinkCtx.buffersinkGetColorRange();
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Output sample aspect ratio from filter graph (video only).
|
|
463
|
+
*
|
|
464
|
+
* Returns the sample aspect ratio of the buffersink output.
|
|
465
|
+
* Matches FFmpeg CLI's av_buffersink_get_sample_aspect_ratio() behavior.
|
|
466
|
+
* Only meaningful for video filters.
|
|
467
|
+
*
|
|
468
|
+
* Direct mapping to av_buffersink_get_sample_aspect_ratio().
|
|
469
|
+
*
|
|
470
|
+
* @returns Sample aspect ratio or null if not initialized
|
|
471
|
+
*
|
|
472
|
+
* @example
|
|
473
|
+
* ```typescript
|
|
474
|
+
* const sar = filter.sampleAspectRatio;
|
|
475
|
+
* if (sar) {
|
|
476
|
+
* console.log(`Filter output SAR: ${sar.num}:${sar.den}`);
|
|
477
|
+
* }
|
|
478
|
+
* ```
|
|
479
|
+
*/
|
|
480
|
+
get sampleAspectRatio() {
|
|
481
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
return this.buffersinkCtx.buffersinkGetSampleAspectRatio();
|
|
485
|
+
}
|
|
150
486
|
/**
|
|
151
487
|
* Check if filter is ready for processing.
|
|
152
488
|
*
|
|
@@ -190,6 +526,10 @@ export class FilterAPI {
|
|
|
190
526
|
* Hardware frames context is automatically detected from frame.
|
|
191
527
|
* Returns null if filter is closed and frame is null.
|
|
192
528
|
*
|
|
529
|
+
* **Note**: This method receives only ONE frame per call.
|
|
530
|
+
* A single input frame can produce multiple output frames (e.g., fps filter, deinterlace).
|
|
531
|
+
* To receive all frames from an input, use {@link processAll} or {@link frames} instead.
|
|
532
|
+
*
|
|
193
533
|
* Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
|
|
194
534
|
*
|
|
195
535
|
* @param frame - Input frame to process (or null to flush)
|
|
@@ -220,8 +560,10 @@ export class FilterAPI {
|
|
|
220
560
|
* // For buffered frames, use the frames() async generator
|
|
221
561
|
* ```
|
|
222
562
|
*
|
|
563
|
+
* @see {@link processAll} For processing multiple output frames
|
|
223
564
|
* @see {@link frames} For processing frame streams
|
|
224
565
|
* @see {@link flush} For end-of-stream handling
|
|
566
|
+
* @see {@link processSync} For synchronous version
|
|
225
567
|
*/
|
|
226
568
|
async process(frame) {
|
|
227
569
|
if (this.isClosed) {
|
|
@@ -232,34 +574,40 @@ export class FilterAPI {
|
|
|
232
574
|
if (!frame) {
|
|
233
575
|
return null;
|
|
234
576
|
}
|
|
235
|
-
|
|
577
|
+
this.initializePromise ??= this.initialize(frame);
|
|
236
578
|
}
|
|
579
|
+
await this.initializePromise;
|
|
237
580
|
if (!this.initialized) {
|
|
238
581
|
return null;
|
|
239
582
|
}
|
|
240
583
|
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
241
584
|
throw new Error('Could not initialize filter contexts');
|
|
242
585
|
}
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// Try to get filtered frame
|
|
247
|
-
const outputFrame = new Frame();
|
|
248
|
-
outputFrame.alloc();
|
|
249
|
-
const getRet = await this.buffersinkCtx.buffersinkGetFrame(outputFrame);
|
|
250
|
-
if (getRet >= 0) {
|
|
251
|
-
return outputFrame;
|
|
252
|
-
}
|
|
253
|
-
else if (getRet === AVERROR_EAGAIN) {
|
|
254
|
-
// Need more input
|
|
255
|
-
outputFrame.free();
|
|
586
|
+
// Check for frame property changes (FFmpeg: dropOnChange/allowReinit logic)
|
|
587
|
+
if (frame && !this.checkFramePropertiesChanged(frame)) {
|
|
588
|
+
// Frame dropped due to property change
|
|
256
589
|
return null;
|
|
257
590
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
591
|
+
// If reinitialized, reinitialize now
|
|
592
|
+
if (!this.initialized && frame) {
|
|
593
|
+
this.initializePromise = this.initialize(frame);
|
|
594
|
+
await this.initializePromise;
|
|
595
|
+
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
596
|
+
throw new Error('Could not reinitialize filter contexts');
|
|
597
|
+
}
|
|
262
598
|
}
|
|
599
|
+
// Rescale timestamps to filter's timeBase
|
|
600
|
+
if (frame && this.calculatedTimeBase) {
|
|
601
|
+
const originalTimeBase = frame.timeBase;
|
|
602
|
+
frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
|
|
603
|
+
frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
|
|
604
|
+
frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
|
|
605
|
+
}
|
|
606
|
+
// Send frame to filter with PUSH flag for immediate processing
|
|
607
|
+
const addRet = await this.buffersrcCtx.buffersrcAddFrame(frame, AV_BUFFERSRC_FLAG_PUSH);
|
|
608
|
+
FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
|
|
609
|
+
// Try to get filtered frame using receive()
|
|
610
|
+
return await this.receive();
|
|
263
611
|
}
|
|
264
612
|
/**
|
|
265
613
|
* Process a frame through the filter synchronously.
|
|
@@ -271,6 +619,10 @@ export class FilterAPI {
|
|
|
271
619
|
* Hardware frames context is automatically detected from frame.
|
|
272
620
|
* Returns null if filter is closed and frame is null.
|
|
273
621
|
*
|
|
622
|
+
* **Note**: This method receives only ONE frame per call.
|
|
623
|
+
* A single input frame can produce multiple output frames (e.g., fps filter, deinterlace).
|
|
624
|
+
* To receive all frames from an input, use {@link processAllSync} or {@link framesSync} instead.
|
|
625
|
+
*
|
|
274
626
|
* Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
|
|
275
627
|
*
|
|
276
628
|
* @param frame - Input frame to process (or null to flush)
|
|
@@ -301,6 +653,9 @@ export class FilterAPI {
|
|
|
301
653
|
* // For buffered frames, use the framesSync() generator
|
|
302
654
|
* ```
|
|
303
655
|
*
|
|
656
|
+
* @see {@link processAllSync} For processing multiple output frames
|
|
657
|
+
* @see {@link framesSync} For processing frame streams
|
|
658
|
+
* @see {@link flushSync} For end-of-stream handling
|
|
304
659
|
* @see {@link process} For async version
|
|
305
660
|
*/
|
|
306
661
|
processSync(frame) {
|
|
@@ -314,114 +669,193 @@ export class FilterAPI {
|
|
|
314
669
|
}
|
|
315
670
|
this.initializeSync(frame);
|
|
316
671
|
}
|
|
672
|
+
if (!this.initialized) {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
317
675
|
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
318
676
|
throw new Error('Could not initialize filter contexts');
|
|
319
677
|
}
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
// Try to get filtered frame
|
|
324
|
-
const outputFrame = new Frame();
|
|
325
|
-
outputFrame.alloc();
|
|
326
|
-
const getRet = this.buffersinkCtx.buffersinkGetFrameSync(outputFrame);
|
|
327
|
-
if (getRet >= 0) {
|
|
328
|
-
return outputFrame;
|
|
329
|
-
}
|
|
330
|
-
else if (getRet === AVERROR_EAGAIN) {
|
|
331
|
-
// Need more input
|
|
332
|
-
outputFrame.free();
|
|
678
|
+
// Check for frame property changes (FFmpeg: dropOnChange/allowReinit logic)
|
|
679
|
+
if (frame && !this.checkFramePropertiesChanged(frame)) {
|
|
680
|
+
// Frame dropped due to property change
|
|
333
681
|
return null;
|
|
334
682
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
683
|
+
// If reinitialized, reinitialize now
|
|
684
|
+
if (!this.initialized && frame) {
|
|
685
|
+
this.initializeSync(frame);
|
|
686
|
+
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
687
|
+
throw new Error('Could not reinitialize filter contexts');
|
|
688
|
+
}
|
|
339
689
|
}
|
|
690
|
+
// Rescale timestamps to filter's timeBase
|
|
691
|
+
if (frame && this.calculatedTimeBase) {
|
|
692
|
+
const originalTimeBase = frame.timeBase;
|
|
693
|
+
frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
|
|
694
|
+
frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
|
|
695
|
+
frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
|
|
696
|
+
}
|
|
697
|
+
// Send frame to filter with PUSH flag for immediate processing
|
|
698
|
+
const addRet = this.buffersrcCtx.buffersrcAddFrameSync(frame, AV_BUFFERSRC_FLAG_PUSH);
|
|
699
|
+
FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
|
|
700
|
+
// Try to get filtered frame using receiveSync()
|
|
701
|
+
return this.receiveSync();
|
|
340
702
|
}
|
|
341
703
|
/**
|
|
342
|
-
* Process
|
|
704
|
+
* Process a frame through the filter.
|
|
343
705
|
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
706
|
+
* Applies filter operations to input frame and receives all available output frames.
|
|
707
|
+
* Returns array of frames - may be empty if filter needs more input.
|
|
708
|
+
* On first frame, automatically builds filter graph with frame properties.
|
|
709
|
+
* One input frame can produce zero, one, or multiple output frames depending on filter.
|
|
710
|
+
* Hardware frames context is automatically detected from frame.
|
|
711
|
+
*
|
|
712
|
+
* Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
|
|
346
713
|
*
|
|
347
|
-
* @param
|
|
714
|
+
* @param frame - Input frame to process (or null to flush)
|
|
348
715
|
*
|
|
349
|
-
* @returns Array of
|
|
716
|
+
* @returns Array of filtered frames (empty if buffered or filter closed)
|
|
350
717
|
*
|
|
351
|
-
* @throws {Error} If filter not
|
|
718
|
+
* @throws {Error} If filter could not be initialized
|
|
352
719
|
*
|
|
353
720
|
* @throws {FFmpegError} If processing fails
|
|
354
721
|
*
|
|
355
722
|
* @example
|
|
356
723
|
* ```typescript
|
|
357
|
-
* const
|
|
358
|
-
* for (const output of
|
|
359
|
-
* console.log(`
|
|
724
|
+
* const frames = await filter.processAll(inputFrame);
|
|
725
|
+
* for (const output of frames) {
|
|
726
|
+
* console.log(`Got filtered frame: pts=${output.pts}`);
|
|
360
727
|
* output.free();
|
|
361
728
|
* }
|
|
362
729
|
* ```
|
|
363
730
|
*
|
|
731
|
+
* @example
|
|
732
|
+
* ```typescript
|
|
733
|
+
* // Process frame - may return multiple frames (e.g. fps filter)
|
|
734
|
+
* const frames = await filter.processAll(frame);
|
|
735
|
+
* for (const output of frames) {
|
|
736
|
+
* yield output;
|
|
737
|
+
* }
|
|
738
|
+
* ```
|
|
739
|
+
*
|
|
364
740
|
* @see {@link process} For single frame processing
|
|
741
|
+
* @see {@link frames} For processing frame streams
|
|
742
|
+
* @see {@link flush} For end-of-stream handling
|
|
743
|
+
* @see {@link processAllSync} For synchronous version
|
|
365
744
|
*/
|
|
366
|
-
async
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
while (!this.isClosed) {
|
|
375
|
-
const additional = await this.receive();
|
|
376
|
-
if (!additional)
|
|
377
|
-
break;
|
|
378
|
-
outputFrames.push(additional);
|
|
745
|
+
async processAll(frame) {
|
|
746
|
+
if (this.isClosed) {
|
|
747
|
+
return [];
|
|
748
|
+
}
|
|
749
|
+
// Open filter if not already done
|
|
750
|
+
if (!this.initialized) {
|
|
751
|
+
if (!frame) {
|
|
752
|
+
return [];
|
|
379
753
|
}
|
|
754
|
+
this.initializePromise ??= this.initialize(frame);
|
|
755
|
+
}
|
|
756
|
+
await this.initializePromise;
|
|
757
|
+
if (!this.initialized) {
|
|
758
|
+
return [];
|
|
759
|
+
}
|
|
760
|
+
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
761
|
+
throw new Error('Could not initialize filter contexts');
|
|
762
|
+
}
|
|
763
|
+
// Rescale timestamps to filter's timeBase
|
|
764
|
+
if (frame && this.calculatedTimeBase) {
|
|
765
|
+
const originalTimeBase = frame.timeBase;
|
|
766
|
+
frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
|
|
767
|
+
frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
|
|
768
|
+
frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
|
|
769
|
+
}
|
|
770
|
+
// Send frame to filter with PUSH flag for immediate processing
|
|
771
|
+
const addRet = await this.buffersrcCtx.buffersrcAddFrame(frame, AV_BUFFERSRC_FLAG_PUSH);
|
|
772
|
+
FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
|
|
773
|
+
// Receive all available frames using receive()
|
|
774
|
+
const frames = [];
|
|
775
|
+
while (true) {
|
|
776
|
+
const outputFrame = await this.receive();
|
|
777
|
+
if (!outputFrame)
|
|
778
|
+
break;
|
|
779
|
+
frames.push(outputFrame);
|
|
380
780
|
}
|
|
381
|
-
return
|
|
781
|
+
return frames;
|
|
382
782
|
}
|
|
383
783
|
/**
|
|
384
|
-
* Process
|
|
385
|
-
* Synchronous version of
|
|
784
|
+
* Process a frame through the filter synchronously.
|
|
785
|
+
* Synchronous version of processAll.
|
|
386
786
|
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
787
|
+
* Applies filter operations to input frame and receives all available output frames.
|
|
788
|
+
* Returns array of frames - may be empty if filter needs more input.
|
|
789
|
+
* On first frame, automatically builds filter graph with frame properties.
|
|
790
|
+
* One input frame can produce zero, one, or multiple output frames depending on filter.
|
|
791
|
+
* Hardware frames context is automatically detected from frame.
|
|
389
792
|
*
|
|
390
|
-
*
|
|
793
|
+
* Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
|
|
391
794
|
*
|
|
392
|
-
* @
|
|
795
|
+
* @param frame - Input frame to process (or null to flush)
|
|
393
796
|
*
|
|
394
|
-
* @
|
|
797
|
+
* @returns Array of filtered frames (empty if buffered or filter closed)
|
|
798
|
+
*
|
|
799
|
+
* @throws {Error} If filter could not be initialized
|
|
395
800
|
*
|
|
396
801
|
* @throws {FFmpegError} If processing fails
|
|
397
802
|
*
|
|
398
803
|
* @example
|
|
399
804
|
* ```typescript
|
|
400
|
-
* const outputs = filter.
|
|
805
|
+
* const outputs = filter.processAllSync(inputFrame);
|
|
401
806
|
* for (const output of outputs) {
|
|
402
|
-
* console.log(`
|
|
807
|
+
* console.log(`Got filtered frame: pts=${output.pts}`);
|
|
403
808
|
* output.free();
|
|
404
809
|
* }
|
|
405
810
|
* ```
|
|
406
811
|
*
|
|
407
|
-
* @
|
|
812
|
+
* @example
|
|
813
|
+
* ```typescript
|
|
814
|
+
* // Process frame - may return multiple frames (e.g. fps filter)
|
|
815
|
+
* const outputs = filter.processAllSync(frame);
|
|
816
|
+
* for (const output of outputs) {
|
|
817
|
+
* yield output;
|
|
818
|
+
* }
|
|
819
|
+
* ```
|
|
820
|
+
*
|
|
821
|
+
* @see {@link processSync} For single frame processing
|
|
822
|
+
* @see {@link framesSync} For processing frame streams
|
|
823
|
+
* @see {@link flushSync} For end-of-stream handling
|
|
824
|
+
* @see {@link process} For async version
|
|
408
825
|
*/
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
while (!this.isClosed) {
|
|
418
|
-
const additional = this.receiveSync();
|
|
419
|
-
if (!additional)
|
|
420
|
-
break;
|
|
421
|
-
outputFrames.push(additional);
|
|
826
|
+
processAllSync(frame) {
|
|
827
|
+
if (this.isClosed) {
|
|
828
|
+
return [];
|
|
829
|
+
}
|
|
830
|
+
// Open filter if not already done
|
|
831
|
+
if (!this.initialized) {
|
|
832
|
+
if (!frame) {
|
|
833
|
+
return [];
|
|
422
834
|
}
|
|
835
|
+
this.initializeSync(frame);
|
|
836
|
+
}
|
|
837
|
+
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
838
|
+
throw new Error('Could not initialize filter contexts');
|
|
839
|
+
}
|
|
840
|
+
// Rescale timestamps to filter's timeBase
|
|
841
|
+
if (frame && this.calculatedTimeBase) {
|
|
842
|
+
const originalTimeBase = frame.timeBase;
|
|
843
|
+
frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
|
|
844
|
+
frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
|
|
845
|
+
frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
|
|
846
|
+
}
|
|
847
|
+
// Send frame to filter with PUSH flag for immediate processing
|
|
848
|
+
const addRet = this.buffersrcCtx.buffersrcAddFrameSync(frame, AV_BUFFERSRC_FLAG_PUSH);
|
|
849
|
+
FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
|
|
850
|
+
// Receive all available frames using receiveSync()
|
|
851
|
+
const frames = [];
|
|
852
|
+
while (true) {
|
|
853
|
+
const outputFrame = this.receiveSync();
|
|
854
|
+
if (!outputFrame)
|
|
855
|
+
break;
|
|
856
|
+
frames.push(outputFrame);
|
|
423
857
|
}
|
|
424
|
-
return
|
|
858
|
+
return frames;
|
|
425
859
|
}
|
|
426
860
|
/**
|
|
427
861
|
* Process frame stream through filter.
|
|
@@ -464,37 +898,78 @@ export class FilterAPI {
|
|
|
464
898
|
* ```
|
|
465
899
|
*
|
|
466
900
|
* @see {@link process} For single frame processing
|
|
467
|
-
* @see {@link
|
|
901
|
+
* @see {@link Decoder.frames} For frames source
|
|
902
|
+
* @see {@link framesSync} For sync version
|
|
468
903
|
*/
|
|
469
904
|
async *frames(frames) {
|
|
470
|
-
for await (const
|
|
905
|
+
for await (const frame_1 of frames) {
|
|
906
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
471
907
|
try {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if (
|
|
475
|
-
|
|
908
|
+
const frame = __addDisposableResource(env_1, frame_1, false);
|
|
909
|
+
// Handle EOF signal
|
|
910
|
+
if (frame === null) {
|
|
911
|
+
// Flush filter
|
|
912
|
+
await this.flush();
|
|
913
|
+
while (true) {
|
|
914
|
+
const remaining = await this.receive();
|
|
915
|
+
if (!remaining)
|
|
916
|
+
break;
|
|
917
|
+
yield remaining;
|
|
918
|
+
}
|
|
919
|
+
// Signal EOF and stop processing
|
|
920
|
+
yield null;
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (this.isClosed) {
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
// Open filter if not already done
|
|
927
|
+
if (!this.initialized) {
|
|
928
|
+
this.initializePromise ??= this.initialize(frame);
|
|
476
929
|
}
|
|
477
|
-
|
|
478
|
-
|
|
930
|
+
await this.initializePromise;
|
|
931
|
+
if (!this.initialized) {
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
935
|
+
throw new Error('Could not initialize filter contexts');
|
|
936
|
+
}
|
|
937
|
+
// Rescale timestamps to filter's timeBase
|
|
938
|
+
if (this.calculatedTimeBase) {
|
|
939
|
+
const originalTimeBase = frame.timeBase;
|
|
940
|
+
frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
|
|
941
|
+
frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
|
|
942
|
+
frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
|
|
943
|
+
}
|
|
944
|
+
// Send frame to filter
|
|
945
|
+
const addRet = await this.buffersrcCtx.buffersrcAddFrame(frame, AV_BUFFERSRC_FLAG_PUSH);
|
|
946
|
+
FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
|
|
947
|
+
// Receive all available frames
|
|
948
|
+
while (true) {
|
|
479
949
|
const buffered = await this.receive();
|
|
480
950
|
if (!buffered)
|
|
481
951
|
break;
|
|
482
952
|
yield buffered;
|
|
483
953
|
}
|
|
484
954
|
}
|
|
955
|
+
catch (e_1) {
|
|
956
|
+
env_1.error = e_1;
|
|
957
|
+
env_1.hasError = true;
|
|
958
|
+
}
|
|
485
959
|
finally {
|
|
486
|
-
|
|
487
|
-
frame.free();
|
|
960
|
+
__disposeResources(env_1);
|
|
488
961
|
}
|
|
489
962
|
}
|
|
490
|
-
// Flush and get remaining frames
|
|
963
|
+
// Flush and get remaining frames (fallback if no null was sent)
|
|
491
964
|
await this.flush();
|
|
492
|
-
while (
|
|
965
|
+
while (true) {
|
|
493
966
|
const remaining = await this.receive();
|
|
494
967
|
if (!remaining)
|
|
495
968
|
break;
|
|
496
969
|
yield remaining;
|
|
497
970
|
}
|
|
971
|
+
// Signal EOF
|
|
972
|
+
yield null;
|
|
498
973
|
}
|
|
499
974
|
/**
|
|
500
975
|
* Process frame stream through filter synchronously.
|
|
@@ -537,37 +1012,78 @@ export class FilterAPI {
|
|
|
537
1012
|
* }
|
|
538
1013
|
* ```
|
|
539
1014
|
*
|
|
1015
|
+
* @see {@link processSync} For single frame processing
|
|
1016
|
+
* @see {@link Decoder.framesSync} For frames source
|
|
540
1017
|
* @see {@link frames} For async version
|
|
541
1018
|
*/
|
|
542
1019
|
*framesSync(frames) {
|
|
543
|
-
for (const
|
|
1020
|
+
for (const frame_2 of frames) {
|
|
1021
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
544
1022
|
try {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (
|
|
548
|
-
|
|
1023
|
+
const frame = __addDisposableResource(env_2, frame_2, false);
|
|
1024
|
+
// Handle EOF signal
|
|
1025
|
+
if (frame === null) {
|
|
1026
|
+
// Flush filter
|
|
1027
|
+
this.flushSync();
|
|
1028
|
+
while (true) {
|
|
1029
|
+
const remaining = this.receiveSync();
|
|
1030
|
+
if (!remaining)
|
|
1031
|
+
break;
|
|
1032
|
+
yield remaining;
|
|
1033
|
+
}
|
|
1034
|
+
// Signal EOF and stop processing
|
|
1035
|
+
yield null;
|
|
1036
|
+
return;
|
|
549
1037
|
}
|
|
550
|
-
|
|
551
|
-
|
|
1038
|
+
if (this.isClosed) {
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
// Open filter if not already done
|
|
1042
|
+
if (!this.initialized) {
|
|
1043
|
+
this.initializeSync(frame);
|
|
1044
|
+
}
|
|
1045
|
+
if (!this.initialized) {
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
1049
|
+
throw new Error('Could not initialize filter contexts');
|
|
1050
|
+
}
|
|
1051
|
+
// Rescale timestamps to filter's timeBase
|
|
1052
|
+
if (this.calculatedTimeBase) {
|
|
1053
|
+
const originalTimeBase = frame.timeBase;
|
|
1054
|
+
frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
|
|
1055
|
+
frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
|
|
1056
|
+
frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
|
|
1057
|
+
}
|
|
1058
|
+
// Send frame to filter
|
|
1059
|
+
const addRet = this.buffersrcCtx.buffersrcAddFrameSync(frame, AV_BUFFERSRC_FLAG_PUSH);
|
|
1060
|
+
FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
|
|
1061
|
+
// Receive all available frames
|
|
1062
|
+
while (true) {
|
|
552
1063
|
const buffered = this.receiveSync();
|
|
553
1064
|
if (!buffered)
|
|
554
1065
|
break;
|
|
555
1066
|
yield buffered;
|
|
556
1067
|
}
|
|
557
1068
|
}
|
|
1069
|
+
catch (e_2) {
|
|
1070
|
+
env_2.error = e_2;
|
|
1071
|
+
env_2.hasError = true;
|
|
1072
|
+
}
|
|
558
1073
|
finally {
|
|
559
|
-
|
|
560
|
-
frame.free();
|
|
1074
|
+
__disposeResources(env_2);
|
|
561
1075
|
}
|
|
562
1076
|
}
|
|
563
|
-
// Flush and get remaining frames
|
|
1077
|
+
// Flush and get remaining frames (fallback if no null was sent)
|
|
564
1078
|
this.flushSync();
|
|
565
|
-
while (
|
|
1079
|
+
while (true) {
|
|
566
1080
|
const remaining = this.receiveSync();
|
|
567
1081
|
if (!remaining)
|
|
568
1082
|
break;
|
|
569
1083
|
yield remaining;
|
|
570
1084
|
}
|
|
1085
|
+
// Signal EOF
|
|
1086
|
+
yield null;
|
|
571
1087
|
}
|
|
572
1088
|
/**
|
|
573
1089
|
* Flush filter and signal end-of-stream.
|
|
@@ -591,14 +1107,15 @@ export class FilterAPI {
|
|
|
591
1107
|
* ```
|
|
592
1108
|
*
|
|
593
1109
|
* @see {@link flushFrames} For async iteration
|
|
594
|
-
* @see {@link
|
|
1110
|
+
* @see {@link receive} For getting flushed frames
|
|
1111
|
+
* @see {@link flushSync} For synchronous version
|
|
595
1112
|
*/
|
|
596
1113
|
async flush() {
|
|
597
1114
|
if (this.isClosed || !this.initialized || !this.buffersrcCtx) {
|
|
598
1115
|
return;
|
|
599
1116
|
}
|
|
600
1117
|
// Send flush frame (null)
|
|
601
|
-
const ret = await this.buffersrcCtx.buffersrcAddFrame(null);
|
|
1118
|
+
const ret = await this.buffersrcCtx.buffersrcAddFrame(null, AV_BUFFERSRC_FLAG_PUSH);
|
|
602
1119
|
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
603
1120
|
FFmpegError.throwIfError(ret, 'Failed to flush filter');
|
|
604
1121
|
}
|
|
@@ -625,6 +1142,8 @@ export class FilterAPI {
|
|
|
625
1142
|
* }
|
|
626
1143
|
* ```
|
|
627
1144
|
*
|
|
1145
|
+
* @see {@link flushFramesSync} For sync iteration
|
|
1146
|
+
* @see {@link receiveSync} For getting flushed frames
|
|
628
1147
|
* @see {@link flush} For async version
|
|
629
1148
|
*/
|
|
630
1149
|
flushSync() {
|
|
@@ -632,7 +1151,7 @@ export class FilterAPI {
|
|
|
632
1151
|
return;
|
|
633
1152
|
}
|
|
634
1153
|
// Send flush frame (null)
|
|
635
|
-
const ret = this.buffersrcCtx.buffersrcAddFrameSync(null);
|
|
1154
|
+
const ret = this.buffersrcCtx.buffersrcAddFrameSync(null, AV_BUFFERSRC_FLAG_PUSH);
|
|
636
1155
|
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
637
1156
|
FFmpegError.throwIfError(ret, 'Failed to flush filter');
|
|
638
1157
|
}
|
|
@@ -656,8 +1175,9 @@ export class FilterAPI {
|
|
|
656
1175
|
* }
|
|
657
1176
|
* ```
|
|
658
1177
|
*
|
|
1178
|
+
* @see {@link process} For frame processing
|
|
659
1179
|
* @see {@link flush} For manual flush
|
|
660
|
-
* @see {@link
|
|
1180
|
+
* @see {@link flushFramesSync} For sync version
|
|
661
1181
|
*/
|
|
662
1182
|
async *flushFrames() {
|
|
663
1183
|
// Send flush signal
|
|
@@ -688,6 +1208,8 @@ export class FilterAPI {
|
|
|
688
1208
|
* }
|
|
689
1209
|
* ```
|
|
690
1210
|
*
|
|
1211
|
+
* @see {@link processSync} For frame processing
|
|
1212
|
+
* @see {@link flushSync} For manual flush
|
|
691
1213
|
* @see {@link flushFrames} For async version
|
|
692
1214
|
*/
|
|
693
1215
|
*flushFramesSync() {
|
|
@@ -720,19 +1242,26 @@ export class FilterAPI {
|
|
|
720
1242
|
* frame.free();
|
|
721
1243
|
* }
|
|
722
1244
|
* ```
|
|
1245
|
+
*
|
|
1246
|
+
* @see {@link process} For frame processing
|
|
1247
|
+
* @see {@link flush} For flushing filter
|
|
1248
|
+
* @see {@link receiveSync} For synchronous version
|
|
723
1249
|
*/
|
|
724
1250
|
async receive() {
|
|
725
1251
|
if (this.isClosed || !this.initialized || !this.buffersinkCtx) {
|
|
726
1252
|
return null;
|
|
727
1253
|
}
|
|
728
|
-
|
|
729
|
-
frame
|
|
730
|
-
|
|
1254
|
+
// Reuse frame - but alloc() instead of unref() for buffersink
|
|
1255
|
+
// buffersink needs a fresh allocated frame, not an unreferenced one
|
|
1256
|
+
this.frame.alloc();
|
|
1257
|
+
const ret = await this.buffersinkCtx.buffersinkGetFrame(this.frame);
|
|
731
1258
|
if (ret >= 0) {
|
|
732
|
-
|
|
1259
|
+
// Post-process output frame (set timeBase from buffersink, calculate duration)
|
|
1260
|
+
this.postProcessOutputFrame(this.frame);
|
|
1261
|
+
// Clone for user (keeps internal frame for reuse)
|
|
1262
|
+
return this.frame.clone();
|
|
733
1263
|
}
|
|
734
1264
|
else {
|
|
735
|
-
frame.free();
|
|
736
1265
|
if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
|
|
737
1266
|
return null;
|
|
738
1267
|
}
|
|
@@ -763,20 +1292,25 @@ export class FilterAPI {
|
|
|
763
1292
|
* }
|
|
764
1293
|
* ```
|
|
765
1294
|
*
|
|
1295
|
+
* @see {@link processSync} For frame processing
|
|
1296
|
+
* @see {@link flushSync} For flushing filter
|
|
766
1297
|
* @see {@link receive} For async version
|
|
767
1298
|
*/
|
|
768
1299
|
receiveSync() {
|
|
769
1300
|
if (this.isClosed || !this.initialized || !this.buffersinkCtx) {
|
|
770
1301
|
return null;
|
|
771
1302
|
}
|
|
772
|
-
|
|
773
|
-
frame
|
|
774
|
-
|
|
1303
|
+
// Reuse frame - but alloc() instead of unref() for buffersink
|
|
1304
|
+
// buffersink needs a fresh allocated frame, not an unreferenced one
|
|
1305
|
+
this.frame.alloc();
|
|
1306
|
+
const ret = this.buffersinkCtx.buffersinkGetFrameSync(this.frame);
|
|
775
1307
|
if (ret >= 0) {
|
|
776
|
-
|
|
1308
|
+
// Post-process output frame (set timeBase from buffersink, calculate duration)
|
|
1309
|
+
this.postProcessOutputFrame(this.frame);
|
|
1310
|
+
// Clone for user (keeps internal frame for reuse)
|
|
1311
|
+
return this.frame.clone();
|
|
777
1312
|
}
|
|
778
1313
|
else {
|
|
779
|
-
frame.free();
|
|
780
1314
|
if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
|
|
781
1315
|
return null;
|
|
782
1316
|
}
|
|
@@ -869,6 +1403,24 @@ export class FilterAPI {
|
|
|
869
1403
|
const ret = this.graph.queueCommand(target, cmd, arg, ts, flags);
|
|
870
1404
|
FFmpegError.throwIfError(ret, 'Failed to queue filter command');
|
|
871
1405
|
}
|
|
1406
|
+
pipeTo(target) {
|
|
1407
|
+
const t = target;
|
|
1408
|
+
// Store reference to next component for flush propagation
|
|
1409
|
+
this.nextComponent = t;
|
|
1410
|
+
// Start worker if not already running
|
|
1411
|
+
this.workerPromise ??= this.runWorker();
|
|
1412
|
+
// Start pipe task: filter.outputQueue -> target.inputQueue (via target.send)
|
|
1413
|
+
this.pipeToPromise = (async () => {
|
|
1414
|
+
while (true) {
|
|
1415
|
+
const frame = await this.receiveFrame();
|
|
1416
|
+
if (!frame)
|
|
1417
|
+
break;
|
|
1418
|
+
await t.sendToQueue(frame);
|
|
1419
|
+
}
|
|
1420
|
+
})();
|
|
1421
|
+
// Return scheduler for chaining (target is now the last component)
|
|
1422
|
+
return new Scheduler(this, t);
|
|
1423
|
+
}
|
|
872
1424
|
/**
|
|
873
1425
|
* Free filter resources.
|
|
874
1426
|
*
|
|
@@ -887,10 +1439,137 @@ export class FilterAPI {
|
|
|
887
1439
|
return;
|
|
888
1440
|
}
|
|
889
1441
|
this.isClosed = true;
|
|
1442
|
+
// Close queues
|
|
1443
|
+
this.inputQueue.close();
|
|
1444
|
+
this.outputQueue.close();
|
|
1445
|
+
this.frame.free();
|
|
890
1446
|
this.graph.free();
|
|
891
1447
|
this.buffersrcCtx = null;
|
|
892
1448
|
this.buffersinkCtx = null;
|
|
893
1449
|
this.initialized = false;
|
|
1450
|
+
this.initializePromise = null;
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Worker loop for push-based processing.
|
|
1454
|
+
*
|
|
1455
|
+
* @internal
|
|
1456
|
+
*/
|
|
1457
|
+
async runWorker() {
|
|
1458
|
+
try {
|
|
1459
|
+
// Outer loop - receive frames
|
|
1460
|
+
while (!this.inputQueue.isClosed) {
|
|
1461
|
+
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
1462
|
+
try {
|
|
1463
|
+
const frame = __addDisposableResource(env_3, await this.inputQueue.receive(), false);
|
|
1464
|
+
if (!frame)
|
|
1465
|
+
break;
|
|
1466
|
+
// Open filter if not already done
|
|
1467
|
+
if (!this.initialized) {
|
|
1468
|
+
this.initializePromise ??= this.initialize(frame);
|
|
1469
|
+
}
|
|
1470
|
+
await this.initializePromise;
|
|
1471
|
+
if (!this.initialized) {
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
1475
|
+
throw new Error('Could not initialize filter contexts');
|
|
1476
|
+
}
|
|
1477
|
+
// Rescale timestamps to filter's timeBase
|
|
1478
|
+
if (this.calculatedTimeBase) {
|
|
1479
|
+
const originalTimeBase = frame.timeBase;
|
|
1480
|
+
frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
|
|
1481
|
+
frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
|
|
1482
|
+
frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
|
|
1483
|
+
}
|
|
1484
|
+
// Send frame to filter
|
|
1485
|
+
const addRet = await this.buffersrcCtx.buffersrcAddFrame(frame, AV_BUFFERSRC_FLAG_PUSH);
|
|
1486
|
+
FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
|
|
1487
|
+
// Receive all available frames
|
|
1488
|
+
while (!this.outputQueue.isClosed) {
|
|
1489
|
+
const buffered = await this.receive();
|
|
1490
|
+
if (!buffered)
|
|
1491
|
+
break;
|
|
1492
|
+
await this.outputQueue.send(buffered);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
catch (e_3) {
|
|
1496
|
+
env_3.error = e_3;
|
|
1497
|
+
env_3.hasError = true;
|
|
1498
|
+
}
|
|
1499
|
+
finally {
|
|
1500
|
+
__disposeResources(env_3);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
// Flush filter at end
|
|
1504
|
+
await this.flush();
|
|
1505
|
+
while (!this.outputQueue.isClosed) {
|
|
1506
|
+
const frame = await this.receive();
|
|
1507
|
+
if (!frame)
|
|
1508
|
+
break;
|
|
1509
|
+
await this.outputQueue.send(frame);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
catch {
|
|
1513
|
+
// Ignore error ?
|
|
1514
|
+
}
|
|
1515
|
+
finally {
|
|
1516
|
+
// Close output queue when done
|
|
1517
|
+
this.outputQueue?.close();
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Send frame to input queue.
|
|
1522
|
+
*
|
|
1523
|
+
* @param frame - Frame to send
|
|
1524
|
+
*
|
|
1525
|
+
* @internal
|
|
1526
|
+
*/
|
|
1527
|
+
async sendToQueue(frame) {
|
|
1528
|
+
await this.inputQueue.send(frame);
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Receive frame from output queue.
|
|
1532
|
+
*
|
|
1533
|
+
* @returns Frame from output queue or null if closed
|
|
1534
|
+
*
|
|
1535
|
+
* @internal
|
|
1536
|
+
*/
|
|
1537
|
+
async receiveFrame() {
|
|
1538
|
+
return await this.outputQueue.receive();
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Flush the entire filter pipeline.
|
|
1542
|
+
*
|
|
1543
|
+
* Propagates flush through worker, output queue, and next component.
|
|
1544
|
+
*
|
|
1545
|
+
* @internal
|
|
1546
|
+
*/
|
|
1547
|
+
async flushPipeline() {
|
|
1548
|
+
// Close input queue to signal end of stream to worker
|
|
1549
|
+
this.inputQueue.close();
|
|
1550
|
+
// Wait for worker to finish processing all frames (if exists)
|
|
1551
|
+
if (this.workerPromise) {
|
|
1552
|
+
await this.workerPromise;
|
|
1553
|
+
}
|
|
1554
|
+
// Flush filter at end (like FFmpeg does)
|
|
1555
|
+
await this.flush();
|
|
1556
|
+
// Send all flushed frames to output queue
|
|
1557
|
+
while (true) {
|
|
1558
|
+
const frame = await this.receive();
|
|
1559
|
+
if (!frame)
|
|
1560
|
+
break;
|
|
1561
|
+
await this.outputQueue.send(frame);
|
|
1562
|
+
}
|
|
1563
|
+
// Close output queue to signal end of stream to pipeTo() task
|
|
1564
|
+
this.outputQueue.close();
|
|
1565
|
+
// Wait for pipeTo() task to finish processing all frames (if exists)
|
|
1566
|
+
if (this.pipeToPromise) {
|
|
1567
|
+
await this.pipeToPromise;
|
|
1568
|
+
}
|
|
1569
|
+
// Then propagate flush to next component
|
|
1570
|
+
if (this.nextComponent) {
|
|
1571
|
+
await this.nextComponent.flushPipeline();
|
|
1572
|
+
}
|
|
894
1573
|
}
|
|
895
1574
|
/**
|
|
896
1575
|
* Initialize filter graph from first frame.
|
|
@@ -908,6 +1587,23 @@ export class FilterAPI {
|
|
|
908
1587
|
* @internal
|
|
909
1588
|
*/
|
|
910
1589
|
async initialize(frame) {
|
|
1590
|
+
// Calculate timeBase from first frame
|
|
1591
|
+
this.calculatedTimeBase = this.calculateTimeBase(frame);
|
|
1592
|
+
// Track initial frame properties for change detection
|
|
1593
|
+
this.lastFrameProps = {
|
|
1594
|
+
format: frame.format,
|
|
1595
|
+
width: frame.width,
|
|
1596
|
+
height: frame.height,
|
|
1597
|
+
sampleRate: frame.sampleRate,
|
|
1598
|
+
channels: frame.channelLayout?.nbChannels ?? 0,
|
|
1599
|
+
};
|
|
1600
|
+
// Set graph options before parsing
|
|
1601
|
+
if (this.options.scaleSwsOpts) {
|
|
1602
|
+
this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
|
|
1603
|
+
}
|
|
1604
|
+
if (this.options.audioResampleOpts) {
|
|
1605
|
+
this.graph.aresampleSwrOpts = this.options.audioResampleOpts;
|
|
1606
|
+
}
|
|
911
1607
|
// Create buffer source and sink
|
|
912
1608
|
this.createBufferSource(frame);
|
|
913
1609
|
this.createBufferSink(frame);
|
|
@@ -937,6 +1633,23 @@ export class FilterAPI {
|
|
|
937
1633
|
* @see {@link initialize} For async version
|
|
938
1634
|
*/
|
|
939
1635
|
initializeSync(frame) {
|
|
1636
|
+
// Calculate timeBase from first frame
|
|
1637
|
+
this.calculatedTimeBase = this.calculateTimeBase(frame);
|
|
1638
|
+
// Track initial frame properties for change detection
|
|
1639
|
+
this.lastFrameProps = {
|
|
1640
|
+
format: frame.format,
|
|
1641
|
+
width: frame.width,
|
|
1642
|
+
height: frame.height,
|
|
1643
|
+
sampleRate: frame.sampleRate,
|
|
1644
|
+
channels: frame.channelLayout?.nbChannels ?? 0,
|
|
1645
|
+
};
|
|
1646
|
+
// Set graph options before parsing
|
|
1647
|
+
if (this.options.scaleSwsOpts) {
|
|
1648
|
+
this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
|
|
1649
|
+
}
|
|
1650
|
+
if (this.options.audioResampleOpts) {
|
|
1651
|
+
this.graph.aresampleSwrOpts = this.options.audioResampleOpts;
|
|
1652
|
+
}
|
|
940
1653
|
// Create buffer source and sink
|
|
941
1654
|
this.createBufferSource(frame);
|
|
942
1655
|
this.createBufferSink(frame);
|
|
@@ -947,6 +1660,116 @@ export class FilterAPI {
|
|
|
947
1660
|
FFmpegError.throwIfError(ret, 'Failed to configure filter graph');
|
|
948
1661
|
this.initialized = true;
|
|
949
1662
|
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Check if frame properties changed and handle according to dropOnChange/allowReinit options.
|
|
1665
|
+
*
|
|
1666
|
+
* Implements FFmpeg's IFILTER_FLAG_DROPCHANGED and IFILTER_FLAG_REINIT logic
|
|
1667
|
+
*
|
|
1668
|
+
* @param frame - Frame to check
|
|
1669
|
+
*
|
|
1670
|
+
* @returns true if frame should be processed, false if frame should be dropped
|
|
1671
|
+
*
|
|
1672
|
+
* @throws {Error} If format changed and allowReinit is false
|
|
1673
|
+
*
|
|
1674
|
+
* @internal
|
|
1675
|
+
*/
|
|
1676
|
+
checkFramePropertiesChanged(frame) {
|
|
1677
|
+
if (!this.lastFrameProps) {
|
|
1678
|
+
return true; // No previous frame, allow
|
|
1679
|
+
}
|
|
1680
|
+
// Check for property changes
|
|
1681
|
+
const changed = frame.format !== this.lastFrameProps.format ||
|
|
1682
|
+
frame.width !== this.lastFrameProps.width ||
|
|
1683
|
+
frame.height !== this.lastFrameProps.height ||
|
|
1684
|
+
frame.sampleRate !== this.lastFrameProps.sampleRate ||
|
|
1685
|
+
(frame.channelLayout?.nbChannels ?? 0) !== this.lastFrameProps.channels;
|
|
1686
|
+
if (!changed) {
|
|
1687
|
+
return true; // No changes, process frame
|
|
1688
|
+
}
|
|
1689
|
+
// Properties changed - check dropOnChange flag
|
|
1690
|
+
if (this.options.dropOnChange) {
|
|
1691
|
+
return false; // Drop frame
|
|
1692
|
+
}
|
|
1693
|
+
// Check allowReinit flag
|
|
1694
|
+
// Default is true (allow reinit), only block if explicitly set to false
|
|
1695
|
+
const allowReinit = this.options.allowReinit !== false;
|
|
1696
|
+
if (!allowReinit && this.initialized) {
|
|
1697
|
+
throw new Error('Frame properties changed but allowReinit is false. ' +
|
|
1698
|
+
`Format: ${this.lastFrameProps.format}->${frame.format}, ` +
|
|
1699
|
+
`Size: ${this.lastFrameProps.width}x${this.lastFrameProps.height}->${frame.width}x${frame.height}`);
|
|
1700
|
+
}
|
|
1701
|
+
// Reinit is allowed - reinitialize filtergraph
|
|
1702
|
+
// Close current graph and reinitialize
|
|
1703
|
+
this.graph.free();
|
|
1704
|
+
this.graph = new FilterGraph();
|
|
1705
|
+
this.buffersrcCtx = null;
|
|
1706
|
+
this.buffersinkCtx = null;
|
|
1707
|
+
this.initialized = false;
|
|
1708
|
+
this.initializePromise = null;
|
|
1709
|
+
this.calculatedTimeBase = null;
|
|
1710
|
+
return true; // Will be reinitialized on next process
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Calculate timeBase from frame based on media type and CFR option.
|
|
1714
|
+
*
|
|
1715
|
+
* Implements FFmpeg's ifilter_parameters_from_frame logic:
|
|
1716
|
+
* - Audio: Always { 1, sample_rate }
|
|
1717
|
+
* - Video CFR: 1/framerate (inverse of framerate)
|
|
1718
|
+
* - Video VFR: Use frame.timeBase
|
|
1719
|
+
*
|
|
1720
|
+
* @param frame - Input frame
|
|
1721
|
+
*
|
|
1722
|
+
* @returns Calculated timeBase
|
|
1723
|
+
*
|
|
1724
|
+
* @internal
|
|
1725
|
+
*/
|
|
1726
|
+
calculateTimeBase(frame) {
|
|
1727
|
+
if (frame.isAudio()) {
|
|
1728
|
+
// Audio: Always { 1, sample_rate }
|
|
1729
|
+
return { num: 1, den: frame.sampleRate };
|
|
1730
|
+
}
|
|
1731
|
+
else {
|
|
1732
|
+
// Video: Check CFR flag
|
|
1733
|
+
if (this.options.cfr) {
|
|
1734
|
+
// CFR mode: timeBase = 1/framerate = inverse(framerate)
|
|
1735
|
+
// Note: framerate is guaranteed to be set (validated in create())
|
|
1736
|
+
return avInvQ(this.options.framerate);
|
|
1737
|
+
}
|
|
1738
|
+
else {
|
|
1739
|
+
// VFR mode: Use frame.timeBase
|
|
1740
|
+
return frame.timeBase;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Post-process output frame from buffersink.
|
|
1746
|
+
*
|
|
1747
|
+
* Applies FFmpeg's fg_output_step() behavior:
|
|
1748
|
+
* 1. Sets frame.timeBase from buffersink (filters can change timeBase, e.g., aresample)
|
|
1749
|
+
* 2. Calculates video frame duration from frame rate if not set
|
|
1750
|
+
*
|
|
1751
|
+
* This must be called AFTER buffersinkGetFrame() for every output frame.
|
|
1752
|
+
*
|
|
1753
|
+
* @param frame - Output frame from buffersink
|
|
1754
|
+
*
|
|
1755
|
+
* @throws {Error} If buffersink context not available
|
|
1756
|
+
*
|
|
1757
|
+
* @internal
|
|
1758
|
+
*/
|
|
1759
|
+
postProcessOutputFrame(frame) {
|
|
1760
|
+
if (!this.buffersinkCtx) {
|
|
1761
|
+
throw new Error('Buffersink context not available');
|
|
1762
|
+
}
|
|
1763
|
+
// Filters can change timeBase (e.g., aresample sets output to {1, out_sample_rate})
|
|
1764
|
+
// Without this, frame has INPUT timeBase instead of filter's OUTPUT timeBase
|
|
1765
|
+
frame.timeBase = this.buffersinkCtx.buffersinkGetTimeBase();
|
|
1766
|
+
if (frame.isVideo() && !frame.duration) {
|
|
1767
|
+
const frameRate = this.buffersinkCtx.buffersinkGetFrameRate();
|
|
1768
|
+
if (frameRate.num > 0 && frameRate.den > 0) {
|
|
1769
|
+
frame.duration = avRescaleQ(1, avInvQ(frameRate), frame.timeBase);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
950
1773
|
/**
|
|
951
1774
|
* Create buffer source with frame parameters.
|
|
952
1775
|
*
|
|
@@ -967,6 +1790,10 @@ export class FilterAPI {
|
|
|
967
1790
|
if (!bufferFilter) {
|
|
968
1791
|
throw new Error(`${filterName} filter not found`);
|
|
969
1792
|
}
|
|
1793
|
+
// Ensure timeBase was calculated
|
|
1794
|
+
if (!this.calculatedTimeBase) {
|
|
1795
|
+
throw new Error('TimeBase not calculated - this should not happen');
|
|
1796
|
+
}
|
|
970
1797
|
// For audio, create with args. For video, use allocFilter + buffersrcParametersSet
|
|
971
1798
|
if (frame.isVideo()) {
|
|
972
1799
|
// Allocate filter without args
|
|
@@ -978,8 +1805,8 @@ export class FilterAPI {
|
|
|
978
1805
|
width: frame.width,
|
|
979
1806
|
height: frame.height,
|
|
980
1807
|
format: frame.format,
|
|
981
|
-
timeBase: this.
|
|
982
|
-
frameRate: this.options.
|
|
1808
|
+
timeBase: this.calculatedTimeBase,
|
|
1809
|
+
frameRate: this.options.framerate,
|
|
983
1810
|
sampleAspectRatio: frame.sampleAspectRatio,
|
|
984
1811
|
colorRange: frame.colorRange,
|
|
985
1812
|
colorSpace: frame.colorSpace,
|
|
@@ -995,7 +1822,7 @@ export class FilterAPI {
|
|
|
995
1822
|
const formatName = avGetSampleFmtName(frame.format);
|
|
996
1823
|
const channelLayout = frame.channelLayout.mask === 0n ? 'stereo' : frame.channelLayout.mask.toString();
|
|
997
1824
|
// eslint-disable-next-line @stylistic/max-len
|
|
998
|
-
const args = `time_base=${this.
|
|
1825
|
+
const args = `time_base=${this.calculatedTimeBase.num}/${this.calculatedTimeBase.den}:sample_rate=${frame.sampleRate}:sample_fmt=${formatName}:channel_layout=${channelLayout}`;
|
|
999
1826
|
this.buffersrcCtx = this.graph.createFilter(bufferFilter, 'in', args);
|
|
1000
1827
|
if (!this.buffersrcCtx) {
|
|
1001
1828
|
throw new Error('Failed to create audio buffer source');
|
|
@@ -1061,8 +1888,8 @@ export class FilterAPI {
|
|
|
1061
1888
|
if (filters) {
|
|
1062
1889
|
for (const filterCtx of filters) {
|
|
1063
1890
|
const filter = filterCtx.filter;
|
|
1064
|
-
if (filter
|
|
1065
|
-
filterCtx.hwDeviceCtx =
|
|
1891
|
+
if (filter?.hasFlags(AVFILTER_FLAG_HWDEVICE)) {
|
|
1892
|
+
filterCtx.hwDeviceCtx = this.options.hardware?.deviceContext ?? frame.hwFramesCtx?.deviceRef ?? null;
|
|
1066
1893
|
// Set extra_hw_frames if specified
|
|
1067
1894
|
if (this.options.extraHWFrames !== undefined && this.options.extraHWFrames > 0) {
|
|
1068
1895
|
filterCtx.extraHWFrames = this.options.extraHWFrames;
|