@mediabunny/server 1.45.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.
Files changed (41) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +276 -0
  3. package/dist/bundles/mediabunny-server.cjs +3361 -0
  4. package/dist/bundles/mediabunny-server.min.cjs +10 -0
  5. package/dist/bundles/mediabunny-server.min.mjs +9 -0
  6. package/dist/bundles/mediabunny-server.mjs +3334 -0
  7. package/dist/mediabunny-server.d.ts +105 -0
  8. package/dist/modules/src/audio-decoder.d.ts +21 -0
  9. package/dist/modules/src/audio-decoder.d.ts.map +1 -0
  10. package/dist/modules/src/audio-decoder.js +132 -0
  11. package/dist/modules/src/audio-encoder.d.ts +35 -0
  12. package/dist/modules/src/audio-encoder.d.ts.map +1 -0
  13. package/dist/modules/src/audio-encoder.js +329 -0
  14. package/dist/modules/src/audio-sample.d.ts +38 -0
  15. package/dist/modules/src/audio-sample.d.ts.map +1 -0
  16. package/dist/modules/src/audio-sample.js +119 -0
  17. package/dist/modules/src/index.d.ts +39 -0
  18. package/dist/modules/src/index.d.ts.map +1 -0
  19. package/dist/modules/src/index.js +132 -0
  20. package/dist/modules/src/misc.d.ts +26 -0
  21. package/dist/modules/src/misc.d.ts.map +1 -0
  22. package/dist/modules/src/misc.js +255 -0
  23. package/dist/modules/src/video-decoder.d.ts +30 -0
  24. package/dist/modules/src/video-decoder.d.ts.map +1 -0
  25. package/dist/modules/src/video-decoder.js +214 -0
  26. package/dist/modules/src/video-encoder.d.ts +35 -0
  27. package/dist/modules/src/video-encoder.d.ts.map +1 -0
  28. package/dist/modules/src/video-encoder.js +474 -0
  29. package/dist/modules/src/video-sample.d.ts +45 -0
  30. package/dist/modules/src/video-sample.d.ts.map +1 -0
  31. package/dist/modules/src/video-sample.js +276 -0
  32. package/dist/modules/tsconfig.tsbuildinfo +1 -0
  33. package/package.json +59 -0
  34. package/src/audio-decoder.ts +120 -0
  35. package/src/audio-encoder.ts +396 -0
  36. package/src/audio-sample.ts +101 -0
  37. package/src/index.ts +104 -0
  38. package/src/misc.ts +242 -0
  39. package/src/video-decoder.ts +224 -0
  40. package/src/video-encoder.ts +568 -0
  41. package/src/video-sample.ts +313 -0
@@ -0,0 +1,568 @@
1
+ /*!
2
+ * Copyright (c) 2026-present, Vanilagy and contributors
3
+ *
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+ import {
10
+ CustomVideoEncoder,
11
+ type MaybePromise,
12
+ QUALITY_MEDIUM,
13
+ VideoCodec,
14
+ VideoSample,
15
+ EncodedPacket,
16
+ EncodedPacketSideData,
17
+ } from 'mediabunny';
18
+ import * as NodeAv from 'node-av';
19
+ import {
20
+ CODEC_TO_CODEC_ID,
21
+ getHardwareEncoderCodec,
22
+ LIBVPX_VP9,
23
+ unmapColorPrimaries,
24
+ unmapMatrixCoefficients,
25
+ unmapTransferCharacteristics,
26
+ } from './misc';
27
+ import { copyVideoSampleToAvFrame, AvFrameVideoSampleResource } from './video-sample';
28
+ import {
29
+ AvcNalUnitType,
30
+ extractAv1CodecInfoFromPacket,
31
+ extractAvcDecoderConfigurationRecord,
32
+ extractHevcDecoderConfigurationRecord,
33
+ extractNalUnitTypeForAvc,
34
+ extractNalUnitTypeForHevc,
35
+ extractVp9CodecInfoFromPacket,
36
+ HevcNalUnitType,
37
+ iterateNalUnitsInAnnexB,
38
+ NalUnitLocation,
39
+ serializeAvcDecoderConfigurationRecord,
40
+ serializeHevcDecoderConfigurationRecord,
41
+ } from '../../../src/codec-data';
42
+ import { extractVideoCodecString } from '../../../src/codec';
43
+ import { assert, binarySearchLessOrEqual, simplifyRational, toUint8Array } from '../../../src/misc';
44
+
45
+ export class NodeAvVideoEncoder extends CustomVideoEncoder {
46
+ frame!: NodeAv.Frame;
47
+ packet!: NodeAv.Packet;
48
+ avCodec!: NodeAv.Codec;
49
+ codecContext: NodeAv.CodecContext | null = null;
50
+ lastBuffer: Buffer | null = null;
51
+ packetEmitted = false;
52
+
53
+ scaler: NodeAv.SoftwareScaleContext | null = null;
54
+ lastScalerKey: string | null = null;
55
+ dstFrame: NodeAv.Frame | null = null;
56
+
57
+ // Bookkeeping to restore the original timing information
58
+ preciseTimings: {
59
+ microsecondTimestamp: number;
60
+ timestamp: number;
61
+ duration: number;
62
+ timestampIsValid: boolean;
63
+ durationIsValid: boolean;
64
+ }[] = [];
65
+
66
+ static override supports(codec: VideoCodec, config: VideoEncoderConfig): boolean {
67
+ return (codec === 'avc' || codec === 'hevc' || codec === 'vp8' || codec === 'vp9' || codec === 'av1')
68
+ && config.bitrateMode !== 'quantizer';
69
+ }
70
+
71
+ async init(): Promise<void> {
72
+ this.frame = new NodeAv.Frame();
73
+ this.frame.alloc();
74
+ this.frame.timeBase = new NodeAv.Rational(1, 1e6);
75
+
76
+ this.packet = new NodeAv.Packet();
77
+ this.packet.alloc();
78
+
79
+ const codecId = CODEC_TO_CODEC_ID[this.codec];
80
+ assert(codecId !== undefined);
81
+
82
+ let codec: NodeAv.Codec | null = null;
83
+ if (this.codec === 'vp9' && this.config.alpha === 'keep') {
84
+ codec = NodeAv.Codec.findEncoderByName(LIBVPX_VP9) ?? NodeAv.Codec.findEncoder(codecId);
85
+ } else if (this.config.hardwareAcceleration === 'prefer-software') {
86
+ codec = NodeAv.Codec.findEncoder(codecId);
87
+ } else {
88
+ codec = getHardwareEncoderCodec(codecId) ?? NodeAv.Codec.findEncoder(codecId);
89
+ }
90
+
91
+ if (!codec) {
92
+ throw new Error(`Unable to obtain libav codec for '${this.codec}'.`);
93
+ }
94
+
95
+ this.avCodec = codec;
96
+
97
+ await this.createCodecContext();
98
+ }
99
+
100
+ async createCodecContext() {
101
+ assert(this.codecContext === null);
102
+
103
+ const codecContext = new NodeAv.CodecContext();
104
+ codecContext.allocContext3(this.avCodec);
105
+
106
+ let pixelFormat = NodeAv.AV_PIX_FMT_YUV420P;
107
+
108
+ if (this.avCodec.pixelFormats) {
109
+ if (!this.avCodec.pixelFormats.includes(NodeAv.AV_PIX_FMT_YUV420P)) {
110
+ pixelFormat = this.avCodec.pixelFormats[0]!;
111
+ }
112
+
113
+ if (this.config.alpha === 'keep' && this.avCodec.pixelFormats.includes(NodeAv.AV_PIX_FMT_YUVA420P)) {
114
+ pixelFormat = NodeAv.AV_PIX_FMT_YUVA420P;
115
+ }
116
+ }
117
+
118
+ const pixelAspectRatio = simplifyRational({
119
+ num: (this.config.displayWidth ?? this.config.width) * this.config.height,
120
+ den: (this.config.displayHeight ?? this.config.height) * this.config.width,
121
+ });
122
+
123
+ codecContext.width = this.config.width;
124
+ codecContext.height = this.config.height;
125
+ codecContext.pixelFormat = pixelFormat;
126
+ codecContext.timeBase = new NodeAv.Rational(1, 1e6);
127
+ codecContext.gopSize = 60;
128
+ codecContext.framerate = new NodeAv.Rational(Math.round(this.config.framerate ?? 0) || 30, 1);
129
+ codecContext.bitRate = BigInt(
130
+ this.config.bitrate ?? QUALITY_MEDIUM._toVideoBitrate(this.codec, this.config.width, this.config.height),
131
+ );
132
+ codecContext.sampleAspectRatio = new NodeAv.Rational(pixelAspectRatio.num, pixelAspectRatio.den);
133
+
134
+ if (this.config.bitrateMode === 'constant') {
135
+ codecContext.rcMinRate = codecContext.bitRate;
136
+ codecContext.rcMaxRate = codecContext.bitRate;
137
+ }
138
+
139
+ const isRealtime = this.config.latencyMode === 'realtime';
140
+
141
+ if (this.avCodec.name === 'libx264') {
142
+ if (isRealtime) {
143
+ codecContext.setOption('tune', 'zerolatency');
144
+ codecContext.setOption('preset', 'ultrafast');
145
+ }
146
+ } else if (this.avCodec.name === 'libx265') {
147
+ codecContext.setOption('x265-params', 'log-level=error');
148
+
149
+ if (isRealtime) {
150
+ codecContext.setOption('tune', 'zerolatency');
151
+ codecContext.setOption('preset', 'ultrafast');
152
+ }
153
+ } else if (this.avCodec.name === 'libvpx') {
154
+ if (isRealtime) {
155
+ codecContext.setOption('deadline', 'realtime');
156
+ codecContext.setOption('cpu-used', '8');
157
+ } else {
158
+ codecContext.setOption('cpu-used', '8');
159
+ }
160
+ } else if (this.avCodec.name === 'libvpx-vp9') {
161
+ codecContext.setOption('deadline', 'realtime');
162
+
163
+ if (isRealtime) {
164
+ codecContext.setOption('cpu-used', '8');
165
+ } else {
166
+ codecContext.setOption('cpu-used', '5');
167
+ }
168
+ } else if (this.avCodec.name === 'libsvtav1') {
169
+ // SVTAV1 can be silenced by setting an environment variable:
170
+ // https://superuser.com/questions/1775236/how-to-remove-svt-av1-information-from-ffmpeg-output
171
+ process.env['SVT_LOG'] = '1';
172
+
173
+ if (isRealtime) {
174
+ codecContext.setOption('preset', '12');
175
+ }
176
+ }
177
+
178
+ const ret = await codecContext.open2();
179
+ NodeAv.FFmpegError.throwIfError(ret, 'Open codec context');
180
+
181
+ this.codecContext = codecContext;
182
+ }
183
+
184
+ async encode(videoSample: VideoSample, options: VideoEncoderEncodeOptions): Promise<void> {
185
+ if (this.codecContext === null) {
186
+ await this.createCodecContext();
187
+ assert(this.codecContext);
188
+ }
189
+
190
+ if (videoSample._data instanceof AvFrameVideoSampleResource) {
191
+ this.frame.ref(videoSample._data.frame);
192
+ } else {
193
+ if (videoSample.format === null) {
194
+ throw new Error('Cannot encode foreign VideoSample with unknown (null) format.');
195
+ }
196
+
197
+ this.lastBuffer = await copyVideoSampleToAvFrame(videoSample, this.frame, this.lastBuffer);
198
+ }
199
+
200
+ let frameToEncode = this.frame;
201
+
202
+ const requiresScaler
203
+ = this.codecContext.pixelFormat !== this.frame.format
204
+ || this.codecContext.width !== this.frame.width
205
+ || this.codecContext.height !== this.frame.height;
206
+
207
+ if (requiresScaler) {
208
+ if (!this.scaler) {
209
+ this.scaler = new NodeAv.SoftwareScaleContext();
210
+ }
211
+
212
+ const key = `${this.frame.width}x${this.frame.height}:${this.frame.format}`;
213
+ const needsConfigure = key !== this.lastScalerKey;
214
+
215
+ if (needsConfigure) {
216
+ this.scaler.getContext(
217
+ this.frame.width, this.frame.height, this.frame.format as NodeAv.AVPixelFormat,
218
+ this.codecContext.width, this.codecContext.height, this.codecContext.pixelFormat,
219
+ NodeAv.SWS_FAST_BILINEAR,
220
+ );
221
+
222
+ this.lastScalerKey = key;
223
+
224
+ const ret = this.scaler.initContext();
225
+ NodeAv.FFmpegError.throwIfError(ret, 'initContext');
226
+ }
227
+
228
+ if (!this.dstFrame) {
229
+ this.dstFrame = new NodeAv.Frame();
230
+ this.dstFrame.alloc();
231
+ this.dstFrame.width = this.codecContext.width;
232
+ this.dstFrame.height = this.codecContext.height;
233
+ this.dstFrame.format = this.codecContext.pixelFormat;
234
+ this.dstFrame.allocBuffer();
235
+ }
236
+
237
+ await this.scaler.scaleFrame(this.dstFrame, this.frame);
238
+ this.dstFrame.copyProps(this.frame);
239
+ frameToEncode = this.dstFrame;
240
+ }
241
+
242
+ frameToEncode.pts = BigInt(videoSample.microsecondTimestamp);
243
+ frameToEncode.duration = BigInt(videoSample.microsecondDuration);
244
+ frameToEncode.timeBase = new NodeAv.Rational(1, 1e6);
245
+
246
+ // Let's just set both for good measure
247
+ frameToEncode.pictType = options?.keyFrame
248
+ ? NodeAv.AV_PICTURE_TYPE_I
249
+ : NodeAv.AV_PICTURE_TYPE_NONE;
250
+ frameToEncode.keyFrame = options?.keyFrame
251
+ ? 1
252
+ : 0;
253
+
254
+ const preciseTimingIndex = binarySearchLessOrEqual(
255
+ this.preciseTimings,
256
+ videoSample.microsecondTimestamp,
257
+ x => x.microsecondTimestamp,
258
+ );
259
+ const existingEntry = preciseTimingIndex !== -1
260
+ ? this.preciseTimings[preciseTimingIndex]
261
+ : null;
262
+ if (existingEntry && existingEntry.microsecondTimestamp === videoSample.microsecondTimestamp) {
263
+ if (existingEntry.timestamp !== videoSample.timestamp) {
264
+ // Mapping isn't unique, can't use the timestamp
265
+ existingEntry.timestampIsValid = false;
266
+ }
267
+ if (existingEntry.duration !== videoSample.duration) {
268
+ // Mapping isn't unique, can't use the duration
269
+ existingEntry.durationIsValid = false;
270
+ }
271
+ } else {
272
+ this.preciseTimings.splice(preciseTimingIndex + 1, 0, {
273
+ microsecondTimestamp: videoSample.microsecondTimestamp,
274
+ timestamp: videoSample.timestamp,
275
+ duration: videoSample.duration,
276
+ timestampIsValid: true,
277
+ durationIsValid: true,
278
+ });
279
+
280
+ // Make sure it doesn't grow indefinitely
281
+ if (this.preciseTimings.length > 128) {
282
+ this.preciseTimings.shift();
283
+ }
284
+ }
285
+
286
+ const ret = await this.codecContext.sendFrame(frameToEncode);
287
+ NodeAv.FFmpegError.throwIfError(ret, 'Send frame');
288
+
289
+ // Keep receiving packets until no more are available for this frame
290
+ while (true) {
291
+ const receiveRet = await this.codecContext.receivePacket(this.packet);
292
+ if (receiveRet === NodeAv.AVERROR_EAGAIN || receiveRet === NodeAv.AVERROR_EOF) {
293
+ break;
294
+ }
295
+
296
+ this.receivePacket(receiveRet);
297
+ }
298
+ }
299
+
300
+ receivePacket(ret: number) {
301
+ assert(this.codecContext);
302
+ NodeAv.FFmpegError.throwIfError(ret, 'Receive packet');
303
+
304
+ if (!this.packet.data) {
305
+ return;
306
+ }
307
+ let packetData = toUint8Array(this.packet.data);
308
+
309
+ let timestamp = Number(this.packet.pts) / 1e6;
310
+ let duration = Number(this.packet.duration) / 1e6;
311
+
312
+ const preciseTimingIndex = binarySearchLessOrEqual(
313
+ this.preciseTimings,
314
+ Number(this.packet.pts),
315
+ x => x.microsecondTimestamp,
316
+ );
317
+ const entry = preciseTimingIndex !== -1
318
+ ? this.preciseTimings[preciseTimingIndex]
319
+ : null;
320
+
321
+ // If there's a relevant timing entry, refine the packet's timing data to get better accuracy than
322
+ // microseconds
323
+ if (entry && entry.microsecondTimestamp === Number(this.packet.pts)) {
324
+ if (entry.timestampIsValid) {
325
+ timestamp = entry.timestamp;
326
+ }
327
+ if (entry.durationIsValid) {
328
+ duration = entry.duration;
329
+ }
330
+ }
331
+
332
+ const metadata: EncodedVideoChunkMetadata = {};
333
+ let decoderConfigCodecString: string | null = null;
334
+ let decoderConfigDescription: Uint8Array | null = null;
335
+
336
+ if (this.codec === 'avc' || this.codec === 'hevc') {
337
+ let expectsAnnexB = false;
338
+ if (this.codec === 'avc') {
339
+ expectsAnnexB = this.config.avc?.format === 'annexb';
340
+ } else {
341
+ // eslint-disable-next-line @stylistic/max-len
342
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
343
+ expectsAnnexB = (this.config as any).hevc?.format === 'annexb';
344
+ }
345
+
346
+ if (!this.packetEmitted) {
347
+ let serializedRecord: Uint8Array;
348
+
349
+ if (this.codec === 'avc') {
350
+ const record = extractAvcDecoderConfigurationRecord(this.packet.data);
351
+ if (!record) {
352
+ throw new Error('Invalid AVC data, could not extract decoder configuration record.');
353
+ }
354
+
355
+ serializedRecord = serializeAvcDecoderConfigurationRecord(record);
356
+ } else {
357
+ const record = extractHevcDecoderConfigurationRecord(this.packet.data);
358
+ if (!record) {
359
+ throw new Error('Invalid HEVC data, could not extract decoder configuration record.');
360
+ }
361
+
362
+ serializedRecord = serializeHevcDecoderConfigurationRecord(record);
363
+ }
364
+
365
+ decoderConfigCodecString = extractVideoCodecString({
366
+ width: this.config.width,
367
+ height: this.config.height,
368
+ codec: this.codec,
369
+ codecDescription: serializedRecord,
370
+ colorSpace: null,
371
+ avcType: 1,
372
+ avcCodecInfo: null,
373
+ hevcCodecInfo: null,
374
+ vp9CodecInfo: null,
375
+ av1CodecInfo: null,
376
+ });
377
+
378
+ if (!expectsAnnexB) {
379
+ decoderConfigDescription = serializedRecord;
380
+ }
381
+ }
382
+
383
+ if (!expectsAnnexB) {
384
+ const NAL_UNIT_LENGTH_SIZE = 4;
385
+
386
+ const nalUnits: NalUnitLocation[] = [];
387
+ for (const loc of iterateNalUnitsInAnnexB(packetData)) {
388
+ if (this.codec === 'avc') {
389
+ const naluType = extractNalUnitTypeForAvc(packetData[loc.offset]!);
390
+
391
+ // Certain NALUs get stripped
392
+ if (
393
+ naluType !== AvcNalUnitType.SPS
394
+ && naluType !== AvcNalUnitType.PPS
395
+ && naluType !== AvcNalUnitType.SPS_EXT
396
+ ) {
397
+ nalUnits.push(loc);
398
+ }
399
+ } else {
400
+ const naluType = extractNalUnitTypeForHevc(packetData[loc.offset]!);
401
+
402
+ // Certain NALUs get stripped
403
+ if (
404
+ naluType !== HevcNalUnitType.SPS_NUT
405
+ && naluType !== HevcNalUnitType.PPS_NUT
406
+ && naluType !== HevcNalUnitType.VPS_NUT
407
+ ) {
408
+ nalUnits.push(loc);
409
+ }
410
+ }
411
+ }
412
+
413
+ let totalSize = 0;
414
+ for (const nalUnit of nalUnits) {
415
+ totalSize += NAL_UNIT_LENGTH_SIZE + nalUnit.length;
416
+ }
417
+
418
+ const lengthPrefixedData = new Uint8Array(totalSize);
419
+ const dataView = new DataView(lengthPrefixedData.buffer);
420
+ let offset = 0;
421
+
422
+ // Write each NAL unit with its length prefix
423
+ for (const nalUnit of nalUnits) {
424
+ const length = nalUnit.length;
425
+
426
+ dataView.setUint32(offset, length, false);
427
+ offset += 4;
428
+
429
+ lengthPrefixedData.set(
430
+ packetData.subarray(nalUnit.offset, nalUnit.offset + nalUnit.length),
431
+ offset,
432
+ );
433
+ offset += nalUnit.length;
434
+ }
435
+
436
+ packetData = lengthPrefixedData;
437
+ }
438
+ } else if (this.codec === 'vp8') {
439
+ if (!this.packetEmitted) {
440
+ decoderConfigCodecString = extractVideoCodecString({
441
+ width: this.config.width,
442
+ height: this.config.height,
443
+ codec: 'vp8',
444
+ codecDescription: null,
445
+ colorSpace: null,
446
+ avcType: null,
447
+ avcCodecInfo: null,
448
+ hevcCodecInfo: null,
449
+ vp9CodecInfo: null,
450
+ av1CodecInfo: null,
451
+ });
452
+ }
453
+ } else if (this.codec === 'vp9') {
454
+ if (!this.packetEmitted) {
455
+ const vp9CodecInfo = extractVp9CodecInfoFromPacket(packetData);
456
+
457
+ decoderConfigCodecString = extractVideoCodecString({
458
+ width: this.config.width,
459
+ height: this.config.height,
460
+ codec: 'vp9',
461
+ codecDescription: null,
462
+ colorSpace: null,
463
+ avcType: null,
464
+ avcCodecInfo: null,
465
+ hevcCodecInfo: null,
466
+ vp9CodecInfo,
467
+ av1CodecInfo: null,
468
+ });
469
+ }
470
+ } else if (this.codec === 'av1') {
471
+ if (!this.packetEmitted) {
472
+ const av1CodecInfo = extractAv1CodecInfoFromPacket(packetData);
473
+
474
+ decoderConfigCodecString = extractVideoCodecString({
475
+ width: this.config.width,
476
+ height: this.config.height,
477
+ codec: 'av1',
478
+ codecDescription: null,
479
+ colorSpace: null,
480
+ avcType: null,
481
+ avcCodecInfo: null,
482
+ hevcCodecInfo: null,
483
+ vp9CodecInfo: null,
484
+ av1CodecInfo,
485
+ });
486
+ }
487
+ } else {
488
+ throw new Error('Unreachable.');
489
+ }
490
+
491
+ const sideData: EncodedPacketSideData = {};
492
+ const matroskaBlockAdditional = this.packet.getSideData(NodeAv.AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL);
493
+ if (matroskaBlockAdditional) {
494
+ sideData.alpha = toUint8Array(matroskaBlockAdditional).subarray(8); // Skip the BlockAddId
495
+ }
496
+
497
+ const packet = new EncodedPacket(
498
+ packetData,
499
+ this.packet.isKeyframe ? 'key' : 'delta',
500
+ timestamp,
501
+ duration,
502
+ undefined,
503
+ undefined,
504
+ sideData,
505
+ );
506
+
507
+ if (decoderConfigCodecString !== null) {
508
+ // Create the decoder config
509
+ metadata.decoderConfig = {
510
+ codec: decoderConfigCodecString,
511
+ codedWidth: this.codecContext.width,
512
+ codedHeight: this.codecContext.height,
513
+ displayAspectWidth: this.config.displayWidth ?? this.codecContext.width,
514
+ displayAspectHeight: this.config.displayHeight ?? this.codecContext.height,
515
+ description: decoderConfigDescription ?? undefined,
516
+ colorSpace: {
517
+ primaries:
518
+ unmapColorPrimaries(this.codecContext.colorPrimaries) as VideoColorPrimaries,
519
+ matrix:
520
+ unmapMatrixCoefficients(this.codecContext.colorSpace) as VideoMatrixCoefficients,
521
+ transfer:
522
+ unmapTransferCharacteristics(this.codecContext.colorTrc) as VideoTransferCharacteristics,
523
+ fullRange: this.codecContext.colorRange === NodeAv.AVCOL_RANGE_JPEG
524
+ ? true
525
+ : this.codecContext.colorRange === NodeAv.AVCOL_RANGE_MPEG
526
+ ? false
527
+ : undefined,
528
+ },
529
+ };
530
+ }
531
+
532
+ this.packetEmitted = true;
533
+ this.onPacket(packet, metadata);
534
+ }
535
+
536
+ async flush(): Promise<void> {
537
+ if (this.codecContext) {
538
+ // Send null frame to signal flush
539
+ const ret = await this.codecContext.sendFrame(null);
540
+ NodeAv.FFmpegError.throwIfError(ret, 'Send frame');
541
+
542
+ // Keep receiving packets until no more are available
543
+ while (true) {
544
+ const receiveRet = await this.codecContext.receivePacket(this.packet);
545
+ if (receiveRet === NodeAv.AVERROR_EAGAIN || receiveRet === NodeAv.AVERROR_EOF) {
546
+ break;
547
+ }
548
+
549
+ this.receivePacket(receiveRet);
550
+ }
551
+
552
+ this.codecContext.freeContext();
553
+ this.codecContext = null;
554
+ // The codec is done now and can't be reused. Any subsequent encode call will first need to recreate a
555
+ // codec context.
556
+ }
557
+
558
+ this.packetEmitted = false;
559
+ }
560
+
561
+ close(): MaybePromise<void> {
562
+ this.codecContext?.freeContext();
563
+ this.frame.free();
564
+ this.packet.free();
565
+ this.scaler?.freeContext();
566
+ this.dstFrame?.free();
567
+ }
568
+ }