螢幕錄製 (Screen Capture)
在遠端工作與線上教學盛行的時代,螢幕錄影成為了剛需。過去我們習慣安裝 OBS 或 Loom 等軟體,但其實現代瀏覽器已經內建了強大的 Screen Capture API,讓我們能直接在網頁上實現「螢幕畫面分享」與「錄影」功能。
本篇將帶你整合 getDisplayMedia 與 MediaRecorder,封裝出一個強大的 Vue 3 useScreenRecorder 組合函式。
一、 核心 API 介紹
實現螢幕錄影主要需要兩個 API 的配合:
1.1 獲取畫面:Screen Capture API
使用 navigator.mediaDevices.getDisplayMedia() 來喚起瀏覽器的選擇視窗,讓使用者選擇要分享「整個螢幕」、「特定視窗」或「Chrome 分頁」。
typescript
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true, // 可選:是否同時錄製系統聲音
});1.2 錄製串流:MediaStream Recording API
取得 stream 後,我們使用 MediaRecorder 來將即時的影音訊號錄製成檔案 (Blob)。
typescript
const recorder = new MediaRecorder(stream);
const chunks = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = () => {
const blob = new Blob(chunks, { type: "video/webm" });
// 產生下載連結或預覽
};
recorder.start();二、 設計 Composable 介面
我們希望 useScreenRecorder 能提供直覺的狀態管理:
typescript
// types/useScreenRecorder.ts
import type { Ref, ComputedRef } from "vue";
export interface UseScreenRecorderReturn {
/** 是否支援此 API */
isSupported: ComputedRef<boolean>;
/** 是否正在錄製中 */
isRecording: Ref<boolean>;
/** 錄製完成的影片連結 (Blob URL) */
recordingUrl: Ref<string | null>;
/** 錄製完成的影片檔案 (Blob) */
recordingBlob: Ref<Blob | null>;
/** 開始錄製 */
startRecording: () => Promise<void>;
/** 停止錄製 */
stopRecording: () => void;
}三、 實作 useScreenRecorder Composable
3.1 完整實作
typescript
// composables/useScreenRecorder.ts
import { ref, computed, onUnmounted } from "vue";
import type { UseScreenRecorderReturn } from "@/types/useScreenRecorder";
export function useScreenRecorder(): UseScreenRecorderReturn {
const isSupported = computed(
() =>
typeof navigator !== "undefined" &&
!!navigator.mediaDevices?.getDisplayMedia &&
typeof MediaRecorder !== "undefined",
);
const isRecording = ref(false);
const recordingBlob = ref<Blob | null>(null);
const recordingUrl = ref<string | null>(null);
let mediaRecorder: MediaRecorder | null = null;
let stream: MediaStream | null = null;
let chunks: BlobPart[] = [];
// 清理資源
function cleanup() {
if (stream) {
stream.getTracks().forEach((track) => track.stop());
stream = null;
}
if (recordingUrl.value) {
URL.revokeObjectURL(recordingUrl.value);
recordingUrl.value = null;
}
mediaRecorder = null;
chunks = [];
}
// 開始錄製
async function startRecording() {
if (!isSupported.value) return;
try {
// 1. 請求螢幕分享權限
stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true, // 嘗試錄製系統音訊
});
// 2. 初始化 Recorder
mediaRecorder = new MediaRecorder(stream);
chunks = [];
// 監聽資料片段
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) chunks.push(e.data);
};
// 監聽停止事件 (包含使用者按下瀏覽器的「停止分享」按鈕)
mediaRecorder.onstop = () => {
isRecording.value = false;
// 組合 Blob
const blob = new Blob(chunks, { type: "video/webm" });
recordingBlob.value = blob;
recordingUrl.value = URL.createObjectURL(blob);
// 確保串流軌道完全停止
if (stream) {
stream.getTracks().forEach((track) => track.stop());
stream = null;
}
};
// 監聽串流本身的停止 (例如使用者直接點擊瀏覽器UI的停止分享)
stream.getVideoTracks()[0].onended = () => {
if (isRecording.value) stopRecording();
};
// 3. 開始錄製
mediaRecorder.start();
isRecording.value = true;
recordingBlob.value = null; // 清除上一次的結果
} catch (err) {
console.error("Failed to start recording:", err);
cleanup();
}
}
// 停止錄製
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== "inactive") {
mediaRecorder.stop();
}
}
onUnmounted(() => {
cleanup();
});
return {
isSupported,
isRecording,
recordingUrl,
recordingBlob,
startRecording,
stopRecording,
};
}NOTE
瀏覽器差異:getDisplayMedia 在不同瀏覽器(Chrome, Firefox, Safari)的權限請求介面略有不同。Safari 需要較新的版本才支援。
四、 實際應用範例
4.1 簡易螢幕錄影機
我們將實作一個包含「開始/停止」按鈕與「影片預覽/下載」功能的介面。
vue
<script setup lang="ts">
import { useScreenRecorder } from "@/composables/useScreenRecorder";
const {
isSupported,
isRecording,
recordingUrl,
startRecording,
stopRecording,
} = useScreenRecorder();
</script>
<template>
<div v-if="isSupported" class="recorder-container">
<div class="controls">
<button v-if="!isRecording" @click="startRecording" class="btn-start">
🎥 開始錄影
</button>
<button v-else @click="stopRecording" class="btn-stop">⏹ 停止錄影</button>
</div>
<div v-if="isRecording" class="status">
🔴 正在錄影中... (請點選「停止分享」或上方按鈕結束)
</div>
<!-- 錄影預覽與下載 -->
<div v-if="recordingUrl" class="preview">
<h3>錄影完成!</h3>
<video :src="recordingUrl" controls autoplay></video>
<a
:href="recordingUrl"
download="screen-recording.webm"
class="btn-download"
>
⬇️ 下載影片
</a>
</div>
</div>
<div v-else>您的瀏覽器不支援螢幕錄製 API。</div>
</template>
<style scoped>
.recorder-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
align-items: center;
}
video {
max-width: 100%;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.status {
color: #ef4444;
font-weight: bold;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
</style>五、 進階技巧與限制
5.1 系統音訊錄製
在使用 getDisplayMedia 時,如果設定 audio: true:
- Windows / Chrome:使用者在選擇分享視窗時,需要勾選「分享系統音訊 (Share system audio)」選項。
- macOS / Safari:系統音訊錄製的支援度較差,通常無法直接錄製系統聲音(除非使用虛擬音源線等第三方方案)。
5.2 混合麥克風聲音
預設的 getDisplayMedia 只能錄製「螢幕畫面」與「系統音效」。如果你想同時錄製「麥克風人聲」(例如旁白解說),你需要:
- 使用
getUserMedia取得麥克風音軌。 - 使用
getDisplayMedia取得螢幕音軌。 - 使用
AudioContext或MediaStream建構子將兩者混合。
typescript
// 混合音訊的概念範例
const screenStream = await navigator.mediaDevices.getDisplayMedia({...});
const micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
const combinedStream = new MediaStream([
...screenStream.getVideoTracks(),
...screenStream.getAudioTracks(), // 系統音(若有的話)
...micStream.getAudioTracks() // 麥克風音
]);WARNING
MediaStream 若包含多個音軌,某些播放器可能只會播放第一軌。更穩定的做法是用 Web Audio API 的 createMediaStreamDestination 進行混音。
總結
| 概念 | 說明 |
|---|---|
| getDisplayMedia | 喚起系統視窗選擇器,取得螢幕串流 |
| MediaRecorder | 將串流錄製為 Blob 檔案 |
| Video/WebM | 瀏覽器錄製的預設格式(相容性最好) |
| 安全性 | 結束錄製時,務必呼叫 track.stop() 釋放硬體資源(並會熄滅瀏覽器分頁上的紅點) |
進階挑戰
- 畫中畫錄影:結合
usePiP,在錄影時顯示一個小的攝影機視窗(Webcam)。 - GIF 轉換:整合
ffmpeg.wasm,將錄製的短片在前端轉檔為 GIF 動圖。 - 雲端上傳:錄製完成後,自動將 Blob 上傳至 S3 或後端伺服器。