前言大家好,我是海怪。最近项目遇到需要在网页上记录。找了一波之后,找到了库react-media-recorder[1]。今天就和大家一起研究一下这个库的源码,从0到1实现一个React的录音、录像、录屏功能。完整的项目代码放在Github[2]上。需求与思路首先要明确我们要实现的功能:录音、录像、录屏。这个录制媒体流的原理其实很简单。只要记住:将输入流存储在blobList中,最后将其转换为预览blobUrl。基本功能有了上面简单的思路,我们可以先做一个简单的录音录像功能。这里先实现基本的HTML结构:constApp=()=>{const[audioUrl,setAudioUrl]=useState('');conststartRecord=async()=>{}conststopRecord=async()=>{}return(reactrecording
StartStop );}start有四个函数,暂停、恢复和停止,添加一个可以查看录制结果。然后启动和停止:constmedisStream=useRef();constrecorder=useRef();constmediaBlobs=useRef([]);//开始conststartRecord=async()=>{//读取输入流medisStream.current=awaitnavigator.mediaDevices.getUserMedia({audio:true,video:false});//生成MediaRecorder对象recorder.current=newMediaRecorder(medisStream.current);//将流转换为blob以存储recorder.current.ondataavailable=(blobEvent)=>{mediaBlobs.current.push(blobEvent.data);}//停止时生成预览bloburlrecorder.current.onstop=()=>{constblob=newBlob(mediaBlobs.current,{type:'audio/wav'})constmediaUrl=URL.createObjectURL(blob);setAudioUrl(mediaUrl);}recorder.current?.start();}//end,不仅要停止MediaRecorder,还要停止所有tracksconststopRecord=async()=>{recorder.current?.stop()medisStream.current?.getTracks().forEach((track)=>track.stop());}从上面可以看出,首先从getUserMedia获取输入流mediaStream,然后也可以开启video:true同步获取视频流然后将mediaStream传给mediaRecorder,使用ondataavailable将blob数据存入当前流。最后一步是调用URL.createObjectURL生成预览链接。这个API在前端非常有用。比如可以在上传图片的时候调用实现图片预览,而不是实际上传到后台显示预览图。点击开始后,可以看到当前网页正在录制:现在剩下的暂停和恢复也实现了:constpauseRecord=async()=>{mediaRecorder.current?.pause();}constresumeRecord=async()=>{mediaRecorder.current?.resume()}Hooks实现了简单的功能之后,我们来尝试将以上功能封装到ReactHooks中。首先,将所有这些逻辑都扔到一个函数中,然后返回给API:constuseMediaRecorder=()=>{const[mediaUrl,setMediaUrl]=useState('');constmediaStream=useRef();constmediaRecorder=useRef();constmediaBlobs=useRef([]);conststartRecord=async()=>{mediaStream.current=awaitnavigator.mediaDevices.getUserMedia({audio:true,video:false});mediaRecorder.current=newMediaRecorder(mediaStream.current);mediaRecorder.current.ondataavailable=(blobEvent)=>{mediaBlobs.current.push(blobEvent.data);}mediaRecorder.current.onstop=()=>{constblob=newBlob(mediaBlobs.current,{type:'audio/wav'})consturl=URL。创造teObjectURL(blob);setMediaUrl(网址);}mediaRecorder.current?.start();}constpauseRecord=async()=>{mediaRecorder.current?.pause();}constresumeRecord=async()=>{mediaRecorder.current?.resume()}conststopRecord=async()=>{mediaRecorder.current?.stop()mediaStream.current?.getTracks().forEach((track)=>track.stop());媒体斑点。当前=[];}return{mediaUrl,startRecord,pauseRecord,resumeRecord,stopRecord,}}只需在App.tsx中获取返回值:constApp=()=>{const{mediaUrl,startRecord,resumeRecord,pauseRecord,stopRecord}=useMediaRecorder();return();}打包后,你现在可以在这个Hook中添加更多的功能来清除数据。生成bloburl时,我们调用URL.createObjectURLAPI来实现,生成最终的url是这样的:blob:http://localhost:3000/e571f5b7-13bd-4c93-bc53-0c84049deb0a每次URL.createObjectURL都会生成一个url->blob引用,这样的引用也会占用资源内存,所以我们可以提供一个方法来销毁这个引用。constuseMediaRecorder=()=>{const[mediaUrl,setMediaUrl]=useState('');...return{...clearBlobUrl:()=>{if(mediaUrl){URL.revokeObjectURL(mediaUrl);}setMediaUrl('');}}}录屏和录屏是通过getUserMedia实现的,而录屏需要调用getDisplayMedia接口。为了更好的区分这两种情况,开发者可以提供音频、视频、屏幕三个参数来告诉我们应该调整哪个接口来获取对应的输入流数据:constuseMediaRecorder=(params:Params)=>{const{audio=true,video=false,screen=false,askPermissionOnMount=false,}=params;const[mediaUrl,setMediaUrl]=useState('');constmediaStream=useRef();constaudioStream=useRef();constmediaRecorder=useRef();constmediaBlobs=useRef([]);constgetMediaStream=useCallback(async()=>{if(screen){//屏幕录制接口mediaStream.current=awaitnavigator.mediaDevices.getDisplayMedia({video:true});mediaStream.current?.getTracks()[0].addEventListener('ended',()=>{stopRecord()})if(audio){//添加音频输入流audioStream.current=awaitnavigator.mediaDevices.getUserMedia({audio:true})audioStream.current?.getAudioTracks().forEach(audioTrack=>mediaStream.current?.addTrack(audioTrack));}}else{//普通视频和录制流mediaStream.current=awaitnavigator.mediaDevices.getUserMedia(({video,audio}))}},[screen,video,audio])//开始录制conststartRecord=async()=>{//获取流awaitgetMediaStream();mediaRecorder.current=newMediaRecorder(mediaStream.current!);mediaRecorder.current.ondataavailable=(blobEvent)=>{mediaBlobs.current.push(blobEvent.data);}mediaRecorder.current.onstop=()=>{const[chunk]=mediaBlobs.current;constblobProperty:BlobPropertyBag=Object.assign({type:chunk.type},video?{type:'video/mp4'}:{type:'audio/wav'});constblob=newBlob(mediaBlobs.current,blobProperty)consturl=URL.createObjectURL(blob);setMediaUrl(url);onStop(url,mediaBlobs.current);}mediaRecorder.current?.start();}...}因为我们已经允许用户录制视频和声音,所以我们正在生成URL时,还要设置对应的blobProperty,生成对应媒体类型的blobUrl。最后在调用钩子的时候,传入screen:true开启录屏功能:注:无论是录像、录音还是录屏,都是调用系统的能力,网页问浏览器就可以了对于这个能力,但是这个的前提是浏览器已经有系统权限,所以必须在系统设置中让浏览器有这些权限才能录屏。将获取媒体流的逻辑全部丢到上面的getMediaStream函数中的方法,可以非常方便的获取用户权限。如果我们想在这个组件刚刚加载的时候获取用户的摄像头、麦克风、录屏权限,可以在useEffect中调用:useEffect(()=>{if(askPermissionOnMount){getMediaStream().then();}},[audio,screen,video,getMediaStream,askPermissionOnMount])预览视频只需要设置getUserMedia{video:true}即可实现录像。为了方便用户边录制边看效果,我们还可以将视频流返回给用户:return{...getMediaStream:()=>mediaStream.current,getAudioStream:()=>audioStream。current}获取到这些mediaStreams后,用户可以直接赋值给srcObject进行预览:previewVideo.current!.srcObject=getMediaStream()||null}>Preview最后静音,我们来实现静音功能,原理也很简单。获取audioStream中的audioTrack,然后设置enabled=false。consttoggleMute=(isMute:boolean)=>{mediaStream.current?.getAudioTracks().forEach(track=>track.enabled=!isMute);audioStream.current?.getAudioTracks().forEach(track=>track.enabled=!isMute)setIsMuted(isMute);}使用它来禁用和启用声道:toggleMute(!isMute)}>{静音了?'打开声音':'静音'}总结以上使用WebRTCAPI简单实现了一个录音、录像、录屏工具的Hook。这里简单总结一下:getUserMedia可以用来获取麦克风和摄像头的流;getDisplayMedia用于获取屏幕的视频,音频流的本质是stream->blobList->bloburl,这里MediaRecorder可以监听stream获取blob数据。MediaRecorder还提供了start、end、pause、resume等多个Record相关的接口。createObjectURL和revokeObjectURL是反义词。一是创建引用,二是销毁静音。您可以通过track.enabled=false关闭音轨。源码非常简洁易懂,非常适合初学者阅读源码!参考[1]react-media-recorder:https://github.com/0x006F/react-media-recorder[2]项目代码:https://github.com/haixiangyan/react-media-recorder[3]react-媒体记录器:https://github.com/0x006F/react-media-recorder