@mediabunny/server 1.51.0 → 1.52.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 +2 -2
- package/dist/bundles/mediabunny-server.cjs +70 -6
- package/dist/bundles/mediabunny-server.min.cjs +2 -2
- package/dist/bundles/mediabunny-server.min.mjs +2 -2
- package/dist/bundles/mediabunny-server.mjs +72 -8
- package/dist/modules/src/audio-encoder.d.ts.map +1 -1
- package/dist/modules/src/audio-encoder.js +1 -1
- package/dist/modules/src/video-encoder.d.ts +1 -0
- package/dist/modules/src/video-encoder.d.ts.map +1 -1
- package/dist/modules/src/video-encoder.js +91 -4
- package/dist/modules/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/audio-encoder.ts +4 -2
- package/src/video-encoder.ts +97 -7
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mediabunny/server",
|
|
3
3
|
"author": "Vanilagy",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.52.0",
|
|
5
5
|
"description": "Adds full video and audio decoder and encoder support to Mediabunny for use in server-side environments (Node, Bun, Deno). Based on NodeAV.",
|
|
6
6
|
"main": "./dist/bundles/mediabunny-server.cjs",
|
|
7
7
|
"module": "./dist/bundles/mediabunny-server.mjs",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"node-av": "^6.0.0",
|
|
38
|
-
"@mediabunny/prores": "^1.
|
|
38
|
+
"@mediabunny/prores": "^1.52.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"mediabunny": "^1.45.0"
|
package/src/audio-encoder.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
AudioSample,
|
|
12
12
|
CustomAudioEncoder,
|
|
13
13
|
type MaybePromise,
|
|
14
|
-
|
|
14
|
+
Quality,
|
|
15
15
|
EncodedPacket,
|
|
16
16
|
} from 'mediabunny';
|
|
17
17
|
import * as NodeAv from 'node-av';
|
|
@@ -112,7 +112,9 @@ export class NodeAvAudioEncoder extends CustomAudioEncoder {
|
|
|
112
112
|
codecContext.codecId = CODEC_TO_CODEC_ID[this.codec]!;
|
|
113
113
|
codecContext.sampleFormat = sampleFormat;
|
|
114
114
|
codecContext.timeBase = new NodeAv.Rational(1, this.config.sampleRate);
|
|
115
|
-
codecContext.bitRate = BigInt(
|
|
115
|
+
codecContext.bitRate = BigInt(
|
|
116
|
+
this.config.bitrate ?? new Quality('medium')._toAudioBitrate(this.codec) ?? 0,
|
|
117
|
+
);
|
|
116
118
|
|
|
117
119
|
if (this.config.bitrateMode === 'constant') {
|
|
118
120
|
codecContext.rcMinRate = codecContext.bitRate;
|
package/src/video-encoder.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import {
|
|
10
10
|
CustomVideoEncoder,
|
|
11
11
|
type MaybePromise,
|
|
12
|
-
|
|
12
|
+
Quality,
|
|
13
13
|
VideoCodec,
|
|
14
14
|
VideoSample,
|
|
15
15
|
EncodedPacket,
|
|
@@ -60,6 +60,7 @@ export class NodeAvVideoEncoder extends CustomVideoEncoder {
|
|
|
60
60
|
lastBuffer: Buffer | null = null;
|
|
61
61
|
packetEmitted = false;
|
|
62
62
|
lastScalerKey: string | null = null;
|
|
63
|
+
quantizer: number | null = null;
|
|
63
64
|
|
|
64
65
|
// Bookkeeping to restore the original timing information
|
|
65
66
|
preciseTimings: {
|
|
@@ -71,10 +72,14 @@ export class NodeAvVideoEncoder extends CustomVideoEncoder {
|
|
|
71
72
|
}[] = [];
|
|
72
73
|
|
|
73
74
|
static override supports(codec: VideoCodec, config: VideoEncoderConfig): boolean {
|
|
75
|
+
if (config.bitrateMode === 'quantizer') {
|
|
76
|
+
return codec === 'avc' || codec === 'hevc' || codec === 'vp9' || codec === 'av1';
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
return (
|
|
75
80
|
codec === 'avc' || codec === 'hevc' || codec === 'vp8' || codec === 'vp9' || codec === 'av1'
|
|
76
81
|
|| codec === 'prores'
|
|
77
|
-
)
|
|
82
|
+
);
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
async init(): Promise<void> {
|
|
@@ -106,7 +111,13 @@ export class NodeAvVideoEncoder extends CustomVideoEncoder {
|
|
|
106
111
|
} else if (this.config.hardwareAcceleration === 'prefer-software') {
|
|
107
112
|
codec = getSoftwareCodec();
|
|
108
113
|
} else {
|
|
109
|
-
|
|
114
|
+
let hardwareCodec = await getHardwareEncoderCodec(codecId);
|
|
115
|
+
if (hardwareCodec && this.config.bitrateMode === 'quantizer' && !hardwareCodec.name?.endsWith('_nvenc')) {
|
|
116
|
+
// NVENC is the only hardware encoder we know how to drive in constant-quantizer mode
|
|
117
|
+
hardwareCodec = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
codec = hardwareCodec ?? getSoftwareCodec();
|
|
110
121
|
}
|
|
111
122
|
|
|
112
123
|
if (!codec) {
|
|
@@ -115,7 +126,11 @@ export class NodeAvVideoEncoder extends CustomVideoEncoder {
|
|
|
115
126
|
|
|
116
127
|
this.avCodec = codec;
|
|
117
128
|
|
|
118
|
-
|
|
129
|
+
if (this.config.bitrateMode !== 'quantizer') {
|
|
130
|
+
// In quantizer mode, the codec context is instead created lazily on the first encode, once the quantizer
|
|
131
|
+
// value is known
|
|
132
|
+
await this.createCodecContext();
|
|
133
|
+
}
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
async createCodecContext() {
|
|
@@ -156,9 +171,14 @@ export class NodeAvVideoEncoder extends CustomVideoEncoder {
|
|
|
156
171
|
codecContext.timeBase = new NodeAv.Rational(1, 1e6);
|
|
157
172
|
codecContext.gopSize = 60;
|
|
158
173
|
codecContext.framerate = new NodeAv.Rational(Math.round(this.config.framerate ?? 0) || 30, 1);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
174
|
+
// In quantizer mode, the quantizer dictates the rate; a target bitrate would put encoders in the wrong rate
|
|
175
|
+
// control mode
|
|
176
|
+
codecContext.bitRate = this.config.bitrateMode === 'quantizer'
|
|
177
|
+
? 0n
|
|
178
|
+
: BigInt(
|
|
179
|
+
this.config.bitrate ?? new Quality('medium')
|
|
180
|
+
._toVideoBitrate(this.codec, this.config.width, this.config.height),
|
|
181
|
+
);
|
|
162
182
|
codecContext.sampleAspectRatio = new NodeAv.Rational(pixelAspectRatio.num, pixelAspectRatio.den);
|
|
163
183
|
|
|
164
184
|
if (this.config.bitrateMode === 'constant') {
|
|
@@ -210,6 +230,41 @@ export class NodeAvVideoEncoder extends CustomVideoEncoder {
|
|
|
210
230
|
codecContext.setOption('forced-idr', '1');
|
|
211
231
|
}
|
|
212
232
|
|
|
233
|
+
if (this.config.bitrateMode === 'quantizer') {
|
|
234
|
+
assert(this.quantizer !== null);
|
|
235
|
+
|
|
236
|
+
// Map the quantizer from the scale used by Mediabunny to the scale expected by the specific FFmpeg encoder
|
|
237
|
+
let mapped: number;
|
|
238
|
+
if (this.avCodec.name === 'libaom-av1' || this.avCodec.name === 'libsvtav1') {
|
|
239
|
+
// Mediabunny uses AV1's quantizer index (0-255), while these encoders expect the 0-63 quantizer scale
|
|
240
|
+
mapped = Math.round(this.quantizer / 4);
|
|
241
|
+
} else {
|
|
242
|
+
// Everything else lines up with Mediabunny directly
|
|
243
|
+
mapped = this.quantizer;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Put the encoder into its constant-quantizer mode
|
|
247
|
+
if (this.avCodec.name === 'libx264' || this.avCodec.name === 'libx265') {
|
|
248
|
+
codecContext.setOption('qp', String(mapped));
|
|
249
|
+
} else if (this.avCodec.name === 'libvpx-vp9' || this.avCodec.name === 'libaom-av1') {
|
|
250
|
+
codecContext.setOption('crf', String(mapped));
|
|
251
|
+
} else if (this.avCodec.name === 'libsvtav1') {
|
|
252
|
+
// qp (unlike crf) selects true constant-quantizer mode
|
|
253
|
+
codecContext.setOption('qp', String(mapped));
|
|
254
|
+
} else if (this.avCodec.name === 'librav1e') {
|
|
255
|
+
codecContext.setOption('qp', String(mapped));
|
|
256
|
+
} else if (this.avCodec.name?.endsWith('_nvenc')) {
|
|
257
|
+
codecContext.setOption('rc', 'constqp');
|
|
258
|
+
codecContext.setOption('qp', String(mapped));
|
|
259
|
+
} else {
|
|
260
|
+
throw new Error(`Encoder '${this.avCodec.name}' cannot be used for quantizer-based encoding.`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Also pin the quantizer range so crf-based encoders (libvpx, libaom) hold the quantizer truly constant
|
|
264
|
+
codecContext.qMin = mapped;
|
|
265
|
+
codecContext.qMax = mapped;
|
|
266
|
+
}
|
|
267
|
+
|
|
213
268
|
if (this.codec === 'prores') {
|
|
214
269
|
// Pick the encoder profile from the requested ProRes four-character code
|
|
215
270
|
const profile = PRORES_FOURCC_TO_PROFILE[this.config.codec as ProresFourCc];
|
|
@@ -225,6 +280,41 @@ export class NodeAvVideoEncoder extends CustomVideoEncoder {
|
|
|
225
280
|
}
|
|
226
281
|
|
|
227
282
|
async encode(videoSample: VideoSample, options: VideoEncoderEncodeOptions): Promise<void> {
|
|
283
|
+
if (this.config.bitrateMode === 'quantizer') {
|
|
284
|
+
let quantizer: number | null | undefined;
|
|
285
|
+
if (this.codec === 'avc') {
|
|
286
|
+
quantizer = options.avc?.quantizer;
|
|
287
|
+
} else if (this.codec === 'hevc') {
|
|
288
|
+
quantizer = options.hevc?.quantizer;
|
|
289
|
+
} else if (this.codec === 'vp9') {
|
|
290
|
+
quantizer = options.vp9?.quantizer;
|
|
291
|
+
} else {
|
|
292
|
+
quantizer = options.av1?.quantizer;
|
|
293
|
+
}
|
|
294
|
+
assert(quantizer !== undefined && quantizer !== null);
|
|
295
|
+
|
|
296
|
+
if (this.codecContext !== null && quantizer !== this.quantizer) {
|
|
297
|
+
// Almost no FFmpeg encoder supports changing the quantizer past init, so drain the current encoder
|
|
298
|
+
// and start a fresh one. Dirty but what else are you gonna do
|
|
299
|
+
const ret = await this.codecContext.sendFrame(null);
|
|
300
|
+
NodeAv.FFmpegError.throwIfError(ret, 'Send frame');
|
|
301
|
+
|
|
302
|
+
while (true) {
|
|
303
|
+
const receiveRet = await this.codecContext.receivePacket(this.packet);
|
|
304
|
+
if (receiveRet === NodeAv.AVERROR_EAGAIN || receiveRet === NodeAv.AVERROR_EOF) {
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.receivePacket(receiveRet);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
this.codecContext.freeContext();
|
|
312
|
+
this.codecContext = null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this.quantizer = quantizer;
|
|
316
|
+
}
|
|
317
|
+
|
|
228
318
|
if (this.codecContext === null) {
|
|
229
319
|
await this.createCodecContext();
|
|
230
320
|
}
|