WebCodecs入门(20 October 2021)
注意
编解码器(VideoDecoder等等)只能在https安全环境下访问!
前言
WebCodecs API允许web应用程序对音频和视频进行编码和解码。
许多WebApi在内部使用媒体编解码器来支持特定用途的api:
-
HTMLMediaElement和MSE
-
WebAudio (decodeAudioData)
-
MediaRecorder
-
WebRTC
但是没有通用的方法来灵活配置和使用这些媒体编解码器。正因为如此,许多web应用都采用JavaScript或WebAssembly实现媒体编解码器,尽管存在一些缺点:
-
增加了下载浏览器中已有的编解码器的带宽。
-
降低性能
-
减少功率效率
WebCodecs API为程序员提供了一种使用浏览器中已经存在的媒体组件的方法,从而消除了这种低效。具体地说:
-
视频和音频解码器
-
视频和音频编码器
-
原始视频帧
-
图像解码器
开始
处理模型
该规范的接口设计是这样的:当以前的任务仍未完成时,可以安排新的编解码器任务。例如,web作者可以调用decode()而无需等待前面的decode()完成。这是通过将底层编解码器任务卸载到单独的线程进行并行执行来实现的。
控制线程和编码器线程
控制线程---- 用户可以初始化和定义编解码器,以及调用其方法;调用其方法时最终会将控制信息作用在解码器线程上;每个全局对象都有一个独立的控制线程;
编码器线程— 是用来取出控制信息并执行的线程,每个编码器实例都有一个单独的线程;编码器的生命周期与编码器线程的生命周期相匹配;
编码器执行循环
-
控制信息队列为空时,继续执行队列
-
从控制信息队列取出前面的信息
-
运行前面信息描述的控制信息步骤
状态控制
state:(只读,可通过方法控制)
- unconfigured // 编解码器还未设置配置,
- configured // 编解码器configure成功后
- closed // 编解码器被关闭后,内存释放
flush() : 结束控制消息队列中的所有控制消息并发出所有输出,返回一个promise,设置关键块为true
close() : 关闭编解码器,释放内存
reset(): 重置编解码器并设置状态为unconfigured
实现步骤
1.解码Decoding
实例化
可以读到的关于解码器的内容(readonly):
state:是否配置了config,closed
decodeQueueSize:编码器的队列长度
const decorderInit = {
output: handleFrame,
error: (e) => {
console.log(e.message);
}
};
const decoder = new VideoDecoder(decorderInit);
decoder.state // 输出 'unconfigured'
- 创建一个解码器对象
- 定义输出帧回调函数
- 定义输出报错信息的回调函数
- 指示传递给decode()的下一个块必须为关键块
- 状态为unconfigured
- 返回解码器对象
配置解码器
我们对码流处理主要在这:
codec: 编解码器
目前提供的有:
video:av01,avc1,vp8,vp09.
audio:mp3,mp4a,opus,vorbis,ulaw,alaw
codedWidth/codedHeight: 标识产出VideoFrame的长宽
displayAspectWidth/displayAspectHeight:视频帧的水平/垂直维度
colorSpace:主要是有关于色彩的配置(不是概念上原色的修改,类似于色域(bt709/bt470bg/smpte170m),是否使用全范围彩色值)
hardwareAcceleration: 支持软硬解的选择
optimizeForLatency:如果为真,这是一个提示,说明所选解码器应该被优化,在VideoFrame输出之前必须被解码的EncodedVideoChunk对象的数量最小化。
- 可用VideoDecoder.isConfigSupported来检验config是否符合,返回的是一个promise
const config = {
codec: 'avc1.42002a',
codedWidth: 1920,
codedHeight: 1080,
hardwareAcceleration: 'no-preference',
};
decoder.configure(config);
decoder.state // 输出 'configured'
开始解码
- 编码视频数据的BufferSource
- 数据块的开始时间戳(以微秒为单位)(数据块中第一个编码帧的媒体时间)
- 数据块是否为关键块
const chunk = new EncodedVideoChunk({
timestamp: data.pts,
type: data.flag ? 'key' : 'delta',
data: data.data
});
decoder.decode(chunk);
渲染
-
等待合适的时机来展示frame
-
绘制到canvas上
let cnv = document.getElementById('canvas_to_render');
let ctx = cnv.getContext('2d');
let ready_frames = [];
let underflow = true;
let time_base = 0;
function handleFrame(frame) {
ready_frames.push(frame);
if (underflow)
setTimeout(render_frame, 0);
}
function delay(time_ms) {
return new Promise((resolve) => {
setTimeout(resolve, time_ms);
});
}
function calculateTimeTillNextFrame(timestamp) {
if (time_base == 0)
time_base = performance.now();
let media_time = performance.now() - time_base;
return Math.max(0, (timestamp / 1000) - media_time);
}
async function render_frame() {
if (ready_frames.length == 0) {
underflow = true;
return;
}
let frame = ready_frames.shift();
underflow = false;
// Based on the frame's timestamp calculate how much of real time waiting
// is needed before showing the next frame.
let time_till_next_frame = calculateTimeTillNextFrame(frame.timestamp);
await delay(time_till_next_frame);
ctx.drawImage(frame, 0, 0);
frame.close();
// Immediately schedule rendering of the next frame
setTimeout(render_frame, 0);
}
2.编码Encoding
实例化、配置编码器
const encorderInit = {
output: handleChunk,
error: (e) => {
console.log(e.message);
}
};
const config = {
codec: 'vp8',
width: 640,
height: 480,
bitrate: 2000000, // 2 Mbps 编码视频的平均比特率,以比特/秒为单位
framerate: 30,
latencyMode:'quality' // 牺牲延迟换取质量,与’realtime'相对
};
const encoder = new VideoEncoder(encorderInit);
encoder.configure(config);
开始编码
- 允许多个frames同时进入队列编码,通过encodeQueueSize查看等待的个数
- frames不需要时通过close告知
let frame_counter = 0;
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// MediaDevices.getUserMedia(), MediaDevices.getDisplayMedia(), HTMLCanvasElement.captureStream().
const track = stream.getVideoTracks()[0];
const media_processor = new MediaStreamTrackProcessor(track);
const reader = media_processor.readable.getReader();
while (true) {
const result = await reader.read();
if (result.done)
break;
let frame = result.value;
if (encoder.encodeQueueSize > 2) {
// Too many frames in flight, encoder is overwhelmed
// let's drop this frame.
frame.close();
} else {
frame_counter++;
const insert_keyframe = (frame_counter % 150) == 0;
encoder.encode(frame, { keyFrame: insert_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
let chunkData = new Uint8Array(chunk.byteLength);
chunk.copyTo(chunkData);
let timestamp = chunk.timestamp; // media time in microseconds
let is_key = chunk.type == 'key'; // can also be 'delta'
fetch(`/upload_chunk?timestamp=${timestamp}&type=${chunk.type}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: chunkData
});
}
音视频编解码转换
通用转换
MediaStreamTrackGenerator / MediaStreamTrackProcessor
// 支持VideoFrame,AudioData
解码:MediaStreamTrackGenerator
const audio = document.createElement('audio');
audio.style.display = 'none';
audio.autoplay = true;
document.body.appendChild(audio);
const generator = new MediaStreamTrackGenerator({
kind: 'audio'
});
const {
writable
} = generator;
const writer = writable.getWriter();
const mediaStream = new MediaStream([generator]);
audio.srcObject = mediaStream;
async function handledFrame(frame) {
await writer.write(frame);
frame.close();
}
.....
// output
const decorderInit = {
output: handledFrame,
error: (e) => {
console.log(e.message);
}
}
编码:MediaStreamTrackProcessor
// MediaDevices.getUserMedia(), MediaDevices.getDisplayMedia(), HTMLCanvasElement.captureStream(),WebRTC ontrack
const track = captureStream.getAudioTracks()[0];
const media_processor = new MediaStreamTrackProcessor(track);
const reader = media_processor.readable.getReader();
while (true) {
const result = await reader.read();
if (result.done)
break;
let frame = result.value;
encoder.encode(frame);
frame.close();
}
解码数据解析
AudioData
let copyDest = new ArrayBuffer(frame.allocationSize({planeIndex: 0,format:'f32'}));
await frame.copyTo(copyDest, {planeIndex: 0});
// 解出来的arrayBuffer 通过createBuffer转成audioBuffer可以直接读取
addAudioChunk(copyDest)
frame.close();
VideoFrame
// 以format为‘I420'为例
const yuv = [];
let copyDest = new ArrayBuffer(frame.allocationSize());
const dest = new Uint8Array(copyDest);
await frame.copyTo(dest);
yuv.push(dest.slice(0,frame.codedWidth*frame.codedHeight));
yuv.push(dest.slice(dest.length*2/3,dest.length*5/6));
yuv.push(dest.slice(dest.length*5/6,dest.length));
// 将解析出来的yuv数据提供给webgl
renderFrame2(glCanvas, frame.codedWidth, frame.codedHeight, yuv[0],yuv[1],yuv[2])
frame.close();
图像解码
前言
img标签目前不支持第一帧以外的操作,同时也不能控制动画中显示哪个帧,WebCodecs在图像领域提供了类似音视频的编解码器。
WebCodecs提供了一个ImageDecoder 方法,允许我们获取到每一帧,获取的结果是VideoFrame,可以直接作用于Canvas上;
初始化
-
init
一个包含以下成员的对象。
type
甲string
要被解码包含MIME类型的图像文件的。data
一个BufferSource
或ReadableStream
字节,表示由type
描述的编码图像类型。premultiplyAlpha
以下之一,如果未提供,则设置为 { “none”, “premultiply”, “default” };colorSpaceConversion
以下之一,如果未提供,则设置为{ “none”, “default” };desiredWidth
一个整数,表示所需的解码输出宽度。desiredHeight
一个整数,表示解码输出的期望高度。preferAnimation
一个Boolean
指示对于有多个轨道的图像,这表明初始轨道选择是否应该选择动画轨道。
示例:
let init = {
type: "image/png",
data: data // ArrayBuffer
};
let imageDecoder = new ImageDecoder(init);
类似gif的图像,可通过以下方法获取帧数
imageDecoder.tracks.ready.then((res)=>{
console.log( imageDecoder.tracks[0]); // 见下面ImageTrack
console.log('imageDecoder.frameCount = ' + imageDecoder.tracks[0].frameCount);
imageIndex = imageDecoder.tracks[0].frameCount; // 帧数
})
解码decode
frameIndex
Optional
一个整数,表示要解码的帧的索引。默认为 0
(第一帧)。
completeFramesOnly
Optional
默认为 true
的 boolean
。当 false
表示对于渐进式图像时,解码器可能会输出细节减少的图像。
返回:
使用包含以下成员的对象解析的 promise
:
image
VideoFrame
包含的解码图像。
complete
一个 boolean
,如果 true
表示 image
包含最终全细节输出。
示例:
console.log(imageDecoder.decode({frameIndex: 1}));
ImageTrack
- animated – 指示此轨道是否包含具有多个帧的动画图像
- frameCount — 这条轨道上的帧数
- repetitionCount — 动画要重复的次数
- selected — 指示是否选择此轨道进行解码
参考文档
https://w3c.github.io/webcodecs/
示例demo:
一个flv解析到webcodecs解码播放的示例:
展示:https://ltsg123.github.io/flv2webcodecs/
代码:https://github.com/ltsg123/flv2webcodecs