处理视频流组件。
现代网络技术提供了多种处理视频的方法。 Media Stream API、 Media Recording API: Media Source API、 和 WebRTC API 相加 功能丰富的工具集,用于录制、传输和播放视频串流。 虽然处理某些高级任务,但这些 API 不允许 Web 编程人员处理视频流的各个组件,例如帧、 以及未混用的已编码视频或音频块 为了获取这些基本组件的低级别访问权限,开发者一直使用 WebAssembly 将视频和音频编解码器引入到浏览器中。但考虑到 现代浏览器已经附带了多种编解码器(通常 通过硬件加速),将它们重新打包为 WebAssembly 似乎是一种浪费, 人力和计算机资源。
WebCodecs API 可消除这种低效问题 让程序员能够使用 。具体而言:
- 视频和音频解码器
- 视频和音频编码器
- 原始视频帧
- 图像解码器
WebCodecs API 对于需要完全控制 媒体内容的处理方式,例如视频编辑器、视频会议、视频 流式传输等
视频处理工作流程
帧是视频处理的核心。因此,在 WebCodecs 中,大多数类 消耗或生成帧视频编码器会将帧转换为经过编码的帧, 数据块。视频解码器执行相反操作。
此外,VideoFrame
也可作为 CanvasImageSource
并具有接受 CanvasImageSource
的构造函数,从而与其他 Web API 完美配合。
因此,它可以在 drawImage()
和 texImage2D()
等函数中使用。此外,它还可以由画布、位图、视频元素和其他视频帧构成。
WebCodecs API 可与 Insertable Streams API 中的类配合使用 用于将 WebCodecs 连接到媒体流轨道。
MediaStreamTrackProcessor
将媒体轨道拆分为单独的帧。MediaStreamTrackGenerator
用于根据帧流创建媒体轨道。
WebCodecs 和 Web Worker
从设计上讲,WebCodecs API 可以异步完成所有在主线程外完成的繁重工作。 但由于帧和区块回调通常一秒可以调用多次, 它们可能会使主线程变得杂乱无章,导致网站响应速度变慢 因此,最好将对各个帧和编码区块的处理移至 Web Worker。
为了帮助你,ReadableStream
提供了一种便捷方式,可以自动传输来自某个媒体的所有帧。
传递给 worker。例如,MediaStreamTrackProcessor
可用于获取
针对来自摄像头的媒体流曲目的 ReadableStream
。之后
流被传输到 Web Worker,其中帧会逐个读取并加入队列
转换为 VideoEncoder
。
借助 HTMLCanvasElement.transferControlToOffscreen
,可以在主线程外完成渲染。但是,如果所有高级工具
VideoFrame
本身可转让,
在工作器之间移动。
WebCodecs 的实际运用
编码
<ph type="x-smartling-placeholder">这一切都以 VideoFrame
开始。
构建视频帧有三种方法。
从画布、图片位图或视频元素等图片来源提取。
const canvas = document.createElement("canvas"); // Draw something on the canvas... const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
使用
MediaStreamTrackProcessor
从MediaStreamTrack
中拉取帧const stream = await navigator.mediaDevices.getUserMedia({…}); const track = stream.getTracks()[0]; const trackProcessor = new MediaStreamTrackProcessor(track); const reader = trackProcessor.readable.getReader(); while (true) { const result = await reader.read(); if (result.done) break; const frameFromCamera = result.value; }
在
BufferSource
中根据二进制像素表示法创建帧const pixelSize = 4; const init = { timestamp: 0, codedWidth: 320, codedHeight: 200, format: "RGBA", }; const data = new Uint8Array(init.codedWidth * init.codedHeight * pixelSize); for (let x = 0; x < init.codedWidth; x++) { for (let y = 0; y < init.codedHeight; y++) { const offset = (y * init.codedWidth + x) * pixelSize; data[offset] = 0x7f; // Red data[offset + 1] = 0xff; // Green data[offset + 2] = 0xd4; // Blue data[offset + 3] = 0x0ff; // Alpha } } const frame = new VideoFrame(data, init);
无论帧来自何处,都可以将其编码为
具有 VideoEncoder
的 EncodedVideoChunk
对象。
在编码之前,需要为 VideoEncoder
提供两个 JavaScript 对象:
- 包含两个函数的 init 字典,用于处理编码区块和
错误。这些函数由开发者定义,一经设置便无法更改
它们会传递给
VideoEncoder
构造函数。 - 编码器配置对象,其中包含输出的参数
视频流您稍后可以通过调用
configure()
更改这些参数。
如果未配置,configure()
方法将抛出 NotSupportedError
。建议您调用静态方法
将 VideoEncoder.isConfigSupported()
替换为要事先检查的配置
配置受支持并等待其 promise。
const init = {
output: handleChunk,
error: (e) => {
console.log(e.message);
},
};
const config = {
codec: "vp8",
width: 640,
height: 480,
bitrate: 2_000_000, // 2 Mbps
framerate: 30,
};
const { supported } = await VideoEncoder.isConfigSupported(config);
if (supported) {
const encoder = new VideoEncoder(init);
encoder.configure(config);
} else {
// Try another config.
}
编码器设置完毕后,即可通过 encode()
方法接受帧。
configure()
和 encode()
都会立即返回,而不会等待
实际工作。它允许多个帧排队等待编码,
而 encodeQueueSize
显示队列中正在等待的请求数量
让之前的编码完成。
系统会通过立即抛出异常来报告错误,如果参数
或者方法调用的顺序违反了 API 协定,或者是调用 error()
回调。
如果编码成功完成,output()
使用新编码区块作为参数来调用回调函数。
这里的另一个重要细节是,当帧处于非活动状态时,
(即通过调用 close()
延长需要的时间)。
let frameCounter = 0;
const track = stream.getVideoTracks()[0];
const trackProcessor = new MediaStreamTrackProcessor(track);
const reader = trackProcessor.readable.getReader();
while (true) {
const result = await reader.read();
if (result.done) break;
const frame = result.value;
if (encoder.encodeQueueSize > 2) {
// Too many frames in flight, encoder is overwhelmed
// let's drop this frame.
frame.close();
} else {
frameCounter++;
const keyFrame = frameCounter % 150 == 0;
encoder.encode(frame, { keyFrame });
frame.close();
}
}
最后,是时候编写一个用于处理代码的函数, 从编码器传出的经过编码的视频块。 通常情况下,此函数会通过网络发送数据块或将数据块复用到媒体中 存储数据
function handleChunk(chunk, metadata) {
if (metadata.decoderConfig) {
// Decoder needs to be configured (or reconfigured) with new parameters
// when metadata has a new decoderConfig.
// Usually it happens in the beginning or when the encoder has a new
// codec specific binary configuration. (VideoDecoderConfig.description).
fetch("/upload_extra_data", {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: metadata.decoderConfig.description,
});
}
// actual bytes of encoded data
const chunkData = new Uint8Array(chunk.byteLength);
chunk.copyTo(chunkData);
fetch(`/upload_chunk?timestamp=${chunk.timestamp}&type=${chunk.type}`, {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: chunkData,
});
}
如果您需要确保所有待处理的编码请求
则可以调用 flush()
并等待其 promise。
await encoder.flush();
解码
<ph type="x-smartling-placeholder">设置 VideoDecoder
的操作与
VideoEncoder
:在创建解码器时传递两个函数,
参数提供给 configure()
。
编解码器参数集因编解码器而异。例如 H.264 编解码器
可能需要一个二进制 blob
AVCC 的编码,除非它采用所谓的附录 B 格式 (encoderConfig.avc = { format: "annexb" }
) 进行编码。
const init = {
output: handleFrame,
error: (e) => {
console.log(e.message);
},
};
const config = {
codec: "vp8",
codedWidth: 640,
codedHeight: 480,
};
const { supported } = await VideoDecoder.isConfigSupported(config);
if (supported) {
const decoder = new VideoDecoder(init);
decoder.configure(config);
} else {
// Try another config.
}
解码器初始化后,您可以开始向其提供 EncodedVideoChunk
对象。
如需创建分块,您需要:
- 编码视频数据的
BufferSource
- 分块的开始时间戳(以微秒为单位,分块中第一个编码帧的媒体时间)
- 分块的类型,其中包括:
<ph type="x-smartling-placeholder">
- </ph>
- 如果数据块可独立于之前的分块进行解码,则为
key
- 如果只能在前面的一个或多个分块完成解码后才能对区块进行解码,则使用
delta
- 如果数据块可独立于之前的分块进行解码,则为
此外,编码器发出的所有数据块都按原样准备好供解码器使用。 关于错误报告和异步性质的所有内容 同样适用于解码器。
const responses = await downloadVideoChunksFromServer(timestamp);
for (let i = 0; i < responses.length; i++) {
const chunk = new EncodedVideoChunk({
timestamp: responses[i].timestamp,
type: responses[i].key ? "key" : "delta",
data: new Uint8Array(responses[i].body),
});
decoder.decode(chunk);
}
await decoder.flush();
现在,我们来展示如何在网页上显示新解码的帧。时间是
最好确保解码器输出回调 (handleFrame()
)
就能快速恢复正常在下面的示例中,它只向
等待呈现的帧数
渲染是单独进行的,包含两个步骤:
- 正在等待合适的时间来显示帧。
- 在画布上绘制边框。
当不再需要帧时,调用 close()
以释放底层内存
在垃圾回收器到达之前,这将减少
内存用量
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let pendingFrames = [];
let underflow = true;
let baseTime = 0;
function handleFrame(frame) {
pendingFrames.push(frame);
if (underflow) setTimeout(renderFrame, 0);
}
function calculateTimeUntilNextFrame(timestamp) {
if (baseTime == 0) baseTime = performance.now();
let mediaTime = performance.now() - baseTime;
return Math.max(0, timestamp / 1000 - mediaTime);
}
async function renderFrame() {
underflow = pendingFrames.length == 0;
if (underflow) return;
const frame = pendingFrames.shift();
// Based on the frame's timestamp calculate how much of real time waiting
// is needed before showing the next frame.
const timeUntilNextFrame = calculateTimeUntilNextFrame(frame.timestamp);
await new Promise((r) => {
setTimeout(r, timeUntilNextFrame);
});
ctx.drawImage(frame, 0, 0);
frame.close();
// Immediately schedule rendering of the next frame
setTimeout(renderFrame, 0);
}
开发提示
使用媒体面板 ,以查看媒体日志和调试 WebCodecs。
<ph type="x-smartling-placeholder">演示
以下演示显示了画布中的动画帧是如何呈现的:
- 由
MediaStreamTrackProcessor
以 25fps 的速率捕获到ReadableStream
中 - 已转给 Web Worker
- 编码为 H.264 视频格式
- 再解码为一个视频帧序列,
- 并使用
transferControlToOffscreen()
在第二个画布上渲染
其他演示
另请参阅我们的其他演示:
使用 WebCodecs API
功能检测
如需检查是否支持 WebCodecs,请执行以下操作:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
请注意,WebCodecs API 仅适用于安全上下文,
因此,如果 self.isSecureContext
为 false,检测将失败。
反馈
Chrome 团队希望了解您使用 WebCodecs API 的体验。
向我们介绍 API 设计
API 是否有什么无法按预期运行?或者 是否缺少实现想法的方法或属性?在 对安全模型有疑问或意见?在 相应的 GitHub 代码库,或者添加 您对现有问题的想法
报告实现存在的问题
您在 Chrome 的实现过程中是否发现了错误?还是
与规范不同?在 new.crbug.com 上提交 bug。
请务必提供尽可能多的细节信息,
然后在组件框中输入 Blink>Media>WebCodecs
。
Glitch 非常适用于分享轻松快速的重现问题。
表示对 API 的支持
您打算使用 WebCodecs API 吗?你的公开支持将帮助 让 Chrome 团队确定各项功能的优先级,并向其他浏览器供应商展示 而是为他们提供支持
发送电子邮件至 media-dev@chromium.org 或发推文
发送给 @ChromiumDev(使用 # 标签)
#WebCodecs
并告诉我们您使用它的地点和方式。