在公司项目开发中,有一个项目里面需要做一个视频播放的功能,播放方式是调用海康平台提供的接口获取流地址来进行视频的播放并且最重要的是需要支持flash。由于前端用的Vue,对比了几个,最后选择了LivePlayer H5播放器。

官网介绍:H5直播/点播播放器,使用简单,功能强大, 免费使用。

官网地址:https://www.liveqing.com/docs/manuals/LivePlayer.html
github源码地址:https://github.com/livegbs/GB28181-Server

因为官网只是做了介绍,并没有具体的详细API,可以参考github源码,如果还不能解决问题,可以在官网加官方QQ群,会有专业人员帮忙解答。

有如下特点:

  • 支持MP4播放;
  • 支持m3u8/HLS播放;
  • 支持HTTP-FLV/WS-FLV播放;
  • 支持RTMP播放;
  • 支持直播和点播播放;
  • 支持播放器快照截图;
  • 支持点播多清晰度播放;
  • 支持全屏或比例显示;
  • 自带的flash支持极速和流畅模式;
  • 自带的flash支持HTTP-FLV播放;
  • 自动检测IE浏览器兼容播放;
  • 支持自定义叠加层;v1.7.9

在这里插入图片描述
属性(Property)

  • video-url: 视频流地址 String default ‘’
  • video-title:视频右上角显示的标题 String default ‘’
  • poster:视频封面图片 String default ‘’
  • autoplay: 自动播放 Boolean default true
  • controls 显示播放器控制栏 Boolean default true
  • loop 是否循环播放 Boolean default false
  • live 是否直播, 标识要不要显示进度条 Boolean default false
  • alt 视频流地址没有指定情况下, 视频所在区域显示的文字, 相当于 html img 标签的 alt 属性 String default ‘无信号’
  • muted 是否静音 Boolean default false
  • aspect 视频显示区域的宽高比, fullscreen 即是全屏展示 String default ‘16:9’
  • loading 指示加载状态, 支持 sync 修饰符
  • fluent 流畅模式, Boolean default true
  • stretch 是否拉伸, Boolean default false
  • timeout m3u8 加载超时(秒) Number default 20
  • show-custom-button 是否在工具栏显示自定义按钮(极速/流畅, 拉伸/标准), Boolean default true
  • resolution 供选择的清晰度配置 String 如 “yh,fhd,hd,sd” ( 说明:yh:原始分辨率,fhd:超清,hd:高清,sd:标清,不配置则不启用,需要提供多清晰度源,比如原画源是test.m3u8,则hd源即为test_hd.m3u8 )
  • resolutiondefault 默认播放的清晰度 String “hd”
  • playback-rates 倍速列表, Array default [0.5, 1, 2, 3]
  • playback-rate 默认倍速, Number default 1
  • hasaudio HTTP-FLV播放时使用:是否有音频,传递该属性用于处理只有音频或只有视频的源 Boolean,默认不配置自动判断
  • hasvideo HTTP-FLV播放时使用:是否有视频,传递该属性用于处理只有音频或只有视频的源 Boolean,默认不配置自动判断

方法(Medthod)

  • play 播放
  • pause 暂停
  • paused 获取暂停状态
  • getCurrentTime 获取当前播放时间进度, 同步返回播放时间进度数据
  • snap 外部 API 方式获取快照, 快照获取成功后, 触发 snapOutside Event
  • getMuted 获取静音状态
  • setMuted 设置静音状态
  • isFullscreen v1.7.6 获取全屏状态
  • requestFullscreen v1.7.6 触发全屏, 需要放置到交互事件回调中调用, 如果已处在全屏状态, 则退出全屏
  • getVolume v1.7.7 获取音量
  • setVolumev1.7.7 设置音量, 0~1

事件(Event)

  • message m3u8 加载失败时触发通知消息, 参数: { type: ‘’, message: ‘’}
  • error 播放器出错回调, 参数: Error Object
  • ended 播放结束, 参数: 无
  • timeupdate 进度更新, 参数: 当前时间进度
  • pause 暂停, 参数: 当前时间进度
  • play 播放, 参数: 当前时间进度
  • snapOutside 外部快照回调, 参数: 快照 Base64 数据
  • snapInside 内部快照回调, 由控制栏快照按钮触发, 参数: 快照 Base64 数据

安装使用(Install)

一、安装:npm install @liveqing/liveplayer

二、编辑webpack.xxx.config.js:

const CopyWebpackPlugin = require('copy-webpack-plugin');

......
    // copy js lib and swf files to dist dir
    new CopyWebpackPlugin([
        { from: 'node_modules/@liveqing/liveplayer/dist/component/crossdomain.xml'},
        { from: 'node_modules/@liveqing/liveplayer/dist/component/liveplayer.swf'},
        { from: 'node_modules/@liveqing/liveplayer/dist/component/liveplayer-lib.min.js', to: 'js/'}
    ]),
......

注意:本地开发的配置文件和生产环境的都要配置。

三、引入依赖js

在index.html中引入:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
		<meta name="renderer" content="webkit">
		<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
		<title>xxx</title>
	</head>
  ...
  <script type="text/javascript" src="./js/liveplayer-lib.min.js"></script>
  ...
	<body>
		...
	</body>
</html>

四、编辑 Vue 组件

......

import LivePlayer from '@liveqing/liveplayer'

......
  components: {
    LivePlayer
  }
......

<LivePlayer :videoUrl="videoUrl" fluent autoplay live stretch></LivePlayer>

实战:播放器组件化

<template>
  <div style="border:#a8cae5 1px solid;">
    <el-row >
      <el-col :span="5" style="height: 25px">
        窗口{{rowIdx*winNum + colIdx + 1}}{{cameraName}}
      </el-col>
      <el-col v-if="isShowTab" :span="19-winNum">
        <el-radio v-if="isShowTab" v-model="protocol" label="hls">HLS</el-radio>
        <el-radio v-if="isShowTab" v-model="protocol" label="rtmp">RTMP</el-radio>
      </el-col>
      <el-col v-if="isShowTab" :span="winNum">
        <el-button size="mini" @click="close">关闭</el-button>
      </el-col>
    </el-row>

    <LivePlayer :cameraId="cameraId"
                :video-url='videoUrl'
                :video-title="videoTitle"
                muted live loading stretch resolution='hd,sd'
                v-loading="bLoading"
                element-loading-background="#000"
                :loading.sync="bLoading"></LivePlayer>

    <div :id="showPTZ" v-if="isShowTab" style="position: absolute;z-index:9999;" :style="{top:ptzTop + '%'}">
      <div class="showPTZ left-panel-accordion-content  ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content-active" style="height: 0px; overflow: auto; display: block;" role="tabpanel" _mask_="1">
        <i :class="iconClass" @click="chgTabState"></i>
        <div class="ptz-panal-up">
          <div id="ptz-speed-container" class="ptz-panal-up-left">
            <div id="ptz-speed" class="ptz-speed"></div>
            <div id="ptz-speed-high" class="ptz-speed-high" style="height: 32.5px; top: 39.5px;"></div>
          </div>
          <div class="ptz-panal-up-right" >
            <span id="ptz-auto" class="ptz-auto direction" title="自动扫描"></span>
            <span @click="controlPtz('UP')" id="ptz-up" class="ptz-up direction" title="上移" ctrlcmd="UP"></span>
            <span @click="controlPtz('DOWN')" id="ptz-down" class="ptz-down direction" title="下移" ctrlcmd="DOWN"></span>
            <span @click="controlPtz('LEFT')" id="ptz-left" class="ptz-left direction" title="左移" ctrlcmd="LEFT"></span>
            <span @click="controlPtz('RIGHT')" id="ptz-right" class="ptz-right direction" title="右移" ctrlcmd="RIGHT"></span>
            <span @click="controlPtz('LEFT_UP')" id="ptz-up-left" class="ptz-up-left direction" title="左上移动" ctrlcmd="LEFT_UP"></span>
            <span @click="controlPtz('RIGHT_UP')" id="ptz-up-right" class="ptz-up-right direction" title="右上移动" ctrlcmd="RIGHT_UP"></span>
            <span @click="controlPtz('LEFT_DOWN')" id="ptz-down-left" class="ptz-down-left direction" title="左下移动" ctrlcmd="LEFT_DOWN"></span>
            <span @click="controlPtz('RIGHT_DOWN')" id="ptz-down-right" class="ptz-down-right direction" title="右下移动" ctrlcmd="RIGHT_DOWN"></span>
            <span @click="controlPtz('ZOOM_IN')" id="ptz-zoomin" class="ptz-zoomin ptz-up-right-btn" title="变大焦距" ctrlcmd="ZOOM_IN"><span class="icon"></span></span>
            <span @click="controlPtz('ZOOM_OUT')" id="ptz-zoomout" class="ptz-zoomout ptz-up-right-btn" title="变小焦距" ctrlcmd="ZOOM_OUT"><span class="icon"></span></span>
            <span @click="controlPtz('13')" id="ptz-focusin" class="ptz-focusin ptz-up-right-btn" title="前调焦点" ctrlcmd="13"><span class="icon"></span></span>
            <span @click="controlPtz('FOCUS_FAR')" id="ptz-focusout" class="ptz-focusout ptz-up-right-btn" title="后调焦点" ctrlcmd="FOCUS_FAR"><span class="icon"></span></span>
            <span @click="controlPtz('IRIS_ENLARGE')" id="ptz-irisout" class="ptz-irisout ptz-up-right-btn" title="扩大光圈" ctrlcmd="IRIS_ENLARGE"><span class="icon"></span></span>
            <span @click="controlPtz('IRIS_REDUCE')" id="ptz-irisin" class="ptz-irisin ptz-up-right-btn" title="缩小光圈" ctrlcmd="IRIS_REDUCE"><span class="icon"></span></span>
          </div>
          <!--          <div id="ptz-slider" class="ptz-slider" style="font-size: 11px !important; text-indent: 3px; top: 39.5px;" title="4">4</div>-->
        </div>
        <div class="loading-container" style="left: 0px; top: 0px; height: 110px; width: 218px;">
          <div class="loading-container-overlay"></div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  import LivePlayer from '@liveqing/liveplayer'
  import { getPreviewURLs,controllerHik } from '../../../../api/zhgd/video/hikVideo.js'
  export default {
    name: 'myVideoPlayer',
    props: ['cameraId', 'videoUrl', 'videoTitle', 'cameraName', 'rowIdx', 'colIdx', 'winNum'],
    components: {
      LivePlayer
    },
    data() {
      return {
        showPTZ: 'showPTZ' + this.rowIdx + this.colIdx,
        iconClass: 'el-icon-d-arrow-right',
        ptzTop: 20,
        isShowTab: this.videoUrl !== '',
        protocol: 'hls',
        bLoading: false
        // ptzRight:50,
        // videoUrl: 'rtmp://58.200.131.2:1935/livetv/hunantv'
      }
    },
    watch: {
      protocol(val) {
        console.log(this.rowIdx + '---' + this.colIdx + val)
        const params = { t: Math.random(), code: this.cameraId, protocol: val, streamType: '1' };
        getPreviewURLs(params).then(data => {
          if (data.code === '0') {
            let purl = data.data.url;
            if (window.location.host.indexOf('2.2.2.2') === 0) {
              purl = data.data.url.replace('36.32.24.38', '2.2.2.2');
            }
            this.videoUrl = purl;
          }
        });
      }
    },
    methods: {
      chgTabState() {
        if (this.iconClass === 'el-icon-d-arrow-right') {
          $('#showPTZ' + this.rowIdx + this.colIdx + ' > .left-panel-accordion-content').css('width', '190px');
          // $('#showPTZ' + this.rowIdx + this.colIdx + ' > .showPTZ').css('right', '0px');
          this.iconClass = 'el-icon-d-arrow-left'
        } else {
          $('#showPTZ' + this.rowIdx + this.colIdx + ' > .left-panel-accordion-content ').css('width', '25px');
          // $('#showPTZ' + this.rowIdx + this.colIdx + ' > .showPTZ').css('right', '0px');
          this.iconClass = 'el-icon-d-arrow-right'
        }
      },
      close() {
        this.$emit('close');
      },
      controlPtz(code){
        var params={t:Math.random(),cameraId:this.cameraId,code:code,action:"0"};

        controllerHik(params).then(data => {
          if(data.code==0){
            window.setTimeout(() => {
              params.action = "1";
              controllerHik(params).then(data => {
                if(data.code==0){
                }else if(data.code=="0x01900050"){
                  alert("设备操作失败!");
                }else{
                  alert(data.msg);
                }
              });
            }, 2000);
          }else if(data.code=="0x01900050"){
            alert("设备操作失败!");
          }else{
            alert(data.msg);
          }
        });
      }
    },
    mounted() {
      $('#showPTZ' + this.rowIdx + this.colIdx + ' > .left-panel-accordion-content').css('width', '25px');
      $('#showPTZ' + this.rowIdx + this.colIdx + ' > .showPTZ').css('right', '0px');
    }
  }
</script>

<style scoped>
   .showPTZ{
    height: 110px !important;
    position: relative;
    overflow: hidden !important;
    display: block;
    /*background-color:rgba(43, 51, 63, 0.7)*/
    background-color:rgba(255, 255, 255, 0.7)
  }
  .ptz-panal-up {
    background-image: none;
    width: 160px;
    margin-left: 20px;
  }
   .ptz-panal-up-left {
    background-image: none;
  }
  .ptz-panal-up-right {
    background-image: none;
    top:8px;
  }
  .ptz-speed{
    background-image: none;
  }
  .left-panel-accordion-content {
    width: 190px;
  }
  .el-icon-d-arrow-left,.el-icon-d-arrow-right{
    position: absolute;
    top:40px;
    font-size: 25px;
    cursor: pointer;
    color: black;
  }
   .ptz-auto {
     background-position: -801px top;
   }
   .ptz-up {
     background-position: -549px top;
   }
   .ptz-down {
     background-position: -591px top;
   }
   .ptz-left {
     background-position: -758px top;
   }
   .ptz-right {
     background-position: -717px top;
   }
   .ptz-up-left {
     background-position: -841px top;
   }
   .ptz-up-right {
     background-position: -633px top;
   }
   .ptz-down-left {
     background-position: -883px top;
   }
   .ptz-down-right {
     background-position: -675px top;
   }
   .ptz-zoomin .icon {
     background-position: -1079px top;
   }
   .ptz-zoomout .icon {
     background-position: -959px top;
   }
   .ptz-focusin .icon {
     background-position: -1119px top;
   }
   .ptz-focusout .icon {
     background-position: -999px top;
   }
   .ptz-irisout .icon {
     background-position: -1159px top;
   }
   .ptz-irisin .icon {
     background-position: -1039px top;
   }
   /*>>> .video-wrapper .video-title {*/
   /*   max-width: 400px;*/
   /*  top:50px;*/
   /* }*/
</style>

说明:

  1. props: [‘cameraId’, ‘videoUrl’, ‘videoTitle’, ‘cameraName’, ‘rowIdx’, ‘colIdx’, ‘winNum’]
  • cameraId:为摄像头id,用于向后端发起请求调用海康接口获取流地址
  • videoUrl:播放视频的流地址
  • videoTitle:播放窗口右上角标题
  • cameraName:当前选中的摄像头名称
  • rowIdx,colIdx:播放窗口的横纵坐标(用于标识第几个播放窗口)
  • winNum:窗口序号
  1. 由于播放视频前需要取流,因为网络等问题可能会导致加载时间过长,默认不显示加载状态,加载样式可以自定义,关键代码:
    # 属性
    v-loading="bLoading"
    element-loading-background="#000"
    :loading.sync="bLoading"
    
    <div class="loading-container" style="left: 0px; top: 0px; height: 110px; width: 218px;">
    	<div class="loading-container-overlay"></div>
    </div>
    
    效果图:在这里插入图片描述
  2. 如果想验证取到的流地址是不是有问题,可以通过vlc这个专业的视频播放软件来验证。
    在这里插入图片描述

效果图:
在这里插入图片描述

  • hls流:对应标清
  • RTMP流:对应高清
  • 半透明云控制台上的图标可以点击,发送给海康接口,用来控制摄像头

调用该组件相关代码:

<el-row v-for="(rowData,rowIdx) in winRows" :key="rowData">
  <el-col v-for="(colData,colIdx) in rowData.cols" :key="colData.url" :span="colData.span" style="border: 1px #0fa2ff solid;">
       <my-video-player
         :cameraId="colData.cameraId"
         :videoUrl="colData.url"
         :videoTitle="colData.title"
         :rowIdx="rowIdx"
         :colIdx="colIdx"
         :winNum="winNum"
         @close="close(colData)"></my-video-player>
  </el-col>
</el-row>

winRows: [
	{ cols: [
	  { span: 24, url: '', title: '', cameraId: '' }
	] }
],
winNum: 1

#分屏方法
setWinNum(winNum) {
  const rows = Math.sqrt(winNum);
  this.winNum = rows;
  // 原来的组件数据存起来
  const oriArray = [];
  for (let i = 0; i < this.winRows.length; i++) {
    for (let j = 0; j < this.winRows[i].cols.length; j++) {
      if (this.winRows[i].cols[j].url !== '') {
        oriArray.push(this.winRows[i].cols[j])
      }
    }
  }
  // 创建一个新的winRows
  this.winRows = [];
  for (let i = 0; i < rows; i++) {
    this.winRows.push({ cols: [] })
  }
  for (let i = 0; i < this.winRows.length; i++) {
    for (let j = 0; j < rows; j++) {
      if (oriArray.length > 0) {
        this.winRows[i].cols.push({ span: 24 / rows, url: oriArray[0].url, title: oriArray[0].title, cameraId: oriArray[0].cameraId })
        oriArray.splice(0, 1);
      } else {
        this.winRows[i].cols.push({ span: 24 / rows, url: '', title: '', cameraId: '' })
      }
    }
  }
}
 
 #关闭播放窗口
 close(colData) {
   colData.url = ''
   colData.title = ''
   colData.cameraId = ''
 },  

#点击摄像头播放
playVideo(treeNode) {
  for (let i = 0; i < this.winRows.length; i++) {
    let isBreak = false;
    for (let j = 0; j < this.winRows[i].cols.length; j++) {
      if (this.winRows[i].cols[j].url === '') {
        const params = { t: Math.random(), code: treeNode.id, protocol: 'hls', streamType: '1' };
        getPreviewURLs(params).then(data => {
          if (data.code === '0') {
            let purl = data.data.url;
            if (window.location.host.indexOf('2.2.2.2') === 0) {
              purl = data.data.url.replace('36.32.24.38', '2.2.2.2');
            }
            const title = treeNode.lineName + '>' + treeNode.workareaName + '>' + treeNode.siteName;
            // + '>' + treeNode.title;
            this.winRows[i].cols[j].cameraId = treeNode.id;
            this.winRows[i].cols[j].url = purl;
            this.winRows[i].cols[j].title = title;
          }
        });
        isBreak = true;
        break;
      } else {
        continue;
      }
    }
    if (isBreak) {
      break;
    }
  }
},     

因为牵扯到多分屏的切换,切换后所有播放组件会重新加载。

Logo

前往低代码交流专区

更多推荐