网站首页 > 文章精选 正文
大家好,很高兴又见面了,我是"高前端?进阶?",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
前言
现代技术提供了丰富的视频处理方式,比如 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 接口表示流中的单个媒体轨道;通常,这些是音频或视频轨道,但也可能存在其他轨道类型。
- MediaStreamTrackProcessor 将媒体轨道分解为单独的帧。
- MediaStreamTrackGenerator 从帧流创建媒体轨道。
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 });
- 使用 MediaStreamTrackProcessor 从 MediaStreamTrack拉取帧
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;
}
- 从 BufferSource 中的二进制像素创建帧
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 对象:
- 带有两个函数的初始化对象,用于处理编码块和错误。 这些函数是开发人员定义的,在传递给 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) {
// 太多帧要处理,编码器过载,丢弃当前帧
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 对象。 要创建块,您需要:
- 编码视频数据的 BufferSource
- 以微秒为单位的块的开始时间戳(块中第一个编码帧的媒体时间)
- 块的类型,其中之一:
- key 如果块可以独立于以前的块解码
- 增量,如果块只能在一个或多个先前的块被解码后被解码
此外,编码器发出的任何块都可以按原样为解码器准备好。 上面所说的关于错误报告和编码器方法的异步性质的所有内容对于解码器也同样适用。
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-12-30 简单的使用SpringBoot整合SpringSecurity
- 2024-12-30 Spring Security 整合OAuth2 springsecurity整合oauth2+jwt+vue
- 2024-12-30 DeepSeek-Coder-V2震撼发布,尝鲜体验
- 2024-12-30 一个数组一行代码,Spring Security就接管了Swagger认证授权
- 2024-12-30 简单漂亮的(图床工具)开源图片上传工具——PicGo
- 2024-12-30 Spring Boot(十一):Spring Security 实现权限控制
- 2024-12-30 绝了!万字搞定 Spring Security,写得太好了
- 2024-12-30 SpringBoot集成Spring Security springboot集成springsecurity
- 2024-12-30 SpringSecurity密码加密方式简介 spring 密码加密
- 2024-12-30 Spring cloud Alibaba 从入门到放弃
- 最近发表
- 标签列表
-
- newcoder (56)
- 字符串的长度是指 (45)
- drawcontours()参数说明 (60)
- unsignedshortint (59)
- postman并发请求 (47)
- python列表删除 (50)
- 左程云什么水平 (56)
- 计算机网络的拓扑结构是指() (45)
- 稳压管的稳压区是工作在什么区 (45)
- 编程题 (64)
- postgresql默认端口 (66)
- 数据库的概念模型独立于 (48)
- 产生系统死锁的原因可能是由于 (51)
- 数据库中只存放视图的 (62)
- 在vi中退出不保存的命令是 (53)
- 哪个命令可以将普通用户转换成超级用户 (49)
- noscript标签的作用 (48)
- 联合利华网申 (49)
- swagger和postman (46)
- 结构化程序设计主要强调 (53)
- 172.1 (57)
- apipostwebsocket (47)
- 唯品会后台 (61)
- 简历助手 (56)
- offshow (61)