赞
踩
哈喽,大家好,我是海怪。
最近项目遇到一个要在网页上录音的需求,在一波搜索后,发现了 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()
- }
在实现简单功能之后,我们来尝试一下把上面的功能都封装成 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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。