node-av 3.1.3 → 5.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 +88 -52
- package/binding.gyp +23 -11
- 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 +320 -78
- package/dist/api/bitstream-filter.js +684 -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 +454 -77
- package/dist/api/decoder.js +1081 -271
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/{media-input.d.ts → demuxer.d.ts} +295 -45
- package/dist/api/demuxer.js +1965 -0
- package/dist/api/demuxer.js.map +1 -0
- package/dist/api/encoder.d.ts +423 -132
- package/dist/api/encoder.js +1089 -240
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/filter-complex.d.ts +769 -0
- package/dist/api/filter-complex.js +1596 -0
- package/dist/api/filter-complex.js.map +1 -0
- package/dist/api/filter-presets.d.ts +80 -5
- package/dist/api/filter-presets.js +117 -7
- package/dist/api/filter-presets.js.map +1 -1
- package/dist/api/filter.d.ts +561 -125
- package/dist/api/filter.js +1083 -274
- 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 +8 -4
- package/dist/api/index.js +17 -8
- package/dist/api/index.js.map +1 -1
- package/dist/api/io-stream.d.ts +6 -6
- 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} +280 -66
- 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 +449 -439
- 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 +533 -56
- 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 +11 -1
- package/dist/api/utilities/audio-sample.js +10 -0
- package/dist/api/utilities/audio-sample.js.map +1 -1
- package/dist/api/utilities/channel-layout.d.ts +1 -0
- package/dist/api/utilities/channel-layout.js +1 -0
- package/dist/api/utilities/channel-layout.js.map +1 -1
- package/dist/api/utilities/image.d.ts +39 -1
- package/dist/api/utilities/image.js +38 -0
- package/dist/api/utilities/image.js.map +1 -1
- package/dist/api/utilities/index.d.ts +3 -0
- package/dist/api/utilities/index.js +6 -0
- package/dist/api/utilities/index.js.map +1 -1
- package/dist/api/utilities/media-type.d.ts +2 -1
- package/dist/api/utilities/media-type.js +1 -0
- package/dist/api/utilities/media-type.js.map +1 -1
- package/dist/api/utilities/pixel-format.d.ts +4 -1
- package/dist/api/utilities/pixel-format.js +3 -0
- package/dist/api/utilities/pixel-format.js.map +1 -1
- package/dist/api/utilities/sample-format.d.ts +6 -1
- package/dist/api/utilities/sample-format.js +5 -0
- package/dist/api/utilities/sample-format.js.map +1 -1
- package/dist/api/utilities/scheduler.d.ts +138 -0
- package/dist/api/utilities/scheduler.js +98 -0
- package/dist/api/utilities/scheduler.js.map +1 -0
- package/dist/api/utilities/streaming.d.ts +105 -15
- package/dist/api/utilities/streaming.js +201 -12
- package/dist/api/utilities/streaming.js.map +1 -1
- package/dist/api/utilities/timestamp.d.ts +15 -1
- package/dist/api/utilities/timestamp.js +14 -0
- package/dist/api/utilities/timestamp.js.map +1 -1
- package/dist/api/utilities/whisper-model.d.ts +310 -0
- package/dist/api/utilities/whisper-model.js +528 -0
- package/dist/api/utilities/whisper-model.js.map +1 -0
- 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/api/whisper.d.ts +324 -0
- package/dist/api/whisper.js +362 -0
- package/dist/api/whisper.js.map +1 -0
- package/dist/constants/constants.d.ts +54 -2
- package/dist/constants/constants.js +48 -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/ffmpeg/index.d.ts +3 -3
- package/dist/ffmpeg/index.js +3 -3
- package/dist/ffmpeg/utils.d.ts +27 -0
- package/dist/ffmpeg/utils.js +28 -16
- package/dist/ffmpeg/utils.js.map +1 -1
- package/dist/lib/binding.d.ts +22 -11
- 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 +229 -1
- package/dist/lib/codec-parameters.js +264 -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/fifo.d.ts +416 -0
- package/dist/lib/fifo.js +453 -0
- package/dist/lib/fifo.js.map +1 -0
- package/dist/lib/filter-context.d.ts +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 +264 -1
- package/dist/lib/frame.js +351 -1
- 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 +2 -0
- package/dist/lib/index.js +4 -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 +76 -27
- 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/rational.d.ts +18 -0
- package/dist/lib/rational.js +19 -0
- package/dist/lib/rational.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 +49 -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/install/check.js +2 -2
- package/package.json +37 -26
- 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, EOF } 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,53 @@ 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
|
-
* Creates and allocates filter graph immediately.
|
|
67
|
-
* Filter configuration is completed on first frame with frame properties.
|
|
68
|
-
* Hardware frames context is automatically detected from input frames.
|
|
69
|
-
*
|
|
70
141
|
* Direct mapping to avfilter_graph_parse_ptr() and avfilter_graph_config().
|
|
71
142
|
*
|
|
72
143
|
* @param description - Filter graph description
|
|
73
144
|
*
|
|
74
|
-
* @param options - Filter options
|
|
145
|
+
* @param options - Filter options
|
|
75
146
|
*
|
|
76
147
|
* @returns Configured filter instance
|
|
77
148
|
*
|
|
149
|
+
* @throws {Error} If cfr=true but framerate is not set
|
|
150
|
+
*
|
|
78
151
|
* @example
|
|
79
152
|
* ```typescript
|
|
80
|
-
* // Simple video filter
|
|
81
|
-
* const filter = FilterAPI.create('scale=640:480'
|
|
82
|
-
* timeBase: video.timeBase
|
|
83
|
-
* });
|
|
153
|
+
* // Simple video filter (VFR mode, auto timeBase)
|
|
154
|
+
* const filter = FilterAPI.create('scale=640:480');
|
|
84
155
|
* ```
|
|
85
156
|
*
|
|
86
157
|
* @example
|
|
87
158
|
* ```typescript
|
|
88
|
-
* //
|
|
89
|
-
* const filter = FilterAPI.create('
|
|
90
|
-
*
|
|
159
|
+
* // CFR mode with constant framerate
|
|
160
|
+
* const filter = FilterAPI.create('scale=1920:1080', {
|
|
161
|
+
* cfr: true,
|
|
162
|
+
* framerate: { num: 25, den: 1 }
|
|
91
163
|
* });
|
|
92
164
|
* ```
|
|
93
165
|
*
|
|
94
166
|
* @example
|
|
95
167
|
* ```typescript
|
|
96
|
-
* // Audio filter
|
|
97
|
-
* const filter = FilterAPI.create('
|
|
98
|
-
*
|
|
168
|
+
* // Audio filter with resampling
|
|
169
|
+
* const filter = FilterAPI.create('aformat=sample_fmts=s16:sample_rates=44100', {
|
|
170
|
+
* audioResampleOpts: 'async=1'
|
|
99
171
|
* });
|
|
100
172
|
* ```
|
|
101
173
|
*
|
|
102
174
|
* @see {@link process} For frame processing
|
|
103
175
|
* @see {@link FilterOptions} For configuration options
|
|
104
176
|
*/
|
|
105
|
-
static create(description, options) {
|
|
177
|
+
static create(description, options = {}) {
|
|
178
|
+
// Validate options: CFR requires framerate
|
|
179
|
+
if (options.cfr && !options.framerate) {
|
|
180
|
+
throw new Error('cfr=true requires framerate to be set');
|
|
181
|
+
}
|
|
106
182
|
// Create graph
|
|
107
183
|
const graph = new FilterGraph();
|
|
108
184
|
graph.alloc();
|
|
@@ -147,6 +223,260 @@ export class FilterAPI {
|
|
|
147
223
|
get isFilterInitialized() {
|
|
148
224
|
return this.initialized;
|
|
149
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Get buffersink filter context.
|
|
228
|
+
*
|
|
229
|
+
* Provides access to the buffersink filter context for advanced operations.
|
|
230
|
+
* Returns null if filter is not initialized.
|
|
231
|
+
*
|
|
232
|
+
* @returns Buffersink context or null
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```typescript
|
|
236
|
+
* const sink = filter.buffersink;
|
|
237
|
+
* if (sink) {
|
|
238
|
+
* const fr = sink.buffersinkGetFrameRate();
|
|
239
|
+
* console.log(`Output frame rate: ${fr.num}/${fr.den}`);
|
|
240
|
+
* }
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
get buffersink() {
|
|
244
|
+
return this.buffersinkCtx;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Output frame rate from filter graph.
|
|
248
|
+
*
|
|
249
|
+
* Returns the frame rate determined by the filter graph output.
|
|
250
|
+
* Returns null if filter is not initialized or frame rate is not set.
|
|
251
|
+
*
|
|
252
|
+
* Direct mapping to av_buffersink_get_frame_rate().
|
|
253
|
+
*
|
|
254
|
+
* @returns Frame rate or null if not available
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```typescript
|
|
258
|
+
* const frameRate = filter.frameRate;
|
|
259
|
+
* if (frameRate) {
|
|
260
|
+
* console.log(`Filter output: ${frameRate.num}/${frameRate.den} fps`);
|
|
261
|
+
* }
|
|
262
|
+
* ```
|
|
263
|
+
*
|
|
264
|
+
* @see {@link timeBase} For output timebase
|
|
265
|
+
*/
|
|
266
|
+
get frameRate() {
|
|
267
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const fr = this.buffersinkCtx.buffersinkGetFrameRate();
|
|
271
|
+
// Return null if frame rate is not set (0/0 or 0/1)
|
|
272
|
+
if (fr.num <= 0 || fr.den <= 0) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
return fr;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Output time base from filter graph.
|
|
279
|
+
*
|
|
280
|
+
* Returns the time base of the buffersink output.
|
|
281
|
+
* Matches FFmpeg CLI's av_buffersink_get_time_base() behavior.
|
|
282
|
+
*
|
|
283
|
+
* Direct mapping to av_buffersink_get_time_base().
|
|
284
|
+
*
|
|
285
|
+
* @returns Time base or null if not initialized
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```typescript
|
|
289
|
+
* const timeBase = filter.timeBase;
|
|
290
|
+
* if (timeBase) {
|
|
291
|
+
* console.log(`Filter timebase: ${timeBase.num}/${timeBase.den}`);
|
|
292
|
+
* }
|
|
293
|
+
* ```
|
|
294
|
+
*
|
|
295
|
+
* @see {@link frameRate} For output frame rate
|
|
296
|
+
*/
|
|
297
|
+
get timeBase() {
|
|
298
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
return this.buffersinkCtx.buffersinkGetTimeBase();
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Output format from filter graph.
|
|
305
|
+
*
|
|
306
|
+
* Returns the pixel format (video) or sample format (audio) of the buffersink output.
|
|
307
|
+
* Matches FFmpeg CLI's av_buffersink_get_format() behavior.
|
|
308
|
+
*
|
|
309
|
+
* Direct mapping to av_buffersink_get_format().
|
|
310
|
+
*
|
|
311
|
+
* @returns Pixel format or sample format, or null if not initialized
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* ```typescript
|
|
315
|
+
* const format = filter.format;
|
|
316
|
+
* if (format !== null) {
|
|
317
|
+
* console.log(`Filter output format: ${format}`);
|
|
318
|
+
* }
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
get format() {
|
|
322
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
return this.buffersinkCtx.buffersinkGetFormat();
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Output dimensions from filter graph (video only).
|
|
329
|
+
*
|
|
330
|
+
* Returns the width and height of the buffersink output.
|
|
331
|
+
* Matches FFmpeg CLI's av_buffersink_get_w() and av_buffersink_get_h() behavior.
|
|
332
|
+
* Only meaningful for video filters.
|
|
333
|
+
*
|
|
334
|
+
* Direct mapping to av_buffersink_get_w() and av_buffersink_get_h().
|
|
335
|
+
*
|
|
336
|
+
* @returns Dimensions object or null if not initialized
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* ```typescript
|
|
340
|
+
* const dims = filter.dimensions;
|
|
341
|
+
* if (dims) {
|
|
342
|
+
* console.log(`Filter output: ${dims.width}x${dims.height}`);
|
|
343
|
+
* }
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
get dimensions() {
|
|
347
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
width: this.buffersinkCtx.buffersinkGetWidth(),
|
|
352
|
+
height: this.buffersinkCtx.buffersinkGetHeight(),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Output sample rate from filter graph (audio only).
|
|
357
|
+
*
|
|
358
|
+
* Returns the sample rate of the buffersink output.
|
|
359
|
+
* Matches FFmpeg CLI's av_buffersink_get_sample_rate() behavior.
|
|
360
|
+
* Only meaningful for audio filters.
|
|
361
|
+
*
|
|
362
|
+
* Direct mapping to av_buffersink_get_sample_rate().
|
|
363
|
+
*
|
|
364
|
+
* @returns Sample rate or null if not initialized
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* ```typescript
|
|
368
|
+
* const sampleRate = filter.sampleRate;
|
|
369
|
+
* if (sampleRate) {
|
|
370
|
+
* console.log(`Filter output sample rate: ${sampleRate} Hz`);
|
|
371
|
+
* }
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
374
|
+
get sampleRate() {
|
|
375
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
return this.buffersinkCtx.buffersinkGetSampleRate();
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Output channel layout from filter graph (audio only).
|
|
382
|
+
*
|
|
383
|
+
* Returns the channel layout of the buffersink output.
|
|
384
|
+
* Matches FFmpeg CLI's av_buffersink_get_ch_layout() behavior.
|
|
385
|
+
* Only meaningful for audio filters.
|
|
386
|
+
*
|
|
387
|
+
* Direct mapping to av_buffersink_get_ch_layout().
|
|
388
|
+
*
|
|
389
|
+
* @returns Channel layout or null if not initialized
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* ```typescript
|
|
393
|
+
* const layout = filter.channelLayout;
|
|
394
|
+
* if (layout) {
|
|
395
|
+
* console.log(`Filter output channels: ${layout.nbChannels}`);
|
|
396
|
+
* }
|
|
397
|
+
* ```
|
|
398
|
+
*/
|
|
399
|
+
get channelLayout() {
|
|
400
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
return this.buffersinkCtx.buffersinkGetChannelLayout();
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Output color space from filter graph (video only).
|
|
407
|
+
*
|
|
408
|
+
* Returns the color space of the buffersink output.
|
|
409
|
+
* Matches FFmpeg CLI's av_buffersink_get_colorspace() behavior.
|
|
410
|
+
* Only meaningful for video filters.
|
|
411
|
+
*
|
|
412
|
+
* Direct mapping to av_buffersink_get_colorspace().
|
|
413
|
+
*
|
|
414
|
+
* @returns Color space or null if not initialized
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* ```typescript
|
|
418
|
+
* const colorSpace = filter.colorSpace;
|
|
419
|
+
* if (colorSpace !== null) {
|
|
420
|
+
* console.log(`Filter output color space: ${colorSpace}`);
|
|
421
|
+
* }
|
|
422
|
+
* ```
|
|
423
|
+
*/
|
|
424
|
+
get colorSpace() {
|
|
425
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
return this.buffersinkCtx.buffersinkGetColorspace();
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Output color range from filter graph (video only).
|
|
432
|
+
*
|
|
433
|
+
* Returns the color range of the buffersink output.
|
|
434
|
+
* Matches FFmpeg CLI's av_buffersink_get_color_range() behavior.
|
|
435
|
+
* Only meaningful for video filters.
|
|
436
|
+
*
|
|
437
|
+
* Direct mapping to av_buffersink_get_color_range().
|
|
438
|
+
*
|
|
439
|
+
* @returns Color range or null if not initialized
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* ```typescript
|
|
443
|
+
* const colorRange = filter.colorRange;
|
|
444
|
+
* if (colorRange !== null) {
|
|
445
|
+
* console.log(`Filter output color range: ${colorRange}`);
|
|
446
|
+
* }
|
|
447
|
+
* ```
|
|
448
|
+
*/
|
|
449
|
+
get colorRange() {
|
|
450
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
return this.buffersinkCtx.buffersinkGetColorRange();
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Output sample aspect ratio from filter graph (video only).
|
|
457
|
+
*
|
|
458
|
+
* Returns the sample aspect ratio of the buffersink output.
|
|
459
|
+
* Matches FFmpeg CLI's av_buffersink_get_sample_aspect_ratio() behavior.
|
|
460
|
+
* Only meaningful for video filters.
|
|
461
|
+
*
|
|
462
|
+
* Direct mapping to av_buffersink_get_sample_aspect_ratio().
|
|
463
|
+
*
|
|
464
|
+
* @returns Sample aspect ratio or null if not initialized
|
|
465
|
+
*
|
|
466
|
+
* @example
|
|
467
|
+
* ```typescript
|
|
468
|
+
* const sar = filter.sampleAspectRatio;
|
|
469
|
+
* if (sar) {
|
|
470
|
+
* console.log(`Filter output SAR: ${sar.num}:${sar.den}`);
|
|
471
|
+
* }
|
|
472
|
+
* ```
|
|
473
|
+
*/
|
|
474
|
+
get sampleAspectRatio() {
|
|
475
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
return this.buffersinkCtx.buffersinkGetSampleAspectRatio();
|
|
479
|
+
}
|
|
150
480
|
/**
|
|
151
481
|
* Check if filter is ready for processing.
|
|
152
482
|
*
|
|
@@ -182,28 +512,33 @@ export class FilterAPI {
|
|
|
182
512
|
return !this.isClosed && this.initialized ? this.graph.dump() : null;
|
|
183
513
|
}
|
|
184
514
|
/**
|
|
185
|
-
*
|
|
515
|
+
* Send a frame to the filter.
|
|
186
516
|
*
|
|
187
|
-
*
|
|
517
|
+
* Sends a frame to the filter for processing.
|
|
518
|
+
* Does not return filtered frames - use {@link receive} to retrieve frames.
|
|
188
519
|
* On first frame, automatically builds filter graph with frame properties.
|
|
189
|
-
*
|
|
190
|
-
* Hardware frames context is automatically detected from frame.
|
|
191
|
-
* Returns null if filter is closed and frame is null.
|
|
520
|
+
* A single input frame can produce zero, one, or multiple output frames.
|
|
192
521
|
*
|
|
193
|
-
*
|
|
522
|
+
* **Important**: This method only SENDS the frame to the filter.
|
|
523
|
+
* You must call {@link receive} separately (potentially multiple times) to get filtered frames.
|
|
194
524
|
*
|
|
195
|
-
*
|
|
525
|
+
* Direct mapping to av_buffersrc_add_frame().
|
|
196
526
|
*
|
|
197
|
-
* @
|
|
527
|
+
* @param frame - Input frame to send to filter
|
|
198
528
|
*
|
|
199
529
|
* @throws {Error} If filter could not be initialized
|
|
200
530
|
*
|
|
201
|
-
* @throws {FFmpegError} If
|
|
531
|
+
* @throws {FFmpegError} If sending frame fails
|
|
202
532
|
*
|
|
203
533
|
* @example
|
|
204
534
|
* ```typescript
|
|
205
|
-
*
|
|
206
|
-
*
|
|
535
|
+
* // Send frame and receive filtered frames
|
|
536
|
+
* await filter.process(inputFrame);
|
|
537
|
+
*
|
|
538
|
+
* // Receive all available filtered frames
|
|
539
|
+
* while (true) {
|
|
540
|
+
* const output = await filter.receive();
|
|
541
|
+
* if (!output) break;
|
|
207
542
|
* console.log(`Got filtered frame: pts=${output.pts}`);
|
|
208
543
|
* output.free();
|
|
209
544
|
* }
|
|
@@ -211,228 +546,267 @@ export class FilterAPI {
|
|
|
211
546
|
*
|
|
212
547
|
* @example
|
|
213
548
|
* ```typescript
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
549
|
+
* for await (const frame of decoder.frames(input.packets())) {
|
|
550
|
+
* // Send frame
|
|
551
|
+
* await filter.process(frame);
|
|
552
|
+
*
|
|
553
|
+
* // Receive available filtered frames
|
|
554
|
+
* let output;
|
|
555
|
+
* while ((output = await filter.receive())) {
|
|
556
|
+
* await encoder.encode(output);
|
|
557
|
+
* output.free();
|
|
558
|
+
* }
|
|
559
|
+
* frame.free();
|
|
219
560
|
* }
|
|
220
|
-
* // For buffered frames, use the frames() async generator
|
|
221
561
|
* ```
|
|
222
562
|
*
|
|
563
|
+
* @see {@link receive} For receiving filtered frames
|
|
564
|
+
* @see {@link processAll} For combined send+receive operation
|
|
223
565
|
* @see {@link frames} For processing frame streams
|
|
224
566
|
* @see {@link flush} For end-of-stream handling
|
|
567
|
+
* @see {@link processSync} For synchronous version
|
|
225
568
|
*/
|
|
226
569
|
async process(frame) {
|
|
227
570
|
if (this.isClosed) {
|
|
228
|
-
return
|
|
571
|
+
return;
|
|
229
572
|
}
|
|
230
573
|
// Open filter if not already done
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
await this.initialize(frame);
|
|
574
|
+
this.initializePromise ??= this.initialize(frame);
|
|
575
|
+
await this.initializePromise;
|
|
576
|
+
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
577
|
+
throw new Error('Could not initialize filter contexts');
|
|
236
578
|
}
|
|
579
|
+
// Check for frame property changes (FFmpeg: dropOnChange/allowReinit logic)
|
|
580
|
+
if (!this.checkFramePropertiesChanged(frame)) {
|
|
581
|
+
// Frame dropped due to property change
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
// If reinitialized, reinitialize now
|
|
237
585
|
if (!this.initialized) {
|
|
238
|
-
|
|
586
|
+
this.initializePromise = this.initialize(frame);
|
|
587
|
+
await this.initializePromise;
|
|
239
588
|
}
|
|
240
589
|
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
241
|
-
throw new Error('Could not
|
|
242
|
-
}
|
|
243
|
-
// Send frame to filter
|
|
244
|
-
const addRet = await this.buffersrcCtx.buffersrcAddFrame(frame);
|
|
245
|
-
FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
|
|
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();
|
|
256
|
-
return null;
|
|
590
|
+
throw new Error('Could not reinitialize filter contexts');
|
|
257
591
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
592
|
+
// Rescale timestamps to filter's timeBase
|
|
593
|
+
if (this.calculatedTimeBase) {
|
|
594
|
+
const originalTimeBase = frame.timeBase;
|
|
595
|
+
frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
|
|
596
|
+
frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
|
|
597
|
+
frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
|
|
262
598
|
}
|
|
599
|
+
// Send frame to filter with PUSH flag for immediate processing
|
|
600
|
+
const addRet = await this.buffersrcCtx.buffersrcAddFrame(frame, AV_BUFFERSRC_FLAG_PUSH);
|
|
601
|
+
FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
|
|
263
602
|
}
|
|
264
603
|
/**
|
|
265
|
-
*
|
|
604
|
+
* Send a frame to the filter synchronously.
|
|
266
605
|
* Synchronous version of process.
|
|
267
606
|
*
|
|
268
|
-
*
|
|
607
|
+
* Sends a frame to the filter for processing.
|
|
608
|
+
* Does not return filtered frames - use {@link receiveSync} to retrieve frames.
|
|
269
609
|
* On first frame, automatically builds filter graph with frame properties.
|
|
270
|
-
*
|
|
271
|
-
* Hardware frames context is automatically detected from frame.
|
|
272
|
-
* Returns null if filter is closed and frame is null.
|
|
610
|
+
* A single input frame can produce zero, one, or multiple output frames.
|
|
273
611
|
*
|
|
274
|
-
*
|
|
612
|
+
* **Important**: This method only SENDS the frame to the filter.
|
|
613
|
+
* You must call {@link receiveSync} separately (potentially multiple times) to get filtered frames.
|
|
275
614
|
*
|
|
276
|
-
*
|
|
615
|
+
* Direct mapping to av_buffersrc_add_frame().
|
|
277
616
|
*
|
|
278
|
-
* @
|
|
617
|
+
* @param frame - Input frame to send to filter
|
|
279
618
|
*
|
|
280
619
|
* @throws {Error} If filter could not be initialized
|
|
281
620
|
*
|
|
282
|
-
* @throws {FFmpegError} If
|
|
621
|
+
* @throws {FFmpegError} If sending frame fails
|
|
283
622
|
*
|
|
284
623
|
* @example
|
|
285
624
|
* ```typescript
|
|
286
|
-
*
|
|
287
|
-
*
|
|
625
|
+
* // Send frame and receive filtered frames
|
|
626
|
+
* filter.processSync(inputFrame);
|
|
627
|
+
*
|
|
628
|
+
* // Receive all available filtered frames
|
|
629
|
+
* let output;
|
|
630
|
+
* while ((output = filter.receiveSync())) {
|
|
288
631
|
* console.log(`Got filtered frame: pts=${output.pts}`);
|
|
289
632
|
* output.free();
|
|
290
633
|
* }
|
|
291
634
|
* ```
|
|
292
635
|
*
|
|
293
|
-
* @
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
* if (output) {
|
|
298
|
-
* // Got output immediately
|
|
299
|
-
* yield output;
|
|
300
|
-
* }
|
|
301
|
-
* // For buffered frames, use the framesSync() generator
|
|
302
|
-
* ```
|
|
303
|
-
*
|
|
636
|
+
* @see {@link receiveSync} For receiving filtered frames
|
|
637
|
+
* @see {@link processAllSync} For combined send+receive operation
|
|
638
|
+
* @see {@link framesSync} For processing frame streams
|
|
639
|
+
* @see {@link flushSync} For end-of-stream handling
|
|
304
640
|
* @see {@link process} For async version
|
|
305
641
|
*/
|
|
306
642
|
processSync(frame) {
|
|
307
643
|
if (this.isClosed) {
|
|
308
|
-
return
|
|
644
|
+
return;
|
|
309
645
|
}
|
|
310
646
|
// Open filter if not already done
|
|
311
647
|
if (!this.initialized) {
|
|
312
|
-
if (!frame) {
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
648
|
this.initializeSync(frame);
|
|
316
649
|
}
|
|
317
650
|
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
318
651
|
throw new Error('Could not initialize filter contexts');
|
|
319
652
|
}
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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();
|
|
333
|
-
return null;
|
|
653
|
+
// Check for frame property changes (FFmpeg: dropOnChange/allowReinit logic)
|
|
654
|
+
if (!this.checkFramePropertiesChanged(frame)) {
|
|
655
|
+
// Frame dropped due to property change
|
|
656
|
+
return;
|
|
334
657
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
658
|
+
// If reinitialized, reinitialize now
|
|
659
|
+
if (!this.initialized) {
|
|
660
|
+
this.initializeSync(frame);
|
|
661
|
+
}
|
|
662
|
+
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
663
|
+
throw new Error('Could not reinitialize filter contexts');
|
|
339
664
|
}
|
|
665
|
+
// Rescale timestamps to filter's timeBase
|
|
666
|
+
if (this.calculatedTimeBase) {
|
|
667
|
+
const originalTimeBase = frame.timeBase;
|
|
668
|
+
frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
|
|
669
|
+
frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
|
|
670
|
+
frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
|
|
671
|
+
}
|
|
672
|
+
// Send frame to filter with PUSH flag for immediate processing
|
|
673
|
+
const addRet = this.buffersrcCtx.buffersrcAddFrameSync(frame, AV_BUFFERSRC_FLAG_PUSH);
|
|
674
|
+
FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
|
|
340
675
|
}
|
|
341
676
|
/**
|
|
342
|
-
* Process
|
|
677
|
+
* Process a frame through the filter.
|
|
678
|
+
*
|
|
679
|
+
* Applies filter operations to input frame and receives all available output frames.
|
|
680
|
+
* Returns array of frames - may be empty if filter needs more input.
|
|
681
|
+
* On first frame, automatically builds filter graph with frame properties.
|
|
682
|
+
* One input frame can produce zero, one, or multiple output frames depending on filter.
|
|
683
|
+
* Hardware frames context is automatically detected from frame.
|
|
343
684
|
*
|
|
344
|
-
*
|
|
345
|
-
* Useful for filters that buffer multiple frames.
|
|
685
|
+
* Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
|
|
346
686
|
*
|
|
347
|
-
* @param
|
|
687
|
+
* @param frame - Input frame to process
|
|
348
688
|
*
|
|
349
|
-
* @returns Array of
|
|
689
|
+
* @returns Array of filtered frames (empty if buffered or filter closed)
|
|
350
690
|
*
|
|
351
|
-
* @throws {Error} If filter not
|
|
691
|
+
* @throws {Error} If filter could not be initialized
|
|
352
692
|
*
|
|
353
693
|
* @throws {FFmpegError} If processing fails
|
|
354
694
|
*
|
|
355
695
|
* @example
|
|
356
696
|
* ```typescript
|
|
357
|
-
* const
|
|
358
|
-
* for (const output of
|
|
359
|
-
* console.log(`
|
|
697
|
+
* const frames = await filter.processAll(inputFrame);
|
|
698
|
+
* for (const output of frames) {
|
|
699
|
+
* console.log(`Got filtered frame: pts=${output.pts}`);
|
|
360
700
|
* output.free();
|
|
361
701
|
* }
|
|
362
702
|
* ```
|
|
363
703
|
*
|
|
704
|
+
* @example
|
|
705
|
+
* ```typescript
|
|
706
|
+
* // Process frame - may return multiple frames (e.g. fps filter)
|
|
707
|
+
* const frames = await filter.processAll(frame);
|
|
708
|
+
* for (const output of frames) {
|
|
709
|
+
* yield output;
|
|
710
|
+
* }
|
|
711
|
+
* ```
|
|
712
|
+
*
|
|
364
713
|
* @see {@link process} For single frame processing
|
|
714
|
+
* @see {@link frames} For processing frame streams
|
|
715
|
+
* @see {@link flush} For end-of-stream handling
|
|
716
|
+
* @see {@link processAllSync} For synchronous version
|
|
365
717
|
*/
|
|
366
|
-
async
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
718
|
+
async processAll(frame) {
|
|
719
|
+
if (frame) {
|
|
720
|
+
await this.process(frame);
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
await this.flush();
|
|
724
|
+
}
|
|
725
|
+
// Receive all available frames
|
|
726
|
+
const frames = [];
|
|
727
|
+
while (true) {
|
|
728
|
+
const outputFrame = await this.receive();
|
|
729
|
+
if (!outputFrame)
|
|
730
|
+
break; // Stop on EAGAIN or EOF
|
|
731
|
+
frames.push(outputFrame); // Only push actual frames
|
|
380
732
|
}
|
|
381
|
-
return
|
|
733
|
+
return frames;
|
|
382
734
|
}
|
|
383
735
|
/**
|
|
384
|
-
* Process
|
|
385
|
-
* Synchronous version of
|
|
736
|
+
* Process a frame through the filter synchronously.
|
|
737
|
+
* Synchronous version of processAll.
|
|
386
738
|
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
739
|
+
* Applies filter operations to input frame and receives all available output frames.
|
|
740
|
+
* Returns array of frames - may be empty if filter needs more input.
|
|
741
|
+
* On first frame, automatically builds filter graph with frame properties.
|
|
742
|
+
* One input frame can produce zero, one, or multiple output frames depending on filter.
|
|
743
|
+
* Hardware frames context is automatically detected from frame.
|
|
389
744
|
*
|
|
390
|
-
*
|
|
745
|
+
* Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
|
|
391
746
|
*
|
|
392
|
-
* @
|
|
747
|
+
* @param frame - Input frame to process
|
|
393
748
|
*
|
|
394
|
-
* @
|
|
749
|
+
* @returns Array of filtered frames (empty if buffered or filter closed)
|
|
750
|
+
*
|
|
751
|
+
* @throws {Error} If filter could not be initialized
|
|
395
752
|
*
|
|
396
753
|
* @throws {FFmpegError} If processing fails
|
|
397
754
|
*
|
|
398
755
|
* @example
|
|
399
756
|
* ```typescript
|
|
400
|
-
* const outputs = filter.
|
|
757
|
+
* const outputs = filter.processAllSync(inputFrame);
|
|
401
758
|
* for (const output of outputs) {
|
|
402
|
-
* console.log(`
|
|
759
|
+
* console.log(`Got filtered frame: pts=${output.pts}`);
|
|
403
760
|
* output.free();
|
|
404
761
|
* }
|
|
405
762
|
* ```
|
|
406
763
|
*
|
|
407
|
-
* @
|
|
764
|
+
* @example
|
|
765
|
+
* ```typescript
|
|
766
|
+
* // Process frame - may return multiple frames (e.g. fps filter)
|
|
767
|
+
* const outputs = filter.processAllSync(frame);
|
|
768
|
+
* for (const output of outputs) {
|
|
769
|
+
* yield output;
|
|
770
|
+
* }
|
|
771
|
+
* ```
|
|
772
|
+
*
|
|
773
|
+
* @see {@link processSync} For single frame processing
|
|
774
|
+
* @see {@link framesSync} For processing frame streams
|
|
775
|
+
* @see {@link flushSync} For end-of-stream handling
|
|
776
|
+
* @see {@link process} For async version
|
|
408
777
|
*/
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const output = this.processSync(frame);
|
|
413
|
-
if (output) {
|
|
414
|
-
outputFrames.push(output);
|
|
415
|
-
}
|
|
416
|
-
// Drain any additional frames
|
|
417
|
-
while (!this.isClosed) {
|
|
418
|
-
const additional = this.receiveSync();
|
|
419
|
-
if (!additional)
|
|
420
|
-
break;
|
|
421
|
-
outputFrames.push(additional);
|
|
422
|
-
}
|
|
778
|
+
processAllSync(frame) {
|
|
779
|
+
if (frame) {
|
|
780
|
+
this.processSync(frame);
|
|
423
781
|
}
|
|
424
|
-
|
|
782
|
+
else {
|
|
783
|
+
this.flushSync();
|
|
784
|
+
}
|
|
785
|
+
// Receive all available frames
|
|
786
|
+
const frames = [];
|
|
787
|
+
while (true) {
|
|
788
|
+
const outputFrame = this.receiveSync();
|
|
789
|
+
if (!outputFrame)
|
|
790
|
+
break; // Stop on EAGAIN or EOF
|
|
791
|
+
frames.push(outputFrame); // Only push actual frames
|
|
792
|
+
}
|
|
793
|
+
return frames;
|
|
425
794
|
}
|
|
426
795
|
/**
|
|
427
796
|
* Process frame stream through filter.
|
|
428
797
|
*
|
|
429
798
|
* High-level async generator for filtering frame streams.
|
|
430
|
-
*
|
|
431
|
-
*
|
|
799
|
+
* Filter is only flushed when EOF (null) signal is explicitly received.
|
|
800
|
+
* Primary interface for stream-based filtering.
|
|
801
|
+
*
|
|
802
|
+
* **EOF Handling:**
|
|
803
|
+
* - Send null to flush filter and get remaining buffered frames
|
|
804
|
+
* - Generator yields null after flushing when null is received
|
|
805
|
+
* - No automatic flushing - filter stays open until EOF or close()
|
|
432
806
|
*
|
|
433
|
-
* @param frames - Async
|
|
807
|
+
* @param frames - Async iterable of frames, single frame, or null to flush
|
|
434
808
|
*
|
|
435
|
-
* @yields {Frame} Filtered frames
|
|
809
|
+
* @yields {Frame | null} Filtered frames, followed by null when explicitly flushed
|
|
436
810
|
*
|
|
437
811
|
* @throws {Error} If filter not ready
|
|
438
812
|
*
|
|
@@ -440,8 +814,12 @@ export class FilterAPI {
|
|
|
440
814
|
*
|
|
441
815
|
* @example
|
|
442
816
|
* ```typescript
|
|
443
|
-
* //
|
|
817
|
+
* // Stream of frames with automatic EOF propagation
|
|
444
818
|
* for await (const frame of filter.frames(decoder.frames(packets))) {
|
|
819
|
+
* if (frame === null) {
|
|
820
|
+
* console.log('Filter flushed');
|
|
821
|
+
* break;
|
|
822
|
+
* }
|
|
445
823
|
* await encoder.encode(frame);
|
|
446
824
|
* frame.free();
|
|
447
825
|
* }
|
|
@@ -449,64 +827,91 @@ export class FilterAPI {
|
|
|
449
827
|
*
|
|
450
828
|
* @example
|
|
451
829
|
* ```typescript
|
|
452
|
-
* //
|
|
453
|
-
* const
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
830
|
+
* // Single frame - no automatic flush
|
|
831
|
+
* for await (const frame of filter.frames(singleFrame)) {
|
|
832
|
+
* await encoder.encode(frame);
|
|
833
|
+
* frame.free();
|
|
834
|
+
* }
|
|
835
|
+
* // Filter remains open, buffered frames not flushed
|
|
836
|
+
* ```
|
|
459
837
|
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
838
|
+
* @example
|
|
839
|
+
* ```typescript
|
|
840
|
+
* // Explicit flush with EOF
|
|
841
|
+
* for await (const frame of filter.frames(null)) {
|
|
842
|
+
* if (frame === null) {
|
|
843
|
+
* console.log('All buffered frames flushed');
|
|
844
|
+
* break;
|
|
845
|
+
* }
|
|
846
|
+
* console.log('Buffered frame:', frame.pts);
|
|
462
847
|
* frame.free();
|
|
463
848
|
* }
|
|
464
849
|
* ```
|
|
465
850
|
*
|
|
466
851
|
* @see {@link process} For single frame processing
|
|
467
|
-
* @see {@link
|
|
852
|
+
* @see {@link Decoder.frames} For frames source
|
|
853
|
+
* @see {@link framesSync} For sync version
|
|
468
854
|
*/
|
|
469
855
|
async *frames(frames) {
|
|
470
|
-
|
|
856
|
+
const self = this;
|
|
857
|
+
const processFrame = async function* (frame) {
|
|
858
|
+
await self.process(frame);
|
|
859
|
+
while (true) {
|
|
860
|
+
const filtered = await self.receive();
|
|
861
|
+
if (!filtered)
|
|
862
|
+
break;
|
|
863
|
+
yield filtered;
|
|
864
|
+
}
|
|
865
|
+
}.bind(this);
|
|
866
|
+
const finalize = async function* () {
|
|
867
|
+
for await (const remaining of self.flushFrames()) {
|
|
868
|
+
yield remaining;
|
|
869
|
+
}
|
|
870
|
+
yield null;
|
|
871
|
+
}.bind(this);
|
|
872
|
+
if (frames === null) {
|
|
873
|
+
yield* finalize();
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (frames instanceof Frame) {
|
|
877
|
+
yield* processFrame(frames);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
for await (const frame_1 of frames) {
|
|
881
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
471
882
|
try {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
// Drain any buffered frames
|
|
478
|
-
while (!this.isClosed) {
|
|
479
|
-
const buffered = await this.receive();
|
|
480
|
-
if (!buffered)
|
|
481
|
-
break;
|
|
482
|
-
yield buffered;
|
|
883
|
+
const frame = __addDisposableResource(env_1, frame_1, false);
|
|
884
|
+
if (frame === null) {
|
|
885
|
+
yield* finalize();
|
|
886
|
+
return;
|
|
483
887
|
}
|
|
888
|
+
yield* processFrame(frame);
|
|
889
|
+
}
|
|
890
|
+
catch (e_1) {
|
|
891
|
+
env_1.error = e_1;
|
|
892
|
+
env_1.hasError = true;
|
|
484
893
|
}
|
|
485
894
|
finally {
|
|
486
|
-
|
|
487
|
-
frame.free();
|
|
895
|
+
__disposeResources(env_1);
|
|
488
896
|
}
|
|
489
897
|
}
|
|
490
|
-
// Flush and get remaining frames
|
|
491
|
-
await this.flush();
|
|
492
|
-
while (!this.isClosed) {
|
|
493
|
-
const remaining = await this.receive();
|
|
494
|
-
if (!remaining)
|
|
495
|
-
break;
|
|
496
|
-
yield remaining;
|
|
497
|
-
}
|
|
498
898
|
}
|
|
499
899
|
/**
|
|
500
900
|
* Process frame stream through filter synchronously.
|
|
501
901
|
* Synchronous version of frames.
|
|
502
902
|
*
|
|
503
903
|
* High-level sync generator for filtering frame streams.
|
|
504
|
-
*
|
|
505
|
-
*
|
|
904
|
+
* Filter is only flushed when EOF (null) signal is explicitly received.
|
|
905
|
+
* Primary interface for stream-based filtering.
|
|
906
|
+
*
|
|
907
|
+
* **EOF Handling:**
|
|
908
|
+
* - Send null to flush filter and get remaining buffered frames
|
|
909
|
+
* - Generator yields null after flushing when null is received
|
|
910
|
+
* - No automatic flushing - filter stays open until EOF or close()
|
|
506
911
|
*
|
|
507
|
-
* @param frames -
|
|
912
|
+
* @param frames - Iterable of frames, single frame, or null to flush
|
|
508
913
|
*
|
|
509
|
-
* @yields {Frame} Filtered frames
|
|
914
|
+
* @yields {Frame | null} Filtered frames, followed by null when explicitly flushed
|
|
510
915
|
*
|
|
511
916
|
* @throws {Error} If filter not ready
|
|
512
917
|
*
|
|
@@ -514,8 +919,12 @@ export class FilterAPI {
|
|
|
514
919
|
*
|
|
515
920
|
* @example
|
|
516
921
|
* ```typescript
|
|
517
|
-
* //
|
|
922
|
+
* // Stream of frames with automatic EOF propagation
|
|
518
923
|
* for (const frame of filter.framesSync(decoder.framesSync(packets))) {
|
|
924
|
+
* if (frame === null) {
|
|
925
|
+
* console.log('Filter flushed');
|
|
926
|
+
* break;
|
|
927
|
+
* }
|
|
519
928
|
* encoder.encodeSync(frame);
|
|
520
929
|
* frame.free();
|
|
521
930
|
* }
|
|
@@ -523,51 +932,83 @@ export class FilterAPI {
|
|
|
523
932
|
*
|
|
524
933
|
* @example
|
|
525
934
|
* ```typescript
|
|
526
|
-
* //
|
|
527
|
-
* const
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
935
|
+
* // Single frame - no automatic flush
|
|
936
|
+
* for (const frame of filter.framesSync(singleFrame)) {
|
|
937
|
+
* encoder.encodeSync(frame);
|
|
938
|
+
* frame.free();
|
|
939
|
+
* }
|
|
940
|
+
* // Filter remains open, buffered frames not flushed
|
|
941
|
+
* ```
|
|
533
942
|
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
943
|
+
* @example
|
|
944
|
+
* ```typescript
|
|
945
|
+
* // Explicit flush with EOF
|
|
946
|
+
* for (const frame of filter.framesSync(null)) {
|
|
947
|
+
* if (frame === null) {
|
|
948
|
+
* console.log('All buffered frames flushed');
|
|
949
|
+
* break;
|
|
950
|
+
* }
|
|
951
|
+
* console.log('Buffered frame:', frame.pts);
|
|
536
952
|
* frame.free();
|
|
537
953
|
* }
|
|
538
954
|
* ```
|
|
539
955
|
*
|
|
956
|
+
* @see {@link processSync} For single frame processing
|
|
957
|
+
* @see {@link Decoder.framesSync} For frames source
|
|
540
958
|
* @see {@link frames} For async version
|
|
541
959
|
*/
|
|
542
960
|
*framesSync(frames) {
|
|
543
|
-
|
|
961
|
+
const self = this;
|
|
962
|
+
// Helper: Process frame and yield all available filtered frames (filters out EAGAIN nulls)
|
|
963
|
+
const processFrame = function* (frame) {
|
|
964
|
+
self.processSync(frame);
|
|
965
|
+
// Receive ALL filtered frames (filter out null/EAGAIN)
|
|
966
|
+
while (true) {
|
|
967
|
+
const filtered = self.receiveSync();
|
|
968
|
+
if (!filtered)
|
|
969
|
+
break; // EAGAIN or EOF - no more frames available
|
|
970
|
+
yield filtered; // Only yield actual frames, not null
|
|
971
|
+
}
|
|
972
|
+
}.bind(this);
|
|
973
|
+
// Helper: Flush filter and signal EOF
|
|
974
|
+
const finalize = function* () {
|
|
975
|
+
for (const remaining of self.flushFramesSync()) {
|
|
976
|
+
yield remaining; // Only yield actual frames
|
|
977
|
+
}
|
|
978
|
+
yield null; // Signal end-of-stream
|
|
979
|
+
}.bind(this);
|
|
980
|
+
// Case 1: EOF input -> flush only
|
|
981
|
+
if (frames === null) {
|
|
982
|
+
yield* finalize();
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
// Case 2: Single frame
|
|
986
|
+
if (frames instanceof Frame) {
|
|
987
|
+
yield* processFrame(frames);
|
|
988
|
+
// No automatic flush - only flush on explicit EOF
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
// Case 3: Iterable of frames
|
|
992
|
+
for (const frame_2 of frames) {
|
|
993
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
544
994
|
try {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (
|
|
548
|
-
yield
|
|
549
|
-
|
|
550
|
-
// Drain any buffered frames
|
|
551
|
-
while (!this.isClosed) {
|
|
552
|
-
const buffered = this.receiveSync();
|
|
553
|
-
if (!buffered)
|
|
554
|
-
break;
|
|
555
|
-
yield buffered;
|
|
995
|
+
const frame = __addDisposableResource(env_2, frame_2, false);
|
|
996
|
+
// Check for EOF signal from upstream
|
|
997
|
+
if (frame === null) {
|
|
998
|
+
yield* finalize();
|
|
999
|
+
return;
|
|
556
1000
|
}
|
|
1001
|
+
yield* processFrame(frame);
|
|
1002
|
+
}
|
|
1003
|
+
catch (e_2) {
|
|
1004
|
+
env_2.error = e_2;
|
|
1005
|
+
env_2.hasError = true;
|
|
557
1006
|
}
|
|
558
1007
|
finally {
|
|
559
|
-
|
|
560
|
-
frame.free();
|
|
1008
|
+
__disposeResources(env_2);
|
|
561
1009
|
}
|
|
562
1010
|
}
|
|
563
|
-
//
|
|
564
|
-
this.flushSync();
|
|
565
|
-
while (!this.isClosed) {
|
|
566
|
-
const remaining = this.receiveSync();
|
|
567
|
-
if (!remaining)
|
|
568
|
-
break;
|
|
569
|
-
yield remaining;
|
|
570
|
-
}
|
|
1011
|
+
// No automatic flush - only flush on explicit EOF
|
|
571
1012
|
}
|
|
572
1013
|
/**
|
|
573
1014
|
* Flush filter and signal end-of-stream.
|
|
@@ -591,14 +1032,15 @@ export class FilterAPI {
|
|
|
591
1032
|
* ```
|
|
592
1033
|
*
|
|
593
1034
|
* @see {@link flushFrames} For async iteration
|
|
594
|
-
* @see {@link
|
|
1035
|
+
* @see {@link receive} For getting flushed frames
|
|
1036
|
+
* @see {@link flushSync} For synchronous version
|
|
595
1037
|
*/
|
|
596
1038
|
async flush() {
|
|
597
1039
|
if (this.isClosed || !this.initialized || !this.buffersrcCtx) {
|
|
598
1040
|
return;
|
|
599
1041
|
}
|
|
600
1042
|
// Send flush frame (null)
|
|
601
|
-
const ret = await this.buffersrcCtx.buffersrcAddFrame(null);
|
|
1043
|
+
const ret = await this.buffersrcCtx.buffersrcAddFrame(null, AV_BUFFERSRC_FLAG_PUSH);
|
|
602
1044
|
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
603
1045
|
FFmpegError.throwIfError(ret, 'Failed to flush filter');
|
|
604
1046
|
}
|
|
@@ -625,6 +1067,8 @@ export class FilterAPI {
|
|
|
625
1067
|
* }
|
|
626
1068
|
* ```
|
|
627
1069
|
*
|
|
1070
|
+
* @see {@link flushFramesSync} For sync iteration
|
|
1071
|
+
* @see {@link receiveSync} For getting flushed frames
|
|
628
1072
|
* @see {@link flush} For async version
|
|
629
1073
|
*/
|
|
630
1074
|
flushSync() {
|
|
@@ -632,7 +1076,7 @@ export class FilterAPI {
|
|
|
632
1076
|
return;
|
|
633
1077
|
}
|
|
634
1078
|
// Send flush frame (null)
|
|
635
|
-
const ret = this.buffersrcCtx.buffersrcAddFrameSync(null);
|
|
1079
|
+
const ret = this.buffersrcCtx.buffersrcAddFrameSync(null, AV_BUFFERSRC_FLAG_PUSH);
|
|
636
1080
|
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
637
1081
|
FFmpegError.throwIfError(ret, 'Failed to flush filter');
|
|
638
1082
|
}
|
|
@@ -656,16 +1100,19 @@ export class FilterAPI {
|
|
|
656
1100
|
* }
|
|
657
1101
|
* ```
|
|
658
1102
|
*
|
|
1103
|
+
* @see {@link process} For frame processing
|
|
659
1104
|
* @see {@link flush} For manual flush
|
|
660
|
-
* @see {@link
|
|
1105
|
+
* @see {@link flushFramesSync} For sync version
|
|
661
1106
|
*/
|
|
662
1107
|
async *flushFrames() {
|
|
663
1108
|
// Send flush signal
|
|
664
1109
|
await this.flush();
|
|
665
|
-
// Yield all remaining frames
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
1110
|
+
// Yield all remaining frames (filter out null/EAGAIN and EOF)
|
|
1111
|
+
while (true) {
|
|
1112
|
+
const frame = await this.receive();
|
|
1113
|
+
if (!frame)
|
|
1114
|
+
break; // Stop on EAGAIN or EOF
|
|
1115
|
+
yield frame; // Only yield actual frames
|
|
669
1116
|
}
|
|
670
1117
|
}
|
|
671
1118
|
/**
|
|
@@ -688,54 +1135,95 @@ export class FilterAPI {
|
|
|
688
1135
|
* }
|
|
689
1136
|
* ```
|
|
690
1137
|
*
|
|
1138
|
+
* @see {@link processSync} For frame processing
|
|
1139
|
+
* @see {@link flushSync} For manual flush
|
|
691
1140
|
* @see {@link flushFrames} For async version
|
|
692
1141
|
*/
|
|
693
1142
|
*flushFramesSync() {
|
|
694
1143
|
// Send flush signal
|
|
695
1144
|
this.flushSync();
|
|
696
|
-
// Yield all remaining frames
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
1145
|
+
// Yield all remaining frames (filter out null/EAGAIN and EOF)
|
|
1146
|
+
while (true) {
|
|
1147
|
+
const frame = this.receiveSync();
|
|
1148
|
+
if (!frame)
|
|
1149
|
+
break; // Stop on EAGAIN or EOF
|
|
1150
|
+
yield frame; // Only yield actual frames
|
|
700
1151
|
}
|
|
701
1152
|
}
|
|
702
1153
|
/**
|
|
703
1154
|
* Receive buffered frame from filter.
|
|
704
1155
|
*
|
|
705
1156
|
* Drains frames buffered by the filter.
|
|
706
|
-
* Call repeatedly until null to get all buffered frames.
|
|
707
|
-
*
|
|
1157
|
+
* Call repeatedly until null or EOF to get all buffered frames.
|
|
1158
|
+
* Implements FFmpeg's send/receive pattern.
|
|
1159
|
+
*
|
|
1160
|
+
* **Return Values:**
|
|
1161
|
+
* - `Frame` - Successfully received frame (AVERROR >= 0)
|
|
1162
|
+
* - `null` - Need more input data (AVERROR_EAGAIN), or filter not initialized
|
|
1163
|
+
* - `undefined` - End of stream reached (AVERROR_EOF), or filter is closed
|
|
708
1164
|
*
|
|
709
1165
|
* Direct mapping to av_buffersink_get_frame().
|
|
710
1166
|
*
|
|
711
|
-
* @returns Buffered frame or
|
|
1167
|
+
* @returns Buffered frame, null if need more data, or undefined if stream ended
|
|
712
1168
|
*
|
|
713
1169
|
* @throws {FFmpegError} If receiving fails
|
|
714
1170
|
*
|
|
715
1171
|
* @example
|
|
716
1172
|
* ```typescript
|
|
717
|
-
*
|
|
718
|
-
* while (
|
|
1173
|
+
* // Process all buffered frames
|
|
1174
|
+
* while (true) {
|
|
1175
|
+
* const frame = await filter.receive();
|
|
1176
|
+
* if (!frame) break; // Stop on EAGAIN or EOF
|
|
719
1177
|
* console.log(`Received frame: pts=${frame.pts}`);
|
|
720
1178
|
* frame.free();
|
|
721
1179
|
* }
|
|
722
1180
|
* ```
|
|
1181
|
+
*
|
|
1182
|
+
* @example
|
|
1183
|
+
* ```typescript
|
|
1184
|
+
* // Handle each return value explicitly
|
|
1185
|
+
* const frame = await filter.receive();
|
|
1186
|
+
* if (frame === EOF) {
|
|
1187
|
+
* console.log('Filter stream ended');
|
|
1188
|
+
* } else if (frame === null) {
|
|
1189
|
+
* console.log('Need more input data');
|
|
1190
|
+
* } else {
|
|
1191
|
+
* console.log(`Got frame: pts=${frame.pts}`);
|
|
1192
|
+
* frame.free();
|
|
1193
|
+
* }
|
|
1194
|
+
* ```
|
|
1195
|
+
*
|
|
1196
|
+
* @see {@link process} For frame processing
|
|
1197
|
+
* @see {@link flush} For flushing filter
|
|
1198
|
+
* @see {@link receiveSync} For synchronous version
|
|
1199
|
+
* @see {@link EOF} For end-of-stream signal
|
|
723
1200
|
*/
|
|
724
1201
|
async receive() {
|
|
725
|
-
if (this.isClosed
|
|
1202
|
+
if (this.isClosed) {
|
|
1203
|
+
return EOF;
|
|
1204
|
+
}
|
|
1205
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
726
1206
|
return null;
|
|
727
1207
|
}
|
|
728
|
-
|
|
729
|
-
frame
|
|
730
|
-
|
|
1208
|
+
// Reuse frame - but alloc() instead of unref() for buffersink
|
|
1209
|
+
// buffersink needs a fresh allocated frame, not an unreferenced one
|
|
1210
|
+
this.frame.alloc();
|
|
1211
|
+
const ret = await this.buffersinkCtx.buffersinkGetFrame(this.frame);
|
|
731
1212
|
if (ret >= 0) {
|
|
732
|
-
|
|
1213
|
+
// Post-process output frame (set timeBase from buffersink, calculate duration)
|
|
1214
|
+
this.postProcessOutputFrame(this.frame);
|
|
1215
|
+
// Clone for user (keeps internal frame for reuse)
|
|
1216
|
+
return this.frame.clone();
|
|
1217
|
+
}
|
|
1218
|
+
else if (ret === AVERROR_EAGAIN) {
|
|
1219
|
+
// Need more data
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
else if (ret === AVERROR_EOF) {
|
|
1223
|
+
// End of stream
|
|
1224
|
+
return EOF;
|
|
733
1225
|
}
|
|
734
1226
|
else {
|
|
735
|
-
frame.free();
|
|
736
|
-
if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
|
|
737
|
-
return null;
|
|
738
|
-
}
|
|
739
1227
|
FFmpegError.throwIfError(ret, 'Failed to receive frame from filter');
|
|
740
1228
|
return null;
|
|
741
1229
|
}
|
|
@@ -745,41 +1233,74 @@ export class FilterAPI {
|
|
|
745
1233
|
* Synchronous version of receive.
|
|
746
1234
|
*
|
|
747
1235
|
* Drains frames buffered by the filter.
|
|
748
|
-
* Call repeatedly until null to get all buffered frames.
|
|
749
|
-
*
|
|
1236
|
+
* Call repeatedly until null or EOF to get all buffered frames.
|
|
1237
|
+
* Implements FFmpeg's send/receive pattern.
|
|
1238
|
+
*
|
|
1239
|
+
* **Return Values:**
|
|
1240
|
+
* - `Frame` - Successfully received frame (AVERROR >= 0)
|
|
1241
|
+
* - `null` - Need more input data (AVERROR_EAGAIN), or filter not initialized
|
|
1242
|
+
* - `undefined` - End of stream reached (AVERROR_EOF), or filter is closed
|
|
750
1243
|
*
|
|
751
1244
|
* Direct mapping to av_buffersink_get_frame().
|
|
752
1245
|
*
|
|
753
|
-
* @returns Buffered frame or
|
|
1246
|
+
* @returns Buffered frame, null if need more data, or undefined if stream ended
|
|
754
1247
|
*
|
|
755
1248
|
* @throws {FFmpegError} If receiving fails
|
|
756
1249
|
*
|
|
757
1250
|
* @example
|
|
758
1251
|
* ```typescript
|
|
759
|
-
*
|
|
760
|
-
* while (
|
|
1252
|
+
* // Process all buffered frames
|
|
1253
|
+
* while (true) {
|
|
1254
|
+
* const frame = filter.receiveSync();
|
|
1255
|
+
* if (!frame) break; // Stop on EAGAIN or EOF
|
|
761
1256
|
* console.log(`Received frame: pts=${frame.pts}`);
|
|
762
1257
|
* frame.free();
|
|
763
1258
|
* }
|
|
764
1259
|
* ```
|
|
765
1260
|
*
|
|
1261
|
+
* @example
|
|
1262
|
+
* ```typescript
|
|
1263
|
+
* // Handle each return value explicitly
|
|
1264
|
+
* const frame = filter.receiveSync();
|
|
1265
|
+
* if (frame === EOF) {
|
|
1266
|
+
* console.log('Filter stream ended');
|
|
1267
|
+
* } else if (frame === null) {
|
|
1268
|
+
* console.log('Need more input data');
|
|
1269
|
+
* } else {
|
|
1270
|
+
* console.log(`Got frame: pts=${frame.pts}`);
|
|
1271
|
+
* frame.free();
|
|
1272
|
+
* }
|
|
1273
|
+
* ```
|
|
1274
|
+
*
|
|
1275
|
+
* @see {@link processSync} For frame processing
|
|
1276
|
+
* @see {@link flushSync} For flushing filter
|
|
766
1277
|
* @see {@link receive} For async version
|
|
1278
|
+
* @see {@link EOF} For end-of-stream signal
|
|
767
1279
|
*/
|
|
768
1280
|
receiveSync() {
|
|
769
|
-
if (this.isClosed
|
|
1281
|
+
if (this.isClosed) {
|
|
1282
|
+
return EOF;
|
|
1283
|
+
}
|
|
1284
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
770
1285
|
return null;
|
|
771
1286
|
}
|
|
772
|
-
|
|
773
|
-
frame
|
|
774
|
-
|
|
1287
|
+
// Reuse frame - but alloc() instead of unref() for buffersink
|
|
1288
|
+
// buffersink needs a fresh allocated frame, not an unreferenced one
|
|
1289
|
+
this.frame.alloc();
|
|
1290
|
+
const ret = this.buffersinkCtx.buffersinkGetFrameSync(this.frame);
|
|
775
1291
|
if (ret >= 0) {
|
|
776
|
-
|
|
1292
|
+
// Post-process output frame (set timeBase from buffersink, calculate duration)
|
|
1293
|
+
this.postProcessOutputFrame(this.frame);
|
|
1294
|
+
// Clone for user (keeps internal frame for reuse)
|
|
1295
|
+
return this.frame.clone();
|
|
1296
|
+
}
|
|
1297
|
+
else if (ret === AVERROR_EAGAIN) {
|
|
1298
|
+
return null; // Need more data
|
|
1299
|
+
}
|
|
1300
|
+
else if (ret === AVERROR_EOF) {
|
|
1301
|
+
return EOF; // End of stream
|
|
777
1302
|
}
|
|
778
1303
|
else {
|
|
779
|
-
frame.free();
|
|
780
|
-
if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
|
|
781
|
-
return null;
|
|
782
|
-
}
|
|
783
1304
|
FFmpegError.throwIfError(ret, 'Failed to receive frame from filter');
|
|
784
1305
|
return null;
|
|
785
1306
|
}
|
|
@@ -869,6 +1390,24 @@ export class FilterAPI {
|
|
|
869
1390
|
const ret = this.graph.queueCommand(target, cmd, arg, ts, flags);
|
|
870
1391
|
FFmpegError.throwIfError(ret, 'Failed to queue filter command');
|
|
871
1392
|
}
|
|
1393
|
+
pipeTo(target) {
|
|
1394
|
+
const t = target;
|
|
1395
|
+
// Store reference to next component for flush propagation
|
|
1396
|
+
this.nextComponent = t;
|
|
1397
|
+
// Start worker if not already running
|
|
1398
|
+
this.workerPromise ??= this.runWorker();
|
|
1399
|
+
// Start pipe task: filter.outputQueue -> target.inputQueue (via target.send)
|
|
1400
|
+
this.pipeToPromise = (async () => {
|
|
1401
|
+
while (true) {
|
|
1402
|
+
const frame = await this.receiveFrame();
|
|
1403
|
+
if (!frame)
|
|
1404
|
+
break;
|
|
1405
|
+
await t.sendToQueue(frame);
|
|
1406
|
+
}
|
|
1407
|
+
})();
|
|
1408
|
+
// Return scheduler for chaining (target is now the last component)
|
|
1409
|
+
return new Scheduler(this, t);
|
|
1410
|
+
}
|
|
872
1411
|
/**
|
|
873
1412
|
* Free filter resources.
|
|
874
1413
|
*
|
|
@@ -887,10 +1426,123 @@ export class FilterAPI {
|
|
|
887
1426
|
return;
|
|
888
1427
|
}
|
|
889
1428
|
this.isClosed = true;
|
|
1429
|
+
// Close queues
|
|
1430
|
+
this.inputQueue.close();
|
|
1431
|
+
this.outputQueue.close();
|
|
1432
|
+
this.frame.free();
|
|
890
1433
|
this.graph.free();
|
|
891
1434
|
this.buffersrcCtx = null;
|
|
892
1435
|
this.buffersinkCtx = null;
|
|
893
1436
|
this.initialized = false;
|
|
1437
|
+
this.initializePromise = null;
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Worker loop for push-based processing.
|
|
1441
|
+
*
|
|
1442
|
+
* @internal
|
|
1443
|
+
*/
|
|
1444
|
+
async runWorker() {
|
|
1445
|
+
try {
|
|
1446
|
+
// Outer loop - receive frames
|
|
1447
|
+
while (!this.inputQueue.isClosed) {
|
|
1448
|
+
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
1449
|
+
try {
|
|
1450
|
+
const frame = __addDisposableResource(env_3, await this.inputQueue.receive(), false);
|
|
1451
|
+
if (!frame)
|
|
1452
|
+
break;
|
|
1453
|
+
await this.process(frame);
|
|
1454
|
+
// Receive all available frames
|
|
1455
|
+
while (!this.outputQueue.isClosed) {
|
|
1456
|
+
const buffered = await this.receive();
|
|
1457
|
+
if (!buffered)
|
|
1458
|
+
break; // Stop on EAGAIN or EOF
|
|
1459
|
+
await this.outputQueue.send(buffered); // Only send actual frames
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
catch (e_3) {
|
|
1463
|
+
env_3.error = e_3;
|
|
1464
|
+
env_3.hasError = true;
|
|
1465
|
+
}
|
|
1466
|
+
finally {
|
|
1467
|
+
__disposeResources(env_3);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
// Flush filter at end
|
|
1471
|
+
await this.flush();
|
|
1472
|
+
while (!this.outputQueue.isClosed) {
|
|
1473
|
+
const frame = await this.receive();
|
|
1474
|
+
if (!frame)
|
|
1475
|
+
break; // Stop on EAGAIN or EOF
|
|
1476
|
+
await this.outputQueue.send(frame); // Only send actual frames
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
catch {
|
|
1480
|
+
// Ignore error ?
|
|
1481
|
+
}
|
|
1482
|
+
finally {
|
|
1483
|
+
// Close output queue when done
|
|
1484
|
+
this.outputQueue?.close();
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Send frame to input queue or flush the pipeline.
|
|
1489
|
+
*
|
|
1490
|
+
* When frame is provided, queues it for filtering.
|
|
1491
|
+
* When null is provided, triggers flush sequence:
|
|
1492
|
+
* - Closes input queue
|
|
1493
|
+
* - Waits for worker completion
|
|
1494
|
+
* - Flushes filter and sends remaining frames to output queue
|
|
1495
|
+
* - Closes output queue
|
|
1496
|
+
* - Waits for pipeTo task completion
|
|
1497
|
+
* - Propagates flush to next component (if any)
|
|
1498
|
+
*
|
|
1499
|
+
* Used by scheduler system for pipeline control.
|
|
1500
|
+
*
|
|
1501
|
+
* @param frame - Frame to send, or null to flush
|
|
1502
|
+
*
|
|
1503
|
+
* @internal
|
|
1504
|
+
*/
|
|
1505
|
+
async sendToQueue(frame) {
|
|
1506
|
+
if (frame) {
|
|
1507
|
+
await this.inputQueue.send(frame);
|
|
1508
|
+
}
|
|
1509
|
+
else {
|
|
1510
|
+
// Close input queue to signal end of stream to worker
|
|
1511
|
+
this.inputQueue.close();
|
|
1512
|
+
// Wait for worker to finish processing all frames (if exists)
|
|
1513
|
+
if (this.workerPromise) {
|
|
1514
|
+
await this.workerPromise;
|
|
1515
|
+
}
|
|
1516
|
+
// Flush filter at end (like FFmpeg does)
|
|
1517
|
+
await this.flush();
|
|
1518
|
+
// Send all flushed frames to output queue
|
|
1519
|
+
while (true) {
|
|
1520
|
+
const frame = await this.receive();
|
|
1521
|
+
if (!frame)
|
|
1522
|
+
break; // Stop on EAGAIN or EOF
|
|
1523
|
+
await this.outputQueue.send(frame); // Only send actual frames
|
|
1524
|
+
}
|
|
1525
|
+
// Close output queue to signal end of stream to pipeTo() task
|
|
1526
|
+
this.outputQueue.close();
|
|
1527
|
+
// Wait for pipeTo() task to finish processing all frames (if exists)
|
|
1528
|
+
if (this.pipeToPromise) {
|
|
1529
|
+
await this.pipeToPromise;
|
|
1530
|
+
}
|
|
1531
|
+
// Then propagate flush to next component
|
|
1532
|
+
if (this.nextComponent) {
|
|
1533
|
+
await this.nextComponent.sendToQueue(null);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Receive frame from output queue.
|
|
1539
|
+
*
|
|
1540
|
+
* @returns Frame from output queue or null if closed
|
|
1541
|
+
*
|
|
1542
|
+
* @internal
|
|
1543
|
+
*/
|
|
1544
|
+
async receiveFrame() {
|
|
1545
|
+
return await this.outputQueue.receive();
|
|
894
1546
|
}
|
|
895
1547
|
/**
|
|
896
1548
|
* Initialize filter graph from first frame.
|
|
@@ -908,6 +1560,23 @@ export class FilterAPI {
|
|
|
908
1560
|
* @internal
|
|
909
1561
|
*/
|
|
910
1562
|
async initialize(frame) {
|
|
1563
|
+
// Calculate timeBase from first frame
|
|
1564
|
+
this.calculatedTimeBase = this.calculateTimeBase(frame);
|
|
1565
|
+
// Track initial frame properties for change detection
|
|
1566
|
+
this.lastFrameProps = {
|
|
1567
|
+
format: frame.format,
|
|
1568
|
+
width: frame.width,
|
|
1569
|
+
height: frame.height,
|
|
1570
|
+
sampleRate: frame.sampleRate,
|
|
1571
|
+
channels: frame.channelLayout?.nbChannels ?? 0,
|
|
1572
|
+
};
|
|
1573
|
+
// Set graph options before parsing
|
|
1574
|
+
if (this.options.scaleSwsOpts) {
|
|
1575
|
+
this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
|
|
1576
|
+
}
|
|
1577
|
+
if (this.options.audioResampleOpts) {
|
|
1578
|
+
this.graph.aresampleSwrOpts = this.options.audioResampleOpts;
|
|
1579
|
+
}
|
|
911
1580
|
// Create buffer source and sink
|
|
912
1581
|
this.createBufferSource(frame);
|
|
913
1582
|
this.createBufferSink(frame);
|
|
@@ -937,6 +1606,23 @@ export class FilterAPI {
|
|
|
937
1606
|
* @see {@link initialize} For async version
|
|
938
1607
|
*/
|
|
939
1608
|
initializeSync(frame) {
|
|
1609
|
+
// Calculate timeBase from first frame
|
|
1610
|
+
this.calculatedTimeBase = this.calculateTimeBase(frame);
|
|
1611
|
+
// Track initial frame properties for change detection
|
|
1612
|
+
this.lastFrameProps = {
|
|
1613
|
+
format: frame.format,
|
|
1614
|
+
width: frame.width,
|
|
1615
|
+
height: frame.height,
|
|
1616
|
+
sampleRate: frame.sampleRate,
|
|
1617
|
+
channels: frame.channelLayout?.nbChannels ?? 0,
|
|
1618
|
+
};
|
|
1619
|
+
// Set graph options before parsing
|
|
1620
|
+
if (this.options.scaleSwsOpts) {
|
|
1621
|
+
this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
|
|
1622
|
+
}
|
|
1623
|
+
if (this.options.audioResampleOpts) {
|
|
1624
|
+
this.graph.aresampleSwrOpts = this.options.audioResampleOpts;
|
|
1625
|
+
}
|
|
940
1626
|
// Create buffer source and sink
|
|
941
1627
|
this.createBufferSource(frame);
|
|
942
1628
|
this.createBufferSink(frame);
|
|
@@ -947,6 +1633,125 @@ export class FilterAPI {
|
|
|
947
1633
|
FFmpegError.throwIfError(ret, 'Failed to configure filter graph');
|
|
948
1634
|
this.initialized = true;
|
|
949
1635
|
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Check if frame properties changed and handle according to dropOnChange/allowReinit options.
|
|
1638
|
+
*
|
|
1639
|
+
* Implements FFmpeg's IFILTER_FLAG_DROPCHANGED and IFILTER_FLAG_REINIT logic
|
|
1640
|
+
*
|
|
1641
|
+
* @param frame - Frame to check
|
|
1642
|
+
*
|
|
1643
|
+
* @returns true if frame should be processed, false if frame should be dropped
|
|
1644
|
+
*
|
|
1645
|
+
* @throws {Error} If format changed and allowReinit is false
|
|
1646
|
+
*
|
|
1647
|
+
* @internal
|
|
1648
|
+
*/
|
|
1649
|
+
checkFramePropertiesChanged(frame) {
|
|
1650
|
+
if (!this.lastFrameProps) {
|
|
1651
|
+
return true; // No previous frame, allow
|
|
1652
|
+
}
|
|
1653
|
+
// Check for property changes
|
|
1654
|
+
const changed = frame.format !== this.lastFrameProps.format ||
|
|
1655
|
+
frame.width !== this.lastFrameProps.width ||
|
|
1656
|
+
frame.height !== this.lastFrameProps.height ||
|
|
1657
|
+
frame.sampleRate !== this.lastFrameProps.sampleRate ||
|
|
1658
|
+
(frame.channelLayout?.nbChannels ?? 0) !== this.lastFrameProps.channels;
|
|
1659
|
+
if (!changed) {
|
|
1660
|
+
return true; // No changes, process frame
|
|
1661
|
+
}
|
|
1662
|
+
// Properties changed - check dropOnChange flag
|
|
1663
|
+
if (this.options.dropOnChange) {
|
|
1664
|
+
return false; // Drop frame
|
|
1665
|
+
}
|
|
1666
|
+
// Check allowReinit flag
|
|
1667
|
+
// Default is true (allow reinit), only block if explicitly set to false
|
|
1668
|
+
const allowReinit = this.options.allowReinit !== false;
|
|
1669
|
+
if (!allowReinit && this.initialized) {
|
|
1670
|
+
throw new Error('Frame properties changed but allowReinit is false. ' +
|
|
1671
|
+
`Format: ${this.lastFrameProps.format}->${frame.format}, ` +
|
|
1672
|
+
`Size: ${this.lastFrameProps.width}x${this.lastFrameProps.height}->${frame.width}x${frame.height}`);
|
|
1673
|
+
}
|
|
1674
|
+
// Close current graph and reinitialize
|
|
1675
|
+
this.graph.free();
|
|
1676
|
+
// Create new graph
|
|
1677
|
+
this.graph = new FilterGraph();
|
|
1678
|
+
this.graph.alloc();
|
|
1679
|
+
// Configure threading
|
|
1680
|
+
if (this.options.threads !== undefined) {
|
|
1681
|
+
this.graph.nbThreads = this.options.threads;
|
|
1682
|
+
}
|
|
1683
|
+
// Configure scaler options
|
|
1684
|
+
if (this.options.scaleSwsOpts) {
|
|
1685
|
+
this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
|
|
1686
|
+
}
|
|
1687
|
+
this.buffersrcCtx = null;
|
|
1688
|
+
this.buffersinkCtx = null;
|
|
1689
|
+
this.initialized = false;
|
|
1690
|
+
this.initializePromise = null;
|
|
1691
|
+
this.calculatedTimeBase = null;
|
|
1692
|
+
return true; // Will be reinitialized on next process
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* Calculate timeBase from frame based on media type and CFR option.
|
|
1696
|
+
*
|
|
1697
|
+
* Implements FFmpeg's ifilter_parameters_from_frame logic:
|
|
1698
|
+
* - Audio: Always { 1, sample_rate }
|
|
1699
|
+
* - Video CFR: 1/framerate (inverse of framerate)
|
|
1700
|
+
* - Video VFR: Use frame.timeBase
|
|
1701
|
+
*
|
|
1702
|
+
* @param frame - Input frame
|
|
1703
|
+
*
|
|
1704
|
+
* @returns Calculated timeBase
|
|
1705
|
+
*
|
|
1706
|
+
* @internal
|
|
1707
|
+
*/
|
|
1708
|
+
calculateTimeBase(frame) {
|
|
1709
|
+
if (frame.isAudio()) {
|
|
1710
|
+
// Audio: Always { 1, sample_rate }
|
|
1711
|
+
return { num: 1, den: frame.sampleRate };
|
|
1712
|
+
}
|
|
1713
|
+
else {
|
|
1714
|
+
// Video: Check CFR flag
|
|
1715
|
+
if (this.options.cfr) {
|
|
1716
|
+
// CFR mode: timeBase = 1/framerate = inverse(framerate)
|
|
1717
|
+
// Note: framerate is guaranteed to be set (validated in create())
|
|
1718
|
+
return avInvQ(this.options.framerate);
|
|
1719
|
+
}
|
|
1720
|
+
else {
|
|
1721
|
+
// VFR mode: Use frame.timeBase
|
|
1722
|
+
return frame.timeBase;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Post-process output frame from buffersink.
|
|
1728
|
+
*
|
|
1729
|
+
* Applies FFmpeg's fg_output_step() behavior:
|
|
1730
|
+
* 1. Sets frame.timeBase from buffersink (filters can change timeBase, e.g., aresample)
|
|
1731
|
+
* 2. Calculates video frame duration from frame rate if not set
|
|
1732
|
+
*
|
|
1733
|
+
* This must be called AFTER buffersinkGetFrame() for every output frame.
|
|
1734
|
+
*
|
|
1735
|
+
* @param frame - Output frame from buffersink
|
|
1736
|
+
*
|
|
1737
|
+
* @throws {Error} If buffersink context not available
|
|
1738
|
+
*
|
|
1739
|
+
* @internal
|
|
1740
|
+
*/
|
|
1741
|
+
postProcessOutputFrame(frame) {
|
|
1742
|
+
if (!this.buffersinkCtx) {
|
|
1743
|
+
throw new Error('Buffersink context not available');
|
|
1744
|
+
}
|
|
1745
|
+
// Filters can change timeBase (e.g., aresample sets output to {1, out_sample_rate})
|
|
1746
|
+
// Without this, frame has INPUT timeBase instead of filter's OUTPUT timeBase
|
|
1747
|
+
frame.timeBase = this.buffersinkCtx.buffersinkGetTimeBase();
|
|
1748
|
+
if (frame.isVideo() && !frame.duration) {
|
|
1749
|
+
const frameRate = this.buffersinkCtx.buffersinkGetFrameRate();
|
|
1750
|
+
if (frameRate.num > 0 && frameRate.den > 0) {
|
|
1751
|
+
frame.duration = avRescaleQ(1, avInvQ(frameRate), frame.timeBase);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
950
1755
|
/**
|
|
951
1756
|
* Create buffer source with frame parameters.
|
|
952
1757
|
*
|
|
@@ -967,6 +1772,10 @@ export class FilterAPI {
|
|
|
967
1772
|
if (!bufferFilter) {
|
|
968
1773
|
throw new Error(`${filterName} filter not found`);
|
|
969
1774
|
}
|
|
1775
|
+
// Ensure timeBase was calculated
|
|
1776
|
+
if (!this.calculatedTimeBase) {
|
|
1777
|
+
throw new Error('TimeBase not calculated - this should not happen');
|
|
1778
|
+
}
|
|
970
1779
|
// For audio, create with args. For video, use allocFilter + buffersrcParametersSet
|
|
971
1780
|
if (frame.isVideo()) {
|
|
972
1781
|
// Allocate filter without args
|
|
@@ -978,8 +1787,8 @@ export class FilterAPI {
|
|
|
978
1787
|
width: frame.width,
|
|
979
1788
|
height: frame.height,
|
|
980
1789
|
format: frame.format,
|
|
981
|
-
timeBase: this.
|
|
982
|
-
frameRate: this.options.
|
|
1790
|
+
timeBase: this.calculatedTimeBase,
|
|
1791
|
+
frameRate: this.options.framerate,
|
|
983
1792
|
sampleAspectRatio: frame.sampleAspectRatio,
|
|
984
1793
|
colorRange: frame.colorRange,
|
|
985
1794
|
colorSpace: frame.colorSpace,
|
|
@@ -995,7 +1804,7 @@ export class FilterAPI {
|
|
|
995
1804
|
const formatName = avGetSampleFmtName(frame.format);
|
|
996
1805
|
const channelLayout = frame.channelLayout.mask === 0n ? 'stereo' : frame.channelLayout.mask.toString();
|
|
997
1806
|
// eslint-disable-next-line @stylistic/max-len
|
|
998
|
-
const args = `time_base=${this.
|
|
1807
|
+
const args = `time_base=${this.calculatedTimeBase.num}/${this.calculatedTimeBase.den}:sample_rate=${frame.sampleRate}:sample_fmt=${formatName}:channel_layout=${channelLayout}`;
|
|
999
1808
|
this.buffersrcCtx = this.graph.createFilter(bufferFilter, 'in', args);
|
|
1000
1809
|
if (!this.buffersrcCtx) {
|
|
1001
1810
|
throw new Error('Failed to create audio buffer source');
|
|
@@ -1061,8 +1870,8 @@ export class FilterAPI {
|
|
|
1061
1870
|
if (filters) {
|
|
1062
1871
|
for (const filterCtx of filters) {
|
|
1063
1872
|
const filter = filterCtx.filter;
|
|
1064
|
-
if (filter
|
|
1065
|
-
filterCtx.hwDeviceCtx =
|
|
1873
|
+
if (filter?.hasFlags(AVFILTER_FLAG_HWDEVICE)) {
|
|
1874
|
+
filterCtx.hwDeviceCtx = this.options.hardware?.deviceContext ?? frame.hwFramesCtx?.deviceRef ?? null;
|
|
1066
1875
|
// Set extra_hw_frames if specified
|
|
1067
1876
|
if (this.options.extraHWFrames !== undefined && this.options.extraHWFrames > 0) {
|
|
1068
1877
|
filterCtx.extraHWFrames = this.options.extraHWFrames;
|