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

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

image.png


前言




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


最近项目遇到一个要在网页上录音的需求,在一波搜索后,发现了 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 关闭音轨来实现


这个小工具库的实现就给大家带到这里了,详情可以查看 react-media-recorder 这个库的源码,非常简洁易懂,很适合入门看源码的同学!



相关文章
|
8月前
|
JavaScript 前端开发 Java
通义灵码 Rules 库合集来了,覆盖Java、TypeScript、Python、Go、JavaScript 等
通义灵码新上的外挂 Project Rules 获得了开发者的一致好评:最小成本适配我的开发风格、相当把团队经验沉淀下来,是个很好功能……
1434 103
|
6月前
|
存储 自然语言处理 前端开发
抖音快手小红书虚拟评论截图生成器,模拟对话制作工具,html+js+css
这是一款纯前端实现的多平台虚拟评论生成器,支持抖音、快手、小红书风格,适用于产品演示与UI设计。采用Vanilla JS与Flexbox布局,利用IndexedDB存储数据,CSS Variables切换主题。
|
5月前
|
JavaScript 前端开发 API
淘宝店铺阿里旺旺采集工具,连接批量转旺旺ID,用Js接口实现效果
以下是淘宝店铺阿里旺旺采集工具的JS实现代码,包含批量获取店铺旺旺ID功能,当然仅
|
4月前
|
编解码 JavaScript 前端开发
如何在网页播放英文的m3u8文件(基于Javascript搭建的在线网页工具)
什么是m3u8?又该如何在网页中高效、便捷地播放英文的m3u8文件呢?今天这篇文章就带你一起了解,并推荐一种基于Javascript搭建的在线网页工具,让你轻松解决播放问题。
902 0
|
8月前
|
JavaScript 前端开发 API
|
6月前
|
存储 前端开发 安全
病历单生成器在线制作,病历单生成器app,HTML+CSS+JS恶搞工具
本项目为医疗病历模拟生成器,旨在为医学教学和软件开发测试提供数据支持,严格遵守《医疗机构病历管理规定》。
|
6月前
|
存储 前端开发 JavaScript
仿真银行app下载安装, 银行卡虚拟余额制作app,用html+css+js实现逼真娱乐工具
这是一个简单的银行账户模拟器项目,用于学习前端开发基础。用户可进行存款、取款操作,所有数据存储于浏览器内存中
|
7月前
|
资源调度 JavaScript 前端开发
Day.js极简轻易快速2kB的JavaScript库-替代Moment.js
dayjs是一个极简快速2kB的JavaScript库,可以为浏览器处理解析、验证、操作和显示日期和时间,它的设计目标是提供一个简单、快速且功能强大的日期处理工具,同时保持极小的体积(仅 2KB 左右)。
408 24
|
7月前
|
人工智能 监控 前端开发
基于 Next.js 的书法字体生成工具架构设计与 SSR 优化实践
本项目是一款书法字体生成工具,采用 Next.js 14(App Router)与 Tailwind CSS 构建前端,阿里云 Serverless 部署后端。通过混合渲染策略(SSG/SSR/CSR)、Web Worker 异步计算及 CDN 字体分片加载优化性能。服务端借助阿里云函数计算处理计算密集型任务,将平均耗时从 1200ms 降至 280ms,支持 1000+ QPS。动态路由与 ARMS 监控提升工程化水平,未来计划引入 WebGPU 和 AI 字体风格迁移技术,进一步优化用户体验。