@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.
@@ -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') && config.bitrateMode !== 'quantizer';
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
- codec = (await (0, misc_1.getHardwareEncoderCodec)(codecId)) ?? getSoftwareCodec();
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
- await this.createCodecContext();
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
- codecContext.bitRate = BigInt(this.config.bitrate ?? mediabunny_1.QUALITY_MEDIUM._toVideoBitrate(this.codec, this.config.width, this.config.height));
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
  }