前端錄屏並儲存影片到本地

_clai發表於2024-04-12

API、第三方庫使用 getDisplayMedia + MediaRecorder + @ozean/set-webm-duration

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>前端錄屏並儲存影片到本地</title>
    <style>
      canvas,
      video {
        width: 640px;
        /* height: 300px; */
        aspect-ratio: 16 / 9;
        border: 1px solid black;
        box-sizing: border-box;
      }
    </style>
  </head>
  <body>
    <h3>前端錄屏並儲存影片到本地</h3>
    <button type="button" id="share-screen">開啟螢幕共享</button>
    <button type="button" hidden id="close-screen">關閉螢幕共享</button>
    <button type="button" hidden id="record-start">錄屏</button>
    <button type="button" hidden id="record-pause">暫停錄屏</button>
    <button type="button" hidden id="record-resume">繼續錄屏</button>
    <button type="button" hidden id="record-stop">停止錄屏</button>
    <button type="button" hidden id="record-download">下載錄屏</button>
    <hr />

    <script type="module">
      // 下載依賴 npm i @ozean/set-webm-duration
      import { setWebmDuration } from '../node_modules/@ozean/set-webm-duration/dist/set-webm-duration.esm.js';

      const shareScreen = document.getElementById('share-screen');
      const closeScreen = document.getElementById('close-screen');
      const recordStart = document.getElementById('record-start');
      const recordPause = document.getElementById('record-pause');
      const recordResume = document.getElementById('record-resume');
      const recordStop = document.getElementById('record-stop');
      const recordDownload = document.getElementById('record-download');

      const constraints = { video: true, audio: true };
      /** @type {MediaRecorder} */
      let recorder = null,
        videoDuration = 0;
      const chunks = [];

      shareScreen.addEventListener('click', shareScreenHandler);
      closeScreen.addEventListener('click', closeScreenHandler);

      recordStart.addEventListener('click', recordStartHandler);
      recordStop.addEventListener('click', recordStopHandler);
      recordPause.addEventListener('click', recordPauseHandler);
      recordResume.addEventListener('click', recordResumeHandler);

      recordDownload.addEventListener('click', downloadVideo);

      // 開啟錄屏
      function recordStartHandler() {
        if (!recorder) throw new Error('未啟動錄屏');

        recorder.start();
        recordStart.setAttribute('hidden', true);
        recordStop.removeAttribute('hidden');
        recordPause.removeAttribute('hidden');
        console.log('開始錄屏');
      }
      // 停止錄屏
      function recordStopHandler() {
        if (!recorder) throw new Error('未啟動錄屏');

        recorder.stop();
        recordStop.setAttribute('hidden', true);
        recordPause.setAttribute('hidden', true);
        recordResume.setAttribute('hidden', true);
        closeScreen.setAttribute('hidden', true);
        recordDownload.removeAttribute('hidden');
      }
      // 暫停錄屏
      function recordPauseHandler() {
        if (!recorder) throw new Error('未啟動錄屏');

        recorder.pause();
        recordPause.setAttribute('hidden', true);
        recordResume.removeAttribute('hidden');
      }
      // 繼續錄屏
      function recordResumeHandler() {
        if (!recorder) throw new Error('未啟動錄屏');

        recorder.resume();
        recordPause.removeAttribute('hidden');
        recordResume.setAttribute('hidden', true);
      }

      // 關閉螢幕共享
      let closed = false;
      function closeScreenHandler() {
        if (!recorder) throw new Error('螢幕共享未開啟');

        closed = true;

        // 共享螢幕,未開啟錄屏
        if (recorder.state === 'inactive') {
          recordStart.setAttribute('hidden', true);
          _close();
          return;
        }

        const result = confirm('關閉螢幕共享後,錄製的內容有可能丟失,確定要關閉螢幕共享嗎?');
        if (result) {
          recordStop.setAttribute('hidden', true);
          recordPause.setAttribute('hidden', true);
          recordResume.setAttribute('hidden', true);
          recordDownload.setAttribute('hidden', true);
          _close();
        }
      }

      function _close() {
        closeScreen.setAttribute('hidden', true);
        removeVideo();
        recorder.stream.getTracks().forEach((track) => track.stop());
        recorder = null;
      }

      function removeVideo() {
        [...document.querySelectorAll('video')].forEach((dom) => dom.remove());
      }

      // 共享螢幕
      async function shareScreenHandler() {
        try {
          removeVideo();
          closed = false;

          let stream = await navigator.mediaDevices.getDisplayMedia(constraints);

          let video = document.createElement('video');
          video.playsInline = true;
          video.autoplay = true;
          video.muted = true;
          video.id = 'share-video';
          document.body.appendChild(video);

          // 顯示共享螢幕內容
          video.srcObject = stream;

          closeScreen.removeAttribute('hidden');
          recordStart.removeAttribute('hidden');
          recordDownload.setAttribute('hidden', true);

          recorder = new MediaRecorder(stream, {
            mimeType: 'video/webm',
          });

          recorder.addEventListener('dataavailable', (event) => {
            chunks.push(event.data);
          });
          recorder.addEventListener('stop', () => {
            if (closed) return;

            const blob = new Blob(chunks, { type: 'video/webm' });
            const url = URL.createObjectURL(blob);
            console.log('錄製結束');

            const video2 = document.createElement('video');
            video2.width = parseInt(getComputedStyle(video).width);
            video2.height = parseInt(getComputedStyle(video).height);
            video.playsInline = true;
            video2.muted = true;
            video2.id = 'record-video';
            video2.loop = true;

            video2.addEventListener('durationchange', setVideoDuration.bind(video2));

            video2.src = url;
            video2.currentTime = 24 * 60 * 60;
            // video2.play();

            // 錄製結束,關閉錄屏、track
            video.srcObject = null;
            video.style.display = 'none';
            stream.getTracks().forEach((track) => track.stop());
            video = stream = recorder = null;

            document.body.appendChild(video2);
            // 繪製錄屏內容 (到 canvas)
            // drawVideo(video2);
          });

          // drawVideo(video);
          // window.addEventListener('resize', () => {
          //   document.getElementById('record-canvas').remove();
          //   drawVideo(video);
          // })
        } catch (err) {
          if (recorder) {
            recorder.stream.getTracks().forEach((track) => track.stop());
            recorder = null;
          }
          console.log(err.name + ' => ' + err.message);
        }
      }

      // 設定影片時長
      async function setVideoDuration() {
        const video2 = this;
        if (video2.duration !== Infinity) {
          video2.currentTime = 0;
          // console.log("video2.duration => ", video2.duration)
          videoDuration = video2.duration;
        }
      }

      // 下載錄屏
      async function downloadVideo() {
        if (chunks.length === 0) throw new Error('錄屏內容為空,請先錄製螢幕共享內容');

        console.log('正在解析影片中,馬上開始下載錄屏');

        // 寫入總時長
        const blob = new Blob(chunks, { type: 'video/webm' });
        const buffer = await blob.arrayBuffer();
        const newBuffer = setWebmDuration(buffer, videoDuration * 1000);
        const newBlob = new Blob([newBuffer]);
        const url = URL.createObjectURL(newBlob);

        const a = document.createElement('a');
        a.href = url;
        a.download = 'video.webm';
        a.click();
        URL.revokeObjectURL(url);
      }

      // 繪製錄屏內容
      function drawVideo(video) {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');

        const width = video.offsetWidth || video.style.width || video.width,
          height = video.offsetHeight || video.style.height || video.height;

        canvas.id = 'record-canvas';
        canvas.width = width * devicePixelRatio;
        canvas.height = height * devicePixelRatio;
        canvas.style.width = width + 'px';
        canvas.style.height = height + 'px';

        document.body.appendChild(canvas);

        requestAnimationFrame(function _draw() {
          ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
          requestAnimationFrame(_draw);
        });
      }
    </script>
  </body>
</html>

相關文章