webRTC探索音視頻的錄製的實現

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一直心癢於“在線視頻通話”卻無從下手,直到最近接觸了webRTC技術。雖然還未有此功力,但卻“誤打誤撞”解決了困擾我的另一個問題:音視頻錄製!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"什麼是webRTC","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://developer.mozilla.org/zh-CN/docs/Web/API/WebRTC_API","title":"","type":null},"content":[{"type":"text","text":"MDN","attrs":{}}]},{"type":"text","text":"描述說:“WebRTC (Web Real-Time Communications) 是一項實時通訊技術,它允許網絡應用或者站點,在不借助中間媒介的情況下,建立瀏覽器之間點對點(Peer-to-Peer)的連接,實現視頻流和(或)音頻流或者其他任意數據的傳輸。WebRTC包含的這些標準使用戶在無需安裝任何插件或者第三方的軟件的情況下,創建點對點(Peer-to-Peer)的數據分享和電話會議成爲可能。”總結下來,其實有四點:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"跨平臺","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"(主要)用於瀏覽器","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"實時傳輸","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"音視頻引擎","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在稍稍深入學習了webRTC之後,我發現其不僅可以用於「音視頻錄製、視頻通話」,還可以用在「照相機、音樂播放器、共享遠程桌面、即時通信工具、P2P網絡加速、文件傳輸、實時人臉識別」等場景上 —— 當然,是結合了其他衆多技術的基礎上完成。不過這些中有幾項似乎讓我們很“熟悉”:照相機、人臉識別、共享桌面。這是因爲RTC使用的是基於音視頻流的API!","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"webRTC音視頻數據採集","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實現數據傳輸最重要的就是數據採集了,這裏有一個非常重要的API:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"let promise=navigator.mediaDevices.getUserMedia(containts);\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個API會提示用戶給予使用媒體輸入的許可,媒體輸入會產生一個","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"MediaStream","attrs":{}}],"attrs":{}},{"type":"text","text":",裏面包含了請求的媒體類型的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"軌道","attrs":{}},{"type":"text","text":"。此流可以包含一個視頻軌道(來自硬件或者虛擬視頻源,比如相機、視頻採集設備和屏幕共享服務等等)、一個音頻軌道(同樣來自硬件或虛擬音頻源,比如麥克風、A/D轉換器等等),也可能是其它軌道類型。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它返回一個 Promise 對象,成功後會resolve回調一個 MediaStream 對象。若用戶拒絕了使用權限,或者需要的媒體源不可用,promise會reject回調一個 PermissionDeniedError 或者 NotFoundError 。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏最重要的就是“軌道”了:因爲它是基於“流”的。它得到的是一個MediaSource 對象(這是一個stream對象)!也就意味着:如果要使用此結果,要麼用","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"srcObject","attrs":{}}],"attrs":{}},{"type":"text","text":"屬性,要麼就得用 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"URL.createObjectURL()","attrs":{}}],"attrs":{}},{"type":"text","text":" 轉爲url!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/fe/fe3f626198bfb015af04008f8da0f471.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"回到API本身,我們可以這麼理解:通過它我們可以得到當前頁面所有的音視頻通道的集合,根據不同的接口我們將它們分爲不同的“軌道”,我們可以對它們進行設置和輸出配置項。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 ","attrs":{}},{"type":"link","attrs":{"href":"https://blog.csdn.net/qq_43624878/article/details/107575226#id=input_30","title":"","type":null},"content":[{"type":"text","text":"這篇文章","attrs":{}}]},{"type":"text","text":" 中我們可以看到實現“拍照”的部分就用到了這個API","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們先在頁面上寫個video元素:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"html"},"content":[{"type":"text","text":"\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏爲什麼要用video元素?我們需要獲取音視頻流,而HTML5中相關的元素也就video和audio了,而能同時獲取這兩個的就只有video元素了(如果你只需要音頻流,你完全可以用audio元素)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據上面getUserMedia API 的用法,我們不難寫出如下的語法:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"navigator.mediaDevices.getUserMedia(constraints)\n .then(gotMediaStream)\n .catch(handleError);\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"啊,我突然想起來,還沒有介紹 ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"contraints","attrs":{}},{"type":"text","text":" 參數呢!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"webRTC獲取約束","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“約束”是指:控制對象的一些配置項。約束分爲兩類:視頻約束和音頻約束。在webRTC中,常用的視頻約束有:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"width","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"height","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"aspectRatio:寬高比","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"frameRate","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"facingMode:攝像頭翻轉(前後攝像頭)(這個配置主要是對於移動端來說,因爲瀏覽器只有前置攝像頭)","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"resizeMode:是否進行剪裁","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而常用的音頻約束有:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"volume:音量","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"sampleRate:採樣率","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"sampleSize:採樣大小","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"echoCancellation:迴音消除設置","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"autoGainControl:自動增益","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"noiseSuppression:降噪","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"latency:延遲(根據不同場景,一般來說越小延遲越小體驗越好)","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"channelCount:單/雙聲道","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"...","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"contraints是什麼?官方把它稱作“MediaStreamContraints”。通過代碼我們可以更清晰地看到它:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"dictionary MediaStreamContraints{\n (boolean or MediaTrackContaints) video = false;\n (boolean or MediaTrackContaints) audio = false;\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它是負責","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"對音視頻約束的採集","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果video/audio是簡單的設置爲bool類型,則只是簡單決定是否要採集","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果video/audio是Media Track,則可進一步設置比如:視頻的分辨率、幀率、音頻的音量、採樣率等","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據上面說明,我們可以在代碼中完善一下配置項 —— 當然,通常使用這種API第一件事是要判斷用戶當前瀏覽器是否支持:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){\n console.log('getUserMedia is not supported!');\n return;\n}else{\n var constraints={\n // 這裏也可以直接:video:false/true,則知識簡單的表示不採集/採集視頻\n video:{\n width:640,\n height:480,\n frameRate:60\n },\n // 這裏也可以直接:audio:false/true,則只是簡單的表示不採集/採集音頻\n audio:{\n noiseSuppression:true,\n echoCancellation:true\n }\n }\n navigator.mediaDevices.getUserMedia(constraints)\n .then(gotMediaStream)\n .catch(handleError);\n\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面就該實現getUserMedia API 成功和失敗時的回調函數了,這裏我們要先搞懂“在回調時要幹什麼” —— 其實無非是將“獲取到的流”交出去:給全局變量保存起來或者直接作爲video/audio的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"srcObject","attrs":{}}],"attrs":{}},{"type":"text","text":"值(成功)或者拋出錯誤(失敗)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var videoplay=document.querySelector('#player');\nfunction gotMediaStream(stream){\n // 【1】\n videoplay.srcObject=stream;\n // 【2】\n // return navigator.mediaDevices.enumerateDevices();\n}\nfunction handleError(err){\n console.log('getUserMedia error:',err);\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"註釋中用了一個API:","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"mediaDevices.enumerateDevices()","attrs":{}}],"attrs":{}},{"type":"text","text":"。MediaDevices方法列舉了所有可用的媒體輸入和輸出設備(如麥克風、相機、耳機等)。返回的承諾通過描述設備的MediaDevice 信息陣列得到解決。我們也可以把它看做上面說的“軌道集合”。比如這裏如果你把註釋按promise方式寫出,就會發現它是一個數組,裏面包含了三個“軌道”。兩個audio(輸入、輸出)一個video:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/69/69dcb6e0780844d4d570817f18ef8788.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而當你輸出stream參數後你會發現","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4b/4b0d016381cefe977857a3f9ef9df64d.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在一些場景下,通過這個API我們可以將一些設備配置暴露給用戶","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此視頻中的第一個效果——實時捕獲音視頻就完成了。這也是下面的基礎:不能捕獲,又怎麼錄製呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們先再加一些需要的節點:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"html"},"content":[{"type":"text","text":"\n\n\n\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面說了,經過捕獲後得到的是一個“流”對象,那麼接收時也一定需要一個能拿到流並進行操作的API :MediaStream API!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://developer.mozilla.org/zh-CN/docs/Web/API/MediaRecorder","title":"","type":null},"content":[{"type":"text","text":"MDN","attrs":{}}]},{"type":"text","text":"上是這樣介紹的:“MediaRecorder 是 MediaStream Recording API 提供的用來進行媒體輕鬆錄製的接口, 他需要通過調用 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"MediaRecorder()","attrs":{}}],"attrs":{}},{"type":"text","text":" 構造方法進行實例化。”因爲是 MediaStream Recording API 提供的接口,所以它有一個構造函數:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"MediaRecorder.MediaRecorder()","attrs":{}}],"attrs":{}},{"type":"text","text":":創建一個新的MediaRecorder對象,對指定的MediaStream 對象進行錄製,支持的配置項包括設置容器的MIME 類型 (例如\"video/webm\" 或者 \"video/mp4\")和音頻及視頻的碼率或者二者同用一個碼率","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據MDN上提供的方法,我們可以得到一個比較清晰的思路:先調用構造函數,將前面獲取到的流傳入,經過API的解析後拿到一個對象,在規定的時間切片下將他們依次傳入數組中(因爲至此工作基本就已經完成了,用數組接收方便以後轉爲Blob對象操作):","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function startRecord(){\n // 定義一個接收數組\n buffer=[];\n\n var options={\n mimeType:'video/webm;codecs=vp8'\n }\n // 返回一個Boolean值,來表示設置的MIME type 是否被當前用戶的設備支持.\n if(!MediaRecorder.isTypeSupported(options.mimeType)){\n console.error(`${options.mimeType} is not supported`);\n return;\n }\n\n try{\n mediaRecorder=new MediaRecorder(window.stream,options);\n }catch(e){\n console.error('Fail to create');\n return;\n }\n mediaRecorder.ondataavailable=handleDataAvailable;\n // 時間片\n // 開始錄製媒體,這個方法調用時可以通過給timeslice參數設置一個毫秒值,如果設置這個毫秒值,那麼錄製的媒體會按照你設置的值進行分割成一個個單獨的區塊\n mediaRecorder.start(10);\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏面有兩點需要注意的地方:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"獲取的stream流:因爲之前捕獲音視頻的函數必然要封裝起來,所以這裏如果再要使用就一定要把這個流對象設置爲全局對象 —— 筆者直接將其掛載到了window對象上,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"在前面代碼中註釋[1]的地方接收對象:","attrs":{}},{"type":"text","text":" ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"window.stream=stream;","attrs":{}}],"attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"這裏定義了一個buffer數組,和mediaRecorder對象一樣,考慮到要在","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"ondataavailable","attrs":{}}],"attrs":{}},{"type":"text","text":" 方法中的使用以及在完成後需要停止繼續捕獲錄製音視頻流,也放在全局下聲明變量","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var buffer;\nvar mediaRecorder;\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ondataavailable方法就是在錄製數據準備完成後才觸發的。在裏面我們需要做的就是按照之前的思路將切片好的數據依次存入","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function handleDataAvailable(e){\n // console.log(e)\n if(e && e.data && e.data.size>0){\n buffer.push(e.data);\n }\n}\n\n//結束錄製\nfunction stopRecord(){\n mediaRecorder.stop();\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後調用:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"let recvideo=document.querySelector('#recplayer');\nlet btnRecord=document.querySelector('#record');\nlet btnPlay=document.querySelector('#recplay');\nlet btnDownload=document.querySelector('#download');\n// 開始/停止錄製\nbtnRecord.onclick=()=>{\n if(btnRecord.textContent==='Start Record'){\n startRecord();\n btnRecord.textContent='Stop Record';\n btnPlay.disabled=true;\n btnDownload.disabled=true;\n }else{\n stopRecord();\n btnRecord.textContent='Start Record';\n btnPlay.disabled=false;\n btnDownload.disabled=false;\n }\n}\n// 播放\nbtnPlay.onclick=()=>{\n var blob=new Blob(buffer,{type: 'video/webm'});\n recvideo.src=window.URL.createObjectURL(blob);\n recvideo.srcObject=null;\n recvideo.controls=true;\n recvideo.play();\n}\n// 下載\nbtnDownload.onclick=()=>{\n var blob=new Blob(buffer,{type:'video/webm'});\n var url=window.URL.createObjectURL(blob);\n var a=document.createElement('a');\n a.href=url;\n a.style.display='none';\n a.download='recording.webm';\n a.click();\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文章到這裏功能就基本實現了,但是還有一點問題:如果只想要錄製音頻,這時候在你說話的時候因爲捕獲流一直在工作所以其實這時候有兩個聲源 —— 你的聲音和捕獲到的你的聲音。所以我們需要將捕獲時的聲音關掉!但是如果你根據前面說的在","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"getUserMedia","attrs":{}}],"attrs":{}},{"type":"text","text":" API中添加 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"volume:0","attrs":{}}],"attrs":{}},{"type":"text","text":" 是不會有任何效果的 —— 因爲至今還沒有任何瀏覽器對這個屬性支持。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是聯想到我們承載音視頻的是一個video/audio容器,所以我們其實可以直接用其對應DOM節點的volume屬性控制:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// 在前面代碼中註釋爲【2】的地方添加代碼\nvideoplay.volume=0;\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大功告成!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文完整代碼已開源到","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/1314mxc/study_for_webRTC","title":"","type":null},"content":[{"type":"text","text":"GitHub:study_for_webRTC","attrs":{}}]},{"type":"text","text":" ,歡迎閱讀、下載 & star!","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章