如何在浏览器中实现 WebRTC 音视频通话?完整接入流程详解

0 阅读21分钟

1 WebRTC 音视频通话功能简介

WebRTC(Web Real-Time Communication)是一项支持浏览器实时音视频通信的技术,开发者可以基于它快速实现一对一视频通话、在线会议、语音聊天、远程协作等实时互动能力。

在实际业务场景中,一个完整的音视频通话流程通常包括:

  • 采集本地摄像头与麦克风数据
  • 将音视频数据实时发送给远端用户
  • 接收远端用户的音视频数据并进行播放
  • 基于“房间”管理用户之间的实时通信关系

不过,需要注意的是,WebRTC 更偏向底层实时通信能力,本身并不等于一个完整的实时音视频系统。

在真实业务场景中,开发者通常还需要进一步处理信令系统、弱网对抗与传输优化、音视频编解码适配、多平台互通、设备兼容性、全球节点调度等问题。

因此,我们需要在 WebRTC 基础上,结合 RTC SDK 完成更完整的实时音视频能力建设。

本文将基于 WebRTC,结合 即构ZEGO 实时音视频 SDK,实现一个简单的实时音视频通话功能,帮助开发者快速理解 Web 音视频通话的基本实现流程。

1.1 RTC 核心概念解释

在开始接入之前,可以先了解几个实时音视频中的核心概念:

流是一组按照指定编码格式封装的实时音视频数据内容。一个流通常包含多个轨道(Track),例如视频轨道、音频轨道。

用户开启摄像头和麦克风后,本地采集到的音视频数据会被封装为音视频流。

推流

推流是指将本地采集到的实时音视频数据发送到实时音视频网络的过程。

例如用户 A 开启摄像头、采集到本地画面和声音、将数据实时发送出去,这个过程就是“推流”。

拉流

拉流是指从实时音视频网络接收远端用户的音视频数据,并进行播放的过程。

例如用户 B 收到用户 A 的音视频流、在页面中播放对方画面和声音,这个过程就是“拉流”。

房间

房间可以理解为一个实时音视频会话空间,用于组织多个用户之间的实时通信关系。

用户需要先登录同一个房间,才能互相进行音视频通信。在同一房间内,用户可以收发实时音视频流、接收用户进出通知、感知音视频流状态变化。

常见的一对一视频通话、在线会议、语聊房、在线课堂,本质上都可以理解为不同类型的“房间”场景。

更多 RTC 相关概念,可参考 ZEGO 官方文档术语说明

2 实现 WebRTC 视频通话的前提条件

2.1 前提条件

在实现基本的实时音视频功能之前,请确保:

2.2 示例代码

我们提供了一个实现了基本流程的完整示例 HTML 文件,可作为开发过程中的参考。

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Zego Express Video Call</title>
    <!-- 此处需要改成正确的 SDK 版本号 -->
    <script src="ZegoExpressWebRTC-x.x.x.js"></script>

</head>

<body>
    <h1>
        Zego RTC Video Call
    </h1>
    <div class="video-wrapper">
        **Local video**
        **Remote video**
        <div id="local-video"></div>
        <div id="remote-video"></div>
    </div>
    <script>
        // 项目唯一标识 AppID,Number 类型,请从 ZEGO 控制台获取
        let appID = 0
        // 接入服务器地址 Server,String 类型,可直接填空字符串
        let server = ""

        // 初始化实例
        const zg = new ZegoExpressEngine(appID, server);
        zg.setDebugVerbose(false)
        // 房间状态更新回调
        // 此处在登录房间成功后,立即进行推流。在实现具体业务时,您可选择其他时机进行推流,只要保证当前房间连接状态是连接成功的即可。
        // 房间状态更新回调
        zg.on('roomStateChanged', async (roomID, reason, errorCode, extendedData) => {
            if (reason == 'LOGINED') {
                console.log("与房间连接成功,只有当房间状态是连接成功时,才能进行推流、拉流等操作。")
            }
        })

        zg.on('roomUserUpdate', (roomID, updateType, userList) => {
            // 其他用户进出房间的通知
        });

        zg.on('roomStreamUpdate', async (roomID, updateType, streamList, extendedData) => {
            // 房间内其他用户音视频流变化的通知
            if (updateType == 'ADD') {
                // 流新增,开始拉流
                // 此处演示拉取流新增的列表中第一条流的音视频
                const streamID = streamList[0].streamID;
                // streamList 中有对应流的 streamID
                const remoteStream = await zg.startPlayingStream(streamID);
                // 创建媒体流播放组件
                const remoteView = zg.createRemoteStreamView(remoteStream);
                remoteView.play("remote-video", {enableAutoplayDialog:true});

            } else if (updateType == 'DELETE') {
                // 流删除,通过流删除列表 streamList 中每个流的 streamID 进行停止拉流。
                const streamID = streamList[0].streamID;
                zg.stopPlayingStream(streamID)
            }
        });

        // 登录房间,成功则返回 true
        // userUpdate 设置为 true 才能收到 roomUserUpdate 回调。
        let userID = "user1"; // userID 用户自己设置,必须保证全局唯一
        let userName = "user1";// userName 用户自己设置,没有唯一性要求,非必填
        let roomID = "123"; // roomID 用户自己设置,必须保证全局唯一
        // token 由用户自己的服务端生成,为了更快跑通流程,可以通过 ZEGO 控制台 https://console.zego.im/ 获取临时的音视频 token,token 为字符串
        let token = ``;

        zg.loginRoom(roomID, token, { userID, userName: userID }, { userUpdate: true }).then(async result => {
            if (result == true) {
                console.log("login success");
                // 与房间连接成功,只有当房间状态是连接成功时,才能进行推流、拉流等操作。
                // 创建流、预览
                // 调用 createZegoStream 接口后,需要等待 ZEGO 服务器返回流媒体对象才能执行后续操作
                const localStream = await zg.createZegoStream();
                // 预览画面
                localStream.playVideo(document.querySelector("#local-video"), {enableAutoplayDialog:true});
                // 开始推流,将自己的音视频流推送到 ZEGO 音视频云,此处 streamID 由用户定义,需全局唯一
                let streamID = new Date().getTime().toString();
                zg.startPublishingStream(streamID, localStream)
            }
        });
        // // 登录房间的第二种写法
        // (async function main(){
        //     await zg.loginRoom(roomID, token, { userID, userName: userID }, { userUpdate: true })
        // })()
    </script>
</body>

</html>

2.3 实现流程

以用户 A 拉取用户 B 的流为例,一次简单的实时音视频通话主要流程如下:

  1. 用户 A 创建实例,登录房间。(登录成功后,可预览自己的画面并推流。)
  2. 用户 B 创建实例,登录同一个房间。登录成功后,用户 B 开始推流,此时 SDK 会触发 roomStreamUpdate 回调,表示房间内有流的变化。
  3. 用户 A 可通过监听 roomStreamUpdate 回调,当回调通知有流新增时,获取用户 B 的流 ID,来拉取播放用户 B 刚刚推送的流。

2.4 创建界面

为方便实现基本的实时音视频功能,您可参考下图实现一个简单的页面。

2.5 创建引擎并监听回调

  1. 创建并初始化一个 ZegoExpressEngine 的实例,将您项目的 AppID 传入参数 “appID”,Server 地址 传入参数 “server”。

注意

  • 若使用 3.7.0 及以上版本 SDK,server 参数可填写控制台获取的Server 地址或者直接填空字符串。
  • ZegoExpressEngine 的实例不能被框架以响应式的方式处理,否则会发生不可预测的问题。例如,Vue3 框架上可以通过 Vue3 的 markRaw 接口标记 SDK 实例,避免被转为代理。
  1. SDK 提供如房间连接状态、音视频流变化、用户进出等通知回调。创建引擎后,您可以通过引擎实例的on方法注册回调。

注意

为避免错过事件通知,建议在创建引擎后立即注册回调

// 项目唯一标识 AppID,Number 类型,请从 ZEGO 控制台获取
let appID = ;
// 接入服务器地址 Server,String 类型,可直接填空字符串
let server = "";

// 初始化实例
const zg = new ZegoExpressEngine(appID, server);

// 房间状态更新回调
zg.on('roomStateChanged', (roomID, reason, errorCode, extendData) => {
    if (reason == 'LOGINING') {
        // 登录中
    } else if (reason == 'LOGINED') {
        // 登录成功
        //只有当房间状态是登录成功或重连成功时,推流(startPublishingStream)、拉流(startPlayingStream)才能正常收发音视频
        //将自己的音视频流推送到 ZEGO 音视频云
    } else if (reason == 'LOGIN_FAILED') {
        // 登录失败
    } else if (reason == 'RECONNECTING') {
        // 重连中
    } else if (reason == 'RECONNECTED') {
        // 重连成功
    } else if (reason == 'RECONNECT_FAILED') {
        // 重连失败
    } else if (reason == 'KICKOUT') {
        // 被踢出房间
    } else if (reason == 'LOGOUT') {
        // 登出成功
    } else if (reason == 'LOGOUT_FAILED') {
        // 登出失败
    }
});

//房间内其他用户进出房间的通知
//只有调用 loginRoom 登录房间时传入 ZegoRoomConfig,且 ZegoRoomConfig 的 userUpdate 参数为 “true” 时,用户才能收到 roomUserUpdate回调。
zg.on('roomUserUpdate', (roomID, updateType, userList) => {
    if (updateType == 'ADD') {
        for (var i = 0; i < userList.length; i++) {
            console.log(userList[i]['userID'], '加入了房间:', roomID)
        }
    } else if (updateType == 'DELETE') {
        for (var i = 0; i < userList.length; i++) {
            console.log(userList[i]['userID'], '退出了房间:', roomID)
        }
    }
});

zg.on('roomStreamUpdate', async (roomID, updateType, streamList, extendedData) => {
    // 房间内其他用户音视频流变化的通知
});

常用通知回调

// 房间连接状态更新回调
// 本地调用 loginRoom 加入房间时,您可通过监听 roomStateChanged 回调实时监控自己在本房间内的连接状态。
zg.on('roomStateChanged', (roomID, reason, errorCode, extendData) => {
    if (reason == 'LOGINING') {
        // 登录中
    } else if (reason == 'LOGINED') {
        // 登录成功
        //只有当房间状态是登录成功或重连成功时,推流(startPublishingStream)、拉流(startPlayingStream)才能正常收发音视频
        //将自己的音视频流推送到 ZEGO 音视频云
    } else if (reason == 'LOGIN_FAILED') {
        // 登录失败
    } else if (reason == 'RECONNECTING') {
        // 重连中
    } else if (reason == 'RECONNECTED') {
        // 重连成功
    } else if (reason == 'RECONNECT_FAILED') {
        // 重连失败
    } else if (reason == 'KICKOUT') {
        // 被踢出房间
    } else if (reason == 'LOGOUT') {
        // 登出成功
    } else if (reason == 'LOGOUT_FAILED') {
        // 登出失败
    }
});

//房间内其他用户推送的音视频流新增/减少的通知
//自己推送的流不能在这里接收到通知
zg.on('roomStreamUpdate', async (roomID, updateType, streamList, extendedData) => {
    if (updateType == 'ADD') {
        // 流新增
        for (var i = 0; i < streamList.length; i++) {
            console.log('房间',roomID,'内新增了流:', streamList[i]['streamID'])
        }
        const message = "其他用户的视频流streamID: " + streamID.toString();
    } else if (updateType == 'DELETE') {
        // 流删除
        for (var i = 0; i < streamList.length; i++) {
            console.log('房间',roomID,'内减少了流:', streamList[i]['streamID'])
        }
    }
});

//房间内其他用户进出房间的通知
//只有调用 loginRoom 登录房间时传入 ZegoRoomConfig,且 ZegoRoomConfig 的 userUpdate 参数为 “true” 时,用户才能收到 roomUserUpdate回调。
zg.on('roomUserUpdate', (roomID, updateType, userList) => {
    if (updateType == 'ADD') {
        for (var i = 0; i < userList.length; i++) {
            console.log(userList[i]['userID'], '加入了房间:', roomID)
        }
    } else if (updateType == 'DELETE') {
        for (var i = 0; i < userList.length; i++) {
            console.log(userList[i]['userID'], '退出了房间:', roomID)
        }
    }
});

//用户推送音视频流的状态通知
//用户推送音视频流的状态发生变更时,会收到该回调。如果网络中断导致推流异常,SDK 在重试推流的同时也会通知状态变化。
zg.on('publisherStateUpdate', result => {
    // 推流状态更新回调
    var state = result['state']
    var streamID = result['streamID']
    var errorCode = result['errorCode']
    var extendedData = result['extendedData']
    if (state == 'PUBLISHING') {
        console.log('成功推送音视频流:', streamID);
    } else if (state == 'NO_PUBLISH') {
        console.log('未推送音视频流');
    } else if (state == 'PUBLISH_REQUESTING') {
        console.log('请求推送音视频流:', streamID);
    }
    console.log('错误码:', errorCode,' 额外信息:', extendedData)
})

//推流质量回调。
//成功推流后,您会定时收到回调音视频流质量数据(如分辨率、帧率、码率等)。
zg.on('publishQualityUpdate', (streamID, stats) => {
    // 推流质量回调
    console.log('流质量回调')
})

//用户拉取音视频流的状态通知
//用户拉取音视频流的状态发生变更时,会收到该回调。如果网络中断导致拉流异常,SDK 会自动进行重试。
zg.on('playerStateUpdate', result => {
    // 拉流状态更新回调
    var state = result['state']
    var streamID = result['streamID']
    var errorCode = result['errorCode']
    var extendedData = result['extendedData']
    if (state == 'PLAYING') {
        console.log('成功拉取音视频流:', streamID);
    } else if (state == 'NO_PLAY') {
        console.log('未拉取音视频流');
    } else if (state == 'PLAY_REQUESTING') {
        console.log('请求拉取音视频流:', streamID);
    }
    console.log('错误码:', errorCode,' 额外信息:', extendedData)
})

//拉取音视频流时的质量回调。
//成功拉流后,您会定时收到拉取音视频流时的质量数据通知(如分辨率、帧率、码率等)。
zg.on('playQualityUpdate', (streamID,stats) => {
    // 拉流质量回调
})

//收到广播消息的通知
zg.on('IMRecvBroadcastMessage', (roomID, chatData) => {
    console.log('广播消息IMRecvBroadcastMessage', roomID, chatData[0].message);
    alert(chatData[0].message)
});

//收到弹幕消息的通知
zg.on('IMRecvBarrageMessage', (roomID, chatData) => {
    console.log('弹幕消息IMRecvBroadcastMessage', roomID, chatData[0].message);
    alert(chatData[0].message)
});

//收到自定义信令消息的通知
zg.on('IMRecvCustomCommand', (roomID, fromUser, command) => {
    console.log('自定义消息IMRecvCustomCommand', roomID, fromUser, command);
    alert(command)
});

2.6 检测兼容性

考虑到不同的浏览器对 WebRTC 的兼容性不同,在实现推拉流功能之前,您需要检测浏览器能否正常运行 WebRTC。

您可以调用 checkSystemRequirements 接口检测浏览器的兼容性,检测结果的含义,请参考 ZegoCapabilityDetection 接口下的参数描述。

const result = await zg.checkSystemRequirements();
// 返回的 result 为兼容性检测结果。 webRTC  true 时表示支持 webRTC,其他属性含义可以参考接口 API 文档。
console.log(result);
// {
//   webRTC: true,
//   customCapture: true,
//   camera: true,
//   microphone: true,
//   videoCodec: { H264: true, H265: false, VP8: true, VP9: true },
//   screenSharing: true,
//   errInfo: {}
// }

您还可以通过 ZEGO 提供的 在线检测工具,在需要检测的浏览器中打开,直接检测浏览器的兼容性。请参考 浏览器兼容性和已知问题 获取 SDK 支持的浏览器兼容版本。

2.7 登录房间

  1. 生成 Token

登录房间需要用于验证身份的 Token,开发者可直接在 ZEGO 控制台获取临时 Token(有效期为 24 小时)来使用,详情请参考 控制台 - 项目管理 中的 “项目信息”。

说明

临时 Token 仅供调试,正式上线前,请从开发者的业务服务器生成 Token,详情可参考 使用 Token 鉴权

  1. 登录房间

调用 loginRoom 接口,传入房间 ID 参数 “roomID”、“token” 和用户参数 “user”,根据实际情况传入参数 “config”,登录房间。如果房间不存在,调用该接口时会创建并登录此房间。

注意

  • “roomID”、“userID” 和 “userName” 参数的取值都为自定义,其中“userName”非必填。
  • “roomID” 和 “userID” 都必须唯一,建议开发者将 “userID” 设置为一个有意义的值,可将其与自己的业务账号系统进行关联。
  • “userID” 必须与生成 token 时传入的 userID 保持一致,否则登录失败。
  • 只有调用 loginRoom 接口登录房间时传入 ZegoRoomConfig 配置,且 “userUpdate” 参数取值为 “true” 时,用户才能收到 roomUserUpdate 回调。
// “roomID”、“userID” 和 “userName” 参数的取值需要根据实际情况填写。
// 登录房间,成功则返回 true。
// userUpdate 设置为 true 才能收到 roomUserUpdate 回调。
let userID = "0001";
let userName = "user0001";
let roomID = "0001";
let token = ;
// 为避免错过任何通知,您需要在登录房间前先监听用户加入/退出房间、房间连接状态变更、推流状态变更等回调。
zg.on('roomStateChanged', async (roomID, reason, errorCode, extendedData) => {

})
zg.loginRoom(roomID, token, { userID: userID, userName: userName }, { userUpdate: true }).then(result => {
     if (result == true) {
        console.log("login success")
     }
});

您可以监听 roomStateChanged 回调实时监控自己与房间的连接状态。只有当房间状态是连接成功时,才能进行推流、拉流等操作。如果登录房间失败,可参考 错误码 进行操作。

2.8 预览自己的画面,并推送到 ZEGO 音视频云

  1. 创建流并预览自己的画面

开始推流前需要创建本端的音视频流,调用 createZegoStream 接口获取 ZegoLocalStream 实例对象 localStream,默认会采集摄像头画面和麦克风声音。

通过 localStream 的 playVideoplayAudio 接口,创建本地媒体流播放组件,播放待推送或者已经成功推流的音视频。

  1. 需等待 createZegoStream 接口返回 ZegoLocalStream 实例对象 localStream 后,再将自己的音视频流推送到 ZEGO 音视频云。

调用 startPublishingStream 接口,传入 “streamID” 和创建流得到的流对象 “localStream”,向远端用户发送本端的音视频流。您可通过监听 PublisherStateUpdateCallBack 回调知晓推流是否成功。

说明

  • “streamID” 由您本地生成,但是需要保证同一个 AppID 下,“streamID” 全局唯一。如果同一个 AppID 下,不同用户各推了一条 “streamID” 相同的流,会导致后推流的用户推流失败。
  • 如果您需要与其它平台互通、或使用 转推 CDN 功能,请将视频编码格式设置为 H.264,详情请参考 设置视频编码属性
// 此处在登录房间成功后,立即进行推流。在实现具体业务时,您可选择其他时机进行推流,只要保证当前房间连接状态是连接成功的即可。

zg.loginRoom(roomID, token, { userID, userName: userID }, { userUpdate: true }).then(async result => {
     if (result == true) {
        console.log("login success")
        // 创建流、预览
        // 调用 createZegoStream 接口后,需要等待 ZEGO 服务器返回 ZegoLocalStream 实例对象才能执行后续操作
        const localStream = await zg.createZegoStream();

        // 预览画面,将播放组件挂载到页面,"local-video" 为组件容器 <div> 元素的 id 。
        localStream.playVideo(document.querySelector("#local-video"));

        // 开始推流,将自己的音视频流推送到 ZEGO 音视频云
        let streamID = new Date().getTime().toString();
        zg.startPublishingStream(streamID, localStream)
     }
});

通过属性设置相关采集参数

可根据需要通过 createZegoStream 接口中的如下属性设置音视频相关采集参数,详情可参考 自定义视频采集

  • camera:摄像头麦克风采集流相关配置
  • screen:屏幕捕捉采集流相关配置
  • custom:第三方流采集相关配置

2.9 拉取其他用户的音视频

进行视频通话时,我们需要拉取到其他用户的音视频。

房间内有其他用户加入时,SDK 会触发 roomStreamUpdate 回调,通知房间内有流新增,基于此可获取其他用户的 “streamID”。此时,调用 startPlayingStream 接口根据传入的其他用户的 “streamID”,拉取远端已推送到 ZEGO 服务器的音视频画面。您可通过监听 PlayerStateUpdateCallBack 回调知晓是否成功拉取音视频。若需要从 CDN 拉流,可参考 使用 CDN 直播

注意

推荐用 createRemoteStreamView 播放媒体流,不建议使用 audio或 video 标签播放媒体流。

// 流状态更新回调
zg.on('roomStreamUpdate', async (roomID, updateType, streamList, extendedData) => {
    // 当 updateType 为 ADD 时,代表有音视频流新增,此时可以调用 startPlayingStream 接口拉取播放该音视频流
    if (updateType == 'ADD') {
        // 流新增,开始拉流
        // 这里为了使示例代码更加简洁,我们只拉取新增的音视频流列表中第的第一条流,在实际的业务中,建议开发者循环遍历 streamList ,拉取每一条音视频流
        const streamID = streamList[0].streamID;
        // streamList 中有对应流的 streamID
        const remoteStream = await zg.startPlayingStream(streamID);

        // 创建媒体流播放组件对象,用于播放远端媒体流 。
        const remoteView = zg.createRemoteStreamView(remoteStream);
        // 将播放组件挂载到页面,"remote-video" 为组件容器 <div> 元素的 id 。
        remoteView.play("remote-video");

    } else if (updateType == 'DELETE') {
        // 流删除,停止拉流
    }
});

注意

  • 部分浏览器因自动播放限制策略问题,使用 ZegoStreamView 媒体流播放组件进行播放媒体流可能受阻,SDK 默认会在界面上弹窗提示恢复播放。
  • 您可以将 ZegoStreamView.play() 方法的第二个参数 options.enableAutoplayDialog 设置为 false 关闭自动弹窗,通过在 autoplayFailed 事件回调中,在页面上显示一个按钮,引导用户点击恢复播放。

至此,您已经成功实现了简单的实时音视频通话,可在浏览器中打开 "index.html",体验实时音视频功能。

2.10 在线测试推拉流功能

在真机中运行项目,运行成功后,可以看到本端视频画面。

为方便体验,ZEGO 提供了一个 Web 端调试示例,在该页面下,输入相同的 AppID、RoomID,输入不同的 UserID、以及对应的 Token,即可加入同一房间与真机设备互通。当成功开始音视频通话时,可以听到远端的音频,看到远端的视频画面。

2.11 停止音视频通话

  1. 停止推流、销毁流

调用 stopPublishingStream 接口停止向远端用户发送本端的音视频流。调用 destroyStream 接口销毁创建的流数据。

// 根据本端 streamID 停止推流
zg.stopPublishingStream(streamID)
// localStream 是调用 createZegoStream 接口获取的 ZegoLocalStream 实例对象
zg.destroyStream(localStream)

2. 停止拉流

调用 stopPlayingStream 接口停止拉取远端推送的音视频流。

注意

如果开发者通过 roomStreamUpdate 回调收到了音视频流 “减少” 的通知,请及时调用 stopPlayingStream 接口停止拉流,避免拉到空流、产生额外的成本;或者,开发者可以根据自己的业务需求,选择合适的时机,主动调用 stopPlayingStream 接口停止拉流。

// 流状态更新回调
zg.on('roomStreamUpdate', async (roomID, updateType, streamList, extendedData) => {
    if (updateType == 'ADD') {
        // 流新增,开始拉流
    } else if (updateType == 'DELETE') {
        // 流删除,通过流删除列表 streamList 中每个流的 streamID 进行停止拉流。
        const streamID = streamList[0].streamID;
        zg.stopPlayingStream(streamID)
    }
});

3. 退出房间

调用 logoutRoom 接口退出房间。

zg.logoutRoom(roomID)

4. 销毁引擎

如果用户彻底不使用音视频功能时,可调用 destroyEngine 接口销毁引擎,释放麦克风、摄像头、内存、CPU 等资源。

zg.destroyEngine()
zg = null

以上整个推拉流过程的 API 调用时序可参考下图:

3 结语

至此,我们已经完成了基于 WebRTC 的简单音视频通话功能实现。通过集成 ZEGO Web RTC SDK,可以在浏览器端快速完成从 SDK 初始化、登录房间、音视频推流与拉流,到实时通话建立的完整流程。

相比直接基于原生 WebRTC 自行搭建实时通信系统,RTC SDK 可以进一步帮助开发者降低在网络传输、设备兼容、弱网优化以及实时音视频调度等方面的接入复杂度,更快完成实时互动能力落地。

本文实现的一对一音视频通话能力,也是实时互动场景中的基础能力模块。在此基础上,还可以进一步扩展:

  • 多人音视频会议
  • 语聊房与连麦互动
  • 在线教育
  • AI 实时语音对话
  • 互动直播
  • 远程协作等更多实时互动场景

后续我们也会继续整理更多 WebRTC 与 RTC SDK 相关的实战内容,包括多人通话、实时互动场景优化、音视频传输机制以及 AI + RTC 结合方案等方向。

4 获取技术支持

如果你在接入过程中遇到问题,可以参考下方常见问题或官方文档进一步排查。

近期有项目规划,可以了解 RTC 产品报价,或点击官网右侧联系商务咨询折扣。

5 常见问题

  1. 为什么登录房间成功后,还是无法推流或拉流?

登录 loginRoom 成功并不代表已经与房间建立稳定连接。建议监听 roomStateChanged 回调,只有当状态为 LOGINEDRECONNECTED 时,再执行startPublishingStreamstartPlayingStream,否则可能出现推流失败、拉流失败等问题。

  1. 为什么收不到 roomUserUpdate 回调?

通常是因为登录房间时,没有开启 userUpdate 配置。需要在 loginRoom 时传入:

zg.loginRoom(
  roomID,
  token,
  { userID, userName },
  { userUpdate: true }
)

只有开启后,SDK 才会通知房间内用户进出事件。

  1. 为什么 roomStreamUpdate 收不到自己的流?

roomStreamUpdate 只会通知“其他用户”的流变化,自己推送的流不会通过该回调返回。自己的推流状态,需要通过publisherStateUpdate进行监听。

  1. 为什么浏览器无法自动播放远端音视频?

这是浏览器的自动播放策略限制导致的。部分浏览器(尤其是 Chrome、Safari)会限制未经用户交互的音视频自动播放。ZEGO SDK 默认会弹出恢复播放提示:

remoteView.play("remote-video", {
  enableAutoplayDialog: true
});

如果需要自定义交互流程,也可以关闭默认弹窗,自行引导用户点击恢复播放。

  1. 为什么创建流 createZegoStream() 失败?

常见原因包括:

  • 浏览器未授权摄像头/麦克风权限
  • 浏览器不支持 WebRTC
  • 当前页面不是 HTTPS
  • 设备没有可用摄像头或麦克风
  • 浏览器被其他应用占用设备

建议先调用:

checkSystemRequirements()

检测浏览器兼容性与设备能力。

  1. 为什么不同用户推流时会互相冲突?

通常是因为使用了相同的 streamID。在同一个 AppID 下streamID 必须全局唯一,否则后推流的用户会推流失败。实际项目中,建议使用业务用户 ID 拼接时间戳,或使用 UUID 生成 streamID。例如:

let streamID = `${userID}_${Date.now()}`

7. ## 临时 Token 可以直接用于正式环境吗?

不建议。

控制台生成的临时 Token仅用于快速调试,默认有效期为 24 小时,正式环境建议由业务服务端动态生成 Token,避免 Token 泄漏,提升账号安全性。否则可能存在非法盗用、恶意推流等风险。

  1. 为什么推荐使用 createRemoteStreamView 播放远端流?

虽然浏览器原生 <video> 标签也能播放媒体流,但在复杂实时互动场景中:

const remoteView = zg.createRemoteStreamView(remoteStream);

通常兼容性与稳定性更好。尤其在自动播放恢复、多路流管理、浏览器兼容、画面恢复等场景下,更推荐使用 SDK 提供的播放组件。

本文主要介绍了基于 WebRTC 与 ZEGO Web RTC SDK 实现简单音视频通话功能的基础接入流程,并结合实际开发中常见的问题进行了说明,可以快速搭建浏览器端的一对一视频通话、在线会议、语聊互动等典型实时互动场景。

后续也会围绕「RTC SDK 接入与实时互动能力实现」持续更新,适合需要构建实时音视频、在线互动、AI 实时通信等能力的开发者参考。