面试官:如何在前端实现屏幕录制并保存

1,296 阅读8分钟

前言

说起前端实现屏幕录制,相信大部分前端的小伙伴都没有接触过相关的业务,也不知道如果在前端实现这样的逻辑,我也是偶然间看到的前端能够实现这个功能,比较好奇就去深入了解了。话不多说,直接看下面的实现逻辑和代码。

navigator.mediaDevices

mediaDevices 是 Navigator 只读属性,返回一个 MediaDevices 对象,该对象可提供对相机和麦克风等媒体输入设备的连接访问,也包括屏幕共享。 返回的 MediaDevices 对象中提供了一个方法来实现弹框询问录屏,这个方法就是:getDisplayMedia。它返回一个 Promise 对象,成功后会resolve回调一个 MediaStream 对象。若用户拒绝了使用权限,或者需要的媒体源不可用,promise会reject回调一个 PermissionDeniedError 或者 NotFoundError 。

async function startVideo() {
  // getDisplayMedia 方法来获取用户的屏幕分享或屏幕捕获流,通常用于制作屏幕录像或视频会议等应用,异步函数
  // 它返回一个 Promise 对象,成功后会resolve回调一个 MediaStream 对象。若用户拒绝了使用权限,或者需要的媒体源不可用,promise会reject回调一个 PermissionDeniedError 或者 NotFoundError 。
  const stream = await navigator.mediaDevices.getDisplayMedia({
    // 拥有两个参数,video 和 audio,在移动端可以配置前置摄像头或后置摄像头
    // 前置摄像头:video: { facingMode: "user" },后置摄像头:video: { facingMode: "environment" },强制使用某种摄像头:video: { facingMode: { exact: "environment" } }
    video: true,
    audio: true
  }).catch((e) => {console.log(e);})
 }

当然 video 和 audio 除了以上的配置,还有其它更加详细的配置参数可以点击这里前往学习。

<template>
  <div class="container">
   <div style="display: flex;">
      <el-button type="primary"  @click="startVideo">开启录制</el-button>
    </div>
  </div>
</template>

<script setup>
async function startVideo() {
  // getDisplayMedia 方法来获取用户的屏幕分享或屏幕捕获流,通常用于制作屏幕录像或视频会议等应用,异步函数
  // 它返回一个 Promise 对象,成功后会resolve回调一个 MediaStream 对象。若用户拒绝了使用权限,或者需要的媒体源不可用,promise会reject回调一个 PermissionDeniedError 或者 NotFoundError 。
  const stream = await navigator.mediaDevices.getDisplayMedia({
    // 拥有两个参数,video 和 audio,在移动端可以配置前置摄像头或后置摄像头
    // 前置摄像头:video: { facingMode: "user" },后置摄像头:video: { facingMode: "environment" },强制使用某种摄像头:video: { facingMode: { exact: "environment" } }
    video: true,
    audio: true
  }).catch((e) => {console.log(e);})
 }
</script>

当我们通过点击按钮调用该函数时,就会弹出以下页面

image.png

当用户选择录制时,就会显示一个录制和结束的小窗口

image.png 这个时候是不是已经有屏幕录制那味了,但是当你点击停止共享时,你会发现啥也没发生,录制的内容也没有保存到本地,这是怎么一回事?

MediaRecorder

getDisplayMedia方法只是帮助我们调用浏览器提供的录制窗口,并没有帮我们记录屏幕录制过程中的数据,所以这个时候我们需要手动记录录屏过程中产生的数据,也就是 MediaRecorder。MediaRecorder构造函数创建一个新的MediaRecorder对象,对指定的对象进行录制(也就是上面代码中通过getDisplayMedia返回的 stream),支持的配置项包括设置容器的 MIME 类型 (例如"video/webm" 或者 "video/mp4") 和音频及视频的码率或者二者同用一个码率。

async function startVideo() {
  // getDisplayMedia 方法来获取用户的屏幕分享或屏幕捕获流,通常用于制作屏幕录像或视频会议等应用,异步函数
  // 它返回一个 Promise 对象,成功后会resolve回调一个 MediaStream 对象。若用户拒绝了使用权限,或者需要的媒体源不可用,promise会reject回调一个 PermissionDeniedError 或者 NotFoundError 。
  const stream = await navigator.mediaDevices.getDisplayMedia({
    // 拥有两个参数,video 和 audio,在移动端可以配置前置摄像头或后置摄像头
    // 前置摄像头:video: { facingMode: "user" },后置摄像头:video: { facingMode: "environment" },强制使用某种摄像头:video: { facingMode: { exact: "environment" } }
    video: true,
    audio: true
  }).catch((e) => {console.log(e);})
  // 判断 MediaRecorder 支持的文件类型
  const mime = MediaRecorder.isTypeSupported("video/webm;codecs=h264")
    ? "video/webm;codecs=h264"
    : "video/webm";
  let mediaRecorder = new MediaRecorder(stream, { mimeType: mime });
  // mimeType 可选参数
  // var types = [
  //   "video/webm",
  //   "audio/webm",
  //   "video/webm;codecs=vp8",
  //   "video/webm;codecs=daala",
  //   "video/webm;codecs=h264",
  //   "audio/webm;codecs=opus",
  //   "video/mpeg",
  // ];
 }

上述代码我们为MediaStream对象创建了一个记录数据的容器,new MediaRecorder(stream, { mimeType: mime });第一个参数是MediaStream对象,第二个参数为新构建的 MediaRecorder 指定录制容器的 MIME 类型。在应用中通过调用 MediaRecorder.isTypeSupported() 来检查浏览器是否支持此种mimeType 。

现在容器有了,我们怎么进行数据收集呢?在容器的实例对象上,我们可以通过监听dataavailable来获取录制过程中产生的数据

mediaRecorder.addEventListener("dataavailable", function (e) {
    // chunks 为提前定义好的数组
   chunks.push(e.data);
 });

现在存储录制视频的数据容器都有了,那我们就应该思考怎么实现在屏幕录制结束后将视频下载到本地。首先我们可以通过监听stop来监听用户停止屏幕录制的行为,并将已经准备好的数组数据下载为视频。

mediaRecorder.addEventListener("stop", () => {
   // 获取一个 blob 对象
   const blob = new Blob(chunks, { type: chunks[0].type });
   // 该方法接收一个 blob 对象或 file 对象,返回值为一个 url 地址
   const url = URL.createObjectURL(blob);
   const a = document.createElement("a");
   a.href = url;
   a.download = "video.webm";  // 文件名
   a.click();
 });

该下载文件的实现思路是将数据转化为一个 blob 对象,并通过 URL.createObjectURL(blob) 获取一个 url 地址并赋值给一个 a 标签,模拟 a 标签的点击事件进行文件的下载。

<template>
  <div class="container">
    <div>
      <el-button type="primary" @click="startVideo">开启录制</el-button>
    </div>
  </div>
</template>

<script setup>
async function startVideo() {
  // getDisplayMedia 方法来获取用户的屏幕分享或屏幕捕获流,通常用于制作屏幕录像或视频会议等应用,异步函数
  // 它返回一个 Promise 对象,成功后会resolve回调一个 MediaStream 对象。若用户拒绝了使用权限,或者需要的媒体源不可用,promise会reject回调一个 PermissionDeniedError 或者 NotFoundError 。
  const stream = await navigator.mediaDevices.getDisplayMedia({
    // 拥有两个参数,video 和 audio,在移动端可以配置前置摄像头或后置摄像头
    // 前置摄像头:video: { facingMode: "user" },后置摄像头:video: { facingMode: "environment" },强制使用某种摄像头:video: { facingMode: { exact: "environment" } }
    video: true,
    audio: true
  }).catch((e) => {console.log(e);})
  // 判断 MediaRecorder 支持的文件类型
  const mime = MediaRecorder.isTypeSupported("video/webm;codecs=h264")
    ? "video/webm;codecs=h264"
    : "video/webm";
  // mimeType 支持的类型,第二个 options 参数是可选项,第一个参数表示要记录的流
  let mediaRecorder = new MediaRecorder(stream, { mimeType: mime });
  const chunks = [];

  mediaRecorder.addEventListener("dataavailable", function (e) {
    chunks.push(e.data);
  });

  mediaRecorder.addEventListener("stop", () => {
    isStart.value = false
    const blob = new Blob(chunks, { type: chunks[0].type });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "video.webm";
    a.click();
  });
  // 开始记录数据
  mediaRecorder.start();
}
</script>

<style lang='scss' scoped>
.container {
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}
</style>

image.png

上述代码就可以将屏幕录制的数据下载到本地了,但是这只是简单的录制和结束,录制屏幕不应该给我们提供暂停录制和继续录制的功能?这个时候我们可以通过MediaRecorder提供的属性和方法来实现:

mediaRecorder.state:返回当前容器的录制状态

mediaRecorder.pause():暂停屏幕录制的数据缓存

mediaRecorder.resume():继续屏幕录制的数据缓存

完整代码

当我们将上述功能都应用到屏幕录制中时,就可以通过状态控制按钮的是否可用以及控制录制屏幕的状态

<template>
  <div class="container">
    <div style="display: flex;">
      <el-button type="primary" :disabled="isStart" @click="startVideo">开启录制</el-button>
      <el-button type="warning" :disabled="!isStart" @click="changeState">{{ state }}</el-button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const state = ref('暂停')
const isStart = ref(false)
let mediaRecorder
async function startVideo() {
  // getDisplayMedia 方法来获取用户的屏幕分享或屏幕捕获流,通常用于制作屏幕录像或视频会议等应用,异步函数
  // 它返回一个 Promise 对象,成功后会resolve回调一个 MediaStream 对象。若用户拒绝了使用权限,或者需要的媒体源不可用,promise会reject回调一个 PermissionDeniedError 或者 NotFoundError 。
  const stream = await navigator.mediaDevices.getDisplayMedia({
    // 拥有两个参数,video 和 audio,在移动端可以配置前置摄像头或后置摄像头
    // 前置摄像头:video: { facingMode: "user" },后置摄像头:video: { facingMode: "environment" },强制使用某种摄像头:video: { facingMode: { exact: "environment" } }
    video: true,
    audio: true
  }).catch((e) => {console.log(e);})
  // 判断 MediaRecorder 支持的文件类型
  const mime = MediaRecorder.isTypeSupported("video/webm;codecs=h264")
    ? "video/webm;codecs=h264"
    : "video/webm";
  // mimeType 支持的类型,第二个 options 参数是可选项,第一个参数表示要记录的流
  mediaRecorder = new MediaRecorder(stream, { mimeType: mime });
  const chunks = [];

  mediaRecorder.addEventListener("dataavailable", function (e) {
    chunks.push(e.data);
  });

  mediaRecorder.addEventListener("stop", () => {
    isStart.value = false
    const blob = new Blob(chunks, { type: chunks[0].type });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "video.webm";
    a.click();
  });
  // 开始记录数据
  mediaRecorder.start();
  isStart.value = true
}

function changeState() {
  // 当前状态为录制中,可通过 mediaRecorder.pause() 暂停录制(不记录暂停过程中产生的数据)
  if (mediaRecorder.state === "recording") {
    mediaRecorder.pause();
    state.value = '继续'
  // 当前状态为暂停,可通过 mediaRecorder.resume() 继续录制(继续记录产生的数据)
  } else if (mediaRecorder.state === "paused") {
    mediaRecorder.resume();
    state.value = '暂停'
  }
}
</script>

<style lang='scss' scoped>
.container {
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}
</style>

这部分代码只是简单的实现了一个屏幕录制以及暂停继续的功能,大致的实现思路如上文所述,大家学习工作中如果遇到相关的业务需求,可在上述代码的基础上添加需要的业务逻辑。