/*! * Copyright (c) 2026-present, Vanilagy and contributors * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { AudioSample, Logging, MaybePromise, registerDecoder, registerEncoder, registerVideoSampleTransformer, VideoSample, } from 'mediabunny'; import * as NodeAv from 'node-av'; import { NodeAvVideoDecoder } from './video-decoder'; import { NodeAvVideoEncoder } from './video-encoder'; import { NodeAvAudioDecoder } from './audio-decoder'; import { NodeAvAudioEncoder } from './audio-encoder'; import { copyVideoSampleToAvFrame, AvFrameVideoSampleResource, transformVideoSample } from './video-sample'; import { copyAudioSampleToAvFrame, AvFrameAudioSampleResource } from './audio-sample'; import { registerProresDecoder } from '@mediabunny/prores'; const SERVER_LOADED_SYMBOL = Symbol.for('@mediabunny/server loaded'); if ((globalThis as Record)[SERVER_LOADED_SYMBOL]) { Logging._error( '[WARNING]\n@mediabunny/server was loaded twice.' + ' This will likely cause the package not to work correctly.' + ' Check if multiple dependencies are importing different versions of @mediabunny/server,' + ' or if something is being bundled incorrectly.', ); } (globalThis as Record)[SERVER_LOADED_SYMBOL] = true; let registered = false; /** * Options for configuring Mediabunny's server-side polyfills. * @group \@mediabunny/server * @public */ type MediabunnyServerOptions = { /** * The hardware context to use for hardware-accelerated decoding and encoding. When set to a * `NodeAv.HardwareContext`, that context is used directly. When set to a function, the function is invoked (with * no caching) every time a hardware decoder or encoder codec is resolved, receiving the codec ID and returning the * context to use for it (or `null` for no hardware acceleration). * * When not set, a context is automatically detected on first use and then cached. */ hardwareContext?: | NodeAv.HardwareContext | null | ((codecId: NodeAv.AVCodecID) => MaybePromise); }; /** @internal */ export let _serverOptions: MediabunnyServerOptions = {}; /** * Registers video and audio decoders and encoders for all codecs, using FFmpeg's libavcodec under the hood. * Additionally, a custom `VideoSample` transformer based on libavfilter is registered to enable resizing, rotation and * cropping of video frames. * * Make sure to call this function before interacting with Mediabunny. * * The decoders and encoders will automatically detect hardware acceleration support for each codec and platform and * make use of it if applicable. * * You can pass optional {@link MediabunnyServerOptions} for additional configuration. * * @group \@mediabunny/server * @public */ export const registerMediabunnyServer = (options: MediabunnyServerOptions = {}) => { if (typeof options !== 'object' || !options) { throw new TypeError('options must be an object.'); } if ( options.hardwareContext != null && !( options.hardwareContext instanceof NodeAv.HardwareContext || typeof options.hardwareContext === 'function' ) ) { throw new TypeError( 'options.hardwareContext, when provided, must be a NodeAv.HardwareContext, a function, or null.', ); } if (registered) { return; } registered = true; _serverOptions = options; NodeAv.Log.setLevel(NodeAv.AV_LOG_ERROR); // Video registerDecoder(NodeAvVideoDecoder); registerProresDecoder(); // Faster than FFmpeg, so we use it registerEncoder(NodeAvVideoEncoder); // Audio registerDecoder(NodeAvAudioDecoder); registerEncoder(NodeAvAudioEncoder); registerVideoSampleTransformer(transformVideoSample); }; export type { MediabunnyServerOptions }; export { AvFrameVideoSampleResource } from './video-sample'; export { AvFrameAudioSampleResource } from './audio-sample'; /** * Copies a `VideoSample` or `AudioSample` into the given NodeAV * [`Frame`](https://seydx.github.io/node-av/api/lib/classes/Frame.html), setting up the frame's format, media type, * timing and data. When the sample is already backed by an `AVFrame` (via `AvFrameVideoSampleResource` or * `AvFrameAudioSampleResource`), the frame is ref'd instead of copied for zero-copy reuse. * * For video samples, the frame's time base is always set to 1/1000000 (microsecond accuracy). For audio samples, the * frame's time base is always set to 1/sampleRate. * * @group \@mediabunny/server * @public */ export const toAvFrame = async (sample: VideoSample | AudioSample, frame: NodeAv.Frame) => { if (!(sample instanceof VideoSample) && !(sample instanceof AudioSample)) { throw new TypeError('sample must be a VideoSample or an AudioSample.'); } if (!(frame instanceof NodeAv.Frame)) { throw new TypeError('frame must be a NodeAv.Frame.'); } if (sample instanceof VideoSample) { if (sample._data instanceof AvFrameVideoSampleResource) { // We're overriding the frame, so release whatever it referenced before, otherwise av_frame_ref leaks it frame.unref(); frame.ref(sample._data.frame); } else { if (sample.format === null) { throw new Error('Cannot convert foreign VideoSample with unknown (null) format.'); } await copyVideoSampleToAvFrame(sample, frame, null); } frame.pts = BigInt(sample.microsecondTimestamp); frame.duration = BigInt(sample.microsecondDuration); frame.timeBase = new NodeAv.Rational(1, 1e6); } else { if (sample._data instanceof AvFrameAudioSampleResource) { // We're overriding the frame, so release whatever it referenced before, otherwise av_frame_ref leaks it frame.unref(); frame.ref(sample._data.frame); } else { copyAudioSampleToAvFrame(sample, frame); } frame.timeBase = new NodeAv.Rational(1, sample.sampleRate); frame.pts = BigInt(Math.round(sample.timestamp * sample.sampleRate)); frame.duration = BigInt(sample.numberOfFrames); } };