阅读 288

前端播放视频

1.m3u8

这种格式前端video标签可以直接播放,也可以使用封装好的插件,

在线播放地址(mr158.cn/m3u8test/

vue项目demo

<template>
  <video ref="videoPlayer" class="video-js vjs-big-play-centered"></video>
</template>
<script>
import videoPlayer from 'video.js'
import 'video.js/dist/video-js.css'
export default {
  name: 'com-VideoPlayer',
  props: {
    sources: {
      type: Array,
      default() {
        return []
      },
    },
    options: {
      type: Object,
      default() {
        return {}
      },
    },
  },
  data() {
    return {
      player: null,
    }
  },
  watch: {
    sources(newValue) {
      const src = newValue && newValue.length > 0 ? newValue[0].src : ''
      if (src && this.player) {
        this.player.src(src)
        this.player.load()
      }
    },
  },
  mounted() {
    const defaultOptions = {
      autoplay: true,
      controls: true,
      preload: 'auto',
      muted: true,
    }
    this.player = videoPlayer(
      this.$refs.videoPlayer,
      { ...defaultOptions, ...this.options, sources: this.sources },
      function onPlayerReady() {},
      function onPlayerError() {}
    )
  },
  beforeDestroy() {
    if (this.player) {
      this.player.dispose()
    }
  },
}
</script>
<style lang="scss" scoped>
.video-js {
  width: 100%;
  video {
    width: 100%;
    height: 100%;
  }
}
.video-js .vjs-tech {
  /* 视频填充整个容器 */
  object-fit: cover;
}
</style>
复制代码

2.rtsp

这种视频前端不能直接播放,需要用插件

rtsp(x协议)://admin(用户名):ys123456(密码)@112.27.144.16(IP地址):554(端口)/Streaming/Channels/2

相关解决方案链接

(1.Vue项目中使用海康威视web插件:evestorm.github.io/posts/762/

方案1(已购买海康服务器)

当例如海康的rtsp的视频,如果海康那边推上平台了,要海康提供AppKey 和

API网关提供的appkey API网关提供的secret

代码(组件内代码)

<template>
  <div class="video-dialog" v-if="showVideo">
    <div class="close">
      <button type="primary" @click="stopVideo">关闭视频</button>
    </div>
    <div id="videoPlayerBox" ref="videoPlayerBox">
      <div id="playBox" v-html="oWebControl === null ? playText : ''">11</div>
    </div>
  </div>
</template>

<script>
export default {
  name: "videoPlayer",
  props: [
    "videoVisible",
    "monitorDeviceNo",
    "monitorDeviceName",
    "videoInfo",
    "layoutGet",
    "hideWin",
    "destoryWin",
  ],
  data() {
    return {
      showVideo:true,
      oWebControl: null,
      pubKey: "", // 公钥
      appkey: "",
      secret: "",
      ip: "",
      port: 1443,
      width: `${document.documentElement.clientWidth}` * 0.5,
      height: 400, // 弹框高度
      playHeight: `${document.documentElement.clientHeight}` * 0.78,
      layout: "1x1",
      left: "",
      top: "",
      buttonIDs: "0,16,256,257,258,259,260,512,513,514,515,516,517,768,769",
      initCount: 0,
      playMode: 0, // 0 预览 1回放
      playText: "启动中。。。",
      cameraIndexCode: "d6c11f20e1ce4aea91d0817576fcbfb3", // 监控点编号
    };
  },
  methods: {
    setLayout() {
      if (this.layoutGet == 1) {
        this.layout = "1x1";
        // this.width = `${document.documentElement.clientWidth}` * 0.6 / 1
        // this.playHeight = `${document.documentElement.clientHeight}` * 0.7 / 1
      } else if (this.layoutGet == 2) {
        this.layout = "2x2";
        // this.width = `${document.documentElement.clientWidth}` * 0.6 / 2
        // this.playHeight = `${document.documentElement.clientHeight}` * 0.7 / 2
      } else if (this.layoutGet == 3) {
        this.layout = "3x3";
        // this.width = `${document.documentElement.clientWidth}` * 0.6 / 3
        // this.playHeight = `${document.documentElement.clientHeight}` * 0.7 / 3
      } else if (this.layoutGet == 4) {
        this.layout = "4x4";
        // this.width = `${document.documentElement.clientWidth}` * 0.6 / 4
        // this.playHeight = `${document.documentElement.clientHeight}` * 0.7 / 4
      }
    },
    show() {
      // console.log(this.videoInfo, 78787)
      this.setLayout();
      // 设置top left
      let bodyW = document.body.clientWidth;
      let bodyH = document.body.clientHeight;
      this.left = bodyW / 2 - this.width / 2;
      this.top = bodyH / 3 - this.height / 3;
      let videoInfo = this.videoInfo;
      this.appkey = videoInfo[0].ekey;
      this.secret = videoInfo[0].esecret;
      this.cameraIndexCode = videoInfo[0].eNote;
      this.ip = videoInfo[0].eaddress.split(":")[0];
      this.port = parseInt(videoInfo[0].eaddress.split(":")[1]);
      this.stopVideo();
      setTimeout(() => {
        this.$nextTick(() => {
          this.initPlugin(() => {
            this.previewVideo();
          });
        });
      }, 500);
    },
    hide() {
      this.handleClose();
    },
    handleClose() {
      this.stopVideo();
    },
    stopVideo() {
      if (this.oWebControl) {
        this.oWebControl.JS_RequestInterface({
          funcName: "stopAllPreview",
        });
        this.oWebControl.JS_HideWnd();
        this.oWebControl.JS_Disconnect().then(
          () => {
            // 断开与插件服务连接成功
            console.log("断开与插件服务连接成功");
          },
          () => {
            // 断开与插件服务连接失败
            console.log("断开与插件服务连接失败");
          }
        );
        this.oWebControl = null;
        this.showVideo=false;
      }
    },
    // 推送消息
    cbIntegrationCallBack() {
      // console.log(oData, '推送消息');
    },
    // RSA加密
    setEncrypt(value) {
      /* eslint-disable */
      let encrypt = new JSEncrypt();
      encrypt.setPublicKey(this.pubKey);
      return encrypt.encrypt(value);
    },
    initPlugin(callback) {
      /* eslint-disable */
      let that = this;
      this.oWebControl = new WebControl({
        szPluginContainer: "playBox", // 指定容器id
        iServicePortStart: 15900, // 指定起止端口号,建议使用该值
        iServicePortEnd: 15909,
        szClassId: "23BF3B0A-2C56-4D97-9C03-0CB103AA8F11", // 用于IE10使用ActiveX的clsid
        cbConnectSuccess: function () {
          // 创建WebControl实例成功
          that.oWebControl
            .JS_StartService("window", {
              // WebControl实例创建成功后需要启动服务
              dllPath: "./VideoPluginConnect.dll", // 值"./VideoPluginConnect.dll"写死
            })
            .then(
              function () {
                // 启动插件服务成功
                that.oWebControl.JS_SetWindowControlCallback({
                  // 设置消息回调
                  cbIntegrationCallBack: that.cbIntegrationCallBack,
                });

                that.oWebControl
                  .JS_CreateWnd("playBox", that.width, that.playHeight)
                  .then(function () {
                    // JS_CreateWnd创建视频播放窗口,宽高可设定
                    that.init(callback); // 创建播放实例成功后初始化
                  });
              },
              function () {
                // 启动插件服务失败
              }
            );
        },
        cbConnectError: function () {
          // 创建WebControl实例失败
          that.oWebControl = null;
          that.playText = "插件未启动,正在尝试启动,请稍候...";
          WebControl.JS_WakeUp("VideoWebPlugin://"); // 程序未启动时执行error函数,采用wakeup来启动程序
          // WebControl.JS_WakeUp("VideoWebPlugin://"); // 程序未启动时执行
          that.initCount++;
          if (that.initCount < 3) {
            setTimeout(function () {
              that.initPlugin();
            }, 3000);
          } else {
            that.playText =
              '插件启动失败,请检查插件是否安装!<a target="_blank" style="color: #30a8ff;text-decoration: underline;" href="http://xx.com/VideoWebPlugin.zip">下载地址(软件大小:62.7MB)</a>';
          }
        },
        cbConnectClose: function (bNormalClose) {
          // 异常断开:bNormalClose = false
          // JS_Disconnect正常断开:bNormalClose = true
          that.oWebControl = null;
        },
      });
    },
    // 获取公钥
    getPubKey(callback) {
      this.oWebControl
        .JS_RequestInterface({
          funcName: "getRSAPubKey",
          argument: JSON.stringify({
            keyLength: 1024,
          }),
        })
        .then((oData) => {
          if (oData.responseMsg.data) {
            this.pubKey = oData.responseMsg.data;
            callback();
          }
        });
    },
    init(callback) {
      let that = this;
      this.getPubKey(() => {
        //  请自行修改以下变量值
        let appkey = this.appkey; // 综合安防管理平台提供的appkey,必填
        let secret = that.setEncrypt(this.secret); // 综合安防管理平台提供的secret,必填
        let ip = this.ip; // 综合安防管理平台IP地址,必填
        let playMode = this.playMode; // 初始播放模式:0-预览,1-回放
        let port = this.port; // 综合安防管理平台端口,若启用HTTPS协议,默认443
        let snapDir = "D:\SnapDir"; // 抓图存储路径
        let videoDir = "D:\VideoDir"; // 紧急录像或录像剪辑存储路径
        let layout = this.layout; // playMode指定模式的布局
        let enableHTTPS = 1; // 是否启用HTTPS协议与综合安防管理平台交互,是为1,否为0
        let encryptedFields = "secret"; // 加密字段,默认加密领域为secret
        let showToolbar = 0; // 是否显示工具栏,0-不显示,非0-显示
        let showSmart = 0; // 是否显示智能信息(如配置移动侦测后画面上的线框),0-不显示,非0-显示
        let buttonIDs = this.buttonIDs; // 自定义工具条按钮
        // /// 请自行修改以上变量值
        that.oWebControl
          .JS_RequestInterface({
            funcName: "init",
            argument: JSON.stringify({
              appkey: appkey, // API网关提供的appkey
              secret: secret, // API网关提供的secret
              ip: ip, // API网关IP地址
              playMode: playMode, // 播放模式(决定显示预览还是回放界面)
              port: port, // 端口
              snapDir: snapDir, // 抓图存储路径
              videoDir: videoDir, // 紧急录像或录像剪辑存储路径
              layout: layout, // 布局
              enableHTTPS: enableHTTPS, // 是否启用HTTPS协议
              encryptedFields: encryptedFields, // 加密字段
              showToolbar: showToolbar, // 是否显示工具栏
              showSmart: showSmart, // 是否显示智能信息
              buttonIDs: buttonIDs, // 自定义工具条按钮
            }),
          })
          .then((oData) => {
            that.oWebControl.JS_Resize(that.width, that.playHeight); // 初始化后resize一次,规避firefox下首次显示窗口后插件窗口未与DIV窗口重合问题
            if (callback) {
              callback();
            }
          });
      });
    },
    // 视频预览功能
    previewClick() {
      if (!this.oWebControl) {
        return;
      }
      // 如果是回放,重新初始化
      if (this.playMode === 1) {
        this.playMode = 0;
        this.oWebControl.JS_HideWnd();
        this.initPlugin(() => {
          this.previewVideo();
        });
      } else if (this.playMode === 0) {
        this.previewVideo();
      }
    },
    previewVideo() {
      for (let i = 0; i < this.videoInfo.length; i++) {
        // let cameraIndexCode = this.cameraIndexCode;             // 获取输入的监控点编号值,必填
        if (this.videoInfo[i].enote) {
          let cameraIndexCode = this.videoInfo[i].enote; // 获取输入的监控点编号值,必填
          let streamMode = 0; // 主子码流标识:0-主码流,1-子码流
          let transMode = 0; // 传输协议:0-UDP,1-TCP
          let gpuMode = 0; // 是否启用GPU硬解,0-不启用,1-启用
          // let wndId = -1;                                         // 播放窗口序号(在2x2以上布局下可指定播放窗口)
          let wndId = i + 1; // 播放窗口序号(在2x2以上布局下可指定播放窗口)
          console.log(cameraIndexCode, 8989);
          this.oWebControl.JS_RequestInterface({
            funcName: "startPreview",
            argument: JSON.stringify({
              cameraIndexCode: cameraIndexCode.trim(), // 监控点编号
              streamMode: streamMode, // 主子码流标识
              transMode: transMode, // 传输协议
              gpuMode: gpuMode, // 是否开启GPU硬解
              wndId: wndId, // 可指定播放窗口
            }),
          });
        }
      }
    },
    // 回放
    playBack() {
      if (!this.oWebControl) {
        return;
      }
      // 如果是预览
      if (this.playMode === 0) {
        this.playMode = 1;
        this.oWebControl.JS_HideWnd();
        this.initPlugin(() => {
          this.backVideo();
        });
      } else if (this.playMode === 1) {
        this.backVideo();
      }
    },
    backVideo() {
      let cameraIndexCode = this.cameraIndexCode;
      // 前30天
      let date = new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000);
      let month =
        date.getMonth() + 1 < 10
          ? "0" + (date.getMonth() + 1)
          : date.getMonth() + 1;
      // 开始时间当天00点
      let str =
        date.getFullYear() + "/" + month + "/" + date.getDate() + " 00:00:00";
      let startTime = String(
        parseInt(new Date(str).getTime() / 1000) - 3 * 60 * 60
      );
      let endTime = String(parseInt(date.getTime() / 1000));
      this.oWebControl.JS_RequestInterface({
        funcName: "startPlayback",
        argument: JSON.stringify({
          cameraIndexCode: cameraIndexCode.trim(), // 监控点编号
          startTimeStamp: startTime, // 录像查询开始时间戳,单位:秒
          endTimeStamp: endTime, // 录像查询结束时间戳,单位:秒
          recordLocation: 1, // 录像存储类型 0-中心存储 1-设备存储
          transMode: 0, // 传输协议 ,0-UDP 1-TCP
          gpuMode: 0, // 是否开启 GPU 硬解,0-不开启 1-开启
          wndId: -1, //可指定播放窗口
        }),
      });
    },
    // 设置窗口裁剪,当因滚动条滚动导致窗口需要被遮住的情况下需要JS_CuttingPartWindow部分窗口
    setWndCover() {
      let iWidth = $(window).width();
      let iHeight = $(window).height();
      let oDivRect = $("#playBox").get(0).getBoundingClientRect();
      let iCoverLeft = oDivRect.left < 0 ? Math.abs(oDivRect.left) : 0;
      let iCoverTop = oDivRect.top < 0 ? Math.abs(oDivRect.top) : 0;
      let iCoverRight =
        oDivRect.right - iWidth > 0 ? Math.round(oDivRect.right - iWidth) : 0;
      let iCoverBottom =
        oDivRect.bottom - iHeight > 0
          ? Math.round(oDivRect.bottom - iHeight)
          : 0;

      iCoverLeft = iCoverLeft > this.width ? this.width : iCoverLeft;
      iCoverTop = iCoverTop > this.playHeight ? this.playHeight : iCoverTop;
      iCoverRight = iCoverRight > this.width ? this.width : iCoverRight;
      iCoverBottom =
        iCoverBottom > this.playHeight ? this.playHeight : iCoverBottom;

      this.oWebControl.JS_RepairPartWindow(
        0,
        0,
        this.width + 1,
        this.playHeight
      ); // 多1个像素点防止还原后边界缺失一个像素条
      if (iCoverLeft != 0) {
        this.oWebControl.JS_CuttingPartWindow(
          0,
          0,
          iCoverLeft,
          this.playHeight
        );
      }
      if (iCoverTop != 0) {
        this.oWebControl.JS_CuttingPartWindow(0, 0, this.width + 1, iCoverTop); // 多剪掉一个像素条,防止出现剪掉一部分窗口后出现一个像素条
      }
      if (iCoverRight != 0) {
        this.oWebControl.JS_CuttingPartWindow(
          this.width - iCoverRight,
          0,
          iCoverRight,
          this.playHeight
        );
      }
      if (iCoverBottom != 0) {
        this.oWebControl.JS_CuttingPartWindow(
          0,
          this.playHeight - iCoverBottom,
          this.width,
          iCoverBottom
        );
      }
    },
    // 拖拽窗口
    onmousedown(e) {
      let that = this;
      let cWidth = document.body.clientWidth;
      let cHeight = document.body.clientHeight;
      this.$refs.videoPlayerBox.onmousemove = function (el) {
        let ev = el || window.event;
        ev.preventDefault();
        // 解决点击标题窗口抖动的问题
        if (Math.abs(ev.movementX) === 0 && Math.abs(ev.movementY) === 0) {
          return;
        }
        that.left += ev.movementX;
        that.top += ev.movementY;
        // 顶部不能超出,左侧、右侧、底部可以超出一半
        if (that.top < 0) {
          that.top = 0;
        }
        if (that.top > cHeight - that.height / 2) {
          that.top = cHeight - that.height / 2;
        }
        if (that.left < -that.width / 2) {
          that.left = -that.width / 2;
        }
        if (that.left > cWidth - that.width / 2) {
          that.left = cWidth - that.width / 2;
        }
        that.oWebControl.JS_Resize(that.width, that.playHeight);
      };
      this.$refs.videoPlayerBox.onmouseup = function () {
        this.onmousemove = null;
        this.onmouseup = null;
      };
      // 阻止默认事件
      if (e.preventDefault) {
        e.preventDefault();
      } else {
        return false;
      }
    },
    onmouseleave(e) {
      // 拖拽过快鼠标划出标题位置,重新拖拽是,鼠标还未移入标题,由于上一次的onmouseup方法没有执行,导致鼠标靠近弹框,弹框移动,解决方法,给onmousemove和onmouseup赋值为null
      this.$refs.videoPlayerBox.onmousemove = null;
      this.$refs.videoPlayerBox.onmouseup = null;
      // 解决拖拽过快,播放器残影问题
      this.oWebControl.JS_Resize(this.width, this.playHeight);
    },
  },
  mounted() {
    // let hksystem = JSON.parse(sessionStorage.getItem('hksystem'));
    // this.appkey = hksystem.appKey;
    // this.secret = hksystem.appSecret;
    // this.ip = hksystem.host.split(':')[0];
    // this.port = hksystem.host.split(':')[1];

    // 监听resize事件,使插件窗口尺寸跟随DIV窗口变化
    // $(window).resize(() => {
    //     if (this.oWebControl != null) {
    //         this.oWebControl.JS_Resize(this.width, this.playHeight);
    //         // this.setWndCover();
    //     }
    // });

    // 监听滚动条scroll事件,使插件窗口跟随浏览器滚动而移动
    // $(window).scroll(() => {
    //     if (this.oWebControl != null) {
    //         this.oWebControl.JS_Resize(this.width, this.playHeight);
    //         this.setWndCover();
    //     }
    // });
    // 标签关闭
    $(window).unload(() => {
      if (this.oWebControl != null) {
        this.oWebControl.JS_HideWnd(); // 先让窗口隐藏,规避可能的插件窗口滞后于浏览器消失问题
        this.oWebControl.JS_Disconnect().then(
          () => {
            // 断开与插件服务连接成功
          },
          () => {
            // 断开与插件服务连接失败
          }
        );
      }
    });
    // this.setLayout()
    this.show();
  },
  watch: {
    monitorDeviceNo: {
      handler(newV, oldV) {
        this.cameraIndexCode = newV;
        if (newV && this.playMode === 0) {
          this.previewVideo();
        } else if (newV && this.playMode === 1) {
          this.backVideo();
        }
      },
    },
    layoutGet: {
      handler(newV, oldV) {
        console.log(newV);
        if (newV && newV != oldV) {
          this.show();
          // this.previewVideo()
        }
      },
    },
    hideWin: {
      handler(newV, oldV) {
        if (newV) {
          this.oWebControl.JS_HideWnd();
          this.showVideo=false
        } else {
          this.oWebControl.JS_ShowWnd();
        }
      },
    },
    destoryWin: {
      handler(newV, oldV) {
          this.stopVideo();
      },
    },
    videoInfo: {
      handler(newV, oldV) {
        if (newV) {
          console.log(newV, 2222);
          this.show();
        }
      },
    },
  },
};
</script>

<style lang="scss" scoped>
.video-dialog {
  // width:790px;
  // height: 100%;
  left:480px;
  top:100px;
  position: fixed;
  z-index: 10000;
  .close{
      width:100%;
      display: flex;
      align-items: center;
      justify-content: center;
      img{
          width:30px;
          height:auto;
      }
  }
}
#videoPlayerBox {
  // width: 100%;
  // height: 100%;
  z-index: 4012;
  .header {
    height: 0.24rem;
    width: 100%;
    display: flex;
    align-items: center;
    font-size: 0.1rem;
    color: #fff;
    text-align: center;
    justify-content: center;
    background: #142c50;
  }
  .closeBtn {
    width: 0.12rem;
    height: 0.12rem;
    position: absolute;
    top: 0.06rem;
    right: 0.06rem;
    cursor: pointer;
  }
  .topbutton {
    height: 26px;
    padding-top: 6px;
    padding-left: 14px;
    background-color: #0d1f3a;
    cursor: pointer;
    .topBtns {
      padding: 0 12px 6px;
      border-bottom: 1px solid #0d1f3a;
      color: #fff;
    }
    .activeBtn {
      color: #4ba2ff;
      border-color: #4ba2ff;
    }
  }
  #playBox {
    // width: 100%;
    // height: 100%;
    color: #ffffff;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-size: 12px;
  }
}
</style>
复制代码

调用处代码

<template>
  <div class="list_view">
        <div class="item_view" v-for="(item, index) in testList" :key="index">
          <rtsp-video
        :hideWin="maskOpen"
        :destoryWin="destroy"
        :data-active="updateTime"
    ref="videoRef"
    :videoInfo="[form]"
    layoutGet="1"
    >22</rtsp-video>
    33
    <div class="name">{{ item.name }}</div>
    <demo-video :list="curShowList" :layout="layout"></demo-video>
    </div>
    </div>
</template>

<script>
import TitleBar from '@/components/TitleBar'
import AutoLoadMixin from '@/plugin/AutoLoadMixin'
import rtspVideo from "@/components/RTSPVideo";
// import demoVideo from "@/components/demoVideo"
export default {
  components: {
    TitleBar,
    rtspVideo,
    // demoVideo
  },
  mixins: [AutoLoadMixin],
  data() {
    return {
      currentIndex: 0,
      curShowList: [
        {
          cameraIndexCode: '2',
          streamMode: 'rtsp',
          transMode: 'rtsp'
        }
      ],
      destroy: false,
      maskOpen: false,
      updateTime: 1,
      activeTab: "1",
      form: {
        areaId: 6,
        createBy: null,
        createTime: "2021-09-07 14:09:38",
        deviceCode: "59560b1dcbdc49ba986149b5d48c60ec",
        deviceCount: "767",
        deviceName: "会所西通道",
        eaddress: "219.146.242.46:1443",
        ekey: "25891522",
        endTime: null,
        enote: "59560b1dcbdc49ba986149b5d48c60ec",
        ertsp: null,
        esecret: "X1LHdWow0bztDywl48j9",
        isWarn: 25,
        latitude: 39.988395,
        left: 834.829268292683,
        longitude: 116.496051,
        online: 0,
        params: {},
        parentId: null,
        parentName: null,
        picName: null,
        picUrl: "http://123.57.155.205/comprehensive/72a6894a56cc47458078ec7bc963de30/72a6894a56cc47458078ec7bc963de30-202151721.png",
        remark: null,
        rootArea: null,
        searchValue: null,
        spaceName: null,
        startTime: null,
        top: 714.0044642857142,
        typeId: 285,
        typeName: "人脸抓拍摄像机",
        updateBy: null,
        updateTime: null,
      }
    }
  },
}
</script>
复制代码

方案2(vlc插件播放)

直接使用rtsp地址来进行播放

(rtsp://admin:ys123456@112.27.144.16:554/Streaming/Channels/2)

使用vlc插件来进行播放,只支持win 并且只能使用ie浏览器或者360兼容模式(vue项目需要配置来兼容ie浏览器)

方案3(ffmpeg转码)

rtsp 视频流,没法在网页中直接播放。在技术支持的群里问了管理员也说没有网页端播放的方案,那只能自己找了。

网上找了一圈很多都是较老的技术了,有用老版本的 chrome 安装 vlc 插件的来播放的,有用 flash 来播放的,而且很多博客抄来抄去都差不多。

最后找到两个看着靠谱的方案:

  • 一种是用 ffmpeg 的 wasm 版直接前端解析转码 rtsp 视频流,转成前端能直接播放的格式
  • 第二种是写一个服务还是调用 ffmpeg 将 rtsp 视频流转码,给前端返回新的视频地址

看到这两个方案我感觉第二种是肯定行得通的,第一种方案暂时不清楚,没怎么用过前端 wasm 技术,只能再去搜索一下。

针对第一种方案,去 =github= 上搜索了一下看看别人有没有用这个的,结果发现暂时还不支持 rtsp 视频流,这是2020年11月4号的回复,后续也没见有啥更新,当他暂时不支持,那就选第二种方案。

方案二的实现就分两步:

  • 首先是转码,想着可以用 java 调用 ffmpeg 来实现,但是要转成什么视频流又没有思路。
  • 第二步就是找前端能播放视频流的插件,转码的最终目的是为了前端网页中能播放,所以先看看有什么播放器能支持什么格式。

之前在很多网站看视频的时候发现网页端播放的视频很多都以 m3u8 结尾的。 之前好奇下载过发现这是一个文件,里面记录了很多视频分片,最后查了知道这个叫 hls,搜索了之后找到了网页播放 hls 的插件 hls.js。 所以现在目标就明确了,首先将 rtsp 转成 hls, 再用 hls.js 来播放。

转码服务

一开始想着自己用 java 调用 ffmpeg 来做转码服务,实现起来应该也不难,但想着这种应该是比较常见的需求,看看有没有轮子。 经过一番搜索,还真找到了一个 rtsp-stream,真的是帮了大忙,节约了好多时间,感谢开源大佬的无私奉献。 这个就是正好完全解决了我的需求,而且是用 Go 写的,部署简单,跨平台,而且作者还很贴心的构建了 Docker 镜像。

根据文档介绍,转换接口是 start 接口,会返回一个 m3u8 的链接,前端播放器只需要播放这个链接就行。 这些都可以交给后端来实现,后端只需要暴露一个接口返回一个 m3u8 的链接即可。流程如下:

前端播放

这里用的是 react, 用 vue 或者别的应该差不多,首先是加载 hls.js 插件

useEffect(() => {
  if (hlsRef.current) {
    hlsRef.current.detachMedia();
  }
  setLoading(true);

  stopVideo();

  const meta = document.createElement('meta');
  meta.name = 'referrer';
  meta.content = 'no-referrer';
  document.getElementsByTagName('head')[0].appendChild(meta);

  const script = document.createElement('script');
  if (script.readyState) {
    // IE
    script.onreadystatechange = () => {
      if (script.readyState === 'loaded' || script.readyState === 'complete') {
        script.onreadystatechange = null;
        initVideo();
      }
    };
  } else {
    // 其他浏览器
    script.onload = () => {
      initVideo();
    };
  }
  script.src = 'https://cdn.jsdelivr.net/npm/hls.js@latest';
  document.getElementsByTagName('head')[0].appendChild(script);

  return () => {
    if (hlsRef.current) {
      hlsRef.current.detachMedia();
    }
    stopVideo();
  };
}, []);
复制代码

initVideo() 方法就是具体要设置播放的操作,首先是获取播放地址,然后再将 hls 实例绑定到 video 标签上。

function initVideo() {
  const requestBody = { cameraCode: '1' };
  axios.post('/video/get-url', requestBody).then(res => {
    setUrlInfo(res);
    const video = document.getElementById('realTime');
    const videoSrc = res.uri;
    if (window.Hls.isSupported()) {
      const hls = new window.Hls();
      hlsRef.current = hls;
      hls.loadSource(videoSrc);
      hls.attachMedia(video);
    }
    setLoading(false);
  }).catch(e => {
    // eslint-disable-next-line no-console
    console.log('err: ', e.message);
    message.error('获取本地视频失败');
    setLoading(false);
  });
}
复制代码

最后是加上一个 video 标签:

<video id="realTime" controls="controls" className={`${prefixCls}-video`} />
复制代码

注意事项

  • hls.js 只支持 H.264 编码的视频,不支持 H.265 编码的视频。如果遇到转码没问题,返回的链接能在 vlc 等播放器播放,但在网页中进度条正常,就是黑屏无图像时,可以去 nvr800 查看一下视频编码。
  • 结束播放时可以给服务器发送一个请求停止转码,避免浪费资源。
文章分类
前端
文章标签