在网页中播放视频,可以通过几种方式实现:
- 通过video标播放视频
- 通过webassembly对视频数据解码,然后渲染到canvas上面
- WebCodec 这次主要介绍一下WebCodec,需要Chrome 94以上版本可以使用。
流程
graph LR
获取摄像头 --> 显示采集画面 --> 编码 --> 显示编码后的画面 --> 解码 --> 显示解码后的画面
获取摄像头
浏览器自带API支持获取摄像头,具体可以参考文档
``` typescript
navigator.mediaDevices.getUserMedia({video: true}).then((mediastream) => {
// 获取摄像头成功
}).catch((err) => {
// 获取摄像头失败
});
```
VideoEncoder
通过VideoEncoder可以在网页中使用浏览器的硬编能力,相比使用wasm进行软编,性能还是有很大的提升的。
videoEncoder = new VideoEncoder({
output: (chunk) => {
// 在handleVideoEncoded中处理编码后的数据
handleVideoEncoded(chunk);
},
error: (error) => {
console.error('video encoder error:', error);
}
});
videoEncoder.configure({
codec: 'vp8',
width: 1280,
height: 720
});
let videoProcessor = new MediaStreamTrackProcessor(stream.getVideoTracks()[0]);
// videoGenerator就是一个videoTrack,可以添加到MediaStream中用<video>来显示
let videoGenerator = new MediaStreamTrackGenerator('video');
let videoTransformer = new TransformStream({
transform: (frame, controller) => {
// 每60帧会插入一个关键帧
const insert_keyframe = (sendFrames % 60) == 0;
videoEncoder.encode(frame, { keyFrame: insert_keyframe });
controller.enqueue(frame);
sendFrames++;
}
});
videoProcessor
.readable
.pipeThrough(videoTransformer)
.pipeTo(videoGenerator.writable);
VideoDeocder
videoDecoder = new VideoDecoder({
output: (frame) => {
if (videoWriter) {
videoWriter.write(frame);
}
},
error: (error) => {
console.error('video decoder error:' + error);
}
});
videoDecoder.configure({
codec: 'vp8',
width: 1280,
height: 720
});
// videoGenerator就是解码后的VideoTrack,可以添加到MediaStream中用<video>来显示
let videoGenerator = new MediaStreamTrackGenerator('video');
videoWriter = videoGenerator.writable.getWriter();
上效果
效果如图:
图中有三个画面,分别是显示采集摄像头画面、编码后的画面、解码后的画面。通过秒表可以看到,采集画面和编码后的画面基本没有时差(此处应该是手机秒表的间隔时间大于了编码耗时,所以截图好多次,都是采集画面和编码画面是一模一样的),解码画面和猜忌画面的延迟大概在20ms左右。 图中视频使用的是1280*720 30帧,视频编码使用的vp8(从api上看,应该是h264、h265、vp都能支持的,但是我只跑通了vp8。。。)
结论
- 优点
- 可以使用浏览器的硬编、硬解的能力,性能上相比软编软解有明显优势
- 可以配置浏览器的WebTransport的能力,在音视频的场景下摆脱对webrtc的依赖,在浏览器端使用自研的协议,不再只能使用webrtc
- 缺点
- 浏览器兼容性,目前只有Chrome支持,并且需要的浏览器版本较高
代码
最后上代码,本地服务用localhost或者127.0.0.1都可以,如果用域名需要https才可以
<!doctype html>
<html>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
<title>WebCodecs Demo</title>
</head>
<body>
<style>
.item {
width: 320px;
display: inline-block;
text-align: center;
}
.video {
width: 1280;
height: 720;
}
</style>
<h1>WebCodecs Demo</h1>
<br />
<button id="startBtn">开始</button>
<br />
<div>
<div class="item">
<span>采集画面:</span>
<video id="localVideo" autoplay class="video"></video>
</div>
<div class="item">
<span>编码画面:</span>
<video id="encodeVideo" autoplay class="video"></video>
</div>
<div class="item">
<span>解码画面::</span>
<video id="playVideo" autoplay class="video"></video>
</div>
</div>
<script type='text/javascript'>
let localVideo = document.getElementById("localVideo");
let encodeVideo = document.getElementById("encodeVideo");
let playVideo = document.getElementById("playVideo");
let startBtn = document.getElementById("startBtn");
let playBtn = document.getElementById("playBtn");
let sendFrames = 0;
const codec = 'vp8';
let waitKeyFrame = true;
let videoEncoder = null;
let videoDecoder = null;
let videoWriter = null; // WritableStreamDefaultWriter
const videoConstraints = {
width: 640,
height: 480
}
startBtn.addEventListener('click', () => {
initVideoEncoder();
initVideoDecoder();
sendFrames = 0;
navigator.mediaDevices.getUserMedia({ video: videoConstraints }).then((stream) => {
localVideo.srcObject = stream;
let videoProcessor = new MediaStreamTrackProcessor(stream.getVideoTracks()[0]);
let videoGenerator = new MediaStreamTrackGenerator('video');
let videoTransformer = new TransformStream({
transform: (frame, controller) => {
const insert_keyframe = (sendFrames % 60) == 0;
videoEncoder.encode(frame, { keyFrame: insert_keyframe });
controller.enqueue(frame);
sendFrames++;
}
});
videoProcessor.readable.pipeThrough(videoTransformer).pipeTo(videoGenerator.writable);
// 此处需要显示videoGenerator,否则一段时间就会报错
let processedStream = new MediaStream();
processedStream.addTrack(videoGenerator);
encodeVideo.srcObject = processedStream;
}).catch((err) => {
})
})
function initVideoEncoder() {
videoEncoder = new VideoEncoder({
output: (chunk) => {
handleVideoEncoded(chunk);
},
error: (error) => {
console.error('video encoder error:', error);
}
});
videoEncoder.configure({ codec: codec, width: videoConstraints.width, height: videoConstraints.height });
}
function handleVideoEncoded(chunk) {
let data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data.buffer);
chunk.data = data;
const encoded = new EncodedVideoChunk(chunk);
videoDecoder.decode(encoded);
}
function initVideoDecoder() {
videoDecoder = new VideoDecoder({
output: (frame) => {
if (videoWriter) {
videoWriter.write(frame);
}
},
error: (error) => {
console.error('video decoder error:' + error);
}
});
videoDecoder.configure({ codec: codec, width: videoConstraints.width, height: videoConstraints.height });
let videoGenerator = new MediaStreamTrackGenerator('video');
videoWriter = videoGenerator.writable.getWriter();
console.log("writer:", videoWriter);
let processedStream = new MediaStream();
processedStream.addTrack(videoGenerator);
playVideo.srcObject = processedStream;
}
</script>
</body>
</html>
其他
如果你也是专注前端多媒体或者对前端多媒体感兴趣,可以关注前端多媒体公众号