浏览器最新支持的WebCodecs 到底是什么?

大家好,很高兴又见面了,我是"高前端‬进阶‬",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

高前端‬进阶

前言

现代技术提供了丰富的视频处理方式,比如 Media Stream API、Media Recording API、Media Source API 和 WebRTC API 共同组成了一个用于录制、传输和播放视频流的丰富工具集。

在Chrome 94+版本上已经支持了WebCodecs!

在解决某些高级任务时,这些 API 不允许 Web 开发者处理视频流的各个组成部分,例如帧、未混合的编码视频或音频块。 为了获得对这些基本组件的底层访问,开发人员一直在使用 WebAssembly 将视频和音频编解码器引入浏览器。 但鉴于现代浏览器已经附带了各种编解码器,将它们重新打包为 WebAssembly 似乎是对人力和计算机资源的浪费。

WebCodecs API 为开发者提供了一种使用浏览器中已经存在的媒体组件的方法,从而提高了效率。 具体包括以下部分:

WebCodecs API 对于需要完全控制媒体内容处理方式的 Web 应用程序非常有用,例如视频编辑器、视频会议、视频流等。

1.视频处理工作流程

帧是视频处理的核心。 因此,在 WebCodecs 中,大多数类要么消费帧,要么生产帧。 视频编码器将帧转换为编码块,而视频解码器则相反。可以通过如下方法判断浏览器是否支持WebCodecs:

if (window.isSecureContext) {
  // 页面上下文完全,同时serviceWorker加载完成
  navigator.serviceWorker.register("/offline-worker.js").then(() => {
  });
}
if ('VideoEncoder' in window) {
  // 支持WebCodecs API
}

请记住,WebCodecs 仅在安全上下文中可用,因此如果 self.isSecureContext 为 false,检测将失败!

VideoFrame 通过成为 CanvasImageSource 并具有接受 CanvasImageSource 的构造函数,可以很好地与其他 Web API 配合使用。 所以它可以用在 drawImage() 和 texImage2D() 等函数中。 它也可以由画布、位图、视频元素和其他视频帧构建。VideoFrame的构造函数如下:

new VideoFrame(image)
new VideoFrame(image, options)
new VideoFrame(data, options)
// 第二个参数为配置对象

下面是VideoFrame的一个典型示例:

const pixelSize = 4;
const init = {timestamp: 0, codedWidth: 320, codedHeight: 200, format: 'RGBA'};
let data = new Uint8Array(init.codedWidth * init.codedHeight * pixelSize);
for (let x = 0; x < init.codedWidth; x++) {
  for (let y = 0; y < init.codedHeight; y++) {
    let 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
  }
}
let frame = new VideoFrame(data, init);

WebCodecs API 与 Insertable Streams API 中的类协同工作,后者将 WebCodecs 连接到媒体流轨道(Media Stream Tracks)。

MediaStreamTrack 接口表示流中的单个媒体轨道;通常,这些是音频或视频轨道,但也可能存在其他轨道类型。

2.WebCodecs 和Web Worker

根据设计,WebCodecs API 异步完成所有繁重的工作并脱离主线程。 但是由于框架和块回调通常可以每秒调用多次,它们可能会使主线程混乱,从而降低网站的响应速度。 因此,最好将单个帧和编码块的处理转移到Web Worker中。

为此,ReadableStream 提供了一种方便的方法来自动将来自媒体轨道的所有帧传输到工作程序。 例如,MediaStreamTrackProcessor 可用于获取来自网络摄像头的媒体流轨道的 ReadableStream。 之后,流被传输到Web Worker,其中帧被一个一个地读取并排队进入 VideoEncoder。

Streams API 的 ReadableStream 接口表示字节数据的可读流。 Fetch API 通过 Response 对象的 body 属性提供了 ReadableStream 的具体实例。

使用
HTMLCanvasElement.transferControlToOffscreen 甚至可以在主线程之外完成渲染。如果所有高级工具都不符合要求,VideoFrame 本身是可转移的,可以在Web Worker之间移动。

2.编码

这一切都始于 VideoFrame,可以通过三种方式构建视频帧。

const canvas = document.createElement("canvas");
// 在Canvas中绘制
const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
const stream = await navigator.mediaDevices.getUserMedia({
  audio: true,
  video: {
    width: { min: 1024, ideal: 1280, max: 1920 },
    height: { min: 576, ideal: 720, max: 1080 }
  }
});
// 获取媒体帧的配置:https://developer.mozilla.org/en-US/docs/Web/API/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;
}
const pixelSize = 4;
const init = {
  timestamp: 0,
  codedWidth: 320,
  codedHeight: 200,
  format: "RGBA",
};
const data = new Uint8Array(init.codedWidth * init.codedHeight * pixelSize);
// 创建Uint8Array对象
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);
// 实例化VideoFrame对象

无论那种方式,都可以使用 VideoEncoder 将帧编码到 EncodedVideoChunk 对象中。在编码之前,需要给 VideoEncoder 两个 JavaScript 对象:

如果浏览器不支持配置,则 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) {
    // 太多帧要处理,编码器过载,丢弃当前帧
    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,
    });
  }
  // 真实编码数据块大小
  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();

3.解码

设置 VideoDecoder 与 VideoEncoder 类似:创建解码器时需要传递两个函数,并将编解码器参数提供给 configure()。

编解码器参数因编解码器而异。 例如,H.264 编解码器可能需要 AVCC 的二进制 blob,除非它以所谓的 Annex 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 对象。 要创建块,您需要:

此外,编码器发出的任何块都可以按原样为解码器准备好。 上面所说的关于错误报告和编码器方法的异步性质的所有内容对于解码器也同样适用。

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() 以在垃圾收集器到达它之前释放底层内存,这将减少 Web 应用程序使用的平均内存量。

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();
  // 立即启动下一帧的调用逻辑
  setTimeout(renderFrame, 0);
}

参考资料


https://developer.chrome.com/articles/webcodecs/(Chrome官方文档)

https://developer.mozilla.org/en-US/docs/Web/API/isSecureContext

https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts

https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream

https://www.w3.org/2020/06/machine-learning-workshop/talks/media_processing_hooks_for_the_web.html

展开阅读全文

页面更新:2024-05-26

标签:进阶   浏览器   编码器   解码器   函数   参数   方法   媒体   编解码器   最新   视频

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top