前言
说起前端实现屏幕录制,相信大部分前端的小伙伴都没有接触过相关的业务,也不知道如果在前端实现这样的逻辑,我也是偶然间看到的前端能够实现这个功能,比较好奇就去深入了解了。话不多说,直接看下面的实现逻辑和代码。
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>
当我们通过点击按钮调用该函数时,就会弹出以下页面
当用户选择录制时,就会显示一个录制和结束的小窗口
这个时候是不是已经有屏幕录制那味了,但是当你点击停止共享时,你会发现啥也没发生,录制的内容也没有保存到本地,这是怎么一回事?
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>
上述代码就可以将屏幕录制的数据下载到本地了,但是这只是简单的录制和结束,录制屏幕不应该给我们提供暂停录制和继续录制的功能?这个时候我们可以通过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>
这部分代码只是简单的实现了一个屏幕录制以及暂停继续的功能,大致的实现思路如上文所述,大家学习工作中如果遇到相关的业务需求,可在上述代码的基础上添加需要的业务逻辑。