原生JS+RecordRTC庫實現攝像頭內容顯示錄像截圖等功能

開源項目名:SimpleCameraJS

ruikocon/SimpleCameraJS (github.com)

SimpleCameraJS (gitee.com)

使用RecordRTC.js配合原生JS實現的攝像頭代碼,實現了簡單的獲取/顯示分辨率,顯示圖像,錄像並保存到本地mp4,截圖並保存到本地jpg的功能。

RecordRTC是 WebRTC-Experiment 的一部分,用於實現攝像頭相關功能,這是RecordRTC的 官網 和 Github

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <title>SimpleCameraJS</title>
    <style>
      /* 簡單設置一下按鈕風格 */
      button {
        width: 120px;
        height: 45px;
        font-size: 15px;
        border-radius: 2em;
        transition: all 0.1s linear;
        box-shadow: 2px 2px 4px #c5c5c5, -2px -2px 4px #f3f3f3;
        border: 1px solid #e0e0e0;
      }

      button:not(:disabled):hover {
        box-shadow: 6px 6px 12px #c5c5c5, -6px -6px 12px #f3f3f3;
      }

      button:not(:disabled):active {
        transform: scale(0.95);
        box-shadow: none;
      }

      /* 以下css爲攝像頭顯示視頻界面周圍的邊框,仿造攝像機,更加美觀,同時在四角顯示分辨率,幀數和錄像時間等信息 */
      /* 參考代碼 https://blog.cubieserver.de/2020/how-to-create-a-video-camera-like-overlay-with-html-css/  */
      .overlay {
        --border-style: 2px solid white;
        --border-space: 20px;
        position: absolute;
        width: 1280px;
        height: 720px;
      }

      .overlay-helper {
        position: absolute;
        width: 100%;
        height: 100%;
      }

      .overlay-element {
        padding: 20px;
        width: 150px;
        height: 100px;
        position: absolute;
      }

      .overlay-text {
        font-size: 18px;
        color: white;

        text-shadow: 1px 1px 0 rgb(58, 58, 58), -1px 1px 0 rgb(58, 58, 58),
          1px -1px 0 rgb(58, 58, 58), -1px -1px 0 rgb(58, 58, 58);
      }

      .overlay .top-left {
        border-left: var(--border-style);
        border-top: var(--border-style);
        top: var(--border-space);
        left: var(--border-space);
        text-align: left;
      }

      .overlay .top-right {
        border-right: var(--border-style);
        border-top: var(--border-style);
        top: var(--border-space);
        right: var(--border-space);
        text-align: right;
      }

      .overlay .bottom-left {
        border-left: var(--border-style);
        border-bottom: var(--border-style);
        bottom: var(--border-space);
        left: var(--border-space);
        text-align: left;
      }

      .overlay .bottom-right {
        border-right: var(--border-style);
        border-bottom: var(--border-style);
        bottom: var(--border-space);
        right: var(--border-space);
        text-align: right;
      }

      #overlay-bottom-left-text {
        position: absolute;
        bottom: var(--border-space);
        left: var(--border-space);
      }

      #overlay-bottom-right-text {
        position: absolute;
        bottom: var(--border-space);
        right: var(--border-space);
      }

      .blink {
        animation: blink 0.5s linear infinite alternate;
      }

      .fade {
        animation: fade 2s;
        animation-timing-function: ease-in-out;
        animation-fill-mode: forwards;
      }

      @keyframes fade {
        to {
          opacity: 0;
        }
      }

      @keyframes blink {
        0% {
          opacity: 1;
        }

        100% {
          opacity: 0;
        }
      }
    </style>
  </head>
  <body>
    <div style="display: flex; width: 600px; justify-content: space-around">
      <button id="open" onclick="open_camera()">開啓攝像頭</button>
      <button id="record" onclick="record_act()" disabled="true">
        開始錄像
      </button>
      <!-- <button id="stop" onclick="record_stop()">停止錄像</button> -->
      <button id="photo" onclick="take_photo()" disabled="true">截圖</button>
      <!-- <button id="close" onclick="close_camera()">關閉攝像頭</button> -->
    </div>
    <br />
    <div id="content">
      <div class="overlay">
        <div class="overlay-helper">
          <div class="overlay-element top-left">
            <span
              id="overlay-top-left-text"
              class="overlay-text blink"
              style="display: none"
            >
              🔴 REC
            </span>
          </div>
          <div class="overlay-element top-right">
            <span
              id="overlay-top-right-text"
              class="overlay-text"
              style="display: none"
            >
              00:00
            </span>
          </div>
          <div class="overlay-element bottom-left">
            <span
              id="overlay-bottom-left-text"
              class="overlay-text"
              style="display: none"
            >
              1 FPS
            </span>
          </div>
          <div class="overlay-element bottom-right">
            <span
              id="overlay-bottom-right-text"
              class="overlay-text"
              style="display: none"
            >
              1x1
            </span>
          </div>
        </div>
        <!-- 這裏可以放一個logo 然後實現漸隱效果 -->
        <div
          style="
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
          "
          id="div_logo"
        >
          <img src="logo.png" height="140px" draggable="false" alt="LOGO" />
        </div>
      </div>
    </div>

    <video
      id="video"
      width="1280"
      height="720"
      autoplay
      style="border: 2px solid #c5c5c5"
    ></video>
    <!-- <script src="js/jquery-3.7.0.min.js"></script> -->
    <script src="RecordRTC.js"></script>
    <script>
      "use strict";
      var scale = 1;
      var recorder;
      var video = document.getElementById("video");
      var video_width = 1280;
      var video_height = 720;
      var video_fps = 60;
      //var img = document.getElementById("img");

      var timer = null;
      var start_time;
      var now_time;


      //默認編碼
      let mimeType = "video/webm\;codecs=h265"; // H26四,把5改成4 因爲有審覈發不出來
      let recorderType = MediaStreamRecorder;
      //加入對支持的編碼格式進行測試的代碼
      isMimeTypeSupported("video/webm\;codecs=h265"); //H26四,把5改成4 因爲有審覈發不出來
      isMimeTypeSupported("video/x-matroska;codecs=avc1"); //MKV
      isMimeTypeSupported("video/webm\;codecs=vp9"); //VP9
      isMimeTypeSupported("video/webm\;codecs=vp8"); //VP8
      isMimeTypeSupported("video/mpeg"); // video/mp4;codecs=avc1
      isMimeTypeSupported("video/mp4;codecs=avc1"); // video/mp4;codecs=avc1
      isMimeTypeSupported("video/webm"); // do NOT pass any codecs (vp8 by default)
      //isMimeTypeSupported("video/webm\;codecs=vp10"); // JUST TEST

      //自行切換編碼格式
      if (isMimeTypeSupported(mimeType) === false) {
        mimeType = "video/x-matroska;codecs=avc1"; // MKV

        if (isMimeTypeSupported(mimeType) === false) {
          mimeType = "video/webm\;codecs=vp9"; // VP9

          if (isMimeTypeSupported(mimeType) === false) {
            mimeType = "video/webm\;codecs=vp8"; // VP8

            if (isMimeTypeSupported(mimeType) === false) {
              mimeType = "video/webm"; // 不使用任何編碼器 (默認vp8)

              if (isMimeTypeSupported(mimeType) === false) {
                //切換到Whammy編碼器 (WebP+WebM)
                mimeType = "video/webm";
                recorderType = WhammyRecorder;
              }
            }
          }
        }
      }

      

      function isMimeTypeSupported(mimeType) {
        if (typeof MediaRecorder === "undefined") {
          console.log(mimeType, "is *** NOT *** supported.");
          return false;
        }

        if (typeof MediaRecorder.isTypeSupported !== "function") {
          console.log(mimeType, "is supported.");
          return true;
        }
        var ret = MediaRecorder.isTypeSupported(mimeType);
        if (ret === false) {
          console.log(mimeType, "is *** NOT *** supported.");
        } else {
          console.log(mimeType, "is supported.");
        }
        return ret;
      }

      //開啓攝像頭之後顯示底部信息
      function show_bottom() {
        document.getElementById("overlay-bottom-left-text").style.display = "";
        document.getElementById("overlay-bottom-right-text").style.display = "";
        document.getElementById("div_logo").classList.add("fade");
        document.getElementById(
          "overlay-bottom-right-text"
        ).innerText = `${video_width} x ${video_height}`;

        document.getElementById(
          "overlay-bottom-left-text"
        ).innerText = `${video_fps} FPS`;
      }

      //開始爲錄像時間計時
      function start_count() {
        document.getElementById("overlay-top-left-text").style.display = "";
        document.getElementById("overlay-top-right-text").style.display = "";
        document
          .querySelector(".overlay")
          .style.setProperty("--border-style", "2px solid red");

        //計時器並不準確,所以使用內置時鐘的當前時間減去之前時間的差值來表示錄像時間
        let dt = new Date();
        start_time = dt.getTime();
        timer = setInterval(function () {
          let d = new Date();
          let now_time = d.getTime();
          let diff_time = now_time - start_time;
          let mini_sec = Math.floor((diff_time % 1000) / 10)
            .toString()
            .padStart(2, "0"); //秒數後面兩位
          let sec = Math.floor((diff_time % 60000) / 1000)
            .toString()
            .padStart(2, "0"); //秒數
          let minute = Math.floor((diff_time % 360000) / 60000)
            .toString()
            .padStart(2, "0"); //分鐘數
          document.getElementById(
            "overlay-top-right-text"
          ).innerText = `${minute}:${sec}:${mini_sec}`;
        }, 10);
      }

      //停止計時
      function stop_count() {
        document.getElementById("overlay-top-left-text").style.display = "none";
        document.getElementById("overlay-top-right-text").style.display =
          "none";
        document
          .querySelector(".overlay")
          .style.setProperty("--border-style", "2px solid white");
        clearInterval(timer);
      }

      async function open_camera() {
        //錄像功能使用RecordRTC https://github.com/muaz-khan/RecordRTC
        //官網    https://recordrtc.org/
        //RecordRTC是WebRTC-Experiment的一部分   https://github.com/muaz-khan/WebRTC-Experiment/tree/master

        //獲取分辨率部分代碼參考   https://usefulangle.com/post/355/javascript-get-camera-resolution
        //沒有直接的方法獲取相機支持的最大分辨率。所以只能指定一個最大的理想值,如果可用則設爲理想值
        //如果理想值不可用,則瀏覽器將使用最接近理想值的值。

        //WebRTC獲取相機分辨率的一個實例代碼   https://webrtc.github.io/samples/src/content/getusermedia/resolution/   *****值得參考!!!****
        navigator.mediaDevices
          .getUserMedia({
            audio: true,
            video: {
              width: { ideal: 3840 },
              height: { ideal: 2160 },
              frameRate: { ideal: 60 },
            },
          })
          .then(async function (stream) {
            let settings = stream.getVideoTracks()[0].getSettings();

            video_width = settings.width;
            video_height = settings.height;
            video_fps = settings.frameRate;

            //console.log("視頻實際寬度: " + width + "px");
            //console.log("視頻實際高度: " + height + "px");
            //console.log("視頻實際FPS: " + video_fps);

            //設置video 寬度 高度 和 視頻流
            video.width = video_width;
            video.height = video_height;
            video.srcObject = stream;
            recorder = RecordRTC(stream, {
              type: "video",
              mimeType: "video/mp4",
              recorderType: recorderType,
            });

            show_bottom();
            //禁用open按鈕,啓用錄像和截圖按鈕
            document.getElementById("record").disabled = false;
            document.getElementById("photo").disabled = false;
            document.getElementById("open").disabled = true;
          })
          .catch(function (e) {
            console.log(e);
            alert("分辨率無效,請重新選擇並開啓攝像頭");
          });
      }

      function close_camera() {
        video.srcObject = null;
      }

      function record_act() {
        console.log(recorder.state);
        if (recorder.state == "inactive" || recorder.state == "stopped") {
          document.getElementById("record").innerHTML = "停止錄像";
          record_start();
        } else if (recorder.state == "recording") {
          record_stop();
          document.getElementById("record").innerHTML = "開始錄像";
        }
      }

      function record_start() {
        recorder.startRecording();
        start_count();
      }

      //停止錄像按鈕事件
      function record_stop() {
        recorder.stopRecording(function () {
          let blob = recorder.getBlob();
          stop_count();
          //保存mp4,有個問題,現在保存的mp4只能在瀏覽器裏打開,有空再研究
          invokeSaveAsDialog(blob, "video.mp4");
        });
      }

      function take_photo() {
        var canvas = document.createElement("canvas");
        canvas.width = video.videoWidth * scale;
        canvas.height = video.videoHeight * scale;
        canvas
          .getContext("2d")
          .drawImage(video, 0, 0, canvas.width, canvas.height);
        //將圖片保存到本地
        downloadBaseFile(canvas.toDataURL("image/jpeg"), "img.jpg");
      }

      //下載圖片到本地
      function downloadBaseFile(contentBase, fileName) {
        const linkSource = contentBase;
        const downloadLink = document.createElement("a");
        document.body.appendChild(downloadLink);

        downloadLink.href = linkSource;
        downloadLink.target = "_self";
        downloadLink.download = fileName;
        downloadLink.click();
        downloadLink.remove();
      }

    </script>
  </body>
</html>

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章