朱俊杰
2022-02-10 37a84e66917d6e22e03e31b0a115e2c16d23ed21
新增实时监控功能

左边是设备通道树,右边是分屏预览
8个文件已修改
16个文件已添加
1338 ■■■■■ 已修改文件
pom.xml 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/IVideoManagerStorager.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.xml 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStoragerImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/utils/CollectionUtil.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/utils/ObjectUtils.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/utils/node/BaseNode.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/utils/node/ForestNode.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/utils/node/ForestNodeManager.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/utils/node/ForestNodeMerger.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/utils/node/INode.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/utils/node/TreeNode.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeviceChannelTree.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeviceChannelTreeNode.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/bean/WVPResult.java 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceQuery.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/api/deviceApi.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/UiHeader.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/channelTree.vue 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/channelTreeItem.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/jessibuca.vue 317 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/live.vue 357 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/router/index.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml
@@ -277,5 +277,16 @@
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
        </resources>
    </build>
</project>
src/main/java/com/genersoft/iot/vmp/storager/IVideoManagerStorager.java
@@ -5,6 +5,7 @@
import com.genersoft.iot.vmp.media.zlm.dto.StreamProxyItem;
import com.genersoft.iot.vmp.media.zlm.dto.StreamPushItem;
import com.genersoft.iot.vmp.service.bean.GPSMsgInfo;
import com.genersoft.iot.vmp.vmanager.bean.DeviceChannelTree;
import com.genersoft.iot.vmp.vmanager.gb28181.platform.bean.ChannelReduce;
import com.github.pagehelper.PageInfo;
@@ -94,6 +95,13 @@
    public List<DeviceChannel> queryChannelsByDeviceIdWithStartAndLimit(String deviceId, String query, Boolean hasSubChannel, Boolean online, int start, int limit);
    /**
     *  获取某个设备的通道树
     * @param deviceId 设备ID
     * @return
     */
    List<DeviceChannelTree> tree(String deviceId);
    /**
     * 获取某个设备的通道列表
     *
     * @param deviceId 设备ID
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java
@@ -1,6 +1,7 @@
package com.genersoft.iot.vmp.storager.dao;
import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
import com.genersoft.iot.vmp.vmanager.bean.DeviceChannelTree;
import com.genersoft.iot.vmp.vmanager.gb28181.platform.bean.ChannelReduce;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;
@@ -201,4 +202,6 @@
    @Select("SELECT * FROM device_channel WHERE deviceId=#{deviceId} AND status=1")
    List<DeviceChannel> queryOnlineChannelsByDeviceId(String deviceId);
    List<DeviceChannelTree> tree(String deviceId);
}
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.xml
New file
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.genersoft.iot.vmp.storager.dao.DeviceChannelMapper">
    <!-- 通用查询映射结果 -->
    <resultMap id="treeNodeResultMap" type="com.genersoft.iot.vmp.vmanager.bean.DeviceChannelTreeNode">
        <id column="id" property="id"/>
        <result column="parentId" property="parentId"/>
        <result column="status" property="status"/>
        <result column="title" property="title"/>
        <result column="value" property="value"/>
        <result column="key" property="key"/>
        <result column="deviceId" property="deviceId"/>
        <result column="channelId" property="channelId"/>
        <result column="longitude" property="lng"/>
        <result column="latitude" property="lat"/>
    </resultMap>
    <select id="tree" resultMap="treeNodeResultMap">
        SELECT
        channelId,
        channelId as id,
        deviceId,
        parentId,
        status,
        name as title,
        channelId as "value",
        channelId as "key",
        channelId,
        longitude,
        latitude
        from device_channel
        where deviceId = #{deviceId}
    </select>
</mapper>
src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStoragerImpl.java
@@ -13,6 +13,8 @@
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorager;
import com.genersoft.iot.vmp.storager.dao.*;
import com.genersoft.iot.vmp.utils.node.ForestNodeMerger;
import com.genersoft.iot.vmp.vmanager.bean.DeviceChannelTree;
import com.genersoft.iot.vmp.vmanager.gb28181.platform.bean.ChannelReduce;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
@@ -329,6 +331,11 @@
    }
    @Override
    public List<DeviceChannelTree> tree(String deviceId) {
        return ForestNodeMerger.merge(deviceChannelMapper.tree(deviceId));
    }
    @Override
    public List<DeviceChannel> queryChannelsByDeviceId(String deviceId) {
        return deviceChannelMapper.queryChannels(deviceId, null,null, null, null);
    }
src/main/java/com/genersoft/iot/vmp/utils/CollectionUtil.java
New file
@@ -0,0 +1,12 @@
package com.genersoft.iot.vmp.utils;
import java.util.Arrays;
public class CollectionUtil {
    public static <T> boolean contains(T[] array, final T element) {
        return array != null && Arrays.stream(array).anyMatch((x) -> {
            return ObjectUtils.nullSafeEquals(x, element);
        });
    }
}
src/main/java/com/genersoft/iot/vmp/utils/ObjectUtils.java
New file
@@ -0,0 +1,41 @@
package com.genersoft.iot.vmp.utils;
import java.util.Arrays;
public class ObjectUtils {
    public static boolean nullSafeEquals(Object o1, Object o2) {
        if (o1 == o2) {
            return true;
        } else if (o1 != null && o2 != null) {
            if (o1.equals(o2)) {
                return true;
            } else {
                return o1.getClass().isArray() && o2.getClass().isArray() && arrayEquals(o1, o2);
            }
        } else {
            return false;
        }
    }
    private static boolean arrayEquals(Object o1, Object o2) {
        if (o1 instanceof Object[] && o2 instanceof Object[]) {
            return Arrays.equals((Object[])((Object[])o1), (Object[])((Object[])o2));
        } else if (o1 instanceof boolean[] && o2 instanceof boolean[]) {
            return Arrays.equals((boolean[])((boolean[])o1), (boolean[])((boolean[])o2));
        } else if (o1 instanceof byte[] && o2 instanceof byte[]) {
            return Arrays.equals((byte[])((byte[])o1), (byte[])((byte[])o2));
        } else if (o1 instanceof char[] && o2 instanceof char[]) {
            return Arrays.equals((char[])((char[])o1), (char[])((char[])o2));
        } else if (o1 instanceof double[] && o2 instanceof double[]) {
            return Arrays.equals((double[])((double[])o1), (double[])((double[])o2));
        } else if (o1 instanceof float[] && o2 instanceof float[]) {
            return Arrays.equals((float[])((float[])o1), (float[])((float[])o2));
        } else if (o1 instanceof int[] && o2 instanceof int[]) {
            return Arrays.equals((int[])((int[])o1), (int[])((int[])o2));
        } else if (o1 instanceof long[] && o2 instanceof long[]) {
            return Arrays.equals((long[])((long[])o1), (long[])((long[])o2));
        } else {
            return o1 instanceof short[] && o2 instanceof short[] && Arrays.equals((short[]) ((short[]) o1), (short[]) ((short[]) o2));
        }
    }
}
src/main/java/com/genersoft/iot/vmp/utils/node/BaseNode.java
New file
@@ -0,0 +1,54 @@
package com.genersoft.iot.vmp.utils.node;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
 * 节点基类
 *
 */
@Data
public class BaseNode<T> implements INode<T> {
    private static final long serialVersionUID = 1L;
    /**
     * 主键ID
     */
    protected String id;
    /**
     * 父节点ID
     */
    protected String parentId;
    /**
     * 子孙节点
     */
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    protected List<T> children = new ArrayList<T>();
    /**
     * 是否有子孙节点
     */
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private Boolean hasChildren;
    /**
     * 是否有子孙节点
     *
     * @return Boolean
     */
    @Override
    public Boolean getHasChildren() {
        if (children.size() > 0) {
            return true;
        } else {
            return this.hasChildren;
        }
    }
}
src/main/java/com/genersoft/iot/vmp/utils/node/ForestNode.java
New file
@@ -0,0 +1,28 @@
package com.genersoft.iot.vmp.utils.node;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * 森林节点类
 *
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class ForestNode extends BaseNode<ForestNode> {
    private static final long serialVersionUID = 1L;
    /**
     * 节点内容
     */
    private Object content;
    public ForestNode(String id, String parentId, Object content) {
        this.id = id;
        this.parentId = parentId;
        this.content = content;
    }
}
src/main/java/com/genersoft/iot/vmp/utils/node/ForestNodeManager.java
New file
@@ -0,0 +1,68 @@
package com.genersoft.iot.vmp.utils.node;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
 * 森林管理类
 *
 * @author smallchill
 */
public class ForestNodeManager<T extends INode<T>> {
    /**
     * 森林的所有节点
     */
    private final ImmutableMap<String, T> nodeMap;
    /**
     * 森林的父节点ID
     */
    private final Map<String, Object> parentIdMap = Maps.newHashMap();
    public ForestNodeManager(List<T> nodes) {
        nodeMap = Maps.uniqueIndex(nodes, INode::getId);
    }
    /**
     * 根据节点ID获取一个节点
     *
     * @param id 节点ID
     * @return 对应的节点对象
     */
    public INode<T> getTreeNodeAt(String id) {
        if (nodeMap.containsKey(id)) {
            return nodeMap.get(id);
        }
        return null;
    }
    /**
     * 增加父节点ID
     *
     * @param parentId 父节点ID
     */
    public void addParentId(String parentId) {
        parentIdMap.put(parentId, "");
    }
    /**
     * 获取树的根节点(一个森林对应多颗树)
     *
     * @return 树的根节点集合
     */
    public List<T> getRoot() {
        List<T> roots = new ArrayList<>();
        nodeMap.forEach((key, node) -> {
            if (node.getParentId() == null || parentIdMap.containsKey(node.getId())) {
                roots.add(node);
            }
        });
        return roots;
    }
}
src/main/java/com/genersoft/iot/vmp/utils/node/ForestNodeMerger.java
New file
@@ -0,0 +1,51 @@
package com.genersoft.iot.vmp.utils.node;
import com.genersoft.iot.vmp.utils.CollectionUtil;
import java.util.List;
/**
 * 森林节点归并类
 *
 */
public class ForestNodeMerger {
    /**
     * 将节点数组归并为一个森林(多棵树)(填充节点的children域)
     * 时间复杂度为O(n^2)
     *
     * @param items 节点域
     * @return 多棵树的根节点集合
     */
    public static <T extends INode<T>> List<T> merge(List<T> items) {
        ForestNodeManager<T> forestNodeManager = new ForestNodeManager<>(items);
        items.forEach(forestNode -> {
            if (forestNode.getParentId() != null) {
                INode<T> node = forestNodeManager.getTreeNodeAt(forestNode.getParentId());
                if (node != null) {
                    node.getChildren().add(forestNode);
                } else {
                    forestNodeManager.addParentId(forestNode.getId());
                }
            }
        });
        return forestNodeManager.getRoot();
    }
    public static <T extends INode<T>> List<T> merge(List<T> items, String[] parentIds) {
        ForestNodeManager<T> forestNodeManager = new ForestNodeManager<>(items);
        items.forEach(forestNode -> {
            if (forestNode.getParentId() != null) {
                INode<T> node = forestNodeManager.getTreeNodeAt(forestNode.getParentId());
                if (CollectionUtil.contains(parentIds, forestNode.getId())){
                    forestNodeManager.addParentId(forestNode.getId());
                } else {
                    if (node != null){
                        node.getChildren().add(forestNode);
                    }
                }
            }
        });
        return forestNodeManager.getRoot();
    }
}
src/main/java/com/genersoft/iot/vmp/utils/node/INode.java
New file
@@ -0,0 +1,42 @@
package com.genersoft.iot.vmp.utils.node;
import java.io.Serializable;
import java.util.List;
/**
 *
 * 节点
 */
public interface INode<T> extends Serializable {
    /**
     * 主键
     *
     * @return String
     */
    String getId();
    /**
     * 父主键
     *
     * @return String
     */
    String getParentId();
    /**
     * 子孙节点
     *
     * @return List<T>
     */
    List<T> getChildren();
    /**
     * 是否有子孙节点
     *
     * @return Boolean
     */
    default Boolean getHasChildren() {
        return false;
    }
}
src/main/java/com/genersoft/iot/vmp/utils/node/TreeNode.java
New file
@@ -0,0 +1,21 @@
package com.genersoft.iot.vmp.utils.node;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * 树型节点类
 *
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class TreeNode extends BaseNode<TreeNode> {
    private static final long serialVersionUID = 1L;
    private String title;
    private String key;
    private String value;
}
src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeviceChannelTree.java
New file
@@ -0,0 +1,50 @@
package com.genersoft.iot.vmp.vmanager.bean;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
import com.genersoft.iot.vmp.utils.node.INode;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.ArrayList;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "DeviceChannelTree对象", description = "DeviceChannelTree对象")
public class DeviceChannelTree extends DeviceChannel implements INode<DeviceChannelTree> {
    private static final long serialVersionUID = 1L;
    /**
     * 主键ID
     */
    private String id;
    /**
     * 父节点ID
     */
    private String parentId;
    private String parentName;
    /**
     * 子孙节点
     */
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private List<DeviceChannelTree> children;
    /**
     * 是否有子孙节点
     */
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private Boolean hasChildren;
    @Override
    public List<DeviceChannelTree> getChildren() {
        if (this.children == null) {
            this.children = new ArrayList<>();
        }
        return this.children;
    }
}
src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeviceChannelTreeNode.java
New file
@@ -0,0 +1,20 @@
package com.genersoft.iot.vmp.vmanager.bean;
import com.genersoft.iot.vmp.utils.node.TreeNode;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class DeviceChannelTreeNode extends TreeNode {
    private Integer status;
    private String deviceId;
    private String channelId;
    private Double lng;
    private Double lat;
}
src/main/java/com/genersoft/iot/vmp/vmanager/bean/WVPResult.java
@@ -1,32 +1,35 @@
package com.genersoft.iot.vmp.vmanager.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WVPResult<T> {
    private int code;
    private String msg;
    private T data;
    public int getCode() {
        return code;
    private static final Integer SUCCESS = 200;
    private static final Integer FAILED = 400;
    public static <T> WVPResult<T> Data(T t, String msg) {
        return new WVPResult<>(SUCCESS, msg, t);
    }
    public void setCode(int code) {
        this.code = code;
    public static <T> WVPResult<T> Data(T t) {
        return Data(t, "成功");
    }
    public String getMsg() {
        return msg;
    public static <T> WVPResult<T> fail(int code, String msg) {
        return new WVPResult<>(code, msg, null);
    }
    public void setMsg(String msg) {
        this.msg = msg;
    public static <T> WVPResult<T> fail(String msg) {
        return fail(FAILED, msg);
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceQuery.java
@@ -10,8 +10,10 @@
import com.genersoft.iot.vmp.service.IDeviceService;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorager;
import com.genersoft.iot.vmp.vmanager.bean.DeviceChannelTree;
import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
import com.github.pagehelper.PageInfo;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
@@ -25,6 +27,7 @@
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.List;
import java.util.UUID;
@Api(tags = "国标设备查询", value = "国标设备查询")
@@ -431,5 +434,9 @@
        return result;
    }
    @GetMapping("/{deviceId}/tree")
    @ApiOperation(value = "通道树形结构", notes = "通道树形结构")
    public WVPResult<List<DeviceChannelTree>> tree(@PathVariable String deviceId) {
        return WVPResult.Data(storager.tree(deviceId));
    }
}
web_src/src/api/deviceApi.js
New file
@@ -0,0 +1,19 @@
import axios from 'axios';
export const tree = (deviceId) => {
  return axios({
    url: `/api/device/query/${deviceId}/tree`,
    method: 'get'
  })
}
export const deviceList = (page, count) => {
  return axios({
    method: 'get',
    url:`/api/device/query/devices`,
    params: {
      page,
      count
    }
  })
}
web_src/src/components/UiHeader.vue
@@ -2,6 +2,7 @@
    <div id="UiHeader">
        <el-menu router :default-active="activeIndex" menu-trigger="click" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b" mode="horizontal">
            <el-menu-item index="/">控制台</el-menu-item>
            <el-menu-item index="/live">实时监控</el-menu-item>
            <el-menu-item index="/deviceList">设备列表</el-menu-item>
            <el-menu-item index="/pushVideoList">推流列表</el-menu-item>
            <el-menu-item index="/streamProxyList">拉流代理</el-menu-item>
web_src/src/components/channelTree.vue
New file
@@ -0,0 +1,70 @@
<template>
  <div>
      <el-tree :data="channelList" :props="props" @node-click="sendDevicePush">
        <span  slot-scope="{ node }">
          <span v-if="node.isLeaf">
            <i class="el-icon-video-camera" :style="{color:node.disabled==1?'#67C23A':'#F56C6C'}"></i>
          </span>
          <span v-else>
            <i class="el-icon-folder"></i>
          </span>
          <span>
            {{ node.label }}
          </span>
        </span>
      </el-tree>
  </div>
</template>
<script>
import ChannelTreeItem from "@/components/channelTreeItem"
import {tree} from '@/api/deviceApi'
export default {
  components: {
    ChannelTreeItem,
  },
  props:{
    device: {
      type: Object,
      required: true
    }
  },
  data() {
      return {
        loading: false,
        channelList: [],
        props: {
          label: 'title',
          children: 'children',
          isLeaf: 'hasChildren',
          disabled: 'status'
        },
      }
  },
  computed: {
  },
  mounted() {
    this.leafs = []
    this.getTree()
  },
  methods: {
    getTree() {
      this.loading = true
      var that = this
      tree(this.device.deviceId).then(function (res) {
          console.log(res.data.data);
          that.channelList = res.data.data;
          that.loading = false;
        }).catch(function (error) {
          console.log(error);
          that.loading = false;
        });
    },
    sendDevicePush(c) {
      if(c.hasChildren) return
      this.$emit('sendDevicePush',c)
    }
  }
}
</script>
web_src/src/components/channelTreeItem.vue
New file
@@ -0,0 +1,74 @@
<template>
  <div>
    <!-- <div :index="item.key" v-for="(item,i) in  list" :key="i+'-'">
      <el-submenu v-if="item.hasChildren">
          <template slot="title">
            <i class="el-icon-video-camera"></i>
            <span slot="title">{{item.title || item.deviceId}}</span>
          </template>
          <channel-list :list="item.children" @sendDevicePush="sendDevicePush"></channel-list>
      </el-submenu>
      <el-menu-item v-else :index="item.key" @click="sendDevicePush(item)">
        <template slot="title" >
          <i class="el-icon-switch-button" :style="{color:item.status==1?'#67C23A':'#F56C6C'}"></i>
          <span slot="title">{{item.title}}</span>
        </template>
      </el-menu-item>
    </div> -->
    <div >
      <template v-if="!item.hasChildren">
          <el-menu-item :index="item.key" @click="sendDevicePush(item)">
            <i class="el-icon-video-camera" :style="{color:item.status==1?'#67C23A':'#F56C6C'}"></i>
            {{item.title}}
          </el-menu-item>
      </template>
      <el-submenu v-else :index="item.key">
        <template slot="title" >
          <i class="el-icon-location-outline"></i>
          {{item.title}}
        </template>
        <template v-for="child in item.children">
          <channel-item
            v-if="child.hasChildren"
            :item="child"
            :key="child.key"
            @sendDevicePush="sendDevicePush"/>
          <el-menu-item v-else :key="child.key" :index="child.key" @click="sendDevicePush(child)">
            <i class="el-icon-video-camera" :style="{color:child.status==1?'#67C23A':'#F56C6C'}"></i>
            {{child.title}}
          </el-menu-item>
        </template>
      </el-submenu>
    </div>
  </div>
</template>
<script>
export default {
  name:'ChannelItem',
  props:{
    list:Array,
    channelId: String,
    item: {
      type: Object,
      required: true
    }
  },
  data () {
    return {
    }
  },
  watch: {
    channelId(val) {
      console.log(val);
    }
  },
  methods: {
    sendDevicePush(c) {
      this.$emit('sendDevicePush',c)
    }
  }
}
</script>
web_src/src/components/jessibuca.vue
New file
@@ -0,0 +1,317 @@
<template>
  <div :id="'jessibuca'+idx" style="width: 100%; height: 100%">
    <div :id="'container'+idx" ref="container" style="width: 100%; height: 100%; background-color: #000" @dblclick="fullscreenSwich">
      <div class="buttons-box" :id="'buttonsBox'+idx">
        <div class="buttons-box-left">
          <i v-if="!playing" class="iconfont icon-play jessibuca-btn" @click="playBtnClick"></i>
          <i v-if="playing" class="iconfont icon-pause jessibuca-btn" @click="pause"></i>
          <i class="iconfont icon-stop jessibuca-btn" @click="destroyButton"></i>
          <i v-if="isNotMute" class="iconfont icon-audio-high jessibuca-btn" @click="jessibuca.mute()"></i>
          <i v-if="!isNotMute" class="iconfont icon-audio-mute jessibuca-btn" @click="jessibuca.cancelMute()"></i>
        </div>
        <div class="buttons-box-right">
          <span class="jessibuca-btn">{{kBps}} kb/s</span>
<!--          <i class="iconfont icon-file-record1 jessibuca-btn"></i>-->
<!--          <i class="iconfont icon-xiangqing2 jessibuca-btn" ></i>-->
          <i class="iconfont icon-camera1196054easyiconnet jessibuca-btn" @click="screenshot" style="font-size: 1rem !important"></i>
          <i class="iconfont icon-shuaxin11 jessibuca-btn" @click="playBtnClick"></i>
          <i v-if="!fullscreen" class="iconfont icon-weibiaoti10 jessibuca-btn" @click="fullscreenSwich"></i>
          <i v-if="fullscreen" class="iconfont icon-weibiaoti11 jessibuca-btn" @click="fullscreenSwich"></i>
        </div>
    </div>
    </div>
  </div>
</template>
<script>
export default {
    name: 'jessibuca',
    data() {
        return {
          jessibuca: null,
          playing: false,
          isNotMute: false,
          quieting: false,
          fullscreen: false,
          loaded: false, // mute
          speed: 0,
          performance: "", // 工作情况
          kBps: 0,
          btnDom: null,
          videoInfo: null,
          volume: 1,
          rotate: 0,
          vod: true, // 点播
          forceNoOffscreen: false,
        };
    },
    props: ['videoUrl', 'error', 'hasAudio', 'height','idx'],
    mounted () {
      window.onerror = (msg) => {
        // console.error(msg)
      };
      let paramUrl = decodeURIComponent(this.$route.params.url)
       this.$nextTick(() =>{
         let dom = document.getElementById("container"+this.idx);
         // dom.style.height = (9/16 ) * dom.clientWidth + "px"
          if (typeof (this.videoUrl) == "undefined") {
            this.videoUrl = paramUrl;
          }
         this.btnDom = document.getElementById("buttonsBox"+this.idx);
          console.log("初始化时的地址为: " + this.videoUrl)
         this.play(this.videoUrl)
        })
    },
    watch:{
        videoUrl(newData, oldData){
            this.play(newData)
        },
        immediate:true
    },
    methods: {
        create(){
          let options =  {};
          console.log(this.$refs.container)
          console.log("hasAudio  " + !!this.hasAudio)
          this.jessibuca = new window.Jessibuca(Object.assign(
            {
              container: this.$refs.container,
              videoBuffer: 0.2, // 最大缓冲时长,单位秒
              isResize: true,
              decoder: "./static/js/jessibuca/index.js",
              // text: "WVP-PRO",
              // background: "bg.jpg",
              loadingText: "加载中",
              hasAudio: !!this.hasAudio,
              debug: false,
              timeout:5,
              supportDblclickFullscreen: false, // 是否支持屏幕的双击事件,触发全屏,取消全屏事件。
              operateBtns: {
                fullscreen: false,
                screenshot: false,
                play: false,
                audio: false,
              },
              record: "record",
              vod: this.vod,
              forceNoOffscreen: this.forceNoOffscreen,
              isNotMute: this.isNotMute,
            },
            options
          ));
          let _this = this;
          this.jessibuca.on("load", function () {
            console.log("on load init");
          });
          this.jessibuca.on("log", function (msg) {
            console.log("on log", msg);
          });
          this.jessibuca.on("record", function (msg) {
            console.log("on record:", msg);
          });
          this.jessibuca.on("pause", function () {
            _this.playing = false;
          });
          this.jessibuca.on("play", function () {
            _this.playing = true;
          });
          this.jessibuca.on("fullscreen", function (msg) {
            console.log("on fullscreen", msg);
            _this.fullscreen = msg
          });
          this.jessibuca.on("mute", function (msg) {
            console.log("on mute", msg);
            _this.isNotMute = !msg;
          });
          this.jessibuca.on("audioInfo", function (msg) {
            // console.log("audioInfo", msg);
          });
          this.jessibuca.on("videoInfo", function (msg) {
            this.videoInfo = msg;
            // console.log("videoInfo", msg);
          });
          this.jessibuca.on("bps", function (bps) {
            // console.log('bps', bps);
          });
          let _ts = 0;
          this.jessibuca.on("timeUpdate", function (ts) {
            // console.log('timeUpdate,old,new,timestamp', _ts, ts, ts - _ts);
            _ts = ts;
          });
          this.jessibuca.on("videoInfo", function (info) {
            console.log("videoInfo", info);
          });
          this.jessibuca.on("error",  (error) =>{
            console.log("error", error);
            this.pause()
          });
          this.jessibuca.on("timeout",  ()=> {
            console.log("timeout");
            // this.pause()
            this.play(this.videoUrl)
          });
          this.jessibuca.on('start', function () {
            console.log('start');
          })
          this.jessibuca.on("performance", function (performance) {
            let show = "卡顿";
            if (performance === 2) {
              show = "非常流畅";
            } else if (performance === 1) {
              show = "流畅";
            }
            _this.performance = show;
          });
          this.jessibuca.on('buffer', function (buffer) {
            // console.log('buffer', buffer);
          })
          this.jessibuca.on('stats', function (stats) {
            // console.log('stats', stats);
          })
          this.jessibuca.on('kBps', function (kBps) {
            _this.kBps = Math.round(kBps);
          });
          // 显示时间戳 PTS
          this.jessibuca.on('videoFrame', function () {
          })
          //
          this.jessibuca.on('metadata', function () {
          });
        },
        playBtnClick: function (event){
          this.play(this.videoUrl)
        },
        play: function (url) {
          console.log(url)
            if (this.jessibuca) {
              this.destroy();
            }
          if(!url){
            return
          }
            this.create();
            this.jessibuca.on("play", () => {
              this.playing = true;
              this.loaded = true;
              this.quieting = this.jessibuca.quieting;
            });
            if (this.jessibuca.hasLoaded()) {
              this.jessibuca.play(url);
            } else {
              this.jessibuca.on("load", () => {
                console.log("load 播放")
                this.jessibuca.play(url);
              });
            }
        },
        pause: function () {
          if (this.jessibuca) {
            this.jessibuca.pause();
          }
          this.playing = false;
          this.err = "";
          this.performance = "";
        },
        destroy: function () {
          if (this.jessibuca) {
            this.jessibuca.destroy();
          }
          if (document.getElementById("buttonsBox"+this.idx) == null) {
            document.getElementById("container"+this.idx).appendChild(this.btnDom)
          }
          this.jessibuca = null;
          this.playing = false;
          this.err = "";
          this.performance = "";
        },
        eventcallbacK: function(type, message) {
            // console.log("player 事件回调")
            // console.log(type)
            // console.log(message)
        },
        fullscreenSwich: function (){
            let isFull = this.isFullscreen()
            this.jessibuca.setFullscreen(!isFull)
            this.fullscreen = !isFull;
        },
        isFullscreen: function (){
          return document.fullscreenElement ||
            document.msFullscreenElement  ||
            document.mozFullScreenElement ||
            document.webkitFullscreenElement || false;
        },
      resize(){
          this.jessibuca.resize()
      },
      screenshot(){
        this.jessibuca.screenshot('截图','png',0.5)
        // let base64 = this.jessibuca.screenshot("shot","jpeg",0.5,'base64')
        // this.$emit('screenshot',base64)
      },
      destroyButton() {
        this.$emit('destroy', this.idx)
        this.destroy()
      }
    },
    destroyed() {
      if (this.jessibuca) {
        this.jessibuca.destroy();
      }
      this.playing = false;
      this.loaded = false;
      this.performance = "";
    },
}
</script>
<style>
  .buttons-box{
    width: 100%;
    height: 28px;
    background-color: rgba(43, 51, 63, 0.7);
    position: absolute;
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    left: 0;
    bottom: 0;
    user-select: none;
    z-index: 10;
  }
  .jessibuca-btn{
    width: 20px;
    color: rgb(255, 255, 255);
    line-height: 27px;
    margin: 0px 10px;
    padding: 0px 2px;
    cursor: pointer;
    text-align: center;
    font-size: 0.8rem !important;
  }
  .buttons-box-right {
    position: absolute;
    right: 0;
  }
</style>
web_src/src/components/live.vue
New file
@@ -0,0 +1,357 @@
<template>
  <div id="devicePosition" style="height: 100%">
    <el-container style="height: 100%">
      <el-header>
        <uiHeader></uiHeader>
      </el-header>
      <el-container v-loading="loading" element-loading-text="拼命加载中">
        <el-aside width="300px" style="background-color: #ffffff">
          <div style="text-align: center;padding-top: 20px;">设备列表</div>
          <el-menu  v-loading="loading">
            <el-submenu v-for="device in deviceList" :key="device.deviceId" :index="device.deviceId" @click="sendDevicePush(item)">
              <template slot="title" >
                <i class="el-icon-location-outline"></i>
                {{device.name}}
              </template>
              <ChannelTree :device="device" @sendDevicePush="sendDevicePush"></ChannelTree>
            </el-submenu>
          </el-menu>
        </el-aside>
          <el-container>
            <!-- <LivePlay></LivePlay> -->
            <el-header height="40px" style="text-align: left;font-size: 17px;line-height: 40px;">
              分屏:
              <i class="el-icon-full-screen btn" :class="{active:spilt==1}" @click="spilt=1"/>
              <i class="el-icon-menu btn" :class="{active:spilt==4}" @click="spilt=4"/>
              <i class="el-icon-s-grid btn" :class="{active:spilt==9}" @click="spilt=9"/>
            </el-header>
            <el-main>
              <div style="width: 100%;height: calc( 100vh - 110px );display: flex;flex-wrap: wrap;background-color: #000;">
                <div v-for="i in spilt" :key="i" class="play-box"
                    :style="liveStyle" :class="{redborder:playerIdx == (i-1)}"
                    @click="playerIdx = (i-1)"
                >
                  <div v-if="!videoUrl[i-1]" style="color: #ffffff;font-size: 30px;font-weight: bold;">{{i}}</div>
                  <player v-else :ref="'player'+i" :videoUrl="videoUrl[i-1]"  fluent autoplay :height="true"
                          :idx="'player'+i" @screenshot="shot" @destroy="destroy"></player>
                  <!-- <player v-else ref="'player'+i" :idx="'player'+i" :visible.sync="showVideoDialog" :videoUrl="videoUrl[i-1]"  :height="true" :hasAudio="hasAudio" fluent autoplay live ></player> -->
                </div>
              </div>
            </el-main>
          </el-container>
      </el-container>
    </el-container>
  </div>
</template>
<script>
  import uiHeader from "./UiHeader.vue";
  import player from './jessibuca.vue'
  import ChannelTree from './channelTree.vue'
  export default {
    name: "live",
    components: {
      uiHeader, player, ChannelTree
    },
    data() {
      return {
        showVideoDialog: true,
        hasAudio: false,
        videoUrl:[''],
        spilt:1,//分屏
        playerIdx:0,//激活播放器
        deviceList: [], //设备列表
        currentDevice: {}, //当前操作设备对象
        videoComponentList: [],
        updateLooper: 0, //数据刷新轮训标志
        currentDeviceChannelsLenth:0,
        winHeight: window.innerHeight - 200,
        currentPage:1,
        count:15,
        total:0,
        getDeviceListLoading: false,
        //channel
        searchSrt: "",
        channelType: "",
        online: "",
        channelTotal:0,
        deviceChannelList:[],
        loading:false
      };
    },
    mounted() {
      this.initData();
    },
    created(){
      this.checkPlayByParam()
    },
    computed:{
      liveStyle(){
        if(this.spilt==1){
          return {width:'100%',height:'100%'}
        }else if(this.spilt==4){
          return {width:'49%',height:'49%'}
        }else if(this.spilt==9){
          return {width:'32%',height:'32%'}
        }
      }
    },
    watch:{
      spilt(newValue){
        console.log("切换画幅;"+newValue)
        let that = this
        for (let i = 1; i <= newValue; i++) {
          if(!that.$refs['player'+i]){
            continue
          }
          this.$nextTick(()=>{
            if(that.$refs['player'+i] instanceof Array){
              that.$refs['player'+i][0].resize()
            }else {
              that.$refs['player'+i].resize()
            }
          })
        }
        window.localStorage.setItem('split',newValue)
      },
      '$route.fullPath':'checkPlayByParam'
    },
    destroyed() {
      clearTimeout(this.updateLooper);
    },
    methods: {
      initData: function () {
        this.getDeviceList();
      },
      destroy(idx) {
        console.log(idx);
        this.clear(idx.substring(idx.length-1))
      },
      getDeviceList: function() {
        let that = this;
        this.$axios({
          method: 'get',
          url:`/api/device/query/devices`,
          params: {
            page: that.currentPage,
            count: that.count
          }
        }).then(function (res) {
          console.log(res.data.list);
          that.total = res.data.total;
          that.deviceList = res.data.list.map(item=>{return {deviceChannelList:[],...item}});
          that.getDeviceListLoading = false;
        }).catch(function (error) {
          console.log(error);
          that.getDeviceListLoading = false;
        });
      },
      //通知设备上传媒体流
      sendDevicePush: function (itemData) {
        if(itemData.status===0){
          this.$message.error('设备离线!');
          return
        }
        this.save(itemData)
        let deviceId = itemData.deviceId;
        // this.isLoging = true;
        let channelId = itemData.channelId;
        console.log("通知设备推流1:" + deviceId + " : " + channelId );
        let idxTmp = this.playerIdx
        let that = this;
        this.loading = true
        this.$axios({
          method: 'get',
          url: '/api/play/start/' + deviceId + '/' + channelId
        }).then(function (res) {
          // that.isLoging = false;
          console.log('=====----=====')
          console.log(res)
          if (res.data.code == 0 && res.data.data) {
            itemData.playUrl = res.data.data.httpsFlv
            that.setPlayUrl(res.data.data.ws_flv,idxTmp)
          }else {
            that.$message.error(res.data.msg);
          }
        }).catch(function (e) {
        }).finally(()=>{
          that.loading = false
        });
      },
      setPlayUrl(url,idx){
        this.$set(this.videoUrl,idx,url)
        let _this = this
        setTimeout(()=>{
          window.localStorage.setItem('videoUrl',JSON.stringify(_this.videoUrl))
        },100)
      },
      checkPlayByParam(){
        let {deviceId,channelId} = this.$route.query
        if(deviceId && channelId){
          this.sendDevicePush({deviceId,channelId})
        }
      },
      convertImageToCanvas(image) {
        var canvas = document.createElement("canvas");
        canvas.width = image.width;
        canvas.height = image.height;
        canvas.getContext("2d").drawImage(image, 0, 0);
        return canvas;
      },
      shot(e){
        // console.log(e)
        // send({code:'image',data:e})
        var base64ToBlob = function(code) {
          let parts = code.split(';base64,');
          let contentType = parts[0].split(':')[1];
          let raw = window.atob(parts[1]);
          let rawLength = raw.length;
          let uInt8Array = new Uint8Array(rawLength);
          for(let i = 0; i < rawLength; ++i) {
              uInt8Array[i] = raw.charCodeAt(i);
          }
          return new Blob([uInt8Array], {
              type: contentType
          });
        };
        let aLink = document.createElement('a');
        let blob = base64ToBlob(e); //new Blob([content]);
        let evt = document.createEvent("HTMLEvents");
        evt.initEvent("click", true, true); //initEvent 不加后两个参数在FF下会报错  事件类型,是否冒泡,是否阻止浏览器的默认行为
        aLink.download = '截图';
        aLink.href = URL.createObjectURL(blob);
        aLink.click();
      },
      save(item){
        let dataStr = window.localStorage.getItem('playData') || '[]'
        let data = JSON.parse(dataStr);
        data[this.playerIdx] = item
        window.localStorage.setItem('playData',JSON.stringify(data))
      },
      clear(idx) {
        let dataStr = window.localStorage.getItem('playData') || '[]'
        let data = JSON.parse(dataStr);
        data[idx-1] = null;
        console.log(data);
        window.localStorage.setItem('playData',JSON.stringify(data))
      },
      loadAndPlay(){
        let dataStr = window.localStorage.getItem('playData') || '[]'
        let data = JSON.parse(dataStr);
        data.forEach((item,i)=>{
          if(item){
            this.playerIdx = i
            this.sendDevicePush(item)
          }
        })
      }
    }
  };
</script>
<style>
  .btn{
    margin: 0 10px;
  }
  .btn:hover{
      color: #409EFF;
  }
  .btn.active{
    color: #409EFF;
  }
  .redborder{
    border: 2px solid red !important;
  }
  .play-box{
    background-color: #000000;
    border: 2px solid #505050;
    display: flex;
    align-items: center;
    justify-content: center;
  }
</style>
<style>
  .videoList {
    display: flex;
    flex-wrap: wrap;
    align-content: flex-start;
  }
  .video-item {
    position: relative;
    width: 15rem;
    height: 10rem;
    margin-right: 1rem;
    background-color: #000000;
  }
  .video-item-img {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    margin: auto;
    width: 100%;
    height: 100%;
  }
  .video-item-img:after {
    content: "";
    display: inline-block;
    position: absolute;
    z-index: 2;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    margin: auto;
    width: 3rem;
    height: 3rem;
    background-image: url("../assets/loading.png");
    background-size: cover;
    background-color: #000000;
  }
  .video-item-title {
    position: absolute;
    bottom: 0;
    color: #000000;
    background-color: #ffffff;
    line-height: 1.5rem;
    padding: 0.3rem;
    width: 14.4rem;
  }
  .baidumap {
    width: 100%;
    height: 100%;
    border: none;
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    margin: auto;
  }
  /* 去除百度地图版权那行字 和 百度logo */
  .baidumap > .BMap_cpyCtrl {
    display: none !important;
  }
  .baidumap > .anchorBL {
    display: none !important;
  }
</style>
web_src/src/router/index.js
@@ -15,6 +15,7 @@
import web from '../components/setting/Web.vue'
import sip from '../components/setting/Sip.vue'
import media from '../components/setting/Media.vue'
import live from '../components/live.vue'
import wasmPlayer from '../components/dialog/jessibuca.vue'
import rtcPlayer from '../components/dialog/rtcPlayer.vue'
@@ -35,6 +36,10 @@
      component: control,
    },
    {
      path: '/live',
      component: live,
    },
    {
      path: '/deviceList',
      component: deviceList,
    },