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

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

前言

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

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

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要用JavaCV结合Swing实现一个专业的录屏软件,需要实现以下功能: 1. 屏幕捕获 2. 视频编码 3. 录制控制(开始、暂停、停止) 4. 视频预览 5. 视频保存 以下是一个基本的实现示例: ```java import org.bytedeco.ffmpeg.global.avcodec; import org.bytedeco.ffmpeg.global.avutil; import org.bytedeco.javacv.*; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; public class ScreenRecorder extends JFrame implements ActionListener { private static final long serialVersionUID = 1L; private JButton startButton, pauseButton, stopButton; private JLabel statusLabel, previewLabel; private Timer timer; private FFmpegFrameRecorder recorder; private Java2DFrameConverter converter; private Robot robot; private Rectangle screenRect; private boolean isRecording = false; private long startTime = 0; private long pauseTime = 0; private int fps = 30; private String outputDir = "output"; private String outputFormat = "mp4"; public ScreenRecorder() throws Exception { super("Screen Recorder"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(400, 300); setLocationRelativeTo(null); setLayout(new BorderLayout()); JPanel controlPanel = new JPanel(); controlPanel.setLayout(new FlowLayout()); startButton = new JButton("Start"); startButton.addActionListener(this); controlPanel.add(startButton); pauseButton = new JButton("Pause"); pauseButton.addActionListener(this); controlPanel.add(pauseButton); stopButton = new JButton("Stop"); stopButton.addActionListener(this); controlPanel.add(stopButton); statusLabel = new JLabel("Ready", JLabel.CENTER); add(statusLabel, BorderLayout.NORTH); add(controlPanel, BorderLayout.SOUTH); previewLabel = new JLabel(); add(previewLabel, BorderLayout.CENTER); converter = new Java2DFrameConverter(); robot = new Robot(); screenRect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()); timer = new Timer(1000 / fps, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (isRecording) { try { BufferedImage image = robot.createScreenCapture(screenRect); Frame frame = converter.convert(image); recorder.record(frame); previewLabel.setIcon(new ImageIcon(image.getScaledInstance(320, 180, Image.SCALE_SMOOTH))); } catch (Exception ex) { ex.printStackTrace(); } } } }); setVisible(true); } private void startRecording() { try { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); String filename = outputDir + File.separator + sdf.format(new Date()) + "." + outputFormat; recorder = new FFmpegFrameRecorder(filename, screenRect.width, screenRect.height); recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); recorder.setFormat(outputFormat); recorder.setFrameRate(fps); recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); recorder.start(); isRecording = true; startTime = System.currentTimeMillis(); timer.start(); statusLabel.setText("Recording..."); startButton.setEnabled(false); pauseButton.setEnabled(true); stopButton.setEnabled(true); } catch (Exception ex) { ex.printStackTrace(); } } private void pauseRecording() { if (isRecording) { if (pauseButton.getText().equals("Pause")) { pauseTime = System.currentTimeMillis(); timer.stop(); statusLabel.setText("Paused"); pauseButton.setText("Resume"); } else { long diff = System.currentTimeMillis() - pauseTime; startTime += diff; timer.start(); statusLabel.setText("Recording..."); pauseButton.setText("Pause"); } } } private void stopRecording() { if (isRecording) { isRecording = false; timer.stop(); try { recorder.stop(); recorder.release(); statusLabel.setText("Ready"); startButton.setEnabled(true); pauseButton.setEnabled(false); stopButton.setEnabled(false); } catch (Exception ex) { ex.printStackTrace(); } } } @Override public void actionPerformed(ActionEvent e) { if (e.getSource() == startButton) { startRecording(); } else if (e.getSource() == pauseButton) { pauseRecording(); } else if (e.getSource() == stopButton) { stopRecording(); } } public static void main(String[] args) throws Exception { new ScreenRecorder(); } } ``` 这个示例中,界面包含了三个按钮:开始、暂停和停止。当用户点击开始按钮时,程序会创建一个FFmpegFrameRecorder对象并开始录制屏幕。当用户点击暂停按钮时,程序会暂停录制并停止计时器。当用户点击停止按钮时,程序会停止录制并保存视频文件。同时,程序会在界面上显示录制的视频预览。 在这个示例中,视频文件的格式为mp4,输出目录为output。用户可以根据需要修改这些设置。 需要注意的是,由于录制屏幕需要操作系统的权限,所以需要以管理员身份运行程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值