Files
mediabunny/packages/server/src/index.ts
T

169 lines
5.9 KiB
TypeScript

/*!
* 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<symbol, unknown>)[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<symbol, unknown>)[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<NodeAv.HardwareContext | null>);
};
/** @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);
}
};