默认情况下,Mediabunny 需要浏览器环境才能完整访问解码器、编码器和视频处理功能。@mediabunny/server 使用 NodeAV 为 Node、Bun 或 Deno 等服务器端环境提供此功能的 polyfill,从而实现在服务器上所有 Mediabunny 功能的使用。其结果是一个与 TypeScript 自然集成的服务器端媒体处理 API,避免了调用 FFmpeg CLI 时的笨拙和低效。

此包添加的功能包括:

  • AVC (H.264)、HEVC (H.265)、VP8、VP9、AV1 和 ProRes 的视频解码器和编码器。支持长度前缀和 Annex B 格式的 AVC/HEVC,以及通过 VP9 和 ProRes 实现的透明视频。
  • AAC、MP3、Vorbis、Opus、FLAC、AC-3、E-AC-3 和 DTS 的音频解码器和编码器。支持 AAC 的 AAC 和 ADTS 两种格式。
  • 视频帧转换支持(缩放、旋转、翻转、裁剪)
  • 在所有平台(macOS、Linux、Windows)上自动硬件加速
  • 内置多线程
  • 零拷贝解码和编码路径
GitHub 页面

安装 ​

此库作为 Mediabunny 的 peer 依赖。请使用 npm 同时安装两者:

npm install mediabunny @mediabunny/server

用法 ​

import { registerMediabunnyServer } from '@mediabunny/server';
registerMediabunnyServer();

就这么简单——你现在可以在服务器上访问完整的 Mediabunny 功能集。


提供了一个可选的 options 参数用于进一步配置:

import { registerMediabunnyServer } from '@mediabunny/server';
import * as NodeAv from 'node-av';

registerMediabunnyServer({
	// 使用特定的硬件渲染设备:
	hardwareContext: NodeAv.HardwareContext.create(
		NodeAv.AV_HWDEVICE_TYPE_VAAPI,
		'/dev/dri/renderD128',
	),
});

上传媒体压缩示例 ​

在这里,我们在 Node.js 中设置一个简单的媒体压缩服务器。客户端的请求体被流式传输到 Mediabunny,媒体经过处理后,输出直接流式传输到磁盘。由于管道化操作,内存使用量为 O(1),并且上传速度过快时会自动通过流背压进行减速。

import { ALL_FORMATS, Conversion, FilePathTarget, Input, Mp4OutputFormat, Output, Quality, ReadableStreamSource } from "mediabunny";
import { registerMediabunnyServer } from "@mediabunny/server";
import { Readable } from "node:stream";
import http from "node:http";

registerMediabunnyServer();

const server = http.createServer(async (req, res) => {
    // 将请求体读取为流
	const stream = Readable.toWeb(req) as ReadableStream<Uint8Array>;
	const input = new Input({
		source: new ReadableStreamSource(stream),
		formats: ALL_FORMATS,
	});

    // 将输出直接流式传输到磁盘,也可以流式传输到 S3 等
	const output = new Output({
		format: new Mp4OutputFormat(),
		target: new FilePathTarget(`./converted-${crypto.randomUUID()}.mp4`),
	});

	try {
		const conversion = await Conversion.init({
			input,
			output,
			video: async track => ({
				codec: 'avc',
				height: Math.min(720, await track.getDisplayHeight()),
				quality: new Quality('medium'),
			}),
		});
		await conversion.execute();
	
		res.statusCode = 204;
		res.end();
	} catch (error) {
		res.statusCode = 500;
		res.end();

		console.error("Error processing media:", error);
	}
});

server.listen(3000);

关于使用 Mediabunny 的其他所有方式,请参阅其 指南。

性能 ​

@mediabunny/server 性能极高,因为它是对 NodeAV 的轻量封装,而 NodeAV 本身又是对 FFmpeg C API 的轻量封装。所有解码器和编码器自动在单独的线程上运行,保持主线程未被阻塞。硬件加速会在所有操作系统上自动检测并利用(除非使用 hardwareAcceleration: 'prefer-software' 明确禁用)。除非通过 VideoSample.copyTo() 和 AudioSample.copyTo() 明确要求,否则视频帧和音频样本数据永远不会从 FFmpeg 复制,并且在可能的情况下会自动使用零拷贝 GPU 解码 -> 编码路径。

高级用法 ​

与 NodeAV 配合使用 ​

@mediabunny/server 提供了 AvFrameVideoSampleResource 和 AvFrameAudioSampleResource,用于创建直接由驻留在 NodeAV 的 Frame(因此也是 FFmpeg 的 AVFrame)中的数据支持的 VideoSample 和 AudioSample 实例,而无需将数据复制到 JavaScript 或从 JavaScript 复制数据。阅读 NodeAV 的文档可以帮助您充分利用此集成。

要在 Mediabunny 和 NodeAV (FFmpeg) 世界之间进行转换,您可以这样做:

import { VideoSample, AudioSample } from 'mediabunny';
import { AvFrameVideoSampleResource, AvFrameAudioSampleResource, toAvFrame } from '@mediabunny/server';

// Frame -> VideoSample
new VideoSample(new AvFrameVideoSampleResource(frame), { timestamp });

// Frame -> AudioSample
new AudioSample(new AvFrameAudioSampleResource(frame));
// (使用帧中的时间戳)

// VideoSample -> Frame
await toAvFrame(videoSample, frame);

// AudioSample -> Frame
await toAvFrame(audioSample, frame);

Electron 示例 ​

例如,在使用 Electron 时,我们可能希望捕获应用的内容,而无需将视频数据从 GPU 移动到 CPU:

import { VideoSample } from 'mediabunny';
import { AvFrameVideoSampleResource } from '@mediabunny/server';
import { HardwareContext, SharedTexture, AV_HWDEVICE_TYPE_VIDEOTOOLBOX } from 'node-av';

// 创建硬件上下文(平台特定)
const hw = HardwareContext.create(AV_HWDEVICE_TYPE_VIDEOTOOLBOX);
using sharedTexture = SharedTexture.create(hw);

// 在 Electron paint 事件中使用离屏渲染
offscreen.webContents.on('paint', (event) => {
    const texture = event.texture;
    if (!texture?.textureInfo) {
        return;
    }

    // 导入为硬件帧(零拷贝)
    const frame = sharedTexture.importTexture(texture.textureInfo, { pts: 0n });
    const sample = new VideoSample(new AvFrameVideoSampleResource(frame), {
        timestamp: 0,
        duration: 0,
    });

    texture.release();
});

麦克风录音示例 ​

此处,我们使用 NodeAV 的 Device API 来访问用户的麦克风:

import { AudioSample } from 'mediabunny';
import { AvFrameAudioSampleResource } from '@mediabunny/server';
import { DeviceAPI, Decoder } from 'node-av';

await using mic = await DeviceAPI.openMicrophone();
const audioStream = mic.audio()!;
using decoder = await Decoder.create(audioStream);

let firstTimestamp: number | null = null;
for await (const frame of decoder.frames(mic.packets(audioStream.index))) {
    if (!frame) {
        break;
    }

    const sample = new AudioSample(new AvFrameAudioSampleResource(frame));
    
    if (firstTimestamp === null) {
        firstTimestamp = sample.timestamp;
    }

    // 偏移时间戳使其从 0 开始
    sample.setTimestamp(sample.timestamp - firstTimestamp);

    // 现在对样本进行处理,例如将其传递给 AudioSampleSource
    // ...
}

视频和音频处理 ​

浏览器环境自带许多优秀的 API,例如 Canvas 2D API,它们非常适合进行视频帧处理,并且与 Mediabunny 集成良好。在服务器端,这些 API 不存在,因此必须使用其他方法:

VideoSample.transform() ​

此方法允许对 VideoSample 实例进行简单转换,当已注册 @mediabunny/server 时即可使用:

const transformed = await sample.transform({
    width: 640,
    height: 360,
    fit: 'cover',
});

NodeAV 滤镜图 ​

FFmpeg 的 libavfilter 是一个功能极其强大且通用的媒体处理库,所有功能均可通过 NodeAV 直接访问。它适用于视频和音频数据。

例如,这里我们将 Mediabunny 的 Conversion API 与滤镜图结合,将视频转换为灰度:

import { Conversion } from 'mediabunny';
import { AvFrameVideoSampleResource, toAvFrame } from '@mediabunny/server';
import { Frame, FilterAPI } from 'node-av';

async function* one(f: Frame) { yield f; }

const conversion = await Conversion.init({
    // ...
    video: {
        process: async (sample) => {
            // VideoSample -> Frame
            using inFrame = new Frame();
            inFrame.alloc();
            await toAvFrame(sample, inFrame);

            // Frame -> filter -> AvFrameVideoSampleResource
            using filter = FilterAPI.create('format=gray');
            for await (const outFrame of filter.frames(one(inFrame))) {
                return outFrame && new AvFrameVideoSampleResource(outFrame);
            }

            return null;
        },
    },
    // ...
});
await conversion.execute();

Canvas API 多态填充 ​

像 Skia Canvas 这样的库为 Canvas 2D API 提供了 GPU 启用的多态填充。将其与 Mediabunny 一起使用只需简单地转换到和从 Canvas API:

const width = videoSample.displayWidth;
const height = videoSample.displayHeight;

const canvas = new Canvas(width, height);
const ctx = canvas.getContext('2d');

// 从 VideoSample 复制数据
const imageData = ctx.createImageData(width, height);
await videoSample.copyTo(imageData.data, { format: 'RGBA' });
ctx.putImageData(imageData, 0, 0);

// 发出绘制命令
ctx.fillStyle = 'red';
ctx.fillRect(20, 20, 100, 60);

// 再次转换为 VideoSample
const pixels = ctx.getImageData(0, 0, width, height).data;
return new VideoSample(pixels, {
    format: 'RGBA',
    codedWidth: width,
    codedHeight: height,
    timestamp: videoSample.timestamp,
    duration: videoSample.duration,
});

控制日志记录 ​

特别是在命令行应用程序中,通常不希望 Mediabunny 干扰您的 stdout 和 stderr 输出。Mediabunny 提供了控制其控制台输出的方式:

import { Logging, LogLevel } from 'mediabunny';

// 默认设置:Mediabunny 可以记录错误、警告和信息消息。
Logging.level = LogLevel.Info;

// 仅记录警告和错误。
Logging.level = LogLevel.Warnings;

// 仅记录错误。
Logging.level = LogLevel.Errors;

// 完全不记录任何内容。
Logging.level = LogLevel.Silent;

您还可以挂钩日志事件:

Logging.on('error', (args: unknown[]) => {
	// 处理错误消息
});

Logging.on('warn', (args: unknown[]) => {
	// 处理警告消息
});

Logging.on('info', (args: unknown[]) => {
	// 处理信息消息
});

实现细节 ​

@mediabunny/server 底层使用了 NodeAV,它提供了 FFmpeg C API 的 N-API C 绑定。使用 NodeAV,此包通过直接使用 libavcodec 提供的 API 实现了 自定义解码器和编码器。

对于编码,视频帧和音频样本通过转换为AVFrame并传递给正确的编码器来传输给 FFmpeg。生成的数据包随后被规范化为 WebCodecs 和Mediabunny Codec Registry所期望的格式。对于解码,上述过程则相反:数据包和解码器元数据被传递给正确的解码器,生成的AVFrame实例被包装在VideoSample或AudioSample实例中。视频帧变换(调整大小、旋转、翻转、裁剪)使用libavfilter API 实现。

只要可能,除非明确需要,否则永远不会将AVFrame复制到 JavaScript 中。这实现了零拷贝的解码->变换->编码路径。