前言
在日常开发中,我们经常会遇到需要录制音频的场景,比如在线会议、语音笔记、教学录制等。传统的音频录制通常只能捕获麦克风输入,但在某些场景下,我们可能需要录制系统音频,也就是电脑正在播放的声音。
今天,我们就来实现一个基于 Vue 3 Composition API 的屏幕音频捕获 Hook,它能够录制系统音频并生成音频文件。
技术原理
在开始编码之前,我们先了解几个关键的技术概念:
1. MediaDevices.getDisplayMedia()
这是实现屏幕共享和系统音频捕获的核心 API。它允许用户选择并授权捕获屏幕内容,同时可以选择捕获系统音频。
2. MediaRecorder
Web API 提供的媒体录制接口,可以将 MediaStream 录制为指定格式的文件。
核心实现
让我们逐步分析这个音频捕获 Hook 的实现:
1. 状态管理
javascript
const inCapture = ref(false);
const screenStream = ref(null);
const recorder = ref(null);
let timer = null;
我们使用 Vue 的 ref 来管理捕获状态、屏幕流和录制器实例,确保状态的响应式更新。
2. 媒体流获取
javascript
function invokeGetDisplayMedia({success, fail}) {
var constraints = {
video: true,
audio: true
};
// 兼容不同浏览器的 API
if(navigator?.mediaDevices?.getDisplayMedia) {
navigator.mediaDevices.getDisplayMedia(constraints).then(success).catch(fail);
} else if(navigator.getDisplayMedia) {
navigator.getDisplayMedia(constraints).then(success).catch(fail);
} else {
fail("当前环境不支持该操作");
}
}
这里我们做了浏览器兼容性处理,确保在不同环境下都能正常调用 API。
3. 音频捕获核心逻辑
javascript
function captureScreen() {
return new Promise((resolve, reject) => {
invokeGetDisplayMedia({
success: (screen) => {
inCapture.value = true;
screenStream.value = screen;
// 监听流结束事件
screenStream.value.onended = () => {
reset();
}
screenStream.value.oninactive = () => {
stopCapture();
}
// 检查音频轨道
const audioTracks = screenStream.value.getAudioTracks();
if (audioTracks.length === 0) {
console.error("没有检测到音频,请检查分享音频选项是否开启");
reset();
reject("没有检测到音频,请检查分享音频选项是否开启");
return;
}
// 创建纯音频流
const audioStream = new MediaStream(audioTracks);
nextTick(() => {
// 配置录制参数
const options = {
mimeType: 'audio/webm;codecs=opus',
audioBitsPerSecond: 128000
};
// 格式兼容性回退
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
options.mimeType = 'audio/webm';
}
// 创建录制器
recorder.value = new MediaRecorder(audioStream, options);
recorder.value.start();
// 处理录制数据
recorder.value.ondataavailable = (event) => {
if (event?.data?.size > 0) {
const file = new File([event.data],
`audio_capture_${new Date().getTime()}.opus`,
{ type: 'audio/opus' });
reset();
resolve(file);
} else {
console.error("录制失败");
reset();
reject("录制失败");
}
}
// 设置最大录制时间
timer = setTimeout(() => {
stopCapture();
}, maxTime);
})
},
fail: (err) => {
reject(err);
}
})
})
}
4. 资源清理
javascript
function reset() {
screenStream.value?.getTracks()?.forEach(track => track.stop());
screenStream.value = null;
inCapture.value = false;
recorder.value = null;
clearTimeout(timer);
timer = null;
}
onUnmounted(() => {
reset();
})
确保在组件卸载时正确清理所有资源,避免内存泄漏。
5. 完整代码
javascript
import { nextTick, onUnmounted, ref } from "vue";
export function useAudioCapture({maxTime = 60 * 1000}) {
const inCapture = ref(false);
const screenStream = ref(null);
const recorder = ref(null);
let timer = null
function stopCapture() {
recorder.value?.stop();
}
function reset() {
screenStream.value?.getTracks()?.forEach(track => track.stop());
screenStream.value = null;
inCapture.value = false;
recorder.value = null;
clearTimeout(timer);
timer = null;
}
onUnmounted(() => {
reset();
})
function captureScreen() {
return new Promise((resolve, reject) => {
invokeGetDisplayMedia({
success: (screen) => {
inCapture.value = true;
screenStream.value = screen;
screenStream.value.onended = () => {
reset();
}
screenStream.value.oninactive = () => {
stopCapture();
}
const audioTracks = screenStream.value.getAudioTracks();
if (audioTracks.length === 0) {
console.error("没有检测到音频,请检查分享音频选项是否开启");
reset();
reject("没有检测到音频,请检查分享音频选项是否开启");
return;
}
const audioStream = new MediaStream(audioTracks);
nextTick(() => {
const options = {
mimeType: 'audio/webm;codecs=opus',
audioBitsPerSecond: 128000
};
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
options.mimeType = 'audio/webm';
}
recorder.value = new MediaRecorder(audioStream, options);
recorder.value.start();
recorder.value.ondataavailable = (event) => {
if (event?.data?.size > 0) {
const file = new File([event.data], `audio_capture_${new Date().getTime()}.opus`, { type: 'audio/opus' });
reset();
resolve(file);
} else {
console.error("录制失败");
reset();
reject("录制失败");
}
}
timer = setTimeout(() => {
stopCapture();
}, maxTime);
})
},
fail: (err) => {
reject(err);
}
})
})
}
function invokeGetDisplayMedia({success, fail}) {
var constraints = {
video : true,
audio : true
};
if(navigator?.mediaDevices?.getDisplayMedia) {
navigator.mediaDevices.getDisplayMedia(constraints).then(success).catch(fail);
} else if(navigator.getDisplayMedia) {
navigator.getDisplayMedia(constraints).then(success).catch(fail);
} else {
fail("当前环境不支持该操作");
}
}
return {
reset,
captureScreen,
stopCapture,
get InCapture(){ return inCapture.value},
}
}
在组件中使用
下面是一个使用示例,展示如何在 Vue 组件中集成这个 Hook:
vue
<template>
<div class="audio-recorder">
<button
@click="startRecording"
:disabled="inCapture"
class="record-button"
>
{{ inCapture ? '录制中...' : '开始录制' }}
</button>
<button
@click="stopRecording"
:disabled="!inCapture"
class="stop-button"
>
停止录制
</button>
<div v-if="lastRecordedFile" class="recording-info">
<p>录制完成!文件: {{ lastRecordedFile.name }}</p>
<audio :src="audioUrl" controls class="audio-player" />
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useAudioCapture } from './useAudioCapture';
const { captureScreen, stopCapture, get InCapture() { return inCapture.value } } =
useAudioCapture({ maxTime: 5 * 60 * 1000 }); // 5分钟限制
const inCapture = ref(false);
const lastRecordedFile = ref(null);
const audioUrl = ref('');
const startRecording = async () => {
try {
const file = await captureScreen();
lastRecordedFile.value = file;
audioUrl.value = URL.createObjectURL(file);
} catch (error) {
console.error('录制失败:', error);
alert(`录制失败: ${error}`);
}
};
const stopRecording = () => {
stopCapture();
};
</script>
<style scoped>
.audio-recorder {
padding: 20px;
text-align: center;
}
.record-button, .stop-button {
margin: 0 10px;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.record-button {
background-color: #4CAF50;
color: white;
}
.record-button:disabled {
background-color: #cccccc;
}
.stop-button {
background-color: #f44336;
color: white;
}
.audio-player {
margin-top: 20px;
width: 100%;
max-width: 400px;
}
</style>
关键特性与优化
1. 音频格式选择
我们优先选择 audio/webm;codecs=opus 格式,因为:
- Opus 编码:提供更好的音质和压缩比
- WebM 容器:现代浏览器支持良好
- 兼容性回退:在不支持时回退到基础格式
2. 错误处理
javascript
// 检查音频轨道是否存在
const audioTracks = screenStream.value.getAudioTracks();
if (audioTracks.length === 0) {
console.error("没有检测到音频,请检查分享音频选项是否开启");
reset();
reject("没有检测到音频,请检查分享音频选项是否开启");
return;
}
3. 自动超时保护
通过 maxTime 参数防止无限期录制,保护系统资源。
浏览器兼容性说明
| 浏览器 | 支持情况 | 注意事项 |
|---|---|---|
| Chrome 74+ | ✅ 完全支持 | 推荐使用 |
| Edge 79+ | ✅ 完全支持 | 基于 Chromium |
| Firefox 66+ | ✅ 基本支持 | 可能需要配置 |
| Safari | ⚠️ 部分支持 | 有限的功能 |
总结
通过这个 Vue 3 Hook,我们实现了一个功能完整、易于使用的屏幕音频捕获解决方案。它不仅提供了基础的录制功能,还考虑了错误处理、资源管理和用户体验等多个方面。
这种基于 Composition API 的实现方式,体现了 Vue 3 在逻辑复用和代码组织方面的优势,可以作为其他媒体处理功能的参考实现。
希望这篇文章能够帮助你在实际项目中更好地处理音频录制需求,如果有任何问题或建议,欢迎在评论区讨论!