iT邦幫忙

2025 iThome 鐵人賽

DAY 11
2
Modern Web

從 Canvas 到各式各樣的 Web API 之旅系列 第 11

Day 11 - 錄製螢幕畫面與聲音!MediaDevices + MediaRecorder

  • 分享至 

  • xImage
  •  

昨天我們用 MediaDevices 打開麥克風/攝影機,並用 MediaRecorder 把「裝置來源」(ex: 麥克風/攝影機)的聲音錄下來。

今天把場景從「裝置」換成「顯示面」:用 getDisplayMedia 讓使用者選擇 全螢幕/視窗/分頁,再交給 MediaRecorder 錄製成影片。換句話說,我們要做的就是——「錄製我的螢幕」(畫面+可用時的來源音)。 💻 ⏺️ 🎙️


為什麼這兩個要放一起?

  • getDisplayMedia:讓使用者從「整個螢幕、單一視窗或單一分頁」中選一個顯示面,並回傳一條 MediaStream
    這條串流一定包含畫面(video track),而是否包含音訊(audio track) 則視瀏覽器與來源而定(例如「分頁」常可勾選分享分頁音訊;「整個螢幕」未必能拿到系統音)。

  • MediaRecorder:把任何 MediaStream 編碼成一段段 Blob chunks,最後組成檔案(可下載或上傳)。

  • 兩者配合:用 getDisplayMedia() 產生的串流 → 交給 MediaRecorder → 就得到「錄製我的螢幕」!🎉


Web API

getDisplayMedia(擷取螢幕)

// 取得螢幕串流(一定有 video;是否有 audio 取決於來源/瀏覽器)
const displayStream = await navigator.mediaDevices.getDisplayMedia({
  // 目標幀率(每秒影格數)為 30。全程顯示游標
  video: { frameRate: 30, cursor: 'always' },
  // 視瀏覽器與選擇來源而定,可能拿到分頁音訊或無音訊
  audio: true,
});

// 使用者在瀏覽器 UI 中按「停止共用」→ video track 會觸發 ended
const [vTrack] = displayStream.getVideoTracks();
vTrack.addEventListener('ended', () => {
  stopShare();
});

// 程式主動關閉分享(唯一做法:把所有 track 停掉)
function stopShare() {
  // 停止 video/audio,釋放資源
  displayStream.getTracks().forEach(t => t.stop());
  preview.srcObject = null;
}

  • 必須由使用者主動點選來觸發(例如 click)。
  • 使用者會看到系統的來源選擇面板(整個螢幕 / 視窗 / 分頁)。
  • 若來源被使用者在瀏覽器 UI 中「停止共用」,video track 會觸發 ended

getDisplayMedia
getDisplayMedia停止共用

MediaRecorder(把串流錄成 Blob)

// 建立錄影器,告訴瀏覽器用什麼容器/編碼輸出
const recorder = new MediaRecorder(stream, { mimeType: someMime });

// 每次可用資料就會觸發一次 dataavailable
recorder.ondataavailable = (e) => chunks.push(e.data);

// onstop 會在 recorder.stop() 之後被呼叫
recorder.onstop = () => {
  const blob = new Blob(chunks, { type: recorder.mimeType });
  const url = URL.createObjectURL(blob);
  // 下載 / 上傳 / 預覽
};

// 每秒丟一個 dataavailable,分段輸出
recorder.start(1000);
  • mimeType 需以 MediaRecorder.isTypeSupported() 探測,常見優先序:

    • video/webm;codecs=vp9,opus
    • video/webm;codecs=vp8,opus
    • video/webm
    • video/mp4(部分瀏覽器)
  • start(timeslice) 會週期性觸發 dataavailable;有些瀏覽器只在 stop() 時吐最後一包。


快速上手:錄製螢幕並下載 WebM

最小可用範例,能錄畫面,若瀏覽器允許也會帶到來源音訊。

<button id="btnStart">Start</button>
<button id="btnStop" disabled>Stop</button>
<video id="preview" playsinline muted autoplay></video>

<script type="module">
  const $ = s => document.querySelector(s);

  // 介面元素:開始/停止按鈕與預覽視窗
  const btnStart = $('#btnStart');
  const btnStop  = $('#btnStop');
  const video    = $('#preview');

  // 錄製相關的狀態:MediaRecorder、資料片段、螢幕串流
  let rec, chunks = [], stream;

  // 點「Start」→ 取得螢幕串流並開始錄製
  btnStart.onclick = async () => {
    btnStart.disabled = true;           // 避免重複點擊
    stream = await navigator.mediaDevices.getDisplayMedia({
      video: true,                      // 讓瀏覽器挑可行的解析度/幀率
      audio: true                       // 若來源支援(如分頁)則帶到來源音
    });

    video.srcObject = stream;           // 讓使用者看到自己正在分享的畫面

    chunks = [];                        // 每次開始錄都重置片段
    rec = new MediaRecorder(stream);    // 讓瀏覽器自動挑選可用的 mimeType
    rec.ondataavailable = (e) => {
      if (e.data.size) chunks.push(e.data); // 收集每段資料(有些瀏覽器只在 stop 時吐最後一段)
    };
    rec.onstop = () => {
      // 組成最終 Blob 並下載
      const blob = new Blob(chunks, { type: rec.mimeType || 'video/webm' });
      const url  = URL.createObjectURL(blob);
      const a    = document.createElement('a');
      a.href = url;
      a.download = `screen-${Date.now()}.webm`; // 用時間戳命名
      a.click();
      setTimeout(() => URL.revokeObjectURL(url), 10_000); // 稍後回收 URL
    };

    rec.start();                        // 最簡:不分段(timeslice),錄到 stop 為止
    btnStop.disabled = false;           // 啟用「Stop」
  };

  // 點「Stop」→ 停止錄製與來源,清理預覽與按鈕狀態
  btnStop.onclick = () => {
    rec?.stop();                        // 觸發 onstop → 產生檔案
    stream?.getTracks().forEach(t => t.stop()); // 停止螢幕分享(視訊/音訊軌)
    video.srcObject = null;             // 關閉預覽
    btnStart.disabled = false;
    btnStop.disabled  = true;
  };
</script>

進階:合併「分頁/系統音」與「麥克風」

多數瀏覽器在 MediaRecorder 只會採用 一條音軌。若你同時想錄到 螢幕來源音(例如分頁播放的影片聲)+ 麥克風旁白,最穩的方式是 用 Web Audio 把兩條音源混成一條,再與影片軌合流。

<!-- 最小可用:單鍵「螢幕 + 麥克風」混音錄影 -->
<button id="btn">Record Screen + Mic</button>
<video id="v" playsinline muted autoplay></video>
<a id="dl"></a>

<script type="module">
  const btn = document.getElementById('btn');
  const v   = document.getElementById('v');
  const dl  = document.getElementById('dl');

  // 這些會在開始/停止之間被重複使用
  let rec, chunks = [];
  let displayStream, micStream, mixedStream;
  let audioCtx, destNode;

  btn.onclick = async () => {
    // 若未錄或已停止 → 開始;否則 → 停止
    if (!rec || rec.state === 'inactive') {
      // 1) 取得螢幕來源(可能含分頁/系統音,視瀏覽器/來源而定)
      displayStream = await navigator.mediaDevices.getDisplayMedia({
        video: true,
        audio: true
      });

      // 2) 取得麥克風(開常見的語音處理有助降噪/回授)
      micStream = await navigator.mediaDevices.getUserMedia({
        audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }
      });

      // 預覽畫面(靜音避免回授)
      v.srcObject = displayStream;

      // 3) 用 Web Audio 把「來源音」與「麥克風」混成**單一音軌**
      const AC = window.AudioContext || window.webkitAudioContext;
      audioCtx = new AC();
      destNode = audioCtx.createMediaStreamDestination();

      const add = (track) => {
        if (!track) return; // 有些情況螢幕來源可能沒有音軌
        const src = audioCtx.createMediaStreamSource(new MediaStream([track]));
        src.connect(destNode);
      };
      add(displayStream.getAudioTracks()[0]);
      add(micStream.getAudioTracks()[0]);

      // 4) 合成「影片軌 + 單一混音軌」為新的串流
      mixedStream = new MediaStream([
        ...displayStream.getVideoTracks(),
        ...destNode.stream.getAudioTracks()
      ]);

      // 5) 交給 MediaRecorder 錄製(最簡:不分段)
      chunks = [];
      rec = new MediaRecorder(mixedStream); // 讓瀏覽器自選可用格式
      rec.ondataavailable = (e) => e.data.size && chunks.push(e.data);
      rec.onstop = () => {
        // 組檔並提供下載
        const type = rec.mimeType || (chunks[0] && chunks[0].type) || 'video/webm';
        const blob = new Blob(chunks, { type });
        const url  = URL.createObjectURL(blob);
        dl.href = url;
        dl.download = `screen-mix-${Date.now()}.webm`;
        dl.textContent = '下載錄影檔';
        // 稍後回收 URL
        setTimeout(() => URL.revokeObjectURL(url), 60_000);
        // 完整清理
        cleanup();
      };

      // 使用者若在瀏覽器 UI 停止分享,也要跟著停止錄影
      const [vTrack] = mixedStream.getVideoTracks();
      vTrack.addEventListener('ended', () => {
        if (rec && rec.state !== 'inactive') rec.stop();
      });

      rec.start();
      btn.textContent = 'Stop';
    } else {
      // 結束錄製(會觸發 onstop → 組檔下載)
      rec.stop();
    }
  };

  function cleanup() {
    displayStream?.getTracks().forEach(t => t.stop());
    micStream?.getTracks().forEach(t => t.stop());
    v.srcObject = null;

    // 關掉音訊處理
    try { audioCtx?.close(); } catch {}
    audioCtx = null; destNode = null; mixedStream = null;
    displayStream = null; micStream = null; rec = null; chunks = [];

    btn.textContent = 'Record Screen + Mic';
  }
</script>

注意

  • getDisplayMedia 是否含「來源音」取決於瀏覽器與你選的來源(整個螢幕 / 視窗 / 分頁)。「分頁」通常較容易錄到分頁音;「整個螢幕 / 視窗」不一定能拿到「系統音」。
  • macOS 上錄「系統音」有其限制;不同瀏覽器策略不同。

注意事項

  • 必須由使用者主動點選來觸發:非使用者點擊直接呼叫 getDisplayMedia 會被擋。
  • 麥克風回授:預覽 <video> 建議 muted,避免「螢幕分頁音 → 預覽播放 → 再被抓到」造成回授。
  • MIME 與容器:用 MediaRecorder.isTypeSupported() 探測;WebM 是網頁上最常見的輸出。部分瀏覽器支援 MP4(H.264/AAC),但跨瀏覽器一致性較差。
  • timeslice 行為:有的瀏覽器會「週期吐 chunk」,有的主要在 stop() 時才給最後一塊。
  • 音軌數量:多數情況 MediaRecorder 只吃到第一條音軌;因此錄「來源音 + 麥克風」要先用 Web Audio 合併。
  • 檔案大小:WebM(vp8/9 + opus)壓縮率不錯,但長時間錄影仍可能很大;建議分段存檔或上傳。

我該用哪個組合?(對照 Day 10)

快速對照
Day 10 錄的是「裝置」(ex: mic/cam,getUserMedia);Day 11 錄的是「顯示面」(整個螢幕 / 視窗 / 分頁,getDisplayMedia)。
兩者都把拿到的 MediaStream 交給 MediaRecorder,就能變成檔案。

  • 只錄音(語音備忘/Podcast 片段) 🎙️
    getUserMedia({ audio: true })MediaRecorder
    最簡單、最穩。

  • 錄螢幕(可含來源音)(產品 Demo/投影片講解) 💻 ⏺️ 🎙️
    getDisplayMedia({ video: true, audio: true })MediaRecorder
    來源若是「分頁」通常較容易帶到分頁音。

  • 錄螢幕 + 麥克風旁白(教學影片/操作解說) 💻 ⏺️ 🎙️ 🎤
    getDisplayMedia(...) + getUserMedia({ audio: true })
    Web Audio 混成單一音軌MediaRecorder
    避免多音軌相容性差,跨瀏覽器更穩。

一句話總結:getUserMedia 給你裝置來源getDisplayMedia 給你螢幕來源;把「來源」交給 MediaRecorder,就得到檔案


範例 Demo

上面用簡單程式碼示範了「螢幕擷取 + 錄影」的基本流程。
想直接體驗完整互動,請看這個線上範例(本文截圖也來自這裡):

MediaDevices+MediaRecorder


👉 歡迎追蹤這個系列,我會從 Canvas 開始,一步步帶你認識更多 Web API 🎯


上一篇
Day 10 - 攝影機、麥克風全開!用 MediaDevices Web API 開鏡頭、分享螢幕、錄音
系列文
從 Canvas 到各式各樣的 Web API 之旅11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言