默认情况下,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)上自动硬件加速
- 内置多线程
- 零拷贝解码和编码路径
安装
此库作为 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 中。这实现了零拷贝的解码->变换->编码路径。