node-av 1.3.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 -38
- package/dist/api/bitstream-filter.d.ts +2 -2
- package/dist/api/bitstream-filter.js +2 -2
- package/dist/api/decoder.d.ts +131 -120
- package/dist/api/decoder.js +191 -203
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/encoder.d.ts +135 -77
- package/dist/api/encoder.js +235 -192
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/filter-presets.d.ts +408 -1534
- package/dist/api/filter-presets.js +1005 -2058
- package/dist/api/filter-presets.js.map +1 -1
- package/dist/api/filter.d.ts +160 -165
- package/dist/api/filter.js +294 -374
- package/dist/api/filter.js.map +1 -1
- package/dist/api/hardware.d.ts +8 -31
- package/dist/api/hardware.js +19 -70
- 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/media-input.d.ts +1 -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 +35 -128
- package/dist/api/media-output.js +136 -208
- package/dist/api/media-output.js.map +1 -1
- package/dist/api/pipeline.d.ts +17 -17
- package/dist/api/pipeline.js +19 -42
- package/dist/api/pipeline.js.map +1 -1
- package/dist/api/types.d.ts +17 -57
- 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/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/package.json +17 -17
- package/release_notes.md +0 -29
package/dist/api/encoder.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AVERROR_EAGAIN, AVERROR_EOF
|
|
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.
|
|
@@ -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,25 +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
|
+
* @throws {Error} If encoder not found or timeBase not provided
|
|
95
96
|
*
|
|
96
|
-
* @throws {FFmpegError} If codec
|
|
97
|
+
* @throws {FFmpegError} If codec allocation fails
|
|
97
98
|
*
|
|
98
99
|
* @example
|
|
99
100
|
* ```typescript
|
|
100
101
|
* // From decoder stream info
|
|
101
|
-
* const
|
|
102
|
-
*
|
|
102
|
+
* const encoder = await Encoder.create(FF_ENCODER_LIBX264, {
|
|
103
|
+
* timeBase: video.timeBase,
|
|
103
104
|
* bitrate: '5M',
|
|
104
105
|
* gopSize: 60,
|
|
105
106
|
* options: {
|
|
@@ -113,12 +114,7 @@ export class Encoder {
|
|
|
113
114
|
* ```typescript
|
|
114
115
|
* // With custom stream info
|
|
115
116
|
* const encoder = await Encoder.create(FF_ENCODER_AAC, {
|
|
116
|
-
*
|
|
117
|
-
* sampleRate: 48000,
|
|
118
|
-
* sampleFormat: AV_SAMPLE_FMT_FLTP,
|
|
119
|
-
* channelLayout: AV_CH_LAYOUT_STEREO,
|
|
120
|
-
* timeBase: { num: 1, den: 48000 }
|
|
121
|
-
* }, {
|
|
117
|
+
* timeBase: audio.timeBase,
|
|
122
118
|
* bitrate: '192k'
|
|
123
119
|
* });
|
|
124
120
|
* ```
|
|
@@ -127,16 +123,16 @@ export class Encoder {
|
|
|
127
123
|
* ```typescript
|
|
128
124
|
* // Hardware encoder
|
|
129
125
|
* const hw = HardwareContext.auto();
|
|
130
|
-
* const
|
|
131
|
-
*
|
|
126
|
+
* const encoderCodec = hw?.getEncoderCodec('h264') ?? FF_ENCODER_H264_VIDEOTOOLBOX;
|
|
127
|
+
* const encoder = await Encoder.create(encoderCodec, {
|
|
128
|
+
* timeBase: video.timeBase,
|
|
132
129
|
* bitrate: '8M'
|
|
133
130
|
* });
|
|
134
131
|
* ```
|
|
135
132
|
*
|
|
136
|
-
* @see {@link Decoder.getOutputStreamInfo} For stream info source
|
|
137
133
|
* @see {@link EncoderOptions} For configuration options
|
|
138
134
|
*/
|
|
139
|
-
static async create(encoderCodec,
|
|
135
|
+
static async create(encoderCodec, options) {
|
|
140
136
|
let codec = null;
|
|
141
137
|
let codecName = '';
|
|
142
138
|
if (encoderCodec instanceof Codec) {
|
|
@@ -157,52 +153,6 @@ export class Encoder {
|
|
|
157
153
|
// Allocate codec context
|
|
158
154
|
const codecContext = new CodecContext();
|
|
159
155
|
codecContext.allocContext3(codec);
|
|
160
|
-
// It's StreamInfo - apply manually
|
|
161
|
-
if (input.type === 'video' && codec.type === AVMEDIA_TYPE_VIDEO) {
|
|
162
|
-
const videoInfo = input;
|
|
163
|
-
const codecPixelformats = codec.pixelFormats;
|
|
164
|
-
if (codecPixelformats && !codecPixelformats.includes(videoInfo.pixelFormat)) {
|
|
165
|
-
codecContext.freeContext();
|
|
166
|
-
const pixelFormatName = avGetPixFmtName(videoInfo.pixelFormat) ?? 'unknown';
|
|
167
|
-
const codecPixFmtNames = codecPixelformats.map(avGetPixFmtName).filter(Boolean).join(', ');
|
|
168
|
-
throw new Error(`Unsupported pixel format for '${codecName}' encoder: ${pixelFormatName}! Supported formats: ${codecPixFmtNames}`);
|
|
169
|
-
}
|
|
170
|
-
codecContext.width = videoInfo.width;
|
|
171
|
-
codecContext.height = videoInfo.height;
|
|
172
|
-
codecContext.pixelFormat = videoInfo.pixelFormat;
|
|
173
|
-
// Set pkt_timebase and timeBase to input timebase
|
|
174
|
-
codecContext.pktTimebase = new Rational(videoInfo.timeBase.num, videoInfo.timeBase.den);
|
|
175
|
-
codecContext.timeBase = new Rational(videoInfo.timeBase.num, videoInfo.timeBase.den);
|
|
176
|
-
if (videoInfo.frameRate) {
|
|
177
|
-
codecContext.framerate = new Rational(videoInfo.frameRate.num, videoInfo.frameRate.den);
|
|
178
|
-
}
|
|
179
|
-
if (videoInfo.sampleAspectRatio) {
|
|
180
|
-
codecContext.sampleAspectRatio = new Rational(videoInfo.sampleAspectRatio.num, videoInfo.sampleAspectRatio.den);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
else if (input.type === 'audio' && codec.type === AVMEDIA_TYPE_AUDIO) {
|
|
184
|
-
const audioInfo = input;
|
|
185
|
-
const codecSampleFormats = codec.sampleFormats;
|
|
186
|
-
if (codecSampleFormats && !codecSampleFormats.includes(audioInfo.sampleFormat)) {
|
|
187
|
-
codecContext.freeContext();
|
|
188
|
-
const sampleFormatName = avGetSampleFmtName(audioInfo.sampleFormat) ?? 'unknown';
|
|
189
|
-
const supportedFormats = codecSampleFormats.map(avGetSampleFmtName).filter(Boolean).join(', ');
|
|
190
|
-
throw new Error(`Unsupported sample format for '${codecName}' encoder: ${sampleFormatName}! Supported formats: ${supportedFormats}`);
|
|
191
|
-
}
|
|
192
|
-
codecContext.sampleRate = audioInfo.sampleRate;
|
|
193
|
-
codecContext.sampleFormat = audioInfo.sampleFormat;
|
|
194
|
-
codecContext.channelLayout = audioInfo.channelLayout;
|
|
195
|
-
// Set both pkt_timebase and timeBase for audio
|
|
196
|
-
codecContext.pktTimebase = new Rational(audioInfo.timeBase.num, audioInfo.timeBase.den);
|
|
197
|
-
codecContext.timeBase = new Rational(audioInfo.timeBase.num, audioInfo.timeBase.den);
|
|
198
|
-
if (audioInfo.frameSize) {
|
|
199
|
-
codecContext.frameSize = audioInfo.frameSize;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
codecContext.freeContext();
|
|
204
|
-
throw new Error(`Unsupported codec type for encoder! Input type: ${input.type}, Codec type: ${codec.type}`);
|
|
205
|
-
}
|
|
206
156
|
// Apply encoder-specific options
|
|
207
157
|
if (options.gopSize !== undefined) {
|
|
208
158
|
codecContext.gopSize = options.gopSize;
|
|
@@ -215,32 +165,28 @@ export class Encoder {
|
|
|
215
165
|
const bitrate = typeof options.bitrate === 'string' ? parseBitrate(options.bitrate) : BigInt(options.bitrate);
|
|
216
166
|
codecContext.bitRate = bitrate;
|
|
217
167
|
}
|
|
218
|
-
if (options.
|
|
219
|
-
|
|
168
|
+
if (options.minRate !== undefined) {
|
|
169
|
+
const minRate = typeof options.minRate === 'string' ? parseBitrate(options.minRate) : BigInt(options.minRate);
|
|
170
|
+
codecContext.rcMinRate = minRate;
|
|
220
171
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
codecContext.
|
|
172
|
+
if (options.maxRate !== undefined) {
|
|
173
|
+
const maxRate = typeof options.maxRate === 'string' ? parseBitrate(options.maxRate) : BigInt(options.maxRate);
|
|
174
|
+
codecContext.rcMaxRate = maxRate;
|
|
224
175
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
codecContext.setOption(key, value.toString());
|
|
229
|
-
}
|
|
176
|
+
if (options.bufSize !== undefined) {
|
|
177
|
+
const bufSize = typeof options.bufSize === 'string' ? parseBitrate(options.bufSize) : BigInt(options.bufSize);
|
|
178
|
+
codecContext.rcBufferSize = Number(bufSize);
|
|
230
179
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
codecContext.freeContext();
|
|
234
|
-
throw new Error(`Hardware encoder '${codecName}' requires a hardware context`);
|
|
180
|
+
if (options.threads !== undefined) {
|
|
181
|
+
codecContext.threadCount = options.threads;
|
|
235
182
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
codecContext.
|
|
240
|
-
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);
|
|
241
187
|
}
|
|
242
|
-
const
|
|
243
|
-
return
|
|
188
|
+
const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
|
|
189
|
+
return new Encoder(codecContext, codec, opts);
|
|
244
190
|
}
|
|
245
191
|
/**
|
|
246
192
|
* Check if encoder is open.
|
|
@@ -253,7 +199,25 @@ export class Encoder {
|
|
|
253
199
|
* ```
|
|
254
200
|
*/
|
|
255
201
|
get isEncoderOpen() {
|
|
256
|
-
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;
|
|
257
221
|
}
|
|
258
222
|
/**
|
|
259
223
|
* Check if encoder uses hardware acceleration.
|
|
@@ -270,14 +234,29 @@ export class Encoder {
|
|
|
270
234
|
* @see {@link HardwareContext} For hardware setup
|
|
271
235
|
*/
|
|
272
236
|
isHardware() {
|
|
273
|
-
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;
|
|
274
253
|
}
|
|
275
254
|
/**
|
|
276
255
|
* Encode a frame to a packet.
|
|
277
256
|
*
|
|
278
257
|
* Sends a frame to the encoder and attempts to receive an encoded packet.
|
|
258
|
+
* On first frame, automatically initializes encoder with frame properties.
|
|
279
259
|
* Handles internal buffering - may return null if more frames needed.
|
|
280
|
-
* Automatically manages encoder state and hardware context binding.
|
|
281
260
|
*
|
|
282
261
|
* Direct mapping to avcodec_send_frame() and avcodec_receive_packet().
|
|
283
262
|
*
|
|
@@ -315,21 +294,24 @@ export class Encoder {
|
|
|
315
294
|
* @see {@link flush} For end-of-stream handling
|
|
316
295
|
*/
|
|
317
296
|
async encode(frame) {
|
|
318
|
-
if (
|
|
297
|
+
if (this.isClosed) {
|
|
298
|
+
if (!frame) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
319
301
|
throw new Error('Encoder is closed');
|
|
320
302
|
}
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
303
|
+
// Open encoder if not already done
|
|
304
|
+
if (!this.initialized) {
|
|
305
|
+
if (!frame) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
await this.initialize(frame);
|
|
327
309
|
}
|
|
328
310
|
// Send frame to encoder
|
|
329
311
|
const sendRet = await this.codecContext.sendFrame(frame);
|
|
330
312
|
if (sendRet < 0 && sendRet !== AVERROR_EOF) {
|
|
331
313
|
// Encoder might be full, try to receive first
|
|
332
|
-
const packet = await this.
|
|
314
|
+
const packet = await this.receive();
|
|
333
315
|
if (packet)
|
|
334
316
|
return packet;
|
|
335
317
|
// If still failing, it's an error
|
|
@@ -338,7 +320,7 @@ export class Encoder {
|
|
|
338
320
|
}
|
|
339
321
|
}
|
|
340
322
|
// Try to receive packet
|
|
341
|
-
return await this.
|
|
323
|
+
return await this.receive();
|
|
342
324
|
}
|
|
343
325
|
/**
|
|
344
326
|
* Encode frame stream to packet stream.
|
|
@@ -349,7 +331,7 @@ export class Encoder {
|
|
|
349
331
|
* Primary interface for stream-based encoding.
|
|
350
332
|
*
|
|
351
333
|
* @param frames - Async iterable of frames (freed automatically)
|
|
352
|
-
* @yields Encoded packets (caller must free)
|
|
334
|
+
* @yields {Packet} Encoded packets (caller must free)
|
|
353
335
|
* @throws {Error} If encoder is closed
|
|
354
336
|
*
|
|
355
337
|
* @throws {FFmpegError} If encoding fails
|
|
@@ -368,11 +350,11 @@ export class Encoder {
|
|
|
368
350
|
* // With frame filtering
|
|
369
351
|
* async function* filteredFrames() {
|
|
370
352
|
* for await (const frame of decoder.frames(input.packets())) {
|
|
371
|
-
* await filter.
|
|
372
|
-
* const filtered = await filter.getFrame();
|
|
353
|
+
* const filtered = await filter.process(frame);
|
|
373
354
|
* if (filtered) {
|
|
374
355
|
* yield filtered;
|
|
375
356
|
* }
|
|
357
|
+
* frame.free();
|
|
376
358
|
* }
|
|
377
359
|
* }
|
|
378
360
|
*
|
|
@@ -400,9 +382,6 @@ export class Encoder {
|
|
|
400
382
|
* @see {@link Decoder.frames} For frame source
|
|
401
383
|
*/
|
|
402
384
|
async *packets(frames) {
|
|
403
|
-
if (!this.isOpen) {
|
|
404
|
-
throw new Error('Encoder is closed');
|
|
405
|
-
}
|
|
406
385
|
// Process frames
|
|
407
386
|
for await (const frame of frames) {
|
|
408
387
|
try {
|
|
@@ -417,29 +396,31 @@ export class Encoder {
|
|
|
417
396
|
}
|
|
418
397
|
}
|
|
419
398
|
// Flush encoder after all frames
|
|
420
|
-
|
|
421
|
-
while (
|
|
422
|
-
|
|
399
|
+
await this.flush();
|
|
400
|
+
while (true) {
|
|
401
|
+
const remaining = await this.receive();
|
|
402
|
+
if (!remaining)
|
|
403
|
+
break;
|
|
404
|
+
yield remaining;
|
|
423
405
|
}
|
|
424
406
|
}
|
|
425
407
|
/**
|
|
426
|
-
* Flush encoder and
|
|
408
|
+
* Flush encoder and signal end-of-stream.
|
|
427
409
|
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
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.
|
|
431
413
|
*
|
|
432
414
|
* Direct mapping to avcodec_send_frame(NULL).
|
|
433
415
|
*
|
|
434
|
-
* @returns Buffered packet or null if none remaining
|
|
435
|
-
*
|
|
436
|
-
* @throws {Error} If encoder is closed
|
|
437
|
-
*
|
|
438
416
|
* @example
|
|
439
417
|
* ```typescript
|
|
440
|
-
* //
|
|
418
|
+
* // Signal end of stream
|
|
419
|
+
* await encoder.flush();
|
|
420
|
+
*
|
|
421
|
+
* // Then get remaining packets
|
|
441
422
|
* let packet;
|
|
442
|
-
* while ((packet = await encoder.
|
|
423
|
+
* while ((packet = await encoder.receive()) !== null) {
|
|
443
424
|
* console.log('Got buffered packet');
|
|
444
425
|
* await output.writePacket(packet);
|
|
445
426
|
* packet.free();
|
|
@@ -447,26 +428,28 @@ export class Encoder {
|
|
|
447
428
|
* ```
|
|
448
429
|
*
|
|
449
430
|
* @see {@link flushPackets} For async iteration
|
|
450
|
-
* @see {@link
|
|
431
|
+
* @see {@link receive} For getting buffered packets
|
|
451
432
|
*/
|
|
452
433
|
async flush() {
|
|
453
|
-
if (!this.
|
|
454
|
-
|
|
434
|
+
if (this.isClosed || !this.initialized) {
|
|
435
|
+
return;
|
|
455
436
|
}
|
|
456
437
|
// Send flush frame (null)
|
|
457
|
-
await this.codecContext.sendFrame(null);
|
|
458
|
-
|
|
459
|
-
|
|
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
|
+
}
|
|
460
444
|
}
|
|
461
445
|
/**
|
|
462
446
|
* Flush all buffered packets as async generator.
|
|
463
447
|
*
|
|
464
448
|
* Convenient async iteration over remaining packets.
|
|
465
|
-
* Automatically handles repeated
|
|
466
|
-
*
|
|
449
|
+
* Automatically handles flush and repeated receive calls.
|
|
450
|
+
* Returns immediately if encoder was never initialized or is closed.
|
|
467
451
|
*
|
|
468
|
-
* @yields Buffered packets
|
|
469
|
-
* @throws {Error} If encoder is closed
|
|
452
|
+
* @yields {Packet} Buffered packets
|
|
470
453
|
*
|
|
471
454
|
* @example
|
|
472
455
|
* ```typescript
|
|
@@ -478,29 +461,86 @@ export class Encoder {
|
|
|
478
461
|
* }
|
|
479
462
|
* ```
|
|
480
463
|
*
|
|
481
|
-
* @see {@link flush} For
|
|
464
|
+
* @see {@link flush} For signaling end-of-stream
|
|
482
465
|
* @see {@link packets} For complete pipeline
|
|
483
466
|
*/
|
|
484
467
|
async *flushPackets() {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
468
|
+
// Send flush signal
|
|
469
|
+
await this.flush();
|
|
488
470
|
let packet;
|
|
489
|
-
while ((packet = await this.
|
|
471
|
+
while ((packet = await this.receive()) !== null) {
|
|
490
472
|
yield packet;
|
|
491
473
|
}
|
|
492
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
|
+
}
|
|
493
534
|
/**
|
|
494
535
|
* Close encoder and free resources.
|
|
495
536
|
*
|
|
496
537
|
* Releases codec context and internal packet buffer.
|
|
497
538
|
* Safe to call multiple times.
|
|
498
|
-
* Does NOT dispose hardware context - caller is responsible.
|
|
499
539
|
* Automatically called by Symbol.dispose.
|
|
500
540
|
*
|
|
501
541
|
* @example
|
|
502
542
|
* ```typescript
|
|
503
|
-
* const encoder = await Encoder.create(FF_ENCODER_LIBX264,
|
|
543
|
+
* const encoder = await Encoder.create(FF_ENCODER_LIBX264, { ... });
|
|
504
544
|
* try {
|
|
505
545
|
* // Use encoder
|
|
506
546
|
* } finally {
|
|
@@ -511,11 +551,48 @@ export class Encoder {
|
|
|
511
551
|
* @see {@link Symbol.dispose} For automatic cleanup
|
|
512
552
|
*/
|
|
513
553
|
close() {
|
|
514
|
-
if (
|
|
554
|
+
if (this.isClosed) {
|
|
515
555
|
return;
|
|
556
|
+
}
|
|
557
|
+
this.isClosed = true;
|
|
516
558
|
this.packet.free();
|
|
517
559
|
this.codecContext.freeContext();
|
|
518
|
-
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;
|
|
519
596
|
}
|
|
520
597
|
/**
|
|
521
598
|
* Get encoder codec.
|
|
@@ -525,14 +602,9 @@ export class Encoder {
|
|
|
525
602
|
*
|
|
526
603
|
* @returns Codec instance
|
|
527
604
|
*
|
|
528
|
-
* @
|
|
529
|
-
* ```typescript
|
|
530
|
-
* const codec = encoder.getCodec();
|
|
531
|
-
* console.log(`Using codec: ${codec.name}`);
|
|
532
|
-
* console.log(`Capabilities: ${codec.capabilities}`);
|
|
533
|
-
* ```
|
|
605
|
+
* @internal
|
|
534
606
|
*
|
|
535
|
-
* @see {@link Codec} For codec
|
|
607
|
+
* @see {@link Codec} For codec details
|
|
536
608
|
*/
|
|
537
609
|
getCodec() {
|
|
538
610
|
return this.codec;
|
|
@@ -540,47 +612,18 @@ export class Encoder {
|
|
|
540
612
|
/**
|
|
541
613
|
* Get underlying codec context.
|
|
542
614
|
*
|
|
543
|
-
* Returns the
|
|
544
|
-
*
|
|
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.
|
|
545
618
|
*
|
|
546
|
-
* @returns Codec context or null
|
|
619
|
+
* @returns Codec context or null if closed/not initialized
|
|
547
620
|
*
|
|
548
621
|
* @internal
|
|
549
|
-
*/
|
|
550
|
-
getCodecContext() {
|
|
551
|
-
return this.isOpen ? this.codecContext : null;
|
|
552
|
-
}
|
|
553
|
-
/**
|
|
554
|
-
* Receive packet from encoder.
|
|
555
|
-
*
|
|
556
|
-
* Internal method to get encoded packets from codec.
|
|
557
|
-
* Handles packet cloning and error checking.
|
|
558
|
-
*
|
|
559
|
-
* Direct mapping to avcodec_receive_packet().
|
|
560
|
-
*
|
|
561
|
-
* @returns Cloned packet or null
|
|
562
|
-
*
|
|
563
|
-
* @throws {FFmpegError} If receive fails with error other than AVERROR_EAGAIN or AVERROR_EOF
|
|
564
622
|
*
|
|
565
|
-
* @
|
|
623
|
+
* @see {@link CodecContext} For context details
|
|
566
624
|
*/
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
this.packet.unref();
|
|
570
|
-
const ret = await this.codecContext.receivePacket(this.packet);
|
|
571
|
-
if (ret === 0) {
|
|
572
|
-
// Got a packet, clone it for the user
|
|
573
|
-
return this.packet.clone();
|
|
574
|
-
}
|
|
575
|
-
else if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
|
|
576
|
-
// Need more data or end of stream
|
|
577
|
-
return null;
|
|
578
|
-
}
|
|
579
|
-
else {
|
|
580
|
-
// Error
|
|
581
|
-
FFmpegError.throwIfError(ret, 'Failed to receive packet');
|
|
582
|
-
return null;
|
|
583
|
-
}
|
|
625
|
+
getCodecContext() {
|
|
626
|
+
return !this.isClosed && this.initialized ? this.codecContext : null;
|
|
584
627
|
}
|
|
585
628
|
/**
|
|
586
629
|
* Dispose of encoder.
|
|
@@ -591,7 +634,7 @@ export class Encoder {
|
|
|
591
634
|
* @example
|
|
592
635
|
* ```typescript
|
|
593
636
|
* {
|
|
594
|
-
* using encoder = await Encoder.create(FF_ENCODER_LIBX264,
|
|
637
|
+
* using encoder = await Encoder.create(FF_ENCODER_LIBX264, { ... });
|
|
595
638
|
* // Encode frames...
|
|
596
639
|
* } // Automatically closed
|
|
597
640
|
* ```
|