@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
|
@@ -65,12 +65,16 @@ class NodeAvVideoEncoder extends mediabunny_1.CustomVideoEncoder {
|
|
|
65
65
|
this.lastBuffer = null;
|
|
66
66
|
this.packetEmitted = false;
|
|
67
67
|
this.lastScalerKey = null;
|
|
68
|
+
this.quantizer = null;
|
|
68
69
|
// Bookkeeping to restore the original timing information
|
|
69
70
|
this.preciseTimings = [];
|
|
70
71
|
}
|
|
71
72
|
static supports(codec, config) {
|
|
73
|
+
if (config.bitrateMode === 'quantizer') {
|
|
74
|
+
return codec === 'avc' || codec === 'hevc' || codec === 'vp9' || codec === 'av1';
|
|
75
|
+
}
|
|
72
76
|
return (codec === 'avc' || codec === 'hevc' || codec === 'vp8' || codec === 'vp9' || codec === 'av1'
|
|
73
|
-
|| codec === 'prores')
|
|
77
|
+
|| codec === 'prores');
|
|
74
78
|
}
|
|
75
79
|
async init() {
|
|
76
80
|
this.frame = new NodeAv.Frame();
|
|
@@ -98,13 +102,22 @@ class NodeAvVideoEncoder extends mediabunny_1.CustomVideoEncoder {
|
|
|
98
102
|
codec = getSoftwareCodec();
|
|
99
103
|
}
|
|
100
104
|
else {
|
|
101
|
-
|
|
105
|
+
let hardwareCodec = await (0, misc_1.getHardwareEncoderCodec)(codecId);
|
|
106
|
+
if (hardwareCodec && this.config.bitrateMode === 'quantizer' && !hardwareCodec.name?.endsWith('_nvenc')) {
|
|
107
|
+
// NVENC is the only hardware encoder we know how to drive in constant-quantizer mode
|
|
108
|
+
hardwareCodec = null;
|
|
109
|
+
}
|
|
110
|
+
codec = hardwareCodec ?? getSoftwareCodec();
|
|
102
111
|
}
|
|
103
112
|
if (!codec) {
|
|
104
113
|
throw new Error(`Unable to obtain libav codec for '${this.codec}'.`);
|
|
105
114
|
}
|
|
106
115
|
this.avCodec = codec;
|
|
107
|
-
|
|
116
|
+
if (this.config.bitrateMode !== 'quantizer') {
|
|
117
|
+
// In quantizer mode, the codec context is instead created lazily on the first encode, once the quantizer
|
|
118
|
+
// value is known
|
|
119
|
+
await this.createCodecContext();
|
|
120
|
+
}
|
|
108
121
|
}
|
|
109
122
|
async createCodecContext() {
|
|
110
123
|
(0, misc_2.assert)(this.codecContext === null);
|
|
@@ -136,7 +149,12 @@ class NodeAvVideoEncoder extends mediabunny_1.CustomVideoEncoder {
|
|
|
136
149
|
codecContext.timeBase = new NodeAv.Rational(1, 1e6);
|
|
137
150
|
codecContext.gopSize = 60;
|
|
138
151
|
codecContext.framerate = new NodeAv.Rational(Math.round(this.config.framerate ?? 0) || 30, 1);
|
|
139
|
-
|
|
152
|
+
// In quantizer mode, the quantizer dictates the rate; a target bitrate would put encoders in the wrong rate
|
|
153
|
+
// control mode
|
|
154
|
+
codecContext.bitRate = this.config.bitrateMode === 'quantizer'
|
|
155
|
+
? 0n
|
|
156
|
+
: BigInt(this.config.bitrate ?? new mediabunny_1.Quality('medium')
|
|
157
|
+
._toVideoBitrate(this.codec, this.config.width, this.config.height));
|
|
140
158
|
codecContext.sampleAspectRatio = new NodeAv.Rational(pixelAspectRatio.num, pixelAspectRatio.den);
|
|
141
159
|
if (this.config.bitrateMode === 'constant') {
|
|
142
160
|
codecContext.rcMinRate = codecContext.bitRate;
|
|
@@ -189,6 +207,43 @@ class NodeAvVideoEncoder extends mediabunny_1.CustomVideoEncoder {
|
|
|
189
207
|
else if (this.avCodec.name === 'hevc_nvenc') {
|
|
190
208
|
codecContext.setOption('forced-idr', '1');
|
|
191
209
|
}
|
|
210
|
+
if (this.config.bitrateMode === 'quantizer') {
|
|
211
|
+
(0, misc_2.assert)(this.quantizer !== null);
|
|
212
|
+
// Map the quantizer from the scale used by Mediabunny to the scale expected by the specific FFmpeg encoder
|
|
213
|
+
let mapped;
|
|
214
|
+
if (this.avCodec.name === 'libaom-av1' || this.avCodec.name === 'libsvtav1') {
|
|
215
|
+
// Mediabunny uses AV1's quantizer index (0-255), while these encoders expect the 0-63 quantizer scale
|
|
216
|
+
mapped = Math.round(this.quantizer / 4);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// Everything else lines up with Mediabunny directly
|
|
220
|
+
mapped = this.quantizer;
|
|
221
|
+
}
|
|
222
|
+
// Put the encoder into its constant-quantizer mode
|
|
223
|
+
if (this.avCodec.name === 'libx264' || this.avCodec.name === 'libx265') {
|
|
224
|
+
codecContext.setOption('qp', String(mapped));
|
|
225
|
+
}
|
|
226
|
+
else if (this.avCodec.name === 'libvpx-vp9' || this.avCodec.name === 'libaom-av1') {
|
|
227
|
+
codecContext.setOption('crf', String(mapped));
|
|
228
|
+
}
|
|
229
|
+
else if (this.avCodec.name === 'libsvtav1') {
|
|
230
|
+
// qp (unlike crf) selects true constant-quantizer mode
|
|
231
|
+
codecContext.setOption('qp', String(mapped));
|
|
232
|
+
}
|
|
233
|
+
else if (this.avCodec.name === 'librav1e') {
|
|
234
|
+
codecContext.setOption('qp', String(mapped));
|
|
235
|
+
}
|
|
236
|
+
else if (this.avCodec.name?.endsWith('_nvenc')) {
|
|
237
|
+
codecContext.setOption('rc', 'constqp');
|
|
238
|
+
codecContext.setOption('qp', String(mapped));
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
throw new Error(`Encoder '${this.avCodec.name}' cannot be used for quantizer-based encoding.`);
|
|
242
|
+
}
|
|
243
|
+
// Also pin the quantizer range so crf-based encoders (libvpx, libaom) hold the quantizer truly constant
|
|
244
|
+
codecContext.qMin = mapped;
|
|
245
|
+
codecContext.qMax = mapped;
|
|
246
|
+
}
|
|
192
247
|
if (this.codec === 'prores') {
|
|
193
248
|
// Pick the encoder profile from the requested ProRes four-character code
|
|
194
249
|
const profile = PRORES_FOURCC_TO_PROFILE[this.config.codec];
|
|
@@ -200,6 +255,38 @@ class NodeAvVideoEncoder extends mediabunny_1.CustomVideoEncoder {
|
|
|
200
255
|
this.codecContext = codecContext;
|
|
201
256
|
}
|
|
202
257
|
async encode(videoSample, options) {
|
|
258
|
+
if (this.config.bitrateMode === 'quantizer') {
|
|
259
|
+
let quantizer;
|
|
260
|
+
if (this.codec === 'avc') {
|
|
261
|
+
quantizer = options.avc?.quantizer;
|
|
262
|
+
}
|
|
263
|
+
else if (this.codec === 'hevc') {
|
|
264
|
+
quantizer = options.hevc?.quantizer;
|
|
265
|
+
}
|
|
266
|
+
else if (this.codec === 'vp9') {
|
|
267
|
+
quantizer = options.vp9?.quantizer;
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
quantizer = options.av1?.quantizer;
|
|
271
|
+
}
|
|
272
|
+
(0, misc_2.assert)(quantizer !== undefined && quantizer !== null);
|
|
273
|
+
if (this.codecContext !== null && quantizer !== this.quantizer) {
|
|
274
|
+
// Almost no FFmpeg encoder supports changing the quantizer past init, so drain the current encoder
|
|
275
|
+
// and start a fresh one. Dirty but what else are you gonna do
|
|
276
|
+
const ret = await this.codecContext.sendFrame(null);
|
|
277
|
+
NodeAv.FFmpegError.throwIfError(ret, 'Send frame');
|
|
278
|
+
while (true) {
|
|
279
|
+
const receiveRet = await this.codecContext.receivePacket(this.packet);
|
|
280
|
+
if (receiveRet === NodeAv.AVERROR_EAGAIN || receiveRet === NodeAv.AVERROR_EOF) {
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
this.receivePacket(receiveRet);
|
|
284
|
+
}
|
|
285
|
+
this.codecContext.freeContext();
|
|
286
|
+
this.codecContext = null;
|
|
287
|
+
}
|
|
288
|
+
this.quantizer = quantizer;
|
|
289
|
+
}
|
|
203
290
|
if (this.codecContext === null) {
|
|
204
291
|
await this.createCodecContext();
|
|
205
292
|
}
|