当前位置:   article > 正文

用JS轻松实现一个录音、录像、录屏工具库

react-media-recorder

前言

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

最近项目遇到一个要在网页上录音的需求,在一波搜索后,发现了 react-media-recorder[1] 这个库。今天就跟大家一起研究一下这个库的源码吧,从 0 到 1 来实现一个 React 的录音、录像和录屏功能。

完整项目代码放在 Github[2]

需求与思路

首先要明确我们要完成的事:录音录像录屏

这种录制媒体流的原理其实很简单。

69addac35fb7004985392a0ffc038a91.png

只需要记住:把输入 stream 存放在 blobList,最后转成预览 blobUrl

82fb3279d1a768856828c0a05279156f.png

基础功能

有了上面的简单思路后,我们可以先做一个简单的录音与录像功能。

这里先把基础的 HTML 结构实现了:

  1. const App = () => {
  2.   const [audioUrl, setAudioUrl] = useState<string>('');
  3.   
  4.   const startRecord = async () => {}
  5.   const stopRecord = async () => {}
  6.   return (
  7.     <div>
  8.       <h1>react 录音</h1>
  9.       <audio src={audioUrl} controls />
  10.       <button onClick={startRecord}>开始</button>
  11.       <button>暂停</button>
  12.       <button>恢复</button>
  13.       <button onClick={stopRecord}>停止</button>
  14.     </div>
  15.   );
  16. }

上面有 开始暂停恢复 以及 停止 四个功能,还加加了一个 <audio> 来查看录音结果。

50abdc951b79799ab90bc67ba7927ed0.png

之后来实现 开始停止

  1. const medisStream = useRef<MediaStream>();
  2. const recorder = useRef<MediaRecorder>();
  3. const mediaBlobs = useRef<Blob[]>([]);
  4. // 开始
  5. const startRecord = async () => {
  6.   // 读取输入流
  7.   medisStream.current = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
  8.   // 生成 MediaRecorder 对象
  9.   recorder.current = new MediaRecorder(medisStream.current);
  10.   // 将 stream 转成 blob 来存放
  11.   recorder.current.ondataavailable = (blobEvent) => {
  12.     mediaBlobs.current.push(blobEvent.data);
  13.   }
  14.   // 停止时生成预览的 blob url
  15.   recorder.current.onstop = () => {
  16.     const blob = new Blob(mediaBlobs.current, { type'audio/wav' })
  17.     const mediaUrl = URL.createObjectURL(blob);
  18.     setAudioUrl(mediaUrl);
  19.   }
  20.   recorder.current?.start();
  21. }
  22. // 结束,不仅让 MediaRecorder 停止,还要让所有音轨停止
  23. const stopRecord = async () => {
  24.   recorder.current?.stop()
  25.   medisStream.current?.getTracks().forEach((track) => track.stop());
  26. }

从上面可以看到,首先从 getUserMedia 获取输入流 mediaStream,以后还可以打开 video: true 来同步获取视频流

然后将 mediaStream 传给 mediaRecorder,通过 ondataavailable 来存放当前流中的 blob 数据。

最后一步,调用 URL.createObjectURL 来生成预览链接,这个 API 在前端非常有用,比如上传图片时也可以调用它来实现图片预览,而不需要真的传到后端才展示预览图片。

在点击 开始 后,就可以看到当前网页正在录音啦:

d339d601cbaab156a2fbba240d97284f.png

现在把剩下的 暂停 以及 恢复 也实现了:

  1. const pauseRecord = async () => {
  2.   mediaRecorder.current?.pause();
  3. }
  4. const resumeRecord = async () => {
  5.   mediaRecorder.current?.resume()
  6. }

Hooks

在实现简单功能之后,我们来尝试一下把上面的功能都封装成 React Hook,首先把这些逻辑都扔在一个函数中,然后返回 API:

  1. const useMediaRecorder = () => {
  2.   const [mediaUrl, setMediaUrl] = useState<string>('');
  3.   const mediaStream = useRef<MediaStream>();
  4.   const mediaRecorder = useRef<MediaRecorder>();
  5.   const mediaBlobs = useRef<Blob[]>([]);
  6.   const startRecord = async () => {
  7.     mediaStream.current = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
  8.     mediaRecorder.current = new MediaRecorder(mediaStream.current);
  9.     mediaRecorder.current.ondataavailable = (blobEvent) => {
  10.       mediaBlobs.current.push(blobEvent.data);
  11.     }
  12.     mediaRecorder.current.onstop = () => {
  13.       const blob = new Blob(mediaBlobs.current, { type'audio/wav' })
  14.       const url = URL.createObjectURL(blob);
  15.       setMediaUrl(url);
  16.     }
  17.     mediaRecorder.current?.start();
  18.   }
  19.   const pauseRecord = async () => {
  20.     mediaRecorder.current?.pause();
  21.   }
  22.   const resumeRecord = async () => {
  23.     mediaRecorder.current?.resume()
  24.   }
  25.   const stopRecord = async () => {
  26.     mediaRecorder.current?.stop()
  27.     mediaStream.current?.getTracks().forEach((track) => track.stop());
  28.     mediaBlobs.current = [];
  29.   }
  30.   return {
  31.     mediaUrl,
  32.     startRecord,
  33.     pauseRecord,
  34.     resumeRecord,
  35.     stopRecord,
  36.   }
  37. }

App.tsx 里拿到返回值就可以了:

  1. const App = () => {
  2.   const { mediaUrl, startRecord, resumeRecord, pauseRecord, stopRecord } = useMediaRecorder();
  3.   return (
  4.     <div>
  5.       <h1>react 录音</h1>
  6.       <audio src={mediaUrl} controls />
  7.       <button onClick={startRecord}>开始</button>
  8.       <button onClick={pauseRecord}>暂停</button>
  9.       <button onClick={resumeRecord}>恢复</button>
  10.       <button onClick={stopRecord}>停止</button>
  11.     </div>
  12.   );
  13. }

封装好之后,现在就可以在这个 Hook 里添加更多的功能了。

清除数据

在生成 blob url 的时候我们调用了 URL.createObjectURL API 来实现,生成后的 url 长这样:

blob:http://localhost:3000/e571f5b7-13bd-4c93-bc53-0c84049deb0a

每次 URL.createObjectURL 后都会生成一个 url -> blob 的引用,这样的引用也是会占用资源内存的,所以我们可以提供一个方法来销毁这个引用。

  1. const useMediaRecorder = () => {
  2.   const [mediaUrl, setMediaUrl] = useState<string>('');
  3.   
  4.   ...
  5.   return {
  6.     ...
  7.     clearBlobUrl: () => {
  8.       if (mediaUrl) {
  9.         URL.revokeObjectURL(mediaUrl);
  10.       }
  11.       setMediaUrl('');
  12.     }
  13.   }
  14. }

录屏

上面录音和录像使用 getUserMedia 来实现,而 录屏则需要调用 getDisplayMedia 这个接口来实现。

为了能更好地区分这两种情况,可以给开发者提供 audio, video 以及 screen 三个参数,告诉我们应该调哪个接口去获取对应的输入流数据:

  1. const useMediaRecorder = (params: Params) => {
  2.   const {
  3.     audio = true,
  4.     video = false,
  5.     screen = false,
  6.     askPermissionOnMount = false,
  7.   } = params;
  8.   const [mediaUrl, setMediaUrl] = useState<string>('');
  9.   const mediaStream = useRef<MediaStream>();
  10.   const audioStream = useRef<MediaStream>();
  11.   const mediaRecorder = useRef<MediaRecorder>();
  12.   const mediaBlobs = useRef<Blob[]>([]);
  13.   const getMediaStream = useCallback(async () => {
  14.     if (screen) {
  15.       // 录屏接口
  16.       mediaStream.current = await navigator.mediaDevices.getDisplayMedia({ video: true });
  17.       mediaStream.current?.getTracks()[0].addEventListener('ended', () => {
  18.         stopRecord()
  19.       })
  20.       if (audio) {
  21.         // 添加音频输入流
  22.         audioStream.current = await navigator.mediaDevices.getUserMedia({ audio: true })
  23.         audioStream.current?.getAudioTracks().forEach(audioTrack => mediaStream.current?.addTrack(audioTrack));
  24.       }
  25.     } else {
  26.       // 普通的录像、录音流
  27.       mediaStream.current = await navigator.mediaDevices.getUserMedia(({ video, audio }))
  28.     }
  29.   }, [screen, video, audio])
  30.   
  31.   // 开始录
  32.   const startRecord = async () => {
  33.     // 获取流
  34.     await getMediaStream();
  35.     mediaRecorder.current = new MediaRecorder(mediaStream.current!);
  36.     mediaRecorder.current.ondataavailable = (blobEvent) => {
  37.       mediaBlobs.current.push(blobEvent.data);
  38.     }
  39.     mediaRecorder.current.onstop = () => {
  40.       const [chunk] = mediaBlobs.current;
  41.       const blobProperty: BlobPropertyBag = Object.assign(
  42.         { type: chunk.type },
  43.         video ? { type'video/mp4' } : { type'audio/wav' }
  44.       );
  45.       const blob = new Blob(mediaBlobs.current, blobProperty)
  46.       const url = URL.createObjectURL(blob);
  47.       setMediaUrl(url);
  48.       onStop(url, mediaBlobs.current);
  49.     }
  50.     mediaRecorder.current?.start();
  51.   }
  52.   
  53.   ...
  54. }

由于我们已经允许用户来录视频以及声音,所以在生成 URL 时,也要设置对应的 blobProperty 来生成对应媒体类型的 blobUrl

最后在调用 hook 时传入 screen: true,可以开启录屏功能:

e71296e5fbde7802a173af858feb67d0.png

注意:无论是录像、录音、录屏都是要调用系统的能力,而网页只是问浏览器要这个能力,但这样的前提是浏览器已经拥有了系统权限了,所以必须在系统设置里允许浏览器有这些权限才能录屏。

9e81659871adaac3c47335aa9946b30d.png

上面把获取媒体流的逻辑都扔在 getMediaStream 函数里的做法,能很方便地用它来获取用户权限,假如我们想在刚加载这个组件时就获取用户摄像头、麦克风、录屏权限,就可以在 useEffect 里调用它

  1. useEffect(() => {
  2.   if (askPermissionOnMount) {
  3.     getMediaStream().then();
  4.   }
  5. }, [audio, screen, video, getMediaStream, askPermissionOnMount])

预览

录像只需要在 getUserMedia 的时候设置 { video: true } 就可以实现录像了。为了能更方便用户在使用时能边录边看效果,我们可以把视频流也返回给用户:

  1. return {
  2.     ...
  3.     getMediaStream: () => mediaStream.current,
  4.     getAudioStream: () => audioStream.current
  5.   }

用户在拿到这些 mediaStream 之后就可以直接赋值到 srcObject 上来进行预览了:

  1. <button onClick={() => previewVideo.current!.srcObject = getMediaStream() || null}>
  2.     预览
  3. </button>
5af9dd6a900af8adf5150b25e1fced08.png

禁音

最后,我们来实现禁音功能,原理也同样简单。拿到 audioStream 里面的 audioTrack,再将它们设置 enabled = false 就可以了。

  1. const toggleMute = (isMute: boolean) => {
  2.   mediaStream.current?.getAudioTracks().forEach(track => track.enabled = !isMute);
  3.   audioStream.current?.getAudioTracks().forEach(track => track.enabled = !isMute)
  4.   setIsMuted(isMute);
  5. }

使用时可以用它来禁用和开启声道:

<button onClick={() => toggleMute(!isMuted)}>{isMuted ? '打开声音' : '禁音'}</button>

总结

上面用 WebRTC 的 API 简单地实现了一个录音、录像、录屏工具 Hook,这里稍微做下总结吧:

  • getUserMedia 可用于获取麦克风以及摄像头的流

  • getDisplayMedia 则用于获取屏幕的视频、音频流

  • 录东西的本质是 stream -> blobList -> blob url,其中 MediaRecorder 可监听 stream 从而获取 blob 数据

  • MediaRecorder 还提供了开始、结束、暂停、恢复等多个与 Record 相关的接口

  • createObjectURLrevokeObjectURL 是反义词,一个是创建引用,另一个是销毁

  • 禁音可通过 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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/571434
推荐阅读
相关标签
  

闽ICP备14008679号