@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
package/src/index.ts ADDED
@@ -0,0 +1,104 @@
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 { AudioSample, registerDecoder, registerEncoder, registerVideoSampleTransformer, VideoSample } from 'mediabunny';
10
+ import * as NodeAv from 'node-av';
11
+ import { NodeAvVideoDecoder } from './video-decoder';
12
+ import { NodeAvVideoEncoder } from './video-encoder';
13
+ import { NodeAvAudioDecoder } from './audio-decoder';
14
+ import { NodeAvAudioEncoder } from './audio-encoder';
15
+ import { copyVideoSampleToAvFrame, AvFrameVideoSampleResource, transformVideoSample } from './video-sample';
16
+ import { copyAudioSampleToAvFrame, AvFrameAudioSampleResource } from './audio-sample';
17
+
18
+ const SERVER_LOADED_SYMBOL = Symbol.for('@mediabunny/server loaded');
19
+ if ((globalThis as Record<symbol, unknown>)[SERVER_LOADED_SYMBOL]) {
20
+ console.error(
21
+ '[WARNING]\n@mediabunny/server was loaded twice.'
22
+ + ' This will likely cause the package not to work correctly.'
23
+ + ' Check if multiple dependencies are importing different versions of @mediabunny/server,'
24
+ + ' or if something is being bundled incorrectly.',
25
+ );
26
+ }
27
+ (globalThis as Record<symbol, unknown>)[SERVER_LOADED_SYMBOL] = true;
28
+
29
+ let registered = false;
30
+
31
+ /**
32
+ * Registers video and audio decoders and encoders for all codecs, using FFmpeg's libavcodec under the hood.
33
+ * Additionally, a custom `VideoSample` transformer based on libavfilter is registered to enable resizing, rotation and
34
+ * cropping of video frames.
35
+ *
36
+ * Make sure to call this function before interacting with Mediabunny.
37
+ *
38
+ * The decoders and encoders will automatically detect hardware acceleration support for each codec and platform and
39
+ * make use of it if applicable.
40
+ *
41
+ * @group \@mediabunny/server
42
+ * @public
43
+ */
44
+ export const registerMediabunnyServer = () => {
45
+ if (registered) {
46
+ return;
47
+ }
48
+ registered = true;
49
+
50
+ NodeAv.Log.setLevel(NodeAv.AV_LOG_ERROR);
51
+
52
+ // Video
53
+ registerDecoder(NodeAvVideoDecoder);
54
+ registerEncoder(NodeAvVideoEncoder);
55
+
56
+ // Audio
57
+ registerDecoder(NodeAvAudioDecoder);
58
+ registerEncoder(NodeAvAudioEncoder);
59
+
60
+ registerVideoSampleTransformer(transformVideoSample);
61
+ };
62
+
63
+ export { AvFrameVideoSampleResource } from './video-sample';
64
+ export { AvFrameAudioSampleResource } from './audio-sample';
65
+
66
+ /**
67
+ * Copies a `VideoSample` or `AudioSample` into the given NodeAV
68
+ * [`Frame`](https://seydx.github.io/node-av/api/lib/classes/Frame.html), setting up the frame's format, media type,
69
+ * timing and data. When the sample is already backed by an `AVFrame` (via `AvFrameVideoSampleResource` or
70
+ * `AvFrameAudioSampleResource`), the frame is ref'd instead of copied for zero-copy reuse.
71
+ *
72
+ * For video samples, the frame's time base is always set to 1/1000000 (microsecond accuracy). For audio samples, the
73
+ * frame's time base is always set to 1/sampleRate.
74
+ *
75
+ * @group \@mediabunny/server
76
+ * @public
77
+ */
78
+ export const toAvFrame = async (sample: VideoSample | AudioSample, frame: NodeAv.Frame) => {
79
+ if (sample instanceof VideoSample) {
80
+ if (sample._data instanceof AvFrameVideoSampleResource) {
81
+ frame.ref(sample._data.frame);
82
+ } else {
83
+ if (sample.format === null) {
84
+ throw new Error('Cannot convert foreign VideoSample with unknown (null) format.');
85
+ }
86
+
87
+ await copyVideoSampleToAvFrame(sample, frame, null);
88
+ }
89
+
90
+ frame.pts = BigInt(sample.microsecondTimestamp);
91
+ frame.duration = BigInt(sample.microsecondDuration);
92
+ frame.timeBase = new NodeAv.Rational(1, 1e6);
93
+ } else {
94
+ if (sample._data instanceof AvFrameAudioSampleResource) {
95
+ frame.ref(sample._data.frame);
96
+ } else {
97
+ copyAudioSampleToAvFrame(sample, frame);
98
+ }
99
+
100
+ frame.timeBase = new NodeAv.Rational(1, sample.sampleRate);
101
+ frame.pts = BigInt(Math.round(sample.timestamp * sample.sampleRate));
102
+ frame.duration = BigInt(sample.numberOfFrames);
103
+ }
104
+ };
package/src/misc.ts ADDED
@@ -0,0 +1,242 @@
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 { VideoSamplePixelFormat, MediaCodec } from 'mediabunny';
10
+ import * as NodeAv from 'node-av';
11
+
12
+ export const CODEC_TO_CODEC_ID: Partial<Record<MediaCodec, NodeAv.AVCodecID>> = {
13
+ avc: NodeAv.AV_CODEC_ID_H264,
14
+ hevc: NodeAv.AV_CODEC_ID_HEVC,
15
+ vp8: NodeAv.AV_CODEC_ID_VP8,
16
+ vp9: NodeAv.AV_CODEC_ID_VP9,
17
+ av1: NodeAv.AV_CODEC_ID_AV1,
18
+
19
+ aac: NodeAv.AV_CODEC_ID_AAC,
20
+ opus: NodeAv.AV_CODEC_ID_OPUS,
21
+ mp3: NodeAv.AV_CODEC_ID_MP3,
22
+ vorbis: NodeAv.AV_CODEC_ID_VORBIS,
23
+ flac: NodeAv.AV_CODEC_ID_FLAC,
24
+ ac3: NodeAv.AV_CODEC_ID_AC3,
25
+ eac3: NodeAv.AV_CODEC_ID_EAC3,
26
+ };
27
+
28
+ let cachedHardwareContext: NodeAv.HardwareContext | null | undefined = undefined;
29
+ export const getHardwareContext = (): NodeAv.HardwareContext | null => {
30
+ if (cachedHardwareContext === undefined) {
31
+ cachedHardwareContext = NodeAv.HardwareContext.auto();
32
+ }
33
+ return cachedHardwareContext;
34
+ };
35
+
36
+ const hardwareDecoderCodecCache = new Map<NodeAv.AVCodecID, NodeAv.Codec | null>();
37
+ export const getHardwareDecoderCodec = (codecId: NodeAv.AVCodecID): NodeAv.Codec | null => {
38
+ if (!hardwareDecoderCodecCache.has(codecId)) {
39
+ const hw = getHardwareContext();
40
+ hardwareDecoderCodecCache.set(codecId, hw?.getDecoderCodec(codecId) ?? null);
41
+ }
42
+ return hardwareDecoderCodecCache.get(codecId)!;
43
+ };
44
+
45
+ const hardwareEncoderCodecCache = new Map<NodeAv.AVCodecID, NodeAv.Codec | null>();
46
+ export const getHardwareEncoderCodec = (codecId: NodeAv.AVCodecID): NodeAv.Codec | null => {
47
+ if (!hardwareEncoderCodecCache.has(codecId)) {
48
+ const hw = getHardwareContext();
49
+ hardwareEncoderCodecCache.set(codecId, hw?.getEncoderCodec(codecId) ?? null);
50
+ }
51
+ return hardwareEncoderCodecCache.get(codecId)!;
52
+ };
53
+
54
+ export const mapColorPrimaries = (primaries: string) => {
55
+ switch (primaries) {
56
+ case 'bt709': return NodeAv.AVCOL_PRI_BT709;
57
+ case 'bt470bg': return NodeAv.AVCOL_PRI_BT470BG;
58
+ case 'smpte170m': return NodeAv.AVCOL_PRI_SMPTE170M;
59
+ case 'bt2020': return NodeAv.AVCOL_PRI_BT2020;
60
+ case 'smpte432': return NodeAv.AVCOL_PRI_SMPTE432;
61
+ }
62
+
63
+ return null;
64
+ };
65
+
66
+ export const unmapColorPrimaries = (primaries: number) => {
67
+ switch (primaries) {
68
+ case NodeAv.AVCOL_PRI_BT709: return 'bt709';
69
+ case NodeAv.AVCOL_PRI_BT470BG: return 'bt470bg';
70
+ case NodeAv.AVCOL_PRI_SMPTE170M: return 'smpte170m';
71
+ case NodeAv.AVCOL_PRI_BT2020: return 'bt2020';
72
+ case NodeAv.AVCOL_PRI_SMPTE432: return 'smpte432';
73
+ }
74
+
75
+ return null;
76
+ };
77
+
78
+ export const mapTransferCharacteristics = (transfer: string) => {
79
+ switch (transfer) {
80
+ case 'bt709': return NodeAv.AVCOL_TRC_BT709;
81
+ case 'smpte170m': return NodeAv.AVCOL_TRC_SMPTE170M;
82
+ case 'iec61966-2-1': return NodeAv.AVCOL_TRC_IEC61966_2_1;
83
+ case 'linear': return NodeAv.AVCOL_TRC_LINEAR;
84
+ case 'pq': return NodeAv.AVCOL_TRC_SMPTE2084;
85
+ case 'hlg': return NodeAv.AVCOL_TRC_ARIB_STD_B67;
86
+ }
87
+
88
+ return null;
89
+ };
90
+
91
+ export const unmapTransferCharacteristics = (transfer: number) => {
92
+ switch (transfer) {
93
+ case NodeAv.AVCOL_TRC_BT709: return 'bt709';
94
+ case NodeAv.AVCOL_TRC_SMPTE170M: return 'smpte170m';
95
+ case NodeAv.AVCOL_TRC_IEC61966_2_1: return 'iec61966-2-1';
96
+ case NodeAv.AVCOL_TRC_LINEAR: return 'linear';
97
+ case NodeAv.AVCOL_TRC_SMPTE2084: return 'pq';
98
+ case NodeAv.AVCOL_TRC_ARIB_STD_B67: return 'hlg';
99
+ }
100
+
101
+ return null;
102
+ };
103
+
104
+ export const mapMatrixCoefficients = (matrix: string) => {
105
+ switch (matrix) {
106
+ case 'rgb': return NodeAv.AVCOL_SPC_RGB;
107
+ case 'bt709': return NodeAv.AVCOL_SPC_BT709;
108
+ case 'bt470bg': return NodeAv.AVCOL_SPC_BT470BG;
109
+ case 'smpte170m': return NodeAv.AVCOL_SPC_SMPTE170M;
110
+ case 'bt2020-ncl': return NodeAv.AVCOL_SPC_BT2020_NCL;
111
+ }
112
+
113
+ return null;
114
+ };
115
+
116
+ export const unmapMatrixCoefficients = (matrix: number) => {
117
+ switch (matrix) {
118
+ case NodeAv.AVCOL_SPC_RGB: return 'rgb';
119
+ case NodeAv.AVCOL_SPC_BT709: return 'bt709';
120
+ case NodeAv.AVCOL_SPC_BT470BG: return 'bt470bg';
121
+ case NodeAv.AVCOL_SPC_SMPTE170M: return 'smpte170m';
122
+ case NodeAv.AVCOL_SPC_BT2020_NCL: return 'bt2020-ncl';
123
+ }
124
+
125
+ return null;
126
+ };
127
+
128
+ export const toPixelFormat = (ffmpegPixelFormat: NodeAv.AVPixelFormat): VideoSamplePixelFormat | null => {
129
+ switch (ffmpegPixelFormat) {
130
+ case NodeAv.AV_PIX_FMT_YUV420P: return 'I420';
131
+ // "deprecated in favor of AV_PIX_FMT_YUV420P and setting color_range"
132
+ case NodeAv.AV_PIX_FMT_YUVJ420P: return 'I420';
133
+ case NodeAv.AV_PIX_FMT_YUV420P10LE: return 'I420P10';
134
+ case NodeAv.AV_PIX_FMT_YUV420P12LE: return 'I420P12';
135
+ case NodeAv.AV_PIX_FMT_YUVA420P: return 'I420A';
136
+ case NodeAv.AV_PIX_FMT_YUVA420P10LE: return 'I420AP10';
137
+
138
+ case NodeAv.AV_PIX_FMT_YUV422P: return 'I422';
139
+ // "deprecated in favor of AV_PIX_FMT_YUV422P and setting color_range"
140
+ case NodeAv.AV_PIX_FMT_YUVJ422P: return 'I422';
141
+ case NodeAv.AV_PIX_FMT_YUV422P10LE: return 'I422P10';
142
+ case NodeAv.AV_PIX_FMT_YUV422P12LE: return 'I422P12';
143
+ case NodeAv.AV_PIX_FMT_YUVA422P: return 'I422A';
144
+ case NodeAv.AV_PIX_FMT_YUVA422P10LE: return 'I422AP10';
145
+ case NodeAv.AV_PIX_FMT_YUVA422P12LE: return 'I422AP12';
146
+
147
+ case NodeAv.AV_PIX_FMT_YUV444P: return 'I444';
148
+ // "deprecated in favor of AV_PIX_FMT_YUV444P and setting color_range"
149
+ case NodeAv.AV_PIX_FMT_YUVJ444P: return 'I444';
150
+ case NodeAv.AV_PIX_FMT_YUV444P10LE: return 'I444P10';
151
+ case NodeAv.AV_PIX_FMT_YUV444P12LE: return 'I444P12';
152
+ case NodeAv.AV_PIX_FMT_YUVA444P: return 'I444A';
153
+ case NodeAv.AV_PIX_FMT_YUVA444P10LE: return 'I444AP10';
154
+ case NodeAv.AV_PIX_FMT_YUVA444P12LE: return 'I444AP12';
155
+
156
+ case NodeAv.AV_PIX_FMT_NV12: return 'NV12';
157
+
158
+ case NodeAv.AV_PIX_FMT_RGBA: return 'RGBA';
159
+ case NodeAv.AV_PIX_FMT_RGB0: return 'RGBX';
160
+ case NodeAv.AV_PIX_FMT_BGRA: return 'BGRA';
161
+ case NodeAv.AV_PIX_FMT_BGR0: return 'BGRX';
162
+
163
+ default: return null;
164
+ }
165
+ };
166
+
167
+ export const fromPixelFormat = (pixelFormat: VideoSamplePixelFormat) => {
168
+ switch (pixelFormat) {
169
+ case 'I420': return NodeAv.AV_PIX_FMT_YUV420P;
170
+ case 'I420P10': return NodeAv.AV_PIX_FMT_YUV420P10LE;
171
+ case 'I420P12': return NodeAv.AV_PIX_FMT_YUV420P12LE;
172
+ case 'I420A': return NodeAv.AV_PIX_FMT_YUVA420P;
173
+ case 'I420AP10': return NodeAv.AV_PIX_FMT_YUVA420P10LE;
174
+
175
+ case 'I422': return NodeAv.AV_PIX_FMT_YUV422P;
176
+ case 'I422P10': return NodeAv.AV_PIX_FMT_YUV422P10LE;
177
+ case 'I422P12': return NodeAv.AV_PIX_FMT_YUV422P12LE;
178
+ case 'I422A': return NodeAv.AV_PIX_FMT_YUVA422P;
179
+ case 'I422AP10': return NodeAv.AV_PIX_FMT_YUVA422P10LE;
180
+ case 'I422AP12': return NodeAv.AV_PIX_FMT_YUVA422P12LE;
181
+
182
+ case 'I444': return NodeAv.AV_PIX_FMT_YUV444P;
183
+ case 'I444P10': return NodeAv.AV_PIX_FMT_YUV444P10LE;
184
+ case 'I444P12': return NodeAv.AV_PIX_FMT_YUV444P12LE;
185
+ case 'I444A': return NodeAv.AV_PIX_FMT_YUVA444P;
186
+ case 'I444AP10': return NodeAv.AV_PIX_FMT_YUVA444P10LE;
187
+ case 'I444AP12': return NodeAv.AV_PIX_FMT_YUVA444P12LE;
188
+
189
+ case 'NV12': return NodeAv.AV_PIX_FMT_NV12;
190
+
191
+ case 'RGBA': return NodeAv.AV_PIX_FMT_RGBA;
192
+ case 'RGBX': return NodeAv.AV_PIX_FMT_RGB0;
193
+ case 'BGRA': return NodeAv.AV_PIX_FMT_BGRA;
194
+ case 'BGRX': return NodeAv.AV_PIX_FMT_BGR0;
195
+
196
+ default: return NodeAv.AV_PIX_FMT_NONE;
197
+ }
198
+ };
199
+
200
+ export const toAudioSampleFormat = (ffmpegSampleFormat: NodeAv.AVSampleFormat): AudioSampleFormat | null => {
201
+ switch (ffmpegSampleFormat) {
202
+ case NodeAv.AV_SAMPLE_FMT_U8: return 'u8';
203
+ case NodeAv.AV_SAMPLE_FMT_S16: return 's16';
204
+ case NodeAv.AV_SAMPLE_FMT_S32: return 's32';
205
+ case NodeAv.AV_SAMPLE_FMT_FLT: return 'f32';
206
+ case NodeAv.AV_SAMPLE_FMT_U8P: return 'u8-planar';
207
+ case NodeAv.AV_SAMPLE_FMT_S16P: return 's16-planar';
208
+ case NodeAv.AV_SAMPLE_FMT_S32P: return 's32-planar';
209
+ case NodeAv.AV_SAMPLE_FMT_FLTP: return 'f32-planar';
210
+
211
+ default: return null;
212
+ }
213
+ };
214
+
215
+ export const fromAudioSampleFormat = (sampleFormat: AudioSampleFormat): NodeAv.AVSampleFormat => {
216
+ switch (sampleFormat) {
217
+ case 'u8': return NodeAv.AV_SAMPLE_FMT_U8;
218
+ case 's16': return NodeAv.AV_SAMPLE_FMT_S16;
219
+ case 's32': return NodeAv.AV_SAMPLE_FMT_S32;
220
+ case 'f32': return NodeAv.AV_SAMPLE_FMT_FLT;
221
+ case 'u8-planar': return NodeAv.AV_SAMPLE_FMT_U8P;
222
+ case 's16-planar': return NodeAv.AV_SAMPLE_FMT_S16P;
223
+ case 's32-planar': return NodeAv.AV_SAMPLE_FMT_S32P;
224
+ case 'f32-planar': return NodeAv.AV_SAMPLE_FMT_FLTP;
225
+
226
+ default: return NodeAv.AV_SAMPLE_FMT_NONE;
227
+ }
228
+ };
229
+
230
+ export const getChannelLayout = (numChannels: number): NodeAv.ChannelLayout => {
231
+ switch (numChannels) {
232
+ case 1: return NodeAv.AV_CHANNEL_LAYOUT_MONO;
233
+ case 2: return NodeAv.AV_CHANNEL_LAYOUT_STEREO;
234
+ case 4: return NodeAv.AV_CHANNEL_LAYOUT_QUAD;
235
+ case 6: return NodeAv.AV_CHANNEL_LAYOUT_5POINT1_BACK;
236
+ case 8: return NodeAv.AV_CHANNEL_LAYOUT_7POINT1;
237
+ default: return { nbChannels: numChannels, order: NodeAv.AV_CHANNEL_ORDER_UNSPEC, mask: 0n };
238
+ }
239
+ };
240
+
241
+ // The value is incorrect in the node-av source code, so we do:
242
+ export const LIBVPX_VP9 = 'libvpx-vp9' as (NodeAv.FFEncoderCodec & NodeAv.FFDecoderCodec);
@@ -0,0 +1,224 @@
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 { CustomVideoDecoder, VideoCodec, EncodedPacket, VideoSample, type MaybePromise, Rational } from 'mediabunny';
10
+ import * as NodeAv from 'node-av';
11
+ import { CODEC_TO_CODEC_ID, getHardwareDecoderCodec, LIBVPX_VP9 } from './misc';
12
+ import { assert, binarySearchLessOrEqual, simplifyRational, toUint8Array } from '../../../src/misc';
13
+ import { AvFrameVideoSampleResource } from './video-sample';
14
+
15
+ export class NodeAvVideoDecoder extends CustomVideoDecoder {
16
+ frame!: NodeAv.Frame;
17
+ packet!: NodeAv.Packet;
18
+ codecContext: NodeAv.CodecContext | null = null;
19
+ pixelAspectRatio!: Rational;
20
+
21
+ // Bookkeeping to restore the original timing information
22
+ preciseTimings: {
23
+ microsecondTimestamp: number;
24
+ timestamp: number;
25
+ duration: number;
26
+ timestampIsValid: boolean;
27
+ durationIsValid: boolean;
28
+ }[] = [];
29
+
30
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
31
+ static override supports(codec: VideoCodec, config: VideoDecoderConfig): boolean {
32
+ return codec === 'avc' || codec === 'hevc' || codec === 'vp8' || codec === 'vp9' || codec === 'av1';
33
+ }
34
+
35
+ async init() {
36
+ this.frame = new NodeAv.Frame();
37
+ this.frame.alloc();
38
+ this.packet = new NodeAv.Packet();
39
+ this.packet.alloc();
40
+ }
41
+
42
+ async initCodecContext(packet: EncodedPacket) {
43
+ assert(this.codecContext === null);
44
+
45
+ const codecId = CODEC_TO_CODEC_ID[this.codec];
46
+ assert(codecId !== undefined);
47
+
48
+ let codec: NodeAv.Codec | null;
49
+ if (this.codec === 'vp9' && packet.sideData.alpha) {
50
+ codec = NodeAv.Codec.findDecoderByName(LIBVPX_VP9) ?? NodeAv.Codec.findDecoder(codecId);
51
+ } else if (
52
+ // This check used to be "is not prefer-hardware", meaning it would default to using software decode. I
53
+ // didn't leave a comment for that back then so I actually don't know what it was for. I'm sure it was to
54
+ // work around an issue but, I don't know. Not using hardware decode at all by defaults feels wrong to me,
55
+ // so I changed it to what it is right now. If an issue is encountered, I can always change it.
56
+ this.config.hardwareAcceleration === 'prefer-software'
57
+ || this.codec === 'av1' // https://github.com/opencv/opencv/issues/24430
58
+ ) {
59
+ codec = NodeAv.Codec.findDecoder(codecId);
60
+ } else {
61
+ codec = getHardwareDecoderCodec(codecId) ?? NodeAv.Codec.findDecoder(codecId);
62
+ }
63
+
64
+ if (!codec) {
65
+ throw new Error(`Unable to obtain libav codec for '${this.codec}'.`);
66
+ }
67
+
68
+ const codecContext = new NodeAv.CodecContext();
69
+ codecContext.allocContext3(codec);
70
+
71
+ this.pixelAspectRatio = simplifyRational({
72
+ num: (this.config.displayAspectWidth ?? this.config.codedWidth ?? 0) * (this.config.codedHeight ?? 0),
73
+ den: (this.config.displayAspectHeight ?? this.config.codedHeight ?? 0) * (this.config.codedWidth ?? 0) || 1,
74
+ });
75
+
76
+ codecContext.width = this.config.codedWidth ?? 0; // Fucky that the dimensions can be optional, but oh well
77
+ codecContext.height = this.config.codedHeight ?? 0;
78
+ codecContext.codecType = NodeAv.AVMEDIA_TYPE_VIDEO;
79
+ codecContext.codecId = codecId;
80
+ codecContext.extraData = this.config.description
81
+ ? Buffer.from(toUint8Array(this.config.description))
82
+ : null;
83
+ codecContext.sampleAspectRatio = new NodeAv.Rational(this.pixelAspectRatio.num, this.pixelAspectRatio.den);
84
+
85
+ const ret = await codecContext.open2();
86
+ NodeAv.FFmpegError.throwIfError(ret, 'Open codec context');
87
+
88
+ this.codecContext = codecContext;
89
+ }
90
+
91
+ async decode(packet: EncodedPacket) {
92
+ if (this.codecContext === null) {
93
+ await this.initCodecContext(packet);
94
+ assert(this.codecContext);
95
+ }
96
+
97
+ this.packet.isKeyframe = packet.type === 'key';
98
+ this.packet.data = Buffer.from(packet.data);
99
+ this.packet.timeBase = { num: 1, den: 1e6 };
100
+ this.packet.pts = BigInt(packet.microsecondTimestamp);
101
+ this.packet.dts = NodeAv.AV_NOPTS_VALUE;
102
+ this.packet.duration = BigInt(packet.microsecondDuration);
103
+
104
+ if (packet.sideData.alpha) {
105
+ const matroskaBlockAdditional = Buffer.alloc(8 + packet.sideData.alpha.byteLength);
106
+ matroskaBlockAdditional[7] = 1; // BlockAddId
107
+ matroskaBlockAdditional.set(packet.sideData.alpha, 8);
108
+
109
+ this.packet.addSideData(NodeAv.AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL, matroskaBlockAdditional);
110
+ }
111
+
112
+ const preciseTimingIndex = binarySearchLessOrEqual(
113
+ this.preciseTimings,
114
+ packet.microsecondTimestamp,
115
+ x => x.microsecondTimestamp,
116
+ );
117
+ const existingEntry = preciseTimingIndex !== -1
118
+ ? this.preciseTimings[preciseTimingIndex]
119
+ : null;
120
+ if (existingEntry && existingEntry.microsecondTimestamp === packet.microsecondTimestamp) {
121
+ if (existingEntry.timestamp !== packet.timestamp) {
122
+ // Mapping isn't unique, can't use the timestamp
123
+ existingEntry.timestampIsValid = false;
124
+ }
125
+ if (existingEntry.duration !== packet.duration) {
126
+ // Mapping isn't unique, can't use the duration
127
+ existingEntry.durationIsValid = false;
128
+ }
129
+ } else {
130
+ this.preciseTimings.splice(preciseTimingIndex + 1, 0, {
131
+ microsecondTimestamp: packet.microsecondTimestamp,
132
+ timestamp: packet.timestamp,
133
+ duration: packet.duration,
134
+ timestampIsValid: true,
135
+ durationIsValid: true,
136
+ });
137
+
138
+ // Make sure it doesn't grow indefinitely
139
+ if (this.preciseTimings.length > 128) {
140
+ this.preciseTimings.shift();
141
+ }
142
+ }
143
+
144
+ const ret = await this.codecContext.sendPacket(this.packet);
145
+ NodeAv.FFmpegError.throwIfError(ret, 'Send packet');
146
+
147
+ while (true) {
148
+ const receiveRet = await this.codecContext.receiveFrame(this.frame);
149
+ if (receiveRet === NodeAv.AVERROR_EAGAIN || receiveRet === NodeAv.AVERROR_EOF) {
150
+ break;
151
+ }
152
+
153
+ this.receiveFrame(receiveRet);
154
+ }
155
+ }
156
+
157
+ receiveFrame(ret: number) {
158
+ NodeAv.FFmpegError.throwIfError(ret, 'Receive frame');
159
+
160
+ this.frame.sampleAspectRatio = new NodeAv.Rational(this.pixelAspectRatio.num, this.pixelAspectRatio.den);
161
+
162
+ let timestamp = Number(this.frame.pts) / 1e6;
163
+ let duration = Number(this.frame.duration) / 1e6;
164
+
165
+ const preciseTimingIndex = binarySearchLessOrEqual(
166
+ this.preciseTimings,
167
+ Number(this.frame.pts),
168
+ x => x.microsecondTimestamp,
169
+ );
170
+ const entry = preciseTimingIndex !== -1
171
+ ? this.preciseTimings[preciseTimingIndex]
172
+ : null;
173
+
174
+ // If there's a relevant timing entry, refine the frame's timing data to get better accuracy than
175
+ // microseconds
176
+ if (entry && entry.microsecondTimestamp === Number(this.frame.pts)) {
177
+ if (entry.timestampIsValid) {
178
+ timestamp = entry.timestamp;
179
+ }
180
+ if (entry.durationIsValid) {
181
+ duration = entry.duration;
182
+ }
183
+ }
184
+
185
+ const clone = this.frame.clone();
186
+ if (!clone) {
187
+ throw new Error('Frame clone allocation failed.');
188
+ }
189
+
190
+ this.onSample(new VideoSample(new AvFrameVideoSampleResource(clone), {
191
+ timestamp,
192
+ duration,
193
+ }));
194
+ }
195
+
196
+ async flush() {
197
+ if (!this.codecContext) {
198
+ return;
199
+ }
200
+
201
+ // Send null packet to signal flush
202
+ const ret = await this.codecContext.sendPacket(null);
203
+ NodeAv.FFmpegError.throwIfError(ret, 'Flush decoder');
204
+
205
+ // Keep receiving frames until no more are available
206
+ while (true) {
207
+ const receiveRet = await this.codecContext.receiveFrame(this.frame);
208
+ if (receiveRet === NodeAv.AVERROR_EAGAIN || receiveRet === NodeAv.AVERROR_EOF) {
209
+ // No more frames available
210
+ break;
211
+ }
212
+
213
+ this.receiveFrame(receiveRet);
214
+ }
215
+
216
+ this.codecContext.flushBuffers();
217
+ }
218
+
219
+ close(): MaybePromise<void> {
220
+ this.codecContext?.freeContext();
221
+ this.frame.free();
222
+ this.packet.free();
223
+ }
224
+ }