Mediabunny 允许您以极高的控制和效率读取媒体文件。您可以使用它来提取元数据(如时长或分辨率),以及以帧级精确的时间从视频和音频轨道中读取实际的媒体数据。支持许多常用的 输入文件格式。通过使用输入源,可以从多个来源读取数据,例如直接从内存、用户磁盘甚至通过网络。
文件始终以部分方式(“惰性”)读取,这意味着仅读取提取所需信息所需的字节,从而保持高性能和低内存占用。因此,大多数读取数据的方法都是异步的并返回 Promise。
并非所有数据的提取程度都相同。以 compute 前缀而非 get 前缀开头的方法表示库可能需要执行更多工作才能检索请求的数据。
创建新的输入
在 Mediabunny 中读取媒体文件围绕着一个核心类 Input 展开,所有读取操作均从此类开始。Input 的一个实例代表我们要读取的一个媒体文件。
首先创建一个 Input 的新实例。这里我们使用 File 实例来创建它,意味着我们将直接从用户的磁盘读取数据:
import { Input, ALL_FORMATS, BlobSource } from 'mediabunny';
const input = new Input({
formats: ALL_FORMATS,
source: new BlobSource(file),
});source 指定 Input 从中读取数据的位置。有关可用输入源的完整列表,请参阅 Input sources。
formats 指定 Input 应支持的格式列表。此字段主要用于树摇优化:使用 ALL_FORMATS 意味着我们可以加载 Mediabunny 支持的任何格式 的文件,但需要包含每种格式的解析器。如果我们知道只读取 MP3 或 WAVE 文件,那么类似以下内容将大幅减小整体包大小:
import { Input, MP3, WAVE } from 'mediabunny';
const input = new Input({
formats: [MP3, WAVE],
// ....
});如果无法识别文件格式,读取操作将抛出错误。有关可用输入格式的完整列表,请参阅 Input formats。
简单地创建 Input 实例将执行零次读取,实际上没有开销。只有在请求数据时才会读取文件。
读取文件元数据
创建好 Input 实例后,现在可以开始读取文件级别的元数据。
您可以检查 Mediabunny 是否可以读取该文件:
await input.canRead();您可以这样查询文件的具体格式:
await input.getFormat(); // => Mp4InputFormat您可以直接检索文件的完整 MIME 类型,包括轨道编解码器:
await input.getMimeType(); // => 'video/mp4; codecs="avc1.42c032, mp4a.40.2"'使用 computeDuration 获取媒体文件的完整时长(秒):
await input.computeDuration(); // => 1905.4615更具体地说,时长定义为所有轨道中的最大结束时间戳。
如果您只需要近似时长并希望避免昂贵的扫描操作,可以直接从文件元数据中读取(如果可用):
await input.getDurationFromMetadata(); // => number | null如果文件未将其时长作为元数据暴露,则解析为 null。
如果底层媒体是直播的(因为时长尚未可知!),computeDuration 和 getDurationFromMetadata 都将不会解析。要获取直到已知点(直播边缘)的时长,请执行以下操作:
await input.computeDuration(undefined, { skipLiveWait: true });
await input.getDurationFromMetadata(undefined, { skipLiveWait: true });由于并非所有媒体文件都从零时间开始,您还可以检索媒体文件的起始时间戳(秒):
await input.getFirstTimestamp(); // => 0.0Mediabunny 还允许您从媒体文件中读取描述性元数据标签,例如标题、艺术家或封面艺术:
await input.getMetadataTags(); // => MetadataTags更多信息,请参阅 MetadataTags。
读取轨道元数据
提取轨道
您可以像这样提取文件中的所有媒体轨道列表:
await input.getTracks(); // => InputTrack[]还有额外的实用方法可用于检索有用的轨道:
await input.getVideoTracks(); // => InputVideoTrack[]
await input.getAudioTracks(); // => InputAudioTrack[]
await input.getPrimaryVideoTrack(); // => InputVideoTrack | null
await input.getPrimaryAudioTrack(); // => InputAudioTrack | null目前不支持读取字幕轨道。
这些方法接受可选的 InputTrackQuery 参数用于过滤和排序轨道。此查询系统对于具有许多轨道的输入(如 HLS 播放列表)特别有用。asc、desc 和 prefer 辅助函数使得表达排序逻辑变得容易。
import { desc, prefer } from 'mediabunny';
// 获取最高分辨率的视频轨道:
await input.getPrimaryVideoTrack({
sortBy: async track => [
desc(await track.getDisplayWidth()),
desc(await track.getBitrate()), // 如果分辨率相同,优先选择比特率最高的
],
});
// 仅获取英语音频轨道:
await input.getAudioTracks({
filter: async track => await track.getLanguageCode() === 'eng',
});
// 获取英语轨道,但仅在存在时获取:
await input.getAudioTracks({
sortBy: async track => prefer(await track.getLanguageCode() === 'eng'),
});通用轨道元数据
一旦获得 InputTrack,即可开始从中提取元数据。
// 获取此轨道在输入文件中的唯一 ID:
track.id; // => number
// 获取此轨道在所有同类型轨道中的 1-based 索引
//(例如:第一个视频轨道为 1,第二个视频轨道为 2,以此类推):
track.number; // => number
// 检查轨道的类型:
track.type; // => 'video' | 'audio' | 'subtitle';
// 或者,使用以下类型谓词方法:
track.isVideoTrack(); // => boolean
track.isAudioTrack(); // => boolean
// 以 ISO 639-2/T 语言代码的形式检索轨道的语言。
// 如果语言未知,则解析为 'und'(未确定)。
await track.getLanguageCode(); // => string
// 此轨道的用户定义名称。
await track.getName(); // => string | null
// 关于轨道预期用途的信息
//(默认、评论、听力障碍者辅助、视力障碍者辅助等)
await track.getDisposition(); // TrackDisposition
// 轨道的峰值比特率(如果文件元数据已暴露):
await track.getBitrate(); // => number | null
// 轨道的平均比特率(如果文件元数据已暴露):
await track.getAverageBitrate(); // => number | null
// 轨道中的所有数据包是否均为关键数据包:
await track.hasOnlyKeyPackets(); // => boolean
// 轨道是否为实时轨道(意味着仍在添加新的媒体数据):
await track.isLive(); // => boolean
// 如果轨道是实时的,返回预期新数据的间隔秒数:
await track.getLiveRefreshInterval(); // => number | null编解码器信息
您可以查询与轨道编解码器相关的元数据:
await track.getCodec(); // => MediaCodec | null当无法识别轨道的编解码器或 Mediabunny 不支持该编解码器时,此字段为 null。有关受支持编解码器的完整列表,请参阅 Codecs。当 Mediabunny 无法识别格式时,您仍可使用 internalCodecId 字段来确定轨道的编解码器,尽管其格式取决于所使用的容器格式,且未被 Mediabunny 统一化。
您还可以从轨道中提取完整的编解码器参数字符串,如 Mediabunny Codec Registry 中所述:
await track.getCodecParameterString(); // => 'avc1.42001f'即使已知编解码器,也不意味着用户的浏览器能够对其进行解码。要检查可解码性,请使用 canDecode:
await track.canDecode(); // => boolean此检查也会考虑 自定义解码器。
轨道时序信息
您可以按如下方式计算轨道的具体持续时间(秒):
await track.computeDuration(); // => 1902.4615类似于 Input 的持续时间,这等于最后一个样本的结束时间戳。如果 Input 包含多个长度不同的轨道,则轨道的持续时间可能短于 Input 的总持续时间。
您还可以基于文件中的元数据检索近似持续时间:
await track.getDurationFromMetadata(); // => number | null您还可以检索轨道的 起始时间戳(秒):
await track.getFirstTimestamp(); // => 0.041666666666666664这与 持续时间 相反:它是第一个样本的时间戳。
警告
轨道的起始时间戳 不 必须为 0。它通常接近零,但也可能略为正数,甚至略为负数。
正起始时间戳 意味着第一个样本是在整体合成开始后呈现的。如果是视频轨道,您可以选择显示占位图像(如黑屏),或将第一帧作为冻结帧显示,直到第二帧开始。
负起始时间戳 意味着轨道在合成开始前就已经开始;这在实质上意味着媒体数据的一些开头部分被“截断”。建议不要显示具有负时间戳的样本。
另一个与轨道时序信息相关的指标是其 时间分辨率,单位为赫兹:
await track.getTimeResolution(); // => 24直观地说,这是轨道的最大可能“帧率”(假设没有两个样本具有相同的时间戳)。数学上,如果 x 等于轨道的时间分辨率,则该轨道的所有时间戳和持续时间都可以表示为:
kx,k∈Z此字段仅提供轨道帧率的上限。要根据帧确定视频轨道的实际帧率,请计算其帧率指标。
某些轨道(尤其是现场录音轨道)的时间戳是相对于 Unix 纪元(1970 年 1 月 1 日,UTC 午夜)的。换句话说,它们的时间戳就是Unix 时间戳。这允许您将媒体数据映射到墙钟时间的确定点。要查看是否属于这种情况,请使用:
await track.isRelativeToUnixEpoch(); // => boolean数据包统计信息
您可以查询有关轨道编码数据包的聚合统计信息:
await track.computePacketStats(); // => PacketStats
type PacketStats = {
// 数据包总数。
packetCount: number;
// 每秒平均数据包数。
// 对于视频轨道,这将等于平均帧率(FPS)。
averagePacketRate: number;
// 每秒平均比特数。
averageBitrate: number;
};例如,对 Big Buck Bunny 的 1080p 版本的视频轨道运行此操作将返回以下内容:
{
packetCount: 14315,
averagePacketRate: 24,
averageBitrate: 9282573.233670976,
}这意味着该视频轨道总共有 14315 帧,帧率正好为 24 Hz,平均比特率为 ~9.28 Mbps。
注意:这些统计信息并非直接从文件元数据中读取,而是需要计算,因此该方法可能 - 取决于文件 - 需要执行多次读取,并可能需要数百毫秒才能解析。为了加快计算速度,您可以通过向方法传递参数来计算仅部分数据包的聚合统计信息:
await track.computePacketStats(50);这将仅查看前 ~50 个数据包然后返回结果。这对于快速估算比特率非常有用,而无需扫描整个文件。要确定视频轨道的帧率,建议使用帧率指标。
视频轨道元数据
除了通用轨道元数据外,视频轨道还有您可以查询的其他元数据:
// 获取轨道编码样本的原始像素尺寸:
await videoTrack.getCodedWidth(); // => number
await videoTrack.getCodedHeight(); // => number
// 获取经过宽高比调整但尚未旋转后的轨道像素尺寸:
await videoTrack.getSquarePixelWidth(); // => number
await videoTrack.getSquarePixelHeight(); // => number
// 获取经过宽高比调整和旋转后轨道样本的显示像素尺寸:
await videoTrack.getDisplayWidth(); // => number
await videoTrack.getDisplayHeight(); // => number
// 获取轨道帧应顺时针旋转的角度(以度为单位):
await videoTrack.getRotation(); // => 0 | 90 | 180 | 270
// 获取轨道帧在旋转后是否应水平翻转:
await videoTrack.getFlip(); // => boolean
// 获取由旋转和翻转派生的完整变换矩阵:
await videoTrack.getTransformationMatrix(); // => TransformationMatrix
// 获取轨道像素的宽高比(通常为 1:1):
await videoTrack.getPixelAspectRatio(); // => { num: number, den: number }要确定视频轨道的帧率(FPS),请计算其帧率指标。
您可以检索轨道的解码器配置,这是一个来自 WebCodecs API 的 VideoDecoderConfig,用于在 VideoDecoder 中使用:
await videoTrack.getDecoderConfig(); // => VideoDecoderConfig | null如果轨道的编解码器未知,此方法可能解析为 null。
例如,以下是 Big Buck Bunny 的 1080p 版本的解码器配置:
{
codec: 'avc1.4d4029',
codedWidth: 1920,
codedHeight: 1080,
description: new Uint8Array([
// AVCDecoderConfigurationRecord 的字节
1, 77, 64, 41, 255, 225, 0, 22, 39, 77, 64, 41, 169, 24, 15, 0,
68, 252, 184, 3, 80, 16, 16, 27, 108, 43, 94, 247, 192, 64, 1, 0,
4, 40, 222, 9, 200,
]),
}您可以直接检索关于视频色彩空间的信息:
await videoTrack.getColorSpace(); // => VideoColorSpaceInit如果色彩空间信息未知,生成的对象将包含 undefined 值。
您还可以直接检查视频是否具有高动态范围(HDR):
await videoTrack.hasHighDynamicRange(); // => boolean此方法与可用的色彩空间元数据进行比较。如果解析为 true,则视频是 HDR;如果解析为 false,则视频可能是也可能不是 HDR。
帧率指标
您可以计算有关视频轨道帧率(FPS)的指标:
const metrics = await videoTrack.computeFrameRateMetrics(); // => FrameRateMetrics
metrics.bestGuessFrameRate; // => number帧率从不从文件元数据(不可靠)中确定,而是始终直接从实际帧时间戳推断得出。有关更多信息,请参阅 FrameRateMetrics。
默认情况下,此方法探测轨道的前 256 个数据包,这足以进行可靠估计。您可以使用 targetPacketCount 选项控制此数量,但请注意,增加它会使得调用成本更高:
await videoTrack.computeFrameRateMetrics({ targetPacketCount: 1024 });音频轨道元数据
除了通用轨道元数据外,音频轨道还有您可以查询的其他元数据:
// 获取音频通道数:
await audioTrack.getNumberOfChannels(); // => number
// 获取以赫兹为单位的音频采样率:
await audioTrack.getSampleRate(); // => number您可以检索轨道的解码器配置,这是来自 WebCodecs API 的 AudioDecoderConfig,用于在 AudioDecoder 中使用:
await audioTrack.getDecoderConfig(); // => AudioDecoderConfig | null如果轨道的编解码器未知,此方法可能解析为 null。
例如,以下是 AAC 音频轨道的解码器配置:
{
codec: 'mp4a.40.2',
numberOfChannels: 2,
sampleRate: 44100,
description: new Uint8Array([
// AudioSpecificConfig 的字节
17, 144,
]),
}轨道配对能力
Mediabunny 具有“轨道配对能力”的概念。如果两个不同的轨道彼此兼容(即可以一起呈现),则被认为是 可配对的。例如,在普通文件中,视频轨道可以与音频轨道配对。如果有多个音频轨道,视频轨道可以与其中每一个配对,但音频轨道之间不能相互配对;它们并不打算同时播放。
这一概念用于模拟许多轨道格式(如 HLS)中更复杂的轨道配置。在这里,音频轨道可能与所有视频轨道配对,也可能只与其中一个配对;这取决于主播放列表。这些跨轨道关系正是轨道配对能力所描述的。
要查看两个轨道是否可配对,请执行以下操作:
const canPair = trackA.canBePairedWith(trackB);每个轨道还提供实用程序,让您轻松找到其可配对的轨道:
// 这些返回轨道数组
await track.getPairableTracks();
await track.getPairableVideoTracks();
await track.getPairableAudioTracks();
// 这些返回一个轨道或 null
await track.getPrimaryPairableVideoTrack();
await track.getPrimaryPairableAudioTrack();
// 这些解析为布尔值:
await track.hasPairableTrack();
await track.hasPairableVideoTrack();
await track.hasPairableAudioTrack();读取媒体数据
Mediabunny 具有 媒体接收器 的概念,这是从 InputTrack 读取媒体数据的方式。媒体接收器在 API 和抽象级别上有所不同,意味着您可以选择最适合您用例的接收器。
有关接收器的完整列表,请参阅 媒体接收器。
示例
遍历轨道的所有原始编码数据包:
import { EncodedPacketSink } from 'mediabunny';
const videoTrack = await input.getPrimaryVideoTrack();
const sink = new EncodedPacketSink(videoTrack);
for await (const packet of sink.packets()) {
console.log(packet.timestamp);
}这里我们遍历视频轨道的所有样本(帧):
import { VideoSampleSink } from 'mediabunny';
const videoTrack = await input.getPrimaryVideoTrack();
const sink = new VideoSampleSink(videoTrack);
for await (const sample of sink.samples()) {
// 例如,让我们将样本绘制到画布上:
sample.draw(ctx, 0, 0);
}我们还可以以更具体的方式使用此接收器:
// 遍历时间戳 300 秒到 305 秒之间的所有帧
for await (const sample of sink.samples(300, 305)) {
// ...
}
// 获取时间戳 42 秒时显示的帧
await sink.getSample(42);我们可能希望从视频轨道中提取缩略图:
import { CanvasSink } from 'mediabunny';
const videoTrack = await input.getPrimaryVideoTrack();
const sink = new CanvasSink(videoTrack, {
width: 320,
height: 180,
});
const startTimestamp = await videoTrack.getFirstTimestamp();
const endTimestamp = await videoTrack.computeDuration();
// 让我们生成五个等间距的缩略图:
const thumbnailTimestamps = [0, 0.2, 0.4, 0.6, 0.8].map(
(t) => startTimestamp + t * (endTimestamp - startTimestamp),
);
for await (const result of sink.canvasesAtTimestamps(thumbnailTimestamps)) {
// 将 MrBeast 的脸添加到缩略图中
}我们可以遍历音频轨道的一部分并使用 Web Audio API 播放它:
import { AudioBufferSink } from 'mediabunny';
const audioTrack = await input.getPrimaryAudioTrack();
const sink = new AudioBufferSink(audioTrack);
for await (const { buffer, timestamp } of sink.buffers(5, 10)) {
const node = audioContext.createBufferSource();
node.buffer = buffer;
node.connect(audioContext.destination);
node.start(timestamp);
}或者我们可以亲自掌控解码过程:
import { EncodedPacketSink } from 'mediabunny';
const videoTrack = await input.getPrimaryVideoTrack();
const sink = new EncodedPacketSink(videoTrack);
const decoder = new VideoDecoder({
output: console.log,
error: console.error,
});
decoder.configure(await videoTrack.getDecoderConfig());
// 让我们处理从时间戳 37 秒到 50 秒的所有数据包:
let currentPacket = await sink.getKeyPacket(37);
while (currentPacket && currentPacket.timestamp < 50) {
decoder.decode(currentPacket.toEncodedVideoChunk());
currentPacket = await sink.getNextPacket(currentPacket);
}
await decoder.flush();如您所见,媒体接收器非常通用,允许高效、稀疏地读取输入文件中的媒体数据。
释放输入资源
当您不再使用它们时,Input 实例将通过垃圾回收自动清理。但是,有时您可能想要提前释放 Input 及其所有连接的资源,或取消任何正在进行的操作。您可以这样操作:
input.dispose();当 Input 被销毁时,正在进行的读取操作将被取消,所有未来的读取操作将失败,任何打开的解码器将被关闭,并且所有正在进行的媒体接收操作都将被取消。被禁止和取消的操作将抛出 InputDisposedError。
您不应在销毁 Input 后继续使用它。虽然某些操作可能仍然有效,但这并未明确说明,并且在未来的更新中可能会发生变化。
Input 还实现了 Disposable,这意味着您可以将其与 JavaScript 显式资源管理功能(如 using 关键字)一起使用:
{
using input = new Input(...);
// `input` 将被自动销毁
}如果您仅在单个作用域内暂时需要 Input,建议使用 using 而不是 const。
输入源
输入源 决定了 Input 从何处读取数据。
所有源都有一个 read 事件,您可以使用该事件检查正在读取文件的哪些区域:
source.on('read', ({ start, end }) => {
console.log(`Reading byte range [${start}, ${end})`);
});您可以通过 slice 派生一个仅表示现有源子部分的 RangedSource:
// 仅前 1024 字节的源:
const sliced = source.slice(0, 1024);
// 从原始源的第 8192 字节开始的源:
const sliced2 = source.slice(8192);本库提供了一些源:
BufferSource
此源使用内存中的 ArrayBuffer 作为底层数据源。
import { BufferSource } from 'mediabunny';
// 您可以直接从 ArrayBuffer 构造 BufferSource:
const source = new BufferSource(arrayBuffer);
// 或者也来自 Uint8Array:
const source = new BufferSource(uint8Array);此源速度最快,但需要将整个输入文件保留在内存中。
BlobSource
此源由底层的 Blob 对象支持。由于 File 继承自 Blob,因此此源非常适合直接从磁盘读取数据(在浏览器中)。
import { BlobSource } from 'mediabunny';
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
const source = new BlobSource(file);
});BlobSource 接受第二个参数作为附加选项:
type BlobSourceOptions = {
// 缓存允许在内存中持有的最大字节数。默认为 8 MiB。
maxCacheSize?: number;
// 处理在没有待处理读取时发生的错误
handleUnhandledError?: (error: unknown) => unknown;
};UrlSource
此源从远程 URL 获取数据,适用于通过网络读取文件。
import { UrlSource } from 'mediabunny';
const source = new UrlSource('https://example.com/bigbuckbunny.mp4');UrlSource 会根据观察到的访问模式智能地预取数据,以最大限度地减少请求次数和延迟。
警告
如果您在浏览器中使用此源且 URL 位于不同的源上,请确保已正确配置 CORS。
UrlSource 接受其第二个参数作为几个选项:
type UrlSourceOptions = {
requestInit?: RequestInit;
getRetryDelay?: (previousAttempts: number, error: unknown, url: string | URL | Request) => number | null;
// 缓存允许在内存中持有的最大字节数。默认为 8 MiB。
maxCacheSize?: number;
// 用于获取的最大并行请求数。默认为 2。
parallelism?: number;
// 用于提供自定义 fetch 函数
fetchFn?: typeof fetch;
// 处理在没有待处理读取时发生的错误
handleUnhandledError?: (error: unknown) => unknown;
};您可以像在使用 Fetch API 一样使用 requestInit 来进一步自定义请求:
const source = new UrlSource('https://example.com/bigbuckbunny.mp4', {
requestInit: {
headers: {
'X-Custom-Header': 'my-value',
},
},
});所有 requestInit 字段都会被尊重,除了 signal,它会被 Mediabunny 覆盖。对作为第一个构造函数参数传递的 Request 的 signal 值也适用相同规则。要取消正在进行的请求,请销毁输入。
如果请求失败,可以使用 getRetryDelay 来控制重试逻辑。当请求失败时,getRetryDelay 应返回在重试之前等待的时间(秒)。返回 null 可防止进一步的重试。
// 使用指数退避重试逻辑的 UrlSource:
const source = new UrlSource('https://example.com/bigbuckbunny.mp4', {
getRetryDelay: (previousAttempts) => Math.min(2 ** previousAttempts, 16),
});未设置 getRetryDelay 将导致使用默认值:
- 无限指数退避模式,上限为 16 秒。
- 如果怀疑是 CORS 错误(即使
navigator.onLine为 true 且源不同,fetch()仍拒绝),则不会进行进一步的重试。
使用 fetchFn 提供自定义的 fetch 函数,通常是为了进行 polyfill。例如,React Native 的 fetch 不支持流式响应体,而这是 UrlSource 所必需的功能。在这种情况下,你可以改用 Expo 的 fetch 函数:
import { UrlSource } from 'mediabunny';
import { fetch } from 'expo/fetch';
const source = new UrlSource('https://example.com/bigbuckbunny.mp4', {
fetchFn: (input, init) => {
if (typeof input !== 'string') {
// Expo 需要字符串 URL
throw new Error('Expected a string URL.');
}
return fetch(input, init);
},
});FilePathSource
此输入源可用于通过文件路径直接从文件加载数据。它需要服务器端环境,如 Node、Bun 或 Deno。
import { FilePathSource } from 'mediabunny';
const source = new FilePathSource('/home/david/Downloads/bigbuckbunny.mp4');FilePathSource 接受第二个参数作为额外选项:
type FilePathSourceOptions = {
// 缓存允许在内存中保留的最大字节数。默认为 8 MiB。
maxCacheSize?: number;
// 处理在没有待处理读取时发生的错误
handleUnhandledError?: (error: unknown) => unknown;
};警告
使用此源时,请确保在完成使用后手动释放 Input,以正确关闭该源持有的内部文件句柄。
CustomSource
这是一个通用型输入源,可用于从任何位置读取数据。
例如,这里我们使用 Node.js 文件系统从磁盘读取文件(尽管对于此场景应使用现有的 FilePathSource):
import { CustomSource } from 'mediabunny';
import { open } from 'node:fs/promises';
const fileHandle = await open('bigbuckbunny.mp4', 'r');
const source = new CustomSource({
read: async (start, end) => {
const buffer = Buffer.alloc(end - start);
await fileHandle.read(buffer, 0, end - start, start);
return buffer;
},
getSize: async () => {
const { size } = await fileHandle.stat();
return size;
},
});CustomSource 的选项类型如下:
type CustomSourceOptions = {
getSize: () => MaybePromise<number>;
read: (start: number, end: number) => MaybePromise<Uint8Array | ReadableStream<Uint8Array>>;
dispose?: () => unknown;
maxCacheSize?: number;
prefetchProfile?: 'none' | 'fileSystem' | 'network';
handleUnhandledError?: (error: unknown) => unknown;
};
type MaybePromise<T> = T | Promise<T>;getSize
当请求整个文件大小时被调用。必须返回或解析为以字节为单位的大小。保证在调用read之前会调用此函数。read
当请求数据时被调用。必须返回或解析为指定字节范围内的字节,或一个产生这些字节的流。你保证满足0 <= start < end < fileSize。dispose
当由该源驱动的Input被释放时被调用。maxCacheSize
缓存允许在内存中保留的最大字节数。默认为 8 MiB。prefetchProfile
指定读取器应配合此源使用的预取配置文件。预取配置定义了如何预加载请求范围之外的字节,以减少未来读取的延迟。'none'(默认):不进行预取;仅请求当前时刻所需的数据。'fileSystem':针对文件系统优化的预取:双向预取少量数据,并与页边界对齐。'network':针对网络优化的预取,或更广泛地说,针对任何高延迟环境的优化预取:尝试最小化读取调用次数,并在检测到顺序访问模式时积极预取数据。
handleUnhandledError
处理在没有待处理读取时发生的错误。默认情况下,这些会成为未处理的 promise 拒绝。
CustomSource 以前被称为 StreamSource,并且仍然可以通过该别名使用,但使用 StreamSource 已被弃用。
ReadableStreamSource
这是一个基于 Uint8Array 的 ReadableStream 的源,代表一个长度未知的追加型字节流。这是用于流式传输仍在构建中的输入文件(且其大小尚未可知)时应使用的源。你也可以用它来流式传输现有文件,但建议使用其他源(如 BlobSource 或 FilePathSource),因为它们支持随机访问。
import { ReadableStreamSource } from 'mediabunny';
const { writable, readable } = new TransformStream<Uint8Array, Uint8Array>();
const source = new ReadableStreamSource(readable);
// 追加数据块
const writer = writable.getWriter();
writer.write(chunk1);
writer.write(chunk2);
writer.close();此源是无大小的,意味着调用 .getSize() 将抛出异常,且由于缺乏随机文件访问能力,读取器的功能受到更多限制。你应该仅在顺序访问模式下使用此源,例如从头到尾读取所有数据包或执行转换。除非增加其最大缓存大小,否则此源不适合随机访问模式。
type ReadableStreamSourceOptions = {
// The maximum number of bytes the cache is allowed to hold
// in memory. Defaults to 16 MiB.
maxCacheSize?: number;
// Handles errors that occur while no read is pending
handleUnhandledError?: (error: unknown) => unknown;
};与 MediaRecorder 配合使用
您可以将 MediaRecorder 与 ReadableStreamSource 结合使用,在录制过程中将录制的流式数据传输到 Mediabunny。以下示例展示了如何将 MediaRecorder 的输出管道传输到 Mediabunny 的 Conversion API,以创建 WAVE 文件:
import {
Input,
Output,
Conversion,
ReadableStreamSource,
ALL_FORMATS,
WavOutputFormat,
BufferTarget,
} from 'mediabunny';
// Set up a TransformStream to convert MediaRecorder's Blobs into Uint8Arrays
const { writable, readable } = new TransformStream<Blob, Uint8Array>({
async transform(chunk, controller) {
const arrayBuffer = await chunk.arrayBuffer();
controller.enqueue(new Uint8Array(arrayBuffer));
},
});
const input = new Input({
source: new ReadableStreamSource(readable),
formats: ALL_FORMATS,
});
const output = new Output({
format: new WavOutputFormat(),
target: new BufferTarget(),
});
const conversionPromise = Conversion.init({ input, output })
.then(conversion => conversion.execute());
const micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
const recorder = new MediaRecorder(micStream);
const writer = writable.getWriter();
recorder.ondataavailable = e => writer.write(e.data);
recorder.onstop = async () => {
await writer.close();
await conversionPromise;
// Get the final .wav file
const wavFile = output.target.buffer!; // => ArrayBuffer
};
recorder.start(1000);
setTimeout(() => recorder.stop(), 10_000); // Stop recording after 10sPathedSource
某些媒体格式引用多个文件。例如,HLS 流由一个主播放列表组成,该播放列表指向一个或多个媒体播放列表,而每个媒体播放列表又引用许多媒体分段文件。要读取这种多文件媒体,Mediabunny 需要一种为每个文件路径解析源的方法。
这可以通过 PathedSource 实现。PathedSource 包装了一个根路径(媒体的入口文件)以及一个回调函数,该函数为每个请求的文件路径生成一个 Source。它是一个抽象类,因此不能直接使用,但它提供了读取多文件媒体所需的接口。它由以下类实现:
UrlSourceFilePathSourceCustomPathedSource
CustomPathedSource
允许您实现用户定义的 PathedSource,以提供任意的“文件路径到数据”映射函数。当您的数据存储在一个自定义结构中时非常有用,例如 OPFS:
import { Input, CustomPathedSource, BlobSource } from 'mediabunny';
const root = await navigator.storage.getDirectory();
const input = new Input({
source: new CustomPathedSource(
'master.m3u8',
async ({ path }) => {
const handle = await root.getFileHandle(path);
const file = await handle.getFile();
return new BlobSource(file);
},
),
// ...
});该回调会在每次请求文件时调用一次(惰性调用,仅在需要时),并接收一个 SourceRequest:
type SourceRequest = {
path: FilePath; // The requested file path
isRoot: boolean; // Whether the requested file is the root file
};初始化输入
某些文件格式在单独的文件中包含轨道初始化信息;CMAF 就是一个例子。要将这些提供给 Mediabunny,请将初始化文件作为单独的 Input 加载,然后将其作为 initInput 传递:
const initInput = new Input({
source: new FilePathSource('init.mp4'),
formats: ALL_FORMATS,
});
const input = new Input({
source: new FilePathSource('data.mp4'),
formats: ALL_FORMATS,
initInput,
});