Vue 3 与 Screen Capture API
概述
Screen Capture API 是现代浏览器提供的一组 API,允许 web 应用捕获用户设备的屏幕内容。它提供了一种简单的方式来捕获整个屏幕、单个窗口或浏览器标签页,并将其转换为可处理的媒体流。
与传统的屏幕捕获方法相比,Screen Capture API 具有以下优势:
- 无需额外插件或扩展
- 更好的性能和更低的延迟
- 更安全的权限模型
- 与其他 Web API 良好集成
- 支持多种捕获模式
本教程将介绍如何在 Vue 3 应用中集成 Screen Capture API,构建一个功能完整的屏幕录制应用。
核心知识点
1. API 基本概念
Screen Capture API 主要包含以下核心接口:
- **MediaDevices.getDisplayMedia()**:用于请求用户授权并捕获屏幕内容
- MediaStream:表示捕获的媒体流
- MediaStreamTrack:表示媒体流中的单个轨道
- DisplayMediaStreamConstraints:用于配置捕获参数
2. 捕获屏幕内容
基本屏幕捕获
async function captureScreen() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: 'always', // 显示鼠标光标
displaySurface: 'monitor' // 捕获整个屏幕
},
audio: false // 不捕获音频
});
return stream;
} catch (error) {
console.error('Error capturing screen:', error);
}
}高级配置选项
async function captureScreenWithOptions() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: 'motion', // 仅当鼠标移动时显示光标
displaySurface: 'browser', // 优先捕获浏览器标签页
logicalSurface: true // 捕获逻辑表面(如虚拟桌面)
},
audio: {
echoCancellation: true,
noiseSuppression: true
} // 捕获系统音频
});
return stream;
} catch (error) {
console.error('Error capturing screen:', error);
}
}3. 监听捕获停止事件
当用户停止屏幕捕获时,可以通过监听 ended 事件来处理:
function setupScreenCapture() {
navigator.mediaDevices.getDisplayMedia({ video: true })
.then(stream => {
// 使用媒体流
// 监听流结束事件
stream.getVideoTracks()[0].onended = () => {
console.log('Screen capture ended');
// 处理捕获结束逻辑
};
})
.catch(error => {
console.error('Error:', error);
});
}4. 结合 MediaRecorder 录制屏幕
将 Screen Capture API 与 MediaRecorder API 结合,可以实现屏幕录制功能:
async function startScreenRecording() {
try {
// 捕获屏幕流
const screenStream = await navigator.mediaDevices.getDisplayMedia({
video: { cursor: 'always' },
audio: true
});
// 创建录制器
const recorder = new MediaRecorder(screenStream, {
mimeType: 'video/webm;codecs=vp9,opus'
});
const chunks = [];
recorder.ondataavailable = (event) => {
if (event.data.size > 0) {
chunks.push(event.data);
}
};
recorder.onstop = () => {
const videoBlob = new Blob(chunks, { type: 'video/webm' });
const videoUrl = URL.createObjectURL(videoBlob);
// 使用 videoUrl 播放或下载视频
};
recorder.start(1000);
return { recorder, stream: screenStream };
} catch (error) {
console.error('Error starting screen recording:', error);
}
}5. Vue 3 组合式 API 封装
创建一个 useScreenCapture 组合式函数来封装 Screen Capture API:
import { ref, onUnmounted } from 'vue';
export function useScreenCapture() {
const screenStream = ref(null);
const isCapturing = ref(false);
const error = ref(null);
// 捕获屏幕
const captureScreen = async (constraints = {}) => {
try {
error.value = null;
// 默认配置
const defaultConstraints = {
video: {
cursor: 'always'
},
audio: false
};
// 合并配置
const captureConstraints = {
...defaultConstraints,
...constraints
};
// 请求屏幕捕获
screenStream.value = await navigator.mediaDevices.getDisplayMedia(captureConstraints);
isCapturing.value = true;
// 监听捕获结束事件
const videoTrack = screenStream.value.getVideoTracks()[0];
if (videoTrack) {
videoTrack.onended = () => {
isCapturing.value = false;
screenStream.value = null;
};
}
return screenStream.value;
} catch (err) {
error.value = `无法捕获屏幕: ${err.message}`;
console.error('Error capturing screen:', err);
throw err;
}
};
// 停止捕获
const stopCapture = () => {
if (screenStream.value) {
screenStream.value.getTracks().forEach(track => track.stop());
screenStream.value = null;
isCapturing.value = false;
}
};
// 组件卸载时清理资源
onUnmounted(() => {
stopCapture();
});
return {
screenStream,
isCapturing,
error,
captureScreen,
stopCapture
};
}6. 结合 MediaRecorder 实现屏幕录制
创建一个更完整的组合式函数,结合 Screen Capture API 和 MediaRecorder API:
import { ref, onUnmounted } from 'vue';
export function useScreenRecorder() {
const screenStream = ref(null);
const recorder = ref(null);
const recordedChunks = ref([]);
const recordedVideo = ref(null);
const isCapturing = ref(false);
const isRecording = ref(false);
const duration = ref(0);
const error = ref(null);
let timer = null;
// 捕获屏幕
const captureScreen = async (captureConstraints = {}) => {
try {
error.value = null;
const defaultCaptureConstraints = {
video: {
cursor: 'always'
},
audio: true
};
const constraints = {
...defaultCaptureConstraints,
...captureConstraints
};
screenStream.value = await navigator.mediaDevices.getDisplayMedia(constraints);
isCapturing.value = true;
// 监听捕获结束事件
const videoTrack = screenStream.value.getVideoTracks()[0];
if (videoTrack) {
videoTrack.onended = () => {
stopRecording();
isCapturing.value = false;
screenStream.value = null;
};
}
return screenStream.value;
} catch (err) {
error.value = `无法捕获屏幕: ${err.message}`;
console.error('Error capturing screen:', err);
throw err;
}
};
// 创建录制器
const createRecorder = (stream, recorderOptions = {}) => {
try {
error.value = null;
// 检查浏览器支持的 MIME 类型
const supportedMimeTypes = ['video/webm;codecs=vp9,opus', 'video/webm;codecs=vp8,opus'];
const mimeType = recorderOptions.mimeType || supportedMimeTypes.find(type => MediaRecorder.isTypeSupported(type));
if (!mimeType) {
throw new Error('No supported MIME type found');
}
recorder.value = new MediaRecorder(stream, { mimeType });
recordedChunks.value = [];
// 处理录制数据
recorder.value.ondataavailable = (event) => {
if (event.data.size > 0) {
recordedChunks.value.push(event.data);
}
};
// 录制结束处理
recorder.value.onstop = () => {
const videoBlob = new Blob(recordedChunks.value, { type: mimeType });
recordedVideo.value = URL.createObjectURL(videoBlob);
stopDurationTimer();
isRecording.value = false;
};
// 录制开始处理
recorder.value.onstart = () => {
isRecording.value = true;
startDurationTimer();
};
return recorder.value;
} catch (err) {
error.value = `创建录制器失败: ${err.message}`;
console.error('Error creating recorder:', err);
throw err;
}
};
// 开始录制
const startRecording = async (captureConstraints = {}) => {
try {
error.value = null;
// 如果没有屏幕流,先捕获屏幕
if (!screenStream.value) {
await captureScreen(captureConstraints);
}
// 创建录制器
if (!recorder.value) {
createRecorder(screenStream.value);
}
// 开始录制
recorder.value.start(1000);
return recorder.value;
} catch (err) {
error.value = `开始录制失败: ${err.message}`;
console.error('Error starting recording:', err);
throw err;
}
};
// 停止录制
const stopRecording = () => {
try {
error.value = null;
if (recorder.value && recorder.value.state !== 'inactive') {
recorder.value.stop();
}
} catch (err) {
error.value = `停止录制失败: ${err.message}`;
console.error('Error stopping recording:', err);
throw err;
} finally {
isRecording.value = false;
stopDurationTimer();
}
};
// 停止捕获
const stopCapture = () => {
stopRecording();
if (screenStream.value) {
screenStream.value.getTracks().forEach(track => track.stop());
screenStream.value = null;
isCapturing.value = false;
}
};
// 重置录制器
const resetRecorder = () => {
stopCapture();
if (recordedVideo.value) {
URL.revokeObjectURL(recordedVideo.value);
recordedVideo.value = null;
}
if (recorder.value) {
recorder.value = null;
}
recordedChunks.value = [];
duration.value = 0;
error.value = null;
};
// 开始时长计时器
const startDurationTimer = () => {
if (timer) {
clearInterval(timer);
}
timer = setInterval(() => {
duration.value += 1;
}, 1000);
};
// 停止时长计时器
const stopDurationTimer = () => {
if (timer) {
clearInterval(timer);
timer = null;
}
};
// 组件卸载时清理资源
onUnmounted(() => {
resetRecorder();
});
return {
screenStream,
recordedVideo,
isCapturing,
isRecording,
duration,
error,
captureScreen,
startRecording,
stopRecording,
stopCapture,
resetRecorder
};
}最佳实践
1. 浏览器兼容性
- 检查 API 支持:在使用前检查浏览器是否支持 Screen Capture API
- 提供降级方案:对于不支持的浏览器,提供替代方案
- 测试不同浏览器:在主流浏览器中测试捕获功能
2. 权限处理
- 明确的权限请求:在请求权限前,向用户解释为什么需要屏幕捕获权限
- 处理权限拒绝:优雅处理用户拒绝权限的情况
- 显示权限状态:向用户显示当前的权限状态
3. 性能优化
- 限制捕获质量:根据实际需求调整视频分辨率和帧率
- 关闭不必要的轨道:仅捕获必要的视频和音频轨道
- 及时释放资源:捕获完成后及时关闭媒体流
- 使用硬件加速:利用浏览器的硬件加速功能
4. 用户体验
- 清晰的状态指示:向用户显示当前捕获状态
- 提供停止按钮:允许用户随时停止捕获
- 显示捕获预览:提供实时预览捕获内容
- 支持快捷键:实现常用的捕获快捷键
5. 安全考虑
- 最小权限原则:仅请求必要的捕获权限
- 保护用户隐私:避免捕获敏感信息
- 安全存储录制内容:妥善处理和存储录制的屏幕内容
- 定期清理数据:定期清理临时录制文件
常见问题与解决方案
1. 浏览器不支持 Screen Capture API
问题:在某些旧浏览器中,Screen Capture API 不被支持
解决方案:
if (!('getDisplayMedia' in navigator.mediaDevices)) {
console.error('Screen Capture API is not supported in this browser');
// 提供降级方案
}2. 用户拒绝屏幕捕获权限
问题:用户拒绝了屏幕捕获权限
解决方案:
try {
const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
} catch (error) {
if (error.name === 'NotAllowedError') {
alert('请允许屏幕捕获权限,否则无法录制屏幕');
} else {
alert(`屏幕捕获失败: ${error.message}`);
}
}3. 捕获的视频质量不佳
问题:捕获的屏幕视频质量不佳,出现模糊或卡顿
解决方案:
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: 'always',
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 60 }
}
});4. 录制的视频没有声音
问题:录制的屏幕视频没有声音
解决方案:
- 确保在 constraints 中包含
audio: true - 检查系统音频设置
- 测试不同的浏览器
5. 捕获的屏幕比例不正确
问题:捕获的屏幕视频比例不正确
解决方案:
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: 'always',
aspectRatio: 16/9
}
});进阶学习资源
- MDN 文档:Screen Capture API
- Web.dev 教程:Screen Capture API: Capture Screen Content
- W3C 规范:Screen Capture
- GitHub 示例:Screen Capture API Samples
- Vue 3 组合式 API 文档:Composition API
实战练习
练习:构建一个屏幕录制应用
目标:使用 Vue 3 和 Screen Capture API 构建一个功能完整的屏幕录制应用
功能要求:
屏幕捕获:
- 支持捕获整个屏幕、单个窗口或浏览器标签页
- 提供捕获选项配置
- 显示捕获预览
屏幕录制:
- 支持开始、暂停和停止录制
- 显示录制时长
- 支持录制系统音频
视频管理:
- 播放录制完成的视频
- 下载录制的视频
- 删除录制的视频
用户体验:
- 响应式设计,适配不同屏幕尺寸
- 清晰的状态指示和错误提示
- 支持键盘快捷键
实现步骤:
- 创建一个 Vue 3 项目
- 实现
useScreenRecorder组合式函数 - 构建屏幕录制界面
- 实现屏幕捕获和录制功能
- 添加视频预览和管理功能
- 测试不同浏览器兼容性
示例代码结构:
<template>
<div class="screen-recorder">
<div class="header">
<h1>屏幕录制器</h1>
</div>
<div class="main">
<div class="preview-container">
<video
ref="videoElement"
class="preview"
:src="recordedVideo || screenStream"
:autoplay="true"
:muted="!recordedVideo"
:controls="recordedVideo"
></video>
<div class="recording-indicator" v-if="isRecording">
<div class="recording-dot"></div>
<span>录制中</span>
</div>
<div class="duration" v-if="isRecording">
{{ formatDuration(duration) }}
</div>
</div>
<div class="controls">
<div class="status" v-if="error">
{{ error }}
</div>
<div class="buttons">
<button
@click="handleStartRecording"
:disabled="isRecording"
>
开始录制
</button>
<button
@click="handleStopRecording"
:disabled="!isRecording"
>
停止录制
</button>
<button
@click="handleSaveVideo"
:disabled="!recordedVideo"
>
保存视频
</button>
<button
@click="handleReset"
:disabled="!isRecording && !recordedVideo"
>
重置
</button>
</div>
<div class="info" v-if="recordedVideo">
<p>录制完成!</p>
<a :href="recordedVideo" :download="`screen-recording-${Date.now()}.webm`">
下载视频
</a>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { useScreenRecorder } from './composables/useScreenRecorder';
const videoElement = ref(null);
const {
screenStream,
recordedVideo,
isCapturing,
isRecording,
duration,
error,
startRecording,
stopRecording,
resetRecorder
} = useScreenRecorder();
// 格式化时长
const formatDuration = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
// 开始录制
const handleStartRecording = async () => {
try {
await startRecording({
video: {
cursor: 'always',
width: { ideal: 1920 },
height: { ideal: 1080 }
},
audio: true
});
} catch (err) {
console.error('Error starting recording:', err);
}
};
// 停止录制
const handleStopRecording = () => {
stopRecording();
};
// 保存视频
const handleSaveVideo = () => {
console.log('Video saved');
// 录制完成后,recordedVideo 会自动生成,用户可以通过下载链接保存
};
// 重置录制器
const handleReset = () => {
resetRecorder();
};
// 组件挂载时初始化
onMounted(() => {
console.log('Screen Recorder initialized');
});
// 监听录制状态变化
watch(isRecording, (newVal) => {
if (newVal) {
console.log('Recording started');
} else {
console.log('Recording stopped');
}
});
</script>
<style scoped>
.screen-recorder {
display: flex;
flex-direction: column;
height: 100vh;
font-family: Arial, sans-serif;
background-color: #f5f5f5;
}
.header {
padding: 1rem;
background-color: #333;
color: white;
text-align: center;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
}
.preview-container {
position: relative;
width: 100%;
max-width: 800px;
margin-bottom: 2rem;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background-color: #000;
}
.preview {
width: 100%;
height: auto;
display: block;
}
.recording-indicator {
position: absolute;
top: 1rem;
left: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
background-color: rgba(255, 0, 0, 0.8);
color: white;
padding: 0.5rem 1rem;
border-radius: 4px;
font-size: 0.9rem;
}
.recording-dot {
width: 8px;
height: 8px;
background-color: white;
border-radius: 50%;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.duration {
position: absolute;
top: 1rem;
right: 1rem;
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 0.5rem 1rem;
border-radius: 4px;
font-size: 0.9rem;
font-weight: bold;
}
.controls {
width: 100%;
max-width: 800px;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.status {
color: red;
font-size: 0.9rem;
text-align: center;
}
.buttons {
display: flex;
gap: 1rem;
flex-wrap: wrap;
justify-content: center;
}
.buttons button {
padding: 0.8rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
background-color: #42b883;
color: white;
}
.buttons button:hover:not(:disabled) {
background-color: #35495e;
transform: translateY(-2px);
}
.buttons button:disabled {
background-color: #ccc;
cursor: not-allowed;
transform: none;
}
.info {
text-align: center;
color: #666;
}
.info a {
color: #42b883;
text-decoration: none;
font-weight: bold;
}
.info a:hover {
text-decoration: underline;
}
</style>进阶学习资源
MDN Web Docs:
Web.dev 文章:
GitHub 仓库:
视频教程:
规范文档:
总结
Screen Capture API 为 web 应用提供了强大的屏幕捕获能力,结合 Vue 3 的组合式 API,可以构建出功能丰富、用户体验良好的屏幕录制应用。在使用过程中,需要注意浏览器兼容性、权限处理和用户体验,始终以用户为中心,提供清晰的反馈和友好的错误处理。
通过本教程的学习,你应该能够:
- 理解 Screen Capture API 的核心概念和基本用法
- 实现 Vue 3 组合式函数封装 API
- 构建功能完整的屏幕录制应用
- 遵循最佳实践,确保应用的兼容性和性能
- 处理常见问题,提供良好的用户体验
Screen Capture API 是 web 应用向多媒体领域扩展的重要一步,它为开发者提供了更多可能性,可以构建出更加强大和灵活的 web 应用。