WebRTC(Web Real-Time Communication),即“网页即时通信”,WebRTC 是一个支持浏览器进行实时语音、视频对话的开源协议,目前主流浏览器都支持WebRTC,即便在网络信号一般的情况下也具备较好的稳定性,WebRTC 可以实现点对点通信,通信双方延时低,使用户无需下载安装任何插件就可以进行实时通信
在WebRTC发布之前,开发实时音视频交互应用的成本很高,需要考虑的技术问题很多,如音视频的编解码问题,数据传输问题,延时、丢包、抖动、回音的处理和消除等,如果要兼容浏览器端的实时音视频通信,还需要额外安装插件, WebRTC 大大降低了音视频开发的门槛,开发者只需要调用 WebRTC API 即可快速构建出音视频应用
下面主要通过 WebRTC 的实时通信过程来对 WebRTC 有一个大概的了解
WebRTC 音视频通信的大体过程
音视频设备检测
设备的基本原理
音频设备
音频输入设备的主要工作是采集音频数据,而采集音频数据的本质就是模数转换(A/D),即将模似信号转换成数字信号,采集到的数据再经过量化、编码,最终形成数字信号,这就是音频设备所要完成的工作
视频设备
视频设备,与音频输入设备很类似,视频设备的模数转换(A/D)模块即光学传感器, 将光转换成数字信号,即 RGB(Red、Green、Blue)数据,获得 RGB 数据后,还要通过 DSP(Digital Signal Processer)进行优化处理,如自动增强、色彩饱和等都属于这一阶段要做的事情,通过 DSP 优化处理后获得 RGB 图像,然后进行压缩、传输,而编码器一般使用的输入格式为 YUV,所以在摄像头内部还有一个专门的模块用于将 RGB 图像转为 YUV 格式的图像
那什么是 YUV 呢?
YUV 也是一种色彩编码方法,它将亮度信息(Y)与色彩信息(UV)分离,即使没有 UV 信息一样可以显示完整的图像,只不过是黑白的,这样的设计很好地解决了彩色电视机与黑白电视的兼容问题(这也是 YUV 设计的初衷)相对于 RGB 颜色空间,YUV 的目的是为了编码、传输的方便,减少带宽占用和信息出错,人眼的视觉特点是对亮度更敏感,对位置、色彩相对来说不敏感,在视频编码系统中为了降低带宽,可以保存更多的亮度信息,保存较少的色差信息
获取音视频设备列表
MediaDevices.enumerateDevices()
此方法返回一个可用的媒体输入和输出设备的列表,例如麦克风,摄像机,耳机设备等
navigator.mediaDevices.enumerateDevices().then(function(deviceInfos) {
deviceInfos.forEach(function(deviceInfo) {
console.log(deviceInfo);
});
})
返回的 deviceInfo 信息格式如下:
- 出于安全原因,除非用户已被授予访问媒体设备的权限(要想授予权限需要使用 HTTPS 请求),否则 label 字段始终为空
设备检测方法
- 返回信息 deviceInfo 中的 kind 字段可以区分出设备是音频设备还是视频设备,同时音频设备能区分出是输入设备和输出设备,我们平时使用的耳机它是一个音频设备,但它同时兼有音频输入设备和音频输出设备的功能
- 对于音频设备和视频设备会设置各自的默认设备, 还是以耳机这个音频设备为例,将耳机插入电脑后,耳机就变成了音频的默认设备,将耳机拔出后,默认设备又切换成了系统的音频设备
- 在获取到所有的设备列表后,如果我们不指定某个具体设备,采集音视频数据时,就会从设备列表中的默认设备上采集数据,如果能从指定的设备上采集到音视频数据,那说明这个设备就是有效的设备,这样我们就可以对音视频设备进行一项一项检测
- 通过调用 getUserMedia 方法 (下面音视频采集的时候会讲到) 进行设备检测
- 视频设备检测:调用 getUserMedia API 采集视频数据并将其展示出来,如果用户能看到自己的视频,说明视频设备是有效的,否则,设备无效
- 音频设备检测:调用 getUserMedia API 采集音频数据,由于音频数据不能直接展示,所以需要使用 JavaScript 将其处理后展示到页面上,这样当用户看到音频数值的变化后,说明音频设备也是有效的
音视频采集
基本概念
- 帧率
帧率表示1秒钟视频内图像的数量,一般帧率达到 10~12fps 人眼就会觉得是连贯的,帧率越高,代表着每秒钟处理的图像数量越高,因此流量会越大,对设备的性能要求也越高,所以在直播系统中一般不会设置太高的帧率,高的帧率可以得到更流畅、更逼真的动画,一般来说 30fps 就是可以接受的,但是将性能提升至 60fps 则可以明显提升交互感和逼真感,但是一般来说超过 75fps 一般就不容易察觉到有明显的流畅度提升了
- 轨(Track)
WebRTC 中的“轨”借鉴了多媒体的概念,两条轨永远不会相交,“轨”在多媒体中表达的就是每条轨数据都是独立的,不会与其他轨相交,如 MP4 中的音频轨、视频轨,它们在 MP4 文件中是被分别存储的
音视频采集接口
mediaDevices.getUserMedia
const mediaStreamContrains = {
video: true,
audio: true
};
const promise = navigator.mediaDevices.getUserMedia(mediaStreamContrains).then(
gotLocalMediaStream
)
const $video = document.querySelector('video');
function gotLocalMediaStream(mediaStream){
$video.srcObject = mediaStream;
}
function handleLocalMediaStreamError(error){
console.log('getUserMedia 接口调用出错: ', error);
}
**srcObject[1]**:属性设定或返回一个对象,这个对象提供了一个与 HTMLMediaElement 关联的媒体源,这个对象通常是 MediaStream,根据规范也可以是 MediaSource, Blob 或者 File,但对于 MediaSource, Blob 和File类型目前浏览器的兼容性不太好,所以对于这几种类型可以通过 URL.createObjectURL() 创建 URL,并将其赋值给 HTMLMediaElement.src
MediaStreamConstraints 参数,可以指定MediaStream中包含哪些类型的媒体轨(音频轨、视频轨),并且可为这些媒体轨设置一些限制
const mediaStreamContrains = {
video: {
frameRate: {min: 15}, // 视频的帧率最小 15 帧每秒
width: {min: 320, ideal: 640}, // 宽度最小是 320,理想的宽度是 640
height: {min: 480, ideal: 720},// 高度最小是 480,最理想高度是 720
facingMode: 'user', // 优先使用前置摄像头
deviceId: '' // 指定使用哪个设备
},
audio: {
echoCancellation: true, // 对音频开启回音消除功能
noiseSuppression: true // 对音频开启降噪功能
}
}
浏览器实现自拍
我们知道视频是由一幅幅帧图像和一组音频构成的,所以拍照的过程其实是从连续播放的视频流(一幅幅画面)中抽取正在显示的那张画面,上面我们讲过可以通过 getUserMedia 获取到视频流,那如何从视频流中获取到正在显示的图片呢?
这里就要用到 canvas 的 drawImage[2]
const ctx = document.querySelector('canvas');
// 需要拍照时执行此代码,完成拍照
ctx.getContext('2d').drawImage($video, 0, 0);
function downLoad(url){
const $a = document.createElement("a");
$a.download = 'photo';
$a.href = url;
document.body.appendChild($a);
$a.click();
$a.remove();
}
// 调用 download 函数进行图片下载
downLoad(ctx.toDataURL("image/jpeg"));
drawImage 的第一个参数支持 HTMLVideoElement
类型,所以可以直接将 $video 作为第一个参数传入,这样就通过 canvas 获取到照片了
然后通过 a 标签的 download 将照片下载下来保存到本地
- 通过 canvas 的 toDataURL 方法获得图片的 URL 地址
- 利用 a 标签的 downLoad 属性来实现图片的下载
音视频录制
基本概念
- ArrayBuffer
ArrayBuffer 对象表示通用的、固定长度的二进制数据缓冲区,可以使用它存储图片、视频等内容,但ArrayBuffer 对象不能直接进行访问,ArrayBuffer 只是描述有这样一块空间可以用来存放二进制数据,但在计算机的内存中并没有真正地为其分配空间,只有当具体类型化后,它才真正地存在于内存中
let buffer = new ArrayBuffer(16); // 创建一个长度为 16 的 buffer
let view = new Uint32Array(buffer);
- ArrayBufferView
是Int32Array、Uint8Array、DataView等类型的总称,这些类型都是使用 ArrayBuffer 类实现的,因此才统称他们为 ArrayBufferView
- Blob
(Binary Large Object)是 JavaScript 的大型二进制对象类型,WebRTC 最终就是使用它将录制好的音视频流保存成多媒体文件的,而它的底层是由上面所讲的 ArrayBuffer 对象的封装类实现的,即 Int8Array、Uint8Array 等类型
音频录制接口
const mediaRecorder = new MediaRecorder(stream[, options]);
stream参数是将要录制的流,它可以是来自于使用 navigator.mediaDevices.getUserMedia
创建的流或者来自于 audio,video 以及 canvas DOM 元素
MediaRecorder.ondataavailable
事件可用于获取录制的媒体资源 (在事件的 data
属性中会提供一个可用的 Blob 对象)
录制的流程如下:
- 使用 getUserMedia 接口获取视频流数据
- 使用 MediaRecorder 接口进行录制(视频流数据来源上一步获取的数据)
- 使用 MediaRecorder 的 ondataavailable 事件获取录制的 buffer 数据
- 将 buffer 数据转成 Blob 类型,然后使用 createObjectURL 生成可访问的视频地址
- 利用 a 标签的 download 属性进行视频下载
<video autoplay playsinline controls id="video-show"></video>
<video id="video-replay"></video>
<button id="record">开始录制</button>
<button id="stop">停止录制</button>
<button id="recplay">录制播放</button>
<button id="download">录制视频下载</button>
let buffer;
const $videoshow = document.getElementById('video-show');
const promise = navigator.mediaDevices.getUserMedia({
video: true
}).then(
stream => {
console.log('stream', stream);
window.stream = stream;
$videoshow.srcObject = stream;
})
function startRecord(){
buffer = [];
// 设置录制下来的多媒体格式
const options = {
mimeType: 'video/webm;codecs=vp8'
}
// 判断浏览器是否支持录制
if(!MediaRecorder.isTypeSupported(options.mimeType)){
console.error(`${options.mimeType} is not supported!`);
return;
}
try{
// 创建录制对象
mediaRecorder = new MediaRecorder(window.stream, options);
console.log('mediaRecorder', mediaRecorder);
}catch(e){
console.error('Failed to create MediaRecorder:', e);
return;
}
// 当有音视频数据来了之后触发该事件
mediaRecorder.ondataavailable = handleDataAvailable;
// 开始录制
mediaRecorder.start(2000); // 若设置了 timeslice 这个毫秒值,那么录制的数据会按照设定的值分割成一个个单独的区块
}
// 当该函数被触发后,将数据压入到 blob 中
function handleDataAvailable(e){
console.log('e', e.data);
if(e && e.data && e.data.size > 0){
buffer.push(e.data);
}
}
document.getElementById('record').onclick = () => {
startRecord();
};
document.getElementById('stop').onclick = () => {
mediaRecorder.stop();
console.log("recorder stopped, data available");
};
// 回放录制文件
const $video = document.getElementById('video-replay');
document.getElementById('recplay').onclick = () => {
const blob = new Blob(buffer, {type: 'video/webm'});
$video.src = window.URL.createObjectURL(blob);
$video.srcObject = null;
$video.controls = true;
$video.play();
};
// 下载录制文件
document.getElementById('download').onclick = () => {
const blob = new Blob(buffer, {type: 'video/webm'});
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.style.display = 'none';
a.download = 'video.webm';
a.click();
};
创建连接
数据采集完成,接下来就要开始建立连接,然后进行数据通信了
要实现一套 1 对 1 的通话系统,通常我们的思路会是在每一端创建一个 socket,然后通过该 socket 与对端相连,当 socket 连接成功之后,就可以通过 socket 向对端发送数据或者接收对端的数据了,WebRTC 中提供了 RTCPeerConnection 类,其工作原理和 socket 基本一样,不过它的功能更强大,实现也更为复杂,下面就来讲讲 WebRTC 中的 RTCPeerConnection
RTCPeerConnection
在音视频通信中,每一方只需要有一个 RTCPeerConnection 对象,用它来接收或发送音视频数据,然而在真实的场景中,为了实现端与端之间的通话,还需要利用信令服务器交换一些信息,比如交换双方的 IP 和 port 地址,这样通信的双方才能彼此建立连接
WebRTC 规范对 WebRTC 要实现的功能、API 等相关信息做了大量的约束,比如规范中定义了如何采集音视频数据、如何录制以及如何传输等,甚至更细的,还定义了都有哪些 API,以及这些 API 的作用是什么,但这些约束只针对于客户端,并没有对服务端做任何限制,这就导致了我们在使用 WebRTC 的时候,必须自己去实现 信令 服务, 这里就不专门研究怎么实现信令服务器了,我们只来看看 RTCPeerConnection 是如何实现一对一通信的
RTCPeerConnection 如何工作呢?
- 获取本地音视频流
为连接的每个端创建一个 RTCPeerConnection 对象,并且给 RTCPeerConnection 对象添加一个本地流,该流是从 getUserMedia 获取的
// 调用 getUserMedia API 获取音视频流
navigator.mediaDevices.getUserMedia(mediaStreamConstraints).
then(gotLocalMediaStream)
function gotLocalMediaStream(mediaStream) {
window.stream = mediaStream;
}
// 创建 RTCPeerConnection 对象
let localPeerConnection = new RTCPeerConnection();
// 将音视频流添加到 RTCPeerConnection 对象中
localPeerConnection.addStream(stream);
- 交换媒体描述信息
获得音视频流后,就可以开始与对端进行媒体协商了(媒体协商就是看看你的设备都支持哪些编解码器,我的设备是否也支持?如果我的设备也支持,那么咱们双方就算协商成功了),这个过程需要通过信令服务器完成
现在假设 A 和 B 需要通讯
- A 通过 createOffer[3] 方法启动创建一个 SDP offer,即得到 A 的本地会话描述
- A 通过 setLocalDescription ****方法保存本地会话描述
- A 通过信令服务器发送信令给 B
localPeerConnection.createOffer([options])
.then((description) => {
// 将 offer 保存到本地
localPeerConnection.setLocalDescription(description)
.then(() => {
setLocalDescriptionSuccess(localPeerConnection);
});
})
- B 接收到带有 A offer 的信令,调用 setRemoteDescription,设置远程会话描述
- B 通过 createAnswer 方法将本地会话描述成功回调
- B 调用 setLocalDescription 设置他自己的本地局部描述回调函数中保存本地会话描述
- B 通过信令服务器发送信令给 A
// B 设置远程会话描述
remotePeerConnection.setRemoteDescription(description)
.then(() => {
setRemoteDescriptionSuccess(remotePeerConnection);
});
remotePeerConnection.createAnswer()
.then((description)=> {
// B 保存本地会话描述
remotePeerConnection.setLocalDescription(description)
.then(() => {
setLocalDescriptionSuccess(remotePeerConnection);
});
});
- A 通过 setRemoteDescription 将 B 的应答 answer 保存为远程会话描述
// A 保存 B 的 应答 answer 为远程会话描述
localPeerConnection.setRemoteDescription(description)
.then(() => {
setRemoteDescriptionSuccess(localPeerConnection);
});
至此就完成了媒体信息交换和协商
- 端与端建立连接
- 当 A 调用 setLocalDescription 函数成功后,会触发 icecandidate 事件(在建立通讯之前,我们需要获得双方的网络信息,例如 IP、端口等,candidate 便是用于保存这些东西的)
localPeerConnection.onicecandidate= function(event) {
// 获取到触发 icecandidate 事件的 RTCPeerConnection 对象
const peerConnection = event.target;
// 获取到具体的 candidate
const iceCandidate = event.candidate;
// 将 candidate 包装成需要的格式,然后通过信令服务器发送给B
}
- B 接收到信令服务器传递过来的 A 的关于 candidate 的信息,把消息包装成 RTCIceCandidate 对象,然后调用 addIceCandidate 保存起来
// 创建 RTCIceCandidate 对象
const newIceCandidate = new RTCIceCandidate(iceCandidate);
remotePeerConnection.addIceCandidate(newIceCandidate);
这样就收集到了一个新的 Candidate,在真实的场景中,每当获得一个新的 Candidate 后,就会通过信令服务器交换给对端,对端再调用 RTCPeerConnection 对象的 addIceCandidate() 方法将收到的 Candidate 保存起来,然后按照 Candidate 的优先级进行连通性检测,如果 Candidate 连通性检测完成,那么端与端之间就建立了连接,这时媒体数据就可以通过这个连接进行传输了
音视频编解码
视频是连续的图像序列,由连续的帧构成,一帧即为一幅图像,由于人眼的视觉暂留效应,当帧序列以一定的速率播放时,我们看到的就是动作连续的视频,由于连续的帧之间相似性极高,为便于储存传输,我们需要对原始的视频进行编码压缩,以去除空间、时间维度的冗余
视频编解码是采用算法将视频数据的冗余信息去除,对图像进行压缩、存储及传输, 再将视频进行解码及格式转换, 追求在可用的计算资源内,尽可能高的视频重建质量和尽可能高的压缩比,以达到带宽和存储容量要求的视频处理技术
视频流传输中最为重要的编解码标准有H.26X系列(H.261、H.263、H.264),MPEG系列,Apple公司的 QuickTime 等
显示远端媒体流
通过 RTCPeerConnection 对象 A 与 B 双方建立连接后,本地的多媒体数据经过编码以后就可以被传送到远端了,远端收到了媒体数据解码后,怎么显示出来呢,下面以 video 为例,看看怎么让 RTCPeerConnection 获得的媒体数据与 video 标签结合起来
当远端有数据流到来的时候,浏览器会回调 onaddstream 函数,在回调函数中将得到的 stream 赋值给 video 标签的 srcObject 对象,这样 video 就与 RTCPeerConnection 进行了绑定,video 就能从 RTCPeerConnection 获取到视频数据,并最终将其显示出来了
localPeerConnection.onaddstream = function(event) {
$remoteVideo.srcObject = event.stream;
}
结语
WebRTC 相关的东西非常非常多,这里只是很浅显地串讲了一下利用 WebRTC 实现实时通信的大体过程,如果感兴趣可以详细研究里面的细节
参考资料
[1]srcObject: https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/srcObject#%E6%B5%8F%E8%A7%88%E5%99%A8%E5%85%BC%E5%AE%B9%E6%80%A7
[2]drawImage: https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/drawImage
[3]createOffer: https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/createOffer
以上文章来源于ELab团队 ,作者ELab.liulili
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。