随着 WebRTC 技术的成熟,实时音视频已经从"原生 App 的专属能力",慢慢变成 Web 端的常见能力:在线教育小班课、视频客服、在线面试、远程问诊......很多业务都希望做到 "用户打开一个网页,就能直接视频通话",无需下载任何客户端。
如果完全基于原生 WebRTC 自己搭建一套系统,往往会遇到大量复杂问题:SDP 信令流程、ICE/候选收集、STUN/TURN 服务器部署、弱网重传策略、音视频编解码兼容等等,这些对普通前端或业务开发来说门槛不低。
因此,越来越多团队会选择使用商业级实时音视频服务,例如即构提供的 Web 实时音视频 SDK。SDK 在 WebRTC 之上封装了房间管理、推拉流、弱网优化、跨网加速等能力,开发者可以用非常少的代码实现稳定的 1v1 视频通话。
这篇文章将基于即构国内官网文档,完整实现一个 Web 端 1v1 视频通话 Demo,并对每一步做文字说明,帮助读者真正理解背后的原理与流程。
一、应用场景与整体思路
在开始写代码之前,先明确一下这个 Demo 打算解决什么问题,以及整体方案长什么样。
1. 典型应用场景
- 视频客服:在企业后台或 SaaS 平台中嵌入视频窗口,客服和用户一键通话。
- 在线面试:在 HR 系统中内置视频面试,避免跳转到第三方会议工具。
- 在线问诊:医生通过网页和患者进行实时视频沟通。
- 在线小班课 / 1v1 辅导:老师和学生直接在 Web 中上课。
这些场景的共同需求是:
- 无需安装应用
- 浏览器即可进入
- 通话稳定、延迟可接受
- 容易集成到现有 Web 系统中
2. 技术方案整体思路
本 Demo 的整体技术思路可以概括为四步:
- 初始化即构 Web SDK:根据 appID 和 server 创建引擎实例。
- 登录房间:用 userID / roomID / token 加入一个"通话房间"。
- 本地推流 + 远端拉流:本地采集摄像头/麦克风并推流,对端收到流后拉取并播放。
- 挂断清理:停止推流/拉流,退出房间,释放资源。
底层复杂的 WebRTC 信令、网络策略、编解码细节,都由即构云和 SDK 处理,我们只需要关注「何时推流、何时拉流、何时退出」。
二、集成即构 Web SDK(对齐官方文档)
官方文档地址(国内):
https://doc-zh.zego.im/real-time-video-web/quick-start/integrating-sdk
即构 Web SDK 支持多种集成方式,常见的是 npm 与 script 两种方式。
1. 使用 npm(适合工程化项目)
如果项目使用 Vue、React、Vite 或 Webpack,这种方式最适合。
安装 SDK:
bash
npm install zego-express-engine-webrtc
在代码中引入:
js
import { ZegoExpressEngine } from "zego-express-engine-webrtc";
这种方式的优势是:
- 版本号可控,方便统一升级/锁定版本;
- 更适合 TS / 模块化工程;
- 方便与打包工具集成,减小最终产物体积。
2. 通过 script 引入(适合快速 Demo)
如果只是想快速跑通 Demo,或者当前项目没有构建工具,也可以直接在 HTML 中通过 <script> 标签引入 SDK。
从即构官网(国内)下载 Web SDK,解压后会看到 dist_js 目录,里面有打包好的 JS 文件,例如:
html
<script src="./dist_js/ZegoExpressWebRTC-3.5.0.js"></script>
这种方式的特点是:
- 上手非常快,只需一个 HTML + JS 文件即可;
- 适合做实验、PoC、内部 Demo;
- 后续需要接入工程化项目时,再切换到 npm 方式即可。
三、1v1 视频通话整体流程拆解
在具体写代码之前,先用一个简单的表格把整个流程拆开,方便在脑子里建立大致模型:
| 步骤 | 功能 | 描述 |
|---|---|---|
| 步骤 1 | 初始化引擎 | 创建 ZegoExpressEngine 实例,与即构云服务建立基础连接 |
| 步骤 2 | 注册回调 | 监听房间状态变化、用户变化、流新增/删除等事件 |
| 步骤 3 | 登录房间 | 使用 loginRoom 进入一个 roomID 对应的房间 |
| 步骤 4 | 创建本地流 | 打开摄像头/麦克风,获取本地媒体流 |
| 步骤 5 | 本地预览 | 在页面上渲染本地画面,确认设备正常 |
| 步骤 6 | 推流 | 将本地流推送到云端,让其他人可以拉取 |
| 步骤 7 | 监听并拉取远端流 | 收到对端推流事件后拉流并显示 |
| 步骤 8 | 挂断清理 | 停止推流/拉流,退出房间,释放资源 |
接下来按照这个顺序,一步步在代码中实现。
四、初始化 SDK 与注册事件
这一步的目的是:
创建引擎实例,并提前把房间、用户、流相关的事件监听好。
1. 初始化引擎
js
// 使用从即构控制台获取的 appID 和 server 参数
const zg = new ZegoExpressEngine(appID, server);
// 可选:关闭调试日志
zg.setDebugVerbose(false);
解释:
appID和server通常在即构控制台创建项目后获得。- 初始化引擎相当于告诉 SDK:后续所有操作都基于这一个应用和环境来进行。
setDebugVerbose(false)是可选配置,用于关闭浏览器中的调试日志输出,在生产环境中可以保持关闭。
2. 监听房间状态
js
zg.on("roomStateChanged", (roomID, reason, errorCode) => {
if (reason === "LOGINED") {
console.log("已成功进入房间:", roomID);
} else if (reason === "CONNECTING") {
console.log("正在连接房间...");
} else if (reason === "DISCONNECTED") {
console.log("已从房间断开,错误码:", errorCode);
}
});
作用:
- 该回调会在房间连接成功、重连、断开时触发。
- 可以用来在页面上展示当前房间状态,例如"已连接""重连中"等。
3. 监听房间内用户变化(可选但推荐)
js
zg.on("roomUserUpdate", (roomID, updateType, userList) => {
console.log("房间用户变化:", updateType, userList);
});
说明:
- 当有用户进入或离开房间时,会触发该事件。
- 在 1v1 场景中,可以用来判断对端是否在线,做一些 UI 提示。
4. 监听流新增 / 删除(这是 1v1 核心)
js
zg.on("roomStreamUpdate", async (roomID, updateType, streamList) => {
if (updateType === "ADD") {
// 有新的流发布(对端开始推流)
const streamID = streamList[0].streamID;
const remoteStream = await zg.startPlayingStream(streamID);
const remoteView = zg.createRemoteStreamView(remoteStream);
remoteView.play("remote-video", {
enableAutoplayDialog: true,
});
} else if (updateType === "DELETE") {
// 有流被移除(对端停止推流或离开房间)
const streamID = streamList[0].streamID;
zg.stopPlayingStream(streamID);
}
});
解释:
- 这是接收远端视频的关键步骤。
- 当对端推流后,本地会收到
ADD类型的roomStreamUpdate事件,从streamList中取出streamID即可开始拉流。 enableAutoplayDialog: true是为了解决浏览器的自动播放策略问题,避免某些环境下视频不自动播放。- 当对端停止推流或离开房间时,会触发
DELETE,此时需要停止拉流并做 UI 清理。
五、登录房间:建立通话的"房间上下文"
下一步是进入一个具体房间,只有进入同一个房间的用户才能互相看到彼此的流。
js
const userID = "user_" + Date.now();
const userName = userID;
const roomID = "room_1";
const success = await zg.loginRoom(
roomID,
token,
{ userID, userName },
{ userUpdate: true }
);
if (!success) {
console.error("登录房间失败");
// 这里可以提示用户"网络异常,请稍后再试"等
return;
}
说明:
userID通常需要保持在应用层唯一,这里用时间戳拼接是简单做法,实际项目中建议使用业务系统中的用户 ID。userName主要用于展示,不要求严格唯一。token一般由业务服务端生成,用于鉴权;在开发阶段可以使用即构控制台提供的测试 token。- 第四个参数
{ userUpdate: true }非常重要,如果不传这个选项,将收不到roomUserUpdate回调。
到这一步为止,当前用户已经加入了一个房间,可以开始创建和推送自己的音视频流。
六、创建本地音视频流并在页面预览
一旦房间登录成功,就可以通过 SDK 创建本地流并开始预览。
js
const localStream = await zg.createZegoStream();
localStream.playVideo(
document.getElementById("local-video"),
{ enableAutoplayDialog: true }
);
说明:
createZegoStream会调用浏览器的getUserMedia接口,因此用户第一次调用时浏览器会弹出"是否允许访问摄像头/麦克风"的权限弹窗。- 建议在 UI 上提前给出提示,例如"即将打开摄像头,请允许浏览器访问设备"。
playVideo会将本地流渲染到指定的 DOM 元素中,通常是一个<div>或<video>元素。- 再次强调
enableAutoplayDialog: true的必要性,这是解决浏览器自动播放限制的关键配置。
这一步的目标是:
让用户在"发起通话之前",先确认本地画面和声音都是正常的。
七、推送本地流(推流):让对端可以看到你
当本地预览正常后,就可以把本地流推送到即构云,让房间里的其他用户通过"拉流"获取。
js
const publishStreamID = String(Date.now());
zg.startPublishingStream(publishStreamID, localStream);
说明:
publishStreamID需要在房间中保持唯一,常见做法是使用时间戳、UUID 或 "前缀 + userID" 等。- 一旦成功推流,房间内其他用户会收到
roomStreamUpdate事件,此时就可以开始拉你的流并播放。 - 推流失败的情况可以通过引擎事件或返回值做错误处理和重试。
八、接收并播放对端视频(拉流)
这个流程前面在注册 roomStreamUpdate 时已经实现,这里再整体梳理一下逻辑:
- 对端推流后,本地会收到
roomStreamUpdate回调,updateType为"ADD"; - 从
streamList[0].streamID取出对端流 ID; - 调用
startPlayingStream(streamID)拉流; - 调用
createRemoteStreamView(remoteStream).play("remote-video")渲染到远端容器。
在 1v1 场景中,每一侧的逻辑是完全对称的:
你推流 → 对端收到流并拉流 → 对端推流 → 你收到流并拉流。
九、挂断通话与资源释放
通话结束时,需要做三件事:
- 停止推流;
- 停止拉流(如果有);
- 退出房间,并清理页面 UI。
示例代码:
js
// 1. 停止推流
zg.stopPublishingStream(publishStreamID);
// 2. 停止拉流(如果已经记录了 remoteStreamID,可以这里一起停止)
if (remoteStreamID) {
zg.stopPlayingStream(remoteStreamID);
}
// 3. 退出房间
zg.logoutRoom(roomID);
// 4. 可选:清理本地和远端视频区域的 DOM 内容
document.getElementById("local-video").innerHTML = "";
document.getElementById("remote-video").innerHTML = "";
说明:
- 在实际项目中,可以将这些操作封装在"挂断"按钮点击事件中。
- 挂断时最好同时做 UI 状态重置,比如按钮置灰、提示"通话已结束"等。
- 如果有录制、统计、日志上报等需求,也可以在挂断时触发对应逻辑。
十、总结与扩展方向
通过这一篇文章,从集成 SDK 到初始化引擎、登录房间、创建本地流、推流、拉流,再到挂断通话,已经完整走完了一个 Web 端 1v1 视频通话的闭环。
可以看到,在即构 SDK 的封装下,开发者无需接触复杂的 WebRTC 信令、ICE 协商、弱网优化算法等底层细节,就可以用相对简洁的代码实现稳定的实时音视频功能。
在此基础上,可以进一步扩展出很多功能,例如:
- 多人房间 / 小班课场景(管理多路流)
- 屏幕共享(用于远程协助或在线演示)
- 美颜、虚拟背景等视频增强效果
- 云录制与回放
- 与 IM / 白板 / 文档协同结合的完整实时互动应用
对于已经在做 Web 端产品的团队来说,把实时音视频作为基础能力集成进去,正在变成一个越来越常见、也越来越必要的选择。而借助即构这类 SDK,可以帮助团队 显著降低实现成本,缩短交付周期,同时保证体验质量。