好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

react-media-recorder轻松实现一个录音、录像、录屏工具库

前言

哈喽,大家好,我是海怪。

最近项目遇到一个要在网页上录音的需求,在一波搜索后,发现了 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轻松实现一个录音、录像、录屏工具库的详细内容...

  阅读:40次