简介 ​

媒体源提供了用于将媒体数据添加到输出文件的 API。不同的媒体源提供不同层次的抽象,并适用于不同的用例。

有关如何使用媒体源创建输出轨道的信息,请查看 编写媒体文件。

大多数媒体源遵循以下代码模式来添加媒体数据:

await mediaSource.add(...);

关闭源 ​

当您完成使用源时(即不再添加额外的媒体数据),最好尽快关闭该源:

mediaSource.close();

手动关闭源在技术上并非必需,在最终化 Output 时会自动发生。然而,如果您的 Output 包含多个轨道,且并非所有轨道同时停止提供数据(例如先添加所有音频,再添加所有视频),那么提前关闭源可以提高性能并降低内存占用。这是因为 Output 可以更好地“预先规划”,因为它知道不必再等待某些轨道(参见 数据包缓冲)。因此,最佳实践是始终在您完成使用媒体源后立即手动关闭所有媒体源。

背压 ​

媒体源是将从输出管道传播到应用程序逻辑中的背压进行传递的机制。如果编码器或 StreamTarget 的可写部分无法跟上处理速度,Output 可能会应用背压。

背压通过 Promise 由媒体源传达。所有具有 add 方法的媒体源都会返回一个 Promise:

mediaSource.add(...); // => Promise<void>

当源准备好接收更多数据时,此 Promise 会解决。在大多数情况下,Promise 会立即解决,但如果输出管道的某一部分过载,它将保持挂起状态,直到输出准备好继续。因此,通过等待此 Promise,您可以自动将背压传播到您的应用程序逻辑中:

// 错误:
while (notDone) { 
	mediaSource.add(...); 
} 

// 正确:
while (notDone) {
	await mediaSource.add(...);
}

视频编码配置 ​

所有内部处理编码的视频源都要求您指定 VideoEncodingConfig,以指定要使用的编解码器配置:

type VideoEncodingConfig = {
	codec: VideoCodec;
	quality: Quality;
	alpha?: 'discard' | 'keep';
	latencyMode?: 'quality' | 'realtime';
	keyFrameInterval?: number;
	fullCodecString?: string;
	hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
	scalabilityMode?: string;
	contentHint?: string;
	sizeChangeBehavior?: 'deny' | 'passThrough' | 'fill' | 'contain' | 'cover';

	transform?: {
		width?: number;
		height?: number;
		fit?: 'fill' | 'contain' | 'cover';
		rotate?: 0 | 90 | 180 | 270;
		flip?: boolean;
		crop?: { left: number; top: number; width: number; height: number };
		frameRate?: number;
		process?: (sample: VideoSample) => MaybePromise<
			CanvasImageSource | VideoSample | (CanvasImageSource | VideoSample)[] | null
		>;
		force?: boolean;
	};

	onEncodedPacket?: (
		packet: EncodedPacket,
		meta: EncodedVideoChunkMetadata | undefined
	) => unknown;
	onEncoderConfig?: (
		config: VideoEncoderConfig
	) => unknown;
};
  • codec:用于编码的 视频编解码器。
  • quality:期望的编码视频质量。请参阅编码质量。
  • alpha:如何处理视频样本中包含的 Alpha 数据。
    • 'discard'(默认值):仅保留样本的颜色数据;视频为不透明。
    • 'keep':样本的 Alpha 数据也会作为侧边数据进行编码。确保将此模式与支持透明度的容器格式(如 WebM 或 Matroska)配对使用。
  • latencyMode: 由 WebCodecs API 指定的延迟模式。浏览器默认为 quality。由媒体流驱动的音视频源将自动使用 realtime 设置。
  • keyFrameInterval: 两个相邻关键帧之间的最大间隔(秒)。默认为 2 秒。更频繁的关键帧可改善跳转行为,但会增加文件大小。当使用多个视频轨道时,此值应为所有轨道设置为相同的值。
  • fullCodecString: 允许您选择性地指定视频编码器使用的完整编解码器字符串,如 Mediabunny Codec Registry 中所定义。例如,在使用 AVC 时可将其设置为 'avc1.42001f'。请注意,该编解码器字符串仍必须与 codec 中指定的编解码器匹配。如果您未设置此字段,将自动生成一个编解码器字符串。
  • hardwareAcceleration: 用于配置此编解码器硬件加速方法的提示。最好保持为 'no-preference'。
  • scalabilityMode: 由 WebRTC-SVC 定义的编码可扩展性模式标识符。
  • contentHint: 由 mst-content-hint 定义的编码视频内容提示。
  • sizeChangeBehavior: 视频帧的大小可能会随时间变化。此字段控制发生这种情况时的行为。默认为 'deny'。
  • transform: 在将视频帧传递给编码器之前可选应用的变换。
    • width: 将帧调整到的宽度(像素)。如果未设置 height,将根据宽高比自动推导。
    • height: 将帧调整到的高度(像素)。如果未设置 width,将根据宽高比自动推导。
    • fit: 当同时设置了 width 和 height 时使用的适配算法。为避免歧义,当 sizeChangeBehavior 为 'fill'、'contain' 或 'deny' 时,不得设置此字段,因为 sizeChangeBehavior 已经决定了适配算法。
      • 'fill' 会将图像拉伸以填充整个框,可能会改变宽高比。
      • 'contain' 会将整个图像包含在框内并保持宽高比。这可能导致上下黑边。
      • 'cover' 会缩放图像直到填满整个框,同时保持宽高比。
    • rotate: 除每帧自身的旋转外,要顺时针旋转的角度。旋转在翻转之前应用。
    • flip: 是否水平翻转帧(关于垂直轴),除每帧自身的翻转外。翻转在旋转之后但在裁剪和调整大小之前应用。
    • crop: 指定要裁剪的帧的矩形区域。裁剪区域将自动限制在帧的尺寸范围内。裁剪在旋转和翻转之后但在调整大小之前执行。
    • frameRate: 要将视频帧流归一化的帧率(赫兹)。
    • process: 允许对视频帧进行自定义用户定义的处理,例如应用叠加层、颜色变换或时间戳修改。将在变换和帧率校正后对每个视频帧调用。必须返回 VideoSample 或 CanvasImageSource、它们的数组,或者对于丢弃帧则返回 null。当返回非时间戳数据时,将使用输入样本的时间戳和持续时间。
    • force: 即使未定义任何变换属性,也强制每个视频帧通过变换步骤。例如,可用于将旋转和翻转烘焙到编码的视频帧中。
  • onEncodedPacket: 每次成功编码数据包时调用。可用于确定编码进度。
  • onEncoderConfig: 当创建 WebCodecs API 使用的内部编码器配置时调用。您可以使用它来检查完整的编解码器字符串。

音频编码配置 ​

所有在内部处理编码的音频源都需要您指定一个 AudioEncodingConfig,以指定要使用的编解码器配置:

type AudioEncodingConfig = {
	codec: AudioCodec;
	quality?: Quality;
	fullCodecString?: string;

	transform?: {
		numberOfChannels?: number;
		sampleRate?: number;
		process?: (sample: AudioSample) => MaybePromise<
			AudioSample | AudioSample[] | null
		>;
	};

	onEncodedPacket?: (
		packet: EncodedPacket,
		meta: EncodedAudioChunkMetadata | undefined
	) => unknown;
	onEncoderConfig?: (
		config: AudioEncoderConfig
	) => unknown;
};
  • codec: 用于编码的音频编解码器。对于未压缩的 PCM 编解码器,可以省略此项。
  • quality: 期望的编码音频质量;不适用于 PCM 编解码器。参见编码质量。
  • fullCodecString: 允许您选择性地指定音频编码器使用的完整编解码器字符串,如Mediabunny 编解码器注册表中所规定。例如,使用 AAC 时您可以将其设置为 'mp4a.40.2'。请注意,编解码器字符串仍必须与 codec 中指定的编解码器匹配。如果您不设置此字段,将自动生成一个编解码器字符串。
  • transform: 在将音频样本传递给编码器之前要应用的可选转换。
    • numberOfChannels: 期望的上混或下混输出通道数。
    • sampleRate: 期望重采样到的输出采样率(赫兹)。
    • process: 允许对音频样本进行自定义用户定义的处理,例如应用音频效果或修改时间戳。在重采样和重新混音后调用每个音频样本。必须返回一个 AudioSample、它们的数组或用于丢弃该样本的 null。
  • onEncodedPacket: 为每个成功编码的数据包调用。有助于确定编码进度。
  • onEncoderConfig: 当 WebCodecs API 使用的内部编码器配置创建时调用。您可以使用它来检查完整的编解码器字符串。

编码质量 ​

Mediabunny 提供了 Quality 类,用于描述期望的编码质量,即媒体应被压缩的程度。

定性质量 ​

定性质量是抽象定义的,并会根据编解码器和媒体参数(如视频尺寸)自动映射到底层比特率或量化值。

import { Quality } from 'mediabunny';

// 可以从这五个预定义值之一创建 Quality
new Quality('very-low');
new Quality('low');
new Quality('medium');
new Quality('high');
new Quality('very-high');

// 或者从 0 到 1 的质量值创建。此处,0 代表最差质量,
// 1 代表最佳质量。
new Quality(0.6);

定量质量 ​

定量质量允许对压缩参数进行显式控制。

import { Quality } from 'mediabunny';

// 1 Mbps 比特率(可变)
new Quality({
	bitrate: 1e6,
});

// 1 Mbps 比特率(恒定)
new Quality({
	bitrate: 1e6,
	bitrateMode: 'constant',
});

// 要使用的特定量化值。有效值范围取决于每个
// 编解码器,并在 Mediabunny 编解码器注册表中定义。如果量化
// 模式不可用则抛出异常。
new Quality({
	quantizer: 32,
});

// 结合量化值和比特率,其中比特率用作后备
new Quality({
	quantizer: 32,
	bitrate: 1e6,
});

视频源 ​

视频源向 Output 上的视频轨道提供数据。它们都扩展了抽象的 VideoSource 类。

VideoSampleSource ​

此源接收视频样本,对其进行编码,并将编码后的数据传递给输出。

import { VideoSampleSource, Quality } from 'mediabunny';

const sampleSource = new VideoSampleSource({
	codec: 'avc',
	quality: new Quality({ bitrate: 1e6 }),
});

await sampleSource.add(videoSample);
videoSample.close(); // 如果不再需要

// 您可以选择强制将样本编码为关键帧:
await sampleSource.add(videoSample, { keyFrame: true });

CanvasSource ​

此源简化了一个常见模式:在渲染循环中重复更新单个画布,并将每一帧添加到输出文件中。

import { CanvasSource, Quality } from 'mediabunny';

const canvasSource = new CanvasSource(canvasElement, {
	codec: 'av1',
	quality: new Quality('medium'),
});

await canvasSource.add(0.0, 0.1); // 时间戳,持续时间(秒)
await canvasSource.add(0.1, 0.1);
await canvasSource.add(0.2, 0.1);

// 您可以选择强制将帧编码为关键帧:
await canvasSource.add(0.3, 0.1, { keyFrame: true });

MediaStreamVideoTrackSource ​

这是用于媒体捕获和流 API的源。如果您希望将实时视频源(如网络摄像头或屏幕录制)管道传输到输出文件,请使用此源。

import { MediaStreamVideoTrackSource, Quality } from 'mediabunny';

// 获取用户的屏幕
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
const videoTrack = stream.getVideoTracks()[0];

const videoTrackSource = new MediaStreamVideoTrackSource(videoTrack, {
	codec: 'vp9',
	quality: new Quality({ bitrate: 1e7 }),
});

// 确保任何内部错误都能正确冒泡
videoTrackSource.errorPromise.catch((error) => ...);

此源不需要额外的方法调用;一旦在 Output 上调用 start(),数据将自动被捕获并管道传输到输出文件。如果您不再需要用户的媒体,请在完成 Output 后停止 videoTrack 的 stop()。

如果您想暂时停止从此源捕获视频帧,可以使用 pause() 和 resume() 方法:

videoTrackSource.pause();

// 稍后:
videoTrackSource.resume();

暂停期间,流发出的视频帧将被忽略。恢复时,视频帧将再次通过,时间戳经过偏移,以确保回放连续且无中断。注意,暂停不会停止底层媒体流。

如果此源是 Output 中唯一的 MediaStreamTrack 源,则它添加的第一个视频样本的时间戳为 0。如果有多个源,则所有轨道中最早的视频样本时间戳为 0,且所有轨道将彼此完美同步。

警告

MediaStreamVideoTrackSource 的内部机制脱离了典型代码流程,但仍可能抛出异常,因此请务必使用 errorPromise 处理任何错误并停止 Output。

EncodedVideoPacketSource ​

这是最基础的视频源,可用于直接将 编码后的数据包 中的视频数据管道传输到输出端。使用此源需要您自行处理编码过程,这使您可以手动使用 WebCodecs API 或接入自己的编码栈。或者,您也可以直接从另一个媒体文件中读取编码后的数据包,从而跳过对视频数据的解码和重新编码。

import { EncodedVideoPacketSource } from 'mediabunny';

// 您必须指定编解码器名称:
const packetSource = new EncodedVideoPacketSource('vp9');

await packetSource.add(packet1);
await packetSource.add(packet2);

重要提示

您必须按解码顺序添加数据包。

在您首次调用 add 时,您需要提供额外的元数据,以便 Output 了解更多关于视频数据形状和格式的信息。这些元数据必须以 WebCodecs API 的 EncodedVideoChunkMetadata 形式呈现。其内容可能如下所示:

await packetSource.add(firstPacket, {
	decoderConfig: {
		codec: 'vp09.00.31.08',
		codedWidth: 1280,
		codedHeight: 720,
		colorSpace: {
			primaries: 'bt709',
			transfer: 'iec61966-2-1',
			matrix: 'smpte170m',
			fullRange: false,
		},
		description: undefined,
	},
});

codec、codedWidth 和 codedHeight 对所有编解码器都是必需的,而 description 对某些编解码器是必需的。其他字段(如 colorSpace)是可选的。Mediabunny 编解码器注册表 规定了每种视频编解码器的 codec 和 description 的格式,您必须严格遵守。

B 帧 ​

某些视频编解码器使用 B 帧,即需要前一个和后一个帧才能解码的帧。例如,您可能遇到以下情况:

Frame 1: 0.0s, I-frame (key frame)
Frame 2: 0.1s, B-frame
Frame 3: 0.2s, P-frame

这些帧的解码顺序为:

Frame 1 -> Frame 3 -> Frame 2

某些文件格式具有明确的“解码时间戳”和“显示时间戳”概念,用于模拟 B 帧或乱序解码。然而,Mediabunny 数据包仅指定其 显示时间戳。解码顺序由您添加数据包的顺序决定,因此在我们的示例中,您必须按以下方式添加数据包:

await packetSource.add(packetForFrame1); // 0.0s
await packetSource.add(packetForFrame3); // 0.2s
await packetSource.add(packetForFrame2); // 0.1s

您允许提供时间戳顺序差异极大的显示时间戳序列,但存在一个硬性约束:

重要提示

您添加的数据包的时间戳不得小于在添加最后一个关键帧之前添加的最大时间戳。

这个规则表述较长,以下示例希望能帮助您理解:

# 合法:
Packet 1: 0.0s, key frame
Packet 2: 0.3s, delta frame
Packet 3: 0.2s, delta frame
Packet 4: 0.1s, delta frame
Packet 5: 0.4s, key frame
Packet 6: 0.5s, delta frame

# 也合法:
Packet 1: 0.0s, key frame
Packet 2: 0.3s, delta frame
Packet 3: 0.2s, delta frame
Packet 4: 0.1s, delta frame
Packet 5: 0.4s, key frame
Packet 6: 0.35s, delta frame
Packet 7: 0.3s, delta frame
Packet 8: 0.5s, delta frame

# 非法:
Packet 1: 0.0s, key frame
Packet 2: 0.3s, delta frame
Packet 3: 0.2s, delta frame
Packet 4: 0.1s, delta frame
Packet 5: 0.4s, key frame
Packet 6: 0.25s, delta frame

音频源 ​

音频源向 Output 上的音频轨道提供数据。它们都继承自抽象类 AudioSource。

AudioSampleSource ​

此源接收 音频样本,对其进行编码,并将编码后的数据传递给输出端。

import { AudioSampleSource, Quality } from 'mediabunny';

const sampleSource = new AudioSampleSource({
	codec: 'aac',
	quality: new Quality({ bitrate: 128e3 }),
});

await sampleSource.add(audioSample);
audioSample.close(); // 如果不再需要

AudioBufferSource ​

此源直接接受 AudioBuffer 实例作为数据,简化了与 Web Audio API 的配合使用。第一个 AudioBuffer 将在时间戳 0 处播放,任何后续的 AudioBuffer 都将追加在所有之前的 AudioBuffer 之后。

import { AudioBufferSource, Quality } from 'mediabunny';

const bufferSource = new AudioBufferSource({
	codec: 'opus',
	quality: new Quality('medium'),
});

await bufferSource.add(audioBuffer1);
await bufferSource.add(audioBuffer2);
await bufferSource.add(audioBuffer3);

MediaStreamAudioTrackSource ​

这是一个用于 Media Capture and Streams API 的源。如果您希望将实时音频源(例如麦克风或来自用户计算机的音频)管道传输到输出文件,请使用此源。

import { MediaStreamAudioTrackSource, Quality } from 'mediabunny';

// 获取用户的麦克风
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const audioTrack = stream.getAudioTracks()[0];

const audioTrackSource = new MediaStreamAudioTrackSource(audioTrack, {
	codec: 'opus',
	quality: new Quality({ bitrate: 128e3 }),
});

// 确保任何内部错误都能正确冒泡
audioTrackSource.errorPromise.catch((error) => ...);

此源不需要额外的方法调用;一旦在 Output 上调用 start(),数据将自动被捕获并管道传输到输出文件。如果在完成 Output 后不再需要用户的媒体,请记得对 audioTrack 调用 stop()。

如果您想暂时停止从此源捕获音频数据,可以使用 pause() 和 resume() 方法:

audioTrackSource.pause();

// 稍后:
audioTrackSource.resume();

暂停期间,流发出的音频数据将被忽略。恢复时,音频数据再次通过,时间戳进行偏移,以确保结果连续播放且播放无间隙。注意,暂停 不会 停止底层媒体流。

如果此源是 Output 中唯一的 MediaStreamTrack 源,则它添加的第一个音频样本从时间戳 0 开始。如果有多个源,则所有轨道中最早的媒体样本从时间戳 0 开始,并且所有轨道将彼此完美同步。

警告

MediaStreamAudioTrackSource 的内部机制脱离了典型的代码流程,但仍可能抛出异常,因此请务必使用 errorPromise 处理任何错误并停止 Output。

EncodedAudioPacketSource ​

这是所有音频源中最基础的一个,此源可用于直接将音频数据的 编码包 管道传输到输出。此源要求您自行处理编码过程,这使您可以手动使用 WebCodecs API 或插入自己的编码堆栈。或者,您可以通过从另一个媒体文件中读取直接获取编码包,从而跳过解码和重新编码音频数据。

import { EncodedAudioPacketSource } from 'mediabunny';

// 您必须指定编解码器名称:
const packetSource = new EncodedAudioPacketSource('aac');

await packetSource.add(packet);

在您第一次调用 add 时,需要提供附加元数据,以便向 Output 提供更多关于音频数据形状和形式的信息。此元数据必须是 WebCodecs API 的 EncodedAudioChunkMetadata 格式。它可能如下所示:

await packetSource.add(firstPacket, {
	decoderConfig: {
		codec: 'mp4a.40.2',
		numberOfChannels: 2,
		sampleRate: 48000,
		description: new Uint8Array([17, 144]),
	},
});

codec、numberOfChannels 和 sampleRate 是所有编解码器必需的,而 description 对于某些编解码器是必需的。Mediabunny Codec Registry 规定了每种音频编解码器的 codec 和 description 格式,您必须遵守这些规定。

字幕源 ​

字幕源将数据馈送到 Output 的字幕轨道。它们都扩展了抽象的 SubtitleSource 类。

TextSubtitleSource ​

此源从定义字幕的文本文件向输出馈送字幕提示。

import { TextSubtitleSource } from 'mediabunny';

const textSource = new TextSubtitleSource('webvtt');

const text = 
`WEBVTT

00:00:00.000 --> 00:00:02.000
这是你最后的机会。

00:00:02.500 --> 00:00:04.000
在此之后,就没有回头的路了。

00:00:04.500 --> 00:00:06.000
如果你服用蓝色药丸,故事就结束了。

00:00:06.500 --> 00:00:08.000
你在床上醒来,相信你想相信的任何事情。

00:00:08.500 --> 00:00:10.000	
如果你服用红色药丸,你就留在仙境里

00:00:10.500 --> 00:00:12.000
我将向你展示兔子洞有多深。
`;

await textSource.add(text);

如果您一次性添加整个字幕文件,请在添加后立即关闭源:

textSource.close();

您也可以逐个小批量地添加提示:

import { TextSubtitleSource } from 'mediabunny';

const textSource = new TextSubtitleSource('webvtt');

await textSource.add('WEBVTT\n\n');
await textSource.add('00:00:00.000 --> 00:00:02.000\n你好!\n\n');
await textSource.add('00:00:02.500 --> 00:00:04.000\n块状块。\n\n');

这些块有一定的约束:一个提示必须完全包含在一个块内,不能跨多个较小的块拆分(尽管一个块可以包含多个提示)。此外,WebVTT 前缀必须首先一次性添加。

为了让 QuickTime 显示 WebVTT 字幕,通常需要指定对齐信息:

00:00:00.000 --> 00:00:02.000 align:center
这是你最后的机会。