瀏覽代碼

Merge remote-tracking branch 'github/wvp-28181-2.0' into wvp-28181-2.0

648540858 3 年之前
父節點
當前提交
d4fe0fb325
共有 23 個文件被更改,包括 1313 次插入15 次删除
  1. 11 0
      pom.xml
  2. 8 0
      src/main/java/com/genersoft/iot/vmp/storager/IVideoManagerStorager.java
  3. 17 0
      src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java
  4. 7 0
      src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStoragerImpl.java
  5. 12 0
      src/main/java/com/genersoft/iot/vmp/utils/CollectionUtil.java
  6. 41 0
      src/main/java/com/genersoft/iot/vmp/utils/ObjectUtils.java
  7. 54 0
      src/main/java/com/genersoft/iot/vmp/utils/node/BaseNode.java
  8. 28 0
      src/main/java/com/genersoft/iot/vmp/utils/node/ForestNode.java
  9. 68 0
      src/main/java/com/genersoft/iot/vmp/utils/node/ForestNodeManager.java
  10. 51 0
      src/main/java/com/genersoft/iot/vmp/utils/node/ForestNodeMerger.java
  11. 42 0
      src/main/java/com/genersoft/iot/vmp/utils/node/INode.java
  12. 21 0
      src/main/java/com/genersoft/iot/vmp/utils/node/TreeNode.java
  13. 65 0
      src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeviceChannelTree.java
  14. 20 0
      src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeviceChannelTreeNode.java
  15. 17 14
      src/main/java/com/genersoft/iot/vmp/vmanager/bean/WVPResult.java
  16. 8 1
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceQuery.java
  17. 19 0
      web_src/src/api/deviceApi.js
  18. 1 0
      web_src/src/components/UiHeader.vue
  19. 70 0
      web_src/src/components/channelTree.vue
  20. 74 0
      web_src/src/components/channelTreeItem.vue
  21. 317 0
      web_src/src/components/jessibuca.vue
  22. 357 0
      web_src/src/components/live.vue
  23. 5 0
      web_src/src/router/index.js

+ 11 - 0
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>

+ 8 - 0
src/main/java/com/genersoft/iot/vmp/storager/IVideoManagerStorager.java

@@ -5,6 +5,7 @@ import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
 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;
 
@@ -93,6 +94,13 @@ public interface IVideoManagerStorager {
 	
 	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);
+
 	/**
 	 * 获取某个设备的通道列表
 	 *

+ 17 - 0
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,20 @@ public interface DeviceChannelMapper {
 
     @Select("SELECT * FROM device_channel WHERE deviceId=#{deviceId} AND status=1")
     List<DeviceChannel> queryOnlineChannelsByDeviceId(String deviceId);
+
+    @Select(" SELECT\n" +
+            "        channelId,\n" +
+            "        channelId as id,\n" +
+            "        deviceId,\n" +
+            "        parentId,\n" +
+            "        status,\n" +
+            "        name as title,\n" +
+            "        channelId as \"value\",\n" +
+            "        channelId as \"key\",\n" +
+            "        channelId,\n" +
+            "        longitude,\n" +
+            "        latitude\n" +
+            "        from device_channel\n" +
+            "        where deviceId = #{deviceId}")
+    List<DeviceChannelTree> tree(String deviceId);
 }

+ 7 - 0
src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStoragerImpl.java

@@ -13,6 +13,8 @@ import com.genersoft.iot.vmp.service.bean.GPSMsgInfo;
 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;
@@ -328,6 +330,11 @@ public class VideoManagerStoragerImpl implements IVideoManagerStorager {
 		return deviceChannelMapper.queryChannelsByDeviceIdWithStartAndLimit(deviceId, null, query, hasSubChannel, online, start, limit);
 	}
 
+	@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);

+ 12 - 0
src/main/java/com/genersoft/iot/vmp/utils/CollectionUtil.java

@@ -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);
+        });
+    }
+}

+ 41 - 0
src/main/java/com/genersoft/iot/vmp/utils/ObjectUtils.java

@@ -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));
+        }
+    }
+}

+ 54 - 0
src/main/java/com/genersoft/iot/vmp/utils/node/BaseNode.java

@@ -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;
+		}
+	}
+
+}

+ 28 - 0
src/main/java/com/genersoft/iot/vmp/utils/node/ForestNode.java

@@ -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;
+	}
+
+}

+ 68 - 0
src/main/java/com/genersoft/iot/vmp/utils/node/ForestNodeManager.java

@@ -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;
+	}
+
+}

+ 51 - 0
src/main/java/com/genersoft/iot/vmp/utils/node/ForestNodeMerger.java

@@ -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();
+	}
+}

+ 42 - 0
src/main/java/com/genersoft/iot/vmp/utils/node/INode.java

@@ -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;
+	}
+
+}

+ 21 - 0
src/main/java/com/genersoft/iot/vmp/utils/node/TreeNode.java

@@ -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;
+}

+ 65 - 0
src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeviceChannelTree.java

@@ -0,0 +1,65 @@
+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;
+
+    private String title;
+
+    private String key;
+
+    private String value;
+
+    /**
+     * 子孙节点
+     */
+    @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;
+    }
+
+    @Override
+    public Boolean getHasChildren() {
+        if (children.size() > 0) {
+            return true;
+        } else {
+            return this.hasChildren;
+        }
+    }
+}

+ 20 - 0
src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeviceChannelTreeNode.java

@@ -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;
+}

+ 17 - 14
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 void setCode(int code) {
-        this.code = code;
+    public static <T> WVPResult<T> Data(T t, String msg) {
+        return new WVPResult<>(SUCCESS, msg, t);
     }
 
-    public String getMsg() {
-        return msg;
+    public static <T> WVPResult<T> Data(T t) {
+        return Data(t, "成功");
     }
 
-    public void setMsg(String msg) {
-        this.msg = msg;
+    public static <T> WVPResult<T> fail(int code, String msg) {
+        return new WVPResult<>(code, msg, null);
     }
 
-    public T getData() {
-        return data;
+    public static <T> WVPResult<T> fail(String msg) {
+        return fail(FAILED, msg);
     }
 
-    public void setData(T data) {
-        this.data = data;
-    }
 }

+ 8 - 1
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceQuery.java

@@ -10,8 +10,10 @@ import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
 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.util.StringUtils;
 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 @@ public class DeviceQuery {
 		return result;
 	}
 
-
+	@GetMapping("/{deviceId}/tree")
+	@ApiOperation(value = "通道树形结构", notes = "通道树形结构")
+	public WVPResult<List<DeviceChannelTree>> tree(@PathVariable String deviceId) {
+		return WVPResult.Data(storager.tree(deviceId));
+	}
 }

+ 19 - 0
web_src/src/api/deviceApi.js

@@ -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
+    }
+  })
+}

+ 1 - 0
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>

+ 70 - 0
web_src/src/components/channelTree.vue

@@ -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>

+ 74 - 0
web_src/src/components/channelTreeItem.vue

@@ -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>

+ 317 - 0
web_src/src/components/jessibuca.vue

@@ -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>

+ 357 - 0
web_src/src/components/live.vue

@@ -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>

+ 5 - 0
web_src/src/router/index.js

@@ -15,6 +15,7 @@ import test from '../components/test.vue'
 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'
@@ -34,6 +35,10 @@ export default new VueRouter({
       path: '/',
       component: control,
     },
+    {
+      path: '/live',
+      component: live,
+    },
     {
       path: '/deviceList',
       component: deviceList,