@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.
Files changed (254) hide show
  1. package/BUILD_LINUX.md +61 -0
  2. package/LICENSE.md +22 -0
  3. package/README.md +662 -0
  4. package/build_mac_local.sh +69 -0
  5. package/dist/api/audio-frame-buffer.d.ts +205 -0
  6. package/dist/api/audio-frame-buffer.js +287 -0
  7. package/dist/api/audio-frame-buffer.js.map +1 -0
  8. package/dist/api/bitstream-filter.d.ts +820 -0
  9. package/dist/api/bitstream-filter.js +1242 -0
  10. package/dist/api/bitstream-filter.js.map +1 -0
  11. package/dist/api/constants.d.ts +44 -0
  12. package/dist/api/constants.js +45 -0
  13. package/dist/api/constants.js.map +1 -0
  14. package/dist/api/data/test_av1.ivf +0 -0
  15. package/dist/api/data/test_h264.h264 +0 -0
  16. package/dist/api/data/test_hevc.h265 +0 -0
  17. package/dist/api/data/test_mjpeg.mjpeg +0 -0
  18. package/dist/api/data/test_vp8.ivf +0 -0
  19. package/dist/api/data/test_vp9.ivf +0 -0
  20. package/dist/api/decoder.d.ts +1088 -0
  21. package/dist/api/decoder.js +1775 -0
  22. package/dist/api/decoder.js.map +1 -0
  23. package/dist/api/demuxer.d.ts +1219 -0
  24. package/dist/api/demuxer.js +2081 -0
  25. package/dist/api/demuxer.js.map +1 -0
  26. package/dist/api/device.d.ts +586 -0
  27. package/dist/api/device.js +961 -0
  28. package/dist/api/device.js.map +1 -0
  29. package/dist/api/encoder.d.ts +1132 -0
  30. package/dist/api/encoder.js +1988 -0
  31. package/dist/api/encoder.js.map +1 -0
  32. package/dist/api/filter-complex.d.ts +821 -0
  33. package/dist/api/filter-complex.js +1604 -0
  34. package/dist/api/filter-complex.js.map +1 -0
  35. package/dist/api/filter-presets.d.ts +1286 -0
  36. package/dist/api/filter-presets.js +2152 -0
  37. package/dist/api/filter-presets.js.map +1 -0
  38. package/dist/api/filter.d.ts +1234 -0
  39. package/dist/api/filter.js +1976 -0
  40. package/dist/api/filter.js.map +1 -0
  41. package/dist/api/fmp4-stream.d.ts +426 -0
  42. package/dist/api/fmp4-stream.js +739 -0
  43. package/dist/api/fmp4-stream.js.map +1 -0
  44. package/dist/api/hardware.d.ts +651 -0
  45. package/dist/api/hardware.js +1260 -0
  46. package/dist/api/hardware.js.map +1 -0
  47. package/dist/api/index.d.ts +17 -0
  48. package/dist/api/index.js +32 -0
  49. package/dist/api/index.js.map +1 -0
  50. package/dist/api/io-stream.d.ts +307 -0
  51. package/dist/api/io-stream.js +282 -0
  52. package/dist/api/io-stream.js.map +1 -0
  53. package/dist/api/muxer.d.ts +957 -0
  54. package/dist/api/muxer.js +2002 -0
  55. package/dist/api/muxer.js.map +1 -0
  56. package/dist/api/pipeline.d.ts +607 -0
  57. package/dist/api/pipeline.js +1145 -0
  58. package/dist/api/pipeline.js.map +1 -0
  59. package/dist/api/utilities/async-queue.d.ts +120 -0
  60. package/dist/api/utilities/async-queue.js +211 -0
  61. package/dist/api/utilities/async-queue.js.map +1 -0
  62. package/dist/api/utilities/audio-sample.d.ts +117 -0
  63. package/dist/api/utilities/audio-sample.js +112 -0
  64. package/dist/api/utilities/audio-sample.js.map +1 -0
  65. package/dist/api/utilities/channel-layout.d.ts +76 -0
  66. package/dist/api/utilities/channel-layout.js +80 -0
  67. package/dist/api/utilities/channel-layout.js.map +1 -0
  68. package/dist/api/utilities/electron-shared-texture.d.ts +328 -0
  69. package/dist/api/utilities/electron-shared-texture.js +503 -0
  70. package/dist/api/utilities/electron-shared-texture.js.map +1 -0
  71. package/dist/api/utilities/image.d.ts +207 -0
  72. package/dist/api/utilities/image.js +213 -0
  73. package/dist/api/utilities/image.js.map +1 -0
  74. package/dist/api/utilities/index.d.ts +12 -0
  75. package/dist/api/utilities/index.js +25 -0
  76. package/dist/api/utilities/index.js.map +1 -0
  77. package/dist/api/utilities/media-type.d.ts +49 -0
  78. package/dist/api/utilities/media-type.js +53 -0
  79. package/dist/api/utilities/media-type.js.map +1 -0
  80. package/dist/api/utilities/pixel-format.d.ts +89 -0
  81. package/dist/api/utilities/pixel-format.js +97 -0
  82. package/dist/api/utilities/pixel-format.js.map +1 -0
  83. package/dist/api/utilities/sample-format.d.ts +129 -0
  84. package/dist/api/utilities/sample-format.js +141 -0
  85. package/dist/api/utilities/sample-format.js.map +1 -0
  86. package/dist/api/utilities/scheduler.d.ts +138 -0
  87. package/dist/api/utilities/scheduler.js +98 -0
  88. package/dist/api/utilities/scheduler.js.map +1 -0
  89. package/dist/api/utilities/streaming.d.ts +186 -0
  90. package/dist/api/utilities/streaming.js +309 -0
  91. package/dist/api/utilities/streaming.js.map +1 -0
  92. package/dist/api/utilities/timestamp.d.ts +193 -0
  93. package/dist/api/utilities/timestamp.js +206 -0
  94. package/dist/api/utilities/timestamp.js.map +1 -0
  95. package/dist/api/utilities/whisper-model.d.ts +310 -0
  96. package/dist/api/utilities/whisper-model.js +528 -0
  97. package/dist/api/utilities/whisper-model.js.map +1 -0
  98. package/dist/api/utils.d.ts +19 -0
  99. package/dist/api/utils.js +39 -0
  100. package/dist/api/utils.js.map +1 -0
  101. package/dist/api/whisper.d.ts +324 -0
  102. package/dist/api/whisper.js +362 -0
  103. package/dist/api/whisper.js.map +1 -0
  104. package/dist/constants/channel-layouts.d.ts +53 -0
  105. package/dist/constants/channel-layouts.js +57 -0
  106. package/dist/constants/channel-layouts.js.map +1 -0
  107. package/dist/constants/constants.d.ts +2325 -0
  108. package/dist/constants/constants.js +1887 -0
  109. package/dist/constants/constants.js.map +1 -0
  110. package/dist/constants/decoders.d.ts +633 -0
  111. package/dist/constants/decoders.js +641 -0
  112. package/dist/constants/decoders.js.map +1 -0
  113. package/dist/constants/encoders.d.ts +295 -0
  114. package/dist/constants/encoders.js +308 -0
  115. package/dist/constants/encoders.js.map +1 -0
  116. package/dist/constants/hardware.d.ts +26 -0
  117. package/dist/constants/hardware.js +27 -0
  118. package/dist/constants/hardware.js.map +1 -0
  119. package/dist/constants/index.d.ts +5 -0
  120. package/dist/constants/index.js +6 -0
  121. package/dist/constants/index.js.map +1 -0
  122. package/dist/ffmpeg/index.d.ts +99 -0
  123. package/dist/ffmpeg/index.js +115 -0
  124. package/dist/ffmpeg/index.js.map +1 -0
  125. package/dist/ffmpeg/utils.d.ts +31 -0
  126. package/dist/ffmpeg/utils.js +68 -0
  127. package/dist/ffmpeg/utils.js.map +1 -0
  128. package/dist/ffmpeg/version.d.ts +6 -0
  129. package/dist/ffmpeg/version.js +7 -0
  130. package/dist/ffmpeg/version.js.map +1 -0
  131. package/dist/index.d.ts +4 -0
  132. package/dist/index.js +9 -0
  133. package/dist/index.js.map +1 -0
  134. package/dist/lib/audio-fifo.d.ts +399 -0
  135. package/dist/lib/audio-fifo.js +431 -0
  136. package/dist/lib/audio-fifo.js.map +1 -0
  137. package/dist/lib/binding.d.ts +228 -0
  138. package/dist/lib/binding.js +60 -0
  139. package/dist/lib/binding.js.map +1 -0
  140. package/dist/lib/bitstream-filter-context.d.ts +379 -0
  141. package/dist/lib/bitstream-filter-context.js +441 -0
  142. package/dist/lib/bitstream-filter-context.js.map +1 -0
  143. package/dist/lib/bitstream-filter.d.ts +140 -0
  144. package/dist/lib/bitstream-filter.js +154 -0
  145. package/dist/lib/bitstream-filter.js.map +1 -0
  146. package/dist/lib/codec-context.d.ts +1071 -0
  147. package/dist/lib/codec-context.js +1354 -0
  148. package/dist/lib/codec-context.js.map +1 -0
  149. package/dist/lib/codec-parameters.d.ts +616 -0
  150. package/dist/lib/codec-parameters.js +761 -0
  151. package/dist/lib/codec-parameters.js.map +1 -0
  152. package/dist/lib/codec-parser.d.ts +201 -0
  153. package/dist/lib/codec-parser.js +213 -0
  154. package/dist/lib/codec-parser.js.map +1 -0
  155. package/dist/lib/codec.d.ts +586 -0
  156. package/dist/lib/codec.js +713 -0
  157. package/dist/lib/codec.js.map +1 -0
  158. package/dist/lib/device.d.ts +291 -0
  159. package/dist/lib/device.js +324 -0
  160. package/dist/lib/device.js.map +1 -0
  161. package/dist/lib/dictionary.d.ts +333 -0
  162. package/dist/lib/dictionary.js +372 -0
  163. package/dist/lib/dictionary.js.map +1 -0
  164. package/dist/lib/error.d.ts +242 -0
  165. package/dist/lib/error.js +303 -0
  166. package/dist/lib/error.js.map +1 -0
  167. package/dist/lib/fifo.d.ts +416 -0
  168. package/dist/lib/fifo.js +453 -0
  169. package/dist/lib/fifo.js.map +1 -0
  170. package/dist/lib/filter-context.d.ts +712 -0
  171. package/dist/lib/filter-context.js +789 -0
  172. package/dist/lib/filter-context.js.map +1 -0
  173. package/dist/lib/filter-graph-segment.d.ts +160 -0
  174. package/dist/lib/filter-graph-segment.js +171 -0
  175. package/dist/lib/filter-graph-segment.js.map +1 -0
  176. package/dist/lib/filter-graph.d.ts +641 -0
  177. package/dist/lib/filter-graph.js +704 -0
  178. package/dist/lib/filter-graph.js.map +1 -0
  179. package/dist/lib/filter-inout.d.ts +198 -0
  180. package/dist/lib/filter-inout.js +257 -0
  181. package/dist/lib/filter-inout.js.map +1 -0
  182. package/dist/lib/filter.d.ts +243 -0
  183. package/dist/lib/filter.js +272 -0
  184. package/dist/lib/filter.js.map +1 -0
  185. package/dist/lib/format-context.d.ts +1254 -0
  186. package/dist/lib/format-context.js +1379 -0
  187. package/dist/lib/format-context.js.map +1 -0
  188. package/dist/lib/frame-utils.d.ts +116 -0
  189. package/dist/lib/frame-utils.js +98 -0
  190. package/dist/lib/frame-utils.js.map +1 -0
  191. package/dist/lib/frame.d.ts +1222 -0
  192. package/dist/lib/frame.js +1435 -0
  193. package/dist/lib/frame.js.map +1 -0
  194. package/dist/lib/hardware-device-context.d.ts +362 -0
  195. package/dist/lib/hardware-device-context.js +383 -0
  196. package/dist/lib/hardware-device-context.js.map +1 -0
  197. package/dist/lib/hardware-frames-context.d.ts +419 -0
  198. package/dist/lib/hardware-frames-context.js +477 -0
  199. package/dist/lib/hardware-frames-context.js.map +1 -0
  200. package/dist/lib/index.d.ts +35 -0
  201. package/dist/lib/index.js +60 -0
  202. package/dist/lib/index.js.map +1 -0
  203. package/dist/lib/input-format.d.ts +249 -0
  204. package/dist/lib/input-format.js +306 -0
  205. package/dist/lib/input-format.js.map +1 -0
  206. package/dist/lib/io-context.d.ts +696 -0
  207. package/dist/lib/io-context.js +769 -0
  208. package/dist/lib/io-context.js.map +1 -0
  209. package/dist/lib/log.d.ts +174 -0
  210. package/dist/lib/log.js +184 -0
  211. package/dist/lib/log.js.map +1 -0
  212. package/dist/lib/native-types.d.ts +946 -0
  213. package/dist/lib/native-types.js +2 -0
  214. package/dist/lib/native-types.js.map +1 -0
  215. package/dist/lib/option.d.ts +927 -0
  216. package/dist/lib/option.js +1583 -0
  217. package/dist/lib/option.js.map +1 -0
  218. package/dist/lib/output-format.d.ts +180 -0
  219. package/dist/lib/output-format.js +213 -0
  220. package/dist/lib/output-format.js.map +1 -0
  221. package/dist/lib/packet.d.ts +501 -0
  222. package/dist/lib/packet.js +590 -0
  223. package/dist/lib/packet.js.map +1 -0
  224. package/dist/lib/rational.d.ts +251 -0
  225. package/dist/lib/rational.js +278 -0
  226. package/dist/lib/rational.js.map +1 -0
  227. package/dist/lib/software-resample-context.d.ts +552 -0
  228. package/dist/lib/software-resample-context.js +592 -0
  229. package/dist/lib/software-resample-context.js.map +1 -0
  230. package/dist/lib/software-scale-context.d.ts +344 -0
  231. package/dist/lib/software-scale-context.js +366 -0
  232. package/dist/lib/software-scale-context.js.map +1 -0
  233. package/dist/lib/stream.d.ts +379 -0
  234. package/dist/lib/stream.js +526 -0
  235. package/dist/lib/stream.js.map +1 -0
  236. package/dist/lib/sync-queue.d.ts +179 -0
  237. package/dist/lib/sync-queue.js +197 -0
  238. package/dist/lib/sync-queue.js.map +1 -0
  239. package/dist/lib/types.d.ts +34 -0
  240. package/dist/lib/types.js +2 -0
  241. package/dist/lib/types.js.map +1 -0
  242. package/dist/lib/utilities.d.ts +1127 -0
  243. package/dist/lib/utilities.js +1225 -0
  244. package/dist/lib/utilities.js.map +1 -0
  245. package/dist/utils/electron.d.ts +49 -0
  246. package/dist/utils/electron.js +63 -0
  247. package/dist/utils/electron.js.map +1 -0
  248. package/dist/utils/index.d.ts +4 -0
  249. package/dist/utils/index.js +5 -0
  250. package/dist/utils/index.js.map +1 -0
  251. package/install/check.js +121 -0
  252. package/install/ffmpeg.js +66 -0
  253. package/jellyfin-ffmpeg.patch +181 -0
  254. 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