此页面收集了展示本库常用操作的一些简短代码片段。

读取文件元数据 ​

import { Input, ALL_FORMATS, BlobSource } from 'mediabunny';

const input = new Input({
	formats: ALL_FORMATS, // 支持所有文件格式
	source: new BlobSource(file), // 假设是一个 File 实例
});

const duration = await input.computeDuration(); // 单位:秒
const allTracks = await input.getTracks(); // 所有轨道列表

// 提取视频元数据
const videoTrack = await input.getPrimaryVideoTrack();
if (videoTrack) {
	await videoTrack.getDisplayWidth(); // 单位:像素
	await videoTrack.getDisplayHeight(); // 单位:像素
	await videoTrack.getRotation(); // 单位:顺时针角度

	// 估算帧率(FPS)
	const frameRateMetrics = await videoTrack.computeFrameRateMetrics();
	const frameRate = frameRateMetrics.bestGuessFrameRate;
}

// 提取音频元数据
const audioTrack = await input.getPrimaryAudioTrack();
if (audioTrack) {
	await audioTrack.getNumberOfChannels();
	await audioTrack.getSampleRate(); // 单位:Hz
}

// 提取元数据标签
const tags = await input.getMetadataTags();
tags.title; // 标题
tags.date; // 发布日期
tags.images[0]; // 封面图
tags.raw['TBPM']; // 自定义标签
// ...
  • 查看 元数据提取示例 以运行此代码。
  • 您不仅可以从 File 实例读取,还可以从其他来源读取——请查看 输入源 了解更多。

读取媒体数据 ​

import {
	Input,
	ALL_FORMATS,
	BlobSource,
	VideoSampleSink,
	AudioSampleSink,
} from 'mediabunny';

const input = new Input({
	formats: ALL_FORMATS,
	source: new BlobSource(file),
});

// 读取视频帧
const videoTrack = await input.getPrimaryVideoTrack();
if (videoTrack) {
	const decodable = await videoTrack.canDecode();
	if (decodable) {
		const sink = new VideoSampleSink(videoTrack);

		// 获取时间戳为 5s 的视频帧
		const videoSample = await sink.getSample(5);
		videoSample.timestamp; // 单位:秒
		videoSample.duration; // 单位:秒

		// 将帧绘制到 canvas 上
		videoSample.draw(ctx, 0, 0);

		// 遍历视频前 30s 的所有帧
		for await (const sample of sink.samples(0, 30)) {
			// ...
		}
	}
}

// 读取音频块
const audioTrack = await input.getPrimaryAudioTrack();
if (audioTrack) {
	const decodable = await audioTrack.canDecode();
	if (decodable) {
		const sink = new AudioSampleSink(audioTrack);

		// 获取时间戳为 5s 的音频块;一段短小的音频
		const audioSample = await sink.getSample(5);
		audioSample.timestamp; // 单位:秒
		audioSample.duration; // 单位:秒
		audioSample.numberOfFrames;

		// 转换为 AudioBuffer 以便与 Web Audio API 配合使用
		const audioBuffer = audioSample.toAudioBuffer();

		// 遍历音频前 30s 的所有样本
		for await (const sample of sink.samples(0, 30)) {
			// ...
		}
	}
}

提取视频缩略图 ​

import {
	Input,
	ALL_FORMATS,
	BlobSource,
	CanvasSink,
} from 'mediabunny';

const input = new Input({
	formats: ALL_FORMATS,
	source: new BlobSource(file),
});

const videoTrack = await input.getPrimaryVideoTrack();
if (videoTrack) {
	const decodable = await videoTrack.canDecode();
	if (decodable) {
		const sink = new CanvasSink(videoTrack, {
			width: 320, // 自动调整缩略图尺寸
		});

		// 获取时间戳为 10s 的缩略图
		const result = await sink.getCanvas(10);
		result.canvas; // HTMLCanvasElement | OffscreenCanvas
		result.timestamp; // 单位:秒
		result.duration; // 单位:秒

		// 生成五个均匀分布的缩略图贯穿整个视频
		const startTimestamp = await videoTrack.getFirstTimestamp();
		const endTimestamp = await videoTrack.computeDuration();
		const timestamps = [0, 0.2, 0.4, 0.6, 0.8].map(
			(t) => startTimestamp + t * (endTimestamp - startTimestamp)
		);

		// 遍历这些时间戳
		for await (const result of sink.canvasesAtTimestamps(timestamps)) {
			// ...
		}
	}
}

提取编码包 ​

import {
	Input,
	ALL_FORMATS,
	BlobSource,
	EncodedPacketSink,
} from 'mediabunny';

const input = new Input({
	formats: ALL_FORMATS,
	source: new BlobSource(file),
});

const videoTrack = await input.getPrimaryVideoTrack();
if (videoTrack) {
	const sink = new EncodedPacketSink(videoTrack);

	// 获取时间戳为 10s 的数据包
	const packet = await sink.getPacket(10);
	packet.data; // Uint8Array
	packet.type; // 'key' | 'delta'
	packet.timestamp; // 单位:秒
	packet.duration; // 单位:秒

	// 获取最接近时间戳 10s 的关键帧数据包
	const keyPacket = await sink.getKeyPacket(10);

	// 获取后续数据包
	const nextPacket = await sink.getNextPacket(keyPacket);

	// 设置手动解码器
	const decoderConfig = await videoTrack.getDecoderConfig();
	const videoDecoder = new VideoDecoder({
		output: console.log,
		error: console.error,
	});
	videoDecoder.configure(decoderConfig);

	// 按解码顺序遍历所有数据包
	for await (const packet of sink.packets()) {
		videoDecoder.decode(packet.toEncodedVideoChunk());
	}

	await videoDecoder.flush();
}

查看 EncodedPacketSink 以获取完整文档。

创建新的媒体文件 ​

import {
	AudioBufferSource,
	BufferTarget,
	CanvasSource,
	Mp4OutputFormat,
	Output,
	Quality,
} from 'mediabunny';

// Output 代表一个新的媒体文件
const output = new Output({
	format: new Mp4OutputFormat(), // 文件格式
	target: new BufferTarget(), // 写入位置(此处为内存)
});

// 示例:添加由 Canvas 驱动的音视频轨道
const videoSource = new CanvasSource(canvas, {
	codec: 'avc',
	quality: new Quality('high'),
});
output.addVideoTrack(videoSource);

// 示例:添加由 AudioBuffers 驱动的音频轨道
const audioSource = new AudioBufferSource({
	codec: 'aac',
	quality: new Quality('high'),
});
output.addAudioTrack(audioSource);

// 设置一些元数据标签
output.setMetadataTags({
	title: 'My Movie',
	artist: 'Me',
});

await output.start();

// 添加一些视频帧
for (let frame = 0; ...) {
	await videoSource.add(frame / 30, 1 / 30);
}

// 添加一些音频数据
await audioSource.add(audioBuffer1);
await audioSource.add(audioBuffer2);

await output.finalize();

const buffer = output.target.buffer; // 包含最终 MP4 文件的 ArrayBuffer
  • 查看 程序化生成示例 以了解浏览器内视频生成的演示。
  • 您可以创建多种不同格式的文件;请查看 输出格式 以获取完整列表。
  • 媒体数据可以从不同的源添加,请参阅 媒体源。

直接写入磁盘 ​

import {
	Output,
	StreamTarget,
} from 'mediabunny';

// File System API
const handle = await window.showSaveFilePicker();
const writableStream = await handle.createWritable();

const output = new Output({
	// `chunked: true` 用于批量处理磁盘操作
	target: new StreamTarget(writableStream, { chunked: true }),
	// ...
});

// ...

await output.finalize();

// 文件已完全写入磁盘

通过网络流式传输 ​

import {
	Output,
	AppendOnlyStreamTarget,
	Mp4OutputFormat,
} from 'mediabunny';

const { writable, readable } = new TransformStream<Uint8Array, Uint8Array>();

const output = new Output({
	target: new AppendOnlyStreamTarget(writable),
	// 我们必须在此使用仅追加格式,例如分片 MP4
	format: new Mp4OutputFormat({ fastStart: 'fragmented' }),
});

const uploadComplete = fetch('https://example.com/upload', {
	method: 'POST',
	body: readable,
	duplex: 'half',
	headers: {
		'Content-Type': output.format.mimeType,
	},
});

await output.start();

// ...

await output.finalize();
await uploadComplete;
  • 此代码会自动处理慢速网络施加的反压(backpressure)。
  • 阅读有关 仅追加格式 的更多信息,这是此模式的要求。

录制实时媒体 ​

import {
	BufferTarget,
	MediaStreamAudioTrackSource,
	MediaStreamVideoTrackSource,
	Output,
	Quality,
	WebMOutputFormat,
} from 'mediabunny';

const userMedia = await navigator.mediaDevices.getUserMedia({
	video: true,
	audio: true,
});
const videoTrack = userMedia.getVideoTracks()[0];
const audioTrack = userMedia.getAudioTracks()[0];

const output = new Output({
	format: new WebMOutputFormat(),
	target: new BufferTarget(),
});

if (videoTrack) {
	const source = new MediaStreamVideoTrackSource(videoTrack, {
		codec: 'vp9',
		quality: new Quality('medium'),
	});
	output.addVideoTrack(source);
}

if (audioTrack) {
	const source = new MediaStreamAudioTrackSource(audioTrack, {
		codec: 'opus',
		quality: new Quality('medium'),
	});
	output.addAudioTrack(source);
}

await output.start();

// 等待...

await output.finalize();

创建透明视频 ​

import {
	BufferTarget,
	CanvasSource,
	Output,
	Quality,
	WebMOutputFormat,
} from 'mediabunny';

const output = new Output({
	// 使用支持透明度的格式:
	format: new WebMOutputFormat(),
	target: new BufferTarget(),
});

const canvas = new OffscreenCanvas(1280, 720);
const context = canvas.getContext('2d', { alpha: true })!;

const source = new CanvasSource(canvas, {
	codec: 'vp9',
	quality: new Quality('medium'),
	alpha: 'keep', // => 同时编码 Alpha 通道数据
});
output.addVideoTrack(source);

await output.start();

// 添加数据...
await source.add(0, 1 / 30);
// ...

await output.finalize();

检查编码支持情况 ​

import {
	MovOutputFormat,
	getFirstEncodableVideoCodec,
	getFirstEncodableAudioCodec,
	getEncodableVideoCodecs,
	getEncodableAudioCodecs,
} from 'mediabunny';

const outputFormat = new MovOutputFormat();

// 查找给定容器格式的最佳受支持的编解码器
const bestVideoCodec = await getFirstEncodableVideoCodec(
	outputFormat.getSupportedVideoCodecs(),
	// 可选地,通过这些参数进行限制:
	{ width: 1920, height: 1080 },
);
const bestAudioCodec = await getFirstEncodableAudioCodec(
	outputFormat.getSupportedAudioCodecs(),
);

// 查找所有受支持的编解码器
const supportedVideoCodecs = await getEncodableVideoCodecs();
const supportedAudioCodecs = await getEncodableAudioCodecs();

转换文件 ​

import {
	Input,
	Output,
	Conversion,
	ALL_FORMATS,
	BlobSource,
	Mp4OutputFormat,
} from 'mediabunny';

// 查看上面的代码片段以获取更多 Input 和 Output 的示例
const input = new Input({
	formats: ALL_FORMATS,
	source: new BlobSource(file),
});
const output = new Output({
	format: new Mp4OutputFormat(),
	target: new BufferTarget(),
});

const conversion = await Conversion.init({ input, output });
if (!conversion.isValid) {
	// 转换不可行,执行时会报错。
	// 请检查 `discardedTracks` 以获取原因。
	return;
}

// 不会出现在输出中的轨道列表:
conversion.discardedTracks;

conversion.onProgress = (progress) => {
	progress; // 0 到 1 之间的数字(包含两端)
};

await conversion.execute();
// 转换完成

const buffer = output.target.buffer; // 包含最终 MP4 文件的 ArrayBuffer
  • 此代码在可能时会自动进行转复用(复制媒体数据),在必要时进行转码(重新编码媒体数据)。
  • 完整文档请参阅 转换媒体文件。

提取音频 ​

import {
	Input,
	Output,
	Conversion,
	WavOutputFormat,
} from 'mediabunny';

const input = new Input(...);
const output = new Output({
	// 写入 .wav 文件,仅保留音频轨道
	format: new WavOutputFormat(),
	// ...
});

const conversion = await Conversion.init({
	input,
	output,
	audio: {
		sampleRate: 16000, // 重采样至 16 kHz
	},
});
await conversion.execute();
// 转换完成
  • 您可以将音频提取到其他纯音频格式,例如 .mp3、.ogg 甚至 .m4a。请参阅 输出格式。

压缩媒体 ​

import {
	Conversion,
	Input,
	Output,
	Quality,
} from 'mediabunny';

const input = new Input(...);
const output = new Output(...);

const conversion = await Conversion.init({
	input,
	output,
	tracks: 'primary', // 仅保留每种类型的第一个轨道
	video: {
		width: 480, // 调整大小至 480p
		quality: new Quality('low'),
	},
	audio: {
		numberOfChannels: 1, // 重采样为单声道
		quality: new Quality('low'),
	},
	trim: {
		// 我们只保留前 60 秒
		start: 0,
	end: 60,
	},
	tags: {}, // 移除所有元数据标签
});

await conversion.execute();
// 转换完成

添加视频叠加层 ​

import {
	Input,
	Output,
	Conversion,
} from 'mediabunny';

// 例如,让我们加载一个水印图片
const watermark = new Image();
watermark.src = '/watermark.jpg';
await new Promise(resolve => watermark.onload = resolve);

const input = new Input(...);
const output = new Output(...);

let ctx: CanvasRenderingContext2D | null = null;
const conversion = await Conversion.init({
	input,
	output,
	video: {
		process: (sample) => {
			if (!ctx) {
				// 创建一个用于图像合成的画布
				const canvas = new OffscreenCanvas(
					sample.displayWidth,
					sample.displayHeight,
				);
				ctx = canvas.getContext('2d')!;
			}

			ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
			sample.draw(ctx, 0, 0);
			ctx.drawImage(watermark, 32, 32);

			return ctx.canvas;
		},
	},
});

await conversion.execute();
// 转换完成

合并多个文件为一个 ​

import {
	Input,
	Output,
	Conversion,
} from 'mediabunny';

// 让我们从一个文件中获取视频轨道...
const videoInput = new Input(...);
// ...以及从另一个文件中获取音频轨道
const audioInput = new Input(...);

const output = new Output(...);

const videoConversion = await Conversion.init({
	input: videoInput,
	output,
	composable: true, // 确保转换不拥有输出对象
	audio: { discard: true },
});
const audioConversion = await Conversion.init({
	input: audioInput,
	output,
	composable: true,
	video: { discard: true },
});

await output.start();
await Promise.all([
	videoConversion.execute(),
	audioConversion.execute(),
]);
await output.finalize();
// 转换完成

完整文档请参阅 可组合转换。

读取 HLS 播放列表 ​

import { Input, UrlSource, HLS_FORMATS, desc } from 'mediabunny';

const input = new Input({
	source: new UrlSource('https://example.com/master.m3u8'),
	formats: HLS_FORMATS,
});

// 获取所有轨道
const tracks = await input.getTracks();

// 按质量获取视频轨道
const sortedVideoTracks = await input.getVideoTracks({
	sortBy: async track => desc(await track.getDisplayHeight()),
});

// 选择一个质量
const bestVideoTrack = sortedVideoTracks[0]!;
// 获取匹配的音频轨道
const matchingAudioTrack = await bestVideoTrack.getPrimaryPairableAudioTrack();

// HLS 轨道可以像其他任何 Mediabunny InputTrack 一样读取
// ...

const isLive = await bestVideoTrack.isLive();
if (isLive) {
	// 使用刷新间隔轮询一些数据,例如持续时间
	let currentDuration: number | null = null;
	const poll = async () => {
		currentDuration = await bestVideoTrack.getDurationFromMetadata({
			skipLiveWait: true,
		});

		const refreshInterval = await bestVideoTrack.getLiveRefreshInterval();
		if (refreshInterval === null) {
			return; // 不再是直播
		}

		setTimeout(poll, 1000 * refreshInterval);
	};
	await poll();
}

深入指南请参阅 读取 HLS。