demo:https://github.com/avdance/webrtc_web
Web中文兴趣组会议:https://github.com/w3c/chinese-ig/blob/main/Meetings.md
webRTC标准:https://www.w3.org/TR/webrtc/#intro
webRTC实现:http://webrtc.github.io/webrtc-org/architecture/
webRTC例子:https://webrtc.github.io/samples/
getUserMedia:https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/getUserMedia
Audio/Video playback:https://www.chromium.org/developers/design-documents/video
媒体源扩展(MSE):https://developer.mozilla.org/zh-CN/docs/Web/API/Media_Source_Extensions_API

  • 有精确的控制时间、媒体质量和内存释放等需求时

加密媒体扩展(EME):https://zh.wikipedia.org/wiki/%E5%8A%A0%E5%AF%86%E5%AA%92%E4%BD%93%E6%89%A9%E5%B1%95

基本概念

  • 摄像头:用于捕捉(采集)图像和视频。
  • 帧率:摄像头一秒钟采集图像的次数称为帧率。帧率越高,视频就越平滑流畅。然而,在直播系统中一般不会设置太高的帧率,因为帧率越高,占的网络带宽就越多。一般情况下,一秒钟可以采集 30 张以上的图像,一些好的摄像头甚至可以采集 100 张以上。
  • 分辨率:常见的分辨率有 2K、1080P、720P、420P 等。分辨率越高图像就越清晰,但同时也带来一个问题,即占用的带宽也就越多。所以,在直播系统中,分辨率的高低与网络带宽有紧密的联系。也就是说,分辨率会跟据你的网络带宽进行动态调整。
  • 宽高比:分辨率一般分为两种宽高比,即 16:9 或 4:3。
  • 麦克风:用于采集音频数据
  • 采样率:一秒内采样的次数。每个采样用几个 bit 表示,称为采样位深或采样大小
  • 轨(Track):每条轨数据都是独立的,不会与其他轨相交,如 MP4 中的音频轨、视频轨,它们在 MP4 文件中是被分别存储的
  • 流(Stream):可以理解为容器。在 WebRTC 中,“流”可以分为媒体流(MediaStream)和数据流(DataStream)。其中,媒体流可以存放 0 个或多个音频轨或视频轨;数据流可以存 0 个或多个数据轨

二进制数据类型

ArrayBuffer

表示通用的、固定长度的二进制数据缓冲区。
但并不能直接对 ArrayBuffer 对象进行访问,在物理内存中并不存在这样一个对象,必须使用其封装类进行实例化后才能进行访问。

  1. let buffer = new ArrayBuffer(16); // 创建一个长度为 16 的 buffer,不可访问
  2. let view = new Uint32Array(buffer); // 可以访问

ArrayBufferView

ArrayBufferView 指的是 Int8Array、Uint8Array、DataView 等类型的总称,而这些类型都是使用 ArrayBuffer 类实现的

Blob

Blob(Binary Large Object)的底层是由 ArrayBuffer 对象的封装类实现的。

  1. var aBlob = new Blob( array, options );
  • array 可以是ArrayBuffer、ArrayBufferView、Blob、DOMString等类型 ;
  • option,用于指定存储成的媒体类型。

2021 年 WebRTC1.0 正式成为各大浏览器厂商的推荐标准。

image.png

  • 两个 WebRTC 终端: 负责音视频采集、编解码、NAT 穿越、音视频数据传输。
  • Signal 服务器: 负责信令处理,如加入房间、离开房间、媒体协商消息的传递等。
  • STUN/TURN 服务器: 负责获取 WebRTC 终端在公网的 IP 地址,以及 NAT 穿越失败后的数据中转。

WebRTC 进行音视频通话的大体过程

  • 当一端(WebRTC 终端)进入房间之前,它首先会检测自己的设备是否可用。如果此时设备可用,则进行音视频数据采集。
  • 采集到的数据一方面可以做预览,也就是让自己可以看到自己的视频;另一方面,可以将其录制下来保存成文件,等到视频通话结束后,上传到服务器让用户回看之前的内容。
  • 在获取音视频数据就绪后,WebRTC 终端要发送 “加入” 信令到 Signal 服务器。Signal 服务器收到该消息后会创建房间。在另外一端,也要做同样的事情,只不过它不是创建房间,而是加入房间了。待第二个终端成功加入房间后,第一个用户会收到 “另一个用户已经加入成功” 的消息。
  • 此时,第一个终端将创建 “媒体连接” 对象,即RTCPeerConnection,并将采集到的音视频数据通过 RTCPeerConnection 对象进行编码,最终通过 P2P 传送给对端。
  • 当然,在进行 P2P 穿越时很有可能失败。所以,当 P2P 穿越失败时,为了保障音视频数据仍然可以互通,则需要通过 TURN 服务器进行音视频数据中转。
  • 这样,当音视频数据 “历尽千辛万苦” 来到对端后,对端首先将收到的音视频数据进行解码,最后再将其展示出来,这样就完成了一端到另一端的单通。如果双方要互通,那么,两方都要通过 RTCPeerConnection 对象传输自己一端的数据,并从另一端接收数据。

兼容性(adapter.js)

  1. ...
  2. <script src="https://webrtc.github.io/adapter/adapter-latest.js </script>
  3. ...

音视频检测

访问这个链接可以测试浏览器音视频设备是否有问题:https://webdemo.agora.io/agora_webrtc_troubleshooting/
image.png

音视频设备的基本原理

:::info 奈奎斯特定理: 在进行模拟 / 数字信号的转换过程中,当采样率大于信号中最高频率的 2 倍时,采样之后的数字信号就完整地保留了原始信号中的信息。 :::

音频采集:人类听觉范围的频率是 20Hz~20kHz 之间,你需要将音频输入设备的采样率设置在 40kHz 以上,这样才能完整地将原始信号保留下来。采集到的数据再经过量化、编码,最终形成数字信号。

视频采集:当实物光通过镜头进行到摄像机后,它会通过视频设备的模数转换(A/D)模块,即光学传感器, 将光转换成数字信号,即 RGB(Red、Green、Blue)数据。获得 RGB 数据后,还要通过 DSP(Digital Signal Processer)进行优化处理,如自动增强、白平衡、色彩饱和等都属于这一阶段要做的事情。

MediaDevices:该接口提供了访问(连接到计算机上的)媒体设备(如摄像头、麦克风)以及截取屏幕的方法。实际上,它允许你访问任何硬件媒体设备

  • enumerateDevices() 方法就可以获取到媒体输入和输出设备列表,例如: 麦克风、相机、耳机等

MediaDeviceInfo,它表示的是每个输入 / 输出设备的信息:

  • deviceID,设备的唯一标识
  • label,设备名称
  • kind,设备种类,可用于识别出是音频设备还是视频设备,是输入设备还是输出设备

音视频设备的检测流程

先排查视频设备,然后再排查音频设备。因此,需要调用两次 getUserMedia API 进行设备检测。
第一次,调用 getUserMedia API 只采集视频数据并将其展示出来。如果用户能看到自己的视频,说明视频设备是有效的;否则,设备无效,可以再次选择不同的视频设备进行重新检测。
第二次,如果用户视频检测通过了,再次调用 getUserMedia API 时,则只采集音频数据。由于音频数据不能直接展示,所以需要使用 JavaScript 中的 AudioContext 对象,将采集到的音频计算后,再将其绘制到页面上。这样,当用户看到音频数值的变化后,说明音频设备也是有效的。

音视频采集

基本概念

非编码帧

:::info 当你要播放某个视频文件时,播放器会按照一定的时间间隔连续地播放从音视频文件中解码后的视频帧,这样视频就动起来了。
而从摄像头获取的本来就是非编码视频帧,所以就不需要解码了。 :::

  • 播放的视频帧之间的时间间隔是非常小的。如按每秒钟 20 帧的帧率计算,每帧之间的间隔是 50ms。
  • 播放器播的是非编码帧(解码后的帧),这些非编码帧就是一幅幅独立的图像。

从摄像头里采集的帧或通过解码器解码后的帧都是非编码帧。非编码帧的格式一般是 YUV 格式或是 RGB 格式

编码帧

通过编码器(如 H264/H265、VP8/VP9)压缩后的帧称为编码帧

H264 编码的帧包括以下三种类型:

  • I 帧:关键帧。压缩率低,可以单独解码成一幅完整的图像。
  • P 帧:参考帧。压缩率较高,解码时依赖于前面已解码的数据。
  • B 帧:前后参考帧。压缩率最高,解码时不光依赖前面已经解码的帧,而且还依赖它后面的 P 帧。换句话说就是,B 帧后面的 P 帧要优先于它进行解码,然后才能将 B 帧解码。 :::info 拍照的过程其实是从连续播放的一幅幅画面中抽取正在显示的那张画面。 :::

采集

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Realtime communication with WebRTC</title>
  5. <link rel="stylesheet", href="css/client.css" />
  6. </head>
  7. <body>
  8. <h1>Realtime communication with WebRTC </h1>
  9. <video autoplay playsinline></video> /* autoplay,表示当页面加载时可以自动播放视频,playsinline,表示在 HTML5 页面内播放视频,而不是使用系统播放器播放视频。*/
  10. <script src="js/client.js"></script>
  11. </body>
  12. </html>

:::info 移动端切换前置/后置摄像头可通过设置video的属性值facingMode来处理,分别为user和environment :::

js/client.js

  1. // const mediaStreamContrains = {
  2. // video: true,
  3. // audio: true
  4. // };
  5. const mediaStreamContrains = {
  6. video: {
  7. frameRate: {min: 20}, // 帧率最小 20 帧每秒
  8. width: {min: 640, ideal: 1280}, // 宽度最小是 640,理想的宽度是 1280
  9. height: {min: 360, ideal: 720}, // 高度最小是 360,最理想高度是 720
  10. aspectRatio: 16/9 // 宽高比是 16:9
  11. },
  12. audio: {
  13. echoCancellation: true, // 开启回音消除功能
  14. noiseSuppression: true, // 开启降噪功能
  15. autoGainControl: true // 开启自动增益功能
  16. }
  17. };
  18. const localVideo = document.querySelector('video');
  19. function gotLocalMediaStream(mediaStream){
  20. localVideo.srcObject = mediaStream;
  21. }
  22. function handleLocalMediaStreamError(error){
  23. console.log('navigator.getUserMedia error: ', error);
  24. }
  25. var promise = navigator.mediaDevices.getUserMedia(mediaStreamContrains).then(
  26. gotLocalMediaStream
  27. ).catch(
  28. handleLocalMediaStreamError
  29. );;

getUserMedia API 控制设备的参数

image.png

拍照

  1. ...
  2. <button id="TakePhoto">Take</button>
  3. ...
  4. <canvas id="picture"></canvas>
  5. ...
  1. ...
  2. var picture = document.querySelector('canvas#picture');
  3. picture.width = 640;
  4. picture.height = 480;
  5. ...
  6. picture.getContext('2d').drawImage(localVideo, 0, 0, picture.width, picture.height);
  7. ...

:::info void ctx.drawImage(image, dx, dy, dWidth, dHeight); :::

  • image:可以是一幅图片,或 HTMLVideoElement。
  • dx, dy:图片起点的 x、y 坐标。
  • dWidth:图片的宽度。
  • dHeight:图片的高度。

音视频录制

服务端录制

优点是不用担心客户因自身电脑问题造成录制失败(如磁盘空间不足),也不会因录制时抢占资源(CPU 占用率过高)而导致其他应用出现问题等;
缺点是实现的复杂度很高,本地高清的视频在上传服务端时由于网络带宽不足,视频的分辨率很有可能会被自动缩小到了 640x360,这就导致用户回看时视频特别模糊,用户体验差。

客户端录制

优点是方便录制方(如老师)操控,并且所录制的视频清晰度高,实现相对简单。
缺点就是录制失败率高。因为客户端在进行录制时会开启第二路编码器,这样会特别耗 CPU。而 CPU 占用过高后,就很容易造成应用程序卡死。除此之外,它对内存、硬盘的要求也特别高。

基本原理

录制后音视频流的存储格式是什么呢?

存储格式的选择对于录制后的回放至关重要。是直接录制原始数据,还是录制成某种多媒体格式(如 MP4 )?

录制下来的音视频流如何播放?

使用普通的播放器播放,还是使用私有播放器播呢?

如果你的业务是多人互动类型,且回放时也要和直播时一样,那么你就必须使用私有播放器,因为普通播放器是不支持同时播放多路视频的。
还有,如果你想让浏览器也能观看回放,那么就需要提供网页播放器。

启动录制后多久可以回放呢?

  • 边录边看,即开始录制几分钟之后用户就可以观看了。
  • 录制完立即回放,即当录制结束后,用户就可以回看录制了。
  • 录完后过一段时间可观看。录制完成后可以对音视频做一些剪辑、转码,制作各种不同的清晰度的回放等等。

    录制

    :::info 只能够实现一路视频和一路音视流的情况
    对于多路音视频流,录制时一般不是录制音视频数据,而是录制桌面加音频,这样就大大简化了客户端实现录制的复杂度了。 :::
  1. <html>
  2. ...
  3. <body>
  4. ...
  5. <button id="record">Start Record</button> <!--开启录制-->
  6. <button id="recplay" disabled>Play</button> <!--播放录制下来的内容-->
  7. <button id="download" disabled>Download</button><!--将录制的视频下载下来-->
  8. ...
  9. </body>
  10. </html>
  1. ...
  2. var buffer;
  3. ...
  4. // 当该函数被触发后,将数据压入到 blob 中
  5. function handleDataAvailable(e){
  6. if(e && e.data && e.data.size > 0){
  7. buffer.push(e.data);
  8. }
  9. }
  10. function startRecord(){
  11. buffer = [];
  12. // 设置录制下来的多媒体格式
  13. var options = {
  14. mimeType: 'video/webm;codecs=vp8'
  15. }
  16. // 判断浏览器是否支持录制
  17. if(!MediaRecorder.isTypeSupported(options.mimeType)){
  18. console.error(`${options.mimeType} is not supported!`);
  19. return;
  20. }
  21. try{
  22. // 创建录制对象
  23. mediaRecorder = new MediaRecorder(window.stream, options);
  24. }catch(e){
  25. console.error('Failed to create MediaRecorder:', e);
  26. return;
  27. }
  28. // 当有音视频数据来了之后触发该事件
  29. mediaRecorder.ondataavailable = handleDataAvailable;
  30. // 开始录制
  31. mediaRecorder.start(10);
  32. }
  33. ...

共享桌面

:::info 桌面可以当作一种特殊的视频数据来看待 :::

基本原理

抓屏、压缩编码、传输、解码、显示、控制

  • 共享者

    • 每秒钟抓取多次屏幕(可以是 3 次、5 次等),每次抓取的屏幕都与上一次抓取的屏幕做比较,取它们的差值,然后对差值进行压缩;
    • 如果是第一次抓屏或切幕的情况,即本次抓取的屏幕与上一次抓取屏幕的变化率超过 80% 时,就做全屏的帧内压缩,其过程与 JPEG 图像压缩类似。
    • 最后再将压缩后的数据通过传输模块传送到观看端;数据到达观看端后,再进行解码,这样即可还原出整幅图片并显示出来。
  • 远程控制端

    • 当用户通过鼠标点击共享桌面的某个位置时,会首先计算出鼠标实际点击的位置,
    • 然后将其作为参数,通过信令发送给共享端。
    • 共享端收到信令后,会模拟本地鼠标,即调用相关的 API,完成最终的操作。
    • 一般情况下,当操作完成后,共享端桌面也发生了一些变化,此时就又回到上面共享者的流程了

:::info 跨段远程桌面控制协议:VNC(Virtual Network Console)

  • 桌面数据:包括了桌面的抓取 (采集)、编码(压缩)、传输、解码和渲染。
  • 信令控制:包括键盘事件、鼠标事件以及接收到这些事件消息后的相关处理等。 :::

  • 共享端桌面数据的采集: WebRTC 对于桌面的采集与 RDP/VNC 使用的技术是相同的,都是利用各平台所提供的相关 API 进行桌面的抓取

  • 共享端桌面数据的编码:
    • WebRTC 对桌面的编码使用的是视频编码技术,即 H264/VP8 等;
    • 但 RDP/VNC 则不一样,它们使用的是图像压缩技术。使用视频编码技术的好处是压缩率高,而坏处是在网络不好的情况下会有模糊等问题。
  • 传输: 编码后的桌面数据会通过流媒体传输协议发送到观看端
  • 观看端解码:
    • WebRTC 对收到的桌面数据通过视频解码技术解码,
    • 而 RDP/VNC 使用的是图像解码技术
  • 观看端渲染: 一般会通过 OpenGL/D3D 等 GPU 进行渲染

共享桌面流程

:::warning 不能在采集桌面的同时采集音频 :::

  1. var promise = navigator.mediaDevices.getDisplayMedia(constraints);
  2. ...
  3. // 得到桌面数据流
  4. function getDeskStream(stream){
  5. localStream = stream;
  6. }
  7. // 抓取桌面
  8. function shareDesktop(){
  9. // 只有在 PC 下才能抓取桌面
  10. if(IsPC()){
  11. // 开始捕获桌面数据
  12. navigator.mediaDevices.getDisplayMedia({video: true})
  13. .then(getDeskStream)
  14. .catch(handleError);
  15. return true;
  16. }
  17. return false;
  18. }
  19. ...

协议

UDP 协议

:::info 实时互动直播系统应该采用UDP协议 :::

举个例子,A 与 B 通讯,A 首先向 B 发送数据,并启动一个定时器
当 B 收到 A 的数据后,B 需要给 A 回一个ACK(确认)消息,反复这样操作,数据就源源不断地从 A 流向了 B。
如果因为某些原因,A 一直收不到 B 的确认消息会怎么办呢?
当 A 的定时器超时后,A 将重发之前没有被确认的消息,并重新设置定时器。

在 TCP 协议中,为了避免重传次数过多,定时器的超时时间会按 2 的指数增长
也就是说,假设第一次设置的超时时间是 1 秒,那么第二次就是 2 秒,第三次是 4 秒……第七次是 64 秒。
如果第七次之后仍然超时,则断开 TCP 连接
你可以计算一下,从第一次超时,到最后断开连接,这之间一共经历了 2 分 07 秒,是不是很恐怖?

如果遇到前面的情况,A 与 B 之间的连接断了,那还算是个不错的情况,因为还可以再重新建立连接。
但如果在第七次重传后,A 收到了 B 的 ACK 消息,那么 A 与 B 之间的数据传输的延迟就达到 1 分钟以上。
对于这样的延迟,实时互动的直播系统是根本无法接受的。

RTP 协议

:::info

RTP 协议

先给音视频数据加个 RTP 头,然后再交给 UDP 进行传输.

  • 序号:用于标识传输包的序号,这样就可以知道这个包是第几个分片了。
  • 起始标记:记录分帧的第一个 UDP 包。
  • 结束标记:记录分帧的最后一个 UDP 包。 :::

image.png
image.png

RTCP 协议

:::info

RTCP 协议

通过报文交换让各端都知道它们自己的网络质量到底是怎样的

  • RR(Reciever Report)
  • SR(Sender Report)
  • FIR(Full Intra Request (FIR) Command)

    • Header:标识该报文的类型,比如是 SR 还是 RR
    • Sender info:用于指明作为发送方,到底发了多少包
    • Report block:指明发送方作为接收方时,它从各个 SSRC 接收包的情况 :::

image.png
image.png
image.png

webRTC 的核心

:::info WebRTC 的核心不是音视频引擎,也不是网络传输,而是SDP(Session Description Protocal)。

  • SDP(Session Description Protocal):用文本描述的各端(PC 端、Mac 端、Android 端、iOS 端等)的能力
  • 能力指的是各端所支持的音频编解码器是什么,这些编解码器设定的参数是什么,使用的传输协议是什么,以及包括的音视频媒体是什么等等 :::

信令交互的一个重要信息就是 SDP 的交换
交换 SDP 的目的是为了让对方知道彼此具有哪些能力
然后根据双方各自的能力进行协商,协商出大家认可的音视频编解码器、编解码器相关的参数(如音频通道数,采样率等)、传输协议等信息。

SDP 规范

  1. v=0
  2. o=- 3409821183230872764 2 IN IP4 127.0.0.1
  3. s=-
  4. t=0 0
  5. ...
  6. m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
  7. m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 122 127 121 125 107 108 109 124 120 123 119 114 115 116
  8. ...
  9. a=rtpmap:111 opus/48000/2
  10. a=rtpmap:103 ISAC/16000
  11. a=rtpmap:104 ISAC/32000
  12. ...

会话描述

  • v=(protocol version,必选)。例子:v=0 ,表示 SDP 的版本号,但不包括次版本号
  • o=(owner/creator and session identifier,必选):o=
    ,该例子是对一个会话发起者的描述
    • o= 表示的是对会话发起者的描述
    • :用户名,当不关心用户名时,可以用 “-” 代替
    • :数字串,在整个会话中,必须是唯一的,建议使用 NTP 时间戳;
    • :版本号,每次会话数据修改后,该版本值会递增;
    • :网络类型,一般为“IN”,表示“internet”;
    • :地址类型,一般为 IP4;
    • :IP 地址。
  • Session Name(必选)。例子:s=,该例子表示一个会话,在整个 SDP 中有且只有一个会话,也就是只有一个 s=。
  • t=(time the session is active,必选)。例子:t= ,该例子描述了会话的开始时间和结束时间。其中, 为 NTP 时间,单位是秒;当均为零时,表示持久会话。


媒体描述

  • m=(media name and transport address,可选)
    • :媒体类型,比如 audio/video 等;
    • :端口;
    • :传输协议,有两种——RTP/AVP 和 UDP;
    • :媒体格式,即数据负载类型 (Payload Type) 列表

SDP 中描述了一路音频流,即m=audio,该音频支持的 Payload ( 即数据负载 ) 类型包括 111、103、104 等等。
在该 SDP 片段中又进一步对 111、103、104 等 Payload 类型做了更详细的描述,
a=rtpmap:111 opus/48000/2 表示 Payload 类型为 111 的数据是 OPUS 编码的音频数据,并且它的采样率是 48000,使用双声道。

WebRTC 对 SDP 规范的修改

  1. //============= 会话描述 ====================
  2. v=0
  3. o=- 7017624586836067756 2 IN IP4 127.0.0.1
  4. s=-
  5. t=0 0
  6. ...
  7. //================ 媒体描述 =================
  8. //================ 音频媒体 =================
  9. /*
  10. * 音频使用端口 1024 收发数据
  11. * UDP/TLS/RTP/SAVPF 表示使用 dtls/srtp 协议对数据加密传输
  12. * 111、103 ... 表示本会话音频数据的 Payload Type
  13. */
  14. m=audio 1024 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126
  15. //============== 网络描述 ==================
  16. // 指明接收或者发送音频使用的 IP 地址,由于 WebRTC 使用 ICE 传输,这个被忽略。
  17. c=IN IP4 0.0.0.0
  18. // 用来设置 rtcp 地址和端口,WebRTC 不使用
  19. a=rtcp:9 IN IP4 0.0.0.0
  20. ...
  21. //============== 音频安全描述 ================
  22. //ICE 协商过程中的安全验证信息
  23. a=ice-ufrag:khLS
  24. a=ice-pwd:cxLzteJaJBou3DspNaPsJhlQ
  25. a=fingerprint:sha-256 FA:14:42:3B:C7:97:1B:E8:AE:0C2:71:03:05:05:16:8F:B9:C7:98:E9:60:43:4B:5B:2C:28:EE:5C:8F3:17
  26. ...
  27. //============== 音频流媒体描述 ================
  28. a=rtpmap:111 opus/48000/2
  29. //minptime 代表最小打包时长是 10ms,useinbandfec=1 代表使用 opus 编码内置 fec 特性
  30. a=fmtp:111 minptime=10;useinbandfec=1
  31. ...
  32. a=rtpmap:103 ISAC/16000
  33. a=rtpmap:104 ISAC/32000
  34. a=rtpmap:9 G722/8000
  35. ...
  36. //================= 视频媒体 =================
  37. m=video 9 UDP/TLS/RTP/SAVPF 100 101 107 116 117 96 97 99 98
  38. ...
  39. //================= 网络描述 =================
  40. c=IN IP4 0.0.0.0
  41. a=rtcp:9 IN IP4 0.0.0.0
  42. ...
  43. //================= 视频安全描述 =================
  44. a=ice-ufrag:khLS
  45. a=ice-pwd:cxLzteJaJBou3DspNaPsJhlQ
  46. a=fingerprint:sha-256 FA:14:42:3B:C7:97:1B:E8:AE:0C2:71:03:05:05:16:8F:B9:C7:98:E9:60:43:4B:5B:2C:28:EE:5C:8F3:17
  47. ...
  48. //================ 视频流描述 ===============
  49. a=mid:video
  50. ...
  51. a=rtpmap:100 VP8/90000
  52. //================ 服务质量描述 ===============
  53. a=rtcp-fb:100 ccm fir
  54. a=rtcp-fb:100 nack // 支持丢包重传,参考 rfc4585
  55. a=rtcp-fb:100 nack pli
  56. a=rtcp-fb:100 goog-remb // 支持使用 rtcp 包来控制发送方的码流
  57. a=rtcp-fb:100 transport-cc
  58. ...

媒体协商

:::info 媒体协商的作用就是让双方找到共同支持的媒体能力,如双方都支持的编解码器,从而最终实现彼此之间的音视频通信 ::: image.png

  • Offer,在双方通讯时,呼叫方发送的 SDP 消息
  • Answer,在双方通讯时,被呼叫方发送的 SDP 消息
  • createOffer ,创建 Offer;
  • createAnswer,创建 Answer;
  • setLocalDescription,设置本地 SDP 信息;
  • setRemoteDescription,设置远端的 SDP 信息。
  • onicecandidate
  • onaddstream

呼叫方

  1. ...
  2. var pcConfig = null;
  3. var pc = new RTCPeerConnection(pcConfig);
  4. ...
  5. // 呼叫方创建 Offer
  6. function doCall() {
  7. console.log('Sending offer to peer');
  8. pc.createOffer(setLocalAndSendMessage, handleCreateOfferError);
  9. }
  10. // 将本地 SDP 描述信息设置到 WebRTC 的 Local 域
  11. function setLocalAndSendMessage(sessionDescription) {
  12. pc.setLocalDescription(sessionDescription);
  13. sendMessage(sessionDescription);
  14. }
  15. // 呼叫方收到 Answer, 将收到的会话描述设置为一个远程会话
  16. socket.on('message', function(message) {
  17. ...
  18. } else if (message.type === 'answer') {
  19. pc.setRemoteDescription(new RTCSessionDescription(message));
  20. } else if (...) {
  21. ...
  22. }
  23. ....
  24. });

被呼叫方

  1. // 被呼叫方收到 Offer,设置呼叫方发送给它的 Offer 作为远端描述
  2. socket.on('message', function(message) {
  3. ...
  4. } else if (message.type === 'offer') {
  5. pc.setRemoteDescription(new RTCSessionDescription(message));
  6. doAnswer();
  7. } else if (...) {
  8. ...
  9. }
  10. ....
  11. });
  12. // 被呼叫方创建 Answer.会生成一个与远程会话兼容的本地会话,并最终将该会话描述发送给呼叫方。
  13. function doAnswer() {
  14. pc.createAnswer().then(
  15. setLocalAndSendMessage,
  16. onCreateSessionDescriptionError
  17. );
  18. }

:::warning 需要特别注意的是,通信双方链路的建立是在设置本地媒体能力,即调用 setLocalDescription 函数之后才进行的。 :::

:::info 谁先发起呼叫谁就发offer,另一方发answer;这完全有应用层控制;比如第一个人进入房间后,就在哪里等待,当发现第二个人上来的时候它就给对方发offer 就好了。如果两个人同时进入房间,就在服务器端建个队列,让他们顺序进入就好了,非常好处理对吧?另外两端都发offer 那协商必然失败。 :::

建立连接

Interactive Connectivity Establishment Candidate (ICE 候选者)

  • host 表示本机候选者,优先级是最高的,host 类型之间的连通性检测就是内网之间的连通性检测
  • srflx:尝试让通信双方直接通过 P2P 进行连接,如果连接成功就使用 P2P 传输数据
  • relay:relay 服务器或 TURN 服务器进行连接
    1. {
    2. IP: xxx.xxx.xxx.xxx,
    3. port: number,
    4. type: host/srflx/relay,
    5. priority: number,
    6. protocol: UDP/TCP,
    7. usernameFragment: string
    8. ...
    9. }

    STUN 协议(RFC5389)

    image.png

:::info 即内网地址和端口经 NAT 映射后的外网地址和端口。 :::

在内网的网关上都有 NAT (Net Address Transport) 功能,NAT 的作用就是进行内外网的地址转换

  • 首先在外网搭建一个 STUN 服务器,现在比较流行的 STUN 服务器是 CoTURN,你可以到 GitHub 上自己下载源码编译安装。
  • 当 STUN 服务器安装好后,从内网主机发送一个 binding request 的 STUN 消息到 STUN 服务器。
  • STUN 服务器收到该请求后,会将请求的 IP 地址和端口填充到 binding response 消息中,然后顺原路将该消息返回给内网主机。此时,收到 binding response 消息的内网主机就可以解析 binding response 消息了,并可以从中得到自己的外网 IP 和端口。

NAT 分类为 4 种类型:
类型(A-B) 建立状况
完全锥型-完全锥型 A通过server获得B的IP:port开始通信
完全锥型-IP限制型 B通过server获得A的IP:port开始通信
完全锥型-port限制型 B通过server获得A的IP:port开始通信
完全锥型-对称型 B通过server获得A的IP:port开始通信

image.png

完全锥型 NAT

image.png

IP 限制型 NAT

image.png

端口限制型 NAT

image.png

对称型 NAT

image.png

TURN 协议RFC5766

relay 型候选者的获取也是通过 STUN 协议完成的,只不过它使用的 STUN 消息类型与获取 srflx 型候选者的 STUN 消息的类型不一样而已

信令服务器

:::info 信令服务器就是进行信令的交换,在通信双方彼此连接、传输媒体数据之前,它们要通过信令服务器交换一些信息,如媒体协商。 :::

  • 房间管理。即每个用户都要加入到一个具体的房间里,比如两个用户 A 与 B 要进行通话,那么它们必须加入到同一个房间里。
  • 信令的交换。即在同一个房间里的用户之间可以相互发送信令。

实时通讯的核心(RTCPeerConnection

:::info SDP 是掌握 WebRTC 运行机制的钥匙,而 RTCPeerConnection 是使用 WebRTC 的钥匙。
RTCPeerConnection 类的工作原理与 socket 基本是一样的 :::

  • 端与端之间要建立连接,但它们是如何知道彼此的外网地址呢?
  • 如果两台主机都是在 NAT 之后,它们又是如何穿越 NAT 进行连接的呢?
  • 如果 NAT 穿越不成功,又该如何保证双方之间的连通性呢?
  • 好不容易双方连通了,如果突然丢包了,该怎么办?
  • 如果传输过程中,传输的数据量过大,超过了网络带宽能够承受的负载,又该如何保障音视频的服务质量呢?
  • 传输的音视频要时刻保持同步,这又该如何做到呢?
  • 数据在传输之前要进行音视频编码,而在接收之后又要做音视频解码,但 WebRTC 支持那么多编解码器,如 H264、 H265、 VP8、 VP9 等,它是如何选择的呢?
  1. · <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Realtime communication with WebRTC</title>
  5. <link rel="stylesheet" href="css/main.css" />
  6. </head>
  7. <body>
  8. <h1>Realtime communication with WebRTC</h1>
  9. <video id="localVideo" autoplay playsinline></video>
  10. <video id="remoteVideo" autoplay playsinline></video>
  11. <div>
  12. <button id="startButton">Start</button>
  13. <button id="callButton">Call</button>
  14. <button id="hangupButton">Hang Up</button>
  15. </div>
  16. <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  17. <script src="js/client.js"></script>
  18. </body>
  19. </html>

音视频监控

:::info 数据监控可以评估出目前用户使用的音视频产品的服务质量是好是坏 :::

chrome://webrtc-internals

当页面中有创建RTCPeerConnection 对象之后,chrome内部会记录每个存活的 RTCPeerConnection 对象,通过“chrome://webrtc-internals”就可以从 Chrome 中取出其中的具体内容。

  • 发送的总字节数及总包数
  • 每秒钟发送的字节数和包数
  • 重传的包数和字节数

image.png

getStats()

:::info getStats 可以获取音视频的统计信息。

  • RTCPeerConnection 对象的 getStats 方法获取的是所有的统计信息,除了收发包的统计信息外,还有候选者、证书、编解码器等其他类型的统计信息。
  • RTCRtpSender 对象的 getStats 方法只统计与发送相关的统计信息。
  • RTCRtpReceiver 对象的 getStats 方法则只统计与接收相关的统计信息。 :::
  1. var pc = new RTCPeerConnection.getStats(selector)
  2. ...
  3. // 获得速个连接的统计信息
  4. pc.getStats().then(
  5. // 在一个连接中有很多 report
  6. reports => {
  7. // 遍历每个 report
  8. reports.forEach( report => {
  9. // 将每个 report 的详细信息打印出来
  10. console.log(report);
  11. });
  12. }).catch( err=>{
  13. console.error(err);
  14. });
  15. );
  16. ...
  17. // 从 PC 上获得 sender 对象
  18. var sender = pc.getSenders()[0];
  19. ...
  20. // 调用 sender 的 getStats 方法
  21. sender.getStats()
  22. .then(reports => { // 得到相关的报告
  23. reports.forEach(report =>{ // 遍历每个报告
  24. if(report.type === 'outbound-rtp'){ // 如果是 rtp 输出流
  25. ....
  26. }
  27. }
  28. );
  29. ...

每个 Report 对象包括以下三个字段:

  • id:对象的唯一标识,是一个字符串。
  • timestamp:时间戳,用来标识该条 Report 是什么时间产生的。
  • type:类型,是 RTCStatsType 类型

image.png
RTCStatsType 类型
image.png

数据展示处理

将 Report 转化为图形:

  • 引入第三方库 graph.js;
  • 启动一个定时器,每秒钟绘制一次图形;
  • 在定时器的回调函数中,读取 RTCStats 统计信息,转化为可量化参数,并将其传给 graph.js 进行绘制。

    1. <html>
    2. ...
    3. <body>
    4. ...
    5. // 引入第三方库 graph.js
    6. <script src="js/third_party/graph.js"></script>
    7. ...
    8. </body>
    9. </html>
  1. ...
  2. var pc = null;
  3. // 定义绘制比特率图形相关的变量
  4. var bitrateGraph;
  5. var bitrateSeries;
  6. // 定义绘制发送包图形相关的变理
  7. var packetGraph;
  8. var packetSeries;
  9. ...
  10. pc = new RTCPeerConnection(null);
  11. ...
  12. //bitrateSeries 用于绘制点
  13. bitrateSeries = new TimelineDataSeries();
  14. //bitrateGraph 用于将 bitrateSeries 绘制的点展示出来
  15. bitrateGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas');
  16. bitrateGraph.updateEndDate(); // 绘制时间轴
  17. // 与上面一样,只不是用于绘制包相关的图
  18. packetSeries = new TimelineDataSeries();
  19. packetGraph = new TimelineGraphView('packetGraph', 'packetCanvas');
  20. packetGraph.updateEndDate();
  21. ...
  22. // 每秒钟获取一次 Report,并更新图形
  23. window.setInterval(() => {
  24. if (!pc) { // 如果 pc 没有创建直接返回
  25. return;
  26. }
  27. // 从 pc 中获取发送者对象
  28. const sender = pc.getSenders()[0];
  29. if (!sender) {
  30. return;
  31. }
  32. sender.getStats().then(res => { // 获取到所有的 Report
  33. res.forEach(report => { // 遍历每个 Report
  34. let bytes;
  35. let packets;
  36. // 我们只对 outbound-rtp 型的 Report 做处理
  37. if (report.type === 'outbound-rtp') {
  38. if (report.isRemote) { // 只对本地的做处理
  39. return;
  40. }
  41. const now = report.timestamp;
  42. bytes = report.bytesSent; // 获取到发送的字节
  43. packets = report.packetsSent; // 获取到发送的包数
  44. // 因为计算的是每秒与上一秒的数据的对比,所以这里要做个判断
  45. // 如果是第一次就不进行绘制
  46. if (lastResult && lastResult.has(report.id)) {
  47. // 计算这一秒与上一秒之间发送数据的差值
  48. var mybytes= (bytes - lastResult.get(report.id).bytesSent);
  49. // 计算走过的时间,因为定时器是秒级的,而时间戳是豪秒级的
  50. var mytime = (now - lastResult.get(report.id).timestamp);
  51. const bitrate = 8 * mybytes / mytime * 1000; // 将数据转成比特位
  52. // 绘制点
  53. bitrateSeries.addPoint(now, bitrate);
  54. // 将会制的数据显示出来
  55. bitrateGraph.setDataSeries([bitrateSeries]);
  56. bitrateGraph.updateEndDate();// 更新时间
  57. // 下面是与包相关的绘制
  58. packetSeries.addPoint(now, packets -
  59. lastResult.get(report.id).packetsSent);
  60. packetGraph.setDataSeries([packetSeries]);
  61. packetGraph.updateEndDate();
  62. }
  63. }
  64. });
  65. // 记录上一次的报告
  66. lastResult = res;
  67. });
  68. }, 1000); // 每秒钟触发一次
  69. ...

:::info canvas手动开启抗锯齿:
canvas.getContext(‘2d’).imageSmoothingEnabled = true; :::

WebRTC 的数据通道(RTCDataChannel)

RTCDataChannel 支持的数据类型也非常多,包括:字符串、Blob、ArrayBuffer 以及 ArrayBufferView. :::info WebRTC 的 RTCDataChannel 使用的传输协议为 SCTP(Stream Control Transport Protocol).
SCTP是运行在UDP上的,本质上是对UDP的封装,在应用层实现了有序性与可靠性的配置。 :::

DC创建

  1. var pc = new RTCPeerConnection(); // 创建 RTCPeerConnection 对象
  2. // 创建 RTCDataChannel 对象的选项
  3. var options = {
  4. ordered: false, // 是否有序
  5. maxPacketLifeTime: 3000 // 最大的存活时间
  6. }
  7. var dc = pc.createDataChannel("dc", options); // 创建 RTCDataChannel 对象
  8. dc.onerror = (error)=> {
  9. // 出错 ...
  10. };
  11. dc.onopen = ()=> {
  12. // 打开,当datachannel打开时的处理逻辑
  13. };
  14. dc.onclose = () => {
  15. // 关闭,当datachannel关闭时的处理逻辑
  16. };
  17. dc.onmessage = (event)=>{
  18. // 收到消息,当收到消息时的处理逻辑
  19. };

options 可配置项:

  • ordered: 消息的传递是否有序
  • maxPacketLifeTime: 重传消息失败的最长时间。也就是说超过这个时间后,即使消息重传失败了也不再进行重传了。
  • maxRetransmits: 重传消息失败的最大次数
  • protocol: 用户自定义的子协议,也就是说可以根据用户自己的业务需求而定义的私有协议,默认为空。
  • negotiated:如果为true,则会删除另一方数据通道的自动设置。这也意味着你可以通过自己的方式在另一侧创建具有相同ID的数据通道。
  • id: 当negotiated为true时,允许你提供自己的 ID 与 channel 进行绑定

传输协商

In-band 协商方式

Out-of-band 协商方式

文件传输

:::info 断点续传:
将一个大文件划分成固定大小 的“块”,然后按“块”进行传输,
并且每传输完一“块”数据,接收端就发一个通知消息告知发送端该块数据它已经接收到了。
发送端收到该消息后,就会更新被传输文件的描述信息,
这样一旦文件传输过程中发生中断,下次启动时还可以从上次发生断点的地方继续传输。

相对于文本传输对数据的有序性和完整性有特别的要求。
:::

数据安全

基本概念

  • 数字证书(X509):用数字证书来验证对方身份
  • 加密:通过非对称加密交换对称加密密钥,防止数据内容被盗取
    • 公钥/私钥
    • 加密算法
      • MD5
      • SHA1
      • HMAC
      • RSA(非对)
      • ECC(非对)
  • 数字签名:证明数据内容没有窜改

    WebRTC数据安全机制

  • DTLS 协议 :解决 A 与 B 之间交换公钥时可能被窃取的问题。

  • SDP交换:

    • ice-ufrag:用户名
    • ice-pwd:密码
    • fingerprint:公钥证书的指纹(信息摘要)

      1. <br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/84728/1637291764452-94133dc7-3723-4a04-b20a-6761f6e08958.png#clientId=u5309c16a-01ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=674&id=ua0fe132a&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1724&originWidth=1550&originalType=binary&ratio=1&rotation=0&showTitle=false&size=363744&status=done&style=none&taskId=u9b92a3af-7918-49fc-9228-0c52dab0db5&title=&width=606)
  • 媒体协商:通过信令服务器交换SDP信息

  • A通过STUN协议(底层使用UDP协议)进行身份认证。如果STUN消息中的用户信息与SDP中的一致,则是合法用户
  • DTLS协商:交换公钥证书并协商密码相关的信息。同时还要通过fingerprint对证书进行验证,确认其没有在传输中被窜改
  • 再使用协商后的密码信息和公钥对数据进行加密,开始传输音视频数据

    DTLS协议

    TLS协议:

  • TLS记录协议:用于数据的加密、数据完整性检测

  • TLS握手协议(TCP):主要用于密钥的交换与身份的确认 :::info DTLS 是 ”webrct的TLS“,因为 webRTC 是基于 UDP 的,TLS 是基于 TCP 的,因此不能直接使用 TLS,因此 DTLS 是运行在 UDP 协议之上的简化版本的 TLS。 :::

:::info RTP/RTCP 是没有加密数据的协议。webrtc 通过 libsrtp库将 RTP/RTCP 协议数据转换成 SRTP/SRTCP 协议数据。 :::

直播系统的搭建

image.png

运行环境的搭建

  • 云主机:信令交换及数据中转
  • 带宽:720P的视频,一般情况需要1.2M的带宽
  • HTTPS:使用 HTTP 请求的 JavaScript 脚本不能访问音视频设备
  • 操作系统:Ubuntu 上边安装依赖库比较方便
  • 域名:国内备案时间>10 天;国外的云主机和域名,一般可以免费使用一年,并且在国外购买域名不需要进行备案

    Web 服务器的实现

    将客户端代码(如 HTML、CSS、JavaScript )放到 Web 服务的 public 目录下,
    这样在通过域名访问时,浏览器就将客户端代码下载下来,并渲染到浏览器上

    信令系统的实现

    客户端命令

  • join,用户加入房间。

  • leave,用户离开房间。
  • message,端到端命令(offer、answer、candidate)。

服务端命令

  • joined,用户已加入。
  • leaved,用户已离开。
  • other_joined,其他用户已加入。
  • bye,其他用户已离开。
  • full,房间已满。

image.png
信令状态机图
在初始时,客户端处于 init/leaved 状态。
在 init/leaved 状态下,用户只能发送 join 消息。
服务端收到 join 消息后,会返回 joined 消息。
此时,客户端会更新为 joined 状态

TURN 服务器的搭建

  • 提供 STUN 服务,客户端可以通过 STUN 服务获取自己的外网地址;
  • 提供数据中继服务

coturn:

  • 下载 coturn 代码
  • 执行 ./configure —prefix=/usr/local/coturn 生成 Makefile
  • 执行 make 来编译 coturn
  • 执行 sudo make install 进行安装

配置:
image.png

数据的采集

通过 getUserMedia 就可以完成数据的采集。注意数据采集的时机。

RTCPeerConnection 的创建

  1. ...
  2. var pcConfig = {
  3. 'iceServers': [{ // 指定 ICE 服务器信令
  4. // 当P2P建立连接失败时,就会用TURN服务器进行数据中继
  5. 'urls': 'turn:stun.al.learningrtc.cn:3478', // turn 服务器地址
  6. 'credential': "passwd", //turn 服务器密码,你要用自己的
  7. 'username': "username" //turn 服务器用户名,你要用自己的
  8. }]
  9. };
  10. ...
  11. function createPeerConnection(){
  12. if(!pc){
  13. pc = new RTCPeerConnection(pcConfig); // 创建 peerconnection 对象
  14. ...
  15. pc.ontrack = getRemoteStream; // 当远端的 track 到来时会触发该事件
  16. }else {
  17. console.log('the pc have be created!');
  18. }
  19. return;
  20. }
  21. ...
  22. // 要将前面获取的音视频数据与RTCPeerConnection对象绑定到一起,这样才能通过 RTCPeerConnection 对象将音视频数据传输出去。
  23. function bindTracks(){
  24. ...
  25. //add all track into peer connection
  26. localStream.getTracks().forEach((track)=>{
  27. pc.addTrack(track, localStream); // 将 track 与 peerconnection 绑定
  28. });
  29. }

数据的传输

即媒体协商,参考上文~

数据的渲染与播放

WebRTC 的底层进行连通性检测:
判断通信的双方

  • 在同一个局域网,双方直接进行连接
  • 不在同一个局域网,尝试用P2P连接
  • 如果P2P连接失败,则使用TURN服务进行数据中继。

:::info 当数据流过来的时候会触发 RTCPeerConnection 对象的 ontrack 事件,只要我们侦听该事件,并在回调函数中将收到的 track 与

标签绑定到一起 :::

  1. var remoteVideo = document.querySelector('video#remotevideo');
  2. ...
  3. function getRemoteStream(e){ // 事件处理函数
  4. remoteStream = e.streams[0]; // 保存远端的流
  5. remoteVideo.srcObject = e.streams[0]; // 与 HTML 中的视频标签绑定
  6. }
  7. ...
  8. pc = new RTCPeerConnection(pcConfig);
  9. ...
  10. pc.ontrack = getRemoteStrea // 当远端的 track 过来时触发该事件
  11. ...

流媒体服务器

  • DTLS 协议
  • ICE 协议
  • SRTP/SRTCP 协议

多人音视频实时通讯架构(流媒体服务器)

1对1通信模型

尽量让两个终端直连,如果不行,则通过 STUN/TURN 中继服务器进行数据中转。

Mesh (适合<4人的通信)-拓扑结果

多个终端两两互联,同时每个终端都分别与STUN/TURN连接,但是STUN/TURN不进行数据中转,否则会非常复杂。
image.png
优势:

  • 不需要开发媒体服务器
  • 充分利用客户端带宽资源
  • 节省服务器资源

缺点

  • 对上行带宽占用很大
  • 对客服端的CPU、Memory有要求
  • 资源占用和参与人数线性相关

MCU(Multipoint Conferencing Unit)(<20人)-星形结构

:::warning freeSWITCH就是基于这种架构。 ::: image.png

  1. 接收共享端发送的音视频流。
  2. 将接收到的音视频流进行解码。
  3. 对于视频流,要进行重新布局,混合处理。
  4. 对于音频流,要进行混音、重采样处理。
  5. 将混合后的音视频进行重新编码。
  6. 发送给接收客户端。

优势:

  • 技术成熟,在硬件视频会议中应用广泛
  • 通过解码再编码可以屏蔽不同编解码设备的差异化
  • 将多路视频混合成一路,所有参与者看到的是相同的画面,客户体验非常好

缺点:

  • 重新解码、编码、混流,需要大量的运算,对CPU资源的消耗很大
  • 重新解码、编码、混流会带来延迟
  • 机器资源耗费很大

SFU(Selective Forwarding Unit)-媒体流路由器

:::info 接收终端的音视频流,根据需要转发给其他终端.
开源实现有:Licode、Janus-gateway、MediaSoup、Medooze ::: 这是最明显而劣势又相对较少的一种架构方案
image.png

优点:

  • 不需要编码、解码,对CPU资源消耗比较小
  • 直接转发降低了延迟,提高了实时性
  • 比较灵活,能够更好第适应不同的网络状况和终端类型

缺点

  • 由于是数据包直接转发,多路视频可能会不同步,相同的视频流,不同的参与人看到的画面也可能不一致
  • 回放困难,因为多路流不一致的问题

simulcast(多基站同播)

image.png
共享者同时向SFU发送多路不同分辨率的视频流,而SFU根据各终端的情况选择某一路发送出去。

SVC(可伸缩的视频编码模式)

image.png

  1. <br /> <br /> 将视频分层,上层依赖于底层,越底层越模糊。带宽不好只传输底层,带宽好则都传输。<br />

SFU开源实现

Licode

C++: 媒体通信
Node:

  • 信令控制
  • 用户管理
  • 房间管理

    image.png

  • Nuve:管理用户、房间、产生token以及房间的均衡负载等工作

    • MangoDB:存储房间和token信息,但不存储用户信息
    • RabbitMQ: 通过消息队列对ErizoController进行控制
  • ErizoController:接收信令和非音视频数据,用于管理控制
  • ErizoAgent:可分布式部署,用于音视频流媒体数据的传输。 :::info 支持分布式集群部署
    代码较重,不易学习;
    性能一般。 :::

    Janus-gateway

    C语言
    插件:

  • SIP:这个插件使得 Janus 成了 SIP 用户的代理,从而允许 WebRTC 终端在 SIP 服务器(如 Asterisk)上注册,并向 SIP 服务器发送或接收音视频流。

  • TextRoom:该插件使用 DataChannel 实现了一个文本聊天室应用。
  • Streaming:它允许 WebRTC 终端观看 / 收听由其他工具生成的预先录制的文件或媒体。
  • VideoRoom:它实现了视频会议的 SFU 服务,实际就是一个音 / 视频路由器。
  • VideoCall:这是一个简单的视频呼叫的应用,允许两个 WebRTC 终端相互通信,它与 WebRTC 官网的例子相似(https://apprtc.appspot.com),不同点是这个插件要经过服务端进行音视频流中转,而 WebRTC 官网的例子走的是 P2P 直连。
  • RecordPlay:该插件有两个功能,一是将发送给 WebRTC 的数据录制下来,二是可以通过 WebRTC 进行回放。

image.png :::info 插件开发,扩展性强
支持丰富的信令协议 :::

MediaSoup

  • 应用层:node
  • 数据处理层: C++

    image.png

    Medooze

    image.png

    传统直播(单向直播而非双向互动)

    | | | | | | —- | —- | —- | —- | | 实时互动直播 | 主要考虑传输的实时性 | UDP | 在线教育、音视频会议 | | 传统直播 | 关注的是画面的质量、音视频是否卡顿等问题 | TCP | 映客、斗鱼这类娱乐直播 |

传输协议

协议名称 底层 传输格式
RTMP(Real Time Messaging Protocol) PC TCP RTMP Chunk Format
- 最少都会有几秒到几十秒的延迟
- librtmp
- 高实时性
- 不太安全
- 不支持H265
- 有访问限制的网络环境(防火墙),无法访问外网
- 浏览器需要安装FLASH插件才能播放


| | HLS(HTTP Live Streaming) | iOS&&>Android 3 | HTTP |
- ts 文件
- m3u8 索引文件
|
- 至少会有一个切片的延迟(10s-30s)
- 可以边下载边播
- 实时性差
- 基于HTTP,可访问外网
- 码率自适应(动态监测网速调整视频质量)
- 浏览器天然支持
| | HTTP-FLV | | | | | | MPEG-DASH | | | |
- 码率自适应(动态监测网速调整视频质量)
- Safari 浏览器不支持
|

  • 推流(流媒体接入)应该使用RTMP
  • 流媒体系统内部分发应该使用RTMP,因为内网系统网路状况好,使用RTMP更能发挥它的高效
  • PC尽量使用RTMP,因为PC基本都安装了Flash
  • 移动端的网页播放器最好用HLS
  • IOS要用HLS,因为不支持RTMP
  • 点播没有实时互动需求,可以接受延迟,最好使用HLS

    直播架构

    image.png

  • 直播客户端:采集、编码、推流(至CDN源节点-RTMP接入服务器)

  • 信令服务器
    • 接收信令并根据信令处理一些和业务相关的逻辑
      • 创建房间
      • 加入房间
      • 离开房间
      • 送礼物
      • 文字聊天
    • 要关注和防止消息的洪泛
  • 观众客户端:

    • 拉流(RTMP从边缘节点拉取)、解码与播放
    • 开源项目
      • Ijkplayer
      • VLC

        流程

  • 1上(3上):主播客户端向信令服务器发送“创建房间”的信令

  • 1下(3下):信令服务器收到该信令后,给主播客户端返回一个推流地址(CDN 网络源站地址)
  • 2上下(4上下):主播客户端收到推流地址后,通过音视频设备进行音视频数据的采集和编码,生成 RTMP 消息,最终将媒体流推送给 CDN 网络

  • 5上:观众端向信令服务器发送“加入房间”的信令

  • 5下:信令服务器收到该信令后,会根据用户所在地区,分配一个与它最接近的“CDN 边缘节点”
  • 6上下:观众端收到该地址后,就可以从该地址拉取媒体流了

    CDN

    :::info 先在各运营商内构建云服务,然后再将不同运营商的云服务通过光纤连接起来,从而实现跨运营商的全网 CDN 云服务。
    某种程度上是缓存,第一次访问没有数据,要去源节点搞数据,后面有缓存后就很快了。 ::: image.png
    节点:

  • 源节点:用于接收用户推送的媒体流

  • 主干节点:起到媒体数据快速传递的作用,比如与其他运营商传送媒体流
  • 边缘节点:在各地级市,数量多,机子性能较低,主要解决最后一公里的问题。用于用户来主动接流。

    应该了解的知识点

  • H264 的基本工作原理

  • YUV 数据格式
  • 如何通过 FFmpeg 进行音视频的编解码
  • 音视频该如何进行同步
  • 移动端 /PC 端使用 WebRTC 与浏览器的区别
  • 如何实现 SFU 流媒体服器