@mediabunny/server 1.45.4 → 1.46.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.
Files changed (37) hide show
  1. package/README.md +16 -0
  2. package/dist/bundles/mediabunny-server.cjs +67 -12
  3. package/dist/bundles/mediabunny-server.min.cjs +2 -2
  4. package/dist/bundles/mediabunny-server.min.mjs +2 -2
  5. package/dist/bundles/mediabunny-server.mjs +74 -13
  6. package/dist/mediabunny-server.d.ts +20 -1
  7. package/dist/modules/src/audio-decoder.d.ts +1 -1
  8. package/dist/modules/src/audio-decoder.d.ts.map +1 -1
  9. package/dist/modules/src/audio-decoder.js +8 -1
  10. package/dist/modules/src/audio-encoder.d.ts +3 -3
  11. package/dist/modules/src/audio-encoder.d.ts.map +1 -1
  12. package/dist/modules/src/audio-encoder.js +6 -2
  13. package/dist/modules/src/audio-sample.d.ts.map +1 -1
  14. package/dist/modules/src/audio-sample.js +3 -0
  15. package/dist/modules/src/index.d.ts +21 -2
  16. package/dist/modules/src/index.d.ts.map +1 -1
  17. package/dist/modules/src/index.js +24 -2
  18. package/dist/modules/src/misc.d.ts +2 -2
  19. package/dist/modules/src/misc.d.ts.map +1 -1
  20. package/dist/modules/src/misc.js +23 -2
  21. package/dist/modules/src/video-decoder.d.ts.map +1 -1
  22. package/dist/modules/src/video-decoder.js +3 -2
  23. package/dist/modules/src/video-encoder.d.ts +3 -3
  24. package/dist/modules/src/video-encoder.d.ts.map +1 -1
  25. package/dist/modules/src/video-encoder.js +8 -4
  26. package/dist/modules/src/video-sample.d.ts.map +1 -1
  27. package/dist/modules/src/video-sample.js +3 -0
  28. package/dist/modules/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +1 -1
  30. package/src/audio-decoder.ts +8 -2
  31. package/src/audio-encoder.ts +7 -3
  32. package/src/audio-sample.ts +3 -0
  33. package/src/index.ts +63 -2
  34. package/src/misc.ts +31 -2
  35. package/src/video-decoder.ts +5 -2
  36. package/src/video-encoder.ts +12 -11
  37. package/src/video-sample.ts +3 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mediabunny/server",
3
3
  "author": "Vanilagy",
4
- "version": "1.45.4",
4
+ "version": "1.46.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",
@@ -15,7 +15,7 @@ import { AvFrameAudioSampleResource } from './audio-sample';
15
15
  export class NodeAvAudioDecoder extends CustomAudioDecoder {
16
16
  frame!: NodeAv.Frame;
17
17
  packet!: NodeAv.Packet;
18
- codecContext!: NodeAv.CodecContext;
18
+ codecContext: NodeAv.CodecContext | null = null;
19
19
 
20
20
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
21
21
  static override supports(codec: AudioCodec, config: AudioDecoderConfig): boolean {
@@ -61,6 +61,8 @@ export class NodeAvAudioDecoder extends CustomAudioDecoder {
61
61
  }
62
62
 
63
63
  async decode(packet: EncodedPacket): Promise<void> {
64
+ assert(this.codecContext);
65
+
64
66
  this.packet.isKeyframe = packet.type === 'key';
65
67
  this.packet.data = Buffer.from(packet.data);
66
68
  this.packet.timeBase = { num: 1, den: this.config.sampleRate };
@@ -71,6 +73,8 @@ export class NodeAvAudioDecoder extends CustomAudioDecoder {
71
73
  const ret = await this.codecContext.sendPacket(this.packet);
72
74
  NodeAv.FFmpegError.throwIfError(ret, 'Send packet');
73
75
 
76
+ this.packet.unref(); // Don't need the data again, so just unref it
77
+
74
78
  while (true) {
75
79
  const receiveRet = await this.codecContext.receiveFrame(this.frame);
76
80
  if (receiveRet === NodeAv.AVERROR_EAGAIN || receiveRet === NodeAv.AVERROR_EOF) {
@@ -94,6 +98,8 @@ export class NodeAvAudioDecoder extends CustomAudioDecoder {
94
98
  }
95
99
 
96
100
  async flush(): Promise<void> {
101
+ assert(this.codecContext);
102
+
97
103
  // Send null packet to signal flush
98
104
  const ret = await this.codecContext.sendPacket(null);
99
105
  NodeAv.FFmpegError.throwIfError(ret, 'Flush decoder');
@@ -113,7 +119,7 @@ export class NodeAvAudioDecoder extends CustomAudioDecoder {
113
119
  }
114
120
 
115
121
  close(): MaybePromise<void> {
116
- this.codecContext.freeContext();
122
+ this.codecContext?.freeContext();
117
123
  this.frame.free();
118
124
  this.packet.free();
119
125
  }
@@ -35,16 +35,16 @@ const FRAME_SIZE_FALLBACK = 1024; // Just 'cause
35
35
  export class NodeAvAudioEncoder extends CustomAudioEncoder {
36
36
  frame!: NodeAv.Frame;
37
37
  packet!: NodeAv.Packet;
38
- avCodec!: NodeAv.Codec;
39
38
  codecContext: NodeAv.CodecContext | null = null;
39
+ resampler: NodeAv.SoftwareResampleContext | null = null;
40
+ dstFrame: NodeAv.Frame | null = null;
41
+ avCodec!: NodeAv.Codec;
40
42
  firstExpectedTimestamp: number | null = null;
41
43
  outputTimestampOffset = 0;
42
44
 
43
- resampler: NodeAv.SoftwareResampleContext | null = null;
44
45
  inputParametersKey: string | null = null;
45
46
  resamplerInputSampleRate: number | null = null;
46
47
  nextResamplerPts: bigint | null = null;
47
- dstFrame: NodeAv.Frame | null = null;
48
48
  packetEmitted = false;
49
49
  adtsHeaderTemplate: AdtsHeaderTemplate | null = null;
50
50
 
@@ -134,6 +134,10 @@ export class NodeAvAudioEncoder extends CustomAudioEncoder {
134
134
  this.firstExpectedTimestamp ??= audioSample.timestamp;
135
135
 
136
136
  if (audioSample._data instanceof AvFrameAudioSampleResource) {
137
+ // Release any buffers still referenced from the previous encode before reffing the new frame, otherwise
138
+ // av_frame_ref leaks them
139
+ // https://github.com/Vanilagy/mediabunny/issues/392
140
+ this.frame.unref();
137
141
  this.frame.ref(audioSample._data.frame);
138
142
  } else {
139
143
  copyAudioSampleToAvFrame(audioSample, this.frame);
@@ -42,6 +42,9 @@ export class AvFrameAudioSampleResource extends AudioSampleResource {
42
42
  constructor(frame: NodeAv.Frame) {
43
43
  super();
44
44
 
45
+ if (!(frame instanceof NodeAv.Frame)) {
46
+ throw new TypeError('frame must be a NodeAv.Frame.');
47
+ }
45
48
  if (frame.getMediaType() !== NodeAv.AVMEDIA_TYPE_AUDIO) {
46
49
  throw new Error('AvFrameAudioSampleResource must be initialized with an audio frame.');
47
50
  }
package/src/index.ts CHANGED
@@ -6,7 +6,14 @@
6
6
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
7
  */
8
8
 
9
- import { AudioSample, registerDecoder, registerEncoder, registerVideoSampleTransformer, VideoSample } from 'mediabunny';
9
+ import {
10
+ AudioSample,
11
+ MaybePromise,
12
+ registerDecoder,
13
+ registerEncoder,
14
+ registerVideoSampleTransformer,
15
+ VideoSample,
16
+ } from 'mediabunny';
10
17
  import * as NodeAv from 'node-av';
11
18
  import { NodeAvVideoDecoder } from './video-decoder';
12
19
  import { NodeAvVideoEncoder } from './video-encoder';
@@ -28,6 +35,29 @@ if ((globalThis as Record<symbol, unknown>)[SERVER_LOADED_SYMBOL]) {
28
35
 
29
36
  let registered = false;
30
37
 
38
+ /**
39
+ * Options for configuring Mediabunny's server-side polyfills.
40
+ * @group \@mediabunny/server
41
+ * @public
42
+ */
43
+ type MediabunnyServerOptions = {
44
+ /**
45
+ * The hardware context to use for hardware-accelerated decoding and encoding. When set to a
46
+ * `NodeAv.HardwareContext`, that context is used directly. When set to a function, the function is invoked (with
47
+ * no caching) every time a hardware decoder or encoder codec is resolved, receiving the codec ID and returning the
48
+ * context to use for it (or `null` for no hardware acceleration).
49
+ *
50
+ * When not set, a context is automatically detected on first use and then cached.
51
+ */
52
+ hardwareContext?:
53
+ | NodeAv.HardwareContext
54
+ | null
55
+ | ((codecId: NodeAv.AVCodecID) => MaybePromise<NodeAv.HardwareContext | null>);
56
+ };
57
+
58
+ /** @internal */
59
+ export let _serverOptions: MediabunnyServerOptions = {};
60
+
31
61
  /**
32
62
  * Registers video and audio decoders and encoders for all codecs, using FFmpeg's libavcodec under the hood.
33
63
  * Additionally, a custom `VideoSample` transformer based on libavfilter is registered to enable resizing, rotation and
@@ -38,14 +68,32 @@ let registered = false;
38
68
  * The decoders and encoders will automatically detect hardware acceleration support for each codec and platform and
39
69
  * make use of it if applicable.
40
70
  *
71
+ * You can pass optional {@link MediabunnyServerOptions} for additional configuration.
72
+ *
41
73
  * @group \@mediabunny/server
42
74
  * @public
43
75
  */
44
- export const registerMediabunnyServer = () => {
76
+ export const registerMediabunnyServer = (options: MediabunnyServerOptions = {}) => {
77
+ if (typeof options !== 'object' || !options) {
78
+ throw new TypeError('options must be an object.');
79
+ }
80
+ if (
81
+ options.hardwareContext != null
82
+ && !(
83
+ options.hardwareContext instanceof NodeAv.HardwareContext || typeof options.hardwareContext === 'function'
84
+ )
85
+ ) {
86
+ throw new TypeError(
87
+ 'options.hardwareContext, when provided, must be a NodeAv.HardwareContext, a function, or null.',
88
+ );
89
+ }
90
+
45
91
  if (registered) {
46
92
  return;
47
93
  }
94
+
48
95
  registered = true;
96
+ _serverOptions = options;
49
97
 
50
98
  NodeAv.Log.setLevel(NodeAv.AV_LOG_ERROR);
51
99
 
@@ -60,6 +108,8 @@ export const registerMediabunnyServer = () => {
60
108
  registerVideoSampleTransformer(transformVideoSample);
61
109
  };
62
110
 
111
+ export type { MediabunnyServerOptions };
112
+
63
113
  export { AvFrameVideoSampleResource } from './video-sample';
64
114
  export { AvFrameAudioSampleResource } from './audio-sample';
65
115
 
@@ -76,8 +126,17 @@ export { AvFrameAudioSampleResource } from './audio-sample';
76
126
  * @public
77
127
  */
78
128
  export const toAvFrame = async (sample: VideoSample | AudioSample, frame: NodeAv.Frame) => {
129
+ if (!(sample instanceof VideoSample) && !(sample instanceof AudioSample)) {
130
+ throw new TypeError('sample must be a VideoSample or an AudioSample.');
131
+ }
132
+ if (!(frame instanceof NodeAv.Frame)) {
133
+ throw new TypeError('frame must be a NodeAv.Frame.');
134
+ }
135
+
79
136
  if (sample instanceof VideoSample) {
80
137
  if (sample._data instanceof AvFrameVideoSampleResource) {
138
+ // We're overriding the frame, so release whatever it referenced before, otherwise av_frame_ref leaks it
139
+ frame.unref();
81
140
  frame.ref(sample._data.frame);
82
141
  } else {
83
142
  if (sample.format === null) {
@@ -92,6 +151,8 @@ export const toAvFrame = async (sample: VideoSample | AudioSample, frame: NodeAv
92
151
  frame.timeBase = new NodeAv.Rational(1, 1e6);
93
152
  } else {
94
153
  if (sample._data instanceof AvFrameAudioSampleResource) {
154
+ // We're overriding the frame, so release whatever it referenced before, otherwise av_frame_ref leaks it
155
+ frame.unref();
95
156
  frame.ref(sample._data.frame);
96
157
  } else {
97
158
  copyAudioSampleToAvFrame(sample, frame);
package/src/misc.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { VideoSamplePixelFormat, MediaCodec } from 'mediabunny';
10
10
  import * as NodeAv from 'node-av';
11
+ import { _serverOptions } from './index';
11
12
 
12
13
  export const CODEC_TO_CODEC_ID: Partial<Record<MediaCodec, NodeAv.AVCodecID>> = {
13
14
  avc: NodeAv.AV_CODEC_ID_H264,
@@ -27,14 +28,35 @@ export const CODEC_TO_CODEC_ID: Partial<Record<MediaCodec, NodeAv.AVCodecID>> =
27
28
 
28
29
  let cachedHardwareContext: NodeAv.HardwareContext | null | undefined = undefined;
29
30
  export const getHardwareContext = (): NodeAv.HardwareContext | null => {
31
+ if (_serverOptions.hardwareContext !== undefined && typeof _serverOptions.hardwareContext !== 'function') {
32
+ // Return the user-provided one
33
+ return _serverOptions.hardwareContext;
34
+ }
35
+
30
36
  if (cachedHardwareContext === undefined) {
31
37
  cachedHardwareContext = NodeAv.HardwareContext.auto();
32
38
  }
33
39
  return cachedHardwareContext;
34
40
  };
35
41
 
42
+ const validateHwContext = (hw: NodeAv.HardwareContext | null) => {
43
+ if (hw !== null && !(hw instanceof NodeAv.HardwareContext)) {
44
+ throw new TypeError(
45
+ 'When serverOptions.hardwareContext is a function, it must return or resolve to a'
46
+ + ' NodeAv.HardwareContext or null.',
47
+ );
48
+ }
49
+ };
50
+
36
51
  const hardwareDecoderCodecCache = new Map<NodeAv.AVCodecID, NodeAv.Codec | null>();
37
- export const getHardwareDecoderCodec = (codecId: NodeAv.AVCodecID): NodeAv.Codec | null => {
52
+ export const getHardwareDecoderCodec = async (codecId: NodeAv.AVCodecID): Promise<NodeAv.Codec | null> => {
53
+ if (typeof _serverOptions.hardwareContext === 'function') {
54
+ const hw = await _serverOptions.hardwareContext(codecId);
55
+ validateHwContext(hw);
56
+
57
+ return hw?.getDecoderCodec(codecId) ?? null;
58
+ }
59
+
38
60
  if (!hardwareDecoderCodecCache.has(codecId)) {
39
61
  const hw = getHardwareContext();
40
62
  hardwareDecoderCodecCache.set(codecId, hw?.getDecoderCodec(codecId) ?? null);
@@ -43,7 +65,14 @@ export const getHardwareDecoderCodec = (codecId: NodeAv.AVCodecID): NodeAv.Codec
43
65
  };
44
66
 
45
67
  const hardwareEncoderCodecCache = new Map<NodeAv.AVCodecID, NodeAv.Codec | null>();
46
- export const getHardwareEncoderCodec = (codecId: NodeAv.AVCodecID): NodeAv.Codec | null => {
68
+ export const getHardwareEncoderCodec = async (codecId: NodeAv.AVCodecID): Promise<NodeAv.Codec | null> => {
69
+ if (typeof _serverOptions.hardwareContext === 'function') {
70
+ const hw = await _serverOptions.hardwareContext(codecId);
71
+ validateHwContext(hw);
72
+
73
+ return hw?.getEncoderCodec(codecId) ?? null;
74
+ }
75
+
47
76
  if (!hardwareEncoderCodecCache.has(codecId)) {
48
77
  const hw = getHardwareContext();
49
78
  hardwareEncoderCodecCache.set(codecId, hw?.getEncoderCodec(codecId) ?? null);
@@ -58,7 +58,7 @@ export class NodeAvVideoDecoder extends CustomVideoDecoder {
58
58
  ) {
59
59
  codec = NodeAv.Codec.findDecoder(codecId);
60
60
  } else {
61
- codec = getHardwareDecoderCodec(codecId) ?? NodeAv.Codec.findDecoder(codecId);
61
+ codec = (await getHardwareDecoderCodec(codecId)) ?? NodeAv.Codec.findDecoder(codecId);
62
62
  }
63
63
 
64
64
  if (!codec) {
@@ -91,9 +91,10 @@ export class NodeAvVideoDecoder extends CustomVideoDecoder {
91
91
  async decode(packet: EncodedPacket) {
92
92
  if (this.codecContext === null) {
93
93
  await this.initCodecContext(packet);
94
- assert(this.codecContext);
95
94
  }
96
95
 
96
+ assert(this.codecContext);
97
+
97
98
  this.packet.isKeyframe = packet.type === 'key';
98
99
  this.packet.data = Buffer.from(packet.data);
99
100
  this.packet.timeBase = { num: 1, den: 1e6 };
@@ -144,6 +145,8 @@ export class NodeAvVideoDecoder extends CustomVideoDecoder {
144
145
  const ret = await this.codecContext.sendPacket(this.packet);
145
146
  NodeAv.FFmpegError.throwIfError(ret, 'Send packet');
146
147
 
148
+ this.packet.unref(); // Don't need the data again, so just unref it
149
+
147
150
  while (true) {
148
151
  const receiveRet = await this.codecContext.receiveFrame(this.frame);
149
152
  if (receiveRet === NodeAv.AVERROR_EAGAIN || receiveRet === NodeAv.AVERROR_EOF) {
@@ -44,14 +44,13 @@ import { assert, binarySearchLessOrEqual, simplifyRational, toUint8Array } from
44
44
  export class NodeAvVideoEncoder extends CustomVideoEncoder {
45
45
  frame!: NodeAv.Frame;
46
46
  packet!: NodeAv.Packet;
47
- avCodec!: NodeAv.Codec;
48
47
  codecContext: NodeAv.CodecContext | null = null;
48
+ scaler: NodeAv.SoftwareScaleContext | null = null;
49
+ dstFrame: NodeAv.Frame | null = null;
50
+ avCodec!: NodeAv.Codec;
49
51
  lastBuffer: Buffer | null = null;
50
52
  packetEmitted = false;
51
-
52
- scaler: NodeAv.SoftwareScaleContext | null = null;
53
53
  lastScalerKey: string | null = null;
54
- dstFrame: NodeAv.Frame | null = null;
55
54
 
56
55
  // Bookkeeping to restore the original timing information
57
56
  preciseTimings: {
@@ -84,7 +83,7 @@ export class NodeAvVideoEncoder extends CustomVideoEncoder {
84
83
  } else if (this.config.hardwareAcceleration === 'prefer-software') {
85
84
  codec = NodeAv.Codec.findEncoder(codecId);
86
85
  } else {
87
- codec = getHardwareEncoderCodec(codecId) ?? NodeAv.Codec.findEncoder(codecId);
86
+ codec = (await getHardwareEncoderCodec(codecId)) ?? NodeAv.Codec.findEncoder(codecId);
88
87
  }
89
88
 
90
89
  if (!codec) {
@@ -183,10 +182,14 @@ export class NodeAvVideoEncoder extends CustomVideoEncoder {
183
182
  async encode(videoSample: VideoSample, options: VideoEncoderEncodeOptions): Promise<void> {
184
183
  if (this.codecContext === null) {
185
184
  await this.createCodecContext();
186
- assert(this.codecContext);
187
185
  }
186
+ assert(this.codecContext);
188
187
 
189
188
  if (videoSample._data instanceof AvFrameVideoSampleResource) {
189
+ // Release any buffers still referenced from the previous encode before reffing the new frame, otherwise
190
+ // av_frame_ref leaks them
191
+ // https://github.com/Vanilagy/mediabunny/issues/392
192
+ this.frame.unref();
190
193
  this.frame.ref(videoSample._data.frame);
191
194
  } else {
192
195
  if (videoSample.format === null) {
@@ -513,12 +516,10 @@ export class NodeAvVideoEncoder extends CustomVideoEncoder {
513
516
  displayAspectHeight: this.config.displayHeight ?? this.codecContext.height,
514
517
  description: decoderConfigDescription ?? undefined,
515
518
  colorSpace: {
516
- primaries:
517
- unmapColorPrimaries(this.codecContext.colorPrimaries) as VideoColorPrimaries,
518
- matrix:
519
- unmapMatrixCoefficients(this.codecContext.colorSpace) as VideoMatrixCoefficients,
519
+ primaries: unmapColorPrimaries(this.codecContext.colorPrimaries) as VideoColorPrimaries,
520
+ matrix: unmapMatrixCoefficients(this.codecContext.colorSpace) as VideoMatrixCoefficients,
520
521
  transfer:
521
- unmapTransferCharacteristics(this.codecContext.colorTrc) as VideoTransferCharacteristics,
522
+ unmapTransferCharacteristics(this.codecContext.colorTrc) as VideoTransferCharacteristics,
522
523
  fullRange: this.codecContext.colorRange === NodeAv.AVCOL_RANGE_JPEG
523
524
  ? true
524
525
  : this.codecContext.colorRange === NodeAv.AVCOL_RANGE_MPEG
@@ -73,6 +73,9 @@ export class AvFrameVideoSampleResource extends VideoSampleResource {
73
73
  constructor(frame: NodeAv.Frame) {
74
74
  super();
75
75
 
76
+ if (!(frame instanceof NodeAv.Frame)) {
77
+ throw new TypeError('frame must be a NodeAv.Frame.');
78
+ }
76
79
  if (frame.getMediaType() !== NodeAv.AVMEDIA_TYPE_VIDEO) {
77
80
  throw new Error('AvFrameVideoSampleResource must be initialized with a video frame.');
78
81
  }