整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

用WebRTC和Node.js开发实时视频聊天应用

不多说,我们直奔主题。这篇文章教大家如何编写一个视频聊天应用,使已连接的两用户端能共享视频和音频。操作很简单,非常适合JavaScript语言训练——更准确地说是WebRTC技术和Node.js。

什么是WebRTC?

Web Real-Time Communications 网页实时通信,简称WebRTC。WebRTC是一个HTML5规范,它允许用户在浏览器之间直接进行实时通信,不需要任何第三方插件。WebRTC可用于多种情境(比如文件共享),但端对端实时音频和视频通信是其主要功能。本文将着重为大家介绍这两项。

WebRTC所做的就是允许接入设备。你可以借WebRTC来实时使用麦克风、摄像头和分享你的屏幕。

所以,WebRTC可以用最简单的方式在网页中实现音频和视频通信。

WebRTC JavaScript API

WebRTC说起来很复杂,它涉及到很多技术。但建立连接、通信和传输数据的操作是通过一套JS API来实现的,还比较简单。其中主要的API包括:

RTCPeerConnection:创建和导航端对端连接。

RTCSessionDescription:描述连接(或潜在连接)的一端,以及它的配置方式。

navigator.getUserMedia:捕捉音频和视频。

为什么选择Node.js?

若要在两个或多个设备之间进行远程连接,你就需要一个服务器。在这种情况下,你也需要一个处理实时通信的服务器。Node.js是为实时可扩展的应用而构建的。要开发自由数据交换的双向连接应用程序,你可能会用到WebSockets,它允许在客户端和服务器之间建立一个会话窗口。来自客户端的请求会以循环的方式,更准确地说是事件循环进行处理,这时Node.js是我们很好的一个选择,因为它采取 “非阻塞(non-blocking) “的方式来解决请求。这样我们在这该过程中就能实现低延迟和高吞吐量。

如果你对开发微服务感兴趣的话,一定要看看查看我们内含650多位微服务专家意见的2020年微服务状态报告!

思路拓展:我们要创建的是什么?

我们会创建一个非常简单的应用程序,它能让我们将音频和视频流传输到连接的设备——一个基础款视频聊天应用程序。我们会用到的技术有:

Express库,提供静态文件,比如代表用户界面(UI)的HTML文件;

socket.io库,在两个设备之间用WebSockets建立连接;

WebRTC,允许媒体设备(摄像头和麦克风)在连接的设备之间传输音频和视频流。

实现视频会话

我们要做的第一件事是给我们的应用程序提供一个作为UI的HTML文件。让我们通过运行:npm init.js来初始化新的node.js项目。然后,我们需要通过运行:npm i -D typescript ts-node nodemon @types/express @types/socket.io安装一些开发依赖项,运行:npm i express socket.io安装生产依赖项。

之后我们就可以在package.json文件中定义脚本,来运行我们的项目了。

{
 "scripts": {
   "start": "ts-node src/index.ts",
   "dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts"
 },
 "devDependencies": {
   "@types/express": "^4.17.2",
   "@types/socket.io": "^2.1.4",
   "nodemon": "^1.19.4",
   "ts-node": "^8.4.1",
   "typescript": "^3.7.2"
 },
 "dependencies": {
   "express": "^4.17.1",
   "socket.io": "^2.3.0"
 }
}

当我们运行npm run dev命令时,nodemon会监控src文件夹中每个以.ts结尾的文件有无任何变化。现在,我们要创建一个src文件夹。在这个文件夹中,我们会创建两个typescript文件:index.ts和server.ts。

在server.ts中,我们会创建server类,并使其与express和socket.io一起工作。

import express, { Application } from "express";
import socketIO, { Server as SocketIOServer } from "socket.io";
import { createServer, Server as HTTPServer } from "http";
 
export class Server {
 private httpServer: HTTPServer;
 private app: Application;
 private io: SocketIOServer;
 
 private readonly DEFAULT_PORT = 5000;
 
 constructor() {
   this.initialize();
 
   this.handleRoutes();
   this.handleSocketConnection();
 }
 
 private initialize(): void {
   this.app = express();
   this.httpServer = createServer(this.app);
   this.io = socketIO(this.httpServer);
 }
 
 private handleRoutes(): void {
   this.app.get("/", (req, res) => {
     res.send(`<h1>Hello World</h1>`); 
   });
 }
 
 private handleSocketConnection(): void {
   this.io.on("connection", socket => {
     console.log("Socket connected.");
   });
 }
 
 public listen(callback: (port: number) => void): void {
   this.httpServer.listen(this.DEFAULT_PORT, () =>
     callback(this.DEFAULT_PORT)
   );
 }
}

为正常运行服务器,我们需要在index.ts文件中创建一个新的Server类实例并调用listen方法。

import { Server } from "./server";
 
const server = new Server();
 
server.listen(port => {
 console.log(`Server is listening on http://localhost:${port}`);
});

现在,如果我们运行:npm run dev会看到下面这样的情景:

当打开浏览器,输入http://localhost:5000,我们应该注意到左上的 “Hello World “信息。

然后我们就可以在public/index.html中创建一个新的HTML文件了。

<!DOCTYPE html>
<html lang="en">
 <head>
   <meta charset="UTF-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <meta http-equiv="X-UA-Compatible" content="ie=edge" />
   <title>Dogeller</title>
   <link
     href="https://fonts.googleapis.com/css?family=Montserrat:300,400,500,700&display=swap"
     rel="stylesheet"
   />
   <link rel="stylesheet" href="./styles.css" />
   <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
 </head>
 <body>
   <div class="container">
     <header class="header">
       <div class="logo-container">
         <img src="./img/doge.png" alt="doge logo" class="logo-img" />
         <h1 class="logo-text">
           Doge<span class="logo-highlight">ller</span>
         </h1>
       </div>
     </header>
     <div class="content-container">
       <div class="active-users-panel" id="active-user-container">
         <h3 class="panel-title">Active Users:</h3>
      </div>
       <div class="video-chat-container">
         <h2 class="talk-info" id="talking-with-info"> 
           Select active user on the left menu.
         </h2>
         <div class="video-container">
           <video autoplay class="remote-video" id="remote-video"></video>
           <video autoplay muted class="local-video" id="local-video"></video>
         </div>
       </div>
     </div>
   </div>
   <script src="./scripts/index.js"></script>
 </body>
</html>

在这个新文件中,我们创建了两个视频元素:一个用于远程视频连接,另一个用于本地视频。你可能已经注意到我们也在导入本地脚本了。现在我们就来创建一个新的文件夹“脚本”,并在这个目录下创建index.js文件。至于样式,你可以从GitHub库中下载它们。

接下来你需要给浏览器提供index.html。首先,你需要告诉express你想提供哪些静态文件。为了实现这一点,我们决定在Server类中实现一个新方法。

private configureApp(): void {
   this.app.use(express.static(path.join(__dirname, "../public")));
 }

不要忘记在initialize中调用configureApp。

private initialize(): void {
   this.app = express();
   this.httpServer = createServer(this.app);
   this.io = socketIO(this.httpServer);
 
   this.configureApp();
   this.handleSocketConnection();
 }

当你输入http://localhost:5000后,你应该能看到你的index.html文件在运行。

下一步要实现的是允许摄像头和视频访问并将其流式传输到local-video元素。要做到这一点,你需要打开public/scripts/index.js文件,并用以下方法实现它。

navigator.getUserMedia(
 { video: true, audio: true },
 stream => {
   const localVideo = document.getElementById("local-video");
   if (localVideo) {
     localVideo.srcObject = stream;
   }
 },
 error => {
   console.warn(error.message);
 }
);

当回到浏览器时,界面会出现一个提示请求访问你的媒体设备,在接受请求后,你电脑的摄像头就开始工作了。

更多细节详见A simple guide to concurrency in Node.js and a few traps that come with it。

如何处理socket连接?

接下来我们讲讲如何处理socket连接。我们需要将客户端与服务器连接起来。为此,我们将使用socket.io。在public/scripts/index.js中,添加以下代码:

this.io.on("connection", socket => {
     const existingSocket = this.activeSockets.find(
       existingSocket => existingSocket === socket.id
     );
 
     if (!existingSocket) {
       this.activeSockets.push(socket.id);
 
       socket.emit("update-user-list", {
         users: this.activeSockets.filter(
           existingSocket => existingSocket !== socket.id
         )
       });
 
       socket.broadcast.emit("update-user-list", {
         users: [socket.id]
       });
     }
   }

页面刷新后,电脑会弹出一条消息,显示 “Socket已连接”

然后我们回到server.ts中,把已连接的socket存储在内存中,这只是为了保留唯一连接。所以,我们需要在Server类中添加一个新的私有字段,如下:

private activeSockets: string[] = [];

然后我们需要在socket连接中检查socket是否已经存在。如果不存在,把新的socket推送到内存中,并向已连接的用户发送数据。

this.io.on("connection", socket => {
     const existingSocket = this.activeSockets.find(
       existingSocket => existingSocket === socket.id
     );
 
     if (!existingSocket) {
       this.activeSockets.push(socket.id);
 
       socket.emit("update-user-list", {
         users: this.activeSockets.filter(
           existingSocket => existingSocket !== socket.id
         )
       });
 
       socket.broadcast.emit("update-user-list", {
         users: [socket.id]
       });
     }
   }

你还需要在socket断开连接时及时响应,所以在socket连接中,你需要添加:

socket.on("disconnect", () => {
   this.activeSockets = this.activeSockets.filter(
     existingSocket => existingSocket !== socket.id
   );
   socket.broadcast.emit("remove-user", {
     socketId: socket.id
   });
 });

客户端(即public/scripts/index.js)这边,你需要妥善处理那些信息:

socket.on("update-user-list", ({ users }) => {
 updateUserList(users);
});
 
socket.on("remove-user", ({ socketId }) => {
 const elToRemove = document.getElementById(socketId);
 
 if (elToRemove) {
   elToRemove.remove();
 }
});

以下是 updateUserList 函数:

function updateUserList(socketIds) {
 const activeUserContainer = document.getElementById("active-user-container");
 
 socketIds.forEach(socketId => {
   const alreadyExistingUser = document.getElementById(socketId);
   if (!alreadyExistingUser) {
     const userContainerEl = createUserItemContainer(socketId);
     activeUserContainer.appendChild(userContainerEl);
   }
 });
}
以及createUserItemContainer函数:
function createUserItemContainer(socketId) {
 const userContainerEl = document.createElement("div");
 
 const usernameEl = document.createElement("p");
 
 userContainerEl.setAttribute("class", "active-user");
 userContainerEl.setAttribute("id", socketId);
 usernameEl.setAttribute("class", "username");
 usernameEl.innerHTML = `Socket: ${socketId}`;
 
 userContainerEl.appendChild(usernameEl);
 
 userContainerEl.addEventListener("click", () => {
   unselectUsersFromList();
   userContainerEl.setAttribute("class", "active-user active-user--selected");
   const talkingWithInfo = document.getElementById("talking-with-info");
   talkingWithInfo.innerHTML = `Talking with: "Socket: ${socketId}"`;
   callUser(socketId);
 }); 
 return userContainerEl;
}

需要注意的是,我们给用户容器元素添加了一个可以调用callUser函数的点击监听器——但现在,它可以是一个空的函数。接下来,当运行两个浏览器窗口(其中一个作为私人窗口)时,你应该注意到你的Web应用程序中有两个已经连接的socket。

点击列表中的活跃用户,这时我们需要调用callUser函数。但是在实现之前,你还需要在window对象中声明两个类。

const { RTCPeerConnection, RTCSessionDescription } = window;

我们会在callUser函数用到这两个类:

async function callUser(socketId) {
 const offer = await peerConnection.createOffer();
 await peerConnection.setLocalDescription(new RTCSessionDescription(offer));
 
 socket.emit("call-user", {
   offer,
   to: socketId
 });
}

现在我们要创建一个本地请求并发送给选定的用户。服务器会监听一个叫做call-user的事件、拦截请求并将其转发给选定的用户。让我们用server.ts来实现该操作:

socket.on("call-user", data => {
   socket.to(data.to).emit("call-made", {
     offer: data.offer,
     socket: socket.id
   });
 });

对于客户端,你需要就call-made事件作出调整:

socket.on("call-made", async data => {
 await peerConnection.setRemoteDescription(
   new RTCSessionDescription(data.offer)
 );
 const answer = await peerConnection.createAnswer();
 await peerConnection.setLocalDescription(new RTCSessionDescription(answer));
 
 socket.emit("make-answer", {
   answer,
   to: data.socket
 });
});

之后,在你从服务器得到的请求上设置一个远程描述,并为这个请求创建一个答复。对于服务器端,你只需要将适当的数据传递给选定的用户即可。然后我们再在server.ts里面添加一个监听器。

socket.on("make-answer", data => {
   socket.to(data.to).emit("answer-made", {
     socket: socket.id,
     answer: data.answer
   });
 });
对于客户端,我们需要处理 answer-made 事件。
socket.on("answer-made", async data => {
 await peerConnection.setRemoteDescription(
   new RTCSessionDescription(data.answer)
 );
 
 if (!isAlreadyCalling) {
   callUser(data.socket);
   isAlreadyCalling = true;
 }
});

我们可以使用标志isAlreadyCalling,它能帮助确保我们只需调用一次用户。

最后你需要做的是添加本地轨道,包括音频和视频到你的连接端。只有做到这一点,我们才能够与连接的用户共享视频和音频。要做到这一点,我们需要在navigator.getMediaDevice回调中调用peerConnection对象的addTrack函数。

navigator.getUserMedia(
 { video: true, audio: true },
 stream => {
   const localVideo = document.getElementById("local-video");
   if (localVideo) {
     localVideo.srcObject = stream;
   }
 
   stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
 },
 error => {
   console.warn(error.message);
 }
);

另外,我们还需要为ontrack事件添加一个适当的处理程序。

peerConnection.ontrack = function({ streams: [stream] }) {
 const remoteVideo = document.getElementById("remote-video");
 if (remoteVideo) {
   remoteVideo.srcObject = stream;
 }
};

如图示,我们已经从传递的对象中获取了流,并改变了远程视频中的srcObject来使用接收到的流。所以现在当你点击活跃用户后,你应该建立一个视频和音频连接,像下图这样:

欲了解细节,请参阅:Node.js and dependency injection – friends or foes?

现在你知道如何编写一个视频聊天应用了吧!

WebRTC是一个很大的话题,内容非常庞杂。如果你想了解它的运作原理,就需要花很大功夫。幸运的是,我们可以访问易于使用的JavaScript API,它可以帮助我们创建很简洁的应用程序,例如视频共享、聊天应用程序等等。

如果你想深入了解WebRTC,点击此WebRTC官方文档的链接。另外,我也推荐你阅读MDN的文档说明,它能帮助你更加了解此技术。

闻一

中国移动公布中期业绩:净利润606亿元 同比增长5.6%


8月11日,中国移动今日发布2016年中期财报。今年上半年,中国移动营收3704亿元,同比增长7.1%,净利润606亿元,同比增长5.6%。

中国移动上半年通信服务收入为3254亿元,同比增长6.9%,这一收入主要包括两方面:1、语音收入1200亿元,同比下滑14.2%;2、流量收入1950亿元,同比增长26.7%,其中手机上网流量同比增长133.9%。

用户数方面,移动用户达到8.37亿户,同比增长2.4%。具体到4G,新增4G基站超过20万个,总计达132万个,4G用户总数达到4.29亿,渗透率达到51.2%,4G网络流量占比也提升到88%。宽带方面,上半年净增1081万户,总数达到6584万户。


新闻二

迅雷第二季度净亏损200万美元


北京时间8月10日晚间消息,迅雷(Nasdaq:XNET)公布了截至6月30日的2016财年第二季度未经审计财报。第二季度,迅雷总营收为3810万美元,同比增长22.3%,环比下滑0.9%。


净亏损400万美元,而上一财季净亏损为540万美元。不按照美国通用会计准则,净亏损200万美元,而上一财季净亏损290万美元。截至2016年6月30日,迅雷所持有的现金、现金等价物和短期投资总额为3.91亿美元,相比之下截至2015年12月31日为4.321亿美元。


业绩展望:迅雷预计,2016财年第三季度总营收将达到3800万美元至4200万美元,中间值同比增长19.4%。


新闻三

谷歌新专利:用无人机搭建视频会议系统


据外媒报道,谷歌新获得一项专利显示,未来将用无人机搭建视频会议系统。而且该设备将能解决视频不流畅、相机角度不合适的问题,使用户感觉合作伙伴与自己身处同一间办公室

ebCodecs 是什么


  • WebCodecs 是一个 Web 规范,21 年 9 月份在 Chrome 94 中实现
  • WebCodecs 提供访问编解码能力的接口,可精细控制音视频数据


Web 音视频 API 存在什么问题


音视频技术在 Web 平台上的应用非常广泛,已有许多 Web API 间接调用了编解码器来实现特定功能:

  • 视频播放:MSE
  • 音频解码:WebAudio
  • 录制视频:MediaRecorder
  • 实时流媒体:WebRTC

但没有方法可以灵活配置或直接访问编解码器,所以许多应用使用 JS 或 WASM (比如 ffmpeg.wasm)来实现编解码功能,尽管存在诸多缺陷或限制

  • 低性能(WebCodecs 编码速度可达到 ffmpeg.wasm 的 20 倍)
  • 高功耗
  • 额外网络开销(下载已内置的编解码器)

这么做的原因是以前的 Web API 在特定场景都存在难以克服的障碍

  • WebAudio 只能解码完整的音频文件,但不支持数据流、不提供解码进度信息、更不支持编码
  • MediaRecorder 只能录制特定格式(WebM、MP4)的视频,无法控制编码速度、输出缓冲区等
  • WebRTC 与 MediaStream API 高度耦合,且不透明,仅能用于实时音视频通信
  • Video 标签、MSE 最常用于视频播放,但无法控制解码速率、缓冲区长度,且只支持播放部分视频容器格式

总结:目前 API 在特定场景做到简单、够用,但无法实现高效且精细地控制


WebCodecs 设计目标


  • 流式传输:对远程、磁盘资源进行流式输入输出
  • 效率:充分利用设备硬件,在 Worker 中运行
  • 组合性:与其他 Web API(如 Streams、WebTransport 和 WebAssembly)配合良好
  • 可恢复性:在出现问题时能够恢复的能力(网络不足、资源缺乏导致的帧下降等)
  • 灵活性:能适应各种场景(硬实时、软实时、非实时),能在此之上实现类似 MSE 或 WebRTC 的功能
  • 对称性:编码和解码具有相似的模式


非 WebCodecs 目标


  • 视频容器 封装/解封装 相关 API
  • 在 JS 或 WASM 中实现编解码器

以上总结于 译 WebCodecs 说明https://hughfenghen.github.io/posts/2023/10/02/webcodecs-explainer/),让大家快速了解 WebCodecs API 的背景和目标


WebCodecs 能做什么


WebCodecs API 介绍


先了解 WebCodecs API 在视频生产消费链路所处的位置



由图可知 WebCodecs API 提供的能力

  • 控制编解码过程
  • 访问编解码前后的底层数据



  • VideoFrame、EncodedVideoChunk 对应编码前的源图像和编码后的压缩数据,两者均提供获取底层二进制数据的接口;
  • VideoEncoder、VideoDecoder 用于前两者的类型转换
  • Web 平台所有表达图像相关的类型,都可以转换为 VideoFrame
  • 这里可以看到编码、解码过程在 API 设计上的对称性
  • 图像编解码习得的知识,同样可以对称迁移到音频编解码



以上就是 WebCodecs 提供的核心 API,新增 API 的数量非常少,主要难点在音视频相关的背景知识。

利用 mp4box.js 解封装 mp4 文件,得到 EncodedVideoChunk 后给 WebCodecs 解码,即可实现 mp4 -> 图像帧。



WebCodecs 不涉及环节

音视频生产消费链路中,由其他 Web API 提供,包括:

  • 音视频数据的采集与渲染
  • 封装/解封装
  • 传输、存储


相关 Web API


基于底层 API 可以构建的基础能力



  • WebAudio:音频处理
  • WebGPU/WebGL + OffscreenCanvas:离屏图像处理
  • OPFS:本地文件读写
  • WebWorker + WASM:密集型计算
  • WebTransport:低延迟网络传输

基于 Web 平台已有的能力,加上 WebCodecs 提供的编解码能力,能帮助开发者实现那些功能呢?


DEMO 演示及实现


WebCodecs 是相对底层 API,简单功能可能也需要写非常多的辅助代码,可以借助 WebAV 封装的工具函数来快速实现功能

WebAV 基于 WebCodecs,提供简单易用的 API 在浏览器中处理音视频数据

接下来演示 DEMO 效果以及基于 WebAV 的代码实现

1.可控解码

以设备最快的速度解码一个 20s 的视频,并将视频帧绘制到 Canvas 上

可控解码的意义不只是它能实现超快速或逐帧播放视频,而在于它能快速遍历所有帧,这是视频处理的基础


,时长00:02


首先从 WebAV 导出一个 MP4Clip 对象,它只需要一个 MP4文件 URL 进行初始化

然后使用 tick 方法获取到视频帧,再绘制到 canvas 上

while true 表示不做任何等待,所以到底有多快取决于网络下载和设备解码的速度


import { MP4Clip } from '@webav/av-cliper'
 
// 传入一个 mp4 文件流即可初始化
const clip = new MP4Clip((await fetch('<mp4 url>')).body)
await clip.ready
 
let time = 0
// 最快速度渲染视频所有帧
while (true) {
  const { state, video: videoFrame } = await clip.tick(time)
  if (state === 'done') break
  if (videoFrame != null && state === 'success') {
    ctx.clearRect(0, 0, cvs.width, cvs.height)
    // 绘制到 Canvas
    ctx.drawImage(videoFrame, 0, 0, videoFrame.codedWidth, videoFrame.codedHeight)
    // 注意,用完立即 close
    videoFrame.close()
  }
  // 时间单位是 微秒,所以差不多每秒取 30 帧,丢掉多余的帧
  time += 33000
}
clip.destroy()


2. 添加水印

给视频添加随时间移动的半透明文字水印


,时长00:23


先把文字转换成图片,这样很容易借助 css 实现各种文字效果;

然后控制图片按照一定规则移动,这里省略了动画的配置;

动画配置方法跟 css 的动画几乎是一样的,只需提供 0%,50% 特定时机的坐标就行了,WebAV 会自动计算出中间状态的坐标值,来实现动画效果;

最后将 MP4Clip 跟 ImgClip 合成输出一个新的视频流


const spr1 = new OffscreenSprite(
  new MP4Clip((await fetch('<mp4 url>')).body)
)
 
const spr2 = new OffscreenSprite(
  new ImgClip('水印')
)
spr2.setAnimation(/* animation config */)
 
const com = new Combinator()
 
await com.add(spr1, { main: true })
await com.add(spr2, { offset: 0 })
// com.ouput() => 输出视频流


3. 绿幕抠图

带绿幕的数字人形象与背景图片合成视频,使用 WebGL 对每帧图像进行处理,将人物背景修改为透明效果

抠图实现参考文章:WebGL Chromakey 实时绿幕抠图https://hughfenghen.github.io/posts/2023/07/07/webgl-chromakey/


,时长00:06


// 创建抠图工具函数
const chromakey = createChromakey(/* 绿幕抠图配置 */)
// 背景绿幕的测试视频
const clip = new MP4Clip((await fetch('<mp4 url>')).body)
// MP4 的每一帧 都会经过 tickInterceptor
clip.tickInterceptor = async (_, tickRet) => {
  if (tickRet.video == null) return tickRet
  return {
    ...tickRet,
    // 抠图之后再返回
    video: await chromakey(tickRet.video)
  }
}


4. 花影

在浏览器中运行的视频录制工具,可用于视频课程制作、直播推流工作台

视频演示视频课程制作的基本操作,包含 “添加摄像头、分享屏幕、修改素材层级、剪切视频片段、预览导出视频” 五个步骤


,时长00:53


WebCodecs 的应用场景


应用场景预测

  • 视频生产
  • 视频剪辑、直播工作台,搭配多人协同、AI 能力
  • 视频消费
  • 播放器、视频会议、云游戏
  • 算力转移
  • 视频压缩、缩略图生成、植入水印、调整速率


视频生产:从零到一


由于缺失编码能力,导致 Web 端少有视频生产工具;

现有的 Web 视频剪辑工具都强依赖服务端能力支持,交互体验存在优化空间;

在 Web 页面借助 Canvas 制作动画是非常简单的,借助 WebCodecs 的编码能力,现在就能将动画快速保存为视频。

视频裁剪、添加水印、内嵌字幕等基础视频剪辑能力,没有 WebCodecs 都是难以实现的,WebCodecs 将填补该领域的空白


视频消费:能力增强


借助 HTMLMediaElement、MSE,Web 平台的视频消费应用已经非常成熟;

以上 API 虽然简单易用,但无法控制细节,常有美中不足之感

比如,缓冲延迟控制、逐帧播放、超快速播放、解码控制等

WebCodecs 将支持构建更强、体验更好的视频消费应用


算力转移:成本体验双赢


目前 Web 使用的音视频服务,其处理过程都是在服务器上完成的

比如,众多在线视频处理工具提供的:压缩(降低分辨率、码率)、水印、变速、预览图 功能

处理流程:用户上传视频 -> 服务器处理 -> 用户下载视频;
整个过程消耗了服务器的计算成本、带宽成本,用户上传下载的等待时间

WebCodecs 能让更多的任务在本地运行,不仅降低了服务运营成本,还能提升用户体验


案例分享


没有 WebCodecs 以上的工具已经存在了,为什么相信它们会应用 WebCodecs?

首先,有了 WebCodecs 之后这些工具能做到体验更好、更便宜、迭代更快;

再结合以往经验和 Web 平台所具备优势,相信 WebCodecs 未来会得到广泛应用

分享两个例子

1. 用户视频消费行为变化

  1. 荒芜 时代
    用户行为:下载电影然后离线观看,装机必备本地视频播放器
  2. Flash 时代
    用户行为:在线观看视频逐渐流行
  3. HTML5 时代
    用户行为:PC 平台 Web 在线观看成为首选
  4. WebCodecs 时代
    补齐音视频编解码能力
    用户行为:期待 WebCodecs 配合 AI 加多人协同,音视频剪辑、视频会议、直播推流等工具将逐渐 Web 化

2. 富文本编辑

Web 开放了几个核心 API,让大部分文字编辑转移到线上,产生大量优秀的知识管理应用

借助 Web 的易访问性、搭配协同编辑,将生产沟通效率提升了一个等级

  • contenteditable:可编辑节点
  • Selection:选区
  • Range:文档片段

还有大量产品案例:Notion、Figma、VSCode...

总结:一旦 Web 平台具备某个领域的基础能力,相关产品不可避免的 Web 化


WebCodecs 的优势与限制


优势


性能



ffmpeg.wasm 最大的障碍就是性能问题,导致难以大规模应用,主要是因为它不能使用硬件加速所以编解码非常慢

测试简单的视频编码场景,WebCodecs 的性能是 ffmpeg.wasm 的 20 倍

Web 平台

Web 平台天然具有的优势:跨平台、便捷性、迭代效率

再加上底层能力越来越完善,已具备构建大型、专业软件的条件;

相信 WebCodecs 也能凭借 Web 平台的加持,获得更大的应用空间


限制


  • 生态不成熟
  • 比如 缺少优秀的 封装/解封装 工具包,支持容器格式有限
  • 兼容性
  • 旧版本浏览器不支持 WebCodecs
  • 受限于浏览器提供的编解码器
  • 编解码的可控参数不够丰富(为了通用性不可避免的交换)
  • 暂无法自定义编解码器

生态不成熟只需要时间和更多开发者的积极参与,一般 to B 产品对兼容性会更宽容一些,to C 的产品可以降级到服务端实现

比较麻烦的是 Web 平台提供的编解码器相对 Native 直接调用来说,还是有一些差距

如果需要自定义编解码器,或对编解码器的参数配置有非常高的要求,技术方案选择的时候需要慎重考虑 WebCodecs


愿景


  • WebCodecs 成为 Web 平台音视频处理的基础;
  • WebCodecs 像 HTML5 一样,促进音视频在 Web 平台的应用和发展。


附录


  • WebAV 基于 WebCodecs 构建的音视频处理 SDK(https://github.com/hughfenghen/WebAV
  • 译 WebCodecs 说明(https://hughfenghen.github.io/posts/2023/10/02/webcodecs-explainer/
  • Web 音视频(零)概览(https://hughfenghen.github.io/posts/2023/07/16/webav-0-overview/
  • WebAV DEMO(https://hughfenghen.github.io/WebAV/demo/1_1-decode-video
  • 花影 在浏览器中运行的视频录制工具(https://github.com/hughfenghen/bloom-shadow

作者:刘俊

来源:微信公众号:哔哩哔哩技术

出处:https://mp.weixin.qq.com/s/d28Xq9dticMdbO0s0N5MzA