這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 最近項目遇到一個要在網頁上錄音的需求,在一波搜索後,發現了 react-media-recorder 這個庫。今天就跟大家一起研究一下這個庫的源碼吧,從 0 到 1 來實現一個 React 的錄音、錄像和錄屏的功能。 完整項目代碼放 ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
前言
最近項目遇到一個要在網頁上錄音的需求,在一波搜索後,發現了 react-media-recorder 這個庫。今天就跟大家一起研究一下這個庫的源碼吧,從 0 到 1 來實現一個 React 的錄音、錄像和錄屏的功能。
完整項目代碼放在 Github
需求與思路
首先要明確我們要完成的事:錄音,錄像,錄屏。
這種錄製媒體流的原理其實很簡單。
只需要記住:把輸入 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 url recorder.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
關閉音軌來實現