深入淺出音視頻與 WebRTC

本文為來(lái)自 教育-成人與創(chuàng)新-前端團(tuán)隊(duì) 成員的文章,已授權(quán) ELab 發(fā)布。

常見(jiàn)的音視頻網(wǎng)絡(luò)通信協(xié)議
普通直播協(xié)議
這類(lèi)直播對(duì)實(shí)時(shí)性要求不那么高,使用CDN進(jìn)行內(nèi)容分發(fā),會(huì)有幾秒甚至十幾秒的延時(shí),主要關(guān)注畫(huà)面質(zhì)量、音視頻是卡頓等問(wèn)題,一般選用 RTMP 和 HLS 協(xié)議

基本概念
RTMP
RTMP (Real Time Messaging Protocol),即“實(shí)時(shí)消息傳輸協(xié)議”, 它實(shí)際上并不能做到真正的實(shí)時(shí),一般情況最少都會(huì)有幾秒到幾十秒的延遲,是 Adobe 公司開(kāi)發(fā)的音視頻數(shù)據(jù)傳輸?shù)膶?shí)時(shí)消息傳送協(xié)議,RTMP 協(xié)議基于 TCP,包括 RTMP 基本協(xié)議及 RTMPT/RTMPS/RTMPE 等多種變種,RTMP 是目前主流的流媒體傳輸協(xié)議之一,對(duì)CDN支持良好,實(shí)現(xiàn)難度較低,是大多數(shù)直播平臺(tái)的選擇,不過(guò)RTMP有一個(gè)最大的不足 —— 不支持瀏覽器,且蘋(píng)果 ios 不支持,Adobe 已停止對(duì)其更新

RTMP目前在 PC 上的使用仍然比較廣泛

HLS
HLS (Http Live Streaming)是由蘋(píng)果公司定義的基于 HTTP 的流媒體實(shí)時(shí)傳輸協(xié)議,被廣泛的應(yīng)用于視頻點(diǎn)播和直播領(lǐng)域,HLS 規(guī)范規(guī)定播放器至少下載一個(gè) ts 切片才能播放,所以 HLS 理論上至少會(huì)有一個(gè)切片的延遲

HLS 在移動(dòng)端兼容性比較好,ios就不用說(shuō)了,Android現(xiàn)在也基本都支持 HLS 協(xié)議了,pc端如果要使用可以使用 hls.js 適配器

HLS 的原理是將整個(gè)流分為多個(gè)小的文件來(lái)下載,每次只下載若干個(gè),服務(wù)器端會(huì)將最新的直播數(shù)據(jù)生成新的小文件,當(dāng)客戶端獲取直播時(shí),它通過(guò)獲取最新的視頻文件片段來(lái)播放,從而保證用戶在任何時(shí)候連接進(jìn)來(lái)時(shí)都會(huì)看到較新的內(nèi)容,實(shí)現(xiàn)近似直播的體驗(yàn);HLS 的延遲一般會(huì)高于普通的流媒體直播協(xié)議,傳輸內(nèi)容包括兩部分:一部分 M3U8 是索引文件,另一部分是 TS 文件,用來(lái)存儲(chǔ)音視頻的媒體信息

RTMP 和 HLS 如何選擇
流媒體推流,一般使用 RTMP 協(xié)議
移動(dòng)端的網(wǎng)頁(yè)播放器最好使用 HLS 協(xié)議,RTMP 不支持瀏覽器
iOS 要使用 HLS 協(xié)議,因?yàn)椴恢С?RTMP 協(xié)議
點(diǎn)播系統(tǒng)最好使用 HLS 協(xié)議,因?yàn)辄c(diǎn)播沒(méi)有實(shí)時(shí)互動(dòng)需求,延遲大一些是可以接受的,并且可以在瀏覽器上直接觀看
普通直播基本架構(gòu)
由直播 客戶端 、 信令 服務(wù)器和 CDN 網(wǎng)絡(luò)這三部分組成

直播 客戶端主要包括音視頻數(shù)據(jù)的采集、編碼、推流、拉流、解碼與播放功能,但實(shí)際上這些功能并不是在同一個(gè)客戶端中實(shí)現(xiàn)的,為什么呢?因?yàn)樽鳛橹鞑?lái)說(shuō),他不需要看到觀眾的視頻或聽(tīng)到觀眾的聲音,而作為觀眾來(lái)講,他們與主播之間是通過(guò)文字進(jìn)行交流的,不需要向主播分享自己的音視頻信息

對(duì)于主播客戶端來(lái)說(shuō),它可以設(shè)備的攝像頭、麥克風(fēng)采集數(shù)據(jù),然后對(duì)采集到的音視頻數(shù)據(jù)進(jìn)行編碼,最后將編碼后的音視頻數(shù)據(jù)推送給 CDN

對(duì)于觀眾客戶端來(lái)說(shuō),它首先需要獲取到主播房間的流媒體地址,觀眾進(jìn)入房間后從 CDN 拉取音視頻數(shù)據(jù),并對(duì)獲取到的音視頻數(shù)據(jù)進(jìn)行解碼,最后進(jìn)行音視頻的渲染與播放

信令 服務(wù)器,主要用于接收信令,并根據(jù)信令處理一些和業(yè)務(wù)相關(guān)的邏輯,如創(chuàng)建房間、加入房間、離開(kāi)房間、文字聊天等

CDN 網(wǎng)絡(luò),主要用于媒體數(shù)據(jù)的分發(fā),傳給它的媒體數(shù)據(jù)可以很快傳送給各地的用戶

實(shí)時(shí)直播協(xié)議
隨著人們對(duì)實(shí)時(shí)性、互動(dòng)性的要求越來(lái)越高,傳統(tǒng)直播技術(shù)越來(lái)越滿足不了人們的需求,WebRTC 技術(shù)正是為了解決人們對(duì)實(shí)時(shí)性、互動(dòng)性需求而提出的新技術(shù)

WebRTC
WebRTC(Web Real-Time Communication),即“網(wǎng)頁(yè)即時(shí)通信”,WebRTC 是一個(gè)支持瀏覽器進(jìn)行實(shí)時(shí)語(yǔ)音、視頻對(duì)話的開(kāi)源協(xié)議,目前主流瀏覽器都支持WebRTC,即便在網(wǎng)絡(luò)信號(hào)一般的情況下也具備較好的穩(wěn)定性,WebRTC 可以實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn)通信,通信雙方延時(shí)低,使用戶無(wú)需下載安裝任何插件就可以進(jìn)行實(shí)時(shí)通信

在WebRTC發(fā)布之前,開(kāi)發(fā)實(shí)時(shí)音視頻交互應(yīng)用的成本很高,需要考慮的技術(shù)問(wèn)題很多,如音視頻的編解碼問(wèn)題,數(shù)據(jù)傳輸問(wèn)題,延時(shí)、丟包、抖動(dòng)、回音的處理和消除等,如果要兼容瀏覽器端的實(shí)時(shí)音視頻通信,還需要額外安裝插件, WebRTC 大大降低了音視頻開(kāi)發(fā)的門(mén)檻,開(kāi)發(fā)者只需要調(diào)用 WebRTC API 即可快速構(gòu)建出音視頻應(yīng)用

下面主要通過(guò) WebRTC 的實(shí)時(shí)通信過(guò)程來(lái)對(duì) WebRTC 有一個(gè)大概的了解

WebRTC 音視頻通信的大體過(guò)程

音視頻設(shè)備檢測(cè)
設(shè)備的基本原理
音頻設(shè)備
音頻輸入設(shè)備的主要工作是采集音頻數(shù)據(jù),而采集音頻數(shù)據(jù)的本質(zhì)就是模數(shù)轉(zhuǎn)換(A/D),即將模似信號(hào)轉(zhuǎn)換成數(shù)字信號(hào),采集到的數(shù)據(jù)再經(jīng)過(guò)量化、編碼,最終形成數(shù)字信號(hào),這就是音頻設(shè)備所要完成的工作

視頻設(shè)備
視頻設(shè)備,與音頻輸入設(shè)備很類(lèi)似,視頻設(shè)備的模數(shù)轉(zhuǎn)換(A/D)模塊即光學(xué)傳感器, 將光轉(zhuǎn)換成數(shù)字信號(hào),即 RGB(Red、Green、Blue)數(shù)據(jù),獲得 RGB 數(shù)據(jù)后,還要通過(guò) DSP(Digital Signal Processer)進(jìn)行優(yōu)化處理,如自動(dòng)增強(qiáng)、色彩飽和等都屬于這一階段要做的事情,通過(guò) DSP 優(yōu)化處理后獲得 RGB 圖像,然后進(jìn)行壓縮、傳輸,而編碼器一般使用的輸入格式為 YUV,所以在攝像頭內(nèi)部還有一個(gè)專(zhuān)門(mén)的模塊用于將 RGB 圖像轉(zhuǎn)為 YUV 格式的圖像

那什么是 YUV 呢?

YUV 也是一種色彩編碼方法,它將亮度信息(Y)與色彩信息(UV)分離,即使沒(méi)有 UV 信息一樣可以顯示完整的圖像,只不過(guò)是黑白的,這樣的設(shè)計(jì)很好地解決了彩色電視機(jī)與黑白電視的兼容問(wèn)題(這也是 YUV 設(shè)計(jì)的初衷)相對(duì)于 RGB 顏色空間,YUV 的目的是為了編碼、傳輸?shù)姆奖悖瑴p少帶寬占用和信息出錯(cuò),人眼的視覺(jué)特點(diǎn)是對(duì)亮度更敏感,對(duì)位置、色彩相對(duì)來(lái)說(shuō)不敏感,在視頻編碼系統(tǒng)中為了降低帶寬,可以保存更多的亮度信息,保存較少的色差信息

獲取音視頻設(shè)備列表
MediaDevices.enumerateDevices()

此方法返回一個(gè)可用的媒體輸入和輸出設(shè)備的列表,例如麥克風(fēng),攝像機(jī),耳機(jī)設(shè)備等

navigator.mediaDevices.enumerateDevices().then(function(deviceInfos) {
  deviceInfos.forEach(function(deviceInfo) {
    console.log(deviceInfo);
  });
})
返回的 deviceInfo 信息格式如下:


出于安全原因,除非用戶已被授予訪問(wèn)媒體設(shè)備的權(quán)限(要想授予權(quán)限需要使用 HTTPS 請(qǐng)求),否則 label 字段始終為空
設(shè)備檢測(cè)方法
返回信息 deviceInfo 中的 kind 字段可以區(qū)分出設(shè)備是音頻設(shè)備還是視頻設(shè)備,同時(shí)音頻設(shè)備能區(qū)分出是輸入設(shè)備和輸出設(shè)備,我們平時(shí)使用的耳機(jī)它是一個(gè)音頻設(shè)備,但它同時(shí)兼有音頻輸入設(shè)備和音頻輸出設(shè)備的功能
對(duì)于音頻設(shè)備和視頻設(shè)備會(huì)設(shè)置各自的默認(rèn)設(shè)備, 還是以耳機(jī)這個(gè)音頻設(shè)備為例,將耳機(jī)插入電腦后,耳機(jī)就變成了音頻的默認(rèn)設(shè)備,將耳機(jī)拔出后,默認(rèn)設(shè)備又切換成了系統(tǒng)的音頻設(shè)備
在獲取到所有的設(shè)備列表后,如果我們不指定某個(gè)具體設(shè)備,采集音視頻數(shù)據(jù)時(shí),就會(huì)從設(shè)備列表中的默認(rèn)設(shè)備上采集數(shù)據(jù),如果能從指定的設(shè)備上采集到音視頻數(shù)據(jù),那說(shuō)明這個(gè)設(shè)備就是有效的設(shè)備,這樣我們就可以對(duì)音視頻設(shè)備進(jìn)行一項(xiàng)一項(xiàng)檢測(cè)
通過(guò)調(diào)用 getUserMedia 方法 (下面音視頻采集的時(shí)候會(huì)講到) 進(jìn)行設(shè)備檢測(cè)

視頻設(shè)備檢測(cè):調(diào)用 getUserMedia API 采集視頻數(shù)據(jù)并將其展示出來(lái),如果用戶能看到自己的視頻,說(shuō)明視頻設(shè)備是有效的,否則,設(shè)備無(wú)效
音頻設(shè)備檢測(cè):調(diào)用 getUserMedia API 采集音頻數(shù)據(jù),由于音頻數(shù)據(jù)不能直接展示,所以需要使用 JavaScript 將其處理后展示到頁(yè)面上,這樣當(dāng)用戶看到音頻數(shù)值的變化后,說(shuō)明音頻設(shè)備也是有效的
音視頻采集
基本概念
幀率
幀率表示1秒鐘視頻內(nèi)圖像的數(shù)量,一般幀率達(dá)到 10~12fps 人眼就會(huì)覺(jué)得是連貫的,幀率越高,代表著每秒鐘處理的圖像數(shù)量越高,因此流量會(huì)越大,對(duì)設(shè)備的性能要求也越高,所以在直播系統(tǒng)中一般不會(huì)設(shè)置太高的幀率,高的幀率可以得到更流暢、更逼真的動(dòng)畫(huà),一般來(lái)說(shuō) 30fps 就是可以接受的,但是將性能提升至 60fps 則可以明顯提升交互感和逼真感,但是一般來(lái)說(shuō)超過(guò) 75fps 一般就不容易察覺(jué)到有明顯的流暢度提升了

軌(Track)
WebRTC 中的“軌”借鑒了多媒體的概念,兩條軌永遠(yuǎn)不會(huì)相交,“軌”在多媒體中表達(dá)的就是每條軌數(shù)據(jù)都是獨(dú)立的,不會(huì)與其他軌相交,如 MP4 中的音頻軌、視頻軌,它們?cè)?MP4 文件中是被分別存儲(chǔ)的

音視頻采集接口
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 接口調(diào)用出錯(cuò): ', error);
}
**srcObject[1]**:屬性設(shè)定或返回一個(gè)對(duì)象,這個(gè)對(duì)象提供了一個(gè)與 HTMLMediaElement 關(guān)聯(lián)的媒體源,這個(gè)對(duì)象通常是 MediaStream,根據(jù)規(guī)范也可以是 MediaSource, Blob 或者 File,但對(duì)于 MediaSource, Blob 和File類(lèi)型目前瀏覽器的兼容性不太好,所以對(duì)于這幾種類(lèi)型可以通過(guò) URL.createObjectURL() 創(chuàng)建 URL,并將其賦值給 HTMLMediaElement.src

MediaStreamConstraints 參數(shù),可以指定MediaStream中包含哪些類(lèi)型的媒體軌(音頻軌、視頻軌),并且可為這些媒體軌設(shè)置一些限制

const mediaStreamContrains = {
    video: {
       frameRate: {min: 15}, // 視頻的幀率最小 15 幀每秒
       width: {min: 320, ideal: 640}, // 寬度最小是 320,理想的寬度是 640
       height: {min: 480, ideal: 720},// 高度最小是 480,最理想高度是 720
       facingMode: 'user', // 優(yōu)先使用前置攝像頭
       deviceId: '' // 指定使用哪個(gè)設(shè)備
    },
    audio: {
       echoCancellation: true, // 對(duì)音頻開(kāi)啟回音消除功能
       noiseSuppression: true // 對(duì)音頻開(kāi)啟降噪功能
    }
}
瀏覽器實(shí)現(xiàn)自拍
我們知道視頻是由一幅幅幀圖像和一組音頻構(gòu)成的,所以拍照的過(guò)程其實(shí)是從連續(xù)播放的視頻流(一幅幅畫(huà)面)中抽取正在顯示的那張畫(huà)面,上面我們講過(guò)可以通過(guò) getUserMedia 獲取到視頻流,那如何從視頻流中獲取到正在顯示的圖片呢?

這里就要用到 canvas 的 drawImage[2]

const ctx = document.querySelector('canvas');
// 需要拍照時(shí)執(zhí)行此代碼,完成拍照
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();
}

// 調(diào)用 download 函數(shù)進(jìn)行圖片下載
downLoad(ctx.toDataURL("image/jpeg"));
drawImage 的第一個(gè)參數(shù)支持 HTMLVideoElement 類(lèi)型,所以可以直接將 $video 作為第一個(gè)參數(shù)傳入,這樣就通過(guò) canvas 獲取到照片了

然后通過(guò) a 標(biāo)簽的 download 將照片下載下來(lái)保存到本地

通過(guò) canvas 的 toDataURL 方法獲得圖片的 URL 地址
利用 a 標(biāo)簽的 downLoad 屬性來(lái)實(shí)現(xiàn)圖片的下載






音視頻錄制
基本概念
ArrayBuffer
ArrayBuffer 對(duì)象表示通用的、固定長(zhǎng)度的二進(jìn)制數(shù)據(jù)緩沖區(qū),可以使用它存儲(chǔ)圖片、視頻等內(nèi)容,但ArrayBuffer 對(duì)象不能直接進(jìn)行訪問(wèn),ArrayBuffer 只是描述有這樣一塊空間可以用來(lái)存放二進(jìn)制數(shù)據(jù),但在計(jì)算機(jī)的內(nèi)存中并沒(méi)有真正地為其分配空間,只有當(dāng)具體類(lèi)型化后,它才真正地存在于內(nèi)存中

let buffer = new ArrayBuffer(16); // 創(chuàng)建一個(gè)長(zhǎng)度為 16 的 buffer
let view = new Uint32Array(buffer);
ArrayBufferView
是Int32Array、Uint8Array、DataView等類(lèi)型的總稱(chēng),這些類(lèi)型都是使用 ArrayBuffer 類(lèi)實(shí)現(xiàn)的,因此才統(tǒng)稱(chēng)他們?yōu)?ArrayBufferView

Blob
(Binary Large Object)是 JavaScript 的大型二進(jìn)制對(duì)象類(lèi)型,WebRTC 最終就是使用它將錄制好的音視頻流保存成多媒體文件的,而它的底層是由上面所講的 ArrayBuffer 對(duì)象的封裝類(lèi)實(shí)現(xiàn)的,即 Int8Array、Uint8Array 等類(lèi)型

音頻錄制接口
const mediaRecorder = new MediaRecorder(stream[, options]);
stream參數(shù)是將要錄制的流,它可以是來(lái)自于使用 navigator.mediaDevices.getUserMedia 創(chuàng)建的流或者來(lái)自于 audio,video 以及 canvas DOM 元素

MediaRecorder.ondataavailable事件可用于獲取錄制的媒體資源 (在事件的 data 屬性中會(huì)提供一個(gè)可用的 Blob 對(duì)象)

錄制的流程如下:

使用 getUserMedia 接口獲取視頻流數(shù)據(jù)
使用 MediaRecorder 接口進(jìn)行錄制(視頻流數(shù)據(jù)來(lái)源上一步獲取的數(shù)據(jù))
使用 MediaRecorder 的 ondataavailable 事件獲取錄制的 buffer 數(shù)據(jù)
將 buffer 數(shù)據(jù)轉(zhuǎn)成 Blob 類(lèi)型,然后使用 createObjectURL 生成可訪問(wèn)的視頻地址
利用 a 標(biāo)簽的 download 屬性進(jìn)行視頻下載
<video autoplay playsinline controls id="video-show"></video>
<video id="video-replay"></video>
<button id="record">開(kāi)始錄制</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 = [];     
  // 設(shè)置錄制下來(lái)的多媒體格式
  const options = {
    mimeType: 'video/webm;codecs=vp8'
  }

  // 判斷瀏覽器是否支持錄制
  if(!MediaRecorder.isTypeSupported(options.mimeType)){
    console.error(`${options.mimeType} is not supported!`);
    return;
  }

  try{
    // 創(chuàng)建錄制對(duì)象
    mediaRecorder = new MediaRecorder(window.stream, options);
    console.log('mediaRecorder', mediaRecorder);
  }catch(e){
    console.error('Failed to create MediaRecorder:', e);
    return;
  }

  // 當(dāng)有音視頻數(shù)據(jù)來(lái)了之后觸發(fā)該事件
  mediaRecorder.ondataavailable = handleDataAvailable;
  // 開(kāi)始錄制
  mediaRecorder.start(2000); // 若設(shè)置了 timeslice 這個(gè)毫秒值,那么錄制的數(shù)據(jù)會(huì)按照設(shè)定的值分割成一個(gè)個(gè)單獨(dú)的區(qū)塊
}

// 當(dāng)該函數(shù)被觸發(fā)后,將數(shù)據(jù)壓入到 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();
};
創(chuàng)建連接
數(shù)據(jù)采集完成,接下來(lái)就要開(kāi)始建立連接,然后進(jìn)行數(shù)據(jù)通信了

要實(shí)現(xiàn)一套 1 對(duì) 1 的通話系統(tǒng),通常我們的思路會(huì)是在每一端創(chuàng)建一個(gè) socket,然后通過(guò)該 socket 與對(duì)端相連,當(dāng) socket 連接成功之后,就可以通過(guò) socket 向?qū)Χ税l(fā)送數(shù)據(jù)或者接收對(duì)端的數(shù)據(jù)了,WebRTC 中提供了 RTCPeerConnection 類(lèi),其工作原理和 socket 基本一樣,不過(guò)它的功能更強(qiáng)大,實(shí)現(xiàn)也更為復(fù)雜,下面就來(lái)講講 WebRTC 中的 RTCPeerConnection

RTCPeerConnection
在音視頻通信中,每一方只需要有一個(gè) RTCPeerConnection 對(duì)象,用它來(lái)接收或發(fā)送音視頻數(shù)據(jù),然而在真實(shí)的場(chǎng)景中,為了實(shí)現(xiàn)端與端之間的通話,還需要利用信令服務(wù)器交換一些信息,比如交換雙方的 IP 和 port 地址,這樣通信的雙方才能彼此建立連接

WebRTC 規(guī)范對(duì) WebRTC 要實(shí)現(xiàn)的功能、API 等相關(guān)信息做了大量的約束,比如規(guī)范中定義了如何采集音視頻數(shù)據(jù)、如何錄制以及如何傳輸?shù)?,甚至更?xì)的,還定義了都有哪些 API,以及這些 API 的作用是什么,但這些約束只針對(duì)于客戶端,并沒(méi)有對(duì)服務(wù)端做任何限制,這就導(dǎo)致了我們?cè)谑褂?WebRTC 的時(shí)候,必須自己去實(shí)現(xiàn) 信令 服務(wù), 這里就不專(zhuān)門(mén)研究怎么實(shí)現(xiàn)信令服務(wù)器了,我們只來(lái)看看 RTCPeerConnection 是如何實(shí)現(xiàn)一對(duì)一通信的

RTCPeerConnection 如何工作呢?

獲取本地音視頻流
為連接的每個(gè)端創(chuàng)建一個(gè) RTCPeerConnection 對(duì)象,并且給 RTCPeerConnection 對(duì)象添加一個(gè)本地流,該流是從 getUserMedia 獲取的

// 調(diào)用 getUserMedia API 獲取音視頻流
navigator.mediaDevices.getUserMedia(mediaStreamConstraints).
  then(gotLocalMediaStream)
 
function gotLocalMediaStream(mediaStream) {
  window.stream = mediaStream;
}
 
// 創(chuàng)建 RTCPeerConnection 對(duì)象
let localPeerConnection = new RTCPeerConnection();
 
// 將音視頻流添加到 RTCPeerConnection 對(duì)象中
localPeerConnection.addStream(stream);
交換媒體描述信息
獲得音視頻流后,就可以開(kāi)始與對(duì)端進(jìn)行媒體協(xié)商了(媒體協(xié)商就是看看你的設(shè)備都支持哪些編解碼器,我的設(shè)備是否也支持?如果我的設(shè)備也支持,那么咱們雙方就算協(xié)商成功了),這個(gè)過(guò)程需要通過(guò)信令服務(wù)器完成

現(xiàn)在假設(shè) A 和 B 需要通訊

A 通過(guò) createOffer[3] 方法啟動(dòng)創(chuàng)建一個(gè) SDP offer,即得到 A 的本地會(huì)話描述
A 通過(guò) setLocalDescription ****方法保存本地會(huì)話描述
A 通過(guò)信令服務(wù)器發(fā)送信令給 B
localPeerConnection.createOffer([options])
  .then((description) => {
        // 將 offer 保存到本地
      localPeerConnection.setLocalDescription(description)
        .then(() => {
          setLocalDescriptionSuccess(localPeerConnection);
        });
   })
B 接收到帶有 A offer 的信令,調(diào)用 setRemoteDescription,設(shè)置遠(yuǎn)程會(huì)話描述
B 通過(guò) createAnswer 方法將本地會(huì)話描述成功回調(diào)
B 調(diào)用 setLocalDescription 設(shè)置他自己的本地局部描述回調(diào)函數(shù)中保存本地會(huì)話描述
B 通過(guò)信令服務(wù)器發(fā)送信令給 A
// B 設(shè)置遠(yuǎn)程會(huì)話描述
remotePeerConnection.setRemoteDescription(description)
.then(() => {
  setRemoteDescriptionSuccess(remotePeerConnection);
});

remotePeerConnection.createAnswer()
.then((description)=> {
  // B 保存本地會(huì)話描述
  remotePeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(remotePeerConnection);
    });
});
A 通過(guò) setRemoteDescription 將 B 的應(yīng)答 answer 保存為遠(yuǎn)程會(huì)話描述
// A 保存 B 的 應(yīng)答 answer 為遠(yuǎn)程會(huì)話描述
  localPeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(localPeerConnection);
    });
至此就完成了媒體信息交換和協(xié)商

端與端建立連接
當(dāng) A 調(diào)用 setLocalDescription 函數(shù)成功后,會(huì)觸發(fā) icecandidate 事件(在建立通訊之前,我們需要獲得雙方的網(wǎng)絡(luò)信息,例如 IP、端口等,candidate 便是用于保存這些東西的)
localPeerConnection.onicecandidate= function(event) {
  // 獲取到觸發(fā) icecandidate 事件的 RTCPeerConnection 對(duì)象
  const peerConnection = event.target;
  // 獲取到具體的 candidate
  const iceCandidate = event.candidate;
  // 將 candidate 包裝成需要的格式,然后通過(guò)信令服務(wù)器發(fā)送給B
 
}
B 接收到信令服務(wù)器傳遞過(guò)來(lái)的 A 的關(guān)于 candidate 的信息,把消息包裝成 RTCIceCandidate 對(duì)象,然后調(diào)用 addIceCandidate 保存起來(lái)
// 創(chuàng)建 RTCIceCandidate 對(duì)象
const newIceCandidate = new RTCIceCandidate(iceCandidate);
remotePeerConnection.addIceCandidate(newIceCandidate);
這樣就收集到了一個(gè)新的 Candidate,在真實(shí)的場(chǎng)景中,每當(dāng)獲得一個(gè)新的 Candidate 后,就會(huì)通過(guò)信令服務(wù)器交換給對(duì)端,對(duì)端再調(diào)用 RTCPeerConnection 對(duì)象的 addIceCandidate() 方法將收到的 Candidate 保存起來(lái),然后按照 Candidate 的優(yōu)先級(jí)進(jìn)行連通性檢測(cè),如果 Candidate 連通性檢測(cè)完成,那么端與端之間就建立了連接,這時(shí)媒體數(shù)據(jù)就可以通過(guò)這個(gè)連接進(jìn)行傳輸了

音視頻編解碼
視頻是連續(xù)的圖像序列,由連續(xù)的幀構(gòu)成,一幀即為一幅圖像,由于人眼的視覺(jué)暫留效應(yīng),當(dāng)幀序列以一定的速率播放時(shí),我們看到的就是動(dòng)作連續(xù)的視頻,由于連續(xù)的幀之間相似性極高,為便于儲(chǔ)存?zhèn)鬏敚覀冃枰獙?duì)原始的視頻進(jìn)行編碼壓縮,以去除空間、時(shí)間維度的冗余

視頻編解碼是采用算法將視頻數(shù)據(jù)的冗余信息去除,對(duì)圖像進(jìn)行壓縮、存儲(chǔ)及傳輸, 再將視頻進(jìn)行解碼及格式轉(zhuǎn)換, 追求在可用的計(jì)算資源內(nèi),盡可能高的視頻重建質(zhì)量和盡可能高的壓縮比,以達(dá)到帶寬和存儲(chǔ)容量要求的視頻處理技術(shù)

視頻流傳輸中最為重要的編解碼標(biāo)準(zhǔn)有H.26X系列(H.261、H.263、H.264),MPEG系列,Apple公司的 QuickTime 等

顯示遠(yuǎn)端媒體流
通過(guò) RTCPeerConnection 對(duì)象 A 與 B 雙方建立連接后,本地的多媒體數(shù)據(jù)經(jīng)過(guò)編碼以后就可以被傳送到遠(yuǎn)端了,遠(yuǎn)端收到了媒體數(shù)據(jù)解碼后,怎么顯示出來(lái)呢,下面以 video 為例,看看怎么讓 RTCPeerConnection 獲得的媒體數(shù)據(jù)與 video 標(biāo)簽結(jié)合起來(lái)

當(dāng)遠(yuǎn)端有數(shù)據(jù)流到來(lái)的時(shí)候,瀏覽器會(huì)回調(diào) onaddstream 函數(shù),在回調(diào)函數(shù)中將得到的 stream 賦值給 video 標(biāo)簽的 srcObject 對(duì)象,這樣 video 就與 RTCPeerConnection 進(jìn)行了綁定,video 就能從 RTCPeerConnection 獲取到視頻數(shù)據(jù),并最終將其顯示出來(lái)了

localPeerConnection.onaddstream = function(event) {
  $remoteVideo.srcObject = event.stream;
}
結(jié)語(yǔ)
WebRTC 相關(guān)的東西非常非常多,這里只是很淺顯地串講了一下利用 WebRTC 實(shí)現(xiàn)實(shí)時(shí)通信的大體過(guò)程,如果感興趣可以詳細(xì)研究里面的細(xì)節(jié)

參考資料
[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.liulili


歡迎關(guān)注微信公眾號(hào) :前端民工