node-av 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/api/bitstream-filter.js +2 -1
- package/dist/api/bitstream-filter.js.map +1 -1
- package/dist/api/decoder.d.ts +10 -1
- package/dist/api/decoder.js +44 -25
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/encoder.d.ts +19 -7
- package/dist/api/encoder.js +94 -130
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/filter-presets.d.ts +316 -0
- package/dist/api/filter-presets.js +823 -0
- package/dist/api/filter-presets.js.map +1 -0
- package/dist/api/filter.d.ts +133 -173
- package/dist/api/filter.js +309 -393
- package/dist/api/filter.js.map +1 -1
- package/dist/api/hardware.d.ts +33 -73
- package/dist/api/hardware.js +86 -134
- package/dist/api/hardware.js.map +1 -1
- package/dist/api/index.d.ts +2 -1
- package/dist/api/index.js +1 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/io-stream.js +2 -1
- package/dist/api/io-stream.js.map +1 -1
- package/dist/api/media-input.d.ts +2 -1
- package/dist/api/media-input.js +2 -1
- package/dist/api/media-input.js.map +1 -1
- package/dist/api/media-output.js +2 -1
- package/dist/api/media-output.js.map +1 -1
- package/dist/api/types.d.ts +7 -1
- package/dist/api/utilities/audio-sample.d.ts +1 -1
- package/dist/api/utilities/image.d.ts +1 -1
- package/dist/api/utilities/media-type.d.ts +1 -1
- package/dist/api/utilities/pixel-format.d.ts +1 -1
- package/dist/api/utilities/sample-format.d.ts +1 -1
- package/dist/api/utilities/timestamp.d.ts +1 -1
- package/dist/{lib → constants}/channel-layouts.d.ts +1 -1
- package/dist/constants/channel-layouts.js.map +1 -0
- package/dist/{lib → constants}/constants.d.ts +19 -4
- package/dist/{lib → constants}/constants.js +15 -1
- package/dist/constants/constants.js.map +1 -0
- package/dist/constants/decoders.d.ts +609 -0
- package/dist/constants/decoders.js +617 -0
- package/dist/constants/decoders.js.map +1 -0
- package/dist/constants/encoders.d.ts +285 -0
- package/dist/constants/encoders.js +298 -0
- package/dist/constants/encoders.js.map +1 -0
- package/dist/constants/index.d.ts +4 -0
- package/dist/constants/index.js +5 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/audio-fifo.d.ts +1 -1
- package/dist/lib/binding.d.ts +7 -5
- package/dist/lib/binding.js.map +1 -1
- package/dist/lib/bitstream-filter.d.ts +1 -1
- package/dist/lib/codec-context.d.ts +1 -1
- package/dist/lib/codec-parameters.d.ts +1 -1
- package/dist/lib/codec-parser.d.ts +1 -1
- package/dist/lib/codec.d.ts +131 -3
- package/dist/lib/codec.js +191 -0
- package/dist/lib/codec.js.map +1 -1
- package/dist/lib/dictionary.d.ts +1 -1
- package/dist/lib/dictionary.js +1 -1
- package/dist/lib/dictionary.js.map +1 -1
- package/dist/lib/error.d.ts +1 -1
- package/dist/lib/error.js.map +1 -1
- package/dist/lib/filter-context.d.ts +58 -1
- package/dist/lib/filter-context.js +72 -0
- package/dist/lib/filter-context.js.map +1 -1
- package/dist/lib/filter-graph.d.ts +1 -1
- package/dist/lib/filter.js +1 -1
- package/dist/lib/filter.js.map +1 -1
- package/dist/lib/format-context.d.ts +1 -1
- package/dist/lib/format-context.js +1 -1
- package/dist/lib/format-context.js.map +1 -1
- package/dist/lib/frame.d.ts +36 -1
- package/dist/lib/frame.js +37 -0
- package/dist/lib/frame.js.map +1 -1
- package/dist/lib/hardware-device-context.d.ts +1 -1
- package/dist/lib/hardware-frames-context.d.ts +1 -1
- package/dist/lib/index.d.ts +0 -2
- package/dist/lib/index.js +0 -3
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/input-format.d.ts +1 -1
- package/dist/lib/io-context.d.ts +1 -1
- package/dist/lib/io-context.js +1 -1
- package/dist/lib/io-context.js.map +1 -1
- package/dist/lib/log.d.ts +1 -1
- package/dist/lib/native-types.d.ts +11 -6
- package/dist/lib/native-types.js +16 -0
- package/dist/lib/native-types.js.map +1 -1
- package/dist/lib/option.d.ts +1 -1
- package/dist/lib/option.js +1 -1
- package/dist/lib/option.js.map +1 -1
- package/dist/lib/output-format.d.ts +1 -1
- package/dist/lib/packet.d.ts +1 -1
- package/dist/lib/software-resample-context.d.ts +1 -1
- package/dist/lib/software-scale-context.d.ts +1 -1
- package/dist/lib/software-scale-context.js +1 -1
- package/dist/lib/software-scale-context.js.map +1 -1
- package/dist/lib/stream.d.ts +1 -1
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/utilities.d.ts +1 -1
- package/package.json +18 -19
- package/release_notes.md +59 -25
- package/dist/lib/channel-layouts.js.map +0 -1
- package/dist/lib/constants.js.map +0 -1
- /package/dist/{lib → constants}/channel-layouts.js +0 -0
package/dist/api/filter.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Filter - High-level wrapper for media filtering
|
|
3
|
+
*
|
|
4
|
+
* Implements FFmpeg CLI's filter graph behavior with proper hardware context handling.
|
|
5
|
+
* Uses lazy initialization for hardware inputs: graph is built when first frame arrives
|
|
6
|
+
* with hw_frames_ctx. For software inputs, initializes immediately.
|
|
7
|
+
*
|
|
8
|
+
* Handles filter graph creation, frame processing, and format conversion.
|
|
9
|
+
* Supports complex filter chains and hardware-accelerated filters.
|
|
10
|
+
*
|
|
11
|
+
* @module api/filter
|
|
12
|
+
*/
|
|
13
|
+
import { AVERROR_EOF, AVFILTER_FLAG_HWDEVICE, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO } from '../constants/constants.js';
|
|
14
|
+
import { AVERROR_EAGAIN, avGetSampleFmtName, avIsHardwarePixelFormat, FFmpegError, Filter, FilterGraph, FilterInOut, Frame } from '../lib/index.js';
|
|
2
15
|
/**
|
|
3
16
|
* High-level filter API for media processing.
|
|
4
17
|
*
|
|
@@ -6,11 +19,15 @@ import { AVERROR_EAGAIN, AVERROR_EOF, AVFILTER_FLAG_HWDEVICE, avGetPixFmtName, a
|
|
|
6
19
|
* Supports both simple filter chains and complex filter graphs.
|
|
7
20
|
* Handles automatic format negotiation and buffer management.
|
|
8
21
|
*
|
|
22
|
+
* The filter graph uses lazy initialization for hardware inputs - it's built when
|
|
23
|
+
* the first frame arrives with hw_frames_ctx. This matches FFmpeg CLI behavior
|
|
24
|
+
* for proper hardware context propagation.
|
|
25
|
+
*
|
|
9
26
|
* @example
|
|
10
27
|
* ```typescript
|
|
11
|
-
* import { FilterAPI, Frame } from '
|
|
28
|
+
* import { FilterAPI, Frame } from '@seydx/av/api';
|
|
12
29
|
*
|
|
13
|
-
* //
|
|
30
|
+
* // Simple video filter from a stream
|
|
14
31
|
* const videoStream = media.video();
|
|
15
32
|
* const filter = await FilterAPI.create('scale=1280:720,format=yuv420p', videoStream);
|
|
16
33
|
*
|
|
@@ -20,38 +37,56 @@ import { AVERROR_EAGAIN, AVERROR_EOF, AVFILTER_FLAG_HWDEVICE, avGetPixFmtName, a
|
|
|
20
37
|
*
|
|
21
38
|
* @example
|
|
22
39
|
* ```typescript
|
|
23
|
-
* //
|
|
40
|
+
* // Hardware acceleration (decoder -> hw filter -> encoder)
|
|
24
41
|
* const hw = await HardwareContext.auto();
|
|
25
|
-
* const
|
|
42
|
+
* const decoder = await Decoder.create(stream, { hardware: hw });
|
|
43
|
+
* const filter = await FilterAPI.create('scale_vt=640:480', decoder.getOutputStreamInfo(), {
|
|
26
44
|
* hardware: hw
|
|
27
45
|
* });
|
|
28
46
|
* ```
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* // Software decode -> hardware encode pipeline with hwupload
|
|
51
|
+
* const decoder = await Decoder.create(stream);
|
|
52
|
+
* const hw = await HardwareContext.auto();
|
|
53
|
+
* const filter = await FilterAPI.create('format=nv12,hwupload', decoder.getOutputStreamInfo(), {
|
|
54
|
+
* hardware: hw // Required for hwupload to create hw_frames_ctx
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* // Hardware decode -> software encode pipeline with hwdownload
|
|
61
|
+
* const hw = await HardwareContext.auto();
|
|
62
|
+
* const decoder = await Decoder.create(stream, { hardware: hw });
|
|
63
|
+
* const filter = await FilterAPI.create('hwdownload,format=yuv420p', decoder.getOutputStreamInfo());
|
|
64
|
+
* ```
|
|
29
65
|
*/
|
|
30
66
|
export class FilterAPI {
|
|
31
|
-
graph;
|
|
67
|
+
graph = null;
|
|
32
68
|
buffersrcCtx = null;
|
|
33
69
|
buffersinkCtx = null;
|
|
34
70
|
config;
|
|
35
71
|
mediaType;
|
|
36
72
|
initialized = false;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
73
|
+
hardware;
|
|
74
|
+
description;
|
|
75
|
+
options;
|
|
40
76
|
/**
|
|
41
77
|
* Create a new Filter instance.
|
|
42
78
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* @param config - Filter configuration
|
|
47
|
-
* @param hardware - Optional hardware context for late framesContext binding
|
|
79
|
+
* @param config - Stream information from input stream
|
|
80
|
+
* @param description - Filter graph description
|
|
81
|
+
* @param options - Filter options including hardware context
|
|
48
82
|
* @internal
|
|
49
83
|
*/
|
|
50
|
-
constructor(config,
|
|
84
|
+
constructor(config, description, options) {
|
|
51
85
|
this.config = config;
|
|
52
|
-
this.
|
|
86
|
+
this.description = description;
|
|
87
|
+
this.options = options;
|
|
88
|
+
this.hardware = options.hardware;
|
|
53
89
|
this.mediaType = config.type === 'video' ? AVMEDIA_TYPE_VIDEO : AVMEDIA_TYPE_AUDIO;
|
|
54
|
-
this.graph = new LowLevelFilterGraph();
|
|
55
90
|
}
|
|
56
91
|
/**
|
|
57
92
|
* Create a filter from a filter description string.
|
|
@@ -59,9 +94,14 @@ export class FilterAPI {
|
|
|
59
94
|
* Accepts either a Stream (from MediaInput/Decoder) or StreamInfo (for raw data).
|
|
60
95
|
* Automatically sets up buffer source and sink filters.
|
|
61
96
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
97
|
+
* For hardware input formats: Uses lazy initialization, waits for first frame
|
|
98
|
+
* with hw_frames_ctx before configuring the filter graph.
|
|
99
|
+
* For software formats: Initializes immediately.
|
|
100
|
+
*
|
|
101
|
+
* Hardware context handling:
|
|
102
|
+
* - hwupload: Requires hardware context, creates its own hw_frames_ctx
|
|
103
|
+
* - hwdownload: Uses hw_frames_ctx propagated from previous filters
|
|
104
|
+
* - Other HW filters: Use propagated hw_frames_ctx or hwupload's output
|
|
65
105
|
*
|
|
66
106
|
* @param description - Filter graph description (e.g., "scale=1280:720" or complex chains)
|
|
67
107
|
* @param input - Stream or StreamInfo describing the input
|
|
@@ -70,6 +110,7 @@ export class FilterAPI {
|
|
|
70
110
|
* @returns Promise resolving to configured Filter instance
|
|
71
111
|
*
|
|
72
112
|
* @throws {FFmpegError} If filter creation or configuration fails
|
|
113
|
+
* @throws {Error} If hardware filter requires hardware context but none provided
|
|
73
114
|
*
|
|
74
115
|
* @example
|
|
75
116
|
* ```typescript
|
|
@@ -78,9 +119,10 @@ export class FilterAPI {
|
|
|
78
119
|
*
|
|
79
120
|
* // Complex filter chain with hardware
|
|
80
121
|
* const hw = await HardwareContext.auto();
|
|
122
|
+
* const decoder = await Decoder.create(stream, { hardware: hw });
|
|
81
123
|
* const filter = await FilterAPI.create(
|
|
82
|
-
* '
|
|
83
|
-
*
|
|
124
|
+
* 'scale_vt=640:480,hwdownload,format=yuv420p',
|
|
125
|
+
* decoder.getOutputStreamInfo(),
|
|
84
126
|
* { hardware: hw }
|
|
85
127
|
* );
|
|
86
128
|
*
|
|
@@ -96,106 +138,37 @@ export class FilterAPI {
|
|
|
96
138
|
*/
|
|
97
139
|
static async create(description, input, options = {}) {
|
|
98
140
|
let config;
|
|
99
|
-
if (input
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
else if (input.codecpar.codecType === AVMEDIA_TYPE_AUDIO) {
|
|
112
|
-
config = {
|
|
113
|
-
type: 'audio',
|
|
114
|
-
sampleRate: input.codecpar.sampleRate,
|
|
115
|
-
sampleFormat: input.codecpar.format,
|
|
116
|
-
channelLayout: input.codecpar.channelLayout.mask,
|
|
117
|
-
timeBase: input.timeBase,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
throw new Error('Unsupported codec type');
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
if (input.type === 'video') {
|
|
126
|
-
config = {
|
|
127
|
-
type: 'video',
|
|
128
|
-
width: input.width,
|
|
129
|
-
height: input.height,
|
|
130
|
-
pixelFormat: input.pixelFormat,
|
|
131
|
-
timeBase: input.timeBase,
|
|
132
|
-
frameRate: input.frameRate,
|
|
133
|
-
sampleAspectRatio: input.sampleAspectRatio,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
config = {
|
|
138
|
-
type: 'audio',
|
|
139
|
-
sampleRate: input.sampleRate,
|
|
140
|
-
sampleFormat: input.sampleFormat,
|
|
141
|
-
channelLayout: typeof input.channelLayout === 'bigint' ? input.channelLayout : input.channelLayout.mask || 3n,
|
|
142
|
-
timeBase: input.timeBase,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
const filter = new FilterAPI(config, options.hardware);
|
|
147
|
-
// Parse the entire filter chain to check if ANY filter requires hardware
|
|
148
|
-
// Split by comma to get individual filters, handle complex chains like:
|
|
149
|
-
// "format=nv12,hwupload,scale_vt=100:100,hwdownload,format=yuv420p"
|
|
150
|
-
const filterNames = description
|
|
151
|
-
.split(',')
|
|
152
|
-
.map((f) => {
|
|
153
|
-
// Extract filter name (before = or : or whitespace)
|
|
154
|
-
const match = /^([a-zA-Z0-9_]+)/.exec(f.trim());
|
|
155
|
-
return match ? match[1] : null;
|
|
156
|
-
})
|
|
157
|
-
.filter(Boolean);
|
|
158
|
-
// Check if chain contains hwupload (which creates hw frames context)
|
|
159
|
-
const hasHwDownload = filterNames.some((name) => name === 'hwdownload');
|
|
160
|
-
const hasHwUpload = filterNames.some((name) => name === 'hwupload');
|
|
161
|
-
// Check each filter in the chain
|
|
162
|
-
let needsHardwareFramesContext = false;
|
|
163
|
-
let needsHardwareDevice = false;
|
|
164
|
-
for (const filterName of filterNames) {
|
|
165
|
-
if (!filterName)
|
|
166
|
-
continue;
|
|
167
|
-
const lowLevelFilter = LowLevelFilter.getByName(filterName);
|
|
168
|
-
if (lowLevelFilter) {
|
|
169
|
-
// Check if this filter needs hardware
|
|
170
|
-
if ((lowLevelFilter.flags & AVFILTER_FLAG_HWDEVICE) !== 0) {
|
|
171
|
-
needsHardwareDevice = true;
|
|
172
|
-
// Only non-hwupload filters need frames context from decoder
|
|
173
|
-
if (filterName !== 'hwupload' && filterName !== 'hwdownload') {
|
|
174
|
-
needsHardwareFramesContext = true;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
// If we have hwupload, we don't need hardware frames context from decoder
|
|
180
|
-
filter.needsHardware = hasHwDownload || (needsHardwareFramesContext && !hasHwUpload);
|
|
181
|
-
// Validation: Hardware filter MUST have HardwareContext
|
|
182
|
-
if (needsHardwareDevice && !options.hardware) {
|
|
183
|
-
throw new Error('Hardware filter in chain requires a hardware context. ' + 'Please provide one via options.hardware');
|
|
184
|
-
}
|
|
185
|
-
// Check if we can initialize immediately
|
|
186
|
-
// Initialize if: (1) we don't need hardware, OR (2) we need hardware AND have framesContext
|
|
187
|
-
if (!filter.needsHardware || (filter.needsHardware && options.hardware?.framesContext)) {
|
|
188
|
-
// Can initialize now
|
|
189
|
-
if (options.hardware?.framesContext && config.type === 'video') {
|
|
190
|
-
config.hwFramesCtx = options.hardware.framesContext;
|
|
191
|
-
}
|
|
192
|
-
await filter.initialize(description, options);
|
|
193
|
-
filter.initialized = true;
|
|
141
|
+
if (input.type === 'video') {
|
|
142
|
+
config = {
|
|
143
|
+
type: 'video',
|
|
144
|
+
width: input.width,
|
|
145
|
+
height: input.height,
|
|
146
|
+
pixelFormat: input.pixelFormat,
|
|
147
|
+
timeBase: input.timeBase,
|
|
148
|
+
frameRate: input.frameRate,
|
|
149
|
+
sampleAspectRatio: input.sampleAspectRatio,
|
|
150
|
+
};
|
|
194
151
|
}
|
|
195
152
|
else {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
153
|
+
config = {
|
|
154
|
+
type: 'audio',
|
|
155
|
+
sampleRate: input.sampleRate,
|
|
156
|
+
sampleFormat: input.sampleFormat,
|
|
157
|
+
channelLayout: input.channelLayout,
|
|
158
|
+
timeBase: input.timeBase,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const filter = new FilterAPI(config, description, options);
|
|
162
|
+
// Check if any filters in the chain require hardware context
|
|
163
|
+
if (config.type === 'video') {
|
|
164
|
+
filter.checkHardwareRequirements(description, options);
|
|
165
|
+
}
|
|
166
|
+
// For video filters, always use lazy initialization to properly detect hardware requirements
|
|
167
|
+
// For audio filters, initialize immediately (no hardware audio processing)
|
|
168
|
+
if (config.type === 'audio') {
|
|
169
|
+
await filter.initialize(null);
|
|
170
|
+
}
|
|
171
|
+
// For video: wait for first frame to detect if hw_frames_ctx is present
|
|
199
172
|
return filter;
|
|
200
173
|
}
|
|
201
174
|
/**
|
|
@@ -204,11 +177,16 @@ export class FilterAPI {
|
|
|
204
177
|
* Sends a frame through the filter graph and returns the filtered result.
|
|
205
178
|
* May return null if the filter needs more input frames.
|
|
206
179
|
*
|
|
180
|
+
* On first frame with hw_frames_ctx, initializes the filter graph (lazy initialization).
|
|
181
|
+
* Subsequent frames are processed normally. FFmpeg automatically propagates
|
|
182
|
+
* hw_frames_ctx through the filter chain.
|
|
183
|
+
*
|
|
207
184
|
* @param frame - Input frame to filter
|
|
208
185
|
*
|
|
209
|
-
* @returns Promise resolving to filtered frame or null
|
|
186
|
+
* @returns Promise resolving to filtered frame or null if more input needed
|
|
210
187
|
*
|
|
211
188
|
* @throws {FFmpegError} If processing fails
|
|
189
|
+
* @throws {Error} If filter not initialized or hardware frame required but not provided
|
|
212
190
|
*
|
|
213
191
|
* @example
|
|
214
192
|
* ```typescript
|
|
@@ -219,29 +197,9 @@ export class FilterAPI {
|
|
|
219
197
|
* ```
|
|
220
198
|
*/
|
|
221
199
|
async process(frame) {
|
|
222
|
-
//
|
|
223
|
-
if (!this.initialized && this.
|
|
224
|
-
|
|
225
|
-
if (this.hardware?.framesContext && this.config.type === 'video') {
|
|
226
|
-
this.config.hwFramesCtx = this.hardware.framesContext;
|
|
227
|
-
// Update pixel format to match hardware frames if using hardware
|
|
228
|
-
if (this.needsHardware) {
|
|
229
|
-
this.config.pixelFormat = this.hardware.getHardwarePixelFormat();
|
|
230
|
-
}
|
|
231
|
-
// Now we can initialize
|
|
232
|
-
await this.initialize(this.pendingInit.description, this.pendingInit.options);
|
|
233
|
-
this.pendingInit = undefined;
|
|
234
|
-
this.initialized = true;
|
|
235
|
-
}
|
|
236
|
-
else if (this.needsHardware) {
|
|
237
|
-
throw new Error('Hardware filter requires frames context which is not yet available');
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
// Software filter or hardware not required, can initialize now
|
|
241
|
-
await this.initialize(this.pendingInit.description, this.pendingInit.options);
|
|
242
|
-
this.pendingInit = undefined;
|
|
243
|
-
this.initialized = true;
|
|
244
|
-
}
|
|
200
|
+
// Lazy initialization for video filters (detect hardware from first frame)
|
|
201
|
+
if (!this.initialized && this.config.type === 'video') {
|
|
202
|
+
await this.initialize(frame);
|
|
245
203
|
}
|
|
246
204
|
if (!this.initialized || !this.buffersrcCtx || !this.buffersinkCtx) {
|
|
247
205
|
throw new Error('Filter not initialized');
|
|
@@ -419,7 +377,7 @@ export class FilterAPI {
|
|
|
419
377
|
* ```typescript
|
|
420
378
|
* for await (const filtered of filter.frames(decoder.frames())) {
|
|
421
379
|
* // Process filtered frame
|
|
422
|
-
* filtered
|
|
380
|
+
* using _ = filtered; // Auto cleanup with using statement
|
|
423
381
|
* }
|
|
424
382
|
* ```
|
|
425
383
|
*/
|
|
@@ -453,6 +411,71 @@ export class FilterAPI {
|
|
|
453
411
|
yield remaining;
|
|
454
412
|
}
|
|
455
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Send a command to a filter in the graph.
|
|
416
|
+
*
|
|
417
|
+
* Allows runtime modification of filter parameters without recreating the graph.
|
|
418
|
+
* Not all filters support commands - check filter documentation.
|
|
419
|
+
*
|
|
420
|
+
* @param target - Filter name or "all" to send to all filters
|
|
421
|
+
* @param cmd - Command name (e.g., "volume", "hue", "brightness")
|
|
422
|
+
* @param arg - Command argument value
|
|
423
|
+
* @param flags - Optional command flags
|
|
424
|
+
*
|
|
425
|
+
* @returns Command response
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* ```typescript
|
|
429
|
+
* // Change volume dynamically
|
|
430
|
+
* const response = filter.sendCommand('volume', 'volume', '0.5');
|
|
431
|
+
* if (response) {
|
|
432
|
+
* console.log('Volume changed successfully');
|
|
433
|
+
* }
|
|
434
|
+
* ```
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* ```typescript
|
|
438
|
+
* // Enable/disable all filters at runtime
|
|
439
|
+
* filter.sendCommand('all', 'enable', 'expr=gte(t,10)');
|
|
440
|
+
* ```
|
|
441
|
+
*/
|
|
442
|
+
sendCommand(target, cmd, arg, flags) {
|
|
443
|
+
if (!this.initialized || !this.graph) {
|
|
444
|
+
throw new Error('Filter not initialized');
|
|
445
|
+
}
|
|
446
|
+
const result = this.graph.sendCommand(target, cmd, arg, flags);
|
|
447
|
+
if (typeof result === 'number') {
|
|
448
|
+
FFmpegError.throwIfError(result, 'Failed to send filter command');
|
|
449
|
+
}
|
|
450
|
+
return result.response;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Queue a command to be executed at a specific time.
|
|
454
|
+
*
|
|
455
|
+
* Commands are executed when processing frames with matching timestamps.
|
|
456
|
+
* Useful for scripted filter changes synchronized with media playback.
|
|
457
|
+
*
|
|
458
|
+
* @param target - Filter name or "all" to send to all filters
|
|
459
|
+
* @param cmd - Command name (e.g., "volume", "hue", "brightness")
|
|
460
|
+
* @param arg - Command argument value
|
|
461
|
+
* @param ts - Timestamp when command should execute (in seconds)
|
|
462
|
+
* @param flags - Optional command flags
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* ```typescript
|
|
466
|
+
* // Schedule volume changes at specific times
|
|
467
|
+
* filter.queueCommand('volume', 'volume', '0.5', 5.0); // At 5 seconds
|
|
468
|
+
* filter.queueCommand('volume', 'volume', '0.8', 10.0); // At 10 seconds
|
|
469
|
+
* filter.queueCommand('volume', 'volume', '0.2', 15.0); // At 15 seconds
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
472
|
+
queueCommand(target, cmd, arg, ts, flags) {
|
|
473
|
+
if (!this.initialized || !this.graph) {
|
|
474
|
+
throw new Error('Filter not initialized');
|
|
475
|
+
}
|
|
476
|
+
const ret = this.graph.queueCommand(target, cmd, arg, ts, flags);
|
|
477
|
+
FFmpegError.throwIfError(ret, 'Failed to queue filter command');
|
|
478
|
+
}
|
|
456
479
|
/**
|
|
457
480
|
* Get the filter graph description.
|
|
458
481
|
*
|
|
@@ -468,7 +491,7 @@ export class FilterAPI {
|
|
|
468
491
|
* ```
|
|
469
492
|
*/
|
|
470
493
|
getGraphDescription() {
|
|
471
|
-
if (!this.initialized) {
|
|
494
|
+
if (!this.initialized || !this.graph) {
|
|
472
495
|
return null;
|
|
473
496
|
}
|
|
474
497
|
return this.graph.dump();
|
|
@@ -489,14 +512,6 @@ export class FilterAPI {
|
|
|
489
512
|
getMediaType() {
|
|
490
513
|
return this.mediaType;
|
|
491
514
|
}
|
|
492
|
-
/**
|
|
493
|
-
* Get the filter configuration.
|
|
494
|
-
*
|
|
495
|
-
* @returns The filter configuration used to create this instance
|
|
496
|
-
*/
|
|
497
|
-
getConfig() {
|
|
498
|
-
return this.config;
|
|
499
|
-
}
|
|
500
515
|
/**
|
|
501
516
|
* Free all filter resources.
|
|
502
517
|
*
|
|
@@ -512,6 +527,7 @@ export class FilterAPI {
|
|
|
512
527
|
free() {
|
|
513
528
|
if (this.graph) {
|
|
514
529
|
this.graph.free();
|
|
530
|
+
this.graph = null;
|
|
515
531
|
}
|
|
516
532
|
this.buffersrcCtx = null;
|
|
517
533
|
this.buffersinkCtx = null;
|
|
@@ -523,34 +539,41 @@ export class FilterAPI {
|
|
|
523
539
|
* Sets up buffer source, buffer sink, and parses the filter description.
|
|
524
540
|
* Configures the graph for processing.
|
|
525
541
|
*
|
|
542
|
+
* For hardware inputs: Uses hw_frames_ctx from first frame
|
|
543
|
+
* For software inputs: Initializes without hw_frames_ctx
|
|
544
|
+
*
|
|
526
545
|
* @internal
|
|
527
546
|
*/
|
|
528
|
-
async initialize(
|
|
529
|
-
//
|
|
547
|
+
async initialize(firstFrame) {
|
|
548
|
+
// Create graph
|
|
549
|
+
this.graph = new FilterGraph();
|
|
530
550
|
this.graph.alloc();
|
|
531
551
|
// Configure threading
|
|
532
|
-
if (options.threads !== undefined) {
|
|
533
|
-
this.graph.nbThreads = options.threads;
|
|
552
|
+
if (this.options.threads !== undefined) {
|
|
553
|
+
this.graph.nbThreads = this.options.threads;
|
|
534
554
|
}
|
|
535
555
|
// Configure scaler options
|
|
536
|
-
if (options.scaleSwsOpts) {
|
|
537
|
-
this.graph.scaleSwsOpts = options.scaleSwsOpts;
|
|
556
|
+
if (this.options.scaleSwsOpts) {
|
|
557
|
+
this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
|
|
558
|
+
}
|
|
559
|
+
// Create buffer source with hw_frames_ctx if needed
|
|
560
|
+
if (firstFrame?.hwFramesCtx && this.config.type === 'video') {
|
|
561
|
+
this.createBufferSourceWithHwFrames(firstFrame);
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
this.createBufferSource();
|
|
538
565
|
}
|
|
539
|
-
// Create buffer source
|
|
540
|
-
this.createBufferSource();
|
|
541
566
|
// Create buffer sink
|
|
542
567
|
this.createBufferSink();
|
|
543
568
|
// Parse filter description
|
|
544
|
-
this.parseFilterDescription(description);
|
|
545
|
-
// Set hw_device_ctx on hardware filters
|
|
569
|
+
this.parseFilterDescription(this.description);
|
|
570
|
+
// Set hw_device_ctx on hardware filters
|
|
546
571
|
if (this.hardware?.deviceContext) {
|
|
547
572
|
const filters = this.graph.filters;
|
|
548
573
|
if (filters) {
|
|
549
574
|
for (const filterCtx of filters) {
|
|
550
|
-
// Check if this filter needs hardware device context
|
|
551
575
|
const filter = filterCtx.filter;
|
|
552
576
|
if (filter && (filter.flags & AVFILTER_FLAG_HWDEVICE) !== 0) {
|
|
553
|
-
// Set hardware device context on this filter
|
|
554
577
|
filterCtx.hwDeviceCtx = this.hardware.deviceContext;
|
|
555
578
|
}
|
|
556
579
|
}
|
|
@@ -562,65 +585,69 @@ export class FilterAPI {
|
|
|
562
585
|
this.initialized = true;
|
|
563
586
|
}
|
|
564
587
|
/**
|
|
565
|
-
* Create
|
|
588
|
+
* Create buffer source with hardware frames context.
|
|
589
|
+
*
|
|
590
|
+
* @internal
|
|
591
|
+
*/
|
|
592
|
+
createBufferSourceWithHwFrames(frame) {
|
|
593
|
+
const filterName = 'buffer';
|
|
594
|
+
const bufferFilter = Filter.getByName(filterName);
|
|
595
|
+
if (!bufferFilter) {
|
|
596
|
+
throw new Error(`${filterName} filter not found`);
|
|
597
|
+
}
|
|
598
|
+
// Allocate filter without args
|
|
599
|
+
this.buffersrcCtx = this.graph.allocFilter(bufferFilter, 'in');
|
|
600
|
+
if (!this.buffersrcCtx) {
|
|
601
|
+
throw new Error('Failed to allocate buffer source');
|
|
602
|
+
}
|
|
603
|
+
// Set parameters including hw_frames_ctx
|
|
604
|
+
const cfg = this.config;
|
|
605
|
+
const ret = this.buffersrcCtx.buffersrcParametersSet({
|
|
606
|
+
width: cfg.width,
|
|
607
|
+
height: cfg.height,
|
|
608
|
+
format: cfg.pixelFormat,
|
|
609
|
+
timeBase: cfg.timeBase,
|
|
610
|
+
frameRate: cfg.frameRate,
|
|
611
|
+
sampleAspectRatio: cfg.sampleAspectRatio,
|
|
612
|
+
hwFramesCtx: frame.hwFramesCtx ?? undefined,
|
|
613
|
+
});
|
|
614
|
+
FFmpegError.throwIfError(ret, 'Failed to set buffer source parameters');
|
|
615
|
+
// Initialize filter
|
|
616
|
+
const initRet = this.buffersrcCtx.init(null);
|
|
617
|
+
FFmpegError.throwIfError(initRet, 'Failed to initialize buffer source');
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Create and configure the buffer source filter without hw_frames_ctx.
|
|
566
621
|
*
|
|
567
622
|
* @internal
|
|
568
623
|
*/
|
|
569
624
|
createBufferSource() {
|
|
570
625
|
const filterName = this.config.type === 'video' ? 'buffer' : 'abuffer';
|
|
571
|
-
const bufferFilter =
|
|
626
|
+
const bufferFilter = Filter.getByName(filterName);
|
|
572
627
|
if (!bufferFilter) {
|
|
573
628
|
throw new Error(`${filterName} filter not found`);
|
|
574
629
|
}
|
|
575
|
-
//
|
|
576
|
-
|
|
577
|
-
if (
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (
|
|
581
|
-
|
|
630
|
+
// Build args string
|
|
631
|
+
let args;
|
|
632
|
+
if (this.config.type === 'video') {
|
|
633
|
+
const cfg = this.config;
|
|
634
|
+
args = `video_size=${cfg.width}x${cfg.height}:pix_fmt=${cfg.pixelFormat}:time_base=${cfg.timeBase.num}/${cfg.timeBase.den}`;
|
|
635
|
+
if (cfg.frameRate) {
|
|
636
|
+
args += `:frame_rate=${cfg.frameRate.num}/${cfg.frameRate.den}`;
|
|
637
|
+
}
|
|
638
|
+
if (cfg.sampleAspectRatio) {
|
|
639
|
+
args += `:pixel_aspect=${cfg.sampleAspectRatio.num}/${cfg.sampleAspectRatio.den}`;
|
|
582
640
|
}
|
|
583
|
-
// Set parameters including hardware frames context (BEFORE init)
|
|
584
|
-
const videoConfig = this.config;
|
|
585
|
-
const ret = this.buffersrcCtx.buffersrcParametersSet({
|
|
586
|
-
width: videoConfig.width,
|
|
587
|
-
height: videoConfig.height,
|
|
588
|
-
format: videoConfig.pixelFormat,
|
|
589
|
-
timeBase: videoConfig.timeBase,
|
|
590
|
-
frameRate: videoConfig.frameRate,
|
|
591
|
-
sampleAspectRatio: videoConfig.sampleAspectRatio,
|
|
592
|
-
hwFramesCtx: videoConfig.hwFramesCtx,
|
|
593
|
-
});
|
|
594
|
-
FFmpegError.throwIfError(ret, 'Failed to set buffer source parameters with hardware frames context');
|
|
595
|
-
// Initialize filter AFTER setting parameters
|
|
596
|
-
const initRet = this.buffersrcCtx.init(null);
|
|
597
|
-
FFmpegError.throwIfError(initRet, 'Failed to initialize buffer source');
|
|
598
641
|
}
|
|
599
642
|
else {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if (cfg.sampleAspectRatio) {
|
|
609
|
-
args += `:pixel_aspect=${cfg.sampleAspectRatio.num}/${cfg.sampleAspectRatio.den}`;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
else {
|
|
613
|
-
const cfg = this.config;
|
|
614
|
-
// Use sample format name from utilities
|
|
615
|
-
const sampleFmtName = avGetSampleFmtName(cfg.sampleFormat);
|
|
616
|
-
// Handle invalid channel layout (0) by using stereo as default
|
|
617
|
-
const channelLayout = cfg.channelLayout === 0n ? 'stereo' : cfg.channelLayout.toString();
|
|
618
|
-
args = `sample_rate=${cfg.sampleRate}:sample_fmt=${sampleFmtName}:channel_layout=${channelLayout}:time_base=${cfg.timeBase.num}/${cfg.timeBase.den}`;
|
|
619
|
-
}
|
|
620
|
-
this.buffersrcCtx = this.graph.createFilter(bufferFilter, 'in', args);
|
|
621
|
-
if (!this.buffersrcCtx) {
|
|
622
|
-
throw new Error('Failed to create buffer source');
|
|
623
|
-
}
|
|
643
|
+
const cfg = this.config;
|
|
644
|
+
const sampleFmtName = avGetSampleFmtName(cfg.sampleFormat);
|
|
645
|
+
const channelLayout = cfg.channelLayout.mask === 0n ? 'stereo' : cfg.channelLayout.mask.toString();
|
|
646
|
+
args = `sample_rate=${cfg.sampleRate}:sample_fmt=${sampleFmtName}:channel_layout=${channelLayout}:time_base=${cfg.timeBase.num}/${cfg.timeBase.den}`;
|
|
647
|
+
}
|
|
648
|
+
this.buffersrcCtx = this.graph.createFilter(bufferFilter, 'in', args);
|
|
649
|
+
if (!this.buffersrcCtx) {
|
|
650
|
+
throw new Error('Failed to create buffer source');
|
|
624
651
|
}
|
|
625
652
|
}
|
|
626
653
|
/**
|
|
@@ -629,12 +656,14 @@ export class FilterAPI {
|
|
|
629
656
|
* @internal
|
|
630
657
|
*/
|
|
631
658
|
createBufferSink() {
|
|
659
|
+
if (!this.graph) {
|
|
660
|
+
throw new Error('Filter graph not initialized');
|
|
661
|
+
}
|
|
632
662
|
const filterName = this.config.type === 'video' ? 'buffersink' : 'abuffersink';
|
|
633
|
-
const sinkFilter =
|
|
663
|
+
const sinkFilter = Filter.getByName(filterName);
|
|
634
664
|
if (!sinkFilter) {
|
|
635
665
|
throw new Error(`${filterName} filter not found`);
|
|
636
666
|
}
|
|
637
|
-
// Create sink filter - no automatic format conversion
|
|
638
667
|
this.buffersinkCtx = this.graph.createFilter(sinkFilter, 'out', null);
|
|
639
668
|
if (!this.buffersinkCtx) {
|
|
640
669
|
throw new Error('Failed to create buffer sink');
|
|
@@ -646,6 +675,9 @@ export class FilterAPI {
|
|
|
646
675
|
* @internal
|
|
647
676
|
*/
|
|
648
677
|
parseFilterDescription(description) {
|
|
678
|
+
if (!this.graph) {
|
|
679
|
+
throw new Error('Filter graph not initialized');
|
|
680
|
+
}
|
|
649
681
|
if (!this.buffersrcCtx || !this.buffersinkCtx) {
|
|
650
682
|
throw new Error('Buffer filters not initialized');
|
|
651
683
|
}
|
|
@@ -657,12 +689,12 @@ export class FilterAPI {
|
|
|
657
689
|
return;
|
|
658
690
|
}
|
|
659
691
|
// Set up inputs and outputs for parsing
|
|
660
|
-
const outputs = new
|
|
692
|
+
const outputs = new FilterInOut();
|
|
661
693
|
outputs.alloc();
|
|
662
694
|
outputs.name = 'in';
|
|
663
695
|
outputs.filterCtx = this.buffersrcCtx;
|
|
664
696
|
outputs.padIdx = 0;
|
|
665
|
-
const inputs = new
|
|
697
|
+
const inputs = new FilterInOut();
|
|
666
698
|
inputs.alloc();
|
|
667
699
|
inputs.name = 'out';
|
|
668
700
|
inputs.filterCtx = this.buffersinkCtx;
|
|
@@ -670,80 +702,74 @@ export class FilterAPI {
|
|
|
670
702
|
// Parse the filter graph
|
|
671
703
|
const ret = this.graph.parsePtr(description, inputs, outputs);
|
|
672
704
|
FFmpegError.throwIfError(ret, 'Failed to parse filter description');
|
|
673
|
-
// Clean up
|
|
705
|
+
// Clean up
|
|
674
706
|
inputs.free();
|
|
675
707
|
outputs.free();
|
|
676
708
|
}
|
|
677
709
|
/**
|
|
678
|
-
*
|
|
710
|
+
* Check if hardware context is required for the filter chain.
|
|
679
711
|
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
712
|
+
* Validates that hardware context is provided when needed:
|
|
713
|
+
* - hwupload: Always requires hardware context
|
|
714
|
+
* - Hardware filters (AVFILTER_FLAG_HWDEVICE): Recommend hardware context
|
|
715
|
+
* - hwdownload: Warns if input is not hardware format
|
|
682
716
|
*
|
|
683
|
-
* @
|
|
684
|
-
* @param cmd - Command name (e.g., "volume", "hue", "brightness")
|
|
685
|
-
* @param arg - Command argument value
|
|
686
|
-
* @param flags - Optional command flags
|
|
687
|
-
*
|
|
688
|
-
* @returns Command response
|
|
689
|
-
*
|
|
690
|
-
* @example
|
|
691
|
-
* ```typescript
|
|
692
|
-
* // Change volume dynamically
|
|
693
|
-
* const response = filter.sendCommand('volume', 'volume', '0.5');
|
|
694
|
-
* if (response) {
|
|
695
|
-
* console.log('Volume changed successfully');
|
|
696
|
-
* }
|
|
697
|
-
* ```
|
|
698
|
-
*
|
|
699
|
-
* @example
|
|
700
|
-
* ```typescript
|
|
701
|
-
* // Enable/disable all filters at runtime
|
|
702
|
-
* filter.sendCommand('all', 'enable', 'expr=gte(t,10)');
|
|
703
|
-
* ```
|
|
717
|
+
* @internal
|
|
704
718
|
*/
|
|
705
|
-
|
|
706
|
-
if (
|
|
707
|
-
|
|
708
|
-
}
|
|
709
|
-
const result = this.graph.sendCommand(target, cmd, arg, flags);
|
|
710
|
-
if (typeof result === 'number') {
|
|
711
|
-
FFmpegError.throwIfError(result, 'Failed to send filter command');
|
|
719
|
+
checkHardwareRequirements(description, options) {
|
|
720
|
+
if (this.config.type !== 'video') {
|
|
721
|
+
return;
|
|
712
722
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
723
|
+
// Parse filter names from description
|
|
724
|
+
const filterNames = description
|
|
725
|
+
.split(',')
|
|
726
|
+
.map((f) => {
|
|
727
|
+
// Extract filter name (before = or : or whitespace)
|
|
728
|
+
const match = /^([a-zA-Z0-9_]+)/.exec(f.trim());
|
|
729
|
+
return match ? match[1] : null;
|
|
730
|
+
})
|
|
731
|
+
.filter(Boolean);
|
|
732
|
+
for (const filterName of filterNames) {
|
|
733
|
+
const lowLevelFilter = Filter.getByName(filterName);
|
|
734
|
+
if (!lowLevelFilter) {
|
|
735
|
+
// Filter will be validated later during graph parsing
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (!options.hardware) {
|
|
739
|
+
if (filterName === 'hwupload' || filterName === 'hwupload_cuda' || (lowLevelFilter.flags & AVFILTER_FLAG_HWDEVICE) !== 0) {
|
|
740
|
+
throw new Error(`Filter '${filterName}' requires a hardware context`);
|
|
741
|
+
}
|
|
742
|
+
else if (filterName === 'hwdownload' && !avIsHardwarePixelFormat(this.config.pixelFormat)) {
|
|
743
|
+
throw new Error(`Pixel Format '${this.config.pixelFormat}' is not hardware compatible`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
// // Check if this is hwupload - always needs hardware context
|
|
747
|
+
// if (filterName === 'hwupload' || filterName === 'hwupload_cuda') {
|
|
748
|
+
// if (!options.hardware) {
|
|
749
|
+
// throw new Error(`Filter '${filterName}' requires a hardware context`);
|
|
750
|
+
// }
|
|
751
|
+
// } else if (filterName === 'hwdownload') {
|
|
752
|
+
// // Check if this is hwdownload - warn if input is not hardware format
|
|
753
|
+
// if (this.config.type === 'video' && !avIsHardwarePixelFormat(this.config.pixelFormat)) {
|
|
754
|
+
// // prettier-ignore
|
|
755
|
+
// console.warn(
|
|
756
|
+
// `Warning: 'hwdownload' filter used with software input format (${this.config.pixelFormat}). ` +
|
|
757
|
+
// 'This will likely fail at runtime. hwdownload expects hardware frames as input. ' +
|
|
758
|
+
// 'Consider removing hwdownload from your filter chain or ensuring hardware input.',
|
|
759
|
+
// );
|
|
760
|
+
// }
|
|
761
|
+
// } else if ((lowLevelFilter.flags & AVFILTER_FLAG_HWDEVICE) !== 0) {
|
|
762
|
+
// // Check if this is a hardware filter
|
|
763
|
+
// if (!options.hardware) {
|
|
764
|
+
// // prettier-ignore
|
|
765
|
+
// console.warn(
|
|
766
|
+
// `Warning: Hardware filter '${filterName}' used without hardware context. ` +
|
|
767
|
+
// "This may work if hw_frames_ctx is propagated from input, but it's recommended " +
|
|
768
|
+
// 'to pass { hardware: HardwareContext } in filter options.',
|
|
769
|
+
// );
|
|
770
|
+
// }
|
|
771
|
+
// }
|
|
744
772
|
}
|
|
745
|
-
const ret = this.graph.queueCommand(target, cmd, arg, ts, flags);
|
|
746
|
-
FFmpegError.throwIfError(ret, 'Failed to queue filter command');
|
|
747
773
|
}
|
|
748
774
|
/**
|
|
749
775
|
* Dispose of the filter.
|
|
@@ -763,114 +789,4 @@ export class FilterAPI {
|
|
|
763
789
|
this.free();
|
|
764
790
|
}
|
|
765
791
|
}
|
|
766
|
-
/**
|
|
767
|
-
* Common filter presets for convenience.
|
|
768
|
-
*
|
|
769
|
-
* Provides pre-defined filter strings for common operations.
|
|
770
|
-
* Can be used with Filter.create() for quick setup.
|
|
771
|
-
*
|
|
772
|
-
* @example
|
|
773
|
-
* ```typescript
|
|
774
|
-
* const filter = await Filter.create(
|
|
775
|
-
* FilterPresets.scale(1280, 720),
|
|
776
|
-
* config
|
|
777
|
-
* );
|
|
778
|
-
* ```
|
|
779
|
-
*/
|
|
780
|
-
export class FilterPresets {
|
|
781
|
-
/**
|
|
782
|
-
* Scale video to specified dimensions.
|
|
783
|
-
*/
|
|
784
|
-
static scale(width, height, flags) {
|
|
785
|
-
const base = `scale=${width}:${height}`;
|
|
786
|
-
return flags ? `${base}:flags=${flags}` : base;
|
|
787
|
-
}
|
|
788
|
-
/**
|
|
789
|
-
* Crop video to specified dimensions.
|
|
790
|
-
*/
|
|
791
|
-
static crop(width, height, x = 0, y = 0) {
|
|
792
|
-
return `crop=${width}:${height}:${x}:${y}`;
|
|
793
|
-
}
|
|
794
|
-
/**
|
|
795
|
-
* Change frame rate.
|
|
796
|
-
*/
|
|
797
|
-
static fps(fps) {
|
|
798
|
-
return `fps=${fps}`;
|
|
799
|
-
}
|
|
800
|
-
/**
|
|
801
|
-
* Convert pixel format.
|
|
802
|
-
* Can accept either format name string or AVPixelFormat enum.
|
|
803
|
-
*/
|
|
804
|
-
static format(pixelFormat) {
|
|
805
|
-
const formatName = typeof pixelFormat === 'string' ? pixelFormat : (avGetPixFmtName(pixelFormat) ?? 'yuv420p');
|
|
806
|
-
return `format=${formatName}`;
|
|
807
|
-
}
|
|
808
|
-
/**
|
|
809
|
-
* Rotate video by angle.
|
|
810
|
-
*/
|
|
811
|
-
static rotate(angle) {
|
|
812
|
-
return `rotate=${angle}*PI/180`;
|
|
813
|
-
}
|
|
814
|
-
/**
|
|
815
|
-
* Flip video horizontally.
|
|
816
|
-
*/
|
|
817
|
-
static hflip() {
|
|
818
|
-
return 'hflip';
|
|
819
|
-
}
|
|
820
|
-
/**
|
|
821
|
-
* Flip video vertically.
|
|
822
|
-
*/
|
|
823
|
-
static vflip() {
|
|
824
|
-
return 'vflip';
|
|
825
|
-
}
|
|
826
|
-
/**
|
|
827
|
-
* Apply fade effect.
|
|
828
|
-
*/
|
|
829
|
-
static fade(type, start, duration) {
|
|
830
|
-
return `fade=t=${type}:st=${start}:d=${duration}`;
|
|
831
|
-
}
|
|
832
|
-
/**
|
|
833
|
-
* Overlay one video on another.
|
|
834
|
-
*/
|
|
835
|
-
static overlay(x = 0, y = 0) {
|
|
836
|
-
return `overlay=${x}:${y}`;
|
|
837
|
-
}
|
|
838
|
-
/**
|
|
839
|
-
* Adjust audio volume.
|
|
840
|
-
*/
|
|
841
|
-
static volume(factor) {
|
|
842
|
-
return `volume=${factor}`;
|
|
843
|
-
}
|
|
844
|
-
/**
|
|
845
|
-
* Convert audio sample format.
|
|
846
|
-
* Can accept either format name string or AVSampleFormat enum.
|
|
847
|
-
*/
|
|
848
|
-
static aformat(sampleFormat, sampleRate, channelLayout) {
|
|
849
|
-
const formatName = typeof sampleFormat === 'string' ? sampleFormat : (avGetSampleFmtName(sampleFormat) ?? 's16');
|
|
850
|
-
let filter = `aformat=sample_fmts=${formatName}`;
|
|
851
|
-
if (sampleRate)
|
|
852
|
-
filter += `:sample_rates=${sampleRate}`;
|
|
853
|
-
if (channelLayout)
|
|
854
|
-
filter += `:channel_layouts=${channelLayout}`;
|
|
855
|
-
return filter;
|
|
856
|
-
}
|
|
857
|
-
/**
|
|
858
|
-
* Change audio tempo without changing pitch.
|
|
859
|
-
*/
|
|
860
|
-
static atempo(factor) {
|
|
861
|
-
return `atempo=${factor}`;
|
|
862
|
-
}
|
|
863
|
-
/**
|
|
864
|
-
* Apply audio fade.
|
|
865
|
-
*/
|
|
866
|
-
static afade(type, start, duration) {
|
|
867
|
-
return `afade=t=${type}:st=${start}:d=${duration}`;
|
|
868
|
-
}
|
|
869
|
-
/**
|
|
870
|
-
* Mix multiple audio streams.
|
|
871
|
-
*/
|
|
872
|
-
static amix(inputs = 2, duration = 'longest') {
|
|
873
|
-
return `amix=inputs=${inputs}:duration=${duration}`;
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
792
|
//# sourceMappingURL=filter.js.map
|