mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-29 11:53:52 +02:00
169 lines
5.9 KiB
TypeScript
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);
|
|
}
|
|
};
|