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

大家好,我是若川。持续组织了6个月源码共读活动,感兴趣的可以点此加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列

前言

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

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

需求与思路

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

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

919e85ad22a616a66f26666d8c3523bb.png

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

3a3b71f1b9f1d83ffc7fff1b52a8c820.png

基础功能

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

这里先把基础的 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> 来查看录音结果。

d73f0bf97dd887bf1f4ff6046488d9fe.png

之后来实现 开始停止

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 urlrecorder.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 在前端非常有用,比如上传图片时也可以调用它来实现图片预览,而不需要真的传到后端才展示预览图片。

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

e4ebc0f9dd8733c42062ded8db684f21.png

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

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-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,可以开启录屏功能:

6e93cac7a3448add85d270206e14844b.png

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

6263499a71899c982fedb9de833f7ef3.png

上面把获取媒体流的逻辑都扔在 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>
ba4200ffef0231b18558fcf6c6efaa69.png

禁音

最后,我们来实现禁音功能,原理也同样简单。拿到 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 相关的接口

  • 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

7e7b28308c7ff3e6b77ca84196854e17.gif

················· 若川简介 ·················

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》20余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结。
同时,最近组织了源码共读活动,帮助3000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。

7566719e670b5351c580129507dec08f.png

识别方二维码加我微信、拉你进源码共读

今日话题

略。分享、收藏、点赞、在看我的文章就是对我最大的支持~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/274847.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

文本字段和表单设计-UI组件系列

重点 (Top highlight)Forms have existed for a significant amount of time, greatly simplifying the task of drafting complaints and various other legal pleadings. With the advance of information and its processing, means to gather the data are also evolving. …

WCF 第四章 绑定 netMsmqBinding

MSMQ 为使用队列创建分布式应用程序提供支持。WCF支持将MSMQ队列作为netMsmqBinding绑定的底层传输协议的通信。 netMsmqBinding绑定允许客户端直接把消息提交到一个队列中同时服务端从队列中读取消息。客户端和服务端之间没有直接通信过程&#xff1b;因此&#xff0c;通信本 …

React 18 RC 版本发布啦,生产环境用起来!

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列今天给…

阿拉伯语排版设计_针对说阿拉伯语的用户的测试和设计

阿拉伯语排版设计Let me start off with some data to put things into perspective “Why?”让我从一些数据入手&#xff0c;以透视“为什么&#xff1f;”的观点。 Arabic is the 5th most spoken language worldwide, with 420 million speakers, and is an official lang…

SVN:“SVN”不是内部命令,解决方法

1、安装完TortoiseSVN-1.6.16.21511-x64-svn-1.6.17.msi 2、在运行窗口cmd---svn&#xff0c;提示&#xff1a; “SVN” 不是内部命令 郁闷&#xff0c;小有纠结 解决方法&#xff1a;安装Slik-Subversion-1.6.17-x64.msi 命令行窗口关闭&#xff0c;再次打开命令行窗口&#x…

7个月,4000+人,500+源码笔记,诚邀你参加源码共读~

大家好&#xff0c;我是若川。按照从易到难的顺序&#xff0c;前面几期&#xff08;比如&#xff1a;validate-npm-package-name、axios工具函数&#xff09;很多都只需要花2-3小时就能看完&#xff0c;并写好笔记。但收获确实很大。开阔视野、查漏补缺、升职加薪。已经有400笔…

火焰和烟雾的训练图像数据集_游戏开发者是烟雾和镜子的大师

火焰和烟雾的训练图像数据集Video games are incredible. They transport us to new worlds, allow us to partake in otherwise impossible situations, and empower us in our every day lives. Games can make us feel like a part of something bigger than ourselves, per…

平衡树SPLAY

一个比线段树代码还要又臭又长的数据结构&#xff0c;各式各样的函数&#xff0c;咱也不知道别人怎么记住的&#xff0c;咱也不敢问 SPLAY的性质 1.某个节点的左子树全部小于此节点&#xff0c;右子树全部大于此节点 2.中序遍历splay输出的序列是按从小到大的顺序 &#xff08;…

为支持两个语言版本,我基于谷歌翻译API写了一款自动翻译的 webpack 插件

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列本文来…

全球 化 化_全球化设计

全球 化 化重点 (Top highlight)Designing for a global audience can feel daunting. Do you localize your product? Or, do you internationalize your product? And what does that even entail?为全球观众设计可能会令人生畏。 您是否将产品本地化&#xff1f; 还是您将…

springMVC_数据的处理过程

1、DispatcherServlet&#xff1a;作为前端控制器&#xff0c;负责分发客户的请求到 Controller 其在web.xml中的配置如下&#xff1a; <servlet><servlet-name>dispatcherServlert</servlet-name><servlet-class>org.springframework.web.servlet.Dis…

JavaScript 新增两个原始数据类型

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列JavaS…

axure低保真原型_如何在Google表格中创建低保真原型

axure低保真原型Google Sheets is a spreadsheet, just like Microsoft Excel.Google表格是一个电子表格&#xff0c;就像Microsoft Excel一样。 Most people associate it with calculating numbers. But Google Sheets is actually great for organizing your ideas, making…

Lerna 运行流程剖析

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列Lerna…

手动创建线程池 效果会更好_创建更好的,可访问的焦点效果

手动创建线程池 效果会更好Most browsers has their own default, outline style for the :focus psuedo-class.大多数浏览器对于&#xff1a;focus psuedo-class具有其默认的轮廓样式。 Chrome’s default outline styleChrome浏览器的默认轮廓样式 This outline style is cr…

eazy ui 复选框单选_UI备忘单:单选按钮,复选框和其他选择器

eazy ui 复选框单选重点 (Top highlight)Pick me! Pick me! No, pick me! In today’s cheat sheet we will be looking at selectors and how they differ. Unlike most of my other cheat sheets, this will focus on two components (radio buttons and checkboxes) side by…

VS2010 VC Project的default Include设置

http://blog.csdn.net/jeffchen/article/details/5491435 VS2010与以往的版本一个最大的不同是&#xff1a;VC Directory设置的位置和以前的版本不一样。VS2010之前&#xff0c;VC Directory的设置都是在IDE的Tools->Options中设置的&#xff1b;VS2010改为&#xff0c;分别…

初级中级高级_初级职位,(半)高级职位

初级中级高级As a recent hire at my new job, as expected, a lot of things seemed scary and overwhelming. The scariest part was not the unfamiliarity with certain tasks or certain tools, but in communicating with higher-level coworkers, managers and bosses. …

如何写好技术文章(看张鑫旭老师的直播总结

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列本文是…

iOS 流媒体 基本使用 和方法注意

项目里面需要添加视频方法 我自定义 选用的是 avplayer 没选择 MediaPlayer 原因很简单 , avplayer 会更容易扩展 有篇博客 也很好地说明了 使用avplayer的优越性 blog.csdn.net/think12/article/details/8549438在iOS開發上&#xff0c;如果遇到需要播放影片&#xff0c;…