node-av 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -59
- package/dist/api/bitstream-filter.d.ts +5 -2
- package/dist/api/bitstream-filter.js +7 -4
- package/dist/api/bitstream-filter.js.map +1 -1
- package/dist/api/decoder.d.ts +135 -119
- package/dist/api/decoder.js +195 -202
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/encoder.d.ts +141 -78
- package/dist/api/encoder.js +241 -193
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/filter-presets.d.ts +699 -573
- package/dist/api/filter-presets.js +1157 -840
- package/dist/api/filter-presets.js.map +1 -1
- package/dist/api/filter.d.ts +180 -157
- package/dist/api/filter.js +314 -366
- package/dist/api/filter.js.map +1 -1
- package/dist/api/hardware.d.ts +28 -29
- package/dist/api/hardware.js +80 -74
- package/dist/api/hardware.js.map +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/io-stream.d.ts +6 -0
- package/dist/api/io-stream.js +6 -0
- package/dist/api/io-stream.js.map +1 -1
- package/dist/api/media-input.d.ts +2 -1
- package/dist/api/media-input.js +3 -8
- package/dist/api/media-input.js.map +1 -1
- package/dist/api/media-output.d.ts +37 -126
- package/dist/api/media-output.js +138 -206
- package/dist/api/media-output.js.map +1 -1
- package/dist/api/pipeline.d.ts +193 -0
- package/dist/api/pipeline.js +36 -42
- package/dist/api/pipeline.js.map +1 -1
- package/dist/api/types.d.ts +22 -57
- package/dist/api/utilities/audio-sample.d.ts +0 -8
- package/dist/api/utilities/audio-sample.js +0 -8
- package/dist/api/utilities/audio-sample.js.map +1 -1
- package/dist/api/utilities/channel-layout.d.ts +0 -8
- package/dist/api/utilities/channel-layout.js +0 -8
- package/dist/api/utilities/channel-layout.js.map +1 -1
- package/dist/api/utilities/image.d.ts +0 -8
- package/dist/api/utilities/image.js +0 -8
- package/dist/api/utilities/image.js.map +1 -1
- package/dist/api/utilities/index.d.ts +3 -3
- package/dist/api/utilities/index.js +3 -3
- package/dist/api/utilities/index.js.map +1 -1
- package/dist/api/utilities/media-type.d.ts +1 -9
- package/dist/api/utilities/media-type.js +1 -9
- package/dist/api/utilities/media-type.js.map +1 -1
- package/dist/api/utilities/pixel-format.d.ts +1 -9
- package/dist/api/utilities/pixel-format.js +1 -9
- package/dist/api/utilities/pixel-format.js.map +1 -1
- package/dist/api/utilities/sample-format.d.ts +1 -9
- package/dist/api/utilities/sample-format.js +1 -9
- package/dist/api/utilities/sample-format.js.map +1 -1
- package/dist/api/utilities/streaming.d.ts +0 -8
- package/dist/api/utilities/streaming.js +0 -8
- package/dist/api/utilities/streaming.js.map +1 -1
- package/dist/api/utilities/timestamp.d.ts +0 -8
- package/dist/api/utilities/timestamp.js +0 -8
- package/dist/api/utilities/timestamp.js.map +1 -1
- package/dist/api/utils.js +2 -0
- package/dist/api/utils.js.map +1 -1
- package/dist/constants/constants.d.ts +1 -1
- package/dist/constants/constants.js +2 -0
- package/dist/constants/constants.js.map +1 -1
- package/dist/lib/binding.d.ts +1 -0
- package/dist/lib/binding.js +2 -0
- package/dist/lib/binding.js.map +1 -1
- package/dist/lib/codec.d.ts +4 -4
- package/dist/lib/codec.js +4 -4
- package/dist/lib/dictionary.d.ts +2 -2
- package/dist/lib/dictionary.js +2 -2
- package/dist/lib/dictionary.js.map +1 -1
- package/dist/lib/error.d.ts +1 -1
- package/dist/lib/error.js +1 -1
- package/dist/lib/filter-context.d.ts +19 -2
- package/dist/lib/filter-context.js +15 -0
- package/dist/lib/filter-context.js.map +1 -1
- package/dist/lib/format-context.d.ts +18 -18
- package/dist/lib/format-context.js +20 -20
- package/dist/lib/format-context.js.map +1 -1
- package/dist/lib/frame.d.ts +43 -1
- package/dist/lib/frame.js +53 -0
- package/dist/lib/frame.js.map +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/native-types.d.ts +1 -0
- package/dist/lib/option.d.ts +176 -0
- package/dist/lib/option.js +176 -0
- package/dist/lib/option.js.map +1 -1
- package/dist/lib/utilities.d.ts +64 -1
- package/dist/lib/utilities.js +65 -0
- package/dist/lib/utilities.js.map +1 -1
- package/install/ffmpeg.js +0 -11
- package/package.json +16 -18
- package/release_notes.md +0 -48
package/dist/api/encoder.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { AVERROR_EAGAIN, AVERROR_EOF } from '../constants/constants.js';
|
|
2
|
+
import { Codec, CodecContext, Dictionary, FFmpegError, Packet, Rational } from '../lib/index.js';
|
|
3
3
|
import { parseBitrate } from './utils.js';
|
|
4
4
|
/**
|
|
5
5
|
* High-level encoder for audio and video streams.
|
|
@@ -12,10 +12,10 @@ import { parseBitrate } from './utils.js';
|
|
|
12
12
|
* @example
|
|
13
13
|
* ```typescript
|
|
14
14
|
* import { Encoder } from 'node-av/api';
|
|
15
|
-
* import { AV_CODEC_ID_H264 } from 'node-av/constants';
|
|
15
|
+
* import { AV_CODEC_ID_H264, FF_ENCODER_LIBX264 } from 'node-av/constants';
|
|
16
16
|
*
|
|
17
17
|
* // Create H.264 encoder
|
|
18
|
-
* const encoder = await Encoder.create(
|
|
18
|
+
* const encoder = await Encoder.create(FF_ENCODER_LIBX264, {
|
|
19
19
|
* type: 'video',
|
|
20
20
|
* width: 1920,
|
|
21
21
|
* height: 1080,
|
|
@@ -37,17 +37,18 @@ import { parseBitrate } from './utils.js';
|
|
|
37
37
|
*
|
|
38
38
|
* @example
|
|
39
39
|
* ```typescript
|
|
40
|
-
* // Hardware-accelerated encoding
|
|
40
|
+
* // Hardware-accelerated encoding with lazy initialization
|
|
41
41
|
* import { HardwareContext } from 'node-av/api';
|
|
42
|
-
* import {
|
|
42
|
+
* import { FF_ENCODER_H264_VIDEOTOOLBOX } from 'node-av/constants';
|
|
43
43
|
*
|
|
44
|
-
* const hw = HardwareContext.
|
|
45
|
-
* const
|
|
46
|
-
*
|
|
44
|
+
* const hw = HardwareContext.auto();
|
|
45
|
+
* const encoderCodec = hw?.getEncoderCodec('h264') ?? FF_ENCODER_H264_VIDEOTOOLBOX;
|
|
46
|
+
* const encoder = await Encoder.create(encoderCodec, {
|
|
47
|
+
* timeBase: video.timeBase,
|
|
47
48
|
* bitrate: '10M'
|
|
48
49
|
* });
|
|
49
50
|
*
|
|
50
|
-
* //
|
|
51
|
+
* // Hardware context will be detected from first frame's hw_frames_ctx
|
|
51
52
|
* for await (const packet of encoder.packets(frames)) {
|
|
52
53
|
* await output.writePacket(packet);
|
|
53
54
|
* packet.free();
|
|
@@ -62,18 +63,19 @@ export class Encoder {
|
|
|
62
63
|
codecContext;
|
|
63
64
|
packet;
|
|
64
65
|
codec;
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
initialized = false;
|
|
67
|
+
isClosed = false;
|
|
68
|
+
opts;
|
|
67
69
|
/**
|
|
68
70
|
* @param codecContext - Configured codec context
|
|
69
71
|
* @param codec - Encoder codec
|
|
70
|
-
* @param
|
|
72
|
+
* @param opts - Encoder options as Dictionary
|
|
71
73
|
* @internal
|
|
72
74
|
*/
|
|
73
|
-
constructor(codecContext, codec,
|
|
75
|
+
constructor(codecContext, codec, opts) {
|
|
74
76
|
this.codecContext = codecContext;
|
|
75
77
|
this.codec = codec;
|
|
76
|
-
this.
|
|
78
|
+
this.opts = opts;
|
|
77
79
|
this.packet = new Packet();
|
|
78
80
|
this.packet.alloc();
|
|
79
81
|
}
|
|
@@ -81,24 +83,24 @@ export class Encoder {
|
|
|
81
83
|
* Create an encoder with specified codec and options.
|
|
82
84
|
*
|
|
83
85
|
* Initializes an encoder with the appropriate codec and configuration.
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
+
* Uses lazy initialization - encoder is opened when first frame is received.
|
|
87
|
+
* Hardware context will be automatically detected from first frame if not provided.
|
|
86
88
|
*
|
|
87
89
|
* Direct mapping to avcodec_find_encoder_by_name() or avcodec_find_encoder().
|
|
88
90
|
*
|
|
89
91
|
* @param encoderCodec - Codec name, ID, or instance to use for encoding
|
|
90
|
-
* @param
|
|
91
|
-
* @param options - Encoder configuration options
|
|
92
|
+
* @param options - Encoder configuration options including required timeBase
|
|
92
93
|
* @returns Configured encoder instance
|
|
93
94
|
*
|
|
94
|
-
* @throws {Error} If encoder not found or
|
|
95
|
-
*
|
|
95
|
+
* @throws {Error} If encoder not found or timeBase not provided
|
|
96
|
+
*
|
|
97
|
+
* @throws {FFmpegError} If codec allocation fails
|
|
96
98
|
*
|
|
97
99
|
* @example
|
|
98
100
|
* ```typescript
|
|
99
101
|
* // From decoder stream info
|
|
100
|
-
* const
|
|
101
|
-
*
|
|
102
|
+
* const encoder = await Encoder.create(FF_ENCODER_LIBX264, {
|
|
103
|
+
* timeBase: video.timeBase,
|
|
102
104
|
* bitrate: '5M',
|
|
103
105
|
* gopSize: 60,
|
|
104
106
|
* options: {
|
|
@@ -111,13 +113,8 @@ export class Encoder {
|
|
|
111
113
|
* @example
|
|
112
114
|
* ```typescript
|
|
113
115
|
* // With custom stream info
|
|
114
|
-
* const encoder = await Encoder.create(
|
|
115
|
-
*
|
|
116
|
-
* sampleRate: 48000,
|
|
117
|
-
* sampleFormat: AV_SAMPLE_FMT_FLTP,
|
|
118
|
-
* channelLayout: AV_CH_LAYOUT_STEREO,
|
|
119
|
-
* timeBase: { num: 1, den: 48000 }
|
|
120
|
-
* }, {
|
|
116
|
+
* const encoder = await Encoder.create(FF_ENCODER_AAC, {
|
|
117
|
+
* timeBase: audio.timeBase,
|
|
121
118
|
* bitrate: '192k'
|
|
122
119
|
* });
|
|
123
120
|
* ```
|
|
@@ -126,16 +123,16 @@ export class Encoder {
|
|
|
126
123
|
* ```typescript
|
|
127
124
|
* // Hardware encoder
|
|
128
125
|
* const hw = HardwareContext.auto();
|
|
129
|
-
* const
|
|
130
|
-
*
|
|
126
|
+
* const encoderCodec = hw?.getEncoderCodec('h264') ?? FF_ENCODER_H264_VIDEOTOOLBOX;
|
|
127
|
+
* const encoder = await Encoder.create(encoderCodec, {
|
|
128
|
+
* timeBase: video.timeBase,
|
|
131
129
|
* bitrate: '8M'
|
|
132
130
|
* });
|
|
133
131
|
* ```
|
|
134
132
|
*
|
|
135
|
-
* @see {@link Decoder.getOutputStreamInfo} For stream info source
|
|
136
133
|
* @see {@link EncoderOptions} For configuration options
|
|
137
134
|
*/
|
|
138
|
-
static async create(encoderCodec,
|
|
135
|
+
static async create(encoderCodec, options) {
|
|
139
136
|
let codec = null;
|
|
140
137
|
let codecName = '';
|
|
141
138
|
if (encoderCodec instanceof Codec) {
|
|
@@ -156,52 +153,6 @@ export class Encoder {
|
|
|
156
153
|
// Allocate codec context
|
|
157
154
|
const codecContext = new CodecContext();
|
|
158
155
|
codecContext.allocContext3(codec);
|
|
159
|
-
// It's StreamInfo - apply manually
|
|
160
|
-
if (input.type === 'video' && codec.type === AVMEDIA_TYPE_VIDEO) {
|
|
161
|
-
const videoInfo = input;
|
|
162
|
-
const codecPixelformats = codec.pixelFormats;
|
|
163
|
-
if (codecPixelformats && !codecPixelformats.includes(videoInfo.pixelFormat)) {
|
|
164
|
-
codecContext.freeContext();
|
|
165
|
-
const pixelFormatName = avGetPixFmtName(videoInfo.pixelFormat) ?? 'unknown';
|
|
166
|
-
const codecPixFmtNames = codecPixelformats.map(avGetPixFmtName).filter(Boolean).join(', ');
|
|
167
|
-
throw new Error(`Unsupported pixel format for '${codecName}' encoder: ${pixelFormatName}! Supported formats: ${codecPixFmtNames}`);
|
|
168
|
-
}
|
|
169
|
-
codecContext.width = videoInfo.width;
|
|
170
|
-
codecContext.height = videoInfo.height;
|
|
171
|
-
codecContext.pixelFormat = videoInfo.pixelFormat;
|
|
172
|
-
// Set pkt_timebase and timeBase to input timebase
|
|
173
|
-
codecContext.pktTimebase = new Rational(videoInfo.timeBase.num, videoInfo.timeBase.den);
|
|
174
|
-
codecContext.timeBase = new Rational(videoInfo.timeBase.num, videoInfo.timeBase.den);
|
|
175
|
-
if (videoInfo.frameRate) {
|
|
176
|
-
codecContext.framerate = new Rational(videoInfo.frameRate.num, videoInfo.frameRate.den);
|
|
177
|
-
}
|
|
178
|
-
if (videoInfo.sampleAspectRatio) {
|
|
179
|
-
codecContext.sampleAspectRatio = new Rational(videoInfo.sampleAspectRatio.num, videoInfo.sampleAspectRatio.den);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
else if (input.type === 'audio' && codec.type === AVMEDIA_TYPE_AUDIO) {
|
|
183
|
-
const audioInfo = input;
|
|
184
|
-
const codecSampleFormats = codec.sampleFormats;
|
|
185
|
-
if (codecSampleFormats && !codecSampleFormats.includes(audioInfo.sampleFormat)) {
|
|
186
|
-
codecContext.freeContext();
|
|
187
|
-
const sampleFormatName = avGetSampleFmtName(audioInfo.sampleFormat) ?? 'unknown';
|
|
188
|
-
const supportedFormats = codecSampleFormats.map(avGetSampleFmtName).filter(Boolean).join(', ');
|
|
189
|
-
throw new Error(`Unsupported sample format for '${codecName}' encoder: ${sampleFormatName}! Supported formats: ${supportedFormats}`);
|
|
190
|
-
}
|
|
191
|
-
codecContext.sampleRate = audioInfo.sampleRate;
|
|
192
|
-
codecContext.sampleFormat = audioInfo.sampleFormat;
|
|
193
|
-
codecContext.channelLayout = audioInfo.channelLayout;
|
|
194
|
-
// Set both pkt_timebase and timeBase for audio
|
|
195
|
-
codecContext.pktTimebase = new Rational(audioInfo.timeBase.num, audioInfo.timeBase.den);
|
|
196
|
-
codecContext.timeBase = new Rational(audioInfo.timeBase.num, audioInfo.timeBase.den);
|
|
197
|
-
if (audioInfo.frameSize) {
|
|
198
|
-
codecContext.frameSize = audioInfo.frameSize;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
codecContext.freeContext();
|
|
203
|
-
throw new Error(`Unsupported codec type for encoder! Input type: ${input.type}, Codec type: ${codec.type}`);
|
|
204
|
-
}
|
|
205
156
|
// Apply encoder-specific options
|
|
206
157
|
if (options.gopSize !== undefined) {
|
|
207
158
|
codecContext.gopSize = options.gopSize;
|
|
@@ -214,32 +165,28 @@ export class Encoder {
|
|
|
214
165
|
const bitrate = typeof options.bitrate === 'string' ? parseBitrate(options.bitrate) : BigInt(options.bitrate);
|
|
215
166
|
codecContext.bitRate = bitrate;
|
|
216
167
|
}
|
|
217
|
-
if (options.
|
|
218
|
-
|
|
168
|
+
if (options.minRate !== undefined) {
|
|
169
|
+
const minRate = typeof options.minRate === 'string' ? parseBitrate(options.minRate) : BigInt(options.minRate);
|
|
170
|
+
codecContext.rcMinRate = minRate;
|
|
219
171
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
codecContext.
|
|
172
|
+
if (options.maxRate !== undefined) {
|
|
173
|
+
const maxRate = typeof options.maxRate === 'string' ? parseBitrate(options.maxRate) : BigInt(options.maxRate);
|
|
174
|
+
codecContext.rcMaxRate = maxRate;
|
|
223
175
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
codecContext.setOption(key, value.toString());
|
|
228
|
-
}
|
|
176
|
+
if (options.bufSize !== undefined) {
|
|
177
|
+
const bufSize = typeof options.bufSize === 'string' ? parseBitrate(options.bufSize) : BigInt(options.bufSize);
|
|
178
|
+
codecContext.rcBufferSize = Number(bufSize);
|
|
229
179
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
codecContext.freeContext();
|
|
233
|
-
throw new Error(`Hardware encoder '${codecName}' requires a hardware context`);
|
|
180
|
+
if (options.threads !== undefined) {
|
|
181
|
+
codecContext.threadCount = options.threads;
|
|
234
182
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
codecContext.
|
|
239
|
-
FFmpegError.throwIfError(openRet, 'Failed to open encoder');
|
|
183
|
+
codecContext.timeBase = new Rational(options.timeBase.num, options.timeBase.den);
|
|
184
|
+
codecContext.pktTimebase = new Rational(options.timeBase.num, options.timeBase.den);
|
|
185
|
+
if (options.frameRate) {
|
|
186
|
+
codecContext.framerate = new Rational(options.frameRate.num, options.frameRate.den);
|
|
240
187
|
}
|
|
241
|
-
const
|
|
242
|
-
return
|
|
188
|
+
const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
|
|
189
|
+
return new Encoder(codecContext, codec, opts);
|
|
243
190
|
}
|
|
244
191
|
/**
|
|
245
192
|
* Check if encoder is open.
|
|
@@ -252,7 +199,25 @@ export class Encoder {
|
|
|
252
199
|
* ```
|
|
253
200
|
*/
|
|
254
201
|
get isEncoderOpen() {
|
|
255
|
-
return this.
|
|
202
|
+
return !this.isClosed;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Check if encoder has been initialized.
|
|
206
|
+
*
|
|
207
|
+
* Returns true after first frame has been processed and encoder opened.
|
|
208
|
+
* Useful for checking if encoder has received frame properties.
|
|
209
|
+
*
|
|
210
|
+
* @returns true if encoder has been initialized with frame data
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```typescript
|
|
214
|
+
* if (!encoder.isEncoderInitialized) {
|
|
215
|
+
* console.log('Encoder will initialize on first frame');
|
|
216
|
+
* }
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
get isEncoderInitialized() {
|
|
220
|
+
return this.initialized;
|
|
256
221
|
}
|
|
257
222
|
/**
|
|
258
223
|
* Check if encoder uses hardware acceleration.
|
|
@@ -269,14 +234,29 @@ export class Encoder {
|
|
|
269
234
|
* @see {@link HardwareContext} For hardware setup
|
|
270
235
|
*/
|
|
271
236
|
isHardware() {
|
|
272
|
-
return
|
|
237
|
+
return this.codec.isHardwareAcceleratedEncoder();
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Check if encoder is ready for processing.
|
|
241
|
+
*
|
|
242
|
+
* @returns true if initialized and ready
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```typescript
|
|
246
|
+
* if (encoder.isReady()) {
|
|
247
|
+
* const packet = await encoder.encode(frame);
|
|
248
|
+
* }
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
isReady() {
|
|
252
|
+
return this.initialized && !this.isClosed;
|
|
273
253
|
}
|
|
274
254
|
/**
|
|
275
255
|
* Encode a frame to a packet.
|
|
276
256
|
*
|
|
277
257
|
* Sends a frame to the encoder and attempts to receive an encoded packet.
|
|
258
|
+
* On first frame, automatically initializes encoder with frame properties.
|
|
278
259
|
* Handles internal buffering - may return null if more frames needed.
|
|
279
|
-
* Automatically manages encoder state and hardware context binding.
|
|
280
260
|
*
|
|
281
261
|
* Direct mapping to avcodec_send_frame() and avcodec_receive_packet().
|
|
282
262
|
*
|
|
@@ -284,6 +264,7 @@ export class Encoder {
|
|
|
284
264
|
* @returns Encoded packet or null if more data needed
|
|
285
265
|
*
|
|
286
266
|
* @throws {Error} If encoder is closed
|
|
267
|
+
*
|
|
287
268
|
* @throws {FFmpegError} If encoding fails
|
|
288
269
|
*
|
|
289
270
|
* @example
|
|
@@ -313,21 +294,24 @@ export class Encoder {
|
|
|
313
294
|
* @see {@link flush} For end-of-stream handling
|
|
314
295
|
*/
|
|
315
296
|
async encode(frame) {
|
|
316
|
-
if (
|
|
297
|
+
if (this.isClosed) {
|
|
298
|
+
if (!frame) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
317
301
|
throw new Error('Encoder is closed');
|
|
318
302
|
}
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
303
|
+
// Open encoder if not already done
|
|
304
|
+
if (!this.initialized) {
|
|
305
|
+
if (!frame) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
await this.initialize(frame);
|
|
325
309
|
}
|
|
326
310
|
// Send frame to encoder
|
|
327
311
|
const sendRet = await this.codecContext.sendFrame(frame);
|
|
328
312
|
if (sendRet < 0 && sendRet !== AVERROR_EOF) {
|
|
329
313
|
// Encoder might be full, try to receive first
|
|
330
|
-
const packet = await this.
|
|
314
|
+
const packet = await this.receive();
|
|
331
315
|
if (packet)
|
|
332
316
|
return packet;
|
|
333
317
|
// If still failing, it's an error
|
|
@@ -336,7 +320,7 @@ export class Encoder {
|
|
|
336
320
|
}
|
|
337
321
|
}
|
|
338
322
|
// Try to receive packet
|
|
339
|
-
return await this.
|
|
323
|
+
return await this.receive();
|
|
340
324
|
}
|
|
341
325
|
/**
|
|
342
326
|
* Encode frame stream to packet stream.
|
|
@@ -347,8 +331,9 @@ export class Encoder {
|
|
|
347
331
|
* Primary interface for stream-based encoding.
|
|
348
332
|
*
|
|
349
333
|
* @param frames - Async iterable of frames (freed automatically)
|
|
350
|
-
* @yields Encoded packets (caller must free)
|
|
334
|
+
* @yields {Packet} Encoded packets (caller must free)
|
|
351
335
|
* @throws {Error} If encoder is closed
|
|
336
|
+
*
|
|
352
337
|
* @throws {FFmpegError} If encoding fails
|
|
353
338
|
*
|
|
354
339
|
* @example
|
|
@@ -365,11 +350,11 @@ export class Encoder {
|
|
|
365
350
|
* // With frame filtering
|
|
366
351
|
* async function* filteredFrames() {
|
|
367
352
|
* for await (const frame of decoder.frames(input.packets())) {
|
|
368
|
-
* await filter.
|
|
369
|
-
* const filtered = await filter.getFrame();
|
|
353
|
+
* const filtered = await filter.process(frame);
|
|
370
354
|
* if (filtered) {
|
|
371
355
|
* yield filtered;
|
|
372
356
|
* }
|
|
357
|
+
* frame.free();
|
|
373
358
|
* }
|
|
374
359
|
* }
|
|
375
360
|
*
|
|
@@ -397,9 +382,6 @@ export class Encoder {
|
|
|
397
382
|
* @see {@link Decoder.frames} For frame source
|
|
398
383
|
*/
|
|
399
384
|
async *packets(frames) {
|
|
400
|
-
if (!this.isOpen) {
|
|
401
|
-
throw new Error('Encoder is closed');
|
|
402
|
-
}
|
|
403
385
|
// Process frames
|
|
404
386
|
for await (const frame of frames) {
|
|
405
387
|
try {
|
|
@@ -414,29 +396,31 @@ export class Encoder {
|
|
|
414
396
|
}
|
|
415
397
|
}
|
|
416
398
|
// Flush encoder after all frames
|
|
417
|
-
|
|
418
|
-
while (
|
|
419
|
-
|
|
399
|
+
await this.flush();
|
|
400
|
+
while (true) {
|
|
401
|
+
const remaining = await this.receive();
|
|
402
|
+
if (!remaining)
|
|
403
|
+
break;
|
|
404
|
+
yield remaining;
|
|
420
405
|
}
|
|
421
406
|
}
|
|
422
407
|
/**
|
|
423
|
-
* Flush encoder and
|
|
408
|
+
* Flush encoder and signal end-of-stream.
|
|
424
409
|
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
410
|
+
* Sends null frame to encoder to signal end-of-stream.
|
|
411
|
+
* Does nothing if encoder was never initialized or is closed.
|
|
412
|
+
* Must call receive() to get remaining buffered packets.
|
|
428
413
|
*
|
|
429
414
|
* Direct mapping to avcodec_send_frame(NULL).
|
|
430
415
|
*
|
|
431
|
-
* @returns Buffered packet or null if none remaining
|
|
432
|
-
*
|
|
433
|
-
* @throws {Error} If encoder is closed
|
|
434
|
-
*
|
|
435
416
|
* @example
|
|
436
417
|
* ```typescript
|
|
437
|
-
* //
|
|
418
|
+
* // Signal end of stream
|
|
419
|
+
* await encoder.flush();
|
|
420
|
+
*
|
|
421
|
+
* // Then get remaining packets
|
|
438
422
|
* let packet;
|
|
439
|
-
* while ((packet = await encoder.
|
|
423
|
+
* while ((packet = await encoder.receive()) !== null) {
|
|
440
424
|
* console.log('Got buffered packet');
|
|
441
425
|
* await output.writePacket(packet);
|
|
442
426
|
* packet.free();
|
|
@@ -444,26 +428,28 @@ export class Encoder {
|
|
|
444
428
|
* ```
|
|
445
429
|
*
|
|
446
430
|
* @see {@link flushPackets} For async iteration
|
|
447
|
-
* @see {@link
|
|
431
|
+
* @see {@link receive} For getting buffered packets
|
|
448
432
|
*/
|
|
449
433
|
async flush() {
|
|
450
|
-
if (!this.
|
|
451
|
-
|
|
434
|
+
if (this.isClosed || !this.initialized) {
|
|
435
|
+
return;
|
|
452
436
|
}
|
|
453
437
|
// Send flush frame (null)
|
|
454
|
-
await this.codecContext.sendFrame(null);
|
|
455
|
-
|
|
456
|
-
|
|
438
|
+
const ret = await this.codecContext.sendFrame(null);
|
|
439
|
+
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
440
|
+
if (ret !== AVERROR_EAGAIN) {
|
|
441
|
+
FFmpegError.throwIfError(ret, 'Failed to flush encoder');
|
|
442
|
+
}
|
|
443
|
+
}
|
|
457
444
|
}
|
|
458
445
|
/**
|
|
459
446
|
* Flush all buffered packets as async generator.
|
|
460
447
|
*
|
|
461
448
|
* Convenient async iteration over remaining packets.
|
|
462
|
-
* Automatically handles repeated
|
|
463
|
-
*
|
|
449
|
+
* Automatically handles flush and repeated receive calls.
|
|
450
|
+
* Returns immediately if encoder was never initialized or is closed.
|
|
464
451
|
*
|
|
465
|
-
* @yields Buffered packets
|
|
466
|
-
* @throws {Error} If encoder is closed
|
|
452
|
+
* @yields {Packet} Buffered packets
|
|
467
453
|
*
|
|
468
454
|
* @example
|
|
469
455
|
* ```typescript
|
|
@@ -475,29 +461,86 @@ export class Encoder {
|
|
|
475
461
|
* }
|
|
476
462
|
* ```
|
|
477
463
|
*
|
|
478
|
-
* @see {@link flush} For
|
|
464
|
+
* @see {@link flush} For signaling end-of-stream
|
|
479
465
|
* @see {@link packets} For complete pipeline
|
|
480
466
|
*/
|
|
481
467
|
async *flushPackets() {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
468
|
+
// Send flush signal
|
|
469
|
+
await this.flush();
|
|
485
470
|
let packet;
|
|
486
|
-
while ((packet = await this.
|
|
471
|
+
while ((packet = await this.receive()) !== null) {
|
|
487
472
|
yield packet;
|
|
488
473
|
}
|
|
489
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* Receive packet from encoder.
|
|
477
|
+
*
|
|
478
|
+
* Gets encoded packets from the codec's internal buffer.
|
|
479
|
+
* Handles packet cloning and error checking.
|
|
480
|
+
* Returns null if encoder is closed, not initialized, or no packets available.
|
|
481
|
+
* Call repeatedly until null to drain all buffered packets.
|
|
482
|
+
*
|
|
483
|
+
* Direct mapping to avcodec_receive_packet().
|
|
484
|
+
*
|
|
485
|
+
* @returns Cloned packet or null if no packets available
|
|
486
|
+
*
|
|
487
|
+
* @throws {FFmpegError} If receive fails with error other than AVERROR_EAGAIN or AVERROR_EOF
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```typescript
|
|
491
|
+
* const packet = await encoder.receive();
|
|
492
|
+
* if (packet) {
|
|
493
|
+
* console.log(`Got packet with PTS: ${packet.pts}`);
|
|
494
|
+
* await output.writePacket(packet);
|
|
495
|
+
* packet.free();
|
|
496
|
+
* }
|
|
497
|
+
* ```
|
|
498
|
+
*
|
|
499
|
+
* @example
|
|
500
|
+
* ```typescript
|
|
501
|
+
* // Drain all buffered packets
|
|
502
|
+
* let packet;
|
|
503
|
+
* while ((packet = await encoder.receive()) !== null) {
|
|
504
|
+
* console.log(`Packet size: ${packet.size}`);
|
|
505
|
+
* await output.writePacket(packet);
|
|
506
|
+
* packet.free();
|
|
507
|
+
* }
|
|
508
|
+
* ```
|
|
509
|
+
*
|
|
510
|
+
* @see {@link encode} For sending frames and receiving packets
|
|
511
|
+
* @see {@link flush} For signaling end-of-stream
|
|
512
|
+
*/
|
|
513
|
+
async receive() {
|
|
514
|
+
if (this.isClosed || !this.initialized) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
// Clear previous packet data
|
|
518
|
+
this.packet.unref();
|
|
519
|
+
const ret = await this.codecContext.receivePacket(this.packet);
|
|
520
|
+
if (ret === 0) {
|
|
521
|
+
// Got a packet, clone it for the user
|
|
522
|
+
return this.packet.clone();
|
|
523
|
+
}
|
|
524
|
+
else if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
|
|
525
|
+
// Need more data or end of stream
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
// Error
|
|
530
|
+
FFmpegError.throwIfError(ret, 'Failed to receive packet');
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
490
534
|
/**
|
|
491
535
|
* Close encoder and free resources.
|
|
492
536
|
*
|
|
493
537
|
* Releases codec context and internal packet buffer.
|
|
494
538
|
* Safe to call multiple times.
|
|
495
|
-
* Does NOT dispose hardware context - caller is responsible.
|
|
496
539
|
* Automatically called by Symbol.dispose.
|
|
497
540
|
*
|
|
498
541
|
* @example
|
|
499
542
|
* ```typescript
|
|
500
|
-
* const encoder = await Encoder.create(
|
|
543
|
+
* const encoder = await Encoder.create(FF_ENCODER_LIBX264, { ... });
|
|
501
544
|
* try {
|
|
502
545
|
* // Use encoder
|
|
503
546
|
* } finally {
|
|
@@ -508,11 +551,48 @@ export class Encoder {
|
|
|
508
551
|
* @see {@link Symbol.dispose} For automatic cleanup
|
|
509
552
|
*/
|
|
510
553
|
close() {
|
|
511
|
-
if (
|
|
554
|
+
if (this.isClosed) {
|
|
512
555
|
return;
|
|
556
|
+
}
|
|
557
|
+
this.isClosed = true;
|
|
513
558
|
this.packet.free();
|
|
514
559
|
this.codecContext.freeContext();
|
|
515
|
-
this.
|
|
560
|
+
this.initialized = false;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Initialize encoder from first frame.
|
|
564
|
+
*
|
|
565
|
+
* Sets codec context parameters from frame properties.
|
|
566
|
+
* Configures hardware context if present in frame.
|
|
567
|
+
* Opens encoder with accumulated options.
|
|
568
|
+
*
|
|
569
|
+
* @param frame - First frame to encode
|
|
570
|
+
*
|
|
571
|
+
* @throws {FFmpegError} If encoder open fails
|
|
572
|
+
*
|
|
573
|
+
* @internal
|
|
574
|
+
*/
|
|
575
|
+
async initialize(frame) {
|
|
576
|
+
if (frame.isVideo()) {
|
|
577
|
+
this.codecContext.width = frame.width;
|
|
578
|
+
this.codecContext.height = frame.height;
|
|
579
|
+
this.codecContext.pixelFormat = frame.format;
|
|
580
|
+
this.codecContext.sampleAspectRatio = frame.sampleAspectRatio;
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
this.codecContext.sampleRate = frame.sampleRate;
|
|
584
|
+
this.codecContext.sampleFormat = frame.format;
|
|
585
|
+
this.codecContext.channelLayout = frame.channelLayout;
|
|
586
|
+
}
|
|
587
|
+
this.codecContext.hwDeviceCtx = frame.hwFramesCtx?.deviceRef ?? null;
|
|
588
|
+
this.codecContext.hwFramesCtx = frame.hwFramesCtx;
|
|
589
|
+
// Open codec
|
|
590
|
+
const openRet = await this.codecContext.open2(this.codec, this.opts);
|
|
591
|
+
if (openRet < 0) {
|
|
592
|
+
this.codecContext.freeContext();
|
|
593
|
+
FFmpegError.throwIfError(openRet, 'Failed to open encoder');
|
|
594
|
+
}
|
|
595
|
+
this.initialized = true;
|
|
516
596
|
}
|
|
517
597
|
/**
|
|
518
598
|
* Get encoder codec.
|
|
@@ -522,14 +602,9 @@ export class Encoder {
|
|
|
522
602
|
*
|
|
523
603
|
* @returns Codec instance
|
|
524
604
|
*
|
|
525
|
-
* @
|
|
526
|
-
* ```typescript
|
|
527
|
-
* const codec = encoder.getCodec();
|
|
528
|
-
* console.log(`Using codec: ${codec.name}`);
|
|
529
|
-
* console.log(`Capabilities: ${codec.capabilities}`);
|
|
530
|
-
* ```
|
|
605
|
+
* @internal
|
|
531
606
|
*
|
|
532
|
-
* @see {@link Codec} For codec
|
|
607
|
+
* @see {@link Codec} For codec details
|
|
533
608
|
*/
|
|
534
609
|
getCodec() {
|
|
535
610
|
return this.codec;
|
|
@@ -537,45 +612,18 @@ export class Encoder {
|
|
|
537
612
|
/**
|
|
538
613
|
* Get underlying codec context.
|
|
539
614
|
*
|
|
540
|
-
* Returns the
|
|
541
|
-
*
|
|
615
|
+
* Returns the codec context for advanced operations.
|
|
616
|
+
* Useful for accessing low-level codec properties and settings.
|
|
617
|
+
* Returns null if encoder is closed or not initialized.
|
|
542
618
|
*
|
|
543
|
-
* @returns Codec context or null
|
|
619
|
+
* @returns Codec context or null if closed/not initialized
|
|
544
620
|
*
|
|
545
621
|
* @internal
|
|
546
|
-
*/
|
|
547
|
-
getCodecContext() {
|
|
548
|
-
return this.isOpen ? this.codecContext : null;
|
|
549
|
-
}
|
|
550
|
-
/**
|
|
551
|
-
* Receive packet from encoder.
|
|
552
|
-
*
|
|
553
|
-
* Internal method to get encoded packets from codec.
|
|
554
|
-
* Handles packet cloning and error checking.
|
|
555
|
-
*
|
|
556
|
-
* Direct mapping to avcodec_receive_packet().
|
|
557
|
-
*
|
|
558
|
-
* @returns Cloned packet or null
|
|
559
622
|
*
|
|
560
|
-
* @
|
|
623
|
+
* @see {@link CodecContext} For context details
|
|
561
624
|
*/
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
this.packet.unref();
|
|
565
|
-
const ret = await this.codecContext.receivePacket(this.packet);
|
|
566
|
-
if (ret === 0) {
|
|
567
|
-
// Got a packet, clone it for the user
|
|
568
|
-
return this.packet.clone();
|
|
569
|
-
}
|
|
570
|
-
else if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
|
|
571
|
-
// Need more data or end of stream
|
|
572
|
-
return null;
|
|
573
|
-
}
|
|
574
|
-
else {
|
|
575
|
-
// Error
|
|
576
|
-
FFmpegError.throwIfError(ret, 'Failed to receive packet');
|
|
577
|
-
return null;
|
|
578
|
-
}
|
|
625
|
+
getCodecContext() {
|
|
626
|
+
return !this.isClosed && this.initialized ? this.codecContext : null;
|
|
579
627
|
}
|
|
580
628
|
/**
|
|
581
629
|
* Dispose of encoder.
|
|
@@ -586,7 +634,7 @@ export class Encoder {
|
|
|
586
634
|
* @example
|
|
587
635
|
* ```typescript
|
|
588
636
|
* {
|
|
589
|
-
* using encoder = await Encoder.create(
|
|
637
|
+
* using encoder = await Encoder.create(FF_ENCODER_LIBX264, { ... });
|
|
590
638
|
* // Encode frames...
|
|
591
639
|
* } // Automatically closed
|
|
592
640
|
* ```
|