基于Vue3實(shí)現(xiàn)屏幕音頻捕獲的具體方案
前言
在日常開(kāi)發(fā)中,我們經(jīng)常會(huì)遇到需要錄制音頻的場(chǎng)景,比如在線會(huì)議、語(yǔ)音筆記、教學(xué)錄制等。傳統(tǒng)的音頻錄制通常只能捕獲麥克風(fēng)輸入,但在某些場(chǎng)景下,我們可能需要錄制系統(tǒng)音頻,也就是電腦正在播放的聲音。
今天,我們就來(lái)實(shí)現(xiàn)一個(gè)基于 Vue 3 Composition API 的屏幕音頻捕獲 Hook,它能夠錄制系統(tǒng)音頻并生成音頻文件。
技術(shù)原理
在開(kāi)始編碼之前,我們先了解幾個(gè)關(guān)鍵的技術(shù)概念:
1. MediaDevices.getDisplayMedia()
這是實(shí)現(xiàn)屏幕共享和系統(tǒng)音頻捕獲的核心 API。它允許用戶選擇并授權(quán)捕獲屏幕內(nèi)容,同時(shí)可以選擇捕獲系統(tǒng)音頻。
2. MediaRecorder
Web API 提供的媒體錄制接口,可以將 MediaStream 錄制為指定格式的文件。
核心實(shí)現(xiàn)
讓我們逐步分析這個(gè)音頻捕獲 Hook 的實(shí)現(xiàn):
1. 狀態(tài)管理
const inCapture = ref(false); const screenStream = ref(null); const recorder = ref(null); let timer = null;
我們使用 Vue 的 ref 來(lái)管理捕獲狀態(tài)、屏幕流和錄制器實(shí)例,確保狀態(tài)的響應(yīng)式更新。
2. 媒體流獲取
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("當(dāng)前環(huán)境不支持該操作");
}
}
這里我們做了瀏覽器兼容性處理,確保在不同環(huán)境下都能正常調(diào)用 API。
3. 音頻捕獲核心邏輯
function captureScreen() {
return new Promise((resolve, reject) => {
invokeGetDisplayMedia({
success: (screen) => {
inCapture.value = true;
screenStream.value = screen;
// 監(jiān)聽(tīng)流結(jié)束事件
screenStream.value.onended = () => {
reset();
}
screenStream.value.oninactive = () => {
stopCapture();
}
// 檢查音頻軌道
const audioTracks = screenStream.value.getAudioTracks();
if (audioTracks.length === 0) {
console.error("沒(méi)有檢測(cè)到音頻,請(qǐng)檢查分享音頻選項(xiàng)是否開(kāi)啟");
reset();
reject("沒(méi)有檢測(cè)到音頻,請(qǐng)檢查分享音頻選項(xiàng)是否開(kāi)啟");
return;
}
// 創(chuàng)建純音頻流
const audioStream = new MediaStream(audioTracks);
nextTick(() => {
// 配置錄制參數(shù)
const options = {
mimeType: 'audio/webm;codecs=opus',
audioBitsPerSecond: 128000
};
// 格式兼容性回退
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
options.mimeType = 'audio/webm';
}
// 創(chuàng)建錄制器
recorder.value = new MediaRecorder(audioStream, options);
recorder.value.start();
// 處理錄制數(shù)據(jù)
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("錄制失敗");
}
}
// 設(shè)置最大錄制時(shí)間
timer = setTimeout(() => {
stopCapture();
}, maxTime);
})
},
fail: (err) => {
reject(err);
}
})
})
}
4. 資源清理
function reset() {
screenStream.value?.getTracks()?.forEach(track => track.stop());
screenStream.value = null;
inCapture.value = false;
recorder.value = null;
clearTimeout(timer);
timer = null;
}
onUnmounted(() => {
reset();
})
確保在組件卸載時(shí)正確清理所有資源,避免內(nèi)存泄漏。
5. 完整代碼
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("沒(méi)有檢測(cè)到音頻,請(qǐng)檢查分享音頻選項(xiàng)是否開(kāi)啟");
reset();
reject("沒(méi)有檢測(cè)到音頻,請(qǐng)檢查分享音頻選項(xiàng)是否開(kāi)啟");
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("當(dāng)前環(huán)境不支持該操作");
}
}
return {
reset,
captureScreen,
stopCapture,
get InCapture(){ return inCapture.value},
}
}
在組件中使用
下面是一個(gè)使用示例,展示如何在 Vue 組件中集成這個(gè) Hook:
<template>
<div class="audio-recorder">
<button
@click="startRecording"
:disabled="inCapture"
class="record-button"
>
{{ inCapture ? '錄制中...' : '開(kāi)始錄制' }}
</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>
關(guān)鍵特性與優(yōu)化
1. 音頻格式選擇
我們優(yōu)先選擇 audio/webm;codecs=opus 格式,因?yàn)椋?/p>
- Opus 編碼:提供更好的音質(zhì)和壓縮比
- WebM 容器:現(xiàn)代瀏覽器支持良好
- 兼容性回退:在不支持時(shí)回退到基礎(chǔ)格式
2. 錯(cuò)誤處理
// 檢查音頻軌道是否存在
const audioTracks = screenStream.value.getAudioTracks();
if (audioTracks.length === 0) {
console.error("沒(méi)有檢測(cè)到音頻,請(qǐng)檢查分享音頻選項(xiàng)是否開(kāi)啟");
reset();
reject("沒(méi)有檢測(cè)到音頻,請(qǐng)檢查分享音頻選項(xiàng)是否開(kāi)啟");
return;
}
3. 自動(dòng)超時(shí)保護(hù)
通過(guò) maxTime 參數(shù)防止無(wú)限期錄制,保護(hù)系統(tǒng)資源。
瀏覽器兼容性說(shuō)明
| 瀏覽器 | 支持情況 | 注意事項(xiàng) |
|---|---|---|
| Chrome 74+ | ? 完全支持 | 推薦使用 |
| Edge 79+ | ? 完全支持 | 基于 Chromium |
| Firefox 66+ | ? 基本支持 | 可能需要配置 |
| Safari | ?? 部分支持 | 有限的功能 |
總結(jié)
通過(guò)這個(gè) Vue 3 Hook,我們實(shí)現(xiàn)了一個(gè)功能完整、易于使用的屏幕音頻捕獲解決方案。它不僅提供了基礎(chǔ)的錄制功能,還考慮了錯(cuò)誤處理、資源管理和用戶體驗(yàn)等多個(gè)方面。
這種基于 Composition API 的實(shí)現(xiàn)方式,體現(xiàn)了 Vue 3 在邏輯復(fù)用和代碼組織方面的優(yōu)勢(shì),可以作為其他媒體處理功能的參考實(shí)現(xiàn)。
以上就是基于Vue3實(shí)現(xiàn)屏幕音頻捕獲的具體方案的詳細(xì)內(nèi)容,更多關(guān)于Vue3屏幕音頻捕獲的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于vue cli 通過(guò)命令行傳參實(shí)現(xiàn)多環(huán)境配置
這篇文章主要介紹了vue項(xiàng)目通過(guò)命令行傳參實(shí)現(xiàn)多環(huán)境配置(基于@vue/cli)的相關(guān)資料,需要的朋友可以參考下2018-07-07
利用vue組件自定義v-model實(shí)現(xiàn)一個(gè)Tab組件方法示例
這篇文章主要給大家介紹了關(guān)于利用vue組件自定義v-model實(shí)現(xiàn)一個(gè)Tab組件的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-12-12
vue 動(dòng)態(tài)創(chuàng)建組件的兩種方法
這篇文章主要介紹了vue 動(dòng)態(tài)創(chuàng)建組件的兩種方法,幫助大家更好的理解和使用vue框架,感興趣的朋友可以了解下2020-12-12

