mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-01 04:43:51 +02:00
Add MediabunnyServerOptions and configurable hardware context (closes #389)
This commit is contained in:
@@ -42,6 +42,9 @@ export class AvFrameAudioSampleResource extends AudioSampleResource {
|
||||
constructor(frame: NodeAv.Frame) {
|
||||
super();
|
||||
|
||||
if (!(frame instanceof NodeAv.Frame)) {
|
||||
throw new TypeError('frame must be a NodeAv.Frame.');
|
||||
}
|
||||
if (frame.getMediaType() !== NodeAv.AVMEDIA_TYPE_AUDIO) {
|
||||
throw new Error('AvFrameAudioSampleResource must be initialized with an audio frame.');
|
||||
}
|
||||
|
||||
@@ -6,7 +6,14 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { AudioSample, registerDecoder, registerEncoder, registerVideoSampleTransformer, VideoSample } from 'mediabunny';
|
||||
import {
|
||||
AudioSample,
|
||||
MaybePromise,
|
||||
registerDecoder,
|
||||
registerEncoder,
|
||||
registerVideoSampleTransformer,
|
||||
VideoSample,
|
||||
} from 'mediabunny';
|
||||
import * as NodeAv from 'node-av';
|
||||
import { NodeAvVideoDecoder } from './video-decoder';
|
||||
import { NodeAvVideoEncoder } from './video-encoder';
|
||||
@@ -28,6 +35,29 @@ if ((globalThis as Record<symbol, unknown>)[SERVER_LOADED_SYMBOL]) {
|
||||
|
||||
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
|
||||
@@ -38,14 +68,32 @@ let registered = false;
|
||||
* 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 = () => {
|
||||
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);
|
||||
|
||||
@@ -60,6 +108,8 @@ export const registerMediabunnyServer = () => {
|
||||
registerVideoSampleTransformer(transformVideoSample);
|
||||
};
|
||||
|
||||
export type { MediabunnyServerOptions };
|
||||
|
||||
export { AvFrameVideoSampleResource } from './video-sample';
|
||||
export { AvFrameAudioSampleResource } from './audio-sample';
|
||||
|
||||
@@ -76,6 +126,13 @@ export { AvFrameAudioSampleResource } from './audio-sample';
|
||||
* @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
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import { VideoSamplePixelFormat, MediaCodec } from 'mediabunny';
|
||||
import * as NodeAv from 'node-av';
|
||||
import { _serverOptions } from './index';
|
||||
|
||||
export const CODEC_TO_CODEC_ID: Partial<Record<MediaCodec, NodeAv.AVCodecID>> = {
|
||||
avc: NodeAv.AV_CODEC_ID_H264,
|
||||
@@ -27,14 +28,35 @@ export const CODEC_TO_CODEC_ID: Partial<Record<MediaCodec, NodeAv.AVCodecID>> =
|
||||
|
||||
let cachedHardwareContext: NodeAv.HardwareContext | null | undefined = undefined;
|
||||
export const getHardwareContext = (): NodeAv.HardwareContext | null => {
|
||||
if (_serverOptions.hardwareContext !== undefined && typeof _serverOptions.hardwareContext !== 'function') {
|
||||
// Return the user-provided one
|
||||
return _serverOptions.hardwareContext;
|
||||
}
|
||||
|
||||
if (cachedHardwareContext === undefined) {
|
||||
cachedHardwareContext = NodeAv.HardwareContext.auto();
|
||||
}
|
||||
return cachedHardwareContext;
|
||||
};
|
||||
|
||||
const validateHwContext = (hw: NodeAv.HardwareContext | null) => {
|
||||
if (hw !== null && !(hw instanceof NodeAv.HardwareContext)) {
|
||||
throw new TypeError(
|
||||
'When serverOptions.hardwareContext is a function, it must return or resolve to a'
|
||||
+ ' NodeAv.HardwareContext or null.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const hardwareDecoderCodecCache = new Map<NodeAv.AVCodecID, NodeAv.Codec | null>();
|
||||
export const getHardwareDecoderCodec = (codecId: NodeAv.AVCodecID): NodeAv.Codec | null => {
|
||||
export const getHardwareDecoderCodec = async (codecId: NodeAv.AVCodecID): Promise<NodeAv.Codec | null> => {
|
||||
if (typeof _serverOptions.hardwareContext === 'function') {
|
||||
const hw = await _serverOptions.hardwareContext(codecId);
|
||||
validateHwContext(hw);
|
||||
|
||||
return hw?.getDecoderCodec(codecId) ?? null;
|
||||
}
|
||||
|
||||
if (!hardwareDecoderCodecCache.has(codecId)) {
|
||||
const hw = getHardwareContext();
|
||||
hardwareDecoderCodecCache.set(codecId, hw?.getDecoderCodec(codecId) ?? null);
|
||||
@@ -43,7 +65,14 @@ export const getHardwareDecoderCodec = (codecId: NodeAv.AVCodecID): NodeAv.Codec
|
||||
};
|
||||
|
||||
const hardwareEncoderCodecCache = new Map<NodeAv.AVCodecID, NodeAv.Codec | null>();
|
||||
export const getHardwareEncoderCodec = (codecId: NodeAv.AVCodecID): NodeAv.Codec | null => {
|
||||
export const getHardwareEncoderCodec = async (codecId: NodeAv.AVCodecID): Promise<NodeAv.Codec | null> => {
|
||||
if (typeof _serverOptions.hardwareContext === 'function') {
|
||||
const hw = await _serverOptions.hardwareContext(codecId);
|
||||
validateHwContext(hw);
|
||||
|
||||
return hw?.getEncoderCodec(codecId) ?? null;
|
||||
}
|
||||
|
||||
if (!hardwareEncoderCodecCache.has(codecId)) {
|
||||
const hw = getHardwareContext();
|
||||
hardwareEncoderCodecCache.set(codecId, hw?.getEncoderCodec(codecId) ?? null);
|
||||
|
||||
@@ -58,7 +58,7 @@ export class NodeAvVideoDecoder extends CustomVideoDecoder {
|
||||
) {
|
||||
codec = NodeAv.Codec.findDecoder(codecId);
|
||||
} else {
|
||||
codec = getHardwareDecoderCodec(codecId) ?? NodeAv.Codec.findDecoder(codecId);
|
||||
codec = (await getHardwareDecoderCodec(codecId)) ?? NodeAv.Codec.findDecoder(codecId);
|
||||
}
|
||||
|
||||
if (!codec) {
|
||||
|
||||
@@ -83,7 +83,7 @@ export class NodeAvVideoEncoder extends CustomVideoEncoder {
|
||||
} else if (this.config.hardwareAcceleration === 'prefer-software') {
|
||||
codec = NodeAv.Codec.findEncoder(codecId);
|
||||
} else {
|
||||
codec = getHardwareEncoderCodec(codecId) ?? NodeAv.Codec.findEncoder(codecId);
|
||||
codec = (await getHardwareEncoderCodec(codecId)) ?? NodeAv.Codec.findEncoder(codecId);
|
||||
}
|
||||
|
||||
if (!codec) {
|
||||
|
||||
@@ -73,6 +73,9 @@ export class AvFrameVideoSampleResource extends VideoSampleResource {
|
||||
constructor(frame: NodeAv.Frame) {
|
||||
super();
|
||||
|
||||
if (!(frame instanceof NodeAv.Frame)) {
|
||||
throw new TypeError('frame must be a NodeAv.Frame.');
|
||||
}
|
||||
if (frame.getMediaType() !== NodeAv.AVMEDIA_TYPE_VIDEO) {
|
||||
throw new Error('AvFrameVideoSampleResource must be initialized with a video frame.');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user