• 作者:老汪软件技巧
  • 发表时间:2024-09-19 11:33
  • 浏览量:

前言

在《WebRTC 探索:前端视角下的实时通信解析(上)》中,我们从 WebRTC 的基本概念入手,探讨了其在现代 Web 应用中的关键作用。通过分析 Web 端的常用 API,我们为理解 WebRTC 的底层技术机制奠定了坚实的基础。这一篇章不仅解答了“什么是 WebRTC”的问题,还为如何利用这些 API 打造实时通信应用提供了清晰的路径。

紧接着,在《WebRTC 探索:前端视角下的实时通信解析(中)》,我们进一步深入技术层面,全面剖析了 WebRTC 连接的核心概念,并逐步揭示了点对点(P2P)会话连接的内部工作流程。通过搭建一个简易的信令服务器,我们展示了如何将理论转化为实际应用,从而为开发功能完备的实时通信系统提供了技术支持和实践指导。

现在,随着对 WebRTC 核心技术和 P2P 连接流程的理解逐步深入,我们也终于来到了这系列文章的最后一站——《WebRTC 探索:前端视角下的实时通信解析(下)》。我们将实际演示如何搭建一个简单的 1v1 音视频通话,让理论知识真正应用于实践。同时,还将介绍多路通信架构,探索如何在 WebRTC 中处理复杂的通信需求。这些内容不仅可以让你动手实现一个功能完整的音视频通话系统,还能让你理解并应用多路通信的设计思路。

1v1 音视频通话 Demo 效果演示演示背景

为了实现 1v1 音视频通话,我们在本地环境中搭建了一个信令服务器,信令服务器的实现基于上一篇文章(《WebRTC 探索:前端视角下的实时通信解析(中)》)介绍的内容。为了便于演示,我们用户页面使用 Vue 框架开发了一个简洁的页面。在这个演示中,两个本地浏览器窗口分别模拟了两位用户的音视频通话场景,即用户“转转”和“采货侠”。通过这一Demo,我们不仅展示了 WebRTC 在点对点(P2P)通信中的应用,同时也验证了我们信令服务器的实际功能。

首先在本地启动两个浏览器窗口分别模拟这两位用户“转转”与“采货侠“进行视频通话

用户“转转”,看到的画面如下:

发起视频通话

当向好友“采货侠”发起视频通话时,“采货侠”用户浏览器端会弹出一个授权音视频设备的弹窗:

好友”采货侠“允许后,直接进入音视频通话。左上角显示的是本地视频图像(转转用户视角):

而在远端(“采货侠”)的视角中,则会看到如下画面:

消息发送与接收

在通话过程中,用户“转转”向“采货侠”发送了一条消息。在“转转”的界面中,会显示如下内容:

远端用户“采货侠”收到的消息如下:

调整视频模式

在实际应用中,可能需要调整视频模式。可以通过以下代码进行控制:

// 切换视频轨道的启用状态
senders.find((s) => s.track.kind === 'video').track.enabled = !send.track.enabled

调整后的“转转”以及对应的“采货侠”画面如下:

在这一部分的演示中,我们展示了一个简单的 1v1 音视频通话 demo。这个演示只是 WebRTC 应用的冰山一角,展示了最基础的功能。虽然看起来简单,但它也已经具备了一个完整 WebRTC 应用的雏形。

接下来,我们将深入探讨 1v1 音视频通话的具体实现及其相关功能。这不仅包括呼叫的发起和应答,还涵盖了媒体流的动态调整和类 IM 功能的实现。我们将详细分析如何使用 PeerConnection 对象进行通信会话的建立、如何在通话过程中切换媒体模式,以及如何通过 datachannel 实现实时文本传输。

1、1v1 音视频通话功能详解

在上一篇(《WebRTC 探索:前端视角下的实时通信解析(中)》)文章中,我们已经详细介绍了 WebRTC 的通信流程和信令交互的实现。如果你已经熟悉这些内容,那么接下来对代码的解析将会更加直观和易于理解。

1.1功能拆解

首先,初始化 PeerConnection 对象,并获取本地媒体流,将其添加到 PeerConnection 中,然后渲染到本地预览中。以下是初始化呼叫端的代码概要:

async initCallerInfo(callerId, calleeId) {
    // 初始化 PeerConnection 对象
    this.localRtcPc = new PeerConnection();
    
    // 获取本地媒体流(音频和视频)
    const localStream = await this.getLocalUserMedia({ audio: true, video: true });
    
    // 将本地媒体流中的每个轨道添加到 PeerConnection
    localStream.getTracks().forEach(track => this.localRtcPc.addTrack(track));
    
    // 将本地媒体流渲染到预览 DOM 元素
    await this.setDomVideoStream("localdemo", localStream);
    
    // 初始化回调函数来处理各种事件
    this.onPcEvent(this.localRtcPc, callerId, calleeId);
    
    // 创建并设置 offer 信令
    const offer = await this.localRtcPc.createOffer();
    await this.localRtcPc.setLocalDescription(offer);
    
    // 通过信令服务器将 offer 发送给被呼叫端
    this.linkSocket.emit("offer", { targetUid: calleeId, userId: callerId, offer });
}

处理 PeerConnection 的各种事件,如媒体流的接收、ICE 候选的处理等。以下是核心回调函数的代码概要:

onPcEvent(pc, localUid, remoteUid) {
    // 创建一个数据通道
    that.channel = pc.createDataChannel('chat');
    
    // 监听远程媒体流的轨道
    pc.ontrack = function(event) {
        that.setRemoteDomVideoStream('remoteVideo', event.track);
    };
    
    // 监听重新协商事件
    pc.onnegotiationneeded = function(e) {
        console.log('重新协商', e);
    };
    
    // 监听 ICE 候选的生成
    pc.onicecandidate = function(event) {
        if (event.candidate) {
            // 通过信令服务器发送 ICE 候选信息
            that.linkSocket.emit('candidate', {
                targetUid: remoteUid,
                userId: localUid,
                candidate: event.candidate,
            });
        } else {
            console.log('在此次协商中,没有更多的候选了');
        }
    };
}

被呼叫端的流程类似于呼叫端,但要处理接收的 offer 信令并返回 answer。以下是接收并响应呼叫的代码概要:

async onRemoteOffer(fromUid, offer) {
    //  接受 呼叫端 的 offer 并设置为 remote desc
    await this.localRtcPc.setRemoteDescription(offer);
    
    // 创建应答信令
    const answer = await this.localRtcPc.createAnswer();
    
    // 设置为 local desc
    await this.localRtcPc.setLocalDescription(answer);
    
    // 通过信令服务器将 answer 发送给 呼叫端
    this.linkSocket.emit("answer", { targetUid: fromUid, userId: getParams("userId"), answer });
}

使用 RTCRtpSender 对象可以控制音视频流的显示与否以及切换视频源。以下是相关代码的概要:

// 切换视频模式(音频模式/视频模式)
const senders = this.localRtcPc.getSenders();
// 找到视频发送方的信息
const send = senders.find(s => s.track.kind === 'video');
// 切换视频轨道的启用状态
send.track.enabled = !send.track.enabled;
// 屏幕分享/摄像头切换
const stream = await this.getShareMedia();
// 获取视频轨道
const [videoTrack] = stream.getVideoTracks();
// 找到视频类型发送方的信息
const send = senders.find(s => s.track.kind === 'video');
// 替换视频轨道
send.replaceTrack(videoTrack);

使用 datachannel 实现 P2P 文本传输。以下是创建和监听 datachannel 的代码概要:

// 创建数据通道
this.channel = pc.createDataChannel("my channel", {
    protocol: "json",
    ordered: true,
});
// 监听数据通道的事件
pc.ondatachannel = function(ev) {
    console.log('Data channel is created!');
    ev.channel.onopen = function() {
        console.log('Data channel ------------open----------------');
    };
    ev.channel.onmessage = function(ev) {
        console.log('Data channel ------------msg----------------');
    };
};

通过以上对 1v1 实战demo的详细分析,我们展示了如何从呼叫开始逐步建立通信会话,并实现音视频通信、模式切换以及类 IM 功能。

1.2 1v1 音视频通话的实际问题与解决方案

在构建1v1音视频通话系统时,实际生产环境中的挑战往往不只是代码层面的实现。虽然我们已经在本地完成了一个基础的音视频通话 demo,但要真正部署并运行在线上,还需要面对一些更为复杂的问题和优化需求。这些问题涵盖了网络、设备兼容性、安全性以及数据存储等多个方面。以下是一些常见的线上问题及其解决方案,希望能够为你提供一个全面的参考视角。

问题类别具体问题解决方案

网络问题

网络抖动和延迟:线上环境中的网络质量不可控,容易出现抖动和延迟。

自适应比特率:根据实时网络状况自动调整视频质量,确保即使在网络波动时也能提供稳定的体验。

丢包重传:部署丢包重传机制,减少因网络不稳定导致的画面卡顿或音频中断。

网络优化:利用 STUN/TURN 服务器提升 NAT 穿透能力,确保在各种网络环境下都能顺利建立连接。

设备兼容性

设备和浏览器的多样性:用户的设备和浏览器各不相同,这会导致兼容性问题。

广泛测试与优化:在不同设备和浏览器上进行兼容性测试,确保应用在各种环境下都能正常运行。

动态调整:根据设备能力和网络状况动态调整视频分辨率和帧率,以提供一致的用户体验。

渐进增强:在核心功能稳定的基础上,逐步引入高级功能,确保每一步的改进都不会影响已有功能的稳定性。

安全性

数据加密:线上传输时用户数据的安全性至关重要。

端到端加密(E2EE):确保音视频数据在传输过程中的全程加密,防止未经授权的访问。

强加密算法:采用如 AES 和 DTLS 等成熟的加密技术,提升数据安全性。

身份验证与授权:实现用户身份验证和权限控制,确保通信的安全性。

数据存储

数据持久化:用户的历史记录或聊天记录需要可靠存储。

分布式存储:使用分布式数据库存储聊天记录,确保数据的可靠性和可扩展性。

数据加密存储:在存储数据时使用加密,保护用户隐私。

备份与恢复:定期备份数据并提供恢复机制,防止数据丢失。

尽管线上部署1v1音视频通话系统需要应对复杂的挑战,但通过合理的设计和优化,能够确保系统在实际生产环境中提供稳定、安全的服务。希望这些问题和解决方案能够为你在实际项目中的实现和优化提供参考和灵感。

2、WebRTC 多路通信架构

在完成了1v1视频通话的实战后,我们已经对WebRTC及其不同的连接方式有了一定的了解。我们知道WebRTC主要实现的是浏览器之间的点对点直接连接,那么当我们希望实现多对多通信时,应该如何处理呢?在这一节,我们将探讨WebRTC的多路通信架构及其在实际应用中的技术实现与挑战。

2.1 多路通信架构概述

在 WebRTC 多对多通信场景中,常用的架构包括 Mesh、SFU 和 MCU 三种方式。每种架构在不同应用场景中都有其优缺点,选择合适的方案需要综合考虑实际需求。

2.2 Mesh 架构(Multiple Direct Connections)属性描述

结构

_《WebRTC 探索:前端视角下的实时通信解析》(下)_《WebRTC 探索:前端视角下的实时通信解析》(下)

每个参与者直接与其他所有参与者建立 P2P 连接,形成网状结构。

优点

无需服务器处理媒体流,节省带宽和服务器资源。

没有中心节点,实现相对简单。

缺点

随着参与者数量的增加,每个参与者的带宽和处理负荷会急剧增加,不适合大规模会议。

使用场景

小规模会议。

实际应用注意点

带宽限制:随着参与者增加,带宽需求呈指数级增长。

网络延迟:每个参与者的网络延迟会累积,可能导致音视频不同步。

示例图

2.3 SFU 架构(Selective Forwarding Unit)属性描述

结构

SFU 架构的服务器仅作为媒体流的路由器,它接收终端的音视频流后,根据需要转发给其他终端,而不进行混流处理。

优点

节约带宽:每个客户端只需向服务器发送一份媒体流,节省客户端资源。

扩展性强:支持更多的参与者,适合大规模会议。

缺点

服务器压力大:所有媒体流的转发都在服务端进行,需要较高的服务器性能。

使用场景

中型会议、在线教育、直播等场景。

实际应用注意点

服务器性能:高并发情况下,服务端可能成为瓶颈。

网络延迟:虽然相较 Mesh 延迟降低,但服务器转发仍可能引入延迟。

常用工具与服务

Mediasoup:高性能的 SFU 服务器,适合中大型会议。

SRS:支持多种协议,适合灵活部署。

示例图

2.4 MCU 架构(Multipoint Conferencing Unit)属性描述

结构

MCU 是一个中心化的服务器,它接收来自各个终端的音视频流,在服务器端进行解码、混合和重新编码,生成统一的音视频流,再发送给所有参与通信的终端。

优点

客户端负载轻:浏览器端处理轻松,适合大规模会议。

统一视图:通过服务器统一处理,提供一致的用户体验。

缺点

服务器压力大:解码、混流、编码等处理对服务器性能要求高。

带宽需求高:服务器端需要处理和传输大量数据。

使用场景

大型会议、需要复杂处理的场景,如在线教育或企业培训。

实际应用注意点

服务器成本:高性能服务器成本较高。

处理延迟:解码和混流过程可能引入延迟。

常用工具与服务

Jitsi:开源的 MCU 解决方案,支持多方视频会议。

SRS:适合需要灵活部署的小型 MCU 场景。

示例图

2.5 实际应用案例分析

为了让大家更好地理解这些架构在实际项目中的应用,以下是一些典型应用场景与技术点分析。

应用场景技术点需要注意的问题常用工具与服务

直播

低延迟流传输

多路流媒体转发

弹幕与互动功能

高并发处理能力

延迟控制

多设备兼容性

SRS (Simple Realtime Server):支持低延迟 RTMP 推流,灵活配置和高性能处理。

FFmpeg:流媒体处理与格式转换。

多人会议

多路视频流管理

动态布局调整

噪声抑制与回声消除

网络波动对通话质量的影响

权限管理与隐私保护

Mediasoup:适合中大型会议的 SFU 解决方案。

Jitsi:提供统一视图的大型会议 MCU 解决方案。

在线教育

画面分割与多窗口展示

实时互动与问答功能

低延迟与高质量音视频同步