新增实时监控功能

左边是设备通道树,右边是分屏预览
This commit is contained in:
朱俊杰 2022-02-10 14:45:33 +08:00
parent 8cbbf1470b
commit 3789e8616f
24 changed files with 1322 additions and 16 deletions

11
pom.xml
View File

@ -277,5 +277,16 @@
</plugin> </plugin>
</plugins> </plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build> </build>
</project> </project>

View File

@ -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.StreamProxyItem;
import com.genersoft.iot.vmp.media.zlm.dto.StreamPushItem; import com.genersoft.iot.vmp.media.zlm.dto.StreamPushItem;
import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; 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.genersoft.iot.vmp.vmanager.gb28181.platform.bean.ChannelReduce;
import com.github.pagehelper.PageInfo; 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); 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);
/** /**
* 获取某个设备的通道列表 * 获取某个设备的通道列表
* *

View File

@ -1,6 +1,7 @@
package com.genersoft.iot.vmp.storager.dao; package com.genersoft.iot.vmp.storager.dao;
import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; 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 com.genersoft.iot.vmp.vmanager.gb28181.platform.bean.ChannelReduce;
import org.apache.ibatis.annotations.*; import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@ -201,4 +202,6 @@ public interface DeviceChannelMapper {
@Select("SELECT * FROM device_channel WHERE deviceId=#{deviceId} AND status=1") @Select("SELECT * FROM device_channel WHERE deviceId=#{deviceId} AND status=1")
List<DeviceChannel> queryOnlineChannelsByDeviceId(String deviceId); List<DeviceChannel> queryOnlineChannelsByDeviceId(String deviceId);
List<DeviceChannelTree> tree(String deviceId);
} }

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

View File

@ -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.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorager; import com.genersoft.iot.vmp.storager.IVideoManagerStorager;
import com.genersoft.iot.vmp.storager.dao.*; 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.genersoft.iot.vmp.vmanager.gb28181.platform.bean.ChannelReduce;
import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
@ -328,6 +330,11 @@ public class VideoManagerStoragerImpl implements IVideoManagerStorager {
return deviceChannelMapper.queryChannelsByDeviceIdWithStartAndLimit(deviceId, null, query, hasSubChannel, online, start, limit); return deviceChannelMapper.queryChannelsByDeviceIdWithStartAndLimit(deviceId, null, query, hasSubChannel, online, start, limit);
} }
@Override
public List<DeviceChannelTree> tree(String deviceId) {
return ForestNodeMerger.merge(deviceChannelMapper.tree(deviceId));
}
@Override @Override
public List<DeviceChannel> queryChannelsByDeviceId(String deviceId) { public List<DeviceChannel> queryChannelsByDeviceId(String deviceId) {
return deviceChannelMapper.queryChannels(deviceId, null,null, null, null); return deviceChannelMapper.queryChannels(deviceId, null,null, null, null);

View 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);
});
}
}

View 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));
}
}
}

View 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;
}
}
}

View 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;
}
}

View 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;
}
}

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

View 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;
}
}

View 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;
}

View 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;
}
}

View 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;
}

View File

@ -1,32 +1,35 @@
package com.genersoft.iot.vmp.vmanager.bean; package com.genersoft.iot.vmp.vmanager.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WVPResult<T> { public class WVPResult<T> {
private int code; private int code;
private String msg; private String msg;
private T data; private T data;
public int getCode() { private static final Integer SUCCESS = 200;
return code; 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) { public static <T> WVPResult<T> Data(T t) {
this.code = code; return Data(t, "成功");
} }
public String getMsg() { public static <T> WVPResult<T> fail(int code, String msg) {
return msg; return new WVPResult<>(code, msg, null);
} }
public void setMsg(String msg) { public static <T> WVPResult<T> fail(String msg) {
this.msg = msg; return fail(FAILED, msg);
} }
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
} }

View File

@ -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.service.IDeviceService;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage; import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorager; 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.genersoft.iot.vmp.vmanager.bean.WVPResult;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiImplicitParams;
@ -25,6 +27,7 @@ import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.DeferredResult;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@Api(tags = "国标设备查询", value = "国标设备查询") @Api(tags = "国标设备查询", value = "国标设备查询")
@ -431,5 +434,9 @@ public class DeviceQuery {
return result; return result;
} }
@GetMapping("/{deviceId}/tree")
@ApiOperation(value = "通道树形结构", notes = "通道树形结构")
public WVPResult<List<DeviceChannelTree>> tree(@PathVariable String deviceId) {
return WVPResult.Data(storager.tree(deviceId));
}
} }

View 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
}
})
}

View File

@ -2,6 +2,7 @@
<div id="UiHeader"> <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 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="/">控制台</el-menu-item>
<el-menu-item index="/live">实时监控</el-menu-item>
<el-menu-item index="/deviceList">设备列表</el-menu-item> <el-menu-item index="/deviceList">设备列表</el-menu-item>
<el-menu-item index="/pushVideoList">推流列表</el-menu-item> <el-menu-item index="/pushVideoList">推流列表</el-menu-item>
<el-menu-item index="/streamProxyList">拉流代理</el-menu-item> <el-menu-item index="/streamProxyList">拉流代理</el-menu-item>

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

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

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

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

View File

@ -15,6 +15,7 @@ import test from '../components/test.vue'
import web from '../components/setting/Web.vue' import web from '../components/setting/Web.vue'
import sip from '../components/setting/Sip.vue' import sip from '../components/setting/Sip.vue'
import media from '../components/setting/Media.vue' import media from '../components/setting/Media.vue'
import live from '../components/live.vue'
import wasmPlayer from '../components/dialog/jessibuca.vue' import wasmPlayer from '../components/dialog/jessibuca.vue'
import rtcPlayer from '../components/dialog/rtcPlayer.vue' import rtcPlayer from '../components/dialog/rtcPlayer.vue'
@ -34,6 +35,10 @@ export default new VueRouter({
path: '/', path: '/',
component: control, component: control,
}, },
{
path: '/live',
component: live,
},
{ {
path: '/deviceList', path: '/deviceList',
component: deviceList, component: deviceList,