@mediabunny/server 1.50.9 → 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mediabunny/server",
3
3
  "author": "Vanilagy",
4
- "version": "1.50.9",
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.50.9"
38
+ "@mediabunny/prores": "^1.52.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "mediabunny": "^1.45.0"
@@ -11,7 +11,7 @@ import {
11
11
  AudioSample,
12
12
  CustomAudioEncoder,
13
13
  type MaybePromise,
14
- QUALITY_MEDIUM,
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(this.config.bitrate ?? QUALITY_MEDIUM._toAudioBitrate(this.codec) ?? 0);
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;
@@ -9,7 +9,7 @@
9
9
  import {
10
10
  CustomVideoEncoder,
11
11
  type MaybePromise,
12
- QUALITY_MEDIUM,
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
- ) && config.bitrateMode !== 'quantizer';
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
- codec = (await getHardwareEncoderCodec(codecId)) ?? getSoftwareCodec();
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
- await this.createCodecContext();
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
- codecContext.bitRate = BigInt(
160
- this.config.bitrate ?? QUALITY_MEDIUM._toVideoBitrate(this.codec, this.config.width, this.config.height),
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
  }