Kaynağa Gözat

1. 添加微调逻辑.
2. 新增支持微调的简化接口

kindring 2 yıl önce
ebeveyn
işleme
0e152b9637

+ 2 - 1
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java

@@ -78,7 +78,8 @@ public interface ISIPCommander {
      * @param zoomSpeed  镜头缩放速度
 	 */
 	void ptzCmd(Device device,String channelId,int leftRight, int upDown, int inOut, int moveSpeed, int zoomSpeed) throws InvalidArgumentException, SipException, ParseException;
-	
+
+	void ptzCmdNew(Device device,String channelId,int direction,int speed) throws InvalidArgumentException, SipException, ParseException;
 	/**
 	 * 前端控制,包括PTZ指令、FI指令、预置位指令、巡航指令、扫描指令和辅助开关指令
 	 * 

+ 25 - 0
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java

@@ -210,6 +210,31 @@ public class SIPCommander implements ISIPCommander {
         transmitRequest(device.getTransport(), request);
     }
 
+
+    @Override
+    public void ptzCmdNew(Device device,String channelId,int direction,int speed) throws InvalidArgumentException, SipException, ParseException{
+        String cmdStr = SipUtils.cmdPtzString(direction, speed);
+        StringBuffer ptzXml = new StringBuffer(200);
+        String charset = device.getCharset();
+        ptzXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
+        ptzXml.append("<Control>\r\n");
+        ptzXml.append("<CmdType>DeviceControl</CmdType>\r\n");
+        ptzXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
+        ptzXml.append("<DeviceID>" + channelId + "</DeviceID>\r\n");
+        ptzXml.append("<PTZCmd>" + cmdStr + "</PTZCmd>\r\n");
+        ptzXml.append("<Info>\r\n");
+        ptzXml.append("<ControlPriority>5</ControlPriority>\r\n");
+        ptzXml.append("</Info>\r\n");
+        ptzXml.append("</Control>\r\n");
+
+        CallIdHeader callIdHeader = device.getTransport().equalsIgnoreCase("TCP") ? tcpSipProvider.getNewCallId()
+                : udpSipProvider.getNewCallId();
+
+        Request request = headerProvider.createMessageRequest(device, ptzXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null, callIdHeader);
+
+        transmitRequest(device.getTransport(), request);
+    };
+
     /**
      * 前端控制,包括PTZ指令、FI指令、预置位指令、巡航指令、扫描指令和辅助开关指令
      *

+ 47 - 4
src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java

@@ -1,9 +1,12 @@
 package com.genersoft.iot.vmp.gb28181.utils;
 
 import com.genersoft.iot.vmp.utils.GitUtil;
+import com.genersoft.iot.vmp.vmanager.gb28181.ptz.PtzController;
 import gov.nist.javax.sip.address.AddressImpl;
 import gov.nist.javax.sip.address.SipUri;
 import gov.nist.javax.sip.header.Subject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javax.sip.PeerUnavailableException;
 import javax.sip.SipFactory;
@@ -16,6 +19,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 
+
 /**
  * @author panlinlin
  * @version 1.0.0
@@ -23,7 +27,7 @@ import java.util.UUID;
  * @createTime 2021年09月27日 15:12:00
  */
 public class SipUtils {
-
+    private final static Logger logger = LoggerFactory.getLogger(PtzController.class);
     public static String getUserIdFromFromHeader(Request request) {
         FromHeader fromHeader = (FromHeader)request.getHeader(FromHeader.NAME);
         return getUserIdFromFromHeader(fromHeader);
@@ -102,14 +106,14 @@ public class SipUtils {
         }
 
         StringBuilder builder = new StringBuilder("A50F01");
-        //A50F01 A5 0F 01
+        //A5 0F 01 01 ff ff
         String strTmp;
         strTmp = String.format("%02X", cmdCode);
         builder.append(strTmp, 0, 2);
-        strTmp = String.format("%02X", moveSpeed);
+        strTmp = String.format("%02X", moveSpeed);// 云台  +- => 15
         builder.append(strTmp, 0, 2);
         builder.append(strTmp, 0, 2);
-        strTmp = String.format("%X", zoomSpeed);
+        strTmp = String.format("%X", zoomSpeed);// 镜头 abcd => 00 15
         builder.append(strTmp, 0, 1).append("0");
         //计算校验码
         int checkCode = (0XA5 + 0X0F + 0X01 + cmdCode + moveSpeed + moveSpeed + (zoomSpeed /*<< 4*/ & 0XF0)) % 0X100;
@@ -118,4 +122,43 @@ public class SipUtils {
         return builder.toString();
     }
 
+    /**
+     *
+     * @param direction 云台移动方向 1:上 2:下 3:左 4:右
+     * @param speed 移动速度
+     * @return
+     */
+    public static String cmdPtzString(int direction,int speed){
+        int cmdCode = 0;
+        if (direction == 4) {
+            cmdCode|=0x01;		// 右移 0 03
+        } else if(direction == 3) {
+            cmdCode|=0x02;		// 左移 04
+        }else if(direction == 2) {
+            cmdCode|=0x04;		// 下移 05
+        }else if(direction == 1) {
+            cmdCode|=0x08;		// 上移 06
+        }
+        int moveSpeed =0 ;
+        moveSpeed |= 0x00;
+        int zoomSpeed =0 ;
+        moveSpeed |= speed;
+        StringBuilder builder = new StringBuilder("A50F01");
+        //A5 0F 01 01 ff ff
+        String strTmp;
+        strTmp = String.format("%02X", cmdCode);
+        builder.append(strTmp, 0, 2);
+        strTmp = String.format("%02X", moveSpeed);// 云台  +- => 15
+        builder.append(strTmp, 0, 2);
+        builder.append(strTmp, 0, 2);
+        strTmp = String.format("%X", zoomSpeed);// 镜头 abcd => 00 15
+        builder.append(strTmp, 0, 1).append("0");
+        //计算校验码
+        int checkCode = (0XA5 + 0X0F + 0X01 + cmdCode + moveSpeed + moveSpeed + (zoomSpeed /*<< 4*/ & 0XF0)) % 0X100;
+        strTmp = String.format("%02X", checkCode);
+        builder.append(strTmp, 0, 2);
+
+        logger.info("cmd code --------{}",strTmp);
+        return builder.toString();
+    }
 }

+ 39 - 0
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/ptz/PtzController.java

@@ -111,6 +111,45 @@ public class PtzController {
 		}
 	}
 
+
+	@Operation(summary = "云台控制新接口")
+	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
+	@Parameter(name = "channelId", description = "通道国标编号", required = true)
+	@Parameter(name = "command", description = "控制指令,允许值: left, right, up, down, stop", required = true)
+	@Parameter(name = "speed", description = "移动速度", required = false)
+	@PostMapping("/c/{deviceId}/{channelId}")
+	public void newPTZ(@PathVariable String deviceId,@PathVariable String channelId, String command,int speed){
+		if (logger.isDebugEnabled()) {
+			logger.debug(String.format("设备云台控制 API调用,deviceId:%s ,channelId:%s ,command:%s ,speed:%d ",deviceId, channelId, command, speed));
+		}
+		Device device = storager.queryVideoDevice(deviceId);
+		int cmdCode = 0;
+		switch (command){
+			case "left":
+				cmdCode = 3;
+				break;
+			case "right":
+				cmdCode = 4;
+				break;
+			case "up":
+				cmdCode = 1;
+				break;
+			case "down":
+				cmdCode = 2;
+				break;
+			case "stop":
+				break;
+			default:
+				break;
+		}
+		try {
+			cmder.ptzCmdNew(device, channelId, cmdCode,speed);
+		} catch (SipException | InvalidArgumentException | ParseException e) {
+			logger.error("[命令发送失败] 云台控制: {}", e.getMessage());
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
+		}
+	}
+
 	@Operation(summary = "聚焦")
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)

+ 131 - 1
web_src/src/components/common/ptzControl.vue

@@ -1,7 +1,7 @@
 <template>
   <div style="display: flex; justify-content: left;">
     <div class="control-wrapper">
-      <div class="control-btn control-top" @mousedown="ptzCamera('up')" @mouseup="ptzCamera('stop')">
+      <div class="control-btn control-top" @mousedown="ptzControlHandleDown('up')" @mouseup="ptzControlHandleUp('up')">
         <i class="el-icon-caret-top"></i>
         <div class="control-inner-btn control-inner"></div>
       </div>
@@ -78,6 +78,22 @@ let sendStopTimer = null;
 let changToLongDownStateTimer = null;
 // 是否长按
 let isLongDown = null;
+
+// 长按与连续短按的时间
+let pressDuration = 1000;
+
+// 从第一次按下按钮到执行命令时的重复时间
+let clickDuration = 700;
+// 连点持续时间
+let conClickDuration = 350;
+
+// 计时器
+let clickTimer = null;
+let conClickTimer = null;
+let pressTimer = null;
+let sendEndTimer = null;
+
+
 export default {
   name: "ptzControl",
   props:{
@@ -96,6 +112,14 @@ export default {
       cruisingGroup: 0,
       scanSpeed: 100,
       scanGroup: 0,
+      direction: '',// 当前按下的按钮的方向
+      step:0,//步长
+      stepValue:5,//
+      clickCount: 0,// 连续点击数量
+      isClick: true,// 是否为点击
+      isLongClick: false,// 是否为连续点击
+      isPress: false,// 是否为长按
+      isSendAutoMove: false,// 是否为长按命令倒计时中
     }
   },
   methods:{
@@ -201,6 +225,112 @@ export default {
         url: '/api/ptz/front_end_command/' + this.deviceId + '/' + this.channelId + '?cmdCode=' + cmdCode + '&parameter1=' + groupNum + '&parameter2=' + parameter + '&combindCode2=0'
       }).then(function (res) {});
     },
+    /**
+     * 按下云台控制按钮逻辑
+     * @param direction 方向
+     */
+    ptzControlHandleDown(direction){
+      if(sendStopTimer){
+        clearTimeout(sendStopTimer);
+      }
+      console.log('--------------按下')
+      this.direction = direction;
+      this.isPress = false;
+      this.isClick = true;
+      // 1200
+      pressTimer = setTimeout(()=>{
+        console.log('长按')
+        // 长按
+        this.isPress = true;
+        this.isClick=false;
+        this.clickCount = 0;
+        this.isSendAutoMove = false;
+        // 发送云台移动命令,步长为0,视作连续移动
+        this.sendCommand(this.direction,0);
+        // 等待850毫秒,如果850毫秒内的抬起了鼠标,则发送停止命令
+        setTimeout(()=>{
+          console.log(`按下的延迟isSendAutoMove: ${this.isSendAutoMove}`)
+          if(this.isSendAutoMove){
+            console.log('长按抬起延迟结束')
+            this.sendCommand('stop',0);
+          }
+          this.isSendAutoMove = true;
+        },600);
+
+      },pressDuration);
+
+      // 处于连点状态,刷新连点计时器
+      clearTimeout(conClickTimer);
+      conClickTimer = null;
+
+
+    },
+    /**
+     * 松开按钮逻辑
+     * @param command
+     */
+    ptzControlHandleUp(command){
+      if(this.isPress){
+        // 如果没有抬起
+        console.log(111111111)
+        let _isSendAutoMove = this.isSendAutoMove;
+        if(!_isSendAutoMove){
+          console.log('长按')
+          this.isSendAutoMove = true;
+          return;
+        }
+        console.log('长按抬起结束')
+        this.sendCommand('stop',0)
+      }else{
+        clearTimeout(pressTimer);
+        this.clickCount ++;
+
+        conClickTimer = setTimeout(()=>{
+          // 结束连点,合并命令.
+          this.isClick = false;
+          conClickTimer=null;
+          clickTimer=null;
+          // 发送
+          console.log(`快速点按${this.clickCount}`)
+          clearTimeout(clickTimer);
+          this.sendCommand(this.direction,this.clickCount);
+        },conClickDuration);
+
+        if(!clickTimer){
+          clickTimer = setTimeout(()=>{
+            // 连点超时
+            if(this.isClick){
+              clearTimeout(conClickTimer);
+              conClickTimer=null;
+              clickTimer=null;
+              // 强制发送当前的
+              console.log(`连按缓存${this.clickCount}`);
+              this.sendCommand(this.direction,this.clickCount);
+            }
+          },clickDuration)
+        }
+
+
+      }
+    },
+    /**
+     * 发送命令至服务器
+     * @param command
+     * @param step
+     * @returns {Promise<void>}
+     */
+    async sendCommand(command,step=0){
+      console.log(`[send] ${command} - ${step}`);
+      // step = step * 5
+      let url = `/api/ptz/c/${this.deviceId}/${this.channelId}/?c=${command}&step=${step*this.stepValue}`
+      console.log(url);
+      let [err,res] = await handle(this.$axios({
+        method: 'post',
+        url: url
+      }));
+      this.clickCount = 0;
+      if(err){console.error(err)}
+    }
   }
 }
 </script>

+ 59 - 13
web_src/src/components/live.vue

@@ -1,18 +1,29 @@
 <template>
-  <div id="devicePosition" style="width:100vw; height: 91vh">
-    <el-container v-loading="loading" style="height: 91vh;" element-loading-text="拼命加载中">
-      <el-aside width="300px" style="background-color: #ffffff">
+  <div id="devicePosition" :style="`width:100vw; height: ${isFullScreen?'100vh':'91vh'}`"  ref="container">
+    <el-container v-loading="loading" style="height: 100%;" element-loading-text="拼命加载中" >
+      <el-aside width="300px" v-show="asideHide" :style="`background-color: ${isFullScreen?'#eee':'#ffffff'}`">
         <DeviceTree :clickEvent="clickEvent" :contextMenuEvent="contextMenuEvent"></DeviceTree>
       </el-aside>
       <el-container>
-        <el-header height="5vh" style="text-align: left;font-size: 17px;line-height:5vh">
-          分屏:
-          <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 height="5vh"
+                   :style="`text-align: left;font-size: 17px;line-height:5vh;
+                   background-color:${isFullScreen?'#000':'#fff'};
+                   color:${!isFullScreen?'#000':'#fff'}`
+">
+          <div class="b-left">
+            <el-button :icon="`el-icon-s-${asideHide?'fold':'unfold'}`" circle size="small" @click="asideHide=!asideHide" :type="isFullScreen?'goon':''"></el-button>
+            <el-button :icon="`el-icon-${isFullScreen?'files':'full-screen'}`" circle size="small" @click="switchFullScreenHandle" :type="isFullScreen?'goon':''"></el-button>
+            分屏监控:
+            <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"/>
+          </div>
+          <div class="b-right">
+            <el-button></el-button>
+          </div>
         </el-header>
-        <el-main style="padding: 0;">
-          <div style="width: 99%;height: 85vh;display: flex;flex-wrap: wrap;background-color: #000;">
+        <el-main style="padding: 0;background-color: #000;">
+          <div :style="`width: 100%;height: ${isFullScreen?'94vh':'85vh'};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)">
@@ -31,6 +42,8 @@
 import uiHeader from "../layout/UiHeader.vue";
 import player from './common/jessibuca.vue'
 import DeviceTree from './common/DeviceTree.vue'
+import {exitFullscreen, launchIntoFullscreen} from "@/until/dom";
+
 
 export default {
   name: "live",
@@ -46,7 +59,8 @@ export default {
       updateLooper: 0, //数据刷新轮训标志
       count: 15,
       total: 0,
-
+      asideHide: true,// 是否隐藏侧边栏
+      isFullScreen: false,// 是否全屏
       //channel
       loading: false
     };
@@ -63,10 +77,10 @@ export default {
       let style = {width: '100%', height: '100%'}
       switch (this.spilt) {
         case 4:
-          style = {width: '49%', height: '49%'}
+          style = {width: '49%', height: '49%','margin-left':'0.5%'}
           break
         case 9:
-          style = {width: '32%', height: '32%'}
+          style = {width: '32%', height: '32%','margin-left':'0.8%'}
           break
       }
       this.$nextTick(() => {
@@ -129,6 +143,7 @@ export default {
       let deviceId = itemData.deviceId;
       // this.isLoging = true;
       let channelId = itemData.channelId;
+      console.log(itemData);
       console.log("通知设备推流1:" + deviceId + " : " + channelId);
       let idxTmp = this.playerIdx
       let that = this;
@@ -199,6 +214,17 @@ export default {
       console.log(data);
       window.localStorage.setItem('playData', JSON.stringify(data))
     },
+    switchFullScreenHandle(){
+      if(this.isFullScreen){
+        // 退出全屏
+        exitFullscreen();
+        this.isFullScreen= false;
+      }else{
+        let el = this.$refs.container;
+        launchIntoFullscreen(el);
+        this.isFullScreen= true;
+      }
+    }
   }
 };
 </script>
@@ -302,4 +328,24 @@ export default {
 .baidumap > .anchorBL {
   display: none !important;
 }
+
+.el-button--goon.is-active,
+.el-button--goon:active {
+  background: #20B2AA;
+  border-color: #20B2AA;
+  color: #fff;
+}
+
+.el-button--goon:focus,
+.el-button--goon:hover {
+  background: #48D1CC;
+  border-color: #48D1CC;
+  color: #fff;
+}
+
+.el-button--goon {
+  color: #FFF;
+  background-color: #292a2a;
+  border-color: #295656;
+}
 </style>

+ 60 - 0
web_src/src/until/dom.js

@@ -0,0 +1,60 @@
+export function comDomHeight(el){
+    let parent = el.offsetParent;
+    let parentWidth = parent.offsetWidth;
+    return parentWidth;
+}
+
+/**
+ * 指定dom进入全屏
+ * @param element
+ */
+export function launchIntoFullscreen(element) {
+    if(element.requestFullscreen){
+        element.requestFullscreen();
+    }
+    else if(element.mozRequestFullScreen) {
+        element.mozRequestFullScreen();
+    }
+    else if(element.webkitRequestFullscreen) {
+        element.webkitRequestFullscreen();
+    }
+    else if(element.msRequestFullscreen) {
+        element.msRequestFullscreen();
+    }
+}
+
+/**
+ * 退出全屏
+ */
+export function exitFullscreen() {
+    if(document.exitFullscreen) {
+        document.exitFullscreen();
+    } else if(document.mozCancelFullScreen) {
+        document.mozCancelFullScreen();
+    } else if(document.webkitExitFullscreen) {
+        document.webkitExitFullscreen();
+    }
+}
+
+/**
+ * 检测是否处于全屏状态
+ * @returns {*|boolean}
+ */
+export function isFullscreenEnabled(){
+    // console.log(document.fullscreenEnabled);
+    if(document.fullscreenEnabled){
+        return document.fullscreenElement
+    }else if(document.mozFullScreenEnabled){
+        return document.mozFullscreenElement
+    }else if(document.webkitFullscreenEnabled){
+        return document.webkitFullscreenElement
+    }else if(document.msFullscreenEnabled){
+        return document.msFullscreenElement
+    }else{
+        return document.mozFullScreenEnabled ||
+            document.webkitFullscreenEnabled ||
+            document.msFullscreenEnabled || false;
+    }
+}
+
+export default {comDomHeight,launchIntoFullscreen,exitFullscreen,isFullscreenEnabled}

+ 32 - 0
web_src/src/until/t_test.js

@@ -0,0 +1,32 @@
+// 长按的时间阈值
+const LONG_PRESS_TIME = 1200;
+// 长按任务的时间间隔
+const LONG_PRESS_TASK_TIME = 1000;
+// 短按的时间阈值
+const SHORT_PRESS_TIME = 300;
+
+// 短按计数器的最大值
+const MAX_SHORT_PRESS_COUNT = 10;
+
+// 短按计时器最大的持续时间
+const MAX_SHORT_PRESS_DURATION = 700;
+
+let pressTimer = null; // 记录按下的计时器
+let releaseTimer = null; // 记录抬起的计时器
+let shortPressTimer = null; // 短按计时器
+let shortPressCount = 0; // 短按计数器
+
+
+let longPressTaskTime = 0;// 开始毫秒数
+
+let startTimeStamp = 0;// 开始的毫秒数
+function down(){
+  // 开始计数
+  if(startTimeStamp===0){
+    startTimeStamp = new Date().getTime();
+  }
+  //
+}
+
+
+

+ 7 - 0
web_src/云台控制长按.md

@@ -0,0 +1,7 @@
+# 云台控制梳理
+需求
+实现长按,与短按功能.
+按下按钮后等待1200毫秒视作长按马上发送一条开始控制命令
+长按后抬起按钮需要发送一条停止控制命令,并且在刚发送长按命令的1000毫秒内抬去按钮,需要等待1000毫秒再进行发送
+短按功能 在按下并抬起按钮的300毫秒后发送一条控制命令,如果是在这300毫秒内多次按下抬起则重置计时器,并且计数加一
+从第一次短按开始短按刷新最多维持700毫秒,700毫秒后强制发送控制命令