大家好,我是若川。持續組織了6個月源碼共讀活動,感興趣的可以點此加我微信 ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列
前言
最近項目遇到一個要在網頁上錄音的需求,在一波搜索后,發現了 react-media-recorder[1] 這個庫。今天就跟大家一起研究一下這個庫的源碼吧,從 0 到 1 來實現一個 React 的錄音、錄像和錄屏功能。
完整項目代碼放在 Github[2]
需求與思路
首先要明確我們要完成的事:錄音,錄像,錄屏。
這種錄制媒體流的原理其實很簡單。

只需要記住:把輸入 stream
存放在 blobList
,最后轉成預覽 blobUrl
。

基礎功能
有了上面的簡單思路后,我們可以先做一個簡單的錄音與錄像功能。
這里先把基礎的 HTML 結構實現了:
const?App?=?()?=>?{const?[audioUrl,?setAudioUrl]?=?useState<string>('');const?startRecord?=?async?()?=>?{}const?stopRecord?=?async?()?=>?{}return?(<div><h1>react?錄音</h1><audio?src={audioUrl}?controls?/><button?onClick={startRecord}>開始</button><button>暫停</button><button>恢復</button><button?onClick={stopRecord}>停止</button></div>);
}
上面有 開始
,暫停
,恢復
以及 停止
四個功能,還加加了一個 <audio>
來查看錄音結果。

之后來實現 開始
與 停止
:
const?medisStream?=?useRef<MediaStream>();
const?recorder?=?useRef<MediaRecorder>();
const?mediaBlobs?=?useRef<Blob[]>([]);//?開始
const?startRecord?=?async?()?=>?{//?讀取輸入流medisStream.current?=?await?navigator.mediaDevices.getUserMedia({?audio:?true,?video:?false?});//?生成?MediaRecorder?對象recorder.current?=?new?MediaRecorder(medisStream.current);//?將?stream?轉成?blob?來存放recorder.current.ondataavailable?=?(blobEvent)?=>?{mediaBlobs.current.push(blobEvent.data);}//?停止時生成預覽的?blob?urlrecorder.current.onstop?=?()?=>?{const?blob?=?new?Blob(mediaBlobs.current,?{?type:?'audio/wav'?})const?mediaUrl?=?URL.createObjectURL(blob);setAudioUrl(mediaUrl);}recorder.current?.start();
}//?結束,不僅讓?MediaRecorder?停止,還要讓所有音軌停止
const?stopRecord?=?async?()?=>?{recorder.current?.stop()medisStream.current?.getTracks().forEach((track)?=>?track.stop());
}
從上面可以看到,首先從 getUserMedia
獲取輸入流 mediaStream
,以后還可以打開 video: true
來同步獲取視頻流。
然后將 mediaStream
傳給 mediaRecorder
,通過 ondataavailable
來存放當前流中的 blob
數據。
最后一步,調用 URL.createObjectURL
來生成預覽鏈接,這個 API 在前端非常有用,比如上傳圖片時也可以調用它來實現圖片預覽,而不需要真的傳到后端才展示預覽圖片。
在點擊 開始
后,就可以看到當前網頁正在錄音啦:

現在把剩下的 暫停
以及 恢復
也實現了:
const?pauseRecord?=?async?()?=>?{mediaRecorder.current?.pause();
}const?resumeRecord?=?async?()?=>?{mediaRecorder.current?.resume()
}
Hooks
在實現簡單功能之后,我們來嘗試一下把上面的功能都封裝成 React Hook,首先把這些邏輯都扔在一個函數中,然后返回 API:
const?useMediaRecorder?=?()?=>?{const?[mediaUrl,?setMediaUrl]?=?useState<string>('');const?mediaStream?=?useRef<MediaStream>();const?mediaRecorder?=?useRef<MediaRecorder>();const?mediaBlobs?=?useRef<Blob[]>([]);const?startRecord?=?async?()?=>?{mediaStream.current?=?await?navigator.mediaDevices.getUserMedia({?audio:?true,?video:?false?});mediaRecorder.current?=?new?MediaRecorder(mediaStream.current);mediaRecorder.current.ondataavailable?=?(blobEvent)?=>?{mediaBlobs.current.push(blobEvent.data);}mediaRecorder.current.onstop?=?()?=>?{const?blob?=?new?Blob(mediaBlobs.current,?{?type:?'audio/wav'?})const?url?=?URL.createObjectURL(blob);setMediaUrl(url);}mediaRecorder.current?.start();}const?pauseRecord?=?async?()?=>?{mediaRecorder.current?.pause();}const?resumeRecord?=?async?()?=>?{mediaRecorder.current?.resume()}const?stopRecord?=?async?()?=>?{mediaRecorder.current?.stop()mediaStream.current?.getTracks().forEach((track)?=>?track.stop());mediaBlobs.current?=?[];}return?{mediaUrl,startRecord,pauseRecord,resumeRecord,stopRecord,}
}
在 App.tsx
里拿到返回值就可以了:
const?App?=?()?=>?{const?{?mediaUrl,?startRecord,?resumeRecord,?pauseRecord,?stopRecord?}?=?useMediaRecorder();return?(<div><h1>react?錄音</h1><audio?src={mediaUrl}?controls?/><button?onClick={startRecord}>開始</button><button?onClick={pauseRecord}>暫停</button><button?onClick={resumeRecord}>恢復</button><button?onClick={stopRecord}>停止</button></div>);
}
封裝好之后,現在就可以在這個 Hook 里添加更多的功能了。
清除數據
在生成 blob url 的時候我們調用了 URL.createObjectURL
API 來實現,生成后的 url 長這樣:
blob:http://localhost:3000/e571f5b7-13bd-4c93-bc53-0c84049deb0a
每次 URL.createObjectURL
后都會生成一個 url -> blob
的引用,這樣的引用也是會占用資源內存的,所以我們可以提供一個方法來銷毀這個引用。
const?useMediaRecorder?=?()?=>?{const?[mediaUrl,?setMediaUrl]?=?useState<string>('');...return?{...clearBlobUrl:?()?=>?{if?(mediaUrl)?{URL.revokeObjectURL(mediaUrl);}setMediaUrl('');}}
}
錄屏
上面錄音和錄像使用 getUserMedia
來實現,而 錄屏則需要調用 getDisplayMedia
這個接口來實現。
為了能更好地區分這兩種情況,可以給開發者提供 audio
, video
以及 screen
三個參數,告訴我們應該調哪個接口去獲取對應的輸入流數據:
const?useMediaRecorder?=?(params:?Params)?=>?{const?{audio?=?true,video?=?false,screen?=?false,askPermissionOnMount?=?false,}?=?params;const?[mediaUrl,?setMediaUrl]?=?useState<string>('');const?mediaStream?=?useRef<MediaStream>();const?audioStream?=?useRef<MediaStream>();const?mediaRecorder?=?useRef<MediaRecorder>();const?mediaBlobs?=?useRef<Blob[]>([]);const?getMediaStream?=?useCallback(async?()?=>?{if?(screen)?{//?錄屏接口mediaStream.current?=?await?navigator.mediaDevices.getDisplayMedia({?video:?true?});mediaStream.current?.getTracks()[0].addEventListener('ended',?()?=>?{stopRecord()})if?(audio)?{//?添加音頻輸入流audioStream.current?=?await?navigator.mediaDevices.getUserMedia({?audio:?true?})audioStream.current?.getAudioTracks().forEach(audioTrack?=>?mediaStream.current?.addTrack(audioTrack));}}?else?{//?普通的錄像、錄音流mediaStream.current?=?await?navigator.mediaDevices.getUserMedia(({?video,?audio?}))}},?[screen,?video,?audio])//?開始錄const?startRecord?=?async?()?=>?{//?獲取流await?getMediaStream();mediaRecorder.current?=?new?MediaRecorder(mediaStream.current!);mediaRecorder.current.ondataavailable?=?(blobEvent)?=>?{mediaBlobs.current.push(blobEvent.data);}mediaRecorder.current.onstop?=?()?=>?{const?[chunk]?=?mediaBlobs.current;const?blobProperty:?BlobPropertyBag?=?Object.assign({?type:?chunk.type?},video???{?type:?'video/mp4'?}?:?{?type:?'audio/wav'?});const?blob?=?new?Blob(mediaBlobs.current,?blobProperty)const?url?=?URL.createObjectURL(blob);setMediaUrl(url);onStop(url,?mediaBlobs.current);}mediaRecorder.current?.start();}...
}
由于我們已經允許用戶來錄視頻以及聲音,所以在生成 URL 時,也要設置對應的 blobProperty
來生成對應媒體類型的 blobUrl
。
最后在調用 hook 時傳入 screen: true
,可以開啟錄屏功能:

注意:無論是錄像、錄音、錄屏都是要調用系統的能力,而網頁只是問瀏覽器要這個能力,但這樣的前提是瀏覽器已經擁有了系統權限了,所以必須在系統設置里允許瀏覽器有這些權限才能錄屏。

上面把獲取媒體流的邏輯都扔在 getMediaStream
函數里的做法,能很方便地用它來獲取用戶權限,假如我們想在剛加載這個組件時就獲取用戶攝像頭、麥克風、錄屏權限,就可以在 useEffect
里調用它:
useEffect(()?=>?{if?(askPermissionOnMount)?{getMediaStream().then();}
},?[audio,?screen,?video,?getMediaStream,?askPermissionOnMount])
預覽
錄像只需要在 getUserMedia
的時候設置 { video: true }
就可以實現錄像了。為了能更方便用戶在使用時能邊錄邊看效果,我們可以把視頻流也返回給用戶:
return?{...getMediaStream:?()?=>?mediaStream.current,getAudioStream:?()?=>?audioStream.current}
用戶在拿到這些 mediaStream
之后就可以直接賦值到 srcObject
上來進行預覽了:
<button?onClick={()?=>?previewVideo.current!.srcObject?=?getMediaStream()?||?null}>預覽
</button>

禁音
最后,我們來實現禁音功能,原理也同樣簡單。拿到 audioStream
里面的 audioTrack
,再將它們設置 enabled = false
就可以了。
const?toggleMute?=?(isMute:?boolean)?=>?{mediaStream.current?.getAudioTracks().forEach(track?=>?track.enabled?=?!isMute);audioStream.current?.getAudioTracks().forEach(track?=>?track.enabled?=?!isMute)setIsMuted(isMute);
}
使用時可以用它來禁用和開啟聲道:
<button?onClick={()?=>?toggleMute(!isMuted)}>{isMuted???'打開聲音'?:?'禁音'}</button>
總結
上面用 WebRTC 的 API 簡單地實現了一個錄音、錄像、錄屏工具 Hook,這里稍微做下總結吧:
getUserMedia
可用于獲取麥克風以及攝像頭的流getDisplayMedia
則用于獲取屏幕的視頻、音頻流錄東西的本質是
stream -> blobList -> blob url
,其中MediaRecorder
可監聽stream
從而獲取blob
數據MediaRecorder
還提供了開始、結束、暫停、恢復等多個與 Record 相關的接口createObjectURL
與revokeObjectURL
是反義詞,一個是創建引用,另一個是銷毀禁音可通過
track.enabled = false
關閉音軌來實現
這個小工具庫的實現就給大家帶到這里了,詳情可以查看 react-media-recorder[3] 這個庫的源碼,非常簡潔易懂,很適合入門看源碼的同學!
如果你也喜歡我的文章,可以點一波關注,或者一鍵三連再走,比心 ??
參考資料
[1]
react-media-recorder: https://github.com/0x006F/react-media-recorder
[2]項目代碼: https://github.com/haixiangyan/react-media-recorder
[3]react-media-recorder: https://github.com/0x006F/react-media-recorder
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助3000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。分享、收藏、點贊、在看我的文章就是對我最大的支持~