用 JS 輕鬆實現一個錄音、錄像、錄屏工具庫
前言
哈嘍,大家好,我是海怪。
最近項目遇到一個要在網頁上錄音的需求,在一波搜索後,發現了 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 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關閉音軌來實現
這個小工具庫的實現就給大家帶到這裏了,詳情可以查看 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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/NSahiP_-sa0hDhR0r4YcVQ