Mediabunny 中的 读取 和 写入 原语提供了转换媒体文件所需的一切。然而,由于这是一个非常常见的操作且细节可能很棘手,Mediabunny 附带了一个内置的文件转换抽象层。
它具有以下功能:
- 转复用(在复制媒体数据的同时更改容器格式)
- 转码(更改轨道的编解码器)
- 移除轨道
- 压缩
- 裁剪
- 视频调整大小与适配
- 视频旋转
- 视频裁剪
- 视频帧率调整
- 视频透明度移除/保留
- 音频重采样
- 音频上下混音
- 用户定义的视频和音频处理
转换 API 的设计目标是简单、多功能、可组合且高性能。
基本用法
运行转换
每个转换过程都由一个 Conversion 实例表示。使用 Conversion.init(...) 创建新实例,然后使用 .execute() 运行转换。
这里,我们将其转换为 WebM:
import {
Input,
Output,
WebMOutputFormat,
BufferTarget,
Conversion,
} from 'mediabunny';
const input = new Input({ ... });
const output = new Output({
format: new WebMOutputFormat(),
target: new BufferTarget(),
});
const conversion = await Conversion.init({ input, output });
if (!conversion.isValid) {
// 转换无效,无法在不报错的情况下执行。
// 此字段提供轨道被丢弃的原因:
conversion.discardedTracks; // => DiscardedTrack[]
return;
}
await conversion.execute();
// output.target.buffer 包含最终文件就是这样!Conversion 只需接受一个 Input 和 Output 实例,然后从输入读取数据并写入输出。如果您不熟悉 Input 和 Output,请查看它们各自的指南。
传递给 Conversion 的 Output 必须是新的;也就是说,它不能添加任何轨道或元数据标签,并且必须处于 'pending' 状态(尚未开始)。对于可组合的转换,此要求会放宽,允许您将转换与其他轨道结合。
未配置时,转换过程会自动处理所有细节,例如:
- 尽可能复制媒体数据,否则进行转码
- 丢弃输出格式不支持的轨道
在执行 Conversion 之前,您应该考虑检查 isValid 和被丢弃的轨道。
监控进度
要监控 Conversion 的进度,请在调用 execute 之前 设置其 onProgress 属性:
const conversion = await Conversion.init({ input, output });
conversion.onProgress = (progress: number) => {
// `progress` 是一个介于 0 和 1 之间(含)的数字
};
await conversion.execute();每当转换进度推进时,都会调用此回调。
警告
进度为 1 并不表示转换已完成;只有当 .execute() 返回的 promise 解析后,转换才算完成。
警告
跟踪转换进度可能会轻微影响性能,因为它需要知道输入文件的总时长。这通常可以忽略不计,但在使用仅追加的输入源(如 ReadableStreamSource)时应避免使用。
如果您想监控转换的输出大小(以字节为单位),只需在 Target 上使用 write 事件:
let currentFileSize = 0;
const stopListening = output.target.on('write', ({ start, end }) => {
currentFileSize = Math.max(currentFileSize, end);
});取消转换
有时,您可能想要取消正在进行的转换过程。为此,请使用 cancel 方法:
await conversion.cancel(); // 一旦转换被取消即解析这将自动释放转换进程使用的所有资源,并导致任何正在进行的 execute 调用抛出 ConversionCanceledError。
如果转换是可组合的,则相应的 Output 不会被取消,并在转换取消后仍可继续使用。
暂停转换
您可以在执行过程中暂停转换,并在稍后恢复。为此,向 execute 方法传递暂停信号:
const controller = new AbortController();
button.onclick = () => controller.abort();
await conversion.execute({
pauseSignal: controller.signal,
});
if (conversion.state === 'idle') {
// 在完成前暂停
} else if (conversion.state === 'done') {
// 运行至完成
}未完成的转换只需再次调用 execute 即可恢复:
await conversion.execute();部分执行
您可以不运行完整的转换,而是仅执行到达到特定时间戳为止:
await conversion.execute({
// 一旦输出时间戳达到 10 秒,暂停执行
until: 10,
});然后通过再次调用 execute 可以恢复并继续该转换。此功能对于可组合转换特别有用。
视频选项
您可以在转换选项中设置 video 属性,以配置转换器对视频轨道的处理行为。选项如下:
type ConversionVideoOptions = {
discard?: boolean;
width?: number;
height?: number;
fit?: 'fill' | 'contain' | 'cover';
rotate?: 0 | 90 | 180 | 270;
flip?: boolean;
allowTransformationMetadata?: boolean;
crop?: { left: number; top: number; width: number; height: number };
frameRate?: number;
codec?: VideoCodec;
quality?: Quality;
alpha?: 'discard' | 'keep'; // 默认为 'discard'
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
keyFrameInterval?: number;
forceTranscode?: boolean;
process?: (sample: VideoSample) => MaybePromise<
CanvasImageSource | VideoSample | (CanvasImageSource | VideoSample)[] | null
>;
processedWidth?: number;
processedHeight?: number;
group?: OutputTrackGroup | OutputTrackGroup[];
};
type MaybePromise<T> = T | Promise<T>;例如,这里我们将视频轨道调整为 720p:
const conversion = await Conversion.init({
input,
output,
video: {
width: 1280,
height: 720,
fit: 'contain',
},
});提供的配置将同样适用于输入的所有视频轨道。如果您想为每个视频轨道应用单独的配置,请查看特定于轨道的选项。
丢弃视频
如果您想移除视频轨道,请使用 discard: true。
调整视频尺寸
width、height 和 fit 属性控制视频的缩放方式。如果仅提供了 width 或 height,另一个值将自动推导以保持视频原始宽高比。如果同时使用了两者,则必须设置 fit 来控制缩放算法:
'fill'会将图像拉伸以填满整个区域,可能会改变宽高比。'contain'会将整个图像包含在区域内,同时保持宽高比。这可能导致黑边(letterboxing)。'cover'会将图像缩放直到填满整个区域,同时保持宽高比。
如果将 width 或 height 与 rotate、flip 或 crop 结合使用,它们控制的是旋转、翻转或裁剪后的尺寸。
如果您想对视频的尺寸应用最大/最小约束,请查看特定于轨道的选项。
在极少数情况下,如果输入视频的尺寸随时间变化,可以使用 fit 字段来控制尺寸变化行为(见 VideoEncodingConfig)。如果未设置,行为默认为 'passThrough'。
旋转和翻转视频
rotate 按指定角度顺时针旋转视频。此旋转应用于原始输入文件中的任何旋转元数据之上,并发生在翻转、裁剪和调整尺寸之前。
flip 水平翻转视频(关于垂直轴)。此翻转应用于原始输入文件中的任何翻转元数据之上,并发生在旋转之后但在裁剪和调整尺寸之前。
默认情况下,Mediabunny 会尝试利用输出文件中的旋转和翻转元数据来执行旋转和翻转操作(如可能)。但是,如果您不希望发生这种情况,或者希望使用 Mediabunny 从文件中剥离所有此类元数据,可以将 allowTransformationMetadata 设置为 false。
裁剪视频
crop 可用于从原始视频中提取矩形区域。该矩形由 left、top、width 和 height 指定,并会根据视频尺寸进行限制。裁剪操作在旋转和翻转之后,但在调整尺寸之前应用。
调整帧率
frameRate 属性可用于设置输出视频的帧率(单位:Hz)。如果未指定,将使用原始输入帧率(可能是可变的)。
转码视频
使用 codec 属性控制输出轨道的编解码器。应将其设置为输出文件支持的 编解码器,否则轨道将被丢弃。当未设置且需要转码时,Mediabunny 会自动选择一个环境可以编码且输出格式可以容纳的编解码器。
使用 quality 属性控制输出视频的质量。例如,您可以使用此字段压缩视频轨道。详见 编码质量。如果设置了此属性,将始终进行转码。如果未设置此属性但仍需转码,则将使用 new Quality('high') 作为值。
使用 keyFrameInterval 属性控制输出视频中关键帧之间的最大间隔(秒)。设置此字段会强制进行转码。如果您想阻止媒体数据的直接复制并强制进行转码步骤,请使用 forceTranscode: true。
使用 hardwareAcceleration 属性控制视频转码时使用硬件加速还是软件加速。
处理视频
process 属性可用于定义自定义视频样本处理函数,例如用于 添加覆盖层、颜色变换或时间戳修改。您需要自行执行此处理,例如使用 Canvas API。
示例:
let ctx: CanvasRenderingContext2D | null = null;
const conversion = await Conversion.init({
video: {
process: (sample) => {
if (!ctx) {
const canvas = new OffscreenCanvas(
sample.displayWidth,
sample.displayHeight,
);
ctx = canvas.getContext('2d')!;
// Convert the video to grayscale
ctx.filter = 'saturate(0)';
}
sample.draw(ctx, 0, 0);
return ctx.canvas;
},
},
});该函数在转换和帧率校正后,对每个输入视频样本进行调用。它必须返回一个 VideoSample、可转换为 VideoSample 的对象、它们的数组,或者为丢弃帧而返回 null。
此函数也可用于手动调整帧的大小。在此情况下,您应使用 processedWidth 和 processedHeight 字段来指示后处理尺寸,以便编码器能更好地预期输出内容。
音频选项
您可以在转换选项中设置 audio 属性,以配置转换器对音频轨道的行为。可用选项如下:
type ConversionAudioOptions = {
discard?: boolean;
codec?: AudioCodec;
quality?: Quality;
numberOfChannels?: number;
sampleRate?: number;
sampleFormat?: 'u8' | 's16' | 's32' | 'f32';
forceTranscode?: boolean;
process?: (sample: AudioSample) => MaybePromise<
AudioSample | AudioSample[] | null
>;
processedNumberOfChannels?: number;
processedSampleRate?: number;
};
type MaybePromise<T> = T | Promise<T>;例如,此处我们将音频轨道转换为单声道并设置特定的采样率:
const conversion = await Conversion.init({
input,
output,
audio: {
numberOfChannels: 1,
sampleRate: 48000,
},
});提供的配置将同等应用于输入的所有音频轨道。如果您希望为每个音频轨道应用单独的配置,请查看特定于轨道的选项。
丢弃音频
如果您想移除音频轨道,请使用 discard: true。
重采样音频
numberOfChannels 属性控制输出音频的声道数(例如,1 表示单声道,2 表示立体声)。如果此值与输入轨道的声道数不同,Mediabunny 将使用 与 Web Audio API 相同的算法 对声道数据进行上混或下混。
sampleRate 属性控制以 Hz 为单位的采样率(例如,44100、48000)。如果此值与输入轨道的采样率不同,Mediabunny 将对音频进行重采样。
转码音频
使用 codec 属性控制输出轨道的编解码器。该值应设置为输出文件支持的 编解码器,否则该轨道将被丢弃。当未设置且需要转码时,Mediabunny 将自动选择一个环境可以编码且输出格式可以容纳的编解码器。
使用 quality 属性控制输出音频的质量。例如,您可以使用该字段压缩音频轨道。更多信息请参阅 编码质量。如果设置了此属性,则始终会发生转码。如果未设置此属性但仍需转码,则将使用 new Quality('high') 作为值。
如果您希望防止媒体数据的直接复制并强制进行转码步骤,请使用 forceTranscode: true。
处理音频
process 属性可用于定义自定义音频样本处理函数,例如应用音频效果、变换或时间戳修改。您需要自行执行此处理。
该函数在重新混音和重采样后,对每个输入音频样本进行调用。它必须返回一个 AudioSample、它们的数组,或者为丢弃样本而返回 null。
此函数也可用于手动执行重新混音或重采样。在此情况下,您应使用 processedNumberOfChannels 和 processedSampleRate 字段来指示后处理参数,以便编码器能更好地预期输出内容。
特定于轨道的选项
您可能需要根据输入轨道的具体情况分别配置视频和音频选项。或者,如果媒体文件包含多个视频或音频轨道,您可能希望仅丢弃特定轨道或分别配置每个轨道。
为此,您可以不传递 video 和 audio 的对象,而是传递一个函数:
const conversion = await Conversion.init({
input,
output,
// Function gets invoked for each video track:
video: async (videoTrack) => {
if (videoTrack.number > 1) {
// Keep only the first video track
return { discard: true };
}
return {
// Shrink width to 640 only if the track is wider
width: Math.min(await videoTrack.getDisplayWidth(), 640),
};
},
// Async functions work too:
audio: async (audioTrack) => {
if (await audioTrack.getLanguageCode() !== 'rus') {
// Keep only Russian audio tracks
return { discard: true };
}
return {
codec: 'aac',
};
},
});有关视频和音频轨道属性的文档,请参考 读取轨道元数据。
轨道扇出
您还可以为单个输入轨道设置(或返回)一个选项数组,这将导致 Mediabunny 为每个条目创建一个输出轨道,换句话说,即从一个输入轨道生成多个输出轨道(扇出)。
这对于以不同质量生成多个版本非常有用,例如用于 HLS:
const conversion = await Conversion.init({
input,
output,
video: [
{ height: 1080, quality: new Quality('high') },
{ height: 720, quality: new Quality('medium') },
{ height: 480, quality: new Quality('low') },
],
});裁剪
使用转换选项中的 trim 属性,仅将输入文件的一部分提取到输出文件中:
type ConversionOptions = {
// ...
trim?: {
start: number; // 秒数
end: number; // 秒数
};
// ...
};例如,这里我们提取从 10 秒到 25 秒的片段:
const conversion = await Conversion.init({
input,
output,
trim: {
start: 10,
end: 25,
},
});在这种情况下,输出时长将为 15 秒。
如果仅设置了 start,则剪辑将持续到输入文件的末尾。如果仅设置了 end,则剪辑将从输入文件的开头开始。
注意,当使用裁剪默认值时,生成的媒体文件将始终从时间戳 0 开始。如果您的输入文件具有起始时间偏移(MPEG-TS 文件常见),并且您希望保留该偏移,请使用 trim: { start: 0 } 以确保时间戳不会发生移位。
您甚至可以使用负裁剪值来偏移媒体的起始位置:
const conversion = await Conversion.init({
// ...
trim: {
start: -2, // 起始处有两秒无媒体数据(冻结画面 / 静音)
},
// ...
});警告
将 start 设置为非默认值目前将强制对视频和音频进行 转码。
在 Mediabunny 的未来版本中,当前通过不设置 start 即可访问的快速“包复制路径”,可能会针对任意裁剪范围提供。
复制媒体数据
在媒体转换中,媒体数据可以直接从旧文件复制到新文件,或者经过转码步骤。如果您来自 FFmpeg,您可能知道通过 -c copy 实现的复制路径。Mediabunny 与 FFmpeg 的不同之处在于,如果配置允许,它将默认尝试执行复制转换。复制转换通常更受欢迎,因为它们快速且无损。
有几个主要因素决定了是否可以进行复制转换:
- 没有强制转码的操作(调整大小、旋转、重采样、编解码器更改等)
- 输出格式必须能够包含输入编解码器
- 输出格式必须支持足够灵活的时间戳
如果您转换为 MP4,由于该格式支持广泛的编解码器以及通过编辑列表支持负时间戳(允许您模拟任意裁剪范围),通常可以进行复制转换。较简单的格式如 MP3 限制更多,可能需要转码或时间戳偏移才能实现复制转换。
您可以通过 copy 转换选项完全配置 Mediabunny 的行为:
type ConversionCopyOptions = {
mode?: 'forced' | 'preferred';
shiftTolerance?: number;
boundaryPolicy?: 'expand' | 'shrink';
boundaryTolerance?: number;
};mode
控制是首选还是要求媒体复制。默认为'preferred'。'forced':尽可能复制编码媒体,并丢弃无法复制的轨道。'preferred':在可能时复制编码媒体,并对无法复制的轨道进行转码。
shiftTolerance
为了能够将媒体复制到输出格式而可应用的最大绝对偏移量(秒)。默认为0,表示不允许任何额外偏移。设置为Infinity以允许任何偏移。0的偏移为您提供完美的 时间线同步:输出时间戳将与输入时间戳完全匹配(仅由裁剪区域产生偏移)。任何非零偏移都将破坏此属性,但在所有情况下仍保持完美的跨轨道和音视频同步。boundaryPolicy
控制为满足请求的裁剪范围将复制哪个媒体区域。默认为'expand'。'expand':至少包含请求范围内的所有媒体。这可能需要因关键帧和包边界而扩展媒体区域,因此可能包含裁剪范围之外的媒体。该区域始终最小化扩展以满足复制标准。'shrink':仅包含完全位于请求裁剪范围内的媒体。这可能需要因关键帧和包边界而缩小媒体区域,因此可能排除裁剪范围内的媒体。该区域始终最小化缩小以满足复制标准。
如果您不希望丢失任何媒体,请使用
expand;如果您希望从不暴露裁剪区域之外的任何媒体,请使用shrink。boundaryTolerance
由于boundaryPolicy导致的被复制媒体区域在其起始处偏离请求裁剪范围的最大量(秒)。默认为Infinity,表示允许任何偏差。换句话说,此字段设置转换允许添加/移除多少秒媒体以实现复制路径。
您可以通过将字段设置为 false 来完全禁用复制转换:
await Conversion.init({
copy: false,
// ...
});元数据标签
默认情况下,输入的任意 描述性元数据标签 都会复制到输出中。如果您想进一步控制写入输出的元数据标签,可以使用 tags 选项:
// Set your own metadata:
const conversion = await Conversion.init({
// ...
tags: {
title: 're:Turning',
artist: 'Alexander Panos',
},
// ...
});
// Or, augment the input's metadata:
const conversion = await Conversion.init({
// ...
tags: (inputTags) => ({
...inputTags, // Keep the existing metadata
images: [{ // And add cover art
data: new Uint8Array(...),
mimeType: 'image/jpeg',
kind: 'coverFront',
}],
comment: undefined, // And remove any comments
}),
// ...
});
// Or, remove all metadata
const conversion = await Conversion.init({
// ...
tags: {},
// ...
});选择音轨
使用 tracks 选项来控制哪些输入音轨被视为进行转换:
const conversion = await Conversion.init({
input,
output,
tracks: 'primary', // Only the primary video and audio tracks
});接受的值为 'all' 或 'primary'。默认为 'all',除非输入格式为 HLS,则默认为 'primary'。这是因为 HLS 通常具有多个音轨的变体,转换所有变体(通常)是不需要的。
对于更细粒度的选择,请使用音轨上的 discard 字段。
丢弃的音轨
如果输入音轨被排除在输出文件之外,则视为 已丢弃。初始化 Conversion 后可以访问已丢弃音轨的列表:
const conversion = await Conversion.init({ input, output });
conversion.discardedTracks; // => DiscardedTrack[]
type DiscardedTrack = {
// The track that was discarded
track: InputTrack;
// The reason for discarding the track
reason:
| 'discarded_by_user'
| 'max_track_count_reached'
| 'max_track_count_of_type_reached'
| 'unknown_source_codec'
| 'undecodable_source_codec'
| 'no_encodable_target_codec';
};由于您可以在执行 Conversion 之前检查此列表,因此您可以决定是否继续转换过程。
如果 isValid 为 false,则已丢弃的音轨导致 Conversion 无效。例如,当格式需要特定编解码器但该编解码器无法编码时,就会发生这种情况。
存在以下原因:
discarded_by_user
您通过设置discard: true丢弃了该音轨。max_track_count_reached
输出中没有更多空间容纳另一个音轨。max_track_count_of_type_reached
输出中没有更多空间容纳另一种类型的音轨,或者输出根本不支持该音轨类型。unknown_source_codec
我们不知道输入音轨的编解码器,因此不知道该怎么做。undecodable_source_codec
已知输入音轨的编解码器,但我们无法对其进行解码。no_encodable_target_codec
我们无法找到能够编码且可包含在输出格式中的编解码器。如果环境不支持必要的编码器,或者您请求的编解码器无法包含在输出格式中,可能会出现此原因。
另一方面,您可以随时查询哪些输入音轨成功进入了输出:
const conversion = await Conversion.init({ input, output });
conversion.utilizedTracks; // => InputTrack[]当扇出(fan-out)从单个音轨产生多个输出音轨时,该音轨可能会在此列表中多次出现。
可组合转换
默认情况下,Conversion 完全拥有其 Output:它需要一个全新的输出,然后为您启动、添加数据并完成最终化。然而,有时您希望转换只是贡献于单个输出文件的多个组件之一——例如,保留输入的音轨同时附加您自己外部制作的音频音轨。为此,请设置 composable: true。
可组合转换仅将其自身的音轨添加到输出中,并在 execute() 运行时泵送其媒体数据。输出生命周期的其余部分由您掌控:您可以添加任何其他音轨,设置任何元数据标签,并自行调用 start() 和 finalize()。这使您能够在转换之外添加其他音轨,甚至让多个转换针对单个 Output。
要使用它,请先初始化所有内容,然后启动 Output,再执行转换:
import {
Input,
Output,
Mp4OutputFormat,
BufferTarget,
Conversion,
AudioBufferSource,
} from 'mediabunny';
const input = new Input({ ... });
const output = new Output({
format: new Mp4OutputFormat(),
target: new BufferTarget(),
});
// 仅使用转换来复制视频
const conversion = await Conversion.init({
input,
output,
audio: { discard: true },
composable: true,
});
// 直接添加我们自己的音频轨道
const audioSource = new AudioBufferSource({
codec: 'aac',
quality: new Quality({ bitrate: 128e3 }),
});
output.addAudioTrack(audioSource);
// 启动输出
await output.start();
// 并发运行转换并馈送我们自己的音频
await Promise.all([
conversion.execute(),
audioSource.add(myAudioBuffer).then(() => audioSource.close()),
]);
// 完成输出
await output.finalize();同步运行
为了避免因缓冲需求导致的高内存占用,重要的是在所有轨道上以大致相同的速度添加媒体数据。为此,您可以通过多次调用 execute 来刻意步进转换:
await output.start();
for (using sample of generateAudioSamples()) {
await audioSource.add(sample);
await conversion.execute({ until: sample.timestamp });
}
// 转换剩余部分
await conversion.execute();
await output.finalize();当运行多个针对同一输出的可组合转换时,您可以使用以下模式:
await output.start();
for (let until = 1; true; until += 1) {
await Promise.all([
conversion1.execute({ until }),
conversion2.execute({ until }),
]);
if (conversion1.state === 'done' && conversion2.state === 'done') {
break;
}
}
await output.finalize();转换直播流
直播输入(如 HLS 直播流)也可与 Conversion API 配合使用。在这种情况下,默认情况下,转换将一直运行直到直播流结束。
如果您只想转换直播流的一部分而不是等待其结束,可以对转换进行裁剪。例如,这里我们捕获接下来 60 秒的直播流:
// 获取直播边缘
const currentDuration = await input.getDurationFromMetadata(undefined, {
skipLiveWait: true,
});
const conversion = await Conversion.init({
input,
output,
trim: {
// 从当前直播边缘开始
start: currentDuration,
// 最晚在 60 秒后结束
end: currentDuration + 60,
},
});