跳至主要內容
Skip to content

螢幕錄製 (Screen Capture)

在遠端工作與線上教學盛行的時代,螢幕錄影成為了剛需。過去我們習慣安裝 OBS 或 Loom 等軟體,但其實現代瀏覽器已經內建了強大的 Screen Capture API,讓我們能直接在網頁上實現「螢幕畫面分享」與「錄影」功能。

本篇將帶你整合 getDisplayMediaMediaRecorder,封裝出一個強大的 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 只能錄製「螢幕畫面」與「系統音效」。如果你想同時錄製「麥克風人聲」(例如旁白解說),你需要:

  1. 使用 getUserMedia 取得麥克風音軌。
  2. 使用 getDisplayMedia 取得螢幕音軌。
  3. 使用 AudioContextMediaStream 建構子將兩者混合。
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() 釋放硬體資源(並會熄滅瀏覽器分頁上的紅點)

進階挑戰

  1. 畫中畫錄影:結合 usePiP,在錄影時顯示一個小的攝影機視窗(Webcam)。
  2. GIF 轉換:整合 ffmpeg.wasm,將錄製的短片在前端轉檔為 GIF 動圖。
  3. 雲端上傳:錄製完成後,自動將 Blob 上傳至 S3 或後端伺服器。

延伸閱讀與資源