前序
说到直播,我们日常生活中见到太多太多了,电商行业的直播,比如天猫,京东,抖音,游戏行业的斗鱼,虎牙,还有社交类的花椒,微视等等,随处可见。那大家有了解过这些直播背后的基础原理吗?又用到了哪些技术点呢?期间的音视频数据流向是怎样形成的?那这篇文章介绍的内容就是利用WebRTC 实现一对一音视频实时通话的整个处理过程。
本次分享的大纲如下:
·如何录制视频
·如何上传视频
·如何播放视频
首先呢,咱们来看一下 WebRTC 实现一对一音视频实时通话的整个处理过程:
从图中我们可以看出 有两个 WebRTC 终端(上图中的两个大方框)、一个 Signal(信令)服务器和一个 STUN/TURN 服务器。其中:·WebRTC 终端,负责音视频采集、编解码、NAT 穿越、音视频数据传输。·Signal 服务器,负责信令处理,如加入房间、离开房间、媒体协商消息的传递等。·STUN/TURN 服务器,负责获取 WebRTC 终端在公网的 IP 地址,以及 NAT 穿越失败后的数据中转。
一.如何进行音视频的采集(录制视频)
检测音视频设备
MediaDevices.enumerateDevices() //获取音视频设备列表的接口
ok,如果上述检测过程顺利的话,你可以获取到相应的设备信息:
你也可以分别用getUserMedia API测试音频和视频是否可用。采集音视频数据
const localVideo = document.querySelector('video'); // 获取video标签
mediaStream对象中存放着 getUserMedia 方法采集到的音视频轨,我们可以在控制台打印输出获取到的mediaStream是啥东西:
具体的API属性与方法调用可以参考 MDN(https://developer.mozilla.org/zh-CN/docs/Web/API/MediaStream)。在getUserMedia方法中,有一个输入参数 constraints,其类型为 MediaStreamConstraints。它可以指定 MediaStream 中包含哪些类型的媒体轨(音频轨、视频轨),并且可为这些媒体轨设置一些限制。
上述步骤完成之后你就可以正常看到下列画面啦:
如何进行音视频的录制音视频的录制一般分为服务端录制和客户端录制,下面主要讲的是利用webrtc进行客户端录制的方式。因为WebRTC 录制音视频流之后,最终是通过 Blob 对象将数据保存成多媒体文件的,所以,在开始之前先了解Blob相关的三个概念:
1)ArrayBufferArrayBuffer 对象表示通用的、固定长度的二进制数据缓冲区。因此,你可以直接使用它存储图片、视频等内容。但你并不能直接对 ArrayBuffer 对象进行访问,就像java语言中的抽象类,需要具象化之后它才真正的存在于内存中,在这之前是没有为其分配内存空间的。
2)ArrayBufferViewArrayBufferView 并不是一个具体的类型,而是代表不同类型的 Array 的描述,如Int8Array,Uint8Array。ArrayBufferView 指的是 Int8Array、Uint8Array、DataView 等类型的总称,而这些类型都是使用 ArrayBuffer 类实现的。
3)Blob Blob(Binary Large Object)是 JavaScript 的大型二进制对象类型,WebRTC 最终就是使用它将录制好的音视频流保存成多媒体文件的。而它的底层是由上面所讲的 ArrayBuffer 对象的封装类实现的,即 Int8Array、Uint8Array 等类型。Blob 对象的格式如下: varaBlob=new Blob( array, options );其中,array 可以是 ArrayBuffer、ArrayBufferView、Blob、DOMString 等类型 ;option,用于指定存储成的媒体类型(MP4,FLV等)。进入正题:WebRTC 为我们提供了一个非常方便的类,即 MediaRecorder,使用方式如下:
var mediaRecorder = new MediaRecorder(stream[, options]);
通过控制台打印出mediaRecorder对象内容:
其中:属性state: 返回录制对象 MediaRecorder的当前状态(闲置中inactive,录制中recording或者暂停 paused)方法 start: 开始录制视频方法 stop: 停止录制视频方法 pause:暂停视频录制需要注意的是,在开启录制时,最好设置一个毫秒级的时间片,这样录制的媒体数据会按照你设置的值分割成一个个单独的区块,否则默认的方式是录制一个非常大的整块内容。分成一块一块的区块会提高效率和可靠性,如果是一整块数据,随着时间的推移,数据块越来越大,读写效率就会变差,而且增加了写入文件的失败率。另外,MediaRecorder 对象提供了一个 ondataavailable 事件,当 MediaRecoder 捕获到数据时就会触发该事件。通过它,我们才能将音视频数据录制下来。
其中,timecode是当前时间戳,data里面的Blob数据就是我们需要的多媒体数据了。具体实现:
var buffer;
var mediaRecorder;
//当该函数被触发后,将数据压入到blob中
let span1 = document.createElement("span")
span1.className = 'time'
function handleDataAvailable(e) {
span1.innerHTML = dayjs(parseInt(e.timecode)).format('YYYY-MM-DD dddd HH:mm:ss A') + ' '
if (e && e.data && e.data.size > 0) {
buffer.push(e.data);
}
}
// 开始录制
function startRecord(type = 0) {
// 防止多次启动报错
if (mediaRecorder && mediaRecorder.state === "recording") {
return;
}
buffer = [];
//设置录制下来的多媒体格式
var options = {
mimeType: 'video/webm;codecs=vp8'
}
//判断浏览器是否支持录制
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.error(`${options.mimeType} is not supported!`);
return;
}
try {
//创建录制对象
mediaRecorder = new MediaRecorder(localVideo.srcObject, options);
console.log(mediaRecorder)
} catch (e) {
console.error('Failed to create MediaRecorder:', e);
return;
}
//当有音视频数据来了之后触发该事件
mediaRecorder.ondataavailable = handleDataAvailable;
//开始录制
mediaRecorder.start(10);
}
录制下来之后你还可以将它下载视频到本地:
function download() {
var blob = new Blob(buffer, { type: 'video/webm' });
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.style.display = 'none';
a.download = 'aaa' + getRandom() + '.webm';
a.click();
a.remove();
}
** 二.如何上传视频**
当一端开启直播后,客户端需要将音视频数据编码封装成一个个数据包推向流媒体服务器,再由另一个客户端从服务器上拉取音视频下来解码拆包才能播放视频,在这个过程中,假设 A 与 B 进行通信,那么AB 之间应该建立一个沟通桥梁,这样双方才可以通信。webrtc为我们提供了一个RTCPeerConnection类, 官方解释是:RTCPeerConnection 接口代表一个由本地计算机到远端的 WebRTC 连接。该接口提供了创建,保持,监控,关闭连接的方法的实现。可以将他理解为功能更强大的socket ! 双方在点对点通信的过程中,需要交换一系列信息,通常这一过程叫做 — 信令(signaling)。在这个阶段webrtc需要完成的内容有:·为每个连接端创建一个 RTCPeerConnection,并添加本地媒体流。·获取并交换本地和远程描述:SDP 格式的本地媒体元数据。·获取并交换网络信息:潜在的连接端点称为 ICE 候选者。由于我们的例子中,通信双方是在同一个页面中(也就是说一个页面同时扮演 A 和 B 两个角色),所以在我们的 JavaScript 代码中会同时存在两个 RTCPeerConnection 对象,我们称它们为 pc1 和 pc2 好啦!(虽然 pc1 和 pc2 是在同一个页面中,但你一定要把 pc1 和 pc2 想像成两个端的连接对象)获取本地音视频流调用 getUserMedia() 获取到本地流,然后将它添加到对应的 RTCPeerConnecton 对象中,代码如下:
const servers = null
交换媒体描述信息当 RTCPeerConnection 对象获得音视频流后,就可以开始与对端进行媒协体协商了。在真实的应用场景中,各端获取的 SDP 信息都要通过信令服务器进行交换。在下面例子中为了减少复杂度就直接在一个页面实现了两个端,没有通过信令服务器交换信息,直接将一端获取的 offer 设置到另一端。下图就是信令服务器工作的流程。
呼叫端(A)创建 offer 信息后,先调用 setLocalDescription 存储本地 offer 描述,再将其发送给 接收端。接收端(B)收到 offer 后,先调用 setRemoteDescription 存储远端 offer 描述;然后又创建 answer 信息,同样需要调用 setLocalDescription 存储本地 answer 描述,再返回给 接收端.呼叫端 拿到 answer 后,再次调用 setRemoteDescription 设置远端 answer 描述。
SDP规范具体内容:
// 会话层描述字段
offer SDP:
v=0 // v=0 ,表示 SDP 的版本号
o=- 5636261141912965574 2 IN IP4 127.0.0.1
//o= 表示的是对会话发起者的描述
// - 表示userName,当不关心用户名时,可以用 “-” 代替
//5636261141912965574 表示sessionId ,唯一,一般用时间戳表示
// 2 表示版本号,每次会话更改后,版本号递增
// IN 网络类型,一般为“IN”,表示“internet”
//IP4 地址类型,一般为 IP4
//127.0.0.1 IP地址
s=- // sessionName,默认为 -
t=0 0 //t=<start time> <stop time>,表示会话开始和结束的时间,t=0 0 表示持久会话
...
// 音频描述
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
// media类型 , 端口号, 传输协议,媒体格式,即数据负载类型 (Payload Type) 列表
//a=*(zero or more media attribute lines,可选)。例子:a=或 a=:, 表示属性,用于进一步描述媒体信息
a=rtcp:9 IN IP4 0.0.0.0
a=rtpmap:111 opus/48000/2
...
// 视频描述
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 124 119 123 118 114 115 116
a=rtpmap:96 VP8/90000
...
对于交换的具体过程,首先我们要创建 offer 类型的 SDP 信息。A 作为呼叫方,调用 RTCPeerConnection 的 createOffer() 方法,得到 A 的本地会话描述,即 offer 类型的 SDP 信息,有如下设置:
localPeerConnection.createOffer(offerOptions)
工作的流程是:
A 使用 setLocalDescription() 设置本地描述,然后将此会话描述发送给 B。B 使用 setRemoteDescription() 设置 A 给它的描述作为远端描述。
之后,B 调用 RTCPeerConnection 的 createAnswer() 方法获得它本地的媒体描述。然后,再调用 setLocalDescription 方法设置本地描述并将该媒体信息描述发给 A。
A 得到 B 的应答描述后,就调用 setRemoteDescription() 设置远程描述。
至此,A B端媒体信息交换和协商就完成了。具体代码如下:
const servers = null
端与端建立连接当呼叫方A端调用 setLocalDescription 函数成功后,就开始收到网络信息了,即开始收集 ICE Candidate,我们需要去监听这个 A的ICE候选信息(B端也是一样的):
let localPeerConnection = new RTCPeerConnection(servers)
其中,target为触发 icecandidate 事件的 RTCPeerConnection 对象,candidate具体的Candidate候选者。每当获得一个新的 Candidate 后,webrtc就会通过信令服务器交换给对端,对端再调用 RTCPeerConnection 对象的 addIceCandidate() 方法将收到的 Candidate 保存起来,然后按照 Candidate 的优先级进行连通性检测。如果 Candidate 连通性检测完成,那么端与端之间就建立了物理连接,这时媒体数据就可以通过这个物理连接不断地传输了。
//监听到Candidate变化则会调用这个函数
Candidate 的样子大概是:
{
webrtc针对Candidate指定的原则是如果 host 类型候选者之间无法建立连接,那么 WebRTC 则会尝试次优先级的候选者,即 srflx 类型的候选者。也就是尝试让通信双方直接通过 P2P 进行连接,如果连接成功就使用 P2P 传输数据;如果失败,就最后尝试使用 relay 方式建立连接。像下面这种方式的就是host方式直接连接。
三.如何播放视频(显示远端媒体流)
A端创建好 RTCPeerConnection 对象后,这个对象提供了一个addstream 事件
// 添加本地媒体流到localPeerConnection并创建offer通知对端
当B端也创建好一个RTCPeerConnection 对象之后,我们需要给 RTCPeerConnection 的 addstream 事件添加回调处理函数,当有数据流到来的时候,浏览器就会回调这个函数,将video 与 RTCPeerConnection得到的远端数据流进行绑定,进而显示视频
// 远端监听remotePeerConnection对象的addStream事件
好啦,文章到此就告一段落啦,关于webrtc和直播还有很多很多技术细节需要我们去细心研究,下回再续 !注:兼容性问题:Google 开发了 adapter.js 这个适配器脚本,以弥补各浏览器 API 不统一的问题。可直接引入:https://webrtc.github.io/adapter/adapter-latest.js.