@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.
- package/LICENSE +373 -0
- package/README.md +276 -0
- package/dist/bundles/mediabunny-server.cjs +3361 -0
- package/dist/bundles/mediabunny-server.min.cjs +10 -0
- package/dist/bundles/mediabunny-server.min.mjs +9 -0
- package/dist/bundles/mediabunny-server.mjs +3334 -0
- package/dist/mediabunny-server.d.ts +105 -0
- package/dist/modules/src/audio-decoder.d.ts +21 -0
- package/dist/modules/src/audio-decoder.d.ts.map +1 -0
- package/dist/modules/src/audio-decoder.js +132 -0
- package/dist/modules/src/audio-encoder.d.ts +35 -0
- package/dist/modules/src/audio-encoder.d.ts.map +1 -0
- package/dist/modules/src/audio-encoder.js +329 -0
- package/dist/modules/src/audio-sample.d.ts +38 -0
- package/dist/modules/src/audio-sample.d.ts.map +1 -0
- package/dist/modules/src/audio-sample.js +119 -0
- package/dist/modules/src/index.d.ts +39 -0
- package/dist/modules/src/index.d.ts.map +1 -0
- package/dist/modules/src/index.js +132 -0
- package/dist/modules/src/misc.d.ts +26 -0
- package/dist/modules/src/misc.d.ts.map +1 -0
- package/dist/modules/src/misc.js +255 -0
- package/dist/modules/src/video-decoder.d.ts +30 -0
- package/dist/modules/src/video-decoder.d.ts.map +1 -0
- package/dist/modules/src/video-decoder.js +214 -0
- package/dist/modules/src/video-encoder.d.ts +35 -0
- package/dist/modules/src/video-encoder.d.ts.map +1 -0
- package/dist/modules/src/video-encoder.js +474 -0
- package/dist/modules/src/video-sample.d.ts +45 -0
- package/dist/modules/src/video-sample.d.ts.map +1 -0
- package/dist/modules/src/video-sample.js +276 -0
- package/dist/modules/tsconfig.tsbuildinfo +1 -0
- package/package.json +59 -0
- package/src/audio-decoder.ts +120 -0
- package/src/audio-encoder.ts +396 -0
- package/src/audio-sample.ts +101 -0
- package/src/index.ts +104 -0
- package/src/misc.ts +242 -0
- package/src/video-decoder.ts +224 -0
- package/src/video-encoder.ts +568 -0
- 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
|
+
}
|