node-av 1.3.0 → 2.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 +47 -40
- package/binding.gyp +12 -0
- package/dist/api/bitstream-filter.d.ts +134 -2
- package/dist/api/bitstream-filter.js +200 -2
- package/dist/api/bitstream-filter.js.map +1 -1
- package/dist/api/decoder.d.ts +261 -105
- package/dist/api/decoder.js +384 -171
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/encoder.d.ts +338 -74
- package/dist/api/encoder.js +546 -188
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/filter-presets.d.ts +479 -1513
- package/dist/api/filter-presets.js +1044 -2005
- package/dist/api/filter-presets.js.map +1 -1
- package/dist/api/filter.d.ts +370 -150
- package/dist/api/filter.js +647 -364
- package/dist/api/filter.js.map +1 -1
- package/dist/api/hardware.d.ts +25 -31
- package/dist/api/hardware.js +36 -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/io-stream.d.ts +6 -0
- package/dist/api/io-stream.js +2 -0
- package/dist/api/io-stream.js.map +1 -1
- package/dist/api/media-input.d.ts +208 -2
- package/dist/api/media-input.js +356 -8
- package/dist/api/media-input.js.map +1 -1
- package/dist/api/media-output.d.ts +142 -104
- package/dist/api/media-output.js +446 -179
- package/dist/api/media-output.js.map +1 -1
- package/dist/api/pipeline.d.ts +82 -17
- package/dist/api/pipeline.js +80 -42
- package/dist/api/pipeline.js.map +1 -1
- package/dist/api/types.d.ts +24 -57
- package/dist/api/utils.js +2 -0
- package/dist/api/utils.js.map +1 -1
- package/dist/lib/audio-fifo.d.ts +103 -0
- package/dist/lib/audio-fifo.js +109 -0
- package/dist/lib/audio-fifo.js.map +1 -1
- package/dist/lib/binding.d.ts +1 -0
- package/dist/lib/binding.js.map +1 -1
- package/dist/lib/bitstream-filter-context.d.ts +79 -0
- package/dist/lib/bitstream-filter-context.js +83 -0
- package/dist/lib/bitstream-filter-context.js.map +1 -1
- package/dist/lib/bitstream-filter.d.ts +2 -0
- package/dist/lib/bitstream-filter.js +2 -0
- package/dist/lib/bitstream-filter.js.map +1 -1
- package/dist/lib/codec-context.d.ts +168 -0
- package/dist/lib/codec-context.js +178 -0
- package/dist/lib/codec-context.js.map +1 -1
- package/dist/lib/codec-parameters.d.ts +3 -0
- package/dist/lib/codec-parameters.js +3 -0
- package/dist/lib/codec-parameters.js.map +1 -1
- package/dist/lib/codec-parser.d.ts +6 -0
- package/dist/lib/codec-parser.js +6 -0
- package/dist/lib/codec-parser.js.map +1 -1
- package/dist/lib/codec.d.ts +12 -0
- package/dist/lib/codec.js +12 -0
- package/dist/lib/codec.js.map +1 -1
- package/dist/lib/dictionary.d.ts +18 -2
- package/dist/lib/dictionary.js +18 -2
- package/dist/lib/dictionary.js.map +1 -1
- package/dist/lib/error.d.ts +8 -0
- package/dist/lib/error.js +9 -0
- package/dist/lib/error.js.map +1 -1
- package/dist/lib/filter-context.d.ts +119 -2
- package/dist/lib/filter-context.js +119 -0
- package/dist/lib/filter-context.js.map +1 -1
- package/dist/lib/filter-graph.d.ts +80 -0
- package/dist/lib/filter-graph.js +84 -0
- package/dist/lib/filter-graph.js.map +1 -1
- package/dist/lib/filter-inout.d.ts +1 -0
- package/dist/lib/filter-inout.js +1 -0
- package/dist/lib/filter-inout.js.map +1 -1
- package/dist/lib/filter.d.ts +2 -0
- package/dist/lib/filter.js +2 -0
- package/dist/lib/filter.js.map +1 -1
- package/dist/lib/format-context.d.ts +356 -20
- package/dist/lib/format-context.js +375 -23
- package/dist/lib/format-context.js.map +1 -1
- package/dist/lib/frame.d.ts +84 -1
- package/dist/lib/frame.js +96 -0
- package/dist/lib/frame.js.map +1 -1
- package/dist/lib/hardware-device-context.d.ts +8 -0
- package/dist/lib/hardware-device-context.js +8 -0
- package/dist/lib/hardware-device-context.js.map +1 -1
- package/dist/lib/hardware-frames-context.d.ts +55 -0
- package/dist/lib/hardware-frames-context.js +57 -0
- package/dist/lib/hardware-frames-context.js.map +1 -1
- package/dist/lib/input-format.d.ts +43 -3
- package/dist/lib/input-format.js +48 -0
- package/dist/lib/input-format.js.map +1 -1
- package/dist/lib/io-context.d.ts +212 -0
- package/dist/lib/io-context.js +228 -0
- package/dist/lib/io-context.js.map +1 -1
- package/dist/lib/log.d.ts +2 -0
- package/dist/lib/log.js +2 -0
- package/dist/lib/log.js.map +1 -1
- package/dist/lib/native-types.d.ts +39 -1
- package/dist/lib/option.d.ts +90 -0
- package/dist/lib/option.js +97 -0
- package/dist/lib/option.js.map +1 -1
- package/dist/lib/output-format.d.ts +4 -0
- package/dist/lib/output-format.js +4 -0
- package/dist/lib/output-format.js.map +1 -1
- package/dist/lib/packet.d.ts +7 -0
- package/dist/lib/packet.js +7 -0
- package/dist/lib/packet.js.map +1 -1
- package/dist/lib/rational.d.ts +1 -0
- package/dist/lib/rational.js +1 -0
- package/dist/lib/rational.js.map +1 -1
- package/dist/lib/software-resample-context.d.ts +64 -0
- package/dist/lib/software-resample-context.js +66 -0
- package/dist/lib/software-resample-context.js.map +1 -1
- package/dist/lib/software-scale-context.d.ts +98 -0
- package/dist/lib/software-scale-context.js +102 -0
- package/dist/lib/software-scale-context.js.map +1 -1
- package/dist/lib/stream.d.ts +1 -0
- package/dist/lib/stream.js +1 -0
- package/dist/lib/stream.js.map +1 -1
- package/dist/lib/utilities.d.ts +60 -0
- package/dist/lib/utilities.js +60 -0
- package/dist/lib/utilities.js.map +1 -1
- package/package.json +18 -18
- 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,22 @@ 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
|
|
71
|
+
*
|
|
69
72
|
* @param codec - Encoder codec
|
|
70
|
-
*
|
|
73
|
+
*
|
|
74
|
+
* @param opts - Encoder options as Dictionary
|
|
75
|
+
*
|
|
71
76
|
* @internal
|
|
72
77
|
*/
|
|
73
|
-
constructor(codecContext, codec,
|
|
78
|
+
constructor(codecContext, codec, opts) {
|
|
74
79
|
this.codecContext = codecContext;
|
|
75
80
|
this.codec = codec;
|
|
76
|
-
this.
|
|
81
|
+
this.opts = opts;
|
|
77
82
|
this.packet = new Packet();
|
|
78
83
|
this.packet.alloc();
|
|
79
84
|
}
|
|
@@ -81,25 +86,26 @@ export class Encoder {
|
|
|
81
86
|
* Create an encoder with specified codec and options.
|
|
82
87
|
*
|
|
83
88
|
* Initializes an encoder with the appropriate codec and configuration.
|
|
84
|
-
*
|
|
85
|
-
*
|
|
89
|
+
* Uses lazy initialization - encoder is opened when first frame is received.
|
|
90
|
+
* Hardware context will be automatically detected from first frame if not provided.
|
|
86
91
|
*
|
|
87
92
|
* Direct mapping to avcodec_find_encoder_by_name() or avcodec_find_encoder().
|
|
88
93
|
*
|
|
89
94
|
* @param encoderCodec - Codec name, ID, or instance to use for encoding
|
|
90
|
-
*
|
|
91
|
-
* @param options - Encoder configuration options
|
|
95
|
+
*
|
|
96
|
+
* @param options - Encoder configuration options including required timeBase
|
|
97
|
+
*
|
|
92
98
|
* @returns Configured encoder instance
|
|
93
99
|
*
|
|
94
|
-
* @throws {Error} If encoder not found or
|
|
100
|
+
* @throws {Error} If encoder not found or timeBase not provided
|
|
95
101
|
*
|
|
96
|
-
* @throws {FFmpegError} If codec
|
|
102
|
+
* @throws {FFmpegError} If codec allocation fails
|
|
97
103
|
*
|
|
98
104
|
* @example
|
|
99
105
|
* ```typescript
|
|
100
106
|
* // From decoder stream info
|
|
101
|
-
* const
|
|
102
|
-
*
|
|
107
|
+
* const encoder = await Encoder.create(FF_ENCODER_LIBX264, {
|
|
108
|
+
* timeBase: video.timeBase,
|
|
103
109
|
* bitrate: '5M',
|
|
104
110
|
* gopSize: 60,
|
|
105
111
|
* options: {
|
|
@@ -113,12 +119,7 @@ export class Encoder {
|
|
|
113
119
|
* ```typescript
|
|
114
120
|
* // With custom stream info
|
|
115
121
|
* 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
|
-
* }, {
|
|
122
|
+
* timeBase: audio.timeBase,
|
|
122
123
|
* bitrate: '192k'
|
|
123
124
|
* });
|
|
124
125
|
* ```
|
|
@@ -127,16 +128,16 @@ export class Encoder {
|
|
|
127
128
|
* ```typescript
|
|
128
129
|
* // Hardware encoder
|
|
129
130
|
* const hw = HardwareContext.auto();
|
|
130
|
-
* const
|
|
131
|
-
*
|
|
131
|
+
* const encoderCodec = hw?.getEncoderCodec('h264') ?? FF_ENCODER_H264_VIDEOTOOLBOX;
|
|
132
|
+
* const encoder = await Encoder.create(encoderCodec, {
|
|
133
|
+
* timeBase: video.timeBase,
|
|
132
134
|
* bitrate: '8M'
|
|
133
135
|
* });
|
|
134
136
|
* ```
|
|
135
137
|
*
|
|
136
|
-
* @see {@link Decoder.getOutputStreamInfo} For stream info source
|
|
137
138
|
* @see {@link EncoderOptions} For configuration options
|
|
138
139
|
*/
|
|
139
|
-
static async create(encoderCodec,
|
|
140
|
+
static async create(encoderCodec, options) {
|
|
140
141
|
let codec = null;
|
|
141
142
|
let codecName = '';
|
|
142
143
|
if (encoderCodec instanceof Codec) {
|
|
@@ -157,52 +158,6 @@ export class Encoder {
|
|
|
157
158
|
// Allocate codec context
|
|
158
159
|
const codecContext = new CodecContext();
|
|
159
160
|
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
161
|
// Apply encoder-specific options
|
|
207
162
|
if (options.gopSize !== undefined) {
|
|
208
163
|
codecContext.gopSize = options.gopSize;
|
|
@@ -215,32 +170,28 @@ export class Encoder {
|
|
|
215
170
|
const bitrate = typeof options.bitrate === 'string' ? parseBitrate(options.bitrate) : BigInt(options.bitrate);
|
|
216
171
|
codecContext.bitRate = bitrate;
|
|
217
172
|
}
|
|
218
|
-
if (options.
|
|
219
|
-
|
|
173
|
+
if (options.minRate !== undefined) {
|
|
174
|
+
const minRate = typeof options.minRate === 'string' ? parseBitrate(options.minRate) : BigInt(options.minRate);
|
|
175
|
+
codecContext.rcMinRate = minRate;
|
|
220
176
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
codecContext.
|
|
177
|
+
if (options.maxRate !== undefined) {
|
|
178
|
+
const maxRate = typeof options.maxRate === 'string' ? parseBitrate(options.maxRate) : BigInt(options.maxRate);
|
|
179
|
+
codecContext.rcMaxRate = maxRate;
|
|
224
180
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
codecContext.setOption(key, value.toString());
|
|
229
|
-
}
|
|
181
|
+
if (options.bufSize !== undefined) {
|
|
182
|
+
const bufSize = typeof options.bufSize === 'string' ? parseBitrate(options.bufSize) : BigInt(options.bufSize);
|
|
183
|
+
codecContext.rcBufferSize = Number(bufSize);
|
|
230
184
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
codecContext.freeContext();
|
|
234
|
-
throw new Error(`Hardware encoder '${codecName}' requires a hardware context`);
|
|
185
|
+
if (options.threads !== undefined) {
|
|
186
|
+
codecContext.threadCount = options.threads;
|
|
235
187
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
codecContext.
|
|
240
|
-
FFmpegError.throwIfError(openRet, 'Failed to open encoder');
|
|
188
|
+
codecContext.timeBase = new Rational(options.timeBase.num, options.timeBase.den);
|
|
189
|
+
codecContext.pktTimebase = new Rational(options.timeBase.num, options.timeBase.den);
|
|
190
|
+
if (options.frameRate) {
|
|
191
|
+
codecContext.framerate = new Rational(options.frameRate.num, options.frameRate.den);
|
|
241
192
|
}
|
|
242
|
-
const
|
|
243
|
-
return
|
|
193
|
+
const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
|
|
194
|
+
return new Encoder(codecContext, codec, opts);
|
|
244
195
|
}
|
|
245
196
|
/**
|
|
246
197
|
* Check if encoder is open.
|
|
@@ -253,7 +204,25 @@ export class Encoder {
|
|
|
253
204
|
* ```
|
|
254
205
|
*/
|
|
255
206
|
get isEncoderOpen() {
|
|
256
|
-
return this.
|
|
207
|
+
return !this.isClosed;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Check if encoder has been initialized.
|
|
211
|
+
*
|
|
212
|
+
* Returns true after first frame has been processed and encoder opened.
|
|
213
|
+
* Useful for checking if encoder has received frame properties.
|
|
214
|
+
*
|
|
215
|
+
* @returns true if encoder has been initialized with frame data
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```typescript
|
|
219
|
+
* if (!encoder.isEncoderInitialized) {
|
|
220
|
+
* console.log('Encoder will initialize on first frame');
|
|
221
|
+
* }
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
get isEncoderInitialized() {
|
|
225
|
+
return this.initialized;
|
|
257
226
|
}
|
|
258
227
|
/**
|
|
259
228
|
* Check if encoder uses hardware acceleration.
|
|
@@ -270,18 +239,34 @@ export class Encoder {
|
|
|
270
239
|
* @see {@link HardwareContext} For hardware setup
|
|
271
240
|
*/
|
|
272
241
|
isHardware() {
|
|
273
|
-
return
|
|
242
|
+
return this.codec.isHardwareAcceleratedEncoder();
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Check if encoder is ready for processing.
|
|
246
|
+
*
|
|
247
|
+
* @returns true if initialized and ready
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```typescript
|
|
251
|
+
* if (encoder.isReady()) {
|
|
252
|
+
* const packet = await encoder.encode(frame);
|
|
253
|
+
* }
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
isReady() {
|
|
257
|
+
return this.initialized && !this.isClosed;
|
|
274
258
|
}
|
|
275
259
|
/**
|
|
276
260
|
* Encode a frame to a packet.
|
|
277
261
|
*
|
|
278
262
|
* Sends a frame to the encoder and attempts to receive an encoded packet.
|
|
263
|
+
* On first frame, automatically initializes encoder with frame properties.
|
|
279
264
|
* Handles internal buffering - may return null if more frames needed.
|
|
280
|
-
* Automatically manages encoder state and hardware context binding.
|
|
281
265
|
*
|
|
282
266
|
* Direct mapping to avcodec_send_frame() and avcodec_receive_packet().
|
|
283
267
|
*
|
|
284
268
|
* @param frame - Raw frame to encode (or null to flush)
|
|
269
|
+
*
|
|
285
270
|
* @returns Encoded packet or null if more data needed
|
|
286
271
|
*
|
|
287
272
|
* @throws {Error} If encoder is closed
|
|
@@ -315,21 +300,96 @@ export class Encoder {
|
|
|
315
300
|
* @see {@link flush} For end-of-stream handling
|
|
316
301
|
*/
|
|
317
302
|
async encode(frame) {
|
|
318
|
-
if (
|
|
303
|
+
if (this.isClosed) {
|
|
304
|
+
if (!frame) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
319
307
|
throw new Error('Encoder is closed');
|
|
320
308
|
}
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
309
|
+
// Open encoder if not already done
|
|
310
|
+
if (!this.initialized) {
|
|
311
|
+
if (!frame) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
await this.initialize(frame);
|
|
327
315
|
}
|
|
328
316
|
// Send frame to encoder
|
|
329
317
|
const sendRet = await this.codecContext.sendFrame(frame);
|
|
330
318
|
if (sendRet < 0 && sendRet !== AVERROR_EOF) {
|
|
331
319
|
// Encoder might be full, try to receive first
|
|
332
|
-
const packet = await this.
|
|
320
|
+
const packet = await this.receive();
|
|
321
|
+
if (packet)
|
|
322
|
+
return packet;
|
|
323
|
+
// If still failing, it's an error
|
|
324
|
+
if (sendRet !== AVERROR_EAGAIN) {
|
|
325
|
+
FFmpegError.throwIfError(sendRet, 'Failed to send frame');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Try to receive packet
|
|
329
|
+
return await this.receive();
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Encode a frame to a packet synchronously.
|
|
333
|
+
* Synchronous version of encode.
|
|
334
|
+
*
|
|
335
|
+
* Sends a frame to the encoder and attempts to receive an encoded packet.
|
|
336
|
+
* On first frame, automatically initializes encoder with frame properties.
|
|
337
|
+
* Handles internal buffering - may return null if more frames needed.
|
|
338
|
+
*
|
|
339
|
+
* Direct mapping to avcodec_send_frame() and avcodec_receive_packet().
|
|
340
|
+
*
|
|
341
|
+
* @param frame - Raw frame to encode (or null to flush)
|
|
342
|
+
*
|
|
343
|
+
* @returns Encoded packet or null if more data needed
|
|
344
|
+
*
|
|
345
|
+
* @throws {Error} If encoder is closed
|
|
346
|
+
*
|
|
347
|
+
* @throws {FFmpegError} If encoding fails
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```typescript
|
|
351
|
+
* const packet = encoder.encodeSync(frame);
|
|
352
|
+
* if (packet) {
|
|
353
|
+
* console.log(`Encoded packet with PTS: ${packet.pts}`);
|
|
354
|
+
* output.writePacketSync(packet);
|
|
355
|
+
* packet.free();
|
|
356
|
+
* }
|
|
357
|
+
* ```
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```typescript
|
|
361
|
+
* // Encode loop
|
|
362
|
+
* for (const frame of decoder.framesSync(packets)) {
|
|
363
|
+
* const packet = encoder.encodeSync(frame);
|
|
364
|
+
* if (packet) {
|
|
365
|
+
* output.writePacketSync(packet);
|
|
366
|
+
* packet.free();
|
|
367
|
+
* }
|
|
368
|
+
* frame.free();
|
|
369
|
+
* }
|
|
370
|
+
* ```
|
|
371
|
+
*
|
|
372
|
+
* @see {@link encode} For async version
|
|
373
|
+
*/
|
|
374
|
+
encodeSync(frame) {
|
|
375
|
+
if (this.isClosed) {
|
|
376
|
+
if (!frame) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
throw new Error('Encoder is closed');
|
|
380
|
+
}
|
|
381
|
+
// Open encoder if not already done
|
|
382
|
+
if (!this.initialized) {
|
|
383
|
+
if (!frame) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
this.initializeSync(frame);
|
|
387
|
+
}
|
|
388
|
+
// Send frame to encoder
|
|
389
|
+
const sendRet = this.codecContext.sendFrameSync(frame);
|
|
390
|
+
if (sendRet < 0 && sendRet !== AVERROR_EOF) {
|
|
391
|
+
// Encoder might be full, try to receive first
|
|
392
|
+
const packet = this.receiveSync();
|
|
333
393
|
if (packet)
|
|
334
394
|
return packet;
|
|
335
395
|
// If still failing, it's an error
|
|
@@ -338,7 +398,7 @@ export class Encoder {
|
|
|
338
398
|
}
|
|
339
399
|
}
|
|
340
400
|
// Try to receive packet
|
|
341
|
-
return
|
|
401
|
+
return this.receiveSync();
|
|
342
402
|
}
|
|
343
403
|
/**
|
|
344
404
|
* Encode frame stream to packet stream.
|
|
@@ -349,7 +409,9 @@ export class Encoder {
|
|
|
349
409
|
* Primary interface for stream-based encoding.
|
|
350
410
|
*
|
|
351
411
|
* @param frames - Async iterable of frames (freed automatically)
|
|
352
|
-
*
|
|
412
|
+
*
|
|
413
|
+
* @yields {Packet} Encoded packets (caller must free)
|
|
414
|
+
*
|
|
353
415
|
* @throws {Error} If encoder is closed
|
|
354
416
|
*
|
|
355
417
|
* @throws {FFmpegError} If encoding fails
|
|
@@ -368,11 +430,11 @@ export class Encoder {
|
|
|
368
430
|
* // With frame filtering
|
|
369
431
|
* async function* filteredFrames() {
|
|
370
432
|
* for await (const frame of decoder.frames(input.packets())) {
|
|
371
|
-
* await filter.
|
|
372
|
-
* const filtered = await filter.getFrame();
|
|
433
|
+
* const filtered = await filter.process(frame);
|
|
373
434
|
* if (filtered) {
|
|
374
435
|
* yield filtered;
|
|
375
436
|
* }
|
|
437
|
+
* frame.free();
|
|
376
438
|
* }
|
|
377
439
|
* }
|
|
378
440
|
*
|
|
@@ -400,9 +462,6 @@ export class Encoder {
|
|
|
400
462
|
* @see {@link Decoder.frames} For frame source
|
|
401
463
|
*/
|
|
402
464
|
async *packets(frames) {
|
|
403
|
-
if (!this.isOpen) {
|
|
404
|
-
throw new Error('Encoder is closed');
|
|
405
|
-
}
|
|
406
465
|
// Process frames
|
|
407
466
|
for await (const frame of frames) {
|
|
408
467
|
try {
|
|
@@ -417,29 +476,101 @@ export class Encoder {
|
|
|
417
476
|
}
|
|
418
477
|
}
|
|
419
478
|
// Flush encoder after all frames
|
|
420
|
-
|
|
421
|
-
while (
|
|
422
|
-
|
|
479
|
+
await this.flush();
|
|
480
|
+
while (true) {
|
|
481
|
+
const remaining = await this.receive();
|
|
482
|
+
if (!remaining)
|
|
483
|
+
break;
|
|
484
|
+
yield remaining;
|
|
423
485
|
}
|
|
424
486
|
}
|
|
425
487
|
/**
|
|
426
|
-
*
|
|
488
|
+
* Encode frame stream to packet stream synchronously.
|
|
489
|
+
* Synchronous version of packets.
|
|
427
490
|
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
491
|
+
* High-level sync generator for complete encoding pipeline.
|
|
492
|
+
* Automatically manages frame memory, encoder state,
|
|
493
|
+
* and flushes buffered packets at end.
|
|
494
|
+
* Primary interface for stream-based encoding.
|
|
431
495
|
*
|
|
432
|
-
*
|
|
496
|
+
* @param frames - Iterable of frames (freed automatically)
|
|
433
497
|
*
|
|
434
|
-
* @
|
|
498
|
+
* @yields {Packet} Encoded packets (caller must free)
|
|
435
499
|
*
|
|
436
500
|
* @throws {Error} If encoder is closed
|
|
437
501
|
*
|
|
502
|
+
* @throws {FFmpegError} If encoding fails
|
|
503
|
+
*
|
|
504
|
+
* @example
|
|
505
|
+
* ```typescript
|
|
506
|
+
* // Basic encoding pipeline
|
|
507
|
+
* for (const packet of encoder.packetsSync(decoder.framesSync(packets))) {
|
|
508
|
+
* output.writePacketSync(packet);
|
|
509
|
+
* packet.free(); // Must free output packets
|
|
510
|
+
* }
|
|
511
|
+
* ```
|
|
512
|
+
*
|
|
513
|
+
* @example
|
|
514
|
+
* ```typescript
|
|
515
|
+
* // With frame filtering
|
|
516
|
+
* function* filteredFrames() {
|
|
517
|
+
* for (const frame of decoder.framesSync(packets)) {
|
|
518
|
+
* const filtered = filter.processSync(frame);
|
|
519
|
+
* if (filtered) {
|
|
520
|
+
* yield filtered;
|
|
521
|
+
* }
|
|
522
|
+
* frame.free();
|
|
523
|
+
* }
|
|
524
|
+
* }
|
|
525
|
+
*
|
|
526
|
+
* for (const packet of encoder.packetsSync(filteredFrames())) {
|
|
527
|
+
* output.writePacketSync(packet);
|
|
528
|
+
* packet.free();
|
|
529
|
+
* }
|
|
530
|
+
* ```
|
|
531
|
+
*
|
|
532
|
+
* @see {@link packets} For async version
|
|
533
|
+
*/
|
|
534
|
+
*packetsSync(frames) {
|
|
535
|
+
// Process frames
|
|
536
|
+
for (const frame of frames) {
|
|
537
|
+
try {
|
|
538
|
+
const packet = this.encodeSync(frame);
|
|
539
|
+
if (packet) {
|
|
540
|
+
yield packet;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
finally {
|
|
544
|
+
// Free the input frame after encoding
|
|
545
|
+
frame.free();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// Flush encoder after all frames
|
|
549
|
+
this.flushSync();
|
|
550
|
+
while (true) {
|
|
551
|
+
const remaining = this.receiveSync();
|
|
552
|
+
if (!remaining)
|
|
553
|
+
break;
|
|
554
|
+
yield remaining;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Flush encoder and signal end-of-stream.
|
|
559
|
+
*
|
|
560
|
+
* Sends null frame to encoder to signal end-of-stream.
|
|
561
|
+
* Does nothing if encoder was never initialized or is closed.
|
|
562
|
+
* Must call receive() to get remaining buffered packets.
|
|
563
|
+
*
|
|
564
|
+
* Direct mapping to avcodec_send_frame(NULL).
|
|
565
|
+
*
|
|
438
566
|
* @example
|
|
439
567
|
* ```typescript
|
|
440
|
-
* //
|
|
568
|
+
* // Signal end of stream
|
|
569
|
+
* await encoder.flush();
|
|
570
|
+
*
|
|
571
|
+
* // Then get remaining packets
|
|
441
572
|
* let packet;
|
|
442
|
-
* while ((packet = await encoder.
|
|
573
|
+
* while ((packet = await encoder.receive()) !== null) {
|
|
443
574
|
* console.log('Got buffered packet');
|
|
444
575
|
* await output.writePacket(packet);
|
|
445
576
|
* packet.free();
|
|
@@ -447,26 +578,66 @@ export class Encoder {
|
|
|
447
578
|
* ```
|
|
448
579
|
*
|
|
449
580
|
* @see {@link flushPackets} For async iteration
|
|
450
|
-
* @see {@link
|
|
581
|
+
* @see {@link receive} For getting buffered packets
|
|
451
582
|
*/
|
|
452
583
|
async flush() {
|
|
453
|
-
if (!this.
|
|
454
|
-
|
|
584
|
+
if (this.isClosed || !this.initialized) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
// Send flush frame (null)
|
|
588
|
+
const ret = await this.codecContext.sendFrame(null);
|
|
589
|
+
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
590
|
+
if (ret !== AVERROR_EAGAIN) {
|
|
591
|
+
FFmpegError.throwIfError(ret, 'Failed to flush encoder');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Flush encoder and signal end-of-stream synchronously.
|
|
597
|
+
* Synchronous version of flush.
|
|
598
|
+
*
|
|
599
|
+
* Sends null frame to encoder to signal end-of-stream.
|
|
600
|
+
* Does nothing if encoder was never initialized or is closed.
|
|
601
|
+
* Must call receiveSync() to get remaining buffered packets.
|
|
602
|
+
*
|
|
603
|
+
* Direct mapping to avcodec_send_frame(NULL).
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* ```typescript
|
|
607
|
+
* // Signal end of stream
|
|
608
|
+
* encoder.flushSync();
|
|
609
|
+
*
|
|
610
|
+
* // Then get remaining packets
|
|
611
|
+
* let packet;
|
|
612
|
+
* while ((packet = encoder.receiveSync()) !== null) {
|
|
613
|
+
* console.log('Got buffered packet');
|
|
614
|
+
* output.writePacketSync(packet);
|
|
615
|
+
* packet.free();
|
|
616
|
+
* }
|
|
617
|
+
* ```
|
|
618
|
+
*
|
|
619
|
+
* @see {@link flush} For async version
|
|
620
|
+
*/
|
|
621
|
+
flushSync() {
|
|
622
|
+
if (this.isClosed || !this.initialized) {
|
|
623
|
+
return;
|
|
455
624
|
}
|
|
456
625
|
// Send flush frame (null)
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
626
|
+
const ret = this.codecContext.sendFrameSync(null);
|
|
627
|
+
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
628
|
+
if (ret !== AVERROR_EAGAIN) {
|
|
629
|
+
FFmpegError.throwIfError(ret, 'Failed to flush encoder');
|
|
630
|
+
}
|
|
631
|
+
}
|
|
460
632
|
}
|
|
461
633
|
/**
|
|
462
634
|
* Flush all buffered packets as async generator.
|
|
463
635
|
*
|
|
464
636
|
* Convenient async iteration over remaining packets.
|
|
465
|
-
* Automatically handles repeated
|
|
466
|
-
*
|
|
637
|
+
* Automatically handles flush and repeated receive calls.
|
|
638
|
+
* Returns immediately if encoder was never initialized or is closed.
|
|
467
639
|
*
|
|
468
|
-
* @yields Buffered packets
|
|
469
|
-
* @throws {Error} If encoder is closed
|
|
640
|
+
* @yields {Packet} Buffered packets
|
|
470
641
|
*
|
|
471
642
|
* @example
|
|
472
643
|
* ```typescript
|
|
@@ -478,96 +649,151 @@ export class Encoder {
|
|
|
478
649
|
* }
|
|
479
650
|
* ```
|
|
480
651
|
*
|
|
481
|
-
* @see {@link flush} For
|
|
652
|
+
* @see {@link flush} For signaling end-of-stream
|
|
482
653
|
* @see {@link packets} For complete pipeline
|
|
483
654
|
*/
|
|
484
655
|
async *flushPackets() {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
656
|
+
// Send flush signal
|
|
657
|
+
await this.flush();
|
|
488
658
|
let packet;
|
|
489
|
-
while ((packet = await this.
|
|
659
|
+
while ((packet = await this.receive()) !== null) {
|
|
490
660
|
yield packet;
|
|
491
661
|
}
|
|
492
662
|
}
|
|
493
663
|
/**
|
|
494
|
-
*
|
|
664
|
+
* Flush all buffered packets as generator synchronously.
|
|
665
|
+
* Synchronous version of flushPackets.
|
|
495
666
|
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
667
|
+
* Convenient sync iteration over remaining packets.
|
|
668
|
+
* Automatically handles flush and repeated receive calls.
|
|
669
|
+
* Returns immediately if encoder was never initialized or is closed.
|
|
670
|
+
*
|
|
671
|
+
* @yields {Packet} Buffered packets
|
|
500
672
|
*
|
|
501
673
|
* @example
|
|
502
674
|
* ```typescript
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
675
|
+
* // Flush at end of encoding
|
|
676
|
+
* for (const packet of encoder.flushPacketsSync()) {
|
|
677
|
+
* console.log('Processing buffered packet');
|
|
678
|
+
* output.writePacketSync(packet);
|
|
679
|
+
* packet.free();
|
|
508
680
|
* }
|
|
509
681
|
* ```
|
|
510
682
|
*
|
|
511
|
-
* @see {@link
|
|
683
|
+
* @see {@link flushPackets} For async version
|
|
512
684
|
*/
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
this.
|
|
518
|
-
|
|
685
|
+
*flushPacketsSync() {
|
|
686
|
+
// Send flush signal
|
|
687
|
+
this.flushSync();
|
|
688
|
+
let packet;
|
|
689
|
+
while ((packet = this.receiveSync()) !== null) {
|
|
690
|
+
yield packet;
|
|
691
|
+
}
|
|
519
692
|
}
|
|
520
693
|
/**
|
|
521
|
-
*
|
|
694
|
+
* Receive packet from encoder.
|
|
522
695
|
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
696
|
+
* Gets encoded packets from the codec's internal buffer.
|
|
697
|
+
* Handles packet cloning and error checking.
|
|
698
|
+
* Returns null if encoder is closed, not initialized, or no packets available.
|
|
699
|
+
* Call repeatedly until null to drain all buffered packets.
|
|
525
700
|
*
|
|
526
|
-
*
|
|
701
|
+
* Direct mapping to avcodec_receive_packet().
|
|
702
|
+
*
|
|
703
|
+
* @returns Cloned packet or null if no packets available
|
|
704
|
+
*
|
|
705
|
+
* @throws {FFmpegError} If receive fails with error other than AVERROR_EAGAIN or AVERROR_EOF
|
|
527
706
|
*
|
|
528
707
|
* @example
|
|
529
708
|
* ```typescript
|
|
530
|
-
* const
|
|
531
|
-
*
|
|
532
|
-
*
|
|
709
|
+
* const packet = await encoder.receive();
|
|
710
|
+
* if (packet) {
|
|
711
|
+
* console.log(`Got packet with PTS: ${packet.pts}`);
|
|
712
|
+
* await output.writePacket(packet);
|
|
713
|
+
* packet.free();
|
|
714
|
+
* }
|
|
533
715
|
* ```
|
|
534
716
|
*
|
|
535
|
-
* @
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
* @returns Codec context or null
|
|
717
|
+
* @example
|
|
718
|
+
* ```typescript
|
|
719
|
+
* // Drain all buffered packets
|
|
720
|
+
* let packet;
|
|
721
|
+
* while ((packet = await encoder.receive()) !== null) {
|
|
722
|
+
* console.log(`Packet size: ${packet.size}`);
|
|
723
|
+
* await output.writePacket(packet);
|
|
724
|
+
* packet.free();
|
|
725
|
+
* }
|
|
726
|
+
* ```
|
|
547
727
|
*
|
|
548
|
-
* @
|
|
728
|
+
* @see {@link encode} For sending frames and receiving packets
|
|
729
|
+
* @see {@link flush} For signaling end-of-stream
|
|
549
730
|
*/
|
|
550
|
-
|
|
551
|
-
|
|
731
|
+
async receive() {
|
|
732
|
+
if (this.isClosed || !this.initialized) {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
// Clear previous packet data
|
|
736
|
+
this.packet.unref();
|
|
737
|
+
const ret = await this.codecContext.receivePacket(this.packet);
|
|
738
|
+
if (ret === 0) {
|
|
739
|
+
// Got a packet, clone it for the user
|
|
740
|
+
return this.packet.clone();
|
|
741
|
+
}
|
|
742
|
+
else if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
|
|
743
|
+
// Need more data or end of stream
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
// Error
|
|
748
|
+
FFmpegError.throwIfError(ret, 'Failed to receive packet');
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
552
751
|
}
|
|
553
752
|
/**
|
|
554
|
-
* Receive packet from encoder.
|
|
753
|
+
* Receive packet from encoder synchronously.
|
|
754
|
+
* Synchronous version of receive.
|
|
555
755
|
*
|
|
556
|
-
*
|
|
756
|
+
* Gets encoded packets from the codec's internal buffer.
|
|
557
757
|
* Handles packet cloning and error checking.
|
|
758
|
+
* Returns null if encoder is closed, not initialized, or no packets available.
|
|
759
|
+
* Call repeatedly until null to drain all buffered packets.
|
|
558
760
|
*
|
|
559
761
|
* Direct mapping to avcodec_receive_packet().
|
|
560
762
|
*
|
|
561
|
-
* @returns Cloned packet or null
|
|
763
|
+
* @returns Cloned packet or null if no packets available
|
|
562
764
|
*
|
|
563
765
|
* @throws {FFmpegError} If receive fails with error other than AVERROR_EAGAIN or AVERROR_EOF
|
|
564
766
|
*
|
|
565
|
-
* @
|
|
767
|
+
* @example
|
|
768
|
+
* ```typescript
|
|
769
|
+
* const packet = encoder.receiveSync();
|
|
770
|
+
* if (packet) {
|
|
771
|
+
* console.log(`Got packet with PTS: ${packet.pts}`);
|
|
772
|
+
* output.writePacketSync(packet);
|
|
773
|
+
* packet.free();
|
|
774
|
+
* }
|
|
775
|
+
* ```
|
|
776
|
+
*
|
|
777
|
+
* @example
|
|
778
|
+
* ```typescript
|
|
779
|
+
* // Drain all buffered packets
|
|
780
|
+
* let packet;
|
|
781
|
+
* while ((packet = encoder.receiveSync()) !== null) {
|
|
782
|
+
* console.log(`Packet size: ${packet.size}`);
|
|
783
|
+
* output.writePacketSync(packet);
|
|
784
|
+
* packet.free();
|
|
785
|
+
* }
|
|
786
|
+
* ```
|
|
787
|
+
*
|
|
788
|
+
* @see {@link receive} For async version
|
|
566
789
|
*/
|
|
567
|
-
|
|
790
|
+
receiveSync() {
|
|
791
|
+
if (this.isClosed || !this.initialized) {
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
568
794
|
// Clear previous packet data
|
|
569
795
|
this.packet.unref();
|
|
570
|
-
const ret =
|
|
796
|
+
const ret = this.codecContext.receivePacketSync(this.packet);
|
|
571
797
|
if (ret === 0) {
|
|
572
798
|
// Got a packet, clone it for the user
|
|
573
799
|
return this.packet.clone();
|
|
@@ -582,6 +808,138 @@ export class Encoder {
|
|
|
582
808
|
return null;
|
|
583
809
|
}
|
|
584
810
|
}
|
|
811
|
+
/**
|
|
812
|
+
* Close encoder and free resources.
|
|
813
|
+
*
|
|
814
|
+
* Releases codec context and internal packet buffer.
|
|
815
|
+
* Safe to call multiple times.
|
|
816
|
+
* Automatically called by Symbol.dispose.
|
|
817
|
+
*
|
|
818
|
+
* @example
|
|
819
|
+
* ```typescript
|
|
820
|
+
* const encoder = await Encoder.create(FF_ENCODER_LIBX264, { ... });
|
|
821
|
+
* try {
|
|
822
|
+
* // Use encoder
|
|
823
|
+
* } finally {
|
|
824
|
+
* encoder.close();
|
|
825
|
+
* }
|
|
826
|
+
* ```
|
|
827
|
+
*
|
|
828
|
+
* @see {@link Symbol.dispose} For automatic cleanup
|
|
829
|
+
*/
|
|
830
|
+
close() {
|
|
831
|
+
if (this.isClosed) {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
this.isClosed = true;
|
|
835
|
+
this.packet.free();
|
|
836
|
+
this.codecContext.freeContext();
|
|
837
|
+
this.initialized = false;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Initialize encoder from first frame.
|
|
841
|
+
*
|
|
842
|
+
* Sets codec context parameters from frame properties.
|
|
843
|
+
* Configures hardware context if present in frame.
|
|
844
|
+
* Opens encoder with accumulated options.
|
|
845
|
+
*
|
|
846
|
+
* @param frame - First frame to encode
|
|
847
|
+
*
|
|
848
|
+
* @throws {FFmpegError} If encoder open fails
|
|
849
|
+
*
|
|
850
|
+
* @internal
|
|
851
|
+
*/
|
|
852
|
+
async initialize(frame) {
|
|
853
|
+
if (frame.isVideo()) {
|
|
854
|
+
this.codecContext.width = frame.width;
|
|
855
|
+
this.codecContext.height = frame.height;
|
|
856
|
+
this.codecContext.pixelFormat = frame.format;
|
|
857
|
+
this.codecContext.sampleAspectRatio = frame.sampleAspectRatio;
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
this.codecContext.sampleRate = frame.sampleRate;
|
|
861
|
+
this.codecContext.sampleFormat = frame.format;
|
|
862
|
+
this.codecContext.channelLayout = frame.channelLayout;
|
|
863
|
+
}
|
|
864
|
+
this.codecContext.hwDeviceCtx = frame.hwFramesCtx?.deviceRef ?? null;
|
|
865
|
+
this.codecContext.hwFramesCtx = frame.hwFramesCtx;
|
|
866
|
+
// Open codec
|
|
867
|
+
const openRet = await this.codecContext.open2(this.codec, this.opts);
|
|
868
|
+
if (openRet < 0) {
|
|
869
|
+
this.codecContext.freeContext();
|
|
870
|
+
FFmpegError.throwIfError(openRet, 'Failed to open encoder');
|
|
871
|
+
}
|
|
872
|
+
this.initialized = true;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Initialize encoder from first frame synchronously.
|
|
876
|
+
* Synchronous version of initialize.
|
|
877
|
+
*
|
|
878
|
+
* Sets codec context parameters from frame properties.
|
|
879
|
+
* Configures hardware context if present in frame.
|
|
880
|
+
* Opens encoder with accumulated options.
|
|
881
|
+
*
|
|
882
|
+
* @param frame - First frame to encode
|
|
883
|
+
*
|
|
884
|
+
* @throws {FFmpegError} If encoder open fails
|
|
885
|
+
*
|
|
886
|
+
* @internal
|
|
887
|
+
*
|
|
888
|
+
* @see {@link initialize} For async version
|
|
889
|
+
*/
|
|
890
|
+
initializeSync(frame) {
|
|
891
|
+
if (frame.isVideo()) {
|
|
892
|
+
this.codecContext.width = frame.width;
|
|
893
|
+
this.codecContext.height = frame.height;
|
|
894
|
+
this.codecContext.pixelFormat = frame.format;
|
|
895
|
+
this.codecContext.sampleAspectRatio = frame.sampleAspectRatio;
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
this.codecContext.sampleRate = frame.sampleRate;
|
|
899
|
+
this.codecContext.sampleFormat = frame.format;
|
|
900
|
+
this.codecContext.channelLayout = frame.channelLayout;
|
|
901
|
+
}
|
|
902
|
+
this.codecContext.hwDeviceCtx = frame.hwFramesCtx?.deviceRef ?? null;
|
|
903
|
+
this.codecContext.hwFramesCtx = frame.hwFramesCtx;
|
|
904
|
+
// Open codec
|
|
905
|
+
const openRet = this.codecContext.open2Sync(this.codec, this.opts);
|
|
906
|
+
if (openRet < 0) {
|
|
907
|
+
this.codecContext.freeContext();
|
|
908
|
+
FFmpegError.throwIfError(openRet, 'Failed to open encoder');
|
|
909
|
+
}
|
|
910
|
+
this.initialized = true;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Get encoder codec.
|
|
914
|
+
*
|
|
915
|
+
* Returns the codec used by this encoder.
|
|
916
|
+
* Useful for checking codec capabilities and properties.
|
|
917
|
+
*
|
|
918
|
+
* @returns Codec instance
|
|
919
|
+
*
|
|
920
|
+
* @internal
|
|
921
|
+
*
|
|
922
|
+
* @see {@link Codec} For codec details
|
|
923
|
+
*/
|
|
924
|
+
getCodec() {
|
|
925
|
+
return this.codec;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Get underlying codec context.
|
|
929
|
+
*
|
|
930
|
+
* Returns the codec context for advanced operations.
|
|
931
|
+
* Useful for accessing low-level codec properties and settings.
|
|
932
|
+
* Returns null if encoder is closed or not initialized.
|
|
933
|
+
*
|
|
934
|
+
* @returns Codec context or null if closed/not initialized
|
|
935
|
+
*
|
|
936
|
+
* @internal
|
|
937
|
+
*
|
|
938
|
+
* @see {@link CodecContext} For context details
|
|
939
|
+
*/
|
|
940
|
+
getCodecContext() {
|
|
941
|
+
return !this.isClosed && this.initialized ? this.codecContext : null;
|
|
942
|
+
}
|
|
585
943
|
/**
|
|
586
944
|
* Dispose of encoder.
|
|
587
945
|
*
|
|
@@ -591,7 +949,7 @@ export class Encoder {
|
|
|
591
949
|
* @example
|
|
592
950
|
* ```typescript
|
|
593
951
|
* {
|
|
594
|
-
* using encoder = await Encoder.create(FF_ENCODER_LIBX264,
|
|
952
|
+
* using encoder = await Encoder.create(FF_ENCODER_LIBX264, { ... });
|
|
595
953
|
* // Encode frames...
|
|
596
954
|
* } // Automatically closed
|
|
597
955
|
* ```
|