前言
哈喽,大家好,我是海怪。
最近项目遇到一个要在网页上录音的需求,在一波搜索后,发现了 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 > ) ; }
上面有 开始,暂停,恢复 以及 停止 四个功能,还加加了一个 来查看录音结果。
之后来实现 开始 与 停止:
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 - 13 bd - 4 c93 - bc53 - 0 c84049deb0a
每次 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
原文地址:https://mp.weixin.qq.com/s/NSahiP_-sa0hDhR0r4YcVQ
dy("nrwz");
查看更多关于react-media-recorder轻松实现一个录音、录像、录屏工具库的详细内容...