Web硬件编解码-WebCodec

WebCodec 是一个较新的 Web API,它为网页应用提供了接近硬件级的编解码能力。使用 WebCodec,开发者可以在浏览器中直接对音频和视频数据进行高效编解码,这对于实现复杂的媒体应用(如实时视频通信、视频编辑和游戏流)非常有用。

WebCodec 编解码流程

解码

创建解码器

设置解码器名称

WebCodec设置上参数与其它平台不同的一点,在于其名称字段比较繁琐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
   if (VideoType == H264)
{
...
std::stringstream stream;
stream << "avc1.";
stream << decimalToHex(profile_idc);
stream << decimalToHex(constraint_set_flags);
stream << decimalToHex(level_idc);
codecName = stream.str();
// codecName = "avc1.640020";
}else if(VideoType == H265)
{
std::stringstream stream;
stream << "hev1.";
switch (general_profile_space) {
case 0:
break;
case 1:
stream << 'A';
break;
case 2:
stream << 'B';
break;
case 3:
stream << 'C';
break;
}
stream << general_profile_idc;
stream << '.';

MUInt32 val = (MUInt32)general_profile_compatibility_flag;
int reversed = 0;
for (int i = 0; i < 32; i++)
{
reversed |= val & 1;
if (i == 31)
break;
reversed <<= 1;
val >>= 1;
}
stream << decimalToHex(reversed, 0);
stream << '.';
if (general_tier_flag == 0)
{
stream << 'L';
}
else
{
stream << 'H';
}
stream << general_level_idc;
MBool hasByte = false;

std::string constraint_string = "";
for (int i = 5; i >= 0; i--)
{
int val = 0;
if (general_constraint_indicator_flags[i] != '\0')
{
val = general_constraint_indicator_flags[i];
}
if (val != 0 || hasByte)
{
constraint_string = "." + decimalToHex(val, 0) + constraint_string;
hasByte = true;
}
}
stream << constraint_string;
codecName = stream.str();
// codecName = "hev1.1.6.L120.90";
}

创建解码器实例

1
2
3
4
5
6
7
8
9
10
const decoder = new VideoDecoder({
output(frame) {
...
// 解码成功回调
},
error(e) {
...
// 解码失败回调
}
});

配置解码器

WebCodec支持Annex-B格式和avc格式的码流,如果设置了description,则表示传入的是avc格式的码流,如果不传,则表示传入的是Annex-B格式的码流

1
2
3
4
5
6
7
8
9
let config = {
codec: codecName,
codedHeight: height,
codedWidth: width,
description: description,
hardwareAcceleration: "prefer-hardware"
};
decoder.configure(config);

处理数据

输入解码数据

使用 decode 方法将压缩的视频数据(通常是 EncodedVideoChunk)送入解码器。

1
2
3
4
5
6
7
8
let chunk = new EncodedVideoChunk({
type: is_sync ? "key" : "delta",
timestamp: timestamp,
duration: duration,
data: data
});
decoder.decode(chunk);

处理输出解码

VideoFrame包含了解码后的一帧数据,可以通过copyTo函数将输入拷贝到cpu中(比较耗时),也可以直接通过texImage2D直接绑定到纹理上。VideoFrame是一个Transferable objects,可以通过postMessage发送到其它Worker中使用。

1
2
3
4
5
6
7
output(frame) {
...
// 绑定纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, frame);
// 下载数据
frame.copyTo(buffer);
},

释放资源

解码完成后,调用 closeflush 释放解码器资源。

1
2
await decoder.flush();
decoder.close();

编码

创建编码器

设置解码器名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
if (VideoType == H264)
{
stream << "avc1.";
switch (Profile)
{
case H264_PROFILE_BASELINE:
{
stream << "4200";
}
break;
case H264_PROFILE_MAIN:
{
stream << "4d00";
}
break;
case H264_PROFILE_HIGH:
{
stream << "6400";
}
break;
default:
{
stream << "4200";
}
break;
}

switch (Level)
{
case H264_LEVEL_30:
{
stream << "1e";
}
break;
case H264_LEVEL_31:
{
stream << "1f";
}
break;
case H264_LEVEL_40:
{
stream << "28";
}
break;
case H264_LEVEL_41:
{
stream << "29";
}
break;
case H264_LEVEL_50:
{
stream << "32";
}
break;
case H264_LEVEL_51:
{
stream << "33";
}
break;
default:
{
stream << "1e";
}
break;
}
}

else if (VideoType == H265)
{
switch (Profile)
{
case HEVC_PROFILE_MAIN:
{
stream << "hev1.1.6.L93.B0";
}
break;
case HEVC_PROFILE_MAIN10:
{
stream << "hev1.2.4.L120.B0";
}
break;
default:
{
stream << "hev1.1.6.L93.B0";
}
break;
}
}

创建编码器实例

使用 VideoEncoder 类创建一个视频编码器实例。

1
2
3
4
5
6
7
8
9
const encoder = new VideoEncoder({
output(chunk, metadata) {

},

error(e) {

}
});

配置解码器

WebCodec支持Annex-B格式和avc格式的码流,可以在config中指定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const config = {
bitrateMode: bitPref,
codec: codec,
width: width,
height: height,
hardwareAcceleration: hw,
bitrate: bitrate,
framerate: frameRate,
keyInterval: keyInterval,
};

switch(codec_type){
case "H264":
config.avc = { format: "annexb" };
break;
case "H265":
config.hevc = { format: "annexb" };
break;
}

encoder.configure(config);

处理数据

输入编码数据

通过 encode 方法将 VideoFrame 对象送入编码器进行编码。

1
2
3
4
5
6
7
8
9
 const init = {
timestamp: timestamp,
duration: duration,
codedWidth: frameWidth,
codedHeight: frameHeight,
format: "I420",
};
let videoFrame = new VideoFrame(dst, init);
encoder.encode(videoFrame,{keyFrame:key_flag});

处理输出解码

通过copyTo可以拷贝编码结果到一个buffer中,调用muxer的write将其写入到文件中

1
2
3
4
5
output(chunk, metadata) {
// metadata 包含sps pps数据
// chunk 编码结果
chunk.copyTo(buffer)
},

释放资源

编码完成后,调用 closeflush 释放编码器资源。

1
2
await encoder.flush();
encoder.close();