您当前的位置:首页 > 电脑百科 > 程序开发 > 框架

WEBRtc 实现视频会议

时间:2022-11-11 14:03:24  来源:今日头条  作者:效能IT哥

 

WebRTC(Web Real-Time Communication)是为了让开发者在浏览器实现多媒体交换的技术,于2011年被加入W3C规范。当前的支持情况可以见下图。

 

WebRTC的核心在于建立PeerConnection实现视频流双端链接,要想理解WebRTC的工作流程,有如下后端服务的概念需要理解:

  • 信令(Signal)服务器
  • TURN/STUN服务器
  • 房间服务器
  • ICE候选者

视频流的传输不是纯前端的工作(显然),然而WebRTC的规范只规定了前端的部分,后端的信令传输不在WebRTC的范围之内,可以随开发者需求自行开发。


下图展现了WebRTC的工作流程

 

信令服务器(图中黄色部分)主要作用是连接建立前的中转工作。需要自行用websocket实现。

STUNSession Traversal Utilities for NAT,NAT会话穿越应用程序)允许位于NAT(或多重NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的NAT之后以及NAT为某一个本地端口所绑定的Inte.NET端端口。这些信息被用来在两个同时处于NAT路由器之后的主机之间创建UDP通信。该协议由RFC 5389定义。

TURNTraversal Using Relay NAT,通过Relay方式穿越NAT),TURN应用模型通过分配TURNServer的地址和端口作为客户端对外的接受地址和端口,即私网用户发出的报文都要经过TURNServer进行Relay转发。解决了STUN应用无法穿透对称NAT(SymmetricNAT)以及类似的防火墙的缺陷。

当STUN无法直接建立P2P时,便可以用TURN进行中转。

房间服务器 和RTC的建立并无直接关系。但考虑到不可能你的服务只能同时支持一对电脑链接,我们必须设置“房间”。在本项目中,我们的“房间”号码就是投屏码。在投屏码投屏的应用逻辑中,被叫方(投屏屏幕)首先用投屏码向房间服务器注册,客户端(请求投屏方)输入正确的投屏码后加入“房间”。自此,RTC之后的信令交换都只在这个“房间”内完成,使服务支持多对计算机互联。在实际实现中,房间服务器和信令服务器可以由同一服务完成。

ICEInteractive Connectivity Establishment,互动式连接建立)提供一种框架,使各种NAT穿透技术可以实现统一。该技术可以让基于SIP的VoIP客户端成功地穿透远程用户与网络之间可能存在的各类防火墙。

具体建立流程描述如下:

1、在连接建立之前,双方不知道彼此,因此都需要向信令服务器进行注册。随后,发起方创建PeerConnection,调用WebRTC的createOffer方法将SDPSession Description Protocol,理解为自己的一个“描述”)传输给信令服务器,由信令服务器做中继传递给被叫方。

2、被叫方收到Offer以后,调用createAnswer方法生成针对发起方Offer的响应。并通过信令服务器发回呼叫方。此时双方均保存有两个Description(对于呼叫方是自己的offer和对面的answer,对于接收方是对面的offer和自己的answer)

3、交换完Offer后需要进行ICE交换,ICE交换同样也要利用信令服务器进行交换。在设置完双方Description之后,发起方会自动向配置的STUN服务器请求自己的ip和端口,STUN服务器会返回可能可用的ICE-Candidate。发起方收到Candidate后需要将其通过信令发送到被叫方。被叫方设定成自己的ICE-Candidate。与此同时,被叫方也需要向STUN服务器发起ICE请求流程,把自己的ICE候选者发送给发起方。双方经过多次“协商”后最终选定ICE的交集进行连接。这也就体现了双方的“互动”。

4、交换完ICE候选者后,P2P的PeerConnection建立完成,就可以传输各种媒体信息了。在实际测试中ICE的交换并不一定在收到Answer后才触发,是可以提前触发的。


呼叫端的流程

0、加个按钮吧!

输入“投屏码”,和屏幕端加入同一个“房间”,以便于进行信令交换!点击按钮后,运行如下代码:也就是说,以下所有的代码,都是在你点击这个按钮后运行的。

socket = io.connect("你的信令服务器地址");
socket.on("connect", function () {
  socket.emit("CONNECT_TO_TV", {
    username: "lgy",
    projCode: store.projCode.toUpperCase(),
  });
});

在这里,我们建立了socket链接,并告诉了服务器我们想加入的“房间”。connect事件在建立连接后自动触发。(我的考虑是点击“链接”按钮再建立链接,而不是一直长连接着,这个链接专门用于RTC流程建立,也就是说点击“链接”前,下文的过程都不会进行。只有点击按钮后,才会有以下的流程)

1、建立PeerConnection对象

const configuration = {
  iceServers: [
    {
      urls: "turn:你的turn服务器地址,端口一般是3478",
      username: "turn用户名",
      credential: "turn密码",
    },
    { urls: "stun:你的stun服务器地址,端口一般是3478" },
  ],
  iceCandidatePoolSize: 2,
};
const peerConnection = new RTCPeerConnection(configuration);

建立RTCPeerConnection是应该传入候选iceServer,其中turnServer由于协议规定,必须有username和credential字段,stunServer不需要身份验证。详情可以参考MDN RTCPeerConnection文档

2、捕获视频流

const transferStream = awAIt navigator.mediaDevices.getUserMedia({
          video: {
            mandatory: {
              chromeMediaSource: "desktop",
              chromeMediaSourceId: Screensources[screenid].id,
              minWidth: 640,
              maxWidth: 1920,
              minHeight: 360,
              maxHeight: 1080,
            },
          },
        });

在这里,我们利用getUserMedia获取到了视频流MediaStream对象,若要同时获取音频,可以再增加audio选项,详情->getUserMedia

 何时获取视频流?
请注意,您不必现在就获取视频流,getUserMedia()会返回一个Promise,因此这里采用了await的写法,但是您最好提前声明一个MediaStream对象,因为在Offer生成之前媒体流必须添加到PeerConnection中,详见
https://stackoverflow.com/questions/17391750/remote-videostream-not-working-with-webrtc

3、创建Offer

// 重要!在生成offer前确保已添加视频流,不然可能连接建立完成后无法触发对面的onaddstream监听器。
peerConnection.addStream(transferStream);
 
const offer = await peerConnection.createOffer({
  offerToReceiveVideo: 1,
 // 已过时,最好用RTCRtpTransceiver替代
});
await peerConnection.setLocalDescription(offer);
 // 设置自己的Description
// 发送websocket到信令服务器
socket.emit("RTC_Client_Offer_To_Server", {
  offer: offer,
});

 关于addStream和offerToReceiveVideo
根据最新的规范,addStream和offerToReceiveVideo两处已经过时,根据官方建议(
https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addstream_event),还是采用最新的addTrack和RTCRtpTransceiver来替换为好。详见下文

这里有几点需要注意:

  • 请在连接建立之前为peerConnection添加stream或tracks
  • 请在调用createOffer时参数务必传入offerToReceiveVideo或设置RTCRtpTransceiver。您可以console.log()您的offer查看,若您的描述十分的短(只有一两行)大概率是没有设置该参数,正常情况下offer应该有几十行。而且没有设置该参数会导致无法触发ICE的收集工作,因而无法触发onicecandidate事件(将在后文提到)。

若采用track的写法,addStream应该这样写:

transferStream.getTracks().forEach(track => {
    peerConnection.addTrack(track, transferStream);
});

你可能已经注意到我们在这里用了socket.emit这一个函数来发送Offer,这是Socket.IO的使用方法,在本项目中我使用了Node.js用做websocket链接,调用Socket.IO这个包。先不用管这是干嘛的,他就是向信令服务器发送了一个指令,要求传送第二个参数(就是Offer)的内容。

4、注册事件监听器

首先注册ICE监听器。当Offer正常交换完成后,会自动触发ICE的收集,收集过后的结果会触发onicecandidate监听器。我们要做的很简单——拿到这个ICE收集结果,并通过类似的方式通过信令服务器传递给接收方。

peerConnection.onicecandidate = function (event) {
       console.log(event);
       if (event.candidate) {
         socket.emit("RTC_Candidate_Exchange", {
           iceCandidate: event.candidate,
         });
       }
};
// 或者你也可以用监听器的写法:
peerConnection.addEventListener('icecandidate', event => {
       console.log(event);
       if (event.candidate) {
         socket.emit("RTC_Candidate_Exchange", {
           iceCandidate: event.candidate,
         });
       }
})

websocket向信令服务器发送了RTC_Candidate_Exchange指令,并传递了从事件中获取的ICE候选人信息。

接着注册ICE接收器,当收到对面的ICE候选人信息时,我们要将它添加到自己的ICE候选人列表。

socket.on("RTC_Candidate_Exchange", async (message) => {
        if (message.iceCandidate) {
          try {
            await peerConnection.addIceCandidate(message.iceCandidate);
          } catch (e) {
            console.error("Error adding received ice candidate", e);
          }
        }
});

当收到信令服务器主题为RTC_Candidate_Exchange的消息时,取出消息中的ICE候选者,使用addIceCandidate加入到自己的列表。

也不能忘记注册Answer接收器——当对面收到了我们的Offer,把Answer发送过来时,添加到RemoteDescription中

socket.on("RTC_TV_Answer_To_Client", async (msg) => {
        if (msg.answer) {
            const remoteDesc = new RTCSessionDescription(msg.answer);
            await peerConnection.setRemoteDescription(remoteDesc);
            console.log("RTC TV answer received", peerConnection);
        }
});

在这里,我们使用RTCSessionDescription包裹了Answer,并将它通过setRemoteDescription(注意最开始的Offer是setLocalDescription,不要搞混)方法加入到了peerConnection中。

事实上到这里必要的工作已经准备完成,但是你肯定想知道你的链接建立的状态,因此我们再注册一个状态监听器来反馈连接的状态:

peerConnection.onconnectionstatechange = function (event) {
    console.log(
      "RTC Connection State Change :",
      peerConnection.connectionState
    );
};

被叫端的流程

前面提到,我们需要让服务器加入以其投屏码命名的“房间”以便信令交互。所以我们可以让页面生成投屏码后向服务器发起Socket注册。

// 发送注册请求,可以携带你想要的数据。
socket.on("connect", function () {
  socket.emit("TV_REGISTER");
});
// 注册成功后服务器发起TV_REGISTER_SUCCESS事件并传回生成的投屏码
socket.on("TV_REGISTER_SUCCESS", function (config) {
  that.code = config.projCode || "获取投屏码失败";
  console.log("Regist Successful, config:", config);
});

第一条“connect”是定义好的事件,将在socket建立成功后触发。我在这里的思路是服务器生成投屏码,再下发过去。当然也可以TV生成然后去服务器“报备”。(默默说一句其实我觉得客户端生成好,要不然断链以后服务端很可能返回另一个投屏码,在网络不好的环境下每次重连都是新的房间就没办法实现自动恢复了,打算之后有空改一下,生成以后存在localStorage里)

被叫端和呼叫端差不多,甚至更为简单。大部分由注册的监听器来完成

首先我们需要创建RTCPeerConnection

peerConnection = new RTCPeerConnection(configuration);

我们需要收到呼叫端的Offer并创建Answer:

socket.on("RTC_Client_Offer_To_TV", async (data) => {
  console.log("RTC_Client_Offer_To_TV");
  if (data.offer) {
    peerConnection.setRemoteDescription(
      new RTCSessionDescription(data.offer)
    );
 // 是呼叫方的Offer,放Remote
    // 创建Answer,并保存为Local
    const answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);
    // 利用信令回复Answer
    socket.emit("RTC_TV_Answer_To_Server", { answer: answer });
  }
});

我们通过注册一个socket事件,当服务器返回RTC_Client_Offer_To_TV事件时,提取出Offer并保存,生成Answer并发布RTC_TV_Answer_To_Server事件让服务器转发给发起端。

类似于呼叫方,注册ICE事件监听器和RTC状态变化监听器(见呼叫方代码,一模一样)

此外,我们需要将视频流提取出来,并作为视频源给到到页面上的video元素中。

peerConnection.onaddstream = (event) => {
    player = document.getElementById('video');
    player.srcObject = event.stream;
}

// 若您在呼叫方使用Track而不是用Stram,则注册这个
peerConnection.addEventListener('track', async (event) => {
    player = document.getElementById('video');
    player.srcObject = remoteStream;
    remoteStream.addTrack(event.track, remoteStream);
});

至此,所有的客户端和投屏端配置已经完成。接下来就要进行后端服务器开发了。我将会在以后的文章中写如何建立websocket信令服务和如何部署TURN/STUN服务器并解释TURN服务器的动态身份验证机制。感谢阅读。



Tags:WEBRtc   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
WebRTC vs WebSocket?WebRTC + WebSocket?
WebRTC vs WebSocket1.什么是WebRTC?WebRTC 是一组 JavaScript API,可以在两个浏览器之间建立点对点连接, 实现音频和视频等数据的传输,也可以用它创建有语音/视频通话功能的应...【详细内容】
2023-01-04  Search: WEBRtc  点击:(526)  评论:(0)  加入收藏
WEBRtc 实现视频会议
WebRTC(Web Real-Time Communication)是为了让开发者在浏览器实现多媒体交换的技术,于2011年被加入W3C规范。当前的支持情况可以见下图。 WebRTC的核心在于建立PeerConnection...【详细内容】
2022-11-11  Search: WEBRtc  点击:(664)  评论:(0)  加入收藏
Android WebRTC 对 AudioRecord 的使用
AudioRecord 是 Android 基于原始PCM音频数据录制的类,WebRCT 对其封装的代码位置位于 org/webrtc/audio/WebRtcAudioRecord.java,接下来我们学习一下 AudioRecord 是如何创建...【详细内容】
2022-01-13  Search: WEBRtc  点击:(394)  评论:(0)  加入收藏
音视频流媒体开发WebRTC 基础知识 -- ICE 交互总结
【网络通信 -- WebRTC】WebRTC 基础知识 -- ICE 交互总结【1】ICE 的一般概念简介ICE 角色offer (主动发起)的一方为 controlling 角色answer (被动接受)的一方为 controlle...【详细内容】
2021-11-30  Search: WEBRtc  点击:(438)  评论:(0)  加入收藏
IOS技术分享|WebRTC iOS源码下载&编译
前言最近对 WebRTC iOS 端源码进行了下载和编译,网上针对 WebRTC iOS 端的编译文章基本都是几年前的,有些地方已经不适用于最新版的 WebRTC 的编译,简单记录下载&编译的过程,以...【详细内容】
2021-11-10  Search: WEBRtc  点击:(732)  评论:(0)  加入收藏
WebRTC 音视频同步原理与实现
所有的基于网络传输的音视频采集播放系统都会存在音视频同步的问题,作为现代互联网实时音视频通信系统的代表,WebRTC 也不例外。本文将对音视频同步的原理以及 WebRTC 的实现...【详细内容】
2021-08-17  Search: WEBRtc  点击:(407)  评论:(0)  加入收藏
使用Swoole协程实现 WebRTC 信令服务器
一、什么是WebRTCWebRTC技术是激烈的开放的Web战争中一大突破-Brendan Eich, inventor of JavaScript。简单来说,WebRTC 是一个音视频处理+及时通讯的开源库。在实时通信中,音...【详细内容】
2020-08-12  Search: WEBRtc  点击:(543)  评论:(0)  加入收藏
▌简易百科推荐
Qt与Flutter:在跨平台UI框架中哪个更受欢迎?
在跨平台UI框架领域,Qt和Flutter是两个备受瞩目的选择。它们各自具有独特的优势,也各自有着广泛的应用场景。本文将对Qt和Flutter进行详细的比较,以探讨在跨平台UI框架中哪个更...【详细内容】
2024-04-12  刘长伟    Tags:UI框架   点击:(1)  评论:(0)  加入收藏
Web Components实践:如何搭建一个框架无关的AI组件库
一、让人又爱又恨的Web ComponentsWeb Components是一种用于构建可重用的Web元素的技术。它允许开发者创建自定义的HTML元素,这些元素可以在不同的Web应用程序中重复使用,并且...【详细内容】
2024-04-03  京东云开发者    Tags:Web Components   点击:(8)  评论:(0)  加入收藏
Kubernetes 集群 CPU 使用率只有 13% :这下大家该知道如何省钱了
作者 | THE STACK译者 | 刘雅梦策划 | Tina根据 CAST AI 对 4000 个 Kubernetes 集群的分析,Kubernetes 集群通常只使用 13% 的 CPU 和平均 20% 的内存,这表明存在严重的过度...【详细内容】
2024-03-08  InfoQ    Tags:Kubernetes   点击:(19)  评论:(0)  加入收藏
Spring Security:保障应用安全的利器
SpringSecurity作为一个功能强大的安全框架,为Java应用程序提供了全面的安全保障,包括认证、授权、防护和集成等方面。本文将介绍SpringSecurity在这些方面的特性和优势,以及它...【详细内容】
2024-02-27  风舞凋零叶    Tags:Spring Security   点击:(55)  评论:(0)  加入收藏
五大跨平台桌面应用开发框架:Electron、Tauri、Flutter等
一、什么是跨平台桌面应用开发框架跨平台桌面应用开发框架是一种工具或框架,它允许开发者使用一种统一的代码库或语言来创建能够在多个操作系统上运行的桌面应用程序。传统上...【详细内容】
2024-02-26  贝格前端工场    Tags:框架   点击:(47)  评论:(0)  加入收藏
Spring Security权限控制框架使用指南
在常用的后台管理系统中,通常都会有访问权限控制的需求,用于限制不同人员对于接口的访问能力,如果用户不具备指定的权限,则不能访问某些接口。本文将用 waynboot-mall 项目举例...【详细内容】
2024-02-19  程序员wayn  微信公众号  Tags:Spring   点击:(39)  评论:(0)  加入收藏
开发者的Kubernetes懒人指南
你可以将本文作为开发者快速了解 Kubernetes 的指南。从基础知识到更高级的主题,如 Helm Chart,以及所有这些如何影响你作为开发者。译自Kubernetes for Lazy Developers。作...【详细内容】
2024-02-01  云云众生s  微信公众号  Tags:Kubernetes   点击:(51)  评论:(0)  加入收藏
链世界:一种简单而有效的人类行为Agent模型强化学习框架
强化学习是一种机器学习的方法,它通过让智能体(Agent)与环境交互,从而学习如何选择最优的行动来最大化累积的奖励。强化学习在许多领域都有广泛的应用,例如游戏、机器人、自动驾...【详细内容】
2024-01-30  大噬元兽  微信公众号  Tags:框架   点击:(68)  评论:(0)  加入收藏
Spring实现Kafka重试Topic,真的太香了
概述Kafka的强大功能之一是每个分区都有一个Consumer的偏移值。该偏移值是消费者将读取的下一条消息的值。可以自动或手动增加该值。如果我们由于错误而无法处理消息并想重...【详细内容】
2024-01-26  HELLO程序员  微信公众号  Tags:Spring   点击:(88)  评论:(0)  加入收藏
SpringBoot如何实现缓存预热?
缓存预热是指在 Spring Boot 项目启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制。那么问题来了,在 Spring Boot 项目启动之后,在什么时候?在哪里可以将数据加载到缓存系...【详细内容】
2024-01-19   Java中文社群  微信公众号  Tags:SpringBoot   点击:(86)  评论:(0)  加入收藏
站内最新
站内热门
站内头条