基於螢石雲實現的九宮格影片監控效果

孔小爽發表於2024-04-25

螢石雲九宮格監控實現流程
說在最前面
將海康錄影機新增到螢石雲控制檯
開始進行開發
程式碼中所用介面
獲取accessToken
獲取裝置列表
獲取攝像頭(錄影機的通道)列表
獲取當前攝像頭的監控地址
實現完整程式碼
展示效果(出於隱私不顯示影片)
額外總結
1、上、下、左、右、放大、縮小是用來操作球機或者可以進行操作的攝像機,截圖和全屏顯示功能均可使用;
2、獲取錄影機下的通道列表,每一條資訊中的`status`為`1`時,代表此通道有攝像機,為`-1`時,代表此通道未連線攝像機;
3、`ipcSerial`欄位的說明
4、上面程式碼說明

說在最前面

本筆記使用的攝像頭為海康攝像頭,攝像頭連線的是海康錄影機。小於等於9個攝像機可以直接使用。

將海康錄影機新增到螢石雲控制檯

    • 在螢石雲開放平臺建立螢石雲賬號(註冊並登入)
      螢石雲開放平臺連結
    • 進入控制檯並進行將裝置進行繫結
        1. 進入控制檯

        2. 進行認證,個人身份許可權較少,企業的比個人的認證要方便一些 。

        3. 建立應用,建立應用之後就可以看到呼叫介面所需要的引數,我這裡建立的是web應用

        4. 其中後續呼叫介面所需要的引數appKey和appSecret分別為應用秘鑰模組中的AppKey和Secret,經常還需要一個accessToken,需要呼叫介面獲取,最好不要用這裡的AccessToken,畢竟有一定的有效期(7天)。

      開始進行開發

        • 安裝並引入
          安裝:npm install ezuikit-js
          main.js中引入:
          import  EZUIKit from 'ezuikit-js';
          Vue.use(EZUIKit );
        • 螢石雲介面基址:https://open.ys7.com/
        • 螢石雲的請求引數都是formData型資料
        程式碼中所用介面
    • 獲取accessToken
          url: '/api/lapp/token/get'
          type: 'post'
          data: {
              appKey: 應用資訊頁面的appKey,
              appSecret: 應用資訊頁面的appSecret
          }
    • 獲取裝置列表
          url: '/api/lapp/device/list'
          type: 'post'
          data: {
              accessToken: 獲取到的accessToken,
              pageSize: 每頁多少條資料,
              pageStart: 開始頁數
          }
      
      
    • 獲取攝像頭(錄影機的通道)列表
          url: '/api/lapp/device/camera/list'
          type: 'post'
          data: {
              accessToken: 獲取到的accessToken,
              deviceSerial: 裝置的序列號
          }
      
      
    • 獲取當前攝像頭的監控地址
          url: '/api/lapp/v2/live/address/get'
          type: 'post'
          data: {
              accessToken: 獲取到的accessToken,
              deviceSerial: 裝置的序列號,
              channelNo: 當前攝像頭在錄影機中的通道號
          }

      實現完整程式碼

      基於螢石雲實現的九宮格影片監控效果
      <template>
        <div class="main">
          <div class="app-container">
            <!-- 左邊影片視窗 -->
            <div class="left" id="divPlugin">
              <div class="hello-ezuikit-js" ref="videoBox">
                <!-- 最多9格 -->
                <div
                  v-for="item in 9"
                  v-show="
                    (select == 1 && selectVideoFirst == item) ||
                      (select == 2 &&
                        item >= selectVideoFirst &&
                        item < selectVideoFirst + 4) ||
                      select == 3
                  "
                  :key="item"
                  :class="select == 1 ? 'width' : select == 2 ? 'width2' : 'width3'"
                  style="position:relative;"
                >
                  <!-- 最多16格 -->
                  <!-- <div
                  v-for="item in 16"
                  v-show="
                    (select == 1 && selectVideoFirst == item) ||
                      (select == 2 &&
                        item >= selectVideoFirst &&
                        item < selectVideoFirst + 4) ||
                      (select == 3 &&
                        item >= selectVideoFirst &&
                        item < selectVideoFirst + 9) ||
                      select == 4
                  "
                  :key="item"
                  :class="
                    select == 1
                      ? 'width'
                      : select == 2
                      ? 'width2'
                      : select == 3
                      ? 'width3'
                      : 'width4'
                  "
                  style="position:relative;"
                > -->
                  <div
                    :id="'video-cover' + item"
                    class="video-cover"
                    :class="{
                      'video-active': selectVideo == item
                    }"
                  ></div>
                  <div :id="'video-container' + item"></div>
                </div>
              </div>
            </div>
            <!-- 右邊操作區 -->
            <div class="right">
              <el-input
                style="width:15.625rem;position: relative;left:1.875rem"
                placeholder="請輸入裝置名稱"
                prefix-icon="el-icon-search"
                v-model="search"
                clearable
              ></el-input>
              <div
                v-if="searchList.length"
                style="width:91%;position: relative;left:1.25rem;height:50%;overflow:auto;top:.3125rem"
              >
                <div
                  v-for="(camera, index) in searchList"
                  @click="selectCamera2(camera)"
                  :key="index"
                  :style="
                    cameraList[selectVideo - 1].ipcSerial == camera.ipcSerial
                      ? 'color:#0079e0'
                      : ''
                  "
                  style="width: 100%;height: 2rem;cursor: pointer"
                >
                  <span
                    v-if="camera.status"
                    style="width:.5rem;height:.5rem;borderRadius:50%;background:#0cdc8c;display:inline-block;margin-right:.9375rem"
                  ></span>
                  <span
                    v-else
                    style="width:.5rem;height:.5rem;borderRadius:50%;background:#aaa;display:inline-block;margin-right:.9375rem"
                  ></span>
                  {{ camera.channelName }}
                </div>
              </div>
              <div
                v-else
                style="width:91%;position: relative;left:1.25rem;height:50%;overflow:auto;top:.3125rem"
              >
                <div
                  v-for="(camera, index) in cameraList"
                  @click="selectCamera(camera, index)"
                  :key="index"
                  :style="
                    cameraList[selectVideo - 1].ipcSerial == camera.ipcSerial
                      ? 'color:#0079e0'
                      : ''
                  "
                  style="width: 100%;height: 2rem;cursor: pointer"
                >
                  <span
                    v-if="camera.status"
                    style="width:.5rem;height:.5rem;borderRadius:50%;background:#0cdc8c;display:inline-block;margin-right:.9375rem"
                  ></span>
                  <span
                    v-else
                    style="width:.5rem;height:.5rem;borderRadius:50%;background:#aaa;display:inline-block;margin-right:.9375rem"
                  ></span>
                  {{ camera.channelName }}
                </div>
              </div>
              <div class="btns">
                <div class="wheel">
                  <div @click="deviceCapture" class="camera">
                    <i class="el-icon-camera"></i>
                  </div>
                  <div class="top">
                    <div
                      @click="startPTZCtrl('0')"
                      class="triangle triangle-top"
                    ></div>
                  </div>
                  <div class="center">
                    <div class="center-left">
                      <div
                        @click="startPTZCtrl('2')"
                        class="triangle triangle-left"
                      ></div>
                    </div>
                    <div class="center-right">
                      <div
                        @click="startPTZCtrl('3')"
                        class="triangle triangle-right"
                      ></div>
                    </div>
                  </div>
                  <div class="bottom">
                    <div
                      @click="startPTZCtrl('1')"
                      class="triangle triangle-bottom"
                    ></div>
                  </div>
                </div>
                <div class="two-btn">
                  <el-button @click="startPTZCtrl('9')" size="mini" type="primary"
                    >-</el-button
                  >
                  <el-button @click="startPTZCtrl('8')" size="mini" type="primary"
                    >+</el-button
                  >
                </div>
                <el-button
                  class="right-btn"
                  @click="showAllScreen"
                  size="small"
                  type="primary"
                  >全屏顯示</el-button
                >
              </div>
            </div>
          </div>
          <!-- 底部切屏按鈕 -->
          <div
            @click="select = 1"
            style="position: absolute;left:1.875rem;top:94.5vh;cursor: pointer;"
          >
            <img
              class="rect"
              v-if="select == 1"
              src="../../assets/images1/one_1.png"
              alt
            />
            <img class="rect" v-else src="../../assets/images1/one.png" alt />
          </div>
          <div
            @click="select = 2"
            style="position: absolute;left:4rem;top:94.5vh;cursor: pointer;"
          >
            <img
              class="rect"
              v-if="select == 2"
              src="../../assets/images1/four_1.png"
              alt
            />
            <img class="rect" v-else src="../../assets/images1/four.png" alt />
          </div>
          <div
            @click="select = 3"
            style="position: absolute;left:6.125rem;top:94.5vh;cursor: pointer;"
          >
            <img
              class="rect"
              v-if="select == 3"
              src="../../assets/images1/nine_1.png"
              alt
            />
            <img class="rect" v-else src="../../assets/images1/nine.png" alt />
          </div>
          <!-- <div
            @click="select = 4"
            style="position: absolute;left:8.25rem;top:94.5vh;cursor: pointer;"
          >
            <img
              class="rect"
              v-if="select == 4"
              src="../../assets/images1/nine_1.png"
              alt
            />
            <img class="rect" v-else src="../../assets/images1/nine.png" alt />
          </div> -->
        </div>
      </template>
      <script>
      import EZUIKit from "ezuikit-js";
      import axios from "axios";
      axios.defaults.baseURL = "/yingshiyun";
      export default {
        name: "Project",
        data() {
          return {
            count: 0,
            selectPlayer: "", // 選中的監控
            deviceList: [], // 錄影機個數
            cameraList: [], // 攝像頭個數
            selectChannelNo: 1, // 選中的通道號
            select: 1, // 選中的網格數
            accessToken: "", // 用appKey和APPSecret請求回來的token
            selectVideo: 1, // 當前選中的video序號
            selectVideoFirst: 1,
            search: "", // 搜尋框
            searchList: []
          };
        },
        created() {},
        async mounted() {
          // 獲得螢石雲的token
          this.getDeviceToken();
        },
        watch: {
          // 選擇顯示video數量
          select(value) {
            this.select = value;
            this.cameraList.forEach((item, index) => {
              if (value == 1) {
                if (item.code == 200)
                  item.player.reSize(
                    this.$refs.videoBox.offsetWidth,
                    this.$refs.videoBox.offsetHeight
                  );
              } else if (value == 2) {
                if (item.code == 200)
                  item.player.reSize(
                    this.$refs.videoBox.offsetWidth / 2 - 2,
                    this.$refs.videoBox.offsetHeight / 2 - 2
                  );
              } else if (value == 3) {
                if (item.code == 200)
                  item.player.reSize(
                    this.$refs.videoBox.offsetWidth / 3,
                    this.$refs.videoBox.offsetHeight / 3
                  );
              }
              // else {
              //   if (item.code == 200)
              //     item.player.reSize(
              //       this.$refs.videoBox.offsetWidth / 4,
              //       this.$refs.videoBox.offsetHeight / 4
              //     );
              // }
            });
          },
          search(value) {
            if (value) {
              this.searchList = this.cameraList.filter(
                item => item.channelName.indexOf(value) > -1
              );
            } else {
              this.searchList = [];
            }
          }
        },
        beforeDestroy() {
          this.players.forEach(item => {
            item.stop();
          });
        },
        methods: {
          // 獲取token
          async getDeviceToken() {
            const data = new FormData();
            data.append("appKey", "螢石雲賬戶的appKey");
            data.append("appSecret", "螢石雲賬戶的appSecret");
            var res = await axios({
              headers: {
                "Content-Type": "application/x-www-form-urlencoded"
              },
              method: "post",
              url: "/api/lapp/token/get",
              data: data
            });
            if (res.data.code == 200) {
              this.accessToken = res.data.data.accessToken;
              // TODO:deviceList(裝置列表)需要利用獲取裝置列表介面獲取
              if (this.deviceList.length) {
                this.deviceList.forEach(item => {
                  this.getChannelList(item);
                });
              }
            }
          },
          // 獲取攝像頭(通道)列表
          async getChannelList(device) {
            const data = new FormData();
            data.append("accessToken", this.accessToken);
            data.append("deviceSerial", device.deviceSerial);
            var res = await axios({
              headers: {
                "Content-Type": "application/x-www-form-urlencoded"
              },
              method: "post",
              url: "/api/lapp/device/camera/list",
              data: data
            });
            this.count++;
            if (res.data.code == 200) {
              var canUseList = [];
              canUseList = res.data.data.filter(
                item => item.deviceSerial != item.ipcSerial
              );
              this.cameraList = [...this.cameraList, ...canUseList];
              if (this.count >= this.deviceList.length) {
                this.cameraList.forEach((item, index) => {
                  this.getEzuikitUrl(item, index);
                });
                this.selectPlayer = this.cameraList[0];
              }
            }
          },
          // 獲取監控地址
          async getEzuikitUrl(item, index, select) {
            const data = new FormData();
            data.append("accessToken", this.accessToken);
            data.append("deviceSerial", item.deviceSerial);
            data.append("channelNo", item.channelNo);
            var res = await axios({
              headers: {
                "Content-Type": "application/x-www-form-urlencoded"
              },
              url: "/api/lapp/v2/live/address/get",
              method: "post",
              data: data
            });
            if (res.data.code == 200) {
              var url = res.data.data.url;
              item.url = url;
              item.code = 200;
              item.msg = res.data.msg;
              // 渲染影片播放
              this.StructureEZUIKitPlayer(url, item, index, select);
            } else {
              var ref = document.querySelector("#video-cover" + (index + 1));
              ref.innerText = res.data.msg;
            }
            this.$set(this.cameraList, index, item);
          },
          // 渲染影片播放
          StructureEZUIKitPlayer(url, item, index, select) {
            if (select) {
              var player = new EZUIKit.EZUIKitPlayer({
                autoplay: false,
                audio: "0",
                id: "video-container", // 影片容器ID
                accessToken: this.accessToken,
                url: url, // 初始化寫死一個離線或者找不到的裝置,避免初始化無法建立播放器;
                template: "simple",
                width: this.$refs.videoBox.offsetWidth / 3,
                height: this.$refs.videoBox.offsetHeight / 3
              });
              this.selectPlayer.player = player;
            } else {
              var player = new EZUIKit.EZUIKitPlayer({
                autoplay: false,
                audio: "0",
                id: `video-container${index + 1}`, // 影片容器ID
                accessToken: this.accessToken,
                url: url, // 初始化寫死一個離線或者找不到的裝置,避免初始化無法建立播放器;
                template: "simple",
                width: this.$refs.videoBox.offsetWidth / 3,
                height: this.$refs.videoBox.offsetHeight / 3
              });
              item.player = player;
            }
            this.select = 3;
          },
          // 開始雲臺控制
          async startPTZCtrl(direction) {
            // 放大縮小:8 放大 9 縮小
            // 方向:0-上,1-下,2-左,3-右,4-左上,5-左下,6-右上,7-右下
            const data = new FormData();
            data.append("accessToken", this.accessToken);
            data.append("deviceSerial", this.selectPlayer.deviceSerial);
            data.append("channelNo", this.selectPlayer.channelNo);
            data.append("direction", direction);
            data.append("speed", "1");
            var res = await axios({
              headers: {
                "Content-Type": "application/x-www-form-urlencoded"
              },
              method: "post",
              url: "/api/lapp/device/ptz/start",
              data: data
            });
            if (res.data.code != 200) {
              this.$message(res.data.msg);
            } else {
              this.stopPTZCtrl(direction);
            }
          },
          // 停止雲臺控制
          async stopPTZCtrl() {
            const data = new FormData();
            data.append("accessToken", this.accessToken);
            data.append("deviceSerial", this.selectPlayer.deviceSerial);
            data.append("channelNo", this.selectPlayer.channelNo);
            data.append("direction", "0");
            var res = await axios({
              headers: {
                "Content-Type": "application/x-www-form-urlencoded"
              },
              method: "post",
              url: "/api/lapp/device/ptz/stop",
              data: data
            });
            this.$message(res.data.msg);
          },
          // 裝置抓拍圖片
          async deviceCapture() {
            this.cameraList[this.selectVideo - 1].player.capturePicture();
          },
          // 全屏顯示
          showAllScreen() {
            this.cameraList[this.selectVideo - 1].player.cancelFullScreen();
            this.cameraList[this.selectVideo - 1].player.fullScreen();
          },
          selectCamera(item, index) {
            this.selectPlayer = item;
            this.selectVideo = index + 1;
            this.selectVideoFirst = index + 1;
          },
          selectCamera2(camera) {
            var index = this.cameraList.findIndex(
              item => item.ipcSerial == camera.ipcSerial
            );
            this.selectPlayer = item;
            this.selectVideo = index + 1;
            this.selectVideoFirst = index + 1;
          }
        }
      };
      </script>
      <style lang="scss" scoped>
      .main {
        position: fixed;
        .app-container {
          width: 94.375rem;
          height: 71vh;
          // border: .125rem solid rgb(116, 228, 24);
          left: 1.875rem;
          background-color: rgb(255, 255, 255);
          margin: 0rem auto;
          position: relative;
          top: 13.125rem;
          // border-radius:.625rem;
          overflow: hidden;
          .left {
            overflow: hidden;
            width: 80%;
            border: 0.125rem solid rgb(226, 181, 33);
            height: 100%;
            position: absolute;
            left: 0;
            top: 0rem;
            .title {
              position: absolute;
              top: 1rem;
              left: 0.75rem;
              font-size: 1rem;
              font-weight: 600;
              color: #000;
            }
            .time {
              position: absolute;
              top: 1rem;
              right: 1.25rem;
              font-size: 1rem;
              font-weight: 600;
              color: #000;
            }
          }
          .left1 {
            width: 80%;
            border: 0.125rem solid rgb(37, 43, 102);
            height: 100%;
            top: 0rem;
      
            position: absolute;
            left: 0;
            overflow: hidden;
          }
          .left2 {
            top: 0rem;
            width: 80%;
            border: 0.125rem solid rgb(37, 43, 102);
            height: 100%;
            position: absolute;
            overflow: hidden;
            left: 0;
          }
          .right {
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            background: rgb(255, 255, 255);
            width: 20%;
            height: 100%;
            top: 0;
      
            // border: .0625rem solid rgb(22, 21, 27);
            position: absolute;
            right: 0;
            .right1 {
              width: 100%;
              text-align: start;
              line-height: 2.5rem;
              color: #6e727a;
              margin: 0.3125rem auto;
              height: 2.5rem;
              padding: 0 1.25rem;
              cursor: pointer;
            }
            .right1:hover {
              background: #1393fc;
              color: rgb(255, 255, 255);
              border: none;
            }
            .right2 {
              width: 100%;
              padding: 0 1.25rem;
              height: 2.5rem;
              margin: 0.3125rem auto;
              line-height: 2.5rem;
              background: #1393fc;
              color: rgb(255, 255, 255);
              cursor: pointer;
            }
          }
        }
      }
      
      .hello-ezuikit-js {
        width: 100%;
        height: 100%;
        display: flex;
        flex-wrap: wrap;
        overflow: hidden;
        background: #ccc;
      }
      
      .width {
        width: 100%;
        height: 100%;
      }
      
      .width2 {
        width: 50%;
        height: 50%;
      }
      
      .width3 {
        width: 33.3%;
        height: 33.3%;
      }
      
      .width4 {
        width: 25%;
        height: 25%;
      }
      
      .video-active {
        border: 0.125rem solid rgb(255, 133, 62) !important;
      }
      
      .rect {
        width: 1.625rem;
        height: 1.625rem;
      }
      
      .video-cover {
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 12px;
        color: rgb(153, 0, 0);
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        z-index: 5;
        border-top: 0.0313rem solid #fff;
        border-right: 0.0313rem solid #fff;
      }
      
      ::v-deep .el-dialog__wrapper {
        display: flex;
        justify-content: center;
        align-items: center;
      }
      
      ::v-deep .el-dialog__header {
        background: #efefef;
      }
      
      ::v-deep .el-dialog {
        width: 36rem;
      }
      
      ::v-deep .el-dialog__body {
        padding-top: 3.75rem;
        display: flex;
        justify-content: center;
      }
      
      ::v-deep .el-form {
        width: 28.125rem;
      }
      
      ::v-deep .el-form-item__label {
        width: 6.875rem !important;
      }
      
      ::v-deep .el-input {
        width: 20rem;
      }
      
      .tabs {
        width: 100%;
        height: 2.4375rem;
        display: flex;
        position: absolute;
        top: 3.375rem;
        left: 0;
        border-top: 1px solid #ccc;
      
        .tab-item {
          width: 50%;
          height: 100%;
          display: flex;
          justify-content: center;
          align-items: center;
        }
      
        .tab-active {
          background: #ccc;
          color: #fff;
        }
      }
      
      .wheel {
        position: relative;
        width: 9.375rem;
        height: 9.375rem;
        border-radius: 50%;
        background: rgb(77, 77, 77);
      
        .camera {
          position: absolute;
          left: 3.75rem;
          top: 3.75rem;
          z-index: 5;
          width: 1.875rem;
          height: 1.875rem;
          text-align: center;
          line-height: 1.875rem;
          font-size: 1.25rem;
          color: #fff;
          cursor: pointer;
        }
      
        .top {
          height: 33.3%;
          display: flex;
          justify-content: center;
          align-items: center;
        }
        .center {
          height: 33.3%;
          display: flex;
          justify-content: space-between;
          align-items: center;
      
          .center-left,
          .center-right {
            width: 33.3%;
            display: flex;
            justify-content: center;
          }
        }
        .bottom {
          height: 33.3%;
          display: flex;
          justify-content: center;
          align-items: center;
        }
      
        .triangle {
          width: 0;
          height: 0;
          border: 0.625rem solid transparent;
          cursor: pointer;
        }
      
        .triangle-top {
          border-bottom: 0.9375rem solid #fff;
        }
      
        .triangle-bottom {
          border-top: 0.9375rem solid #fff;
        }
      
        .triangle-left {
          border-right: 0.9375rem solid #fff;
        }
      
        .triangle-right {
          border-left: 0.9375rem solid #fff;
        }
      }
      
      .bg-black {
        display: flex;
        justify-content: center;
        align-items: center;
        background: #000;
        color: rgb(151, 0, 0);
        font-size: 12px;
      }
      
      .btns {
        display: flex;
        flex-direction: column;
        align-items: center;
      
        .two-btn {
          width: 11.25rem;
          margin: 0.9375rem auto;
      
          .el-button {
            width: 50%;
            margin: 0;
          }
        }
      }
      
      ::v-deep .el-button {
        background: rgb(77, 77, 77);
        border-color: rgb(77, 77, 77);
      }
      
      ::v-deep .el-button.right-btn {
        width: 11.25rem;
        margin: 0 auto 0.625rem;
      }
      </style>
      View Code

      展示效果(出於隱私不顯示影片)

      額外總結
      1、上、下、左、右、放大、縮小是用來操作球機或者可以進行操作的攝像機,截圖和全屏顯示功能均可使用;
      2、獲取錄影機下的通道列表,每一條資訊中的status為1時,代表此通道有攝像機,為-1時,代表此通道未連線攝像機;
      3、ipcSerial欄位的說明
      利用裝置序列號獲取通道列表時:
      (1)status欄位為1時,且ipcSerial和deviceSerial相同時,代表當前裝置是直連的攝像頭,攝像頭的;
      (2)status欄位為-1時,且ipcSerial和deviceSerial相同時,代表此通道未連線攝像機,連線螢石雲的裝置為硬碟錄影機;

      4、上面程式碼說明
      (1)因為我的裝置使用介面新增的,裝置列表資料庫存了一份,所以應用中沒有使用介面獲取裝置列表;
      (2)程式碼中判斷當前通道是否接入攝像頭是判斷ipcSerial和deviceSerial是否相等;如果螢石雲直接連的攝像頭,需要改變判斷方式;
      (3)程式碼中注掉的部分是16格影片,使用16格影片時需要解除註釋,渲染影片播放方法中的this.select=4,寬高比也應為/4。

相關文章