@revizly/node-av 5.2.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BUILD_LINUX.md +61 -0
- package/LICENSE.md +22 -0
- package/README.md +662 -0
- package/build_mac_local.sh +69 -0
- package/dist/api/audio-frame-buffer.d.ts +205 -0
- package/dist/api/audio-frame-buffer.js +287 -0
- package/dist/api/audio-frame-buffer.js.map +1 -0
- package/dist/api/bitstream-filter.d.ts +820 -0
- package/dist/api/bitstream-filter.js +1242 -0
- package/dist/api/bitstream-filter.js.map +1 -0
- package/dist/api/constants.d.ts +44 -0
- package/dist/api/constants.js +45 -0
- package/dist/api/constants.js.map +1 -0
- package/dist/api/data/test_av1.ivf +0 -0
- package/dist/api/data/test_h264.h264 +0 -0
- package/dist/api/data/test_hevc.h265 +0 -0
- package/dist/api/data/test_mjpeg.mjpeg +0 -0
- package/dist/api/data/test_vp8.ivf +0 -0
- package/dist/api/data/test_vp9.ivf +0 -0
- package/dist/api/decoder.d.ts +1088 -0
- package/dist/api/decoder.js +1775 -0
- package/dist/api/decoder.js.map +1 -0
- package/dist/api/demuxer.d.ts +1219 -0
- package/dist/api/demuxer.js +2081 -0
- package/dist/api/demuxer.js.map +1 -0
- package/dist/api/device.d.ts +586 -0
- package/dist/api/device.js +961 -0
- package/dist/api/device.js.map +1 -0
- package/dist/api/encoder.d.ts +1132 -0
- package/dist/api/encoder.js +1988 -0
- package/dist/api/encoder.js.map +1 -0
- package/dist/api/filter-complex.d.ts +821 -0
- package/dist/api/filter-complex.js +1604 -0
- package/dist/api/filter-complex.js.map +1 -0
- package/dist/api/filter-presets.d.ts +1286 -0
- package/dist/api/filter-presets.js +2152 -0
- package/dist/api/filter-presets.js.map +1 -0
- package/dist/api/filter.d.ts +1234 -0
- package/dist/api/filter.js +1976 -0
- package/dist/api/filter.js.map +1 -0
- package/dist/api/fmp4-stream.d.ts +426 -0
- package/dist/api/fmp4-stream.js +739 -0
- package/dist/api/fmp4-stream.js.map +1 -0
- package/dist/api/hardware.d.ts +651 -0
- package/dist/api/hardware.js +1260 -0
- package/dist/api/hardware.js.map +1 -0
- package/dist/api/index.d.ts +17 -0
- package/dist/api/index.js +32 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/io-stream.d.ts +307 -0
- package/dist/api/io-stream.js +282 -0
- package/dist/api/io-stream.js.map +1 -0
- package/dist/api/muxer.d.ts +957 -0
- package/dist/api/muxer.js +2002 -0
- package/dist/api/muxer.js.map +1 -0
- package/dist/api/pipeline.d.ts +607 -0
- package/dist/api/pipeline.js +1145 -0
- package/dist/api/pipeline.js.map +1 -0
- package/dist/api/utilities/async-queue.d.ts +120 -0
- package/dist/api/utilities/async-queue.js +211 -0
- package/dist/api/utilities/async-queue.js.map +1 -0
- package/dist/api/utilities/audio-sample.d.ts +117 -0
- package/dist/api/utilities/audio-sample.js +112 -0
- package/dist/api/utilities/audio-sample.js.map +1 -0
- package/dist/api/utilities/channel-layout.d.ts +76 -0
- package/dist/api/utilities/channel-layout.js +80 -0
- package/dist/api/utilities/channel-layout.js.map +1 -0
- package/dist/api/utilities/electron-shared-texture.d.ts +328 -0
- package/dist/api/utilities/electron-shared-texture.js +503 -0
- package/dist/api/utilities/electron-shared-texture.js.map +1 -0
- package/dist/api/utilities/image.d.ts +207 -0
- package/dist/api/utilities/image.js +213 -0
- package/dist/api/utilities/image.js.map +1 -0
- package/dist/api/utilities/index.d.ts +12 -0
- package/dist/api/utilities/index.js +25 -0
- package/dist/api/utilities/index.js.map +1 -0
- package/dist/api/utilities/media-type.d.ts +49 -0
- package/dist/api/utilities/media-type.js +53 -0
- package/dist/api/utilities/media-type.js.map +1 -0
- package/dist/api/utilities/pixel-format.d.ts +89 -0
- package/dist/api/utilities/pixel-format.js +97 -0
- package/dist/api/utilities/pixel-format.js.map +1 -0
- package/dist/api/utilities/sample-format.d.ts +129 -0
- package/dist/api/utilities/sample-format.js +141 -0
- package/dist/api/utilities/sample-format.js.map +1 -0
- package/dist/api/utilities/scheduler.d.ts +138 -0
- package/dist/api/utilities/scheduler.js +98 -0
- package/dist/api/utilities/scheduler.js.map +1 -0
- package/dist/api/utilities/streaming.d.ts +186 -0
- package/dist/api/utilities/streaming.js +309 -0
- package/dist/api/utilities/streaming.js.map +1 -0
- package/dist/api/utilities/timestamp.d.ts +193 -0
- package/dist/api/utilities/timestamp.js +206 -0
- package/dist/api/utilities/timestamp.js.map +1 -0
- package/dist/api/utilities/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/utils.d.ts +19 -0
- package/dist/api/utils.js +39 -0
- package/dist/api/utils.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/channel-layouts.d.ts +53 -0
- package/dist/constants/channel-layouts.js +57 -0
- package/dist/constants/channel-layouts.js.map +1 -0
- package/dist/constants/constants.d.ts +2325 -0
- package/dist/constants/constants.js +1887 -0
- package/dist/constants/constants.js.map +1 -0
- package/dist/constants/decoders.d.ts +633 -0
- package/dist/constants/decoders.js +641 -0
- package/dist/constants/decoders.js.map +1 -0
- package/dist/constants/encoders.d.ts +295 -0
- package/dist/constants/encoders.js +308 -0
- package/dist/constants/encoders.js.map +1 -0
- package/dist/constants/hardware.d.ts +26 -0
- package/dist/constants/hardware.js +27 -0
- package/dist/constants/hardware.js.map +1 -0
- package/dist/constants/index.d.ts +5 -0
- package/dist/constants/index.js +6 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +99 -0
- package/dist/ffmpeg/index.js +115 -0
- package/dist/ffmpeg/index.js.map +1 -0
- package/dist/ffmpeg/utils.d.ts +31 -0
- package/dist/ffmpeg/utils.js +68 -0
- package/dist/ffmpeg/utils.js.map +1 -0
- package/dist/ffmpeg/version.d.ts +6 -0
- package/dist/ffmpeg/version.js +7 -0
- package/dist/ffmpeg/version.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/audio-fifo.d.ts +399 -0
- package/dist/lib/audio-fifo.js +431 -0
- package/dist/lib/audio-fifo.js.map +1 -0
- package/dist/lib/binding.d.ts +228 -0
- package/dist/lib/binding.js +60 -0
- package/dist/lib/binding.js.map +1 -0
- package/dist/lib/bitstream-filter-context.d.ts +379 -0
- package/dist/lib/bitstream-filter-context.js +441 -0
- package/dist/lib/bitstream-filter-context.js.map +1 -0
- package/dist/lib/bitstream-filter.d.ts +140 -0
- package/dist/lib/bitstream-filter.js +154 -0
- package/dist/lib/bitstream-filter.js.map +1 -0
- package/dist/lib/codec-context.d.ts +1071 -0
- package/dist/lib/codec-context.js +1354 -0
- package/dist/lib/codec-context.js.map +1 -0
- package/dist/lib/codec-parameters.d.ts +616 -0
- package/dist/lib/codec-parameters.js +761 -0
- package/dist/lib/codec-parameters.js.map +1 -0
- package/dist/lib/codec-parser.d.ts +201 -0
- package/dist/lib/codec-parser.js +213 -0
- package/dist/lib/codec-parser.js.map +1 -0
- package/dist/lib/codec.d.ts +586 -0
- package/dist/lib/codec.js +713 -0
- package/dist/lib/codec.js.map +1 -0
- package/dist/lib/device.d.ts +291 -0
- package/dist/lib/device.js +324 -0
- package/dist/lib/device.js.map +1 -0
- package/dist/lib/dictionary.d.ts +333 -0
- package/dist/lib/dictionary.js +372 -0
- package/dist/lib/dictionary.js.map +1 -0
- package/dist/lib/error.d.ts +242 -0
- package/dist/lib/error.js +303 -0
- package/dist/lib/error.js.map +1 -0
- package/dist/lib/fifo.d.ts +416 -0
- package/dist/lib/fifo.js +453 -0
- package/dist/lib/fifo.js.map +1 -0
- package/dist/lib/filter-context.d.ts +712 -0
- package/dist/lib/filter-context.js +789 -0
- package/dist/lib/filter-context.js.map +1 -0
- package/dist/lib/filter-graph-segment.d.ts +160 -0
- package/dist/lib/filter-graph-segment.js +171 -0
- package/dist/lib/filter-graph-segment.js.map +1 -0
- package/dist/lib/filter-graph.d.ts +641 -0
- package/dist/lib/filter-graph.js +704 -0
- package/dist/lib/filter-graph.js.map +1 -0
- package/dist/lib/filter-inout.d.ts +198 -0
- package/dist/lib/filter-inout.js +257 -0
- package/dist/lib/filter-inout.js.map +1 -0
- package/dist/lib/filter.d.ts +243 -0
- package/dist/lib/filter.js +272 -0
- package/dist/lib/filter.js.map +1 -0
- package/dist/lib/format-context.d.ts +1254 -0
- package/dist/lib/format-context.js +1379 -0
- package/dist/lib/format-context.js.map +1 -0
- package/dist/lib/frame-utils.d.ts +116 -0
- package/dist/lib/frame-utils.js +98 -0
- package/dist/lib/frame-utils.js.map +1 -0
- package/dist/lib/frame.d.ts +1222 -0
- package/dist/lib/frame.js +1435 -0
- package/dist/lib/frame.js.map +1 -0
- package/dist/lib/hardware-device-context.d.ts +362 -0
- package/dist/lib/hardware-device-context.js +383 -0
- package/dist/lib/hardware-device-context.js.map +1 -0
- package/dist/lib/hardware-frames-context.d.ts +419 -0
- package/dist/lib/hardware-frames-context.js +477 -0
- package/dist/lib/hardware-frames-context.js.map +1 -0
- package/dist/lib/index.d.ts +35 -0
- package/dist/lib/index.js +60 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/input-format.d.ts +249 -0
- package/dist/lib/input-format.js +306 -0
- package/dist/lib/input-format.js.map +1 -0
- package/dist/lib/io-context.d.ts +696 -0
- package/dist/lib/io-context.js +769 -0
- package/dist/lib/io-context.js.map +1 -0
- package/dist/lib/log.d.ts +174 -0
- package/dist/lib/log.js +184 -0
- package/dist/lib/log.js.map +1 -0
- package/dist/lib/native-types.d.ts +946 -0
- package/dist/lib/native-types.js +2 -0
- package/dist/lib/native-types.js.map +1 -0
- package/dist/lib/option.d.ts +927 -0
- package/dist/lib/option.js +1583 -0
- package/dist/lib/option.js.map +1 -0
- package/dist/lib/output-format.d.ts +180 -0
- package/dist/lib/output-format.js +213 -0
- package/dist/lib/output-format.js.map +1 -0
- package/dist/lib/packet.d.ts +501 -0
- package/dist/lib/packet.js +590 -0
- package/dist/lib/packet.js.map +1 -0
- package/dist/lib/rational.d.ts +251 -0
- package/dist/lib/rational.js +278 -0
- package/dist/lib/rational.js.map +1 -0
- package/dist/lib/software-resample-context.d.ts +552 -0
- package/dist/lib/software-resample-context.js +592 -0
- package/dist/lib/software-resample-context.js.map +1 -0
- package/dist/lib/software-scale-context.d.ts +344 -0
- package/dist/lib/software-scale-context.js +366 -0
- package/dist/lib/software-scale-context.js.map +1 -0
- package/dist/lib/stream.d.ts +379 -0
- package/dist/lib/stream.js +526 -0
- package/dist/lib/stream.js.map +1 -0
- package/dist/lib/sync-queue.d.ts +179 -0
- package/dist/lib/sync-queue.js +197 -0
- package/dist/lib/sync-queue.js.map +1 -0
- package/dist/lib/types.d.ts +34 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/utilities.d.ts +1127 -0
- package/dist/lib/utilities.js +1225 -0
- package/dist/lib/utilities.js.map +1 -0
- package/dist/utils/electron.d.ts +49 -0
- package/dist/utils/electron.js +63 -0
- package/dist/utils/electron.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/install/check.js +121 -0
- package/install/ffmpeg.js +66 -0
- package/jellyfin-ffmpeg.patch +181 -0
- package/package.json +129 -0
|
@@ -0,0 +1,1976 @@
|
|
|
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_KEEP_REF, AV_BUFFERSRC_FLAG_PUSH, AVERROR_EAGAIN, AVERROR_EOF, AVERROR_FILTER_NOT_FOUND, 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';
|
|
65
|
+
/**
|
|
66
|
+
* High-level filter API for audio and video processing.
|
|
67
|
+
*
|
|
68
|
+
* Provides simplified interface for applying FFmpeg filters to frames.
|
|
69
|
+
* Handles filter graph construction, frame buffering, and command control.
|
|
70
|
+
* Supports both software and hardware-accelerated filtering operations.
|
|
71
|
+
* Essential component for effects, transformations, and format conversions.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* import { FilterAPI } from 'node-av/api';
|
|
76
|
+
*
|
|
77
|
+
* // Create video filter - initializes on first frame
|
|
78
|
+
* const filter = FilterAPI.create('scale=1280:720', {
|
|
79
|
+
* timeBase: video.timeBase,
|
|
80
|
+
* });
|
|
81
|
+
*
|
|
82
|
+
* // Process frame - first frame configures filter graph
|
|
83
|
+
* const output = await filter.process(inputFrame);
|
|
84
|
+
* if (output) {
|
|
85
|
+
* console.log(`Filtered frame: ${output.width}x${output.height}`);
|
|
86
|
+
* output.free();
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* // Hardware-accelerated filtering - hw context detected from frame
|
|
93
|
+
* const filter = FilterAPI.create('hwupload,scale_cuda=1920:1080,hwdownload', {
|
|
94
|
+
* timeBase: video.timeBase,
|
|
95
|
+
* });
|
|
96
|
+
* // Hardware frames context will be automatically detected from first frame
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* @see {@link FilterGraph} For low-level filter graph API
|
|
100
|
+
* @see {@link Frame} For frame operations
|
|
101
|
+
*/
|
|
102
|
+
export class FilterAPI {
|
|
103
|
+
graph;
|
|
104
|
+
description;
|
|
105
|
+
options;
|
|
106
|
+
buffersrcCtx = null;
|
|
107
|
+
buffersinkCtx = null;
|
|
108
|
+
frame = new Frame(); // Reusable frame for receive operations
|
|
109
|
+
initializePromise = null;
|
|
110
|
+
initialized = false;
|
|
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
|
+
signal;
|
|
122
|
+
pipeToPromise = null;
|
|
123
|
+
/**
|
|
124
|
+
* @param graph - Filter graph instance
|
|
125
|
+
*
|
|
126
|
+
* @param description - Filter description string
|
|
127
|
+
*
|
|
128
|
+
* @param options - Filter options
|
|
129
|
+
*
|
|
130
|
+
* @internal
|
|
131
|
+
*/
|
|
132
|
+
constructor(graph, description, options) {
|
|
133
|
+
this.graph = graph;
|
|
134
|
+
this.description = description;
|
|
135
|
+
this.options = options;
|
|
136
|
+
this.inputQueue = new AsyncQueue(FRAME_THREAD_QUEUE_SIZE);
|
|
137
|
+
this.outputQueue = new AsyncQueue(FRAME_THREAD_QUEUE_SIZE);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Create a filter with specified description and configuration.
|
|
141
|
+
*
|
|
142
|
+
* Direct mapping to avfilter_graph_parse_ptr() and avfilter_graph_config().
|
|
143
|
+
*
|
|
144
|
+
* @param description - Filter graph description
|
|
145
|
+
*
|
|
146
|
+
* @param options - Filter options
|
|
147
|
+
*
|
|
148
|
+
* @returns Configured filter instance
|
|
149
|
+
*
|
|
150
|
+
* @throws {Error} If cfr=true but framerate is not set
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* // Simple video filter (VFR mode, auto timeBase)
|
|
155
|
+
* const filter = FilterAPI.create('scale=640:480');
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```typescript
|
|
160
|
+
* // CFR mode with constant framerate
|
|
161
|
+
* const filter = FilterAPI.create('scale=1920:1080', {
|
|
162
|
+
* cfr: true,
|
|
163
|
+
* framerate: { num: 25, den: 1 }
|
|
164
|
+
* });
|
|
165
|
+
* ```
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```typescript
|
|
169
|
+
* // Audio filter with resampling
|
|
170
|
+
* const filter = FilterAPI.create('aformat=sample_fmts=s16:sample_rates=44100', {
|
|
171
|
+
* audioResampleOpts: 'async=1'
|
|
172
|
+
* });
|
|
173
|
+
* ```
|
|
174
|
+
*
|
|
175
|
+
* @see {@link process} For frame processing
|
|
176
|
+
* @see {@link FilterOptions} For configuration options
|
|
177
|
+
*/
|
|
178
|
+
static create(description, options = {}) {
|
|
179
|
+
// Validate options: CFR requires framerate
|
|
180
|
+
if (options.cfr && !options.framerate) {
|
|
181
|
+
throw new Error('cfr=true requires framerate to be set');
|
|
182
|
+
}
|
|
183
|
+
// Create graph
|
|
184
|
+
const graph = new FilterGraph();
|
|
185
|
+
graph.alloc();
|
|
186
|
+
// Configure threading
|
|
187
|
+
if (options.threads !== undefined) {
|
|
188
|
+
graph.nbThreads = options.threads;
|
|
189
|
+
}
|
|
190
|
+
// Configure scaler options
|
|
191
|
+
if (options.scaleSwsOpts) {
|
|
192
|
+
graph.scaleSwsOpts = options.scaleSwsOpts;
|
|
193
|
+
}
|
|
194
|
+
const filter = new FilterAPI(graph, description, options);
|
|
195
|
+
if (options.signal) {
|
|
196
|
+
options.signal.throwIfAborted();
|
|
197
|
+
filter.signal = options.signal;
|
|
198
|
+
}
|
|
199
|
+
return filter;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Check if filter is open.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* if (filter.isFilterOpen) {
|
|
207
|
+
* const output = await filter.process(frame);
|
|
208
|
+
* }
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
get isFilterOpen() {
|
|
212
|
+
return !this.isClosed;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Check if filter has been initialized.
|
|
216
|
+
*
|
|
217
|
+
* Returns true after first frame has been processed and filter graph configured.
|
|
218
|
+
* Useful for checking if filter has received frame properties.
|
|
219
|
+
*
|
|
220
|
+
* @returns true if filter graph has been built from first frame
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```typescript
|
|
224
|
+
* if (!filter.isFilterInitialized) {
|
|
225
|
+
* console.log('Filter will initialize on first frame');
|
|
226
|
+
* }
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
get isFilterInitialized() {
|
|
230
|
+
return this.initialized;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Get buffersink filter context.
|
|
234
|
+
*
|
|
235
|
+
* Provides access to the buffersink filter context for advanced operations.
|
|
236
|
+
* Returns null if filter is not initialized.
|
|
237
|
+
*
|
|
238
|
+
* @returns Buffersink context or null
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```typescript
|
|
242
|
+
* const sink = filter.buffersink;
|
|
243
|
+
* if (sink) {
|
|
244
|
+
* const fr = sink.buffersinkGetFrameRate();
|
|
245
|
+
* console.log(`Output frame rate: ${fr.num}/${fr.den}`);
|
|
246
|
+
* }
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
get buffersink() {
|
|
250
|
+
return this.buffersinkCtx;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Output frame rate from filter graph.
|
|
254
|
+
*
|
|
255
|
+
* Returns the frame rate determined by the filter graph output.
|
|
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
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Check if filter is ready for processing.
|
|
488
|
+
*
|
|
489
|
+
* @returns true if initialized and ready
|
|
490
|
+
*
|
|
491
|
+
* @example
|
|
492
|
+
* ```typescript
|
|
493
|
+
* if (filter.isReady()) {
|
|
494
|
+
* const output = await filter.process(frame);
|
|
495
|
+
* }
|
|
496
|
+
* ```
|
|
497
|
+
*/
|
|
498
|
+
isReady() {
|
|
499
|
+
return this.initialized && this.buffersrcCtx !== null && this.buffersinkCtx !== null && !this.isClosed;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Get filter graph description.
|
|
503
|
+
*
|
|
504
|
+
* Returns human-readable graph structure.
|
|
505
|
+
* Useful for debugging filter chains.
|
|
506
|
+
*
|
|
507
|
+
* Direct mapping to avfilter_graph_dump().
|
|
508
|
+
*
|
|
509
|
+
* @returns Graph description or null if closed
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* ```typescript
|
|
513
|
+
* const description = filter.getGraphDescription();
|
|
514
|
+
* console.log('Filter graph:', description);
|
|
515
|
+
* ```
|
|
516
|
+
*/
|
|
517
|
+
getGraphDescription() {
|
|
518
|
+
return !this.isClosed && this.initialized ? this.graph.dump() : null;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Send a frame to the filter.
|
|
522
|
+
*
|
|
523
|
+
* Sends a frame to the filter for processing.
|
|
524
|
+
* Does not return filtered frames - use {@link receive} to retrieve frames.
|
|
525
|
+
* On first frame, automatically builds filter graph with frame properties.
|
|
526
|
+
* A single input frame can produce zero, one, or multiple output frames.
|
|
527
|
+
*
|
|
528
|
+
* **Important**: This method only SENDS the frame to the filter.
|
|
529
|
+
* You must call {@link receive} separately (potentially multiple times) to get filtered frames.
|
|
530
|
+
*
|
|
531
|
+
* Direct mapping to av_buffersrc_add_frame().
|
|
532
|
+
*
|
|
533
|
+
* @param frame - Input frame to send to filter, or null to flush
|
|
534
|
+
*
|
|
535
|
+
* @throws {Error} If filter could not be initialized
|
|
536
|
+
*
|
|
537
|
+
* @throws {FFmpegError} If sending frame fails
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* ```typescript
|
|
541
|
+
* // Send frame and receive filtered frames
|
|
542
|
+
* await filter.process(inputFrame);
|
|
543
|
+
*
|
|
544
|
+
* // Receive all available filtered frames
|
|
545
|
+
* while (true) {
|
|
546
|
+
* const output = await filter.receive();
|
|
547
|
+
* if (!output) break;
|
|
548
|
+
* console.log(`Got filtered frame: pts=${output.pts}`);
|
|
549
|
+
* output.free();
|
|
550
|
+
* }
|
|
551
|
+
* ```
|
|
552
|
+
*
|
|
553
|
+
* @example
|
|
554
|
+
* ```typescript
|
|
555
|
+
* for await (const frame of decoder.frames(input.packets())) {
|
|
556
|
+
* // Send frame
|
|
557
|
+
* await filter.process(frame);
|
|
558
|
+
*
|
|
559
|
+
* // Receive available filtered frames
|
|
560
|
+
* let output;
|
|
561
|
+
* while ((output = await filter.receive())) {
|
|
562
|
+
* await encoder.encode(output);
|
|
563
|
+
* output.free();
|
|
564
|
+
* }
|
|
565
|
+
* frame.free();
|
|
566
|
+
* }
|
|
567
|
+
* ```
|
|
568
|
+
*
|
|
569
|
+
* @see {@link receive} For receiving filtered frames
|
|
570
|
+
* @see {@link processAll} For combined send+receive operation
|
|
571
|
+
* @see {@link frames} For processing frame streams
|
|
572
|
+
* @see {@link flush} For end-of-stream handling
|
|
573
|
+
* @see {@link processSync} For synchronous version
|
|
574
|
+
*/
|
|
575
|
+
async process(frame) {
|
|
576
|
+
this.signal?.throwIfAborted();
|
|
577
|
+
if (this.isClosed) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
// Null frame = flush filter
|
|
581
|
+
if (frame === null) {
|
|
582
|
+
await this.flush();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
// Open filter if not already done
|
|
586
|
+
this.initializePromise ??= this.initialize(frame);
|
|
587
|
+
await this.initializePromise;
|
|
588
|
+
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
589
|
+
throw new Error('Could not initialize filter contexts');
|
|
590
|
+
}
|
|
591
|
+
// Check for frame property changes (FFmpeg: dropOnChange/allowReinit logic)
|
|
592
|
+
if (!this.checkFramePropertiesChanged(frame)) {
|
|
593
|
+
// Frame dropped due to property change
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
// If reinitialized, reinitialize now
|
|
597
|
+
if (!this.initialized) {
|
|
598
|
+
this.initializePromise = this.initialize(frame);
|
|
599
|
+
await this.initializePromise;
|
|
600
|
+
}
|
|
601
|
+
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
602
|
+
throw new Error('Could not reinitialize filter contexts');
|
|
603
|
+
}
|
|
604
|
+
// Rescale timestamps to filter's timeBase
|
|
605
|
+
if (this.calculatedTimeBase) {
|
|
606
|
+
const originalTimeBase = frame.timeBase;
|
|
607
|
+
frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
|
|
608
|
+
frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
|
|
609
|
+
frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
|
|
610
|
+
}
|
|
611
|
+
// Send frame to filter with PUSH flag for immediate processing
|
|
612
|
+
// KEEP_REF preserves the input frame's hw_frames_ctx for reuse across multiple filters
|
|
613
|
+
const addRet = await this.buffersrcCtx.buffersrcAddFrame(frame, (AV_BUFFERSRC_FLAG_PUSH | AV_BUFFERSRC_FLAG_KEEP_REF));
|
|
614
|
+
FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Send a frame to the filter synchronously.
|
|
618
|
+
* Synchronous version of process.
|
|
619
|
+
*
|
|
620
|
+
* Sends a frame to the filter for processing.
|
|
621
|
+
* Does not return filtered frames - use {@link receiveSync} to retrieve frames.
|
|
622
|
+
* On first frame, automatically builds filter graph with frame properties.
|
|
623
|
+
* A single input frame can produce zero, one, or multiple output frames.
|
|
624
|
+
*
|
|
625
|
+
* **Important**: This method only SENDS the frame to the filter.
|
|
626
|
+
* You must call {@link receiveSync} separately (potentially multiple times) to get filtered frames.
|
|
627
|
+
*
|
|
628
|
+
* Direct mapping to av_buffersrc_add_frame().
|
|
629
|
+
*
|
|
630
|
+
* @param frame - Input frame to send to filter, or null to flush
|
|
631
|
+
*
|
|
632
|
+
* @throws {Error} If filter could not be initialized
|
|
633
|
+
*
|
|
634
|
+
* @throws {FFmpegError} If sending frame fails
|
|
635
|
+
*
|
|
636
|
+
* @example
|
|
637
|
+
* ```typescript
|
|
638
|
+
* // Send frame and receive filtered frames
|
|
639
|
+
* filter.processSync(inputFrame);
|
|
640
|
+
*
|
|
641
|
+
* // Receive all available filtered frames
|
|
642
|
+
* let output;
|
|
643
|
+
* while ((output = filter.receiveSync())) {
|
|
644
|
+
* console.log(`Got filtered frame: pts=${output.pts}`);
|
|
645
|
+
* output.free();
|
|
646
|
+
* }
|
|
647
|
+
* ```
|
|
648
|
+
*
|
|
649
|
+
* @see {@link receiveSync} For receiving filtered frames
|
|
650
|
+
* @see {@link processAllSync} For combined send+receive operation
|
|
651
|
+
* @see {@link framesSync} For processing frame streams
|
|
652
|
+
* @see {@link flushSync} For end-of-stream handling
|
|
653
|
+
* @see {@link process} For async version
|
|
654
|
+
*/
|
|
655
|
+
processSync(frame) {
|
|
656
|
+
if (this.isClosed) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
// Null frame = flush filter
|
|
660
|
+
if (frame === null) {
|
|
661
|
+
this.flushSync();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
// Open filter if not already done
|
|
665
|
+
if (!this.initialized) {
|
|
666
|
+
this.initializeSync(frame);
|
|
667
|
+
}
|
|
668
|
+
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
669
|
+
throw new Error('Could not initialize filter contexts');
|
|
670
|
+
}
|
|
671
|
+
// Check for frame property changes (FFmpeg: dropOnChange/allowReinit logic)
|
|
672
|
+
if (!this.checkFramePropertiesChanged(frame)) {
|
|
673
|
+
// Frame dropped due to property change
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
// If reinitialized, reinitialize now
|
|
677
|
+
if (!this.initialized) {
|
|
678
|
+
this.initializeSync(frame);
|
|
679
|
+
}
|
|
680
|
+
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
681
|
+
throw new Error('Could not reinitialize filter contexts');
|
|
682
|
+
}
|
|
683
|
+
// Rescale timestamps to filter's timeBase
|
|
684
|
+
if (this.calculatedTimeBase) {
|
|
685
|
+
const originalTimeBase = frame.timeBase;
|
|
686
|
+
frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
|
|
687
|
+
frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
|
|
688
|
+
frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
|
|
689
|
+
}
|
|
690
|
+
// Send frame to filter with PUSH flag for immediate processing
|
|
691
|
+
// KEEP_REF preserves the input frame's hw_frames_ctx for reuse across multiple filters
|
|
692
|
+
const addRet = this.buffersrcCtx.buffersrcAddFrameSync(frame, (AV_BUFFERSRC_FLAG_PUSH | AV_BUFFERSRC_FLAG_KEEP_REF));
|
|
693
|
+
FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Process a frame through the filter.
|
|
697
|
+
*
|
|
698
|
+
* Applies filter operations to input frame and receives all available output frames.
|
|
699
|
+
* Returns array of frames - may be empty if filter needs more input.
|
|
700
|
+
* On first frame, automatically builds filter graph with frame properties.
|
|
701
|
+
* One input frame can produce zero, one, or multiple output frames depending on filter.
|
|
702
|
+
* Hardware frames context is automatically detected from frame.
|
|
703
|
+
*
|
|
704
|
+
* Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
|
|
705
|
+
*
|
|
706
|
+
* @param frame - Input frame to process
|
|
707
|
+
*
|
|
708
|
+
* @returns Array of filtered frames (empty if buffered or filter closed)
|
|
709
|
+
*
|
|
710
|
+
* @throws {Error} If filter could not be initialized
|
|
711
|
+
*
|
|
712
|
+
* @throws {FFmpegError} If processing fails
|
|
713
|
+
*
|
|
714
|
+
* @example
|
|
715
|
+
* ```typescript
|
|
716
|
+
* const frames = await filter.processAll(inputFrame);
|
|
717
|
+
* for (const output of frames) {
|
|
718
|
+
* console.log(`Got filtered frame: pts=${output.pts}`);
|
|
719
|
+
* output.free();
|
|
720
|
+
* }
|
|
721
|
+
* ```
|
|
722
|
+
*
|
|
723
|
+
* @example
|
|
724
|
+
* ```typescript
|
|
725
|
+
* // Process frame - may return multiple frames (e.g. fps filter)
|
|
726
|
+
* const frames = await filter.processAll(frame);
|
|
727
|
+
* for (const output of frames) {
|
|
728
|
+
* yield output;
|
|
729
|
+
* }
|
|
730
|
+
* ```
|
|
731
|
+
*
|
|
732
|
+
* @see {@link process} For single frame processing
|
|
733
|
+
* @see {@link frames} For processing frame streams
|
|
734
|
+
* @see {@link flush} For end-of-stream handling
|
|
735
|
+
* @see {@link processAllSync} For synchronous version
|
|
736
|
+
*/
|
|
737
|
+
async processAll(frame) {
|
|
738
|
+
this.signal?.throwIfAborted();
|
|
739
|
+
await this.process(frame);
|
|
740
|
+
// Receive all available frames
|
|
741
|
+
const frames = [];
|
|
742
|
+
while (true) {
|
|
743
|
+
const outputFrame = await this.receive();
|
|
744
|
+
if (!outputFrame)
|
|
745
|
+
break; // Stop on EAGAIN or EOF
|
|
746
|
+
frames.push(outputFrame); // Only push actual frames
|
|
747
|
+
}
|
|
748
|
+
return frames;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Process a frame through the filter synchronously.
|
|
752
|
+
* Synchronous version of processAll.
|
|
753
|
+
*
|
|
754
|
+
* Applies filter operations to input frame and receives all available output frames.
|
|
755
|
+
* Returns array of frames - may be empty if filter needs more input.
|
|
756
|
+
* On first frame, automatically builds filter graph with frame properties.
|
|
757
|
+
* One input frame can produce zero, one, or multiple output frames depending on filter.
|
|
758
|
+
* Hardware frames context is automatically detected from frame.
|
|
759
|
+
*
|
|
760
|
+
* Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
|
|
761
|
+
*
|
|
762
|
+
* @param frame - Input frame to process
|
|
763
|
+
*
|
|
764
|
+
* @returns Array of filtered frames (empty if buffered or filter closed)
|
|
765
|
+
*
|
|
766
|
+
* @throws {Error} If filter could not be initialized
|
|
767
|
+
*
|
|
768
|
+
* @throws {FFmpegError} If processing fails
|
|
769
|
+
*
|
|
770
|
+
* @example
|
|
771
|
+
* ```typescript
|
|
772
|
+
* const outputs = filter.processAllSync(inputFrame);
|
|
773
|
+
* for (const output of outputs) {
|
|
774
|
+
* console.log(`Got filtered frame: pts=${output.pts}`);
|
|
775
|
+
* output.free();
|
|
776
|
+
* }
|
|
777
|
+
* ```
|
|
778
|
+
*
|
|
779
|
+
* @example
|
|
780
|
+
* ```typescript
|
|
781
|
+
* // Process frame - may return multiple frames (e.g. fps filter)
|
|
782
|
+
* const outputs = filter.processAllSync(frame);
|
|
783
|
+
* for (const output of outputs) {
|
|
784
|
+
* yield output;
|
|
785
|
+
* }
|
|
786
|
+
* ```
|
|
787
|
+
*
|
|
788
|
+
* @see {@link processSync} For single frame processing
|
|
789
|
+
* @see {@link framesSync} For processing frame streams
|
|
790
|
+
* @see {@link flushSync} For end-of-stream handling
|
|
791
|
+
* @see {@link process} For async version
|
|
792
|
+
*/
|
|
793
|
+
processAllSync(frame) {
|
|
794
|
+
this.processSync(frame);
|
|
795
|
+
// Receive all available frames
|
|
796
|
+
const frames = [];
|
|
797
|
+
while (true) {
|
|
798
|
+
const outputFrame = this.receiveSync();
|
|
799
|
+
if (!outputFrame)
|
|
800
|
+
break; // Stop on EAGAIN or EOF
|
|
801
|
+
frames.push(outputFrame); // Only push actual frames
|
|
802
|
+
}
|
|
803
|
+
return frames;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Process frame stream through filter.
|
|
807
|
+
*
|
|
808
|
+
* High-level async generator for filtering frame streams.
|
|
809
|
+
* Filter is only flushed when EOF (null) signal is explicitly received.
|
|
810
|
+
* Primary interface for stream-based filtering.
|
|
811
|
+
*
|
|
812
|
+
* **EOF Handling:**
|
|
813
|
+
* - Send null to flush filter and get remaining buffered frames
|
|
814
|
+
* - Generator yields null after flushing when null is received
|
|
815
|
+
* - No automatic flushing - filter stays open until EOF or close()
|
|
816
|
+
*
|
|
817
|
+
* @param frames - Async iterable of frames, single frame, or null to flush
|
|
818
|
+
*
|
|
819
|
+
* @yields {Frame | null} Filtered frames, followed by null when explicitly flushed
|
|
820
|
+
*
|
|
821
|
+
* @throws {Error} If filter not ready
|
|
822
|
+
*
|
|
823
|
+
* @throws {FFmpegError} If processing fails
|
|
824
|
+
*
|
|
825
|
+
* @example
|
|
826
|
+
* ```typescript
|
|
827
|
+
* // Stream of frames with automatic EOF propagation
|
|
828
|
+
* for await (const frame of filter.frames(decoder.frames(packets))) {
|
|
829
|
+
* if (frame === null) {
|
|
830
|
+
* console.log('Filter flushed');
|
|
831
|
+
* break;
|
|
832
|
+
* }
|
|
833
|
+
* await encoder.encode(frame);
|
|
834
|
+
* frame.free();
|
|
835
|
+
* }
|
|
836
|
+
* ```
|
|
837
|
+
*
|
|
838
|
+
* @example
|
|
839
|
+
* ```typescript
|
|
840
|
+
* // Single frame - no automatic flush
|
|
841
|
+
* for await (const frame of filter.frames(singleFrame)) {
|
|
842
|
+
* await encoder.encode(frame);
|
|
843
|
+
* frame.free();
|
|
844
|
+
* }
|
|
845
|
+
* // Filter remains open, buffered frames not flushed
|
|
846
|
+
* ```
|
|
847
|
+
*
|
|
848
|
+
* @example
|
|
849
|
+
* ```typescript
|
|
850
|
+
* // Explicit flush with EOF
|
|
851
|
+
* for await (const frame of filter.frames(null)) {
|
|
852
|
+
* if (frame === null) {
|
|
853
|
+
* console.log('All buffered frames flushed');
|
|
854
|
+
* break;
|
|
855
|
+
* }
|
|
856
|
+
* console.log('Buffered frame:', frame.pts);
|
|
857
|
+
* frame.free();
|
|
858
|
+
* }
|
|
859
|
+
* ```
|
|
860
|
+
*
|
|
861
|
+
* @see {@link process} For single frame processing
|
|
862
|
+
* @see {@link Decoder.frames} For frames source
|
|
863
|
+
* @see {@link framesSync} For sync version
|
|
864
|
+
*/
|
|
865
|
+
async *frames(frames) {
|
|
866
|
+
const self = this;
|
|
867
|
+
const processFrame = async function* (frame) {
|
|
868
|
+
await self.process(frame);
|
|
869
|
+
while (true) {
|
|
870
|
+
const filtered = await self.receive();
|
|
871
|
+
if (!filtered)
|
|
872
|
+
break;
|
|
873
|
+
yield filtered;
|
|
874
|
+
}
|
|
875
|
+
}.bind(this);
|
|
876
|
+
const finalize = async function* () {
|
|
877
|
+
for await (const remaining of self.flushFrames()) {
|
|
878
|
+
yield remaining;
|
|
879
|
+
}
|
|
880
|
+
yield null;
|
|
881
|
+
}.bind(this);
|
|
882
|
+
if (frames === null) {
|
|
883
|
+
yield* finalize();
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (frames instanceof Frame) {
|
|
887
|
+
yield* processFrame(frames);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
for await (const frame_1 of frames) {
|
|
891
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
892
|
+
try {
|
|
893
|
+
const frame = __addDisposableResource(env_1, frame_1, false);
|
|
894
|
+
this.signal?.throwIfAborted();
|
|
895
|
+
if (frame === null) {
|
|
896
|
+
yield* finalize();
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
yield* processFrame(frame);
|
|
900
|
+
}
|
|
901
|
+
catch (e_1) {
|
|
902
|
+
env_1.error = e_1;
|
|
903
|
+
env_1.hasError = true;
|
|
904
|
+
}
|
|
905
|
+
finally {
|
|
906
|
+
__disposeResources(env_1);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Process frame stream through filter synchronously.
|
|
912
|
+
* Synchronous version of frames.
|
|
913
|
+
*
|
|
914
|
+
* High-level sync generator for filtering frame streams.
|
|
915
|
+
* Filter is only flushed when EOF (null) signal is explicitly received.
|
|
916
|
+
* Primary interface for stream-based filtering.
|
|
917
|
+
*
|
|
918
|
+
* **EOF Handling:**
|
|
919
|
+
* - Send null to flush filter and get remaining buffered frames
|
|
920
|
+
* - Generator yields null after flushing when null is received
|
|
921
|
+
* - No automatic flushing - filter stays open until EOF or close()
|
|
922
|
+
*
|
|
923
|
+
* @param frames - Iterable of frames, single frame, or null to flush
|
|
924
|
+
*
|
|
925
|
+
* @yields {Frame | null} Filtered frames, followed by null when explicitly flushed
|
|
926
|
+
*
|
|
927
|
+
* @throws {Error} If filter not ready
|
|
928
|
+
*
|
|
929
|
+
* @throws {FFmpegError} If processing fails
|
|
930
|
+
*
|
|
931
|
+
* @example
|
|
932
|
+
* ```typescript
|
|
933
|
+
* // Stream of frames with automatic EOF propagation
|
|
934
|
+
* for (const frame of filter.framesSync(decoder.framesSync(packets))) {
|
|
935
|
+
* if (frame === null) {
|
|
936
|
+
* console.log('Filter flushed');
|
|
937
|
+
* break;
|
|
938
|
+
* }
|
|
939
|
+
* encoder.encodeSync(frame);
|
|
940
|
+
* frame.free();
|
|
941
|
+
* }
|
|
942
|
+
* ```
|
|
943
|
+
*
|
|
944
|
+
* @example
|
|
945
|
+
* ```typescript
|
|
946
|
+
* // Single frame - no automatic flush
|
|
947
|
+
* for (const frame of filter.framesSync(singleFrame)) {
|
|
948
|
+
* encoder.encodeSync(frame);
|
|
949
|
+
* frame.free();
|
|
950
|
+
* }
|
|
951
|
+
* // Filter remains open, buffered frames not flushed
|
|
952
|
+
* ```
|
|
953
|
+
*
|
|
954
|
+
* @example
|
|
955
|
+
* ```typescript
|
|
956
|
+
* // Explicit flush with EOF
|
|
957
|
+
* for (const frame of filter.framesSync(null)) {
|
|
958
|
+
* if (frame === null) {
|
|
959
|
+
* console.log('All buffered frames flushed');
|
|
960
|
+
* break;
|
|
961
|
+
* }
|
|
962
|
+
* console.log('Buffered frame:', frame.pts);
|
|
963
|
+
* frame.free();
|
|
964
|
+
* }
|
|
965
|
+
* ```
|
|
966
|
+
*
|
|
967
|
+
* @see {@link processSync} For single frame processing
|
|
968
|
+
* @see {@link Decoder.framesSync} For frames source
|
|
969
|
+
* @see {@link frames} For async version
|
|
970
|
+
*/
|
|
971
|
+
*framesSync(frames) {
|
|
972
|
+
const self = this;
|
|
973
|
+
// Helper: Process frame and yield all available filtered frames (filters out EAGAIN nulls)
|
|
974
|
+
const processFrame = function* (frame) {
|
|
975
|
+
self.processSync(frame);
|
|
976
|
+
// Receive ALL filtered frames (filter out null/EAGAIN)
|
|
977
|
+
while (true) {
|
|
978
|
+
const filtered = self.receiveSync();
|
|
979
|
+
if (!filtered)
|
|
980
|
+
break; // EAGAIN or EOF - no more frames available
|
|
981
|
+
yield filtered; // Only yield actual frames, not null
|
|
982
|
+
}
|
|
983
|
+
}.bind(this);
|
|
984
|
+
// Helper: Flush filter and signal EOF
|
|
985
|
+
const finalize = function* () {
|
|
986
|
+
for (const remaining of self.flushFramesSync()) {
|
|
987
|
+
yield remaining; // Only yield actual frames
|
|
988
|
+
}
|
|
989
|
+
yield null; // Signal end-of-stream
|
|
990
|
+
}.bind(this);
|
|
991
|
+
// Case 1: EOF input -> flush only
|
|
992
|
+
if (frames === null) {
|
|
993
|
+
yield* finalize();
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
// Case 2: Single frame
|
|
997
|
+
if (frames instanceof Frame) {
|
|
998
|
+
yield* processFrame(frames);
|
|
999
|
+
// No automatic flush - only flush on explicit EOF
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
// Case 3: Iterable of frames
|
|
1003
|
+
for (const frame_2 of frames) {
|
|
1004
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
1005
|
+
try {
|
|
1006
|
+
const frame = __addDisposableResource(env_2, frame_2, false);
|
|
1007
|
+
// Check for EOF signal from upstream
|
|
1008
|
+
if (frame === null) {
|
|
1009
|
+
yield* finalize();
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
yield* processFrame(frame);
|
|
1013
|
+
}
|
|
1014
|
+
catch (e_2) {
|
|
1015
|
+
env_2.error = e_2;
|
|
1016
|
+
env_2.hasError = true;
|
|
1017
|
+
}
|
|
1018
|
+
finally {
|
|
1019
|
+
__disposeResources(env_2);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
// No automatic flush - only flush on explicit EOF
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Flush filter and signal end-of-stream.
|
|
1026
|
+
*
|
|
1027
|
+
* Sends null frame to flush buffered data.
|
|
1028
|
+
* Must call receive() to get flushed frames.
|
|
1029
|
+
* Does nothing if filter is closed or was never initialized.
|
|
1030
|
+
*
|
|
1031
|
+
* Direct mapping to av_buffersrc_add_frame(NULL).
|
|
1032
|
+
*
|
|
1033
|
+
* @throws {FFmpegError} If flush fails
|
|
1034
|
+
*
|
|
1035
|
+
* @example
|
|
1036
|
+
* ```typescript
|
|
1037
|
+
* await filter.flush();
|
|
1038
|
+
* // Get remaining frames
|
|
1039
|
+
* let frame;
|
|
1040
|
+
* while ((frame = await filter.receive()) !== null) {
|
|
1041
|
+
* frame.free();
|
|
1042
|
+
* }
|
|
1043
|
+
* ```
|
|
1044
|
+
*
|
|
1045
|
+
* @see {@link flushFrames} For async iteration
|
|
1046
|
+
* @see {@link receive} For getting flushed frames
|
|
1047
|
+
* @see {@link flushSync} For synchronous version
|
|
1048
|
+
*/
|
|
1049
|
+
async flush() {
|
|
1050
|
+
this.signal?.throwIfAborted();
|
|
1051
|
+
if (this.isClosed || !this.initialized || !this.buffersrcCtx) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
// Send flush frame (null)
|
|
1055
|
+
const ret = await this.buffersrcCtx.buffersrcAddFrame(null, AV_BUFFERSRC_FLAG_PUSH);
|
|
1056
|
+
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
1057
|
+
FFmpegError.throwIfError(ret, 'Failed to flush filter');
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Flush filter and signal end-of-stream synchronously.
|
|
1062
|
+
* Synchronous version of flush.
|
|
1063
|
+
*
|
|
1064
|
+
* Sends null frame to flush buffered data.
|
|
1065
|
+
* Must call receiveSync() to get flushed frames.
|
|
1066
|
+
* Does nothing if filter is closed or was never initialized.
|
|
1067
|
+
*
|
|
1068
|
+
* Direct mapping to av_buffersrc_add_frame(NULL).
|
|
1069
|
+
*
|
|
1070
|
+
* @throws {FFmpegError} If flush fails
|
|
1071
|
+
*
|
|
1072
|
+
* @example
|
|
1073
|
+
* ```typescript
|
|
1074
|
+
* filter.flushSync();
|
|
1075
|
+
* // Get remaining frames
|
|
1076
|
+
* let frame;
|
|
1077
|
+
* while ((frame = filter.receiveSync()) !== null) {
|
|
1078
|
+
* frame.free();
|
|
1079
|
+
* }
|
|
1080
|
+
* ```
|
|
1081
|
+
*
|
|
1082
|
+
* @see {@link flushFramesSync} For sync iteration
|
|
1083
|
+
* @see {@link receiveSync} For getting flushed frames
|
|
1084
|
+
* @see {@link flush} For async version
|
|
1085
|
+
*/
|
|
1086
|
+
flushSync() {
|
|
1087
|
+
if (this.isClosed || !this.initialized || !this.buffersrcCtx) {
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
// Send flush frame (null)
|
|
1091
|
+
const ret = this.buffersrcCtx.buffersrcAddFrameSync(null, AV_BUFFERSRC_FLAG_PUSH);
|
|
1092
|
+
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
1093
|
+
FFmpegError.throwIfError(ret, 'Failed to flush filter');
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Flush filter and yield remaining frames.
|
|
1098
|
+
*
|
|
1099
|
+
* Convenient async generator for flushing.
|
|
1100
|
+
* Combines flush and receive operations.
|
|
1101
|
+
* Returns immediately if filter is closed or was never initialized.
|
|
1102
|
+
*
|
|
1103
|
+
* @yields {Frame} Remaining frames from filter
|
|
1104
|
+
*
|
|
1105
|
+
* @throws {FFmpegError} If flush fails
|
|
1106
|
+
*
|
|
1107
|
+
* @example
|
|
1108
|
+
* ```typescript
|
|
1109
|
+
* for await (const frame of filter.flushFrames()) {
|
|
1110
|
+
* console.log(`Flushed frame: pts=${frame.pts}`);
|
|
1111
|
+
* frame.free();
|
|
1112
|
+
* }
|
|
1113
|
+
* ```
|
|
1114
|
+
*
|
|
1115
|
+
* @see {@link process} For frame processing
|
|
1116
|
+
* @see {@link flush} For manual flush
|
|
1117
|
+
* @see {@link flushFramesSync} For sync version
|
|
1118
|
+
*/
|
|
1119
|
+
async *flushFrames() {
|
|
1120
|
+
// Send flush signal
|
|
1121
|
+
await this.flush();
|
|
1122
|
+
// Yield all remaining frames (filter out null/EAGAIN and EOF)
|
|
1123
|
+
while (true) {
|
|
1124
|
+
const frame = await this.receive();
|
|
1125
|
+
if (!frame)
|
|
1126
|
+
break; // Stop on EAGAIN or EOF
|
|
1127
|
+
yield frame; // Only yield actual frames
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Flush filter and yield remaining frames synchronously.
|
|
1132
|
+
* Synchronous version of flushFrames.
|
|
1133
|
+
*
|
|
1134
|
+
* Convenient sync generator for flushing.
|
|
1135
|
+
* Combines flush and receive operations.
|
|
1136
|
+
* Returns immediately if filter is closed or was never initialized.
|
|
1137
|
+
*
|
|
1138
|
+
* @yields {Frame} Remaining frames from filter
|
|
1139
|
+
*
|
|
1140
|
+
* @throws {FFmpegError} If flush fails
|
|
1141
|
+
*
|
|
1142
|
+
* @example
|
|
1143
|
+
* ```typescript
|
|
1144
|
+
* for (const frame of filter.flushFramesSync()) {
|
|
1145
|
+
* console.log(`Flushed frame: pts=${frame.pts}`);
|
|
1146
|
+
* frame.free();
|
|
1147
|
+
* }
|
|
1148
|
+
* ```
|
|
1149
|
+
*
|
|
1150
|
+
* @see {@link processSync} For frame processing
|
|
1151
|
+
* @see {@link flushSync} For manual flush
|
|
1152
|
+
* @see {@link flushFrames} For async version
|
|
1153
|
+
*/
|
|
1154
|
+
*flushFramesSync() {
|
|
1155
|
+
// Send flush signal
|
|
1156
|
+
this.flushSync();
|
|
1157
|
+
// Yield all remaining frames (filter out null/EAGAIN and EOF)
|
|
1158
|
+
while (true) {
|
|
1159
|
+
const frame = this.receiveSync();
|
|
1160
|
+
if (!frame)
|
|
1161
|
+
break; // Stop on EAGAIN or EOF
|
|
1162
|
+
yield frame; // Only yield actual frames
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Receive buffered frame from filter.
|
|
1167
|
+
*
|
|
1168
|
+
* Drains frames buffered by the filter.
|
|
1169
|
+
* Call repeatedly until null or EOF to get all buffered frames.
|
|
1170
|
+
* Implements FFmpeg's send/receive pattern.
|
|
1171
|
+
*
|
|
1172
|
+
* **Return Values:**
|
|
1173
|
+
* - `Frame` - Successfully received frame (AVERROR >= 0)
|
|
1174
|
+
* - `null` - Need more input data (AVERROR_EAGAIN), or filter not initialized
|
|
1175
|
+
* - `undefined` - End of stream reached (AVERROR_EOF), or filter is closed
|
|
1176
|
+
*
|
|
1177
|
+
* Direct mapping to av_buffersink_get_frame().
|
|
1178
|
+
*
|
|
1179
|
+
* @returns Buffered frame, null if need more data, or undefined if stream ended
|
|
1180
|
+
*
|
|
1181
|
+
* @throws {FFmpegError} If receiving fails
|
|
1182
|
+
*
|
|
1183
|
+
* @throws {Error} If frame cloning fails (out of memory)
|
|
1184
|
+
*
|
|
1185
|
+
* @example
|
|
1186
|
+
* ```typescript
|
|
1187
|
+
* // Process all buffered frames
|
|
1188
|
+
* while (true) {
|
|
1189
|
+
* const frame = await filter.receive();
|
|
1190
|
+
* if (!frame) break; // Stop on EAGAIN or EOF
|
|
1191
|
+
* console.log(`Received frame: pts=${frame.pts}`);
|
|
1192
|
+
* frame.free();
|
|
1193
|
+
* }
|
|
1194
|
+
* ```
|
|
1195
|
+
*
|
|
1196
|
+
* @example
|
|
1197
|
+
* ```typescript
|
|
1198
|
+
* // Handle each return value explicitly
|
|
1199
|
+
* const frame = await filter.receive();
|
|
1200
|
+
* if (frame === EOF) {
|
|
1201
|
+
* console.log('Filter stream ended');
|
|
1202
|
+
* } else if (frame === null) {
|
|
1203
|
+
* console.log('Need more input data');
|
|
1204
|
+
* } else {
|
|
1205
|
+
* console.log(`Got frame: pts=${frame.pts}`);
|
|
1206
|
+
* frame.free();
|
|
1207
|
+
* }
|
|
1208
|
+
* ```
|
|
1209
|
+
*
|
|
1210
|
+
* @see {@link process} For frame processing
|
|
1211
|
+
* @see {@link flush} For flushing filter
|
|
1212
|
+
* @see {@link receiveSync} For synchronous version
|
|
1213
|
+
* @see {@link EOF} For end-of-stream signal
|
|
1214
|
+
*/
|
|
1215
|
+
async receive() {
|
|
1216
|
+
if (this.isClosed) {
|
|
1217
|
+
return EOF;
|
|
1218
|
+
}
|
|
1219
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
// Reuse frame - but alloc() instead of unref() for buffersink
|
|
1223
|
+
// buffersink needs a fresh allocated frame, not an unreferenced one
|
|
1224
|
+
this.frame.alloc();
|
|
1225
|
+
const ret = await this.buffersinkCtx.buffersinkGetFrame(this.frame);
|
|
1226
|
+
if (ret >= 0) {
|
|
1227
|
+
// Post-process output frame (set timeBase from buffersink, calculate duration)
|
|
1228
|
+
this.postProcessOutputFrame(this.frame);
|
|
1229
|
+
// Clone for user (keeps internal frame for reuse)
|
|
1230
|
+
const cloned = this.frame.clone();
|
|
1231
|
+
if (!cloned) {
|
|
1232
|
+
throw new Error('Failed to clone frame (out of memory)');
|
|
1233
|
+
}
|
|
1234
|
+
return cloned;
|
|
1235
|
+
}
|
|
1236
|
+
else if (ret === AVERROR_EAGAIN) {
|
|
1237
|
+
// Need more data
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
else if (ret === AVERROR_EOF) {
|
|
1241
|
+
// End of stream
|
|
1242
|
+
return EOF;
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
FFmpegError.throwIfError(ret, 'Failed to receive frame from filter');
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Receive buffered frame from filter synchronously.
|
|
1251
|
+
* Synchronous version of receive.
|
|
1252
|
+
*
|
|
1253
|
+
* Drains frames buffered by the filter.
|
|
1254
|
+
* Call repeatedly until null or EOF to get all buffered frames.
|
|
1255
|
+
* Implements FFmpeg's send/receive pattern.
|
|
1256
|
+
*
|
|
1257
|
+
* **Return Values:**
|
|
1258
|
+
* - `Frame` - Successfully received frame (AVERROR >= 0)
|
|
1259
|
+
* - `null` - Need more input data (AVERROR_EAGAIN), or filter not initialized
|
|
1260
|
+
* - `undefined` - End of stream reached (AVERROR_EOF), or filter is closed
|
|
1261
|
+
*
|
|
1262
|
+
* Direct mapping to av_buffersink_get_frame().
|
|
1263
|
+
*
|
|
1264
|
+
* @returns Buffered frame, null if need more data, or undefined if stream ended
|
|
1265
|
+
*
|
|
1266
|
+
* @throws {FFmpegError} If receiving fails
|
|
1267
|
+
*
|
|
1268
|
+
* @throws {Error} If frame cloning fails (out of memory)
|
|
1269
|
+
*
|
|
1270
|
+
* @example
|
|
1271
|
+
* ```typescript
|
|
1272
|
+
* // Process all buffered frames
|
|
1273
|
+
* while (true) {
|
|
1274
|
+
* const frame = filter.receiveSync();
|
|
1275
|
+
* if (!frame) break; // Stop on EAGAIN or EOF
|
|
1276
|
+
* console.log(`Received frame: pts=${frame.pts}`);
|
|
1277
|
+
* frame.free();
|
|
1278
|
+
* }
|
|
1279
|
+
* ```
|
|
1280
|
+
*
|
|
1281
|
+
* @example
|
|
1282
|
+
* ```typescript
|
|
1283
|
+
* // Handle each return value explicitly
|
|
1284
|
+
* const frame = filter.receiveSync();
|
|
1285
|
+
* if (frame === EOF) {
|
|
1286
|
+
* console.log('Filter stream ended');
|
|
1287
|
+
* } else if (frame === null) {
|
|
1288
|
+
* console.log('Need more input data');
|
|
1289
|
+
* } else {
|
|
1290
|
+
* console.log(`Got frame: pts=${frame.pts}`);
|
|
1291
|
+
* frame.free();
|
|
1292
|
+
* }
|
|
1293
|
+
* ```
|
|
1294
|
+
*
|
|
1295
|
+
* @see {@link processSync} For frame processing
|
|
1296
|
+
* @see {@link flushSync} For flushing filter
|
|
1297
|
+
* @see {@link receive} For async version
|
|
1298
|
+
* @see {@link EOF} For end-of-stream signal
|
|
1299
|
+
*/
|
|
1300
|
+
receiveSync() {
|
|
1301
|
+
if (this.isClosed) {
|
|
1302
|
+
return EOF;
|
|
1303
|
+
}
|
|
1304
|
+
if (!this.initialized || !this.buffersinkCtx) {
|
|
1305
|
+
return null;
|
|
1306
|
+
}
|
|
1307
|
+
// Reuse frame - but alloc() instead of unref() for buffersink
|
|
1308
|
+
// buffersink needs a fresh allocated frame, not an unreferenced one
|
|
1309
|
+
this.frame.alloc();
|
|
1310
|
+
const ret = this.buffersinkCtx.buffersinkGetFrameSync(this.frame);
|
|
1311
|
+
if (ret >= 0) {
|
|
1312
|
+
// Post-process output frame (set timeBase from buffersink, calculate duration)
|
|
1313
|
+
this.postProcessOutputFrame(this.frame);
|
|
1314
|
+
// Clone for user (keeps internal frame for reuse)
|
|
1315
|
+
const cloned = this.frame.clone();
|
|
1316
|
+
if (!cloned) {
|
|
1317
|
+
throw new Error('Failed to clone frame (out of memory)');
|
|
1318
|
+
}
|
|
1319
|
+
return cloned;
|
|
1320
|
+
}
|
|
1321
|
+
else if (ret === AVERROR_EAGAIN) {
|
|
1322
|
+
return null; // Need more data
|
|
1323
|
+
}
|
|
1324
|
+
else if (ret === AVERROR_EOF) {
|
|
1325
|
+
return EOF; // End of stream
|
|
1326
|
+
}
|
|
1327
|
+
else {
|
|
1328
|
+
FFmpegError.throwIfError(ret, 'Failed to receive frame from filter');
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Send command to filter.
|
|
1334
|
+
*
|
|
1335
|
+
* Sends runtime command to specific filter in graph.
|
|
1336
|
+
* Allows dynamic parameter adjustment.
|
|
1337
|
+
*
|
|
1338
|
+
* Direct mapping to avfilter_graph_send_command().
|
|
1339
|
+
*
|
|
1340
|
+
* @param target - Target filter name
|
|
1341
|
+
*
|
|
1342
|
+
* @param cmd - Command name
|
|
1343
|
+
*
|
|
1344
|
+
* @param arg - Command argument
|
|
1345
|
+
*
|
|
1346
|
+
* @param flags - Command flags
|
|
1347
|
+
*
|
|
1348
|
+
* @returns Response string from filter
|
|
1349
|
+
*
|
|
1350
|
+
* @throws {Error} If filter not ready
|
|
1351
|
+
*
|
|
1352
|
+
* @throws {FFmpegError} If command fails
|
|
1353
|
+
*
|
|
1354
|
+
* @example
|
|
1355
|
+
* ```typescript
|
|
1356
|
+
* // Change volume at runtime
|
|
1357
|
+
* const response = filter.sendCommand('volume', 'volume', '0.5');
|
|
1358
|
+
* console.log(`Volume changed: ${response}`);
|
|
1359
|
+
* ```
|
|
1360
|
+
*
|
|
1361
|
+
* @see {@link queueCommand} For delayed commands
|
|
1362
|
+
*/
|
|
1363
|
+
sendCommand(target, cmd, arg, flags) {
|
|
1364
|
+
if (this.isClosed) {
|
|
1365
|
+
throw new Error('Filter is closed');
|
|
1366
|
+
}
|
|
1367
|
+
if (!this.initialized) {
|
|
1368
|
+
throw new Error('Filter not initialized');
|
|
1369
|
+
}
|
|
1370
|
+
const result = this.graph.sendCommand(target, cmd, arg, flags);
|
|
1371
|
+
if (typeof result === 'number') {
|
|
1372
|
+
FFmpegError.throwIfError(result, 'Failed to send filter command');
|
|
1373
|
+
return '';
|
|
1374
|
+
}
|
|
1375
|
+
return result.response ?? '';
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Queue command for later execution.
|
|
1379
|
+
*
|
|
1380
|
+
* Schedules command to execute at specific timestamp.
|
|
1381
|
+
* Useful for synchronized parameter changes.
|
|
1382
|
+
*
|
|
1383
|
+
* Direct mapping to avfilter_graph_queue_command().
|
|
1384
|
+
*
|
|
1385
|
+
* @param target - Target filter name
|
|
1386
|
+
*
|
|
1387
|
+
* @param cmd - Command name
|
|
1388
|
+
*
|
|
1389
|
+
* @param arg - Command argument
|
|
1390
|
+
*
|
|
1391
|
+
* @param ts - Timestamp for execution
|
|
1392
|
+
*
|
|
1393
|
+
* @param flags - Command flags
|
|
1394
|
+
*
|
|
1395
|
+
* @throws {Error} If filter not ready
|
|
1396
|
+
*
|
|
1397
|
+
* @throws {FFmpegError} If queue fails
|
|
1398
|
+
*
|
|
1399
|
+
* @example
|
|
1400
|
+
* ```typescript
|
|
1401
|
+
* // Queue volume change at 10 seconds
|
|
1402
|
+
* filter.queueCommand('volume', 'volume', '0.8', 10.0);
|
|
1403
|
+
* ```
|
|
1404
|
+
*
|
|
1405
|
+
* @see {@link sendCommand} For immediate commands
|
|
1406
|
+
*/
|
|
1407
|
+
queueCommand(target, cmd, arg, ts, flags) {
|
|
1408
|
+
if (this.isClosed) {
|
|
1409
|
+
throw new Error('Filter is closed');
|
|
1410
|
+
}
|
|
1411
|
+
if (!this.initialized) {
|
|
1412
|
+
throw new Error('Filter not initialized');
|
|
1413
|
+
}
|
|
1414
|
+
const ret = this.graph.queueCommand(target, cmd, arg, ts, flags);
|
|
1415
|
+
FFmpegError.throwIfError(ret, 'Failed to queue filter command');
|
|
1416
|
+
}
|
|
1417
|
+
pipeTo(target) {
|
|
1418
|
+
const t = target;
|
|
1419
|
+
// Store reference to next component for flush propagation
|
|
1420
|
+
this.nextComponent = t;
|
|
1421
|
+
// Start worker if not already running
|
|
1422
|
+
this.workerPromise ??= this.runWorker();
|
|
1423
|
+
// Start pipe task: filter.outputQueue -> target.inputQueue (via target.send)
|
|
1424
|
+
this.pipeToPromise = (async () => {
|
|
1425
|
+
while (true) {
|
|
1426
|
+
const frame = await this.receiveFrame();
|
|
1427
|
+
if (!frame)
|
|
1428
|
+
break;
|
|
1429
|
+
await t.sendToQueue(frame);
|
|
1430
|
+
}
|
|
1431
|
+
})();
|
|
1432
|
+
// Return scheduler for chaining (target is now the last component)
|
|
1433
|
+
return new Scheduler(this, t);
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Free filter resources.
|
|
1437
|
+
*
|
|
1438
|
+
* Releases filter graph and contexts.
|
|
1439
|
+
* Safe to call multiple times.
|
|
1440
|
+
*
|
|
1441
|
+
* @example
|
|
1442
|
+
* ```typescript
|
|
1443
|
+
* filter.close();
|
|
1444
|
+
* ```
|
|
1445
|
+
*
|
|
1446
|
+
* @see {@link Symbol.dispose} For automatic cleanup
|
|
1447
|
+
*/
|
|
1448
|
+
close() {
|
|
1449
|
+
if (this.isClosed) {
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
this.isClosed = true;
|
|
1453
|
+
// Close queues
|
|
1454
|
+
this.inputQueue.close();
|
|
1455
|
+
this.outputQueue.close();
|
|
1456
|
+
this.buffersrcCtx?.free();
|
|
1457
|
+
this.buffersinkCtx?.free();
|
|
1458
|
+
this.buffersrcCtx = null;
|
|
1459
|
+
this.buffersinkCtx = null;
|
|
1460
|
+
this.frame.free();
|
|
1461
|
+
this.graph.free();
|
|
1462
|
+
this.initialized = false;
|
|
1463
|
+
this.initializePromise = null;
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Worker loop for push-based processing.
|
|
1467
|
+
*
|
|
1468
|
+
* @internal
|
|
1469
|
+
*/
|
|
1470
|
+
async runWorker() {
|
|
1471
|
+
try {
|
|
1472
|
+
// Outer loop - receive frames
|
|
1473
|
+
while (!this.inputQueue.isClosed) {
|
|
1474
|
+
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
1475
|
+
try {
|
|
1476
|
+
const frame = __addDisposableResource(env_3, await this.inputQueue.receive(), false);
|
|
1477
|
+
if (!frame)
|
|
1478
|
+
break;
|
|
1479
|
+
await this.process(frame);
|
|
1480
|
+
// Receive all available frames
|
|
1481
|
+
while (!this.outputQueue.isClosed) {
|
|
1482
|
+
const buffered = await this.receive();
|
|
1483
|
+
if (!buffered)
|
|
1484
|
+
break; // Stop on EAGAIN or EOF
|
|
1485
|
+
await this.outputQueue.send(buffered); // Only send actual frames
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
catch (e_3) {
|
|
1489
|
+
env_3.error = e_3;
|
|
1490
|
+
env_3.hasError = true;
|
|
1491
|
+
}
|
|
1492
|
+
finally {
|
|
1493
|
+
__disposeResources(env_3);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
// Flush filter at end
|
|
1497
|
+
await this.flush();
|
|
1498
|
+
while (!this.outputQueue.isClosed) {
|
|
1499
|
+
const frame = await this.receive();
|
|
1500
|
+
if (!frame)
|
|
1501
|
+
break; // Stop on EAGAIN or EOF
|
|
1502
|
+
await this.outputQueue.send(frame); // Only send actual frames
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
catch (error) {
|
|
1506
|
+
// Propagate error to both queues so upstream and downstream know
|
|
1507
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1508
|
+
this.inputQueue?.closeWithError(err);
|
|
1509
|
+
this.outputQueue?.closeWithError(err);
|
|
1510
|
+
}
|
|
1511
|
+
finally {
|
|
1512
|
+
// Close output queue when done (if not already closed with error)
|
|
1513
|
+
this.outputQueue?.close();
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Send frame to input queue or flush the pipeline.
|
|
1518
|
+
*
|
|
1519
|
+
* When frame is provided, queues it for filtering.
|
|
1520
|
+
* When null is provided, triggers flush sequence:
|
|
1521
|
+
* - Closes input queue
|
|
1522
|
+
* - Waits for worker completion
|
|
1523
|
+
* - Flushes filter and sends remaining frames to output queue
|
|
1524
|
+
* - Closes output queue
|
|
1525
|
+
* - Waits for pipeTo task completion
|
|
1526
|
+
* - Propagates flush to next component (if any)
|
|
1527
|
+
*
|
|
1528
|
+
* Used by scheduler system for pipeline control.
|
|
1529
|
+
*
|
|
1530
|
+
* @param frame - Frame to send, or null to flush
|
|
1531
|
+
*
|
|
1532
|
+
* @internal
|
|
1533
|
+
*/
|
|
1534
|
+
async sendToQueue(frame) {
|
|
1535
|
+
if (frame) {
|
|
1536
|
+
await this.inputQueue.send(frame);
|
|
1537
|
+
}
|
|
1538
|
+
else {
|
|
1539
|
+
// Close input queue to signal end of stream to worker
|
|
1540
|
+
this.inputQueue.close();
|
|
1541
|
+
// Wait for worker to finish processing all frames (if exists)
|
|
1542
|
+
if (this.workerPromise) {
|
|
1543
|
+
await this.workerPromise;
|
|
1544
|
+
}
|
|
1545
|
+
// Flush filter at end (like FFmpeg does)
|
|
1546
|
+
await this.flush();
|
|
1547
|
+
// Send all flushed frames to output queue
|
|
1548
|
+
while (true) {
|
|
1549
|
+
const frame = await this.receive();
|
|
1550
|
+
if (!frame)
|
|
1551
|
+
break; // Stop on EAGAIN or EOF
|
|
1552
|
+
await this.outputQueue.send(frame); // Only send actual frames
|
|
1553
|
+
}
|
|
1554
|
+
// Close output queue to signal end of stream to pipeTo() task
|
|
1555
|
+
this.outputQueue.close();
|
|
1556
|
+
// Wait for pipeTo() task to finish processing all frames (if exists)
|
|
1557
|
+
if (this.pipeToPromise) {
|
|
1558
|
+
await this.pipeToPromise;
|
|
1559
|
+
}
|
|
1560
|
+
// Then propagate flush to next component
|
|
1561
|
+
if (this.nextComponent) {
|
|
1562
|
+
await this.nextComponent.sendToQueue(null);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Receive frame from output queue.
|
|
1568
|
+
*
|
|
1569
|
+
* @returns Frame from output queue or null if closed
|
|
1570
|
+
*
|
|
1571
|
+
* @internal
|
|
1572
|
+
*/
|
|
1573
|
+
async receiveFrame() {
|
|
1574
|
+
return await this.outputQueue.receive();
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Initialize filter graph from first frame.
|
|
1578
|
+
*
|
|
1579
|
+
* Creates and configures filter graph components.
|
|
1580
|
+
* Sets buffer source parameters from frame properties.
|
|
1581
|
+
* Automatically configures hardware frames context if present.
|
|
1582
|
+
*
|
|
1583
|
+
* @param frame - First frame to process, provides format and hw context
|
|
1584
|
+
*
|
|
1585
|
+
* @throws {Error} If initialization fails
|
|
1586
|
+
*
|
|
1587
|
+
* @throws {FFmpegError} If configuration fails
|
|
1588
|
+
*
|
|
1589
|
+
* @internal
|
|
1590
|
+
*/
|
|
1591
|
+
async initialize(frame) {
|
|
1592
|
+
// Calculate timeBase from first frame
|
|
1593
|
+
this.calculatedTimeBase = this.calculateTimeBase(frame);
|
|
1594
|
+
// Track initial frame properties for change detection
|
|
1595
|
+
this.lastFrameProps = {
|
|
1596
|
+
format: frame.format,
|
|
1597
|
+
width: frame.width,
|
|
1598
|
+
height: frame.height,
|
|
1599
|
+
sampleRate: frame.sampleRate,
|
|
1600
|
+
channels: frame.channelLayout?.nbChannels ?? 0,
|
|
1601
|
+
};
|
|
1602
|
+
// Set graph options before parsing
|
|
1603
|
+
if (this.options.scaleSwsOpts) {
|
|
1604
|
+
this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
|
|
1605
|
+
}
|
|
1606
|
+
if (this.options.audioResampleOpts) {
|
|
1607
|
+
this.graph.aresampleSwrOpts = this.options.audioResampleOpts;
|
|
1608
|
+
}
|
|
1609
|
+
// Create buffer source and sink
|
|
1610
|
+
this.createBufferSource(frame);
|
|
1611
|
+
this.createBufferSink(frame);
|
|
1612
|
+
// Parse filter description
|
|
1613
|
+
this.parseFilterDescription(frame);
|
|
1614
|
+
// Configure the graph
|
|
1615
|
+
const ret = await this.graph.config();
|
|
1616
|
+
FFmpegError.throwIfError(ret, 'Failed to configure filter graph');
|
|
1617
|
+
this.initialized = true;
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Initialize filter graph from first frame synchronously.
|
|
1621
|
+
* Synchronous version of initialize.
|
|
1622
|
+
*
|
|
1623
|
+
* Creates and configures filter graph components.
|
|
1624
|
+
* Sets buffer source parameters from frame properties.
|
|
1625
|
+
* Automatically configures hardware frames context if present.
|
|
1626
|
+
*
|
|
1627
|
+
* @param frame - First frame to process, provides format and hw context
|
|
1628
|
+
*
|
|
1629
|
+
* @throws {Error} If initialization fails
|
|
1630
|
+
*
|
|
1631
|
+
* @throws {FFmpegError} If configuration fails
|
|
1632
|
+
*
|
|
1633
|
+
* @internal
|
|
1634
|
+
*
|
|
1635
|
+
* @see {@link initialize} For async version
|
|
1636
|
+
*/
|
|
1637
|
+
initializeSync(frame) {
|
|
1638
|
+
// Calculate timeBase from first frame
|
|
1639
|
+
this.calculatedTimeBase = this.calculateTimeBase(frame);
|
|
1640
|
+
// Track initial frame properties for change detection
|
|
1641
|
+
this.lastFrameProps = {
|
|
1642
|
+
format: frame.format,
|
|
1643
|
+
width: frame.width,
|
|
1644
|
+
height: frame.height,
|
|
1645
|
+
sampleRate: frame.sampleRate,
|
|
1646
|
+
channels: frame.channelLayout?.nbChannels ?? 0,
|
|
1647
|
+
};
|
|
1648
|
+
// Set graph options before parsing
|
|
1649
|
+
if (this.options.scaleSwsOpts) {
|
|
1650
|
+
this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
|
|
1651
|
+
}
|
|
1652
|
+
if (this.options.audioResampleOpts) {
|
|
1653
|
+
this.graph.aresampleSwrOpts = this.options.audioResampleOpts;
|
|
1654
|
+
}
|
|
1655
|
+
// Create buffer source and sink
|
|
1656
|
+
this.createBufferSource(frame);
|
|
1657
|
+
this.createBufferSink(frame);
|
|
1658
|
+
// Parse filter description
|
|
1659
|
+
this.parseFilterDescription(frame);
|
|
1660
|
+
// Configure the graph
|
|
1661
|
+
const ret = this.graph.configSync();
|
|
1662
|
+
FFmpegError.throwIfError(ret, 'Failed to configure filter graph');
|
|
1663
|
+
this.initialized = true;
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Check if frame properties changed and handle according to dropOnChange/allowReinit options.
|
|
1667
|
+
*
|
|
1668
|
+
* Implements FFmpeg's IFILTER_FLAG_DROPCHANGED and IFILTER_FLAG_REINIT logic
|
|
1669
|
+
*
|
|
1670
|
+
* @param frame - Frame to check
|
|
1671
|
+
*
|
|
1672
|
+
* @returns true if frame should be processed, false if frame should be dropped
|
|
1673
|
+
*
|
|
1674
|
+
* @throws {Error} If format changed and allowReinit is false
|
|
1675
|
+
*
|
|
1676
|
+
* @internal
|
|
1677
|
+
*/
|
|
1678
|
+
checkFramePropertiesChanged(frame) {
|
|
1679
|
+
if (!this.lastFrameProps) {
|
|
1680
|
+
return true; // No previous frame, allow
|
|
1681
|
+
}
|
|
1682
|
+
// Check for property changes
|
|
1683
|
+
const changed = frame.format !== this.lastFrameProps.format ||
|
|
1684
|
+
frame.width !== this.lastFrameProps.width ||
|
|
1685
|
+
frame.height !== this.lastFrameProps.height ||
|
|
1686
|
+
frame.sampleRate !== this.lastFrameProps.sampleRate ||
|
|
1687
|
+
(frame.channelLayout?.nbChannels ?? 0) !== this.lastFrameProps.channels;
|
|
1688
|
+
if (!changed) {
|
|
1689
|
+
return true; // No changes, process frame
|
|
1690
|
+
}
|
|
1691
|
+
// Properties changed - check dropOnChange flag
|
|
1692
|
+
if (this.options.dropOnChange) {
|
|
1693
|
+
return false; // Drop frame
|
|
1694
|
+
}
|
|
1695
|
+
// Check allowReinit flag
|
|
1696
|
+
// Default is true (allow reinit), only block if explicitly set to false
|
|
1697
|
+
const allowReinit = this.options.allowReinit !== false;
|
|
1698
|
+
if (!allowReinit && this.initialized) {
|
|
1699
|
+
throw new Error('Frame properties changed but allowReinit is false. ' +
|
|
1700
|
+
`Format: ${this.lastFrameProps.format}->${frame.format}, ` +
|
|
1701
|
+
`Size: ${this.lastFrameProps.width}x${this.lastFrameProps.height}->${frame.width}x${frame.height}`);
|
|
1702
|
+
}
|
|
1703
|
+
// Close current graph and reinitialize
|
|
1704
|
+
this.graph.free();
|
|
1705
|
+
// Create new graph
|
|
1706
|
+
this.graph = new FilterGraph();
|
|
1707
|
+
this.graph.alloc();
|
|
1708
|
+
// Configure threading
|
|
1709
|
+
if (this.options.threads !== undefined) {
|
|
1710
|
+
this.graph.nbThreads = this.options.threads;
|
|
1711
|
+
}
|
|
1712
|
+
// Configure scaler options
|
|
1713
|
+
if (this.options.scaleSwsOpts) {
|
|
1714
|
+
this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
|
|
1715
|
+
}
|
|
1716
|
+
this.buffersrcCtx = null;
|
|
1717
|
+
this.buffersinkCtx = null;
|
|
1718
|
+
this.initialized = false;
|
|
1719
|
+
this.initializePromise = null;
|
|
1720
|
+
this.calculatedTimeBase = null;
|
|
1721
|
+
return true; // Will be reinitialized on next process
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Calculate timeBase from frame based on media type and CFR option.
|
|
1725
|
+
*
|
|
1726
|
+
* Implements FFmpeg's ifilter_parameters_from_frame logic:
|
|
1727
|
+
* - Audio: Always { 1, sample_rate }
|
|
1728
|
+
* - Video CFR: 1/framerate (inverse of framerate)
|
|
1729
|
+
* - Video VFR: Use frame.timeBase
|
|
1730
|
+
*
|
|
1731
|
+
* @param frame - Input frame
|
|
1732
|
+
*
|
|
1733
|
+
* @returns Calculated timeBase
|
|
1734
|
+
*
|
|
1735
|
+
* @internal
|
|
1736
|
+
*/
|
|
1737
|
+
calculateTimeBase(frame) {
|
|
1738
|
+
if (frame.isAudio()) {
|
|
1739
|
+
// Audio: Always { 1, sample_rate }
|
|
1740
|
+
return { num: 1, den: frame.sampleRate };
|
|
1741
|
+
}
|
|
1742
|
+
else {
|
|
1743
|
+
// Video: Check CFR flag
|
|
1744
|
+
if (this.options.cfr) {
|
|
1745
|
+
// CFR mode: timeBase = 1/framerate = inverse(framerate)
|
|
1746
|
+
// Note: framerate is guaranteed to be set (validated in create())
|
|
1747
|
+
return avInvQ(this.options.framerate);
|
|
1748
|
+
}
|
|
1749
|
+
else {
|
|
1750
|
+
// VFR mode: Use frame.timeBase
|
|
1751
|
+
return frame.timeBase;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Post-process output frame from buffersink.
|
|
1757
|
+
*
|
|
1758
|
+
* Applies FFmpeg's fg_output_step() behavior:
|
|
1759
|
+
* 1. Sets frame.timeBase from buffersink (filters can change timeBase, e.g., aresample)
|
|
1760
|
+
* 2. Calculates video frame duration from frame rate if not set
|
|
1761
|
+
*
|
|
1762
|
+
* This must be called AFTER buffersinkGetFrame() for every output frame.
|
|
1763
|
+
*
|
|
1764
|
+
* @param frame - Output frame from buffersink
|
|
1765
|
+
*
|
|
1766
|
+
* @throws {Error} If buffersink context not available
|
|
1767
|
+
*
|
|
1768
|
+
* @internal
|
|
1769
|
+
*/
|
|
1770
|
+
postProcessOutputFrame(frame) {
|
|
1771
|
+
if (!this.buffersinkCtx) {
|
|
1772
|
+
throw new Error('Buffersink context not available');
|
|
1773
|
+
}
|
|
1774
|
+
// Filters can change timeBase (e.g., aresample sets output to {1, out_sample_rate})
|
|
1775
|
+
// Without this, frame has INPUT timeBase instead of filter's OUTPUT timeBase
|
|
1776
|
+
frame.timeBase = this.buffersinkCtx.buffersinkGetTimeBase();
|
|
1777
|
+
if (frame.isVideo() && !frame.duration) {
|
|
1778
|
+
const frameRate = this.buffersinkCtx.buffersinkGetFrameRate();
|
|
1779
|
+
if (frameRate.num > 0 && frameRate.den > 0) {
|
|
1780
|
+
frame.duration = avRescaleQ(1, avInvQ(frameRate), frame.timeBase);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Create buffer source with frame parameters.
|
|
1786
|
+
*
|
|
1787
|
+
* Configures buffer source with frame properties including hardware context.
|
|
1788
|
+
* Automatically detects video/audio and sets appropriate parameters.
|
|
1789
|
+
*
|
|
1790
|
+
* @param frame - Frame providing format, dimensions, and hw_frames_ctx
|
|
1791
|
+
*
|
|
1792
|
+
* @throws {Error} If creation fails
|
|
1793
|
+
*
|
|
1794
|
+
* @throws {FFmpegError} If configuration fails
|
|
1795
|
+
*
|
|
1796
|
+
* @internal
|
|
1797
|
+
*/
|
|
1798
|
+
createBufferSource(frame) {
|
|
1799
|
+
const filterName = frame.isVideo() ? 'buffer' : 'abuffer';
|
|
1800
|
+
const bufferFilter = Filter.getByName(filterName);
|
|
1801
|
+
if (!bufferFilter) {
|
|
1802
|
+
throw new FFmpegError(AVERROR_FILTER_NOT_FOUND);
|
|
1803
|
+
}
|
|
1804
|
+
// Ensure timeBase was calculated
|
|
1805
|
+
if (!this.calculatedTimeBase) {
|
|
1806
|
+
throw new Error('TimeBase not calculated - this should not happen');
|
|
1807
|
+
}
|
|
1808
|
+
// For audio, create with args. For video, use allocFilter + buffersrcParametersSet
|
|
1809
|
+
if (frame.isVideo()) {
|
|
1810
|
+
// Allocate filter without args
|
|
1811
|
+
this.buffersrcCtx = this.graph.allocFilter(bufferFilter, 'in');
|
|
1812
|
+
if (!this.buffersrcCtx) {
|
|
1813
|
+
throw new Error('Failed to allocate buffer source');
|
|
1814
|
+
}
|
|
1815
|
+
const ret = this.buffersrcCtx.buffersrcParametersSet({
|
|
1816
|
+
width: frame.width,
|
|
1817
|
+
height: frame.height,
|
|
1818
|
+
format: frame.format,
|
|
1819
|
+
timeBase: this.calculatedTimeBase,
|
|
1820
|
+
frameRate: this.options.framerate,
|
|
1821
|
+
sampleAspectRatio: frame.sampleAspectRatio,
|
|
1822
|
+
colorRange: frame.colorRange,
|
|
1823
|
+
colorSpace: frame.colorSpace,
|
|
1824
|
+
hwFramesCtx: frame.hwFramesCtx,
|
|
1825
|
+
});
|
|
1826
|
+
FFmpegError.throwIfError(ret, 'Failed to set buffer source parameters');
|
|
1827
|
+
// Initialize filter
|
|
1828
|
+
const initRet = this.buffersrcCtx.init(null);
|
|
1829
|
+
FFmpegError.throwIfError(initRet, 'Failed to initialize buffer source');
|
|
1830
|
+
}
|
|
1831
|
+
else {
|
|
1832
|
+
// For audio, create with args string
|
|
1833
|
+
const formatName = avGetSampleFmtName(frame.format);
|
|
1834
|
+
const channelLayout = frame.channelLayout.mask === 0n ? `${frame.channelLayout.nbChannels}c` : frame.channelLayout.mask.toString();
|
|
1835
|
+
// eslint-disable-next-line @stylistic/max-len
|
|
1836
|
+
const args = `time_base=${this.calculatedTimeBase.num}/${this.calculatedTimeBase.den}:sample_rate=${frame.sampleRate}:sample_fmt=${formatName}:channel_layout=${channelLayout}`;
|
|
1837
|
+
this.buffersrcCtx = this.graph.createFilter(bufferFilter, 'in', args);
|
|
1838
|
+
if (!this.buffersrcCtx) {
|
|
1839
|
+
throw new Error('Failed to create audio buffer source');
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Create buffer sink.
|
|
1845
|
+
*
|
|
1846
|
+
* @param frame - Frame
|
|
1847
|
+
*
|
|
1848
|
+
* @throws {Error} If creation fails
|
|
1849
|
+
*
|
|
1850
|
+
* @internal
|
|
1851
|
+
*/
|
|
1852
|
+
createBufferSink(frame) {
|
|
1853
|
+
const filterName = frame.isVideo() ? 'buffersink' : 'abuffersink';
|
|
1854
|
+
const sinkFilter = Filter.getByName(filterName);
|
|
1855
|
+
if (!sinkFilter) {
|
|
1856
|
+
throw new FFmpegError(AVERROR_FILTER_NOT_FOUND);
|
|
1857
|
+
}
|
|
1858
|
+
this.buffersinkCtx = this.graph.createFilter(sinkFilter, 'out', null);
|
|
1859
|
+
if (!this.buffersinkCtx) {
|
|
1860
|
+
throw new Error('Failed to create buffer sink');
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Parse filter description and build graph.
|
|
1865
|
+
*
|
|
1866
|
+
* Uses the Segment API to parse filters, which allows setting hw_device_ctx
|
|
1867
|
+
* before filter initialization when needed. Works for both hardware and software filters.
|
|
1868
|
+
*
|
|
1869
|
+
* @param frame - First frame to process, provides hw_frames_ctx if any
|
|
1870
|
+
*
|
|
1871
|
+
* @throws {Error} If parsing fails
|
|
1872
|
+
*
|
|
1873
|
+
* @throws {FFmpegError} If graph construction fails
|
|
1874
|
+
*
|
|
1875
|
+
* @internal
|
|
1876
|
+
*/
|
|
1877
|
+
parseFilterDescription(frame) {
|
|
1878
|
+
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
1879
|
+
throw new Error('Buffer filters not initialized');
|
|
1880
|
+
}
|
|
1881
|
+
// Handle empty or simple passthrough
|
|
1882
|
+
if (!this.description || this.description === 'null' || this.description === 'anull') {
|
|
1883
|
+
// Direct connection for null filters
|
|
1884
|
+
const ret = this.buffersrcCtx.link(0, this.buffersinkCtx, 0);
|
|
1885
|
+
FFmpegError.throwIfError(ret, 'Failed to link buffer filters');
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
// Step 1: Parse the filter description into a segment
|
|
1889
|
+
const segment = this.graph.segmentParse(this.description);
|
|
1890
|
+
if (!segment) {
|
|
1891
|
+
throw new Error('Failed to parse filter segment');
|
|
1892
|
+
}
|
|
1893
|
+
try {
|
|
1894
|
+
// Step 2: Create filter instances (but don't initialize yet)
|
|
1895
|
+
let ret = segment.createFilters();
|
|
1896
|
+
FFmpegError.throwIfError(ret, 'Failed to create filters in segment');
|
|
1897
|
+
// Step 3: Set hw_device_ctx on filters that need it BEFORE initialization (if provided)
|
|
1898
|
+
const filters = this.graph.filters;
|
|
1899
|
+
if (filters) {
|
|
1900
|
+
for (const filterCtx of filters) {
|
|
1901
|
+
const filter = filterCtx.filter;
|
|
1902
|
+
if (filter?.hasFlags(AVFILTER_FLAG_HWDEVICE)) {
|
|
1903
|
+
filterCtx.hwDeviceCtx = this.options.hardware?.deviceContext ?? frame.hwFramesCtx?.deviceRef ?? null;
|
|
1904
|
+
// Set extra_hw_frames if specified
|
|
1905
|
+
if (this.options.extraHWFrames !== undefined && this.options.extraHWFrames > 0) {
|
|
1906
|
+
filterCtx.extraHWFrames = this.options.extraHWFrames;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
// Step 4: Apply options to filters
|
|
1912
|
+
ret = segment.applyOpts();
|
|
1913
|
+
FFmpegError.throwIfError(ret, 'Failed to apply options to segment');
|
|
1914
|
+
// Step 5: Initialize and link filters in the segment
|
|
1915
|
+
// Create empty FilterInOut objects - segment.apply() will populate them with
|
|
1916
|
+
// the segment's unconnected input/output pads
|
|
1917
|
+
const inputs = new FilterInOut();
|
|
1918
|
+
const outputs = new FilterInOut();
|
|
1919
|
+
// Apply the segment - this initializes and links all filters within the segment,
|
|
1920
|
+
// and returns the segment's unconnected pads in inputs/outputs
|
|
1921
|
+
ret = segment.apply(inputs, outputs);
|
|
1922
|
+
FFmpegError.throwIfError(ret, 'Failed to apply segment');
|
|
1923
|
+
// Step 6: Manually link buffersrc/buffersink to the segment's unconnected pads
|
|
1924
|
+
// After segment.apply():
|
|
1925
|
+
// - inputs contains the segment's free INPUT pads (where buffersrc connects TO)
|
|
1926
|
+
// - outputs contains the segment's free OUTPUT pads (where buffersink connects FROM)
|
|
1927
|
+
// Link buffersrc -> first segment input (if any)
|
|
1928
|
+
const segmentInput = inputs.filterCtx;
|
|
1929
|
+
if (segmentInput) {
|
|
1930
|
+
ret = this.buffersrcCtx.link(0, segmentInput, inputs.padIdx);
|
|
1931
|
+
FFmpegError.throwIfError(ret, 'Failed to link buffersrc to segment');
|
|
1932
|
+
}
|
|
1933
|
+
else {
|
|
1934
|
+
// No segment inputs means the filter doesn't accept input
|
|
1935
|
+
throw new Error('Segment has no input pads - cannot connect buffersrc');
|
|
1936
|
+
}
|
|
1937
|
+
// Link last segment output -> buffersink (if any)
|
|
1938
|
+
const segmentOutput = outputs.filterCtx;
|
|
1939
|
+
if (segmentOutput) {
|
|
1940
|
+
ret = segmentOutput.link(outputs.padIdx, this.buffersinkCtx, 0);
|
|
1941
|
+
FFmpegError.throwIfError(ret, 'Failed to link segment to buffersink');
|
|
1942
|
+
}
|
|
1943
|
+
else {
|
|
1944
|
+
// No segment outputs means the filter doesn't produce output
|
|
1945
|
+
throw new Error('Segment has no output pads - cannot connect buffersink');
|
|
1946
|
+
}
|
|
1947
|
+
// Clean up FilterInOut structures
|
|
1948
|
+
inputs.free();
|
|
1949
|
+
outputs.free();
|
|
1950
|
+
}
|
|
1951
|
+
finally {
|
|
1952
|
+
// Always free the segment
|
|
1953
|
+
segment.free();
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* Dispose of filter.
|
|
1958
|
+
*
|
|
1959
|
+
* Implements Disposable interface for automatic cleanup.
|
|
1960
|
+
* Equivalent to calling close().
|
|
1961
|
+
*
|
|
1962
|
+
* @example
|
|
1963
|
+
* ```typescript
|
|
1964
|
+
* {
|
|
1965
|
+
* using filter = FilterAPI.create('scale=640:480', { ... });
|
|
1966
|
+
* // Use filter...
|
|
1967
|
+
* } // Automatically freed
|
|
1968
|
+
* ```
|
|
1969
|
+
*
|
|
1970
|
+
* @see {@link close} For manual cleanup
|
|
1971
|
+
*/
|
|
1972
|
+
[Symbol.dispose]() {
|
|
1973
|
+
this.close();
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
//# sourceMappingURL=filter.js.map
|