一直心癢于“在線視頻通話”卻無從下手,直到最近接觸了webRTC技術,雖然還未有此功力,但卻“誤打誤撞”解決了困擾我的另一個問題:音視頻錄制!
webRTC實戰一(聲音有些不穩定,因為是晚上錄制的,是在抱歉,不過視頻主要是為了演示)
什么是webRTC
MDN描述說:“WebRTC (Web Real-Time Communications) 是一項實時通訊技術,它允許網路應用或者站點,在不借助中間媒介的情況下,建立瀏覽器之間點對點(Peer-to-Peer)的連接,實作視頻流和(或)音頻流或者其他任意資料的傳輸,WebRTC包含的這些標準使用戶在無需安裝任何插件或者第三方的軟體的情況下,創建點對點(Peer-to-Peer)的資料分享和電話會議成為可能,”
總結下來,其實有四點:
- 跨平臺
- (主要)用于瀏覽器
- 實時傳輸
- 音視頻引擎
在稍稍深入學習了webRTC之后,我發現其不僅可以用于「音視頻錄制、視頻通話」,還可以用在「照相機、音樂播放器、共享遠程桌面、即時通信工具、P2P網路加速、檔案傳輸、實時人臉識別」等場景上 —— 當然,是結合了其他眾多技術的基礎上完成,
不過這些中有幾項似乎讓我們很“熟悉”:照相機、人臉識別、共享桌面,這是因為RTC使用的是基于音視頻流的API!
webRTC音視頻資料采集
實作資料傳輸最重要的就是資料采集了,這里有一個非常重要的API:
let promise=navigator.mediaDevices.getUserMedia(containts);
這個API會提示用戶給予使用媒體輸入的許可,媒體輸入會產生一個MediaStream,里面包含了請求的媒體型別的軌道,此流可以包含一個視頻軌道(來自硬體或者虛擬視頻源,比如相機、視頻采集設備和螢屏共享服務等等)、一個音頻軌道(同樣來自硬體或虛擬音頻源,比如麥克風、A/D轉換器等等),也可能是其它軌道型別,
它回傳一個 Promise 物件,成功后會resolve回呼一個 MediaStream 物件,若用戶拒絕了使用權限,或者需要的媒體源不可用,promise會reject回呼一個 PermissionDeniedError 或者 NotFoundError ,
這里最重要的就是“軌道”了:因為它是基于“流”的,它得到的是一個MediaSource 物件(這是一個stream物件)!也就意味著:如果要使用此結果,要么用srcObject屬性,要么就得用 URL.createObjectURL() 轉為url!

回到API本身,我們可以這么理解:通過它我們可以得到當前頁面所有的音視頻通道的集合,根據不同的介面我們將它們分為不同的“軌道”,我們可以對它們進行設定和輸出配置項,
在 這篇文章 中我們可以看到實作“拍照”的部分就用到了這個API
我們先在頁面上寫個video元素:
<video autoplay playsinline id="player"></video>
這里為什么要用video元素?
我們需要獲取音視頻流,而HTML5中相關的元素也就video和audio了,而能同時獲取這兩個的就只有video元素了(如果你只需要音頻流,你完全可以用audio元素),
根據上面getUserMedia API 的用法,我們不難寫出如下的語法:
navigator.mediaDevices.getUserMedia(constraints)
.then(gotMediaStream)
.catch(handleError);
啊,我突然想起來,還沒有介紹 contraints 引數呢!
webRTC獲取約束
“約束”是指:控制物件的一些配置項,約束分為兩類:視頻約束和音頻約束,
在webRTC中,常用的視頻約束有:
- width
- height
- aspectRatio:寬高比
- frameRate
- facingMode:攝像頭翻轉(前后攝像頭)(這個配置主要是對于移動端來說,因為瀏覽器只有前置攝像頭)
- resizeMode:是否進行剪裁
而常用的音頻約束有:
- volume:音量
- sampleRate:采樣率
- sampleSize:采樣大小
- echoCancellation:回音消除設定
- autoGainControl:自動增益
- noiseSuppression:降噪
- latency:延遲(根據不同場景,一般來說越小延遲越小體驗越好)
- channelCount:單/雙聲道
- …
contraints是什么?官方把它稱作“MediaStreamContraints”,通過代碼我們可以更清晰地看到它:
dictionary MediaStreamContraints{
(boolean or MediaTrackContaints) video = false;
(boolean or MediaTrackContaints) audio = false;
}
它是負責對音視頻約束的采集
- 如果video/audio是簡單的設定為bool型別,則只是簡單決定是否要采集
- 如果video/audio是Media Track,則可進一步設定比如:視頻的解析度、幀率、音頻的音量、采樣率等
根據上面說明,我們可以在代碼中完善一下配置項 —— 當然,通常使用這種API第一件事是要判斷用戶當前瀏覽器是否支持:
if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){
console.log('getUserMedia is not supported!');
return;
}else{
var constraints={
// 這里也可以直接:video:false/true,則知識簡單的表示不采集/采集視頻
video:{
width:640,
height:480,
frameRate:60
},
// 這里也可以直接:audio:false/true,則只是簡單的表示不采集/采集音頻
audio:{
noiseSuppression:true,
echoCancellation:true
}
}
navigator.mediaDevices.getUserMedia(constraints)
.then(gotMediaStream)
.catch(handleError);
}
下面就該實作getUserMedia API 成功和失敗時的回呼函式了,這里我們要先搞懂“在回呼時要干什么” —— 其實無非是將“獲取到的流”交出去:給全域變數保存起來或者直接作為video/audio的srcObject值(成功)或者拋出錯誤(失敗)
var videoplay=document.querySelector('#player');
function gotMediaStream(stream){
// 【1】
videoplay.srcObject=stream;
// 【2】
// return navigator.mediaDevices.enumerateDevices();
}
function handleError(err){
console.log('getUserMedia error:',err);
}
注釋中用了一個API:
mediaDevices.enumerateDevices(),MediaDevices方法列舉了所有可用的媒體輸入和輸出設備(如麥克風、相機、耳機等),回傳的承諾通過描述設備的MediaDevice 資訊陣列得到解決,
我們也可以把它看做上面說的“軌道集合”,比如這里如果你把注釋按promise方式寫出,就會發現它是一個陣列,里面包含了三個“軌道”,兩個audio(輸入、輸出)一個video:
而當你輸出stream引數后你會發現
在一些場景下,通過這個API我們可以將一些設備配置暴露給用戶
至此視頻中的第一個效果——實時捕獲音視頻就完成了,
這也是下面的基礎:不能捕獲,又怎么錄制呢?
我們先再加一些需要的節點:
<video id="recplayer" playsinline></video>
<button id="record">Start Record</button>
<button id="recplay" disabled>Play</button>
<button id="download" disabled>Download</button>
前面說了,經過捕獲后得到的是一個“流”物件,那么接收時也一定需要一個能拿到流并進行操作的API :MediaStream API!
MDN上是這樣介紹的:“MediaRecorder 是 MediaStream Recording API 提供的用來進行媒體輕松錄制的介面, 他需要通過呼叫 MediaRecorder() 構造方法進行實體化,”
因為是 MediaStream Recording API 提供的介面,所以它有一個建構式:
MediaRecorder.MediaRecorder():創建一個新的MediaRecorder物件,對指定的MediaStream 物件進行錄制,支持的配置項包括設定容器的MIME 型別 (例如"video/webm" 或者 “video/mp4”)和音頻及視頻的碼率或者二者同用一個碼率
根據MDN上提供的方法,我們可以得到一個比較清晰的思路:先呼叫建構式,將前面獲取到的流傳入,經過API的決議后拿到一個對象,在規定的時間切片下將他們依次傳入陣列中(因為至此作業基本就已經完成了,用陣列接收方便以后轉為Blob物件操作):
function startRecord(){
// 定義一個接收陣列
buffer=[];
var options={
mimeType:'video/webm;codecs=vp8'
}
// 回傳一個Boolean值,來表示設定的MIME type 是否被當前用戶的設備支持.
if(!MediaRecorder.isTypeSupported(options.mimeType)){
console.error(`${options.mimeType} is not supported`);
return;
}
try{
mediaRecorder=new MediaRecorder(window.stream,options);
}catch(e){
console.error('Fail to create');
return;
}
mediaRecorder.ondataavailable=handleDataAvailable;
// 時間片
// 開始錄制媒體,這個方法呼叫時可以通過給timeslice引數設定一個毫秒值,如果設定這個毫秒值,那么錄制的媒體會按照你設定的值進行分割成一個個單獨的區塊
mediaRecorder.start(10);
}
這里面有兩點需要注意的地方:
- 獲取的stream流:因為之前捕獲音視頻的函式必然要封裝起來,所以這里如果再要使用就一定要把這個流物件設定為全域物件 —— 筆者直接將其掛載到了window物件上,在前面代碼中注釋[1]的地方接收物件:
window.stream=stream; - 這里定義了一個buffer陣列,和mediaRecorder物件一樣,考慮到要在
ondataavailable方法中的使用以及在完成后需要停止繼續捕獲錄制音視頻流,也放在全域下宣告變數
var buffer;
var mediaRecorder;
ondataavailable方法就是在錄制資料準備完成后才觸發的,在里面我們需要做的就是按照之前的思路將切片好的資料依次存入
function handleDataAvailable(e){
// console.log(e)
if(e && e.data && e.data.size>0){
buffer.push(e.data);
}
}
//結束錄制
function stopRecord(){
mediaRecorder.stop();
}
然后呼叫:
let recvideo=document.querySelector('#recplayer');
let btnRecord=document.querySelector('#record');
let btnPlay=document.querySelector('#recplay');
let btnDownload=document.querySelector('#download');
// 開始/停止錄制
btnRecord.onclick=()=>{
if(btnRecord.textContent==='Start Record'){
startRecord();
btnRecord.textContent='Stop Record';
btnPlay.disabled=true;
btnDownload.disabled=true;
}else{
stopRecord();
btnRecord.textContent='Start Record';
btnPlay.disabled=false;
btnDownload.disabled=false;
}
}
// 播放
btnPlay.onclick=()=>{
var blob=new Blob(buffer,{type: 'video/webm'});
recvideo.src=window.URL.createObjectURL(blob);
recvideo.srcObject=null;
recvideo.controls=true;
recvideo.play();
}
// 下載
btnDownload.onclick=()=>{
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='recording.webm';
a.click();
}
文章到這里視頻中的功能就基本實作了,但是還有一點問題:如果只想要錄制音頻,這時候在你說話的時候因為捕獲流一直在作業所以其實這時候有兩個聲源 —— 你的聲音和捕獲到的你的聲音,
所以我們需要將捕獲時的聲音關掉!
但是如果你根據前面說的在getUserMedia API中添加 volume:0 是不會有任何效果的 —— 因為至今還沒有任何瀏覽器對這個屬性支持,
但是聯想到我們承載音視頻的是一個video/audio容器,所以我們其實可以直接用其對應DOM節點的volume屬性控制:
// 在前面代碼中注釋為【2】的地方添加代碼
videoplay.volume=0;
大功告成!
本文完整代碼已開源到GitHub:study_for_webRTC ,歡迎閱讀、下載 & star!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/277770.html
標籤:其他


