这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

前言

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

完整项目代码放在 Github

需求与思路

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

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

只需要记住:把输入 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()
}

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

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

上面把获取媒体流的逻辑都扔在 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 相关的接口
  • createObjectURLrevokeObjectURL 是反义词,一个是创建引用,另一个是销毁
  • 禁音可通过 track.enabled = false 关闭音轨来实现

本文转载于:

https://juejin.cn/post/7071101341396893732

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

记录--用JS轻松实现一个录音、录像、录屏的工具库的更多相关文章

  1. Tippy.js – 轻量的 Javascript Tooltip 工具库

    工具提示(Tooltip)在网站中的一个小功能,但却有很重要的作用,常用于显示一些温馨的提示信息.如果网站中的工具提示功能做得非常有创意的话能够加深用户对网站印象.Tippy.js 是一款帮助你快速创 ...

  2. Restive.js – 轻松让网站变成响应式和自适应

    Restive.js 是一个 jQuery 插件,可以帮助您轻松快捷地添加响应式功能到你网站,适应几乎所有拥有 Web 功能的设备.使用设备检测,高级管理断点,以及方向管理的组合,Restive.js ...

  3. Nunchuck.js - 轻松实现多个设备的数据同步

    Nunchuck.js 是对用于移动设备上的浏览器应用程序的控制库,通过浏览器轻松实现多设备数据同步.他们提供了一个库,很容易使开发人员能够整合移动浏览器控件到桌面的基于浏览器的 JavaScript ...

  4. store.js - 轻松实现本地存储(LocalStorage)

    store.js 是一个兼容所有浏览器的 LocalStorage 包装器,不需要借助 Cookie 或者 Flash.store.js 会根据浏览器自动选择使用 localStorage.globa ...

  5. Nightwatch.js – 轻松实现浏览器的自动测试

    Nightwatch.js 是一个易于使用的,基于 Node.js 平台的浏览器自动化测试解决方案.它使用强大的 Selenium WebDriver API 来在 DOM 元素上执行命令和断言. 语 ...

  6. 记录一些js框架用途

    accounting.min.js 货币格式化alertify.min.js 提示信息库amd.loader.js 按需动态加载js文件angular-cookies.js 处理cookieangul ...

  7. (Demo分享)利用JavaScript(JS)实现一个九宫格拖拽功能

    利用JavaScript(JS)实现一个九宫格拖拽功能   Demo实现了对任意方格进行拖拽,可以交换位置,其中Demo-1利用了勾股定理判断距离! Demo-1整体思路: 1.首先div实现自由移动 ...

  8. C#开源录音组件、录像组件、录屏组件及demo源码

    在多媒体系统中,一般都会涉及到录音.录像.录屏问题,采集得到的数据可以用来传输.播放.或存储.所以,对于像课件录制系统.语音视频录制系统.录屏系统等,多媒体数据的采集就是最基础的功能之一. MCapt ...

  9. 用原生js来写一个swiper滑块插件

        是不是有点印象了,没错,他的最基本的用法就是左右滑动,插件使用者只需要写几行简单的html和js即可实现一个简单滑动效果,不过你完全可以组合各种元素来适应不同的场景. 当然插件我已经写好了,咱 ...

  10. 学以致用:手把手教你撸一个工具库并打包发布,顺便解决JS浮点数计算精度问题

    本文讲解的是怎么实现一个工具库并打包发布到npm给大家使用.本文实现的工具是一个分数计算器,大家考虑如下情况: \[ \sqrt{(((\frac{1}{3}+3.5)*\frac{2}{9}-\fr ...

随机推荐

  1. ABC 333

    ABCDE 赛时 AC. F 列方程:\(f_{i,j}\) 表示有 \(i\) 个人,第 \(j\) 个人最终活下来的概率. \(f_{i,1}=\dfrac{1}{2}f_{i,i}\),因为只有 ...

  2. 【OpenGL ES】Blinn改进的冯氏光照模型

    1 前言 ​ 光照元素主要有环境光(ambient).漫反射光(diffuse).镜面反射光(specular),光照模型主要有冯氏模型和 Blinn 改进的冯氏模型,两者区别在与镜面反射光的计算,冯 ...

  3. od命令

    od命令 od命令会读取所指定的文件的内容,并将其内容以八进制字节码呈现出来. 语法 od [OPTION]... [FILE]... od [-abcdfilosx]... [FILE] [[+]O ...

  4. C++ 多线程的错误和如何避免(13)

    在 async 任务中抛出的异常会被 std::future::get() 触发 #include <future> #include <iostream> int main( ...

  5. 2021-07-20 value!==value,JavaScript中NaN

    关于NaN NaN 属性代表一个"不是数字"的number类型的字面量值.这个特殊的值是因为运算不能执行而导致的,不能执行的原因要么是因为其中的运算对象之一非数字. NaN的出现场 ...

  6. sql题目---day39

    # 1.查询所有的课程的名称以及对应的任课老师姓名 #where select teacher.tname,course.cname from teacher,course where course. ...

  7. 有了这份Java面试中的葵花宝典,让你面试起飞!!!

    HashMap面试题 HashMap与HashTable的区别 1.HashMap线程不安全 HashTable 线程是安全的采用synchronized 2.HashMap允许存放key 为null ...

  8. Golang 常用工具记录

    Golang 常用工具记录 1 golang 类 1.1 日常使用的 copier 复制结构体到另外一个结构体 等等类似的功能 asynq Go中简单.可靠.高效的分布式任务队列,使用 redis 做 ...

  9. 【Azure Developer】Python 获取 Azure 中订阅(subscription)信息,包含ID, Name等

    问题描述 在Azure AD中注册一个Applicaiton后,对其进行授权,能够查看所有订阅的ReadOnly权限,然后,是否可以同通过Python代码,在完成Authorization后(使用Cl ...

  10. Langchain 介绍与入门

    官方介绍 LangChain 是一个利用LLM开发应用程序的框架.它让应用程序具备: 上下文感知能力:将LLM连接到上下文源(提示说明.少量示例.用以形成其响应的内容等) 推理:依靠LLM进行推理(例 ...