mirror of
https://gitee.com/pan648540858/wvp-GB28181-pro.git
synced 2026-06-29 13:37:49 +08:00
Compare commits
24 Commits
b3c192a8a9
...
1fc8848cf9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fc8848cf9 | ||
|
|
2f1ec0e33f | ||
|
|
cd293bc116 | ||
|
|
5f50d4c96f | ||
|
|
0c7701d36f | ||
|
|
6ade3060b5 | ||
|
|
f6ca930492 | ||
|
|
58cc8a8baf | ||
|
|
9a52d29ded | ||
|
|
1b7661039b | ||
|
|
a78599d1d3 | ||
|
|
232bca91c2 | ||
|
|
9f25d0ce37 | ||
|
|
79a7e82656 | ||
|
|
d3e89786c2 | ||
|
|
c59bacdc2c | ||
|
|
6931a95ecf | ||
|
|
fb6c84de29 | ||
|
|
70bc01bd90 | ||
|
|
3417244705 | ||
|
|
5eea3a19f5 | ||
|
|
f5494c0b95 | ||
|
|
ab371e00df | ||
|
|
042c78fcf6 |
@ -16,6 +16,7 @@ public class ChannelDataType {
|
||||
public final static String DOWNLOAD_SERVICE = "sourceChannelDownloadService";
|
||||
public final static String PTZ_SERVICE = "sourceChannelPTZService";
|
||||
public final static String OTHER_SERVICE = "sourceChannelOtherService";
|
||||
public final static String BROADCAST_SERVICE = "sourceChannelBroadcastService";
|
||||
|
||||
|
||||
public static String getDateTypeDesc(Integer dataType) {
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
package com.genersoft.iot.vmp.gb28181.bean;
|
||||
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class FrontEndControlCodeForDragZoom implements IFrontEndControlCode {
|
||||
|
||||
private final FrontEndControlType type = FrontEndControlType.DRAG_ZOOM;
|
||||
|
||||
@Override
|
||||
public FrontEndControlType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助开关控制指令: 1为zoomIn 拉框放大, 2为zoomOut 拉框缩小
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
private Integer code;
|
||||
|
||||
/**
|
||||
* 播放窗口长度像素值(必选)
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
protected Integer length;
|
||||
|
||||
/**
|
||||
* 播放窗口长度像素值(必选)
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
protected Integer width;
|
||||
|
||||
/**
|
||||
* 拉框中心的横轴坐标像素值(必选)
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
protected Integer midPointX;
|
||||
|
||||
/**
|
||||
* 拉框中心的纵轴坐标像素值(必选)
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
protected Integer midPointY;
|
||||
|
||||
/**
|
||||
* 拉框长度像素值(必选)
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
protected Integer lengthX;
|
||||
|
||||
/**
|
||||
* 拉框宽度像素值(必选)
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
protected Integer lengthY;
|
||||
|
||||
|
||||
@Override
|
||||
public String encode() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@ -2,5 +2,5 @@ package com.genersoft.iot.vmp.gb28181.bean;
|
||||
|
||||
public enum FrontEndControlType {
|
||||
|
||||
PTZ,FI,PRESET,TOUR,SCAN,AUXILIARY
|
||||
PTZ,FI,PRESET,TOUR,SCAN,AUXILIARY,DRAG_ZOOM
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import com.genersoft.iot.vmp.gb28181.utils.VectorTileCatch;
|
||||
import com.genersoft.iot.vmp.service.bean.ErrorCallback;
|
||||
import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
|
||||
import com.genersoft.iot.vmp.utils.DateUtil;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.AudioTalkResult;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
|
||||
@ -352,6 +353,42 @@ public class ChannelController {
|
||||
channelPlayService.stopPlay(channel);
|
||||
}
|
||||
|
||||
@Operation(summary = "开始对讲", security = @SecurityRequirement(name = JwtUtils.HEADER))
|
||||
@GetMapping("/talk/start")
|
||||
public AudioTalkResult startTalk(Integer channelId){
|
||||
Assert.notNull(channelId,"参数异常");
|
||||
CommonGBChannel channel = channelService.getOne(channelId);
|
||||
Assert.notNull(channel, "通道不存在");
|
||||
return channelPlayService.startTalk(channel);
|
||||
}
|
||||
|
||||
@Operation(summary = "停止对讲", security = @SecurityRequirement(name = JwtUtils.HEADER))
|
||||
@GetMapping("/talk/stop")
|
||||
public void stopTalk(Integer channelId){
|
||||
Assert.notNull(channelId,"参数异常");
|
||||
CommonGBChannel channel = channelService.getOne(channelId);
|
||||
Assert.notNull(channel, "通道不存在");
|
||||
channelPlayService.stopTalk(channel);
|
||||
}
|
||||
|
||||
@Operation(summary = "开始喊话", security = @SecurityRequirement(name = JwtUtils.HEADER))
|
||||
@GetMapping("/broadcast/start")
|
||||
public AudioTalkResult startBroadcast(Integer channelId){
|
||||
Assert.notNull(channelId,"参数异常");
|
||||
CommonGBChannel channel = channelService.getOne(channelId);
|
||||
Assert.notNull(channel, "通道不存在");
|
||||
return channelPlayService.startBroadcast(channel);
|
||||
}
|
||||
|
||||
@Operation(summary = "停止喊话", security = @SecurityRequirement(name = JwtUtils.HEADER))
|
||||
@GetMapping("/broadcast/stop")
|
||||
public void stopBroadcast(Integer channelId){
|
||||
Assert.notNull(channelId,"参数异常");
|
||||
CommonGBChannel channel = channelService.getOne(channelId);
|
||||
Assert.notNull(channel, "通道不存在");
|
||||
channelPlayService.stopBroadcast(channel);
|
||||
}
|
||||
|
||||
@Operation(summary = "录像查询", security = @SecurityRequirement(name = JwtUtils.HEADER))
|
||||
@Parameter(name = "channelId", description = "通道ID", required = true)
|
||||
@Parameter(name = "startTime", description = "开始时间", required = true)
|
||||
|
||||
@ -598,4 +598,94 @@ public class ChannelFrontEndController {
|
||||
channelControlService.auxiliary(channel, controlCode, callback);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Operation(summary = "拉框放大", security = @SecurityRequirement(name = JwtUtils.HEADER))
|
||||
@Parameter(name = "channelId", description = "通道ID", required = true)
|
||||
@Parameter(name = "length", description = "播放窗口长度像素值", required = true)
|
||||
@Parameter(name = "width", description = "播放窗口宽度像素值", required = true)
|
||||
@Parameter(name = "midPointX", description = "拉框中心的横轴坐标像素值", required = true)
|
||||
@Parameter(name = "midPointY", description = "拉框中心的纵轴坐标像素值", required = true)
|
||||
@Parameter(name = "lengthX", description = "拉框长度像素值", required = true)
|
||||
@Parameter(name = "lengthY", description = "拉框宽度像素值", required = true)
|
||||
@GetMapping("/drag_zoom_in")
|
||||
public DeferredResult<WVPResult<String>> dragZoomIn(Integer channelId, int length, int width, int midPointX, int midPointY, int lengthX, int lengthY){
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("[通用通道]拉框放大 API调用,channelId:{} ,length:{} ,width:{} ,midPointX:{} ,midPointY:{} ,lengthX:{} ,lengthY:{}",channelId, length, width, midPointX, midPointY, lengthX, lengthY);
|
||||
}
|
||||
|
||||
CommonGBChannel channel = channelService.getOne(channelId);
|
||||
Assert.notNull(channel, "通道不存在");
|
||||
|
||||
FrontEndControlCodeForDragZoom controlCode = new FrontEndControlCodeForDragZoom();
|
||||
controlCode.setCode(1);
|
||||
controlCode.setLength(length);
|
||||
controlCode.setWidth(width);
|
||||
controlCode.setMidPointX(midPointX);
|
||||
controlCode.setMidPointY(midPointY);
|
||||
controlCode.setLengthX(lengthX);
|
||||
controlCode.setLengthY(lengthY);
|
||||
|
||||
DeferredResult<WVPResult<String>> result = new DeferredResult<>();
|
||||
|
||||
result.onTimeout(()->{
|
||||
WVPResult<String> wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时");
|
||||
result.setResult(wvpResult);
|
||||
});
|
||||
|
||||
ErrorCallback<String> callback = (code, msg, data) -> {
|
||||
WVPResult<String> wvpResult = new WVPResult<>();
|
||||
wvpResult.setCode(code);
|
||||
wvpResult.setMsg(msg);
|
||||
wvpResult.setData(data);
|
||||
result.setResult(wvpResult);
|
||||
};
|
||||
channelControlService.dragZoom(channel, controlCode, callback);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Operation(summary = "拉框缩小", security = @SecurityRequirement(name = JwtUtils.HEADER))
|
||||
@Parameter(name = "channelId", description = "通道ID", required = true)
|
||||
@Parameter(name = "length", description = "播放窗口长度像素值", required = true)
|
||||
@Parameter(name = "width", description = "播放窗口宽度像素值", required = true)
|
||||
@Parameter(name = "midPointX", description = "拉框中心的横轴坐标像素值", required = true)
|
||||
@Parameter(name = "midPointY", description = "拉框中心的纵轴坐标像素值", required = true)
|
||||
@Parameter(name = "lengthX", description = "拉框长度像素值", required = true)
|
||||
@Parameter(name = "lengthY", description = "拉框宽度像素值", required = true)
|
||||
@GetMapping("/drag_zoom_out")
|
||||
public DeferredResult<WVPResult<String>> dragZoomOut(Integer channelId, Integer length, Integer width, Integer midPointX, Integer midPointY, Integer lengthX, Integer lengthY){
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("[通用通道]拉框缩小 API调用,channelId:{} ,length:{} ,width:{} ,midPointX:{} ,midPointY:{} ,lengthX:{} ,lengthY:{}",channelId, length, width, midPointX, midPointY, lengthX, lengthY);
|
||||
}
|
||||
|
||||
CommonGBChannel channel = channelService.getOne(channelId);
|
||||
Assert.notNull(channel, "通道不存在");
|
||||
|
||||
FrontEndControlCodeForDragZoom controlCode = new FrontEndControlCodeForDragZoom();
|
||||
controlCode.setCode(2);
|
||||
controlCode.setLength(length);
|
||||
controlCode.setWidth(width);
|
||||
controlCode.setMidPointX(midPointX);
|
||||
controlCode.setMidPointY(midPointY);
|
||||
controlCode.setLengthX(lengthX);
|
||||
controlCode.setLengthY(lengthY);
|
||||
|
||||
DeferredResult<WVPResult<String>> result = new DeferredResult<>();
|
||||
|
||||
result.onTimeout(()->{
|
||||
WVPResult<String> wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时");
|
||||
result.setResult(wvpResult);
|
||||
});
|
||||
|
||||
ErrorCallback<String> callback = (code, msg, data) -> {
|
||||
WVPResult<String> wvpResult = new WVPResult<>();
|
||||
wvpResult.setCode(code);
|
||||
wvpResult.setMsg(msg);
|
||||
wvpResult.setData(data);
|
||||
result.setResult(wvpResult);
|
||||
};
|
||||
channelControlService.dragZoom(channel, controlCode, callback);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,66 +165,50 @@ public class DeviceControl {
|
||||
@Parameter(name = "channelId", description = "通道国标编号", required = true)
|
||||
@Parameter(name = "length", description = "播放窗口长度像素值", required = true)
|
||||
@Parameter(name = "width", description = "播放窗口宽度像素值", required = true)
|
||||
@Parameter(name = "midpointx", description = "拉框中心的横轴坐标像素值", required = true)
|
||||
@Parameter(name = "midpointy", description = "拉框中心的纵轴坐标像素值", required = true)
|
||||
@Parameter(name = "lengthx", description = "拉框长度像素值", required = true)
|
||||
@Parameter(name = "lengthy", description = "拉框宽度像素值", required = true)
|
||||
@Parameter(name = "midPointX", description = "拉框中心的横轴坐标像素值", required = true)
|
||||
@Parameter(name = "midPointY", description = "拉框中心的纵轴坐标像素值", required = true)
|
||||
@Parameter(name = "lengthX", description = "拉框长度像素值", required = true)
|
||||
@Parameter(name = "lengthY", description = "拉框宽度像素值", required = true)
|
||||
@GetMapping("drag_zoom/zoom_in")
|
||||
public DeferredResult<WVPResult<String>> dragZoomIn(@RequestParam String deviceId, String channelId,
|
||||
public void dragZoomIn(@RequestParam String deviceId, String channelId,
|
||||
@RequestParam int length,
|
||||
@RequestParam int width,
|
||||
@RequestParam int midpointx,
|
||||
@RequestParam int midpointy,
|
||||
@RequestParam int lengthx,
|
||||
@RequestParam int lengthy) {
|
||||
@RequestParam int midPointX,
|
||||
@RequestParam int midPointY,
|
||||
@RequestParam int lengthX,
|
||||
@RequestParam int lengthY) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(String.format("设备拉框放大 API调用,deviceId:%s ,channelId:%s ,length:%d ,width:%d ,midpointx:%d ,midpointy:%d ,lengthx:%d ,lengthy:%d",deviceId, channelId, length, width, midpointx, midpointy,lengthx, lengthy));
|
||||
log.debug(String.format("设备拉框放大 API调用,deviceId:%s ,channelId:%s ,length:%d ,width:%d ,midPointX:%d ,midPointY:%d ,lengthX:%d ,lengthY:%d",deviceId, channelId, length, width, midPointX, midPointY,lengthX, lengthY));
|
||||
}
|
||||
Device device = deviceService.getDeviceByDeviceId(deviceId);
|
||||
Assert.notNull(device, "设备不存在");
|
||||
DeferredResult<WVPResult<String>> result = new DeferredResult<>();
|
||||
deviceService.dragZoomIn(device, channelId, length, width, midpointx, midpointy, lengthx,lengthy, (code, msg, data) -> {
|
||||
result.setResult(new WVPResult<>(code, msg, data));
|
||||
});
|
||||
result.onTimeout(() -> {
|
||||
log.warn("[设备拉框放大] 操作超时, 设备未返回应答指令, {}", deviceId);
|
||||
result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答"));
|
||||
});
|
||||
return result;
|
||||
deviceService.dragZoomIn(device, channelId, length, width, midPointX, midPointY, lengthX, lengthY);
|
||||
}
|
||||
|
||||
@Operation(summary = "拉框缩小", security = @SecurityRequirement(name = JwtUtils.HEADER))
|
||||
@Parameter(name = "deviceId", description = "设备国标编号", required = true)
|
||||
@Parameter(name = "channelId", description = "通道国标编号")
|
||||
@Parameter(name = "length", description = "播放窗口长度像素值", required = true)
|
||||
@Parameter(name = "width", description = "拉框中心的横轴坐标像素值", required = true)
|
||||
@Parameter(name = "midpointx", description = "拉框中心的横轴坐标像素值", required = true)
|
||||
@Parameter(name = "midpointy", description = "拉框中心的纵轴坐标像素值", required = true)
|
||||
@Parameter(name = "lengthx", description = "拉框长度像素值", required = true)
|
||||
@Parameter(name = "lengthy", description = "拉框宽度像素值", required = true)
|
||||
@Parameter(name = "width", description = "播放窗口宽像素值", required = true)
|
||||
@Parameter(name = "midPointX", description = "拉框中心的横轴坐标像素值", required = true)
|
||||
@Parameter(name = "midPointY", description = "拉框中心的纵轴坐标像素值", required = true)
|
||||
@Parameter(name = "lengthX", description = "拉框长度像素值", required = true)
|
||||
@Parameter(name = "lengthY", description = "拉框宽度像素值", required = true)
|
||||
@GetMapping("/drag_zoom/zoom_out")
|
||||
public DeferredResult<WVPResult<String>> dragZoomOut(@RequestParam String deviceId,
|
||||
public void dragZoomOut(@RequestParam String deviceId,
|
||||
@RequestParam(required = false) String channelId,
|
||||
@RequestParam int length,
|
||||
@RequestParam int width,
|
||||
@RequestParam int midpointx,
|
||||
@RequestParam int midpointy,
|
||||
@RequestParam int lengthx,
|
||||
@RequestParam int lengthy){
|
||||
@RequestParam int midPointX,
|
||||
@RequestParam int midPointY,
|
||||
@RequestParam int lengthX,
|
||||
@RequestParam int lengthY){
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(String.format("设备拉框缩小 API调用,deviceId:%s ,channelId:%s ,length:%d ,width:%d ,midpointx:%d ,midpointy:%d ,lengthx:%d ,lengthy:%d",deviceId, channelId, length, width, midpointx, midpointy,lengthx, lengthy));
|
||||
log.debug(String.format("设备拉框缩小 API调用,deviceId:%s ,channelId:%s ,length:%d ,width:%d ,midPointX:%d ,midPointY:%d ,lengthX:%d ,lengthY:%d",deviceId, channelId, length, width, midPointX, midPointY,lengthX, lengthY));
|
||||
}
|
||||
Device device = deviceService.getDeviceByDeviceId(deviceId);
|
||||
Assert.notNull(device, "设备不存在");
|
||||
DeferredResult<WVPResult<String>> result = new DeferredResult<>();
|
||||
deviceService.dragZoomOut(device, channelId, length, width, midpointx, midpointy, lengthx,lengthy, (code, msg, data) -> {
|
||||
result.setResult(new WVPResult<>(code, msg, data));
|
||||
});
|
||||
result.onTimeout(() -> {
|
||||
log.warn("[设备拉框放大] 操作超时, 设备未返回应答指令, {}", deviceId);
|
||||
result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答"));
|
||||
});
|
||||
return result;
|
||||
deviceService.dragZoomOut(device, channelId, length, width, midPointX, midPointY, lengthX,lengthY);
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,9 +185,9 @@ public interface IDeviceService {
|
||||
|
||||
void homePosition(Device device, String channelId, Boolean enabled, Integer resetTime, Integer presetIndex, ErrorCallback<String> callback);
|
||||
|
||||
void dragZoomIn(Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy, ErrorCallback<String> callback);
|
||||
void dragZoomIn(Device device, String channelId, int length, int width, int midPointX, int midPointY, int lengthX, int lengthY);
|
||||
|
||||
void dragZoomOut(Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy, ErrorCallback<String> callback);
|
||||
void dragZoomOut(Device device, String channelId, int length, int width, int midPointX, int midPointY, int lengthX, int lengthY);
|
||||
|
||||
void deviceStatus(Device device, ErrorCallback<String> callback);
|
||||
|
||||
|
||||
@ -16,4 +16,5 @@ public interface IGbChannelControlService {
|
||||
void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper controlCode, ErrorCallback<String> callback);
|
||||
void auxiliary(CommonGBChannel channel, FrontEndControlCodeForAuxiliary frontEndControlCode, ErrorCallback<String> callback);
|
||||
void queryPreset(CommonGBChannel channel, ErrorCallback<List<Preset>> callback);
|
||||
void dragZoom(CommonGBChannel channel, FrontEndControlCodeForDragZoom frontEndControlCode, ErrorCallback<String> callback);
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import com.genersoft.iot.vmp.gb28181.bean.CommonRecordInfo;
|
||||
import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo;
|
||||
import com.genersoft.iot.vmp.gb28181.bean.Platform;
|
||||
import com.genersoft.iot.vmp.service.bean.ErrorCallback;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.AudioTalkResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -41,4 +42,12 @@ public interface IGbChannelPlayService {
|
||||
|
||||
|
||||
void getSnap(CommonGBChannel channel, ErrorCallback<byte[]> callback);
|
||||
|
||||
AudioTalkResult startTalk(CommonGBChannel channel);
|
||||
|
||||
void stopTalk(CommonGBChannel channel);
|
||||
|
||||
AudioTalkResult startBroadcast(CommonGBChannel channel);
|
||||
|
||||
void stopBroadcast(CommonGBChannel channel);
|
||||
}
|
||||
|
||||
@ -18,4 +18,8 @@ public interface IPTZService {
|
||||
|
||||
void queryPresetList(CommonGBChannel channel, ErrorCallback<List<Preset>> callback);
|
||||
|
||||
void dragZoomIn(CommonGBChannel channel, int length, int width, int midPointX, int midPointY, int lengthX, int lengthY);
|
||||
|
||||
void dragZoomOut(CommonGBChannel channel, int length, int width, int midPointX, int midPointY, int lengthX, int lengthY);
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
package com.genersoft.iot.vmp.gb28181.service;
|
||||
|
||||
import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.AudioTalkResult;
|
||||
|
||||
/**
|
||||
* 资源能力接入-语音对讲
|
||||
*/
|
||||
public interface ISourceBroadcastService {
|
||||
|
||||
AudioTalkResult startTalk(CommonGBChannel channel);
|
||||
|
||||
void stopTalk(CommonGBChannel channel);
|
||||
|
||||
AudioTalkResult startBroadcast(CommonGBChannel channel);
|
||||
|
||||
void stopBroadcast(CommonGBChannel channel);
|
||||
}
|
||||
@ -25,4 +25,6 @@ public interface ISourcePTZService {
|
||||
void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper frontEndControlCode, ErrorCallback<String> callback);
|
||||
|
||||
void queryPreset(CommonGBChannel channel, ErrorCallback<List<Preset>> callback);
|
||||
|
||||
void dragZoom(CommonGBChannel channel, FrontEndControlCodeForDragZoom frontEndControlCode, ErrorCallback<String> callback);
|
||||
}
|
||||
|
||||
@ -574,9 +574,9 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
|
||||
@Override
|
||||
public boolean removeCatalogSubscribe(@NotNull Device device, CommonCallback<Boolean> callback) {
|
||||
log.info("[移除目录订阅]: {}", device.getDeviceId());
|
||||
String key = SubscribeTaskForCatalog.getKey(device);
|
||||
if (subscribeTaskRunner.containsKey(key)) {
|
||||
log.info("[移除目录订阅]: {}", device.getDeviceId());
|
||||
SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key);
|
||||
if (transactionInfo == null) {
|
||||
log.warn("[移除目录订阅] 未找到事务信息,{}", device.getDeviceId());
|
||||
@ -638,9 +638,9 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
|
||||
@Override
|
||||
public boolean removeMobilePositionSubscribe(Device device, CommonCallback<Boolean> callback) {
|
||||
log.info("[移除移动位置订阅]: {}", device.getDeviceId());
|
||||
String key = SubscribeTaskForMobilPosition.getKey(device);
|
||||
if (subscribeTaskRunner.containsKey(key)) {
|
||||
log.info("[移除移动位置订阅]: {}", device.getDeviceId());
|
||||
SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key);
|
||||
if (transactionInfo == null) {
|
||||
log.warn("[移除移动位置订阅] 未找到事务信息,{}", device.getDeviceId());
|
||||
@ -703,9 +703,9 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
|
||||
@Override
|
||||
public boolean removeAlarmSubscribe(Device device, CommonCallback<Boolean> callback) {
|
||||
log.info("[移除报警订阅]: {}", device.getDeviceId());
|
||||
String key = SubscribeTaskForAlarm.getKey(device);
|
||||
if (subscribeTaskRunner.containsKey(key)) {
|
||||
log.info("[移除报警订阅]: {}", device.getDeviceId());
|
||||
SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key);
|
||||
if (transactionInfo == null) {
|
||||
log.warn("[移除报警订阅] 未找到事务信息,{}", device.getDeviceId());
|
||||
@ -1276,9 +1276,9 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dragZoomIn(Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy, ErrorCallback<String> callback) {
|
||||
public void dragZoomIn(Device device, String channelId, int length, int width, int midPointX, int midPointY, int lengthX, int lengthY) {
|
||||
if (!userSetting.getServerId().equals(device.getServerId())) {
|
||||
redisRpcService.dragZoomIn(device.getServerId(), device, channelId, length, width, midpointx, midpointy, lengthx, lengthy);
|
||||
redisRpcService.dragZoomIn(device.getServerId(), device, channelId, length, width, midPointX, midPointY, lengthX, lengthY);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1286,24 +1286,23 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
cmdXml.append("<DragZoomIn>\r\n");
|
||||
cmdXml.append("<Length>" + length+ "</Length>\r\n");
|
||||
cmdXml.append("<Width>" + width+ "</Width>\r\n");
|
||||
cmdXml.append("<MidPointX>" + midpointx+ "</MidPointX>\r\n");
|
||||
cmdXml.append("<MidPointY>" + midpointy+ "</MidPointY>\r\n");
|
||||
cmdXml.append("<LengthX>" + lengthx+ "</LengthX>\r\n");
|
||||
cmdXml.append("<LengthY>" + lengthy+ "</LengthY>\r\n");
|
||||
cmdXml.append("<MidPointX>" + midPointX+ "</MidPointX>\r\n");
|
||||
cmdXml.append("<MidPointY>" + midPointY+ "</MidPointY>\r\n");
|
||||
cmdXml.append("<LengthX>" + lengthX+ "</LengthX>\r\n");
|
||||
cmdXml.append("<LengthY>" + lengthY+ "</LengthY>\r\n");
|
||||
cmdXml.append("</DragZoomIn>\r\n");
|
||||
try {
|
||||
sipCommander.dragZoomCmd(device, channelId, cmdXml.toString(), callback);
|
||||
sipCommander.dragZoomCmd(device, channelId, cmdXml.toString());
|
||||
} catch (InvalidArgumentException | SipException | ParseException e) {
|
||||
log.error("[命令发送失败] 拉框放大: {}", e.getMessage());
|
||||
callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null);
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dragZoomOut(Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy, ErrorCallback<String> callback) {
|
||||
public void dragZoomOut(Device device, String channelId, int length, int width, int midPointX, int midPointY, int lengthX, int lengthY) {
|
||||
if (!userSetting.getServerId().equals(device.getServerId())) {
|
||||
redisRpcService.dragZoomOut(device.getServerId(), device, channelId, length, width, midpointx, midpointy, lengthx, lengthy);
|
||||
redisRpcService.dragZoomOut(device.getServerId(), device, channelId, length, width, midPointX, midPointY, lengthX, lengthY);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1311,16 +1310,15 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
cmdXml.append("<DragZoomOut>\r\n");
|
||||
cmdXml.append("<Length>" + length+ "</Length>\r\n");
|
||||
cmdXml.append("<Width>" + width+ "</Width>\r\n");
|
||||
cmdXml.append("<MidPointX>" + midpointx+ "</MidPointX>\r\n");
|
||||
cmdXml.append("<MidPointY>" + midpointy+ "</MidPointY>\r\n");
|
||||
cmdXml.append("<LengthX>" + lengthx+ "</LengthX>\r\n");
|
||||
cmdXml.append("<LengthY>" + lengthy+ "</LengthY>\r\n");
|
||||
cmdXml.append("<MidPointX>" + midPointX+ "</MidPointX>\r\n");
|
||||
cmdXml.append("<MidPointY>" + midPointY+ "</MidPointY>\r\n");
|
||||
cmdXml.append("<LengthX>" + lengthX+ "</LengthX>\r\n");
|
||||
cmdXml.append("<LengthY>" + lengthY+ "</LengthY>\r\n");
|
||||
cmdXml.append("</DragZoomOut>\r\n");
|
||||
try {
|
||||
sipCommander.dragZoomCmd(device, channelId, cmdXml.toString(), callback);
|
||||
sipCommander.dragZoomCmd(device, channelId, cmdXml.toString());
|
||||
} catch (InvalidArgumentException | SipException | ParseException e) {
|
||||
log.error("[命令发送失败] 拉框放大: {}", e.getMessage());
|
||||
callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null);
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,4 +125,17 @@ public class GbChannelControlServiceImpl implements IGbChannelControlService {
|
||||
}
|
||||
sourcePTZService.queryPreset(channel, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dragZoom(CommonGBChannel channel, FrontEndControlCodeForDragZoom frontEndControlCode, ErrorCallback<String> callback) {
|
||||
log.info("[通用通道] 拉框{} 类型: {}, 编号:{}", frontEndControlCode.getCode() == 1 ? "放大": "缩小", channel.getDataType(), channel.getGbDeviceId());
|
||||
Integer dataType = channel.getDataType();
|
||||
ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType);
|
||||
if (sourcePTZService == null) {
|
||||
// 通道数据异常
|
||||
log.error("[点播通用通道] 类型: {} 不支持拉框控制", dataType);
|
||||
throw new PlayException(Response.BUSY_HERE, "channel not support");
|
||||
}
|
||||
sourcePTZService.dragZoom(channel, frontEndControlCode, callback);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,9 +7,11 @@ import com.genersoft.iot.vmp.conf.UserSetting;
|
||||
import com.genersoft.iot.vmp.gb28181.bean.*;
|
||||
import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper;
|
||||
import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
|
||||
import com.genersoft.iot.vmp.gb28181.service.ISourceBroadcastService;
|
||||
import com.genersoft.iot.vmp.gb28181.service.ISourceDownloadService;
|
||||
import com.genersoft.iot.vmp.gb28181.service.ISourcePlayService;
|
||||
import com.genersoft.iot.vmp.gb28181.service.ISourcePlaybackService;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.AudioTalkResult;
|
||||
import com.genersoft.iot.vmp.service.bean.ErrorCallback;
|
||||
import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -39,6 +41,9 @@ public class GbChannelPlayServiceImpl implements IGbChannelPlayService {
|
||||
@Autowired
|
||||
private Map<String, ISourceDownloadService> sourceDownloadServiceMap;
|
||||
|
||||
@Autowired
|
||||
private Map<String, ISourceBroadcastService> sourceBroadcastServiceMap;
|
||||
|
||||
|
||||
@Override
|
||||
public void startInvite(CommonGBChannel channel, InviteMessageInfo inviteInfo, Platform platform, ErrorCallback<StreamInfo> callback) {
|
||||
@ -247,4 +252,52 @@ public class GbChannelPlayServiceImpl implements IGbChannelPlayService {
|
||||
}
|
||||
sourceChannelPlayService.getSnap(channel, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AudioTalkResult startTalk(CommonGBChannel channel) {
|
||||
log.info("[通用通道] 开始对讲, 类型: {}, 编号:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId());
|
||||
Integer dataType = channel.getDataType();
|
||||
ISourceBroadcastService broadcastService = sourceBroadcastServiceMap.get(ChannelDataType.BROADCAST_SERVICE + dataType);
|
||||
if (broadcastService == null) {
|
||||
log.error("[通用通道] 类型编号: {} 不支持对讲", dataType);
|
||||
throw new PlayException(Response.BUSY_HERE, "channel not support");
|
||||
}
|
||||
return broadcastService.startTalk(channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopTalk(CommonGBChannel channel) {
|
||||
log.info("[通用通道] 停止对讲, 类型: {}, 编号:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId());
|
||||
Integer dataType = channel.getDataType();
|
||||
ISourceBroadcastService broadcastService = sourceBroadcastServiceMap.get(ChannelDataType.BROADCAST_SERVICE + dataType);
|
||||
if (broadcastService == null) {
|
||||
log.error("[通用通道] 类型编号: {} 不支持对讲", dataType);
|
||||
throw new PlayException(Response.BUSY_HERE, "channel not support");
|
||||
}
|
||||
broadcastService.stopTalk(channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AudioTalkResult startBroadcast(CommonGBChannel channel) {
|
||||
log.info("[通用通道] 开始喊话, 类型: {}, 编号:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId());
|
||||
Integer dataType = channel.getDataType();
|
||||
ISourceBroadcastService broadcastService = sourceBroadcastServiceMap.get(ChannelDataType.BROADCAST_SERVICE + dataType);
|
||||
if (broadcastService == null) {
|
||||
log.error("[通用通道] 类型编号: {} 不支持喊话", dataType);
|
||||
throw new PlayException(Response.BUSY_HERE, "channel not support");
|
||||
}
|
||||
return broadcastService.startBroadcast(channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopBroadcast(CommonGBChannel channel) {
|
||||
log.info("[通用通道] 停止喊话, 类型: {}, 编号:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId());
|
||||
Integer dataType = channel.getDataType();
|
||||
ISourceBroadcastService broadcastService = sourceBroadcastServiceMap.get(ChannelDataType.BROADCAST_SERVICE + dataType);
|
||||
if (broadcastService == null) {
|
||||
log.error("[通用通道] 类型编号: {} 不支持喊话", dataType);
|
||||
throw new PlayException(Response.BUSY_HERE, "channel not support");
|
||||
}
|
||||
broadcastService.stopBroadcast(channel);
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,10 +87,38 @@ public class PTZServiceImpl implements IPTZService {
|
||||
if (device == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备ID");
|
||||
}
|
||||
DeviceChannel deviceChannel = deviceChannelService.getOneById(channel.getGbId());
|
||||
DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId());
|
||||
frontEndCommand(device, deviceChannel.getDeviceId(), cmdCode, parameter1, parameter2, combindCode2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dragZoomIn(CommonGBChannel channel, int length, int width, int midPointX, int midPointY, int lengthX, int lengthY) {
|
||||
if (channel.getDataType() != ChannelDataType.GB28181) {
|
||||
log.warn("[INFO 消息] 只有国标通道的支持云台控制, 通道ID: {}", channel.getGbId());
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "不支持");
|
||||
}
|
||||
Device device = deviceService.getDevice(channel.getDataDeviceId());
|
||||
if (device == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备ID");
|
||||
}
|
||||
DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId());
|
||||
deviceService.dragZoomIn(device, deviceChannel.getDeviceId(), length, width, midPointX, midPointY, lengthX, lengthY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dragZoomOut(CommonGBChannel channel, int length, int width, int midPointX, int midPointY, int lengthX, int lengthY) {
|
||||
if (channel.getDataType() != ChannelDataType.GB28181) {
|
||||
log.warn("[INFO 消息] 只有国标通道的支持云台控制, 通道ID: {}", channel.getGbId());
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "不支持");
|
||||
}
|
||||
Device device = deviceService.getDevice(channel.getDataDeviceId());
|
||||
if (device == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备ID");
|
||||
}
|
||||
DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId());
|
||||
deviceService.dragZoomOut(device, deviceChannel.getDeviceId(), length, width, midPointX, midPointY, lengthX, lengthY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queryPresetList(CommonGBChannel channel, ErrorCallback<List<Preset>> callback) {
|
||||
if (channel.getDataType() != ChannelDataType.GB28181) {
|
||||
@ -108,4 +136,6 @@ public class PTZServiceImpl implements IPTZService {
|
||||
}
|
||||
deviceService.queryPreset(device, deviceChannel.getDeviceId(), callback);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -1229,6 +1229,12 @@ public class PlayServiceImpl implements IPlayService {
|
||||
audioBroadcastResult.setApp(app);
|
||||
audioBroadcastResult.setStream(stream);
|
||||
audioBroadcastResult.setStreamInfo(new StreamContent(mediaServerService.getStreamInfoByAppAndStream(mediaServerItem, app, stream, null, null, null, false)));
|
||||
if (!broadcastMode) {
|
||||
audioBroadcastResult.setPlayStreamInfo(new StreamContent(
|
||||
mediaServerService.getStreamInfoByAppAndStream(mediaServerItem,
|
||||
MediaStreamUtil.GB28181_TALK, stream + "_talk",
|
||||
null, null, null, true)));
|
||||
}
|
||||
audioBroadcastResult.setCodec("G.711");
|
||||
return audioBroadcastResult;
|
||||
}
|
||||
|
||||
@ -0,0 +1,85 @@
|
||||
package com.genersoft.iot.vmp.gb28181.service.impl;
|
||||
|
||||
import com.genersoft.iot.vmp.common.enums.ChannelDataType;
|
||||
import com.genersoft.iot.vmp.conf.exception.ControllerException;
|
||||
import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
|
||||
import com.genersoft.iot.vmp.gb28181.bean.Device;
|
||||
import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
|
||||
import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService;
|
||||
import com.genersoft.iot.vmp.gb28181.service.IDeviceService;
|
||||
import com.genersoft.iot.vmp.gb28181.service.IPlayService;
|
||||
import com.genersoft.iot.vmp.gb28181.service.ISourceBroadcastService;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.AudioTalkResult;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service(ChannelDataType.BROADCAST_SERVICE + ChannelDataType.GB28181)
|
||||
public class SourceBroadcastServiceForGbImpl implements ISourceBroadcastService {
|
||||
|
||||
@Autowired
|
||||
private IPlayService playService;
|
||||
|
||||
@Autowired
|
||||
private IDeviceService deviceService;
|
||||
|
||||
@Autowired
|
||||
private IDeviceChannelService deviceChannelService;
|
||||
|
||||
@Override
|
||||
public AudioTalkResult startBroadcast(CommonGBChannel channel) {
|
||||
Device device = deviceService.getDevice(channel.getDataDeviceId());
|
||||
if (device == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR400.getCode(), "未找到设备");
|
||||
}
|
||||
DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId());
|
||||
if (deviceChannel == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR400.getCode(), "未找到通道");
|
||||
}
|
||||
AudioBroadcastResult abResult = playService.audioBroadcast(
|
||||
device.getDeviceId(), deviceChannel.getDeviceId(), true);
|
||||
AudioTalkResult result = new AudioTalkResult();
|
||||
result.setPushStream(abResult.getStreamInfo());
|
||||
result.setPlayStream(null);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopBroadcast(CommonGBChannel channel) {
|
||||
Device device = deviceService.getDevice(channel.getDataDeviceId());
|
||||
if (device == null) return;
|
||||
DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId());
|
||||
if (deviceChannel == null) return;
|
||||
playService.stopAudioBroadcast(device, deviceChannel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AudioTalkResult startTalk(CommonGBChannel channel) {
|
||||
Device device = deviceService.getDevice(channel.getDataDeviceId());
|
||||
if (device == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR400.getCode(), "未找到设备");
|
||||
}
|
||||
DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId());
|
||||
if (deviceChannel == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR400.getCode(), "未找到通道");
|
||||
}
|
||||
AudioBroadcastResult abResult = playService.audioBroadcast(
|
||||
device.getDeviceId(), deviceChannel.getDeviceId(), false);
|
||||
AudioTalkResult result = new AudioTalkResult();
|
||||
result.setPushStream(abResult.getStreamInfo());
|
||||
result.setPlayStream(abResult.getPlayStreamInfo());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopTalk(CommonGBChannel channel) {
|
||||
Device device = deviceService.getDevice(channel.getDataDeviceId());
|
||||
if (device == null) return;
|
||||
DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId());
|
||||
if (deviceChannel == null) return;
|
||||
playService.stopTalk(device, deviceChannel, null);
|
||||
}
|
||||
}
|
||||
@ -121,6 +121,11 @@ public class SourcePTZServiceForGbImpl implements ISourcePTZService {
|
||||
log.error("[FI失败] 未知的聚焦指令 {}", frontEndControlCode.getFocus());
|
||||
callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null);
|
||||
}
|
||||
if (frontEndControlCode.getFocusSpeed() == null) {
|
||||
callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null);
|
||||
return;
|
||||
}
|
||||
focusSpeed = frontEndControlCode.getFocusSpeed();
|
||||
}
|
||||
if (frontEndControlCode.getIris() != null) {
|
||||
if (frontEndControlCode.getIris() == 0) {
|
||||
@ -131,17 +136,12 @@ public class SourcePTZServiceForGbImpl implements ISourcePTZService {
|
||||
log.error("[FI失败] 未知的光圈指令 {}", frontEndControlCode.getIris());
|
||||
callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null);
|
||||
}
|
||||
if (frontEndControlCode.getIrisSpeed() == null) {
|
||||
callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null);
|
||||
return;
|
||||
}
|
||||
irisSpeed = frontEndControlCode.getIrisSpeed();
|
||||
}
|
||||
if (frontEndControlCode.getFocusSpeed() == null) {
|
||||
callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null);
|
||||
return;
|
||||
}
|
||||
if (frontEndControlCode.getIrisSpeed() == null) {
|
||||
callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null);
|
||||
return;
|
||||
}
|
||||
focusSpeed = frontEndControlCode.getFocusSpeed();
|
||||
irisSpeed = frontEndControlCode.getIrisSpeed();
|
||||
}
|
||||
ptzService.frontEndCommand(channel, cmdCode, focusSpeed, irisSpeed, parameter3);
|
||||
callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null);
|
||||
@ -348,4 +348,16 @@ public class SourcePTZServiceForGbImpl implements ISourcePTZService {
|
||||
public void queryPreset(CommonGBChannel channel, ErrorCallback<List<Preset>> callback) {
|
||||
ptzService.queryPresetList(channel, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dragZoom(CommonGBChannel channel, FrontEndControlCodeForDragZoom controlCode, ErrorCallback<String> callback) {
|
||||
if (controlCode.getCode() == 1) {
|
||||
ptzService.dragZoomIn(channel, controlCode.getLength(), controlCode.getWidth(), controlCode.getMidPointX(),
|
||||
controlCode.getMidPointY(), controlCode.getLengthX(), controlCode.getLengthY());
|
||||
}else {
|
||||
ptzService.dragZoomOut(channel, controlCode.getLength(), controlCode.getWidth(), controlCode.getMidPointX(),
|
||||
controlCode.getMidPointY(), controlCode.getLengthX(), controlCode.getLengthY());
|
||||
}
|
||||
callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -293,7 +293,7 @@ public interface ISIPCommander {
|
||||
* @param channelId 通道id
|
||||
* @param cmdString 前端控制指令串
|
||||
*/
|
||||
void dragZoomCmd(Device device, String channelId, String cmdString, ErrorCallback<String> callback) throws InvalidArgumentException, SipException, ParseException;
|
||||
void dragZoomCmd(Device device, String channelId, String cmdString) throws InvalidArgumentException, SipException, ParseException;
|
||||
|
||||
|
||||
void playbackControlCmd(Device device, DeviceChannel channel, String stream, String content, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException;
|
||||
|
||||
@ -496,7 +496,7 @@ public class SIPCommander implements ISIPCommander {
|
||||
}
|
||||
if (!mediaServerItem.isRtpEnable()) {
|
||||
// 单端口暂不支持语音喊话
|
||||
log.info("[语音喊话] 单端口暂不支持此操作");
|
||||
log.warn("[语音喊话] 单端口暂不支持此操作");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1292,7 +1292,7 @@ public class SIPCommander implements ISIPCommander {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dragZoomCmd(Device device, String channelId, String cmdString, ErrorCallback<String> callback) throws InvalidArgumentException, SipException, ParseException {
|
||||
public void dragZoomCmd(Device device, String channelId, String cmdString) throws InvalidArgumentException, SipException, ParseException {
|
||||
|
||||
String cmdType = "DeviceControl";
|
||||
int sn = (int) ((Math.random() * 9 + 1) * 100000);
|
||||
@ -1311,9 +1311,6 @@ public class SIPCommander implements ISIPCommander {
|
||||
dragXml.append(cmdString);
|
||||
dragXml.append("</Control>\r\n");
|
||||
|
||||
MessageEvent<String> messageEvent = MessageEvent.getInstance(cmdType, sn + "", channelId, 1000L, callback);
|
||||
messageSubscribe.addSubscribe(messageEvent);
|
||||
|
||||
Request request = headerProvider.createMessageRequest(device, dragXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()));
|
||||
sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()),request);
|
||||
}
|
||||
|
||||
@ -241,6 +241,19 @@ public class DeviceControlQueryMessageHandler extends SIPRequestProcessorParent
|
||||
}
|
||||
}));
|
||||
break;
|
||||
case DRAG_ZOOM:
|
||||
channelControlService.dragZoom(channel, (FrontEndControlCodeForDragZoom) frontEndControlCode, ((code, msg, data) -> {
|
||||
try {
|
||||
if (code == ErrorCode.SUCCESS.getCode()) {
|
||||
responseAck(request, Response.OK);
|
||||
}else {
|
||||
responseAck(request, Response.FORBIDDEN);
|
||||
}
|
||||
} catch (InvalidArgumentException | SipException | ParseException exception) {
|
||||
log.error("[命令发送失败] 辅助开关指令: {}", exception.getMessage());
|
||||
}
|
||||
}));
|
||||
break;
|
||||
default:
|
||||
log.info("[INFO 消息] 设备不支持的控制方式");
|
||||
try {
|
||||
@ -390,9 +403,7 @@ public class DeviceControlQueryMessageHandler extends SIPRequestProcessorParent
|
||||
cmdXml.append("<LengthX>" + dragZoom.getLengthX() + "</LengthX>\r\n");
|
||||
cmdXml.append("<LengthY>" + dragZoom.getLengthY() + "</LengthY>\r\n");
|
||||
cmdXml.append("</" + type.getVal() + ">\r\n");
|
||||
cmder.dragZoomCmd(device, deviceChannel.getDeviceId(), cmdXml.toString(), (code, msg, data) -> {
|
||||
|
||||
});
|
||||
cmder.dragZoomCmd(device, deviceChannel.getDeviceId(), cmdXml.toString());
|
||||
responseAck(request, Response.OK);
|
||||
} catch (Exception e) {
|
||||
log.error("[命令发送失败] 拉框控制: {}", e.getMessage());
|
||||
|
||||
@ -155,4 +155,9 @@ public class SourcePTZServiceForJTImpl implements ISourcePTZService {
|
||||
public void queryPreset(CommonGBChannel channel, ErrorCallback<List<Preset>> callback) {
|
||||
callback.run(ErrorCode.ERROR486.getCode(), ErrorCode.ERROR486.getMsg(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dragZoom(CommonGBChannel channel, FrontEndControlCodeForDragZoom frontEndControlCode, ErrorCallback<String> callback) {
|
||||
callback.run(ErrorCode.ERROR486.getCode(), ErrorCode.ERROR486.getMsg(), null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,10 +4,8 @@ import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.alibaba.fastjson2.TypeReference;
|
||||
import com.genersoft.iot.vmp.conf.exception.ControllerException;
|
||||
import com.genersoft.iot.vmp.media.bean.MediaServer;
|
||||
import com.genersoft.iot.vmp.media.zlm.dto.*;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.*;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
@ -17,11 +15,9 @@ import org.springframework.stereotype.Component;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.ConnectException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -334,11 +330,6 @@ public class ZLMRESTfulUtils {
|
||||
|
||||
public ZLMResult<StreamProxyResult> addFFmpegSource(MediaServer mediaServer, String src_url, String dst_url, Integer timeout_sec,
|
||||
boolean enable_audio, boolean enable_mp4, String ffmpeg_cmd_key){
|
||||
try {
|
||||
src_url = URLEncoder.encode(src_url, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(),"url编码失败");
|
||||
}
|
||||
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("src_url", src_url);
|
||||
|
||||
@ -69,9 +69,9 @@ public interface IRedisRpcService {
|
||||
|
||||
WVPResult<String> homePosition(String serverId, Device device, String channelId, Boolean enabled, Integer resetTime, Integer presetIndex);
|
||||
|
||||
void dragZoomIn(String serverId, Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy);
|
||||
void dragZoomIn(String serverId, Device device, String channelId, int length, int width, int midPointX, int midPointY, int lengthX, int lengthY);
|
||||
|
||||
void dragZoomOut(String serverId, Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy);
|
||||
void dragZoomOut(String serverId, Device device, String channelId, int length, int width, int midPointX, int midPointY, int lengthX, int lengthY);
|
||||
|
||||
WVPResult<String> deviceStatus(String serverId, Device device);
|
||||
|
||||
|
||||
@ -318,10 +318,10 @@ public class RedisRpcDeviceController extends RpcController {
|
||||
String channelId = paramJson.getString("channelId");
|
||||
Integer length = paramJson.getInteger("length");
|
||||
Integer width = paramJson.getInteger("width");
|
||||
Integer midpointx = paramJson.getInteger("midpointx");
|
||||
Integer midpointy = paramJson.getInteger("midpointy");
|
||||
Integer lengthx = paramJson.getInteger("lengthx");
|
||||
Integer lengthy = paramJson.getInteger("lengthy");
|
||||
Integer midPointX = paramJson.getInteger("midPointX");
|
||||
Integer midPointY = paramJson.getInteger("midPointY");
|
||||
Integer lengthX = paramJson.getInteger("lengthX");
|
||||
Integer lengthY = paramJson.getInteger("lengthY");
|
||||
|
||||
Device device = deviceService.getDeviceByDeviceId(deviceId);
|
||||
|
||||
@ -332,18 +332,15 @@ public class RedisRpcDeviceController extends RpcController {
|
||||
return response;
|
||||
}
|
||||
try {
|
||||
deviceService.dragZoomIn(device, channelId, length, width, midpointx, midpointy, lengthx, lengthy, (code, msg, data) -> {
|
||||
response.setStatusCode(ErrorCode.SUCCESS.getCode());
|
||||
response.setBody(new WVPResult<>(code, msg, data));
|
||||
// 手动发送结果
|
||||
sendResponse(response);
|
||||
});
|
||||
deviceService.dragZoomIn(device, channelId, length, width, midPointX, midPointY, lengthX, lengthY);
|
||||
}catch (ControllerException e) {
|
||||
response.setStatusCode(e.getCode());
|
||||
response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg()));
|
||||
sendResponse(response);
|
||||
return response;
|
||||
}
|
||||
return null;
|
||||
response.setStatusCode(ErrorCode.SUCCESS.getCode());
|
||||
response.setBody(WVPResult.success());
|
||||
return response;
|
||||
}
|
||||
|
||||
@RedisRpcMapping("dragZoomOut")
|
||||
@ -353,10 +350,10 @@ public class RedisRpcDeviceController extends RpcController {
|
||||
String channelId = paramJson.getString("channelId");
|
||||
Integer length = paramJson.getInteger("length");
|
||||
Integer width = paramJson.getInteger("width");
|
||||
Integer midpointx = paramJson.getInteger("midpointx");
|
||||
Integer midpointy = paramJson.getInteger("midpointy");
|
||||
Integer lengthx = paramJson.getInteger("lengthx");
|
||||
Integer lengthy = paramJson.getInteger("lengthy");
|
||||
Integer midPointX = paramJson.getInteger("midPointX");
|
||||
Integer midPointY = paramJson.getInteger("midPointY");
|
||||
Integer lengthX = paramJson.getInteger("lengthX");
|
||||
Integer lengthY = paramJson.getInteger("lengthY");
|
||||
|
||||
Device device = deviceService.getDeviceByDeviceId(deviceId);
|
||||
|
||||
@ -367,18 +364,15 @@ public class RedisRpcDeviceController extends RpcController {
|
||||
return response;
|
||||
}
|
||||
try {
|
||||
deviceService.dragZoomOut(device, channelId, length, width, midpointx, midpointy, lengthx, lengthy, (code, msg, data) -> {
|
||||
response.setStatusCode(ErrorCode.SUCCESS.getCode());
|
||||
response.setBody(new WVPResult<>(code, msg, data));
|
||||
// 手动发送结果
|
||||
sendResponse(response);
|
||||
});
|
||||
deviceService.dragZoomOut(device, channelId, length, width, midPointX, midPointY, lengthX, lengthY);
|
||||
}catch (ControllerException e) {
|
||||
response.setStatusCode(e.getCode());
|
||||
response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg()));
|
||||
sendResponse(response);
|
||||
return response;
|
||||
}
|
||||
return null;
|
||||
response.setStatusCode(ErrorCode.SUCCESS.getCode());
|
||||
response.setBody(WVPResult.success());
|
||||
return response;
|
||||
}
|
||||
|
||||
@RedisRpcMapping("alarm")
|
||||
|
||||
@ -433,33 +433,33 @@ public class RedisRpcServiceImpl implements IRedisRpcService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dragZoomIn(String serverId, Device device, String channelId, int length, int width, int midpointx,
|
||||
int midpointy, int lengthx, int lengthy) {
|
||||
public void dragZoomIn(String serverId, Device device, String channelId, int length, int width, int midPointX,
|
||||
int midPointY, int lengthX, int lengthY) {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("device", device.getDeviceId());
|
||||
jsonObject.put("channelId", channelId);
|
||||
jsonObject.put("length", length);
|
||||
jsonObject.put("width", width);
|
||||
jsonObject.put("midpointx", midpointx);
|
||||
jsonObject.put("midpointy", midpointy);
|
||||
jsonObject.put("lengthx", lengthx);
|
||||
jsonObject.put("lengthy", lengthy);
|
||||
jsonObject.put("midPointX", midPointX);
|
||||
jsonObject.put("midPointY", midPointY);
|
||||
jsonObject.put("lengthX", lengthX);
|
||||
jsonObject.put("lengthY", lengthY);
|
||||
RedisRpcRequest request = buildRequest("device/dragZoomIn", jsonObject);
|
||||
request.setToId(serverId);
|
||||
redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dragZoomOut(String serverId, Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy) {
|
||||
public void dragZoomOut(String serverId, Device device, String channelId, int length, int width, int midPointX, int midPointY, int lengthX, int lengthY) {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("device", device.getDeviceId());
|
||||
jsonObject.put("channelId", channelId);
|
||||
jsonObject.put("length", length);
|
||||
jsonObject.put("width", width);
|
||||
jsonObject.put("midpointx", midpointx);
|
||||
jsonObject.put("midpointy", midpointy);
|
||||
jsonObject.put("lengthx", lengthx);
|
||||
jsonObject.put("lengthy", lengthy);
|
||||
jsonObject.put("midPointX", midPointX);
|
||||
jsonObject.put("midPointY", midPointY);
|
||||
jsonObject.put("lengthX", lengthX);
|
||||
jsonObject.put("lengthY", lengthY);
|
||||
RedisRpcRequest request = buildRequest("device/dragZoomOut", jsonObject);
|
||||
request.setToId(serverId);
|
||||
redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS);
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
package com.genersoft.iot.vmp.vmanager.bean;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @author lin
|
||||
*/
|
||||
@Setter
|
||||
@Getter
|
||||
public class AudioBroadcastResult {
|
||||
/**
|
||||
* 推流的各个方式流地址
|
||||
@ -24,36 +29,10 @@ public class AudioBroadcastResult {
|
||||
*/
|
||||
private String stream;
|
||||
|
||||
/**
|
||||
* 播放流地址(设备音频通过ZLM播放给浏览器),对讲时设置
|
||||
*/
|
||||
private StreamContent playStreamInfo;
|
||||
|
||||
public StreamContent getStreamInfo() {
|
||||
return streamInfo;
|
||||
}
|
||||
|
||||
public void setStreamInfo(StreamContent streamInfo) {
|
||||
this.streamInfo = streamInfo;
|
||||
}
|
||||
|
||||
public String getCodec() {
|
||||
return codec;
|
||||
}
|
||||
|
||||
public void setCodec(String codec) {
|
||||
this.codec = codec;
|
||||
}
|
||||
|
||||
public String getApp() {
|
||||
return app;
|
||||
}
|
||||
|
||||
public void setApp(String app) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
public String getStream() {
|
||||
return stream;
|
||||
}
|
||||
|
||||
public void setStream(String stream) {
|
||||
this.stream = stream;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
package com.genersoft.iot.vmp.vmanager.bean;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "对讲信息")
|
||||
public class AudioTalkResult {
|
||||
|
||||
@Schema(description = "推流地址(浏览器 WebRTC推流到ZLM)")
|
||||
private StreamContent pushStream;
|
||||
|
||||
@Schema(description = "播放地址(设备音频通过ZLM播放给浏览器),喊话时为null")
|
||||
private StreamContent playStream;
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
package com.genersoft.iot.vmp.gb28181.bean;
|
||||
|
||||
import org.dom4j.DocumentHelper;
|
||||
import org.dom4j.Element;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class DeviceAlarmNotifyTest {
|
||||
|
||||
@Test
|
||||
void fromXml_withoutAlarmType_shouldNotThrowNpe() throws Exception {
|
||||
String xml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Notify>
|
||||
<DeviceID>55123456781381000010</DeviceID>
|
||||
<AlarmPriority>1</AlarmPriority>
|
||||
<AlarmMethod>7</AlarmMethod>
|
||||
<AlarmTime>2026-06-05T09:46:05</AlarmTime>
|
||||
<AlarmDescription>1001,1780623964994529058,55123456781381000010,25123456781381000050,55LCPCweb10</AlarmDescription>
|
||||
<Longitude>0.0</Longitude>
|
||||
<Latitude>0.0</Latitude>
|
||||
</Notify>
|
||||
""";
|
||||
|
||||
Element root = DocumentHelper.parseText(xml).getRootElement();
|
||||
DeviceAlarmNotify notify = DeviceAlarmNotify.fromXml(root);
|
||||
|
||||
assertNotNull(notify);
|
||||
assertEquals(Integer.valueOf(7), notify.getAlarmMethod());
|
||||
assertNull(notify.getAlarmType(), "AlarmType should be null when not present in XML");
|
||||
|
||||
// Simulate the exact code path from AlarmNotifyMessageHandler.executeTaskQueue lines 131-141
|
||||
// which was causing the NPE
|
||||
AlarmChannelMessage alarmChannelMessage = new AlarmChannelMessage();
|
||||
assertDoesNotThrow(() -> {
|
||||
alarmChannelMessage.setAlarmType(notify.getAlarmType());
|
||||
alarmChannelMessage.setAlarmSn(notify.getAlarmMethod());
|
||||
alarmChannelMessage.setAlarmDescription(notify.getAlarmDescription());
|
||||
alarmChannelMessage.setGbId(notify.getChannelId());
|
||||
}, "setAlarmType(null) should not throw NPE when field type is Integer");
|
||||
assertNull(alarmChannelMessage.getAlarmType());
|
||||
assertEquals(Integer.valueOf(7), alarmChannelMessage.getAlarmSn());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromXml_withAlarmType_shouldParseCorrectly() throws Exception {
|
||||
String xml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Notify>
|
||||
<DeviceID>34020000001320000001</DeviceID>
|
||||
<AlarmPriority>1</AlarmPriority>
|
||||
<AlarmMethod>2</AlarmMethod>
|
||||
<AlarmTime>2026-06-05T10:30:00</AlarmTime>
|
||||
<AlarmDescription>Video loss alarm</AlarmDescription>
|
||||
<Longitude>116.397</Longitude>
|
||||
<Latitude>39.908</Latitude>
|
||||
<AlarmType>1</AlarmType>
|
||||
</Notify>
|
||||
""";
|
||||
|
||||
Element root = DocumentHelper.parseText(xml).getRootElement();
|
||||
DeviceAlarmNotify notify = DeviceAlarmNotify.fromXml(root);
|
||||
|
||||
assertNotNull(notify);
|
||||
assertEquals(Integer.valueOf(2), notify.getAlarmMethod());
|
||||
assertEquals(Integer.valueOf(1), notify.getAlarmType());
|
||||
|
||||
AlarmChannelMessage msg = new AlarmChannelMessage();
|
||||
assertDoesNotThrow(() -> msg.setAlarmType(notify.getAlarmType()));
|
||||
assertEquals(Integer.valueOf(1), msg.getAlarmType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromXml_withAlarmTypeInInfo_shouldUseInfoValue() throws Exception {
|
||||
String xml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Notify>
|
||||
<DeviceID>34020000001320000001</DeviceID>
|
||||
<AlarmPriority>1</AlarmPriority>
|
||||
<AlarmMethod>5</AlarmMethod>
|
||||
<AlarmTime>2026-06-05T10:30:00</AlarmTime>
|
||||
<AlarmDescription>Motion detection</AlarmDescription>
|
||||
<Longitude>116.397</Longitude>
|
||||
<Latitude>39.908</Latitude>
|
||||
<AlarmType>9</AlarmType>
|
||||
<Info>
|
||||
<AlarmType>2</AlarmType>
|
||||
</Info>
|
||||
</Notify>
|
||||
""";
|
||||
|
||||
Element root = DocumentHelper.parseText(xml).getRootElement();
|
||||
DeviceAlarmNotify notify = DeviceAlarmNotify.fromXml(root);
|
||||
|
||||
assertNotNull(notify);
|
||||
assertEquals(Integer.valueOf(2), notify.getAlarmType(),
|
||||
"AlarmType should use Info/AlarmType value when present");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package com.genersoft.iot.vmp.jt1078.dao.provider;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class JTChannelProviderTest {
|
||||
|
||||
private final JTChannelProvider provider = new JTChannelProvider();
|
||||
|
||||
@Test
|
||||
void selectAll_withQuery_shouldUseBindVariable() {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("terminalDbId", 1);
|
||||
params.put("query", "test-channel");
|
||||
String sql = provider.selectAll(params);
|
||||
assertTrue(sql.contains("#{query}"), "should use #{query} bind variable");
|
||||
assertFalse(sql.contains("test-channel"), "should not contain raw query value");
|
||||
assertTrue(sql.contains("concat('%',#{query},'%')"), "should use concat with bind variable");
|
||||
assertTrue(sql.contains("#{terminalDbId}"), "should use #{terminalDbId} bind variable");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectAll_withoutQuery_shouldNotContainLike() {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("terminalDbId", 1);
|
||||
String sql = provider.selectAll(params);
|
||||
assertFalse(sql.contains("LIKE"), "should not contain LIKE clause when no query");
|
||||
assertTrue(sql.contains("#{terminalDbId}"), "should still have terminalDbId condition");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectChannelByChannelId_shouldUseBindVariables() {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("terminalDbId", 5);
|
||||
params.put("channelId", 100);
|
||||
String sql = provider.selectChannelByChannelId(params);
|
||||
assertTrue(sql.contains("#{terminalDbId}"), "should use #{terminalDbId}");
|
||||
assertTrue(sql.contains("#{channelId}"), "should use #{channelId}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectChannelById_shouldUseBindVariable() {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("id", 42);
|
||||
String sql = provider.selectChannelById(params);
|
||||
assertTrue(sql.contains("#{id}"), "should use #{id} bind variable");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectAll_shouldOrderByChannelId() {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("terminalDbId", 1);
|
||||
String sql = provider.selectAll(params);
|
||||
assertTrue(sql.contains("ORDER BY jc.channel_id"), "should order by channel_id");
|
||||
}
|
||||
|
||||
@Test
|
||||
void baseSql_shouldHaveJoins() {
|
||||
assertTrue(JTChannelProvider.BASE_SQL.contains("LEFT join wvp_device_channel"), "should have LEFT JOIN");
|
||||
assertTrue(JTChannelProvider.BASE_SQL.contains("wvp_jt_channel"), "should query from jt_channel");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
package com.genersoft.iot.vmp.streamProxy.dao.provider;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class StreamProxyProviderTest {
|
||||
|
||||
private final StreamProxyProvider provider = new StreamProxyProvider();
|
||||
|
||||
@Test
|
||||
void select_shouldUseBindVariable() {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("id", 123);
|
||||
String sql = provider.select(params);
|
||||
assertTrue(sql.contains("#{id}"), "should use #{id} bind variable");
|
||||
assertFalse(sql.contains("123"), "should not contain raw value");
|
||||
assertTrue(sql.contains("WHERE st.id = #{id}"), "should have proper WHERE clause");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectOneByAppAndStream_shouldUseBindVariables() {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("app", "testApp");
|
||||
params.put("stream", "testStream");
|
||||
String sql = provider.selectOneByAppAndStream(params);
|
||||
assertTrue(sql.contains("#{app}"), "should use #{app} bind variable");
|
||||
assertTrue(sql.contains("#{stream}"), "should use #{stream} bind variable");
|
||||
assertFalse(sql.contains("testApp"), "should not contain raw app value");
|
||||
assertFalse(sql.contains("testStream"), "should not contain raw stream value");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectForPushingInMediaServer_shouldUseBindVariable() {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("mediaServerId", "server-001");
|
||||
String sql = provider.selectForPushingInMediaServer(params);
|
||||
assertTrue(sql.contains("#{mediaServerId}"), "should use #{mediaServerId} bind variable");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectAll_withQuery_shouldUseBindVariable() {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("query", "test-query");
|
||||
String sql = provider.selectAll(params);
|
||||
assertTrue(sql.contains("#{query}"), "should use #{query} bind variable");
|
||||
assertFalse(sql.contains("test-query"), "should not contain raw query value");
|
||||
assertTrue(sql.contains("LIKE concat('%',#{query},'%')"), "should use concat with bind variable");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectAll_withMediaServerId_shouldUseBindVariable() {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("mediaServerId", "server-001");
|
||||
String sql = provider.selectAll(params);
|
||||
assertTrue(sql.contains("#{mediaServerId}"), "should use #{mediaServerId} bind variable");
|
||||
assertFalse(sql.contains("server-001"), "should not contain raw server id");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectAll_withPullingTrue() {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("pulling", true);
|
||||
String sql = provider.selectAll(params);
|
||||
assertTrue(sql.contains("st.pulling=1"), "should filter by pulling=1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectAll_withPullingFalse() {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("pulling", false);
|
||||
String sql = provider.selectAll(params);
|
||||
assertTrue(sql.contains("st.pulling=0"), "should filter by pulling=0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectAll_withoutParams_shouldReturnBaseQuery() {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
String sql = provider.selectAll(params);
|
||||
assertTrue(sql.contains("FROM wvp_stream_proxy"), "should have FROM clause");
|
||||
assertTrue(sql.contains("LEFT join wvp_device_channel"), "should have JOIN clause");
|
||||
assertTrue(sql.contains("order by"), "should have ORDER BY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBaseSelectSql_shouldReturnValidSql() {
|
||||
String sql = provider.getBaseSelectSql();
|
||||
assertTrue(sql.contains("SELECT"), "should start with SELECT");
|
||||
assertTrue(sql.contains("FROM wvp_stream_proxy"), "should have FROM");
|
||||
assertTrue(sql.contains("LEFT join wvp_device_channel"), "should have LEFT JOIN");
|
||||
}
|
||||
}
|
||||
@ -270,6 +270,38 @@ export function stopPlayChannel(channelId) {
|
||||
})
|
||||
}
|
||||
|
||||
export function talkStart(channelId) {
|
||||
return request({
|
||||
method: 'get',
|
||||
url: '/api/common/channel/talk/start',
|
||||
params: { channelId }
|
||||
})
|
||||
}
|
||||
|
||||
export function talkStop(channelId) {
|
||||
return request({
|
||||
method: 'get',
|
||||
url: '/api/common/channel/talk/stop',
|
||||
params: { channelId }
|
||||
})
|
||||
}
|
||||
|
||||
export function broadcastStart(channelId) {
|
||||
return request({
|
||||
method: 'get',
|
||||
url: '/api/common/channel/broadcast/start',
|
||||
params: { channelId }
|
||||
})
|
||||
}
|
||||
|
||||
export function broadcastStop(channelId) {
|
||||
return request({
|
||||
method: 'get',
|
||||
url: '/api/common/channel/broadcast/stop',
|
||||
params: { channelId }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 前端控制
|
||||
|
||||
@ -512,6 +544,20 @@ export function focus({ channelId, command, speed }) {
|
||||
}
|
||||
})
|
||||
}
|
||||
export function dragZoomIn(params) {
|
||||
return request({
|
||||
method: 'get',
|
||||
url: '/api/common/channel/front-end/drag_zoom_in',
|
||||
params
|
||||
})
|
||||
}
|
||||
export function dragZoomOut(params) {
|
||||
return request({
|
||||
method: 'get',
|
||||
url: '/api/common/channel/front-end/drag_zoom_out',
|
||||
params
|
||||
})
|
||||
}
|
||||
export function queryRecord({ channelId, startTime, endTime }) {
|
||||
return request({
|
||||
method: 'get',
|
||||
|
||||
@ -69,6 +69,14 @@ export function resetGuard(deviceId) {
|
||||
})
|
||||
}
|
||||
|
||||
export function homePosition(params) {
|
||||
return request({
|
||||
method: 'get',
|
||||
url: '/api/device/control/home_position',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function subscribeCatalog(params) {
|
||||
const { id, cycle } = params
|
||||
return request({
|
||||
@ -275,3 +283,19 @@ export function getRegisterTimeStatistics({ deviceId, count }) {
|
||||
})
|
||||
}
|
||||
|
||||
export function dragZoomIn(params) {
|
||||
return request({
|
||||
method: 'get',
|
||||
url: '/api/device/control/drag_zoom/zoom_in',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function dragZoomOut(params) {
|
||||
return request({
|
||||
method: 'get',
|
||||
url: '/api/device/control/drag_zoom/zoom_out',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -182,7 +182,7 @@ export function wiper([deviceId, channelDeviceId, command]) {
|
||||
})
|
||||
}
|
||||
|
||||
export function ptz([deviceId, channelId, command, horizonSpeed, verticalSpeed, zoomSpeed]) {
|
||||
export function ptz({ deviceId, channelId, command, horizonSpeed, verticalSpeed, zoomSpeed }) {
|
||||
return request({
|
||||
method: 'get',
|
||||
url: `/api/front-end/ptz/${deviceId}/${channelId}`,
|
||||
|
||||
208
web/src/mixins/dragZoom.js
Normal file
208
web/src/mixins/dragZoom.js
Normal file
@ -0,0 +1,208 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
dragGridEnabled: true,
|
||||
overlayCanvas: null,
|
||||
overlayCtx: null,
|
||||
dragActive: false,
|
||||
dragStart: null,
|
||||
dragCurrent: null,
|
||||
dragVideoRect: null,
|
||||
dragCallback: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dragRect() {
|
||||
if (!this.dragStart || !this.dragCurrent) return null
|
||||
return {
|
||||
left: Math.min(this.dragStart.x, this.dragCurrent.x),
|
||||
top: Math.min(this.dragStart.y, this.dragCurrent.y),
|
||||
width: Math.abs(this.dragCurrent.x - this.dragStart.x),
|
||||
height: Math.abs(this.dragCurrent.y - this.dragStart.y)
|
||||
}
|
||||
},
|
||||
dragInfo() {
|
||||
if (!this.dragRect) return null
|
||||
return {
|
||||
midX: Math.round(this.dragRect.left + this.dragRect.width / 2),
|
||||
midY: Math.round(this.dragRect.top + this.dragRect.height / 2),
|
||||
width: Math.round(this.dragRect.width),
|
||||
height: Math.round(this.dragRect.height)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this._removeCanvas()
|
||||
},
|
||||
methods: {
|
||||
getVideoElement() {
|
||||
return null
|
||||
},
|
||||
_ensureCanvas() {
|
||||
this._removeCanvas()
|
||||
const videoRect = this.getVideoRect()
|
||||
if (!videoRect) return null
|
||||
const parentRect = this.$el.getBoundingClientRect()
|
||||
const w = Math.round(videoRect.width)
|
||||
const h = Math.round(videoRect.height)
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.style.position = 'absolute'
|
||||
canvas.style.left = (videoRect.left - parentRect.left) + 'px'
|
||||
canvas.style.top = (videoRect.top - parentRect.top) + 'px'
|
||||
canvas.style.width = w + 'px'
|
||||
canvas.style.height = h + 'px'
|
||||
canvas.width = w
|
||||
canvas.height = h
|
||||
canvas.style.zIndex = '999'
|
||||
canvas.style.pointerEvents = 'none'
|
||||
console.log('this.dragGridEnabled: ' + this.dragGridEnabled)
|
||||
if (this.dragGridEnabled) {
|
||||
console.log('加载网格背景')
|
||||
canvas.style.backgroundImage =
|
||||
'linear-gradient(rgba(64, 158, 255, 0.3) 1px, transparent 2px),' +
|
||||
'linear-gradient(90deg, rgba(64, 158, 255, 0.3) 1px, transparent 2px)'
|
||||
canvas.style.backgroundSize = '25px 25px'
|
||||
canvas.style.border = '2px solid #409EFF'
|
||||
}
|
||||
|
||||
this.$el.appendChild(canvas)
|
||||
console.log(this.$el)
|
||||
const ctx = canvas.getContext('2d')
|
||||
this.overlayCanvas = canvas
|
||||
this.overlayCtx = ctx
|
||||
return { canvas, ctx }
|
||||
},
|
||||
_removeCanvas() {
|
||||
this._unbindDragEvents()
|
||||
if (this.overlayCanvas && this.overlayCanvas.parentNode) {
|
||||
this.overlayCanvas.parentNode.removeChild(this.overlayCanvas)
|
||||
}
|
||||
this.overlayCanvas = null
|
||||
this.overlayCtx = null
|
||||
},
|
||||
_bindDragEvents() {
|
||||
const c = this.overlayCanvas
|
||||
if (!c) return
|
||||
c.style.pointerEvents = 'auto'
|
||||
c.style.cursor = 'crosshair'
|
||||
c.addEventListener('mousedown', this._onDragMouseDown)
|
||||
c.addEventListener('mousemove', this._onDragMove)
|
||||
c.addEventListener('mouseup', this._onDragEnd)
|
||||
c.addEventListener('mouseleave', this._onDragEnd)
|
||||
},
|
||||
_unbindDragEvents() {
|
||||
const c = this.overlayCanvas
|
||||
if (!c) return
|
||||
c.style.pointerEvents = 'none'
|
||||
c.style.cursor = 'default'
|
||||
c.removeEventListener('mousedown', this._onDragMouseDown)
|
||||
c.removeEventListener('mousemove', this._onDragMove)
|
||||
c.removeEventListener('mouseup', this._onDragEnd)
|
||||
c.removeEventListener('mouseleave', this._onDragEnd)
|
||||
},
|
||||
_drawOverlay() {
|
||||
const ctx = this.overlayCtx
|
||||
const canvas = this.overlayCanvas
|
||||
if (!ctx || !canvas) return
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
if (this.dragRect) {
|
||||
this._drawDragRect(ctx)
|
||||
}
|
||||
},
|
||||
_drawDragRect(ctx) {
|
||||
const r = this.dragRect
|
||||
if (!r) return
|
||||
ctx.strokeStyle = '#409EFF'
|
||||
ctx.lineWidth = 2
|
||||
ctx.setLineDash([6, 3])
|
||||
ctx.fillStyle = 'rgba(64, 158, 255, 0.15)'
|
||||
ctx.beginPath()
|
||||
ctx.rect(r.left, r.top, r.width, r.height)
|
||||
ctx.fill()
|
||||
ctx.stroke()
|
||||
ctx.setLineDash([])
|
||||
const info = this.dragInfo
|
||||
if (!info) return
|
||||
const text = '\u4E2D\u5FC3: (' + info.midX + ', ' + info.midY + ') \u5927\u5C0F: ' + info.width + ' \u00D7 ' + info.height
|
||||
ctx.font = '12px sans-serif'
|
||||
const textW = ctx.measureText(text).width
|
||||
const labelW = textW + 16
|
||||
const labelH = 22
|
||||
const labelX = r.left
|
||||
const labelY = r.top + r.height + 6
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'
|
||||
ctx.fillRect(labelX, labelY, labelW, labelH)
|
||||
ctx.fillStyle = '#fff'
|
||||
ctx.fillText(text, labelX + 8, labelY + 15)
|
||||
},
|
||||
startDragZoom(callback) {
|
||||
this._ensureCanvas()
|
||||
this._bindDragEvents()
|
||||
this.dragCallback = callback || null
|
||||
this.dragActive = true
|
||||
this.dragStart = null
|
||||
this.dragCurrent = null
|
||||
this.dragVideoRect = null
|
||||
},
|
||||
_onDragMouseDown(e) {
|
||||
if (!this.dragActive) return
|
||||
e.preventDefault()
|
||||
const videoRect = this.getVideoRect()
|
||||
if (!videoRect) return
|
||||
this.dragVideoRect = videoRect
|
||||
this.dragStart = {
|
||||
x: e.clientX - videoRect.left,
|
||||
y: e.clientY - videoRect.top
|
||||
}
|
||||
this.dragCurrent = { ...this.dragStart }
|
||||
console.log('[dragZoom mousedown] getVideoRect:', JSON.stringify(videoRect), 'clientX/Y:', e.clientX, e.clientY, 'dragStart:', JSON.stringify(this.dragStart))
|
||||
this._drawOverlay()
|
||||
},
|
||||
_onDragMove(e) {
|
||||
if (!this.dragActive || !this.dragStart || !this.dragVideoRect) return
|
||||
e.preventDefault()
|
||||
this.dragCurrent = {
|
||||
x: e.clientX - this.dragVideoRect.left,
|
||||
y: e.clientY - this.dragVideoRect.top
|
||||
}
|
||||
this._drawOverlay()
|
||||
},
|
||||
_onDragEnd() {
|
||||
if (!this.dragActive) return
|
||||
if (!this.dragStart || !this.dragCurrent) return
|
||||
const sx = Math.min(this.dragStart.x, this.dragCurrent.x)
|
||||
const sy = Math.min(this.dragStart.y, this.dragCurrent.y)
|
||||
const ex = Math.max(this.dragStart.x, this.dragCurrent.x)
|
||||
const ey = Math.max(this.dragStart.y, this.dragCurrent.y)
|
||||
const rectW = ex - sx
|
||||
const rectH = ey - sy
|
||||
if (rectW < 10 || rectH < 10) {
|
||||
this._resetDrag()
|
||||
return
|
||||
}
|
||||
console.log('[dragZoom dragEnd] sx:', sx, 'sy:', sy, 'ex:', ex, 'ey:', ey, 'rectW:', rectW, 'rectH:', rectH)
|
||||
if (this.dragCallback) {
|
||||
const params = {
|
||||
length: Math.round(this.dragVideoRect.width),
|
||||
width: Math.round(this.dragVideoRect.height),
|
||||
midPointX: Math.round(sx + rectW / 2),
|
||||
midPointY: Math.round(sy + rectH / 2),
|
||||
lengthX: Math.round(rectW),
|
||||
lengthY: Math.round(rectH)
|
||||
}
|
||||
console.log('[dragZoom dragEnd] callback params:', JSON.stringify(params))
|
||||
this.dragCallback(params)
|
||||
}
|
||||
this._resetDrag()
|
||||
},
|
||||
_resetDrag() {
|
||||
this._unbindDragEvents()
|
||||
this.dragActive = false
|
||||
this.dragStart = null
|
||||
this.dragCurrent = null
|
||||
this.dragVideoRect = null
|
||||
this.dragCallback = null
|
||||
this._removeCanvas()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -75,7 +75,7 @@ export const constantRoutes = [
|
||||
path: '/channel',
|
||||
name: 'Channel',
|
||||
component: () => import('@/views/channel/index'),
|
||||
meta: { title: '通道列表', icon: 'channelManger'}
|
||||
meta: { title: '通道列表', icon: 'channelManger' }
|
||||
},
|
||||
{
|
||||
path: '/channel/record/:channelId',
|
||||
|
||||
@ -48,7 +48,9 @@ import {
|
||||
stopPlayback,
|
||||
pausePlayback,
|
||||
resumePlayback,
|
||||
seekPlayback, speedPlayback, getAllForMap, test, saveLevel, resetLevel, clearThin, thinProgress, drawThin, saveThin
|
||||
seekPlayback, speedPlayback, getAllForMap, test, saveLevel, resetLevel, clearThin, thinProgress, drawThin, saveThin,
|
||||
dragZoomIn, dragZoomOut,
|
||||
talkStart, talkStop, broadcastStart, broadcastStop
|
||||
} from '@/api/commonChannel'
|
||||
|
||||
const actions = {
|
||||
@ -282,6 +284,46 @@ const actions = {
|
||||
})
|
||||
})
|
||||
},
|
||||
talkStart({ commit }, channelId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
talkStart(channelId).then(response => {
|
||||
const { data } = response
|
||||
resolve(data)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
talkStop({ commit }, channelId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
talkStop(channelId).then(response => {
|
||||
const { data } = response
|
||||
resolve(data)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
broadcastStart({ commit }, channelId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
broadcastStart(channelId).then(response => {
|
||||
const { data } = response
|
||||
resolve(data)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
broadcastStop({ commit }, channelId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
broadcastStop(channelId).then(response => {
|
||||
const { data } = response
|
||||
resolve(data)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
getList({ commit }, param) {
|
||||
return new Promise((resolve, reject) => {
|
||||
getList(param).then(response => {
|
||||
@ -492,6 +534,26 @@ const actions = {
|
||||
})
|
||||
})
|
||||
},
|
||||
dragZoomIn({ commit }, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
dragZoomIn(params).then(response => {
|
||||
const { data } = response
|
||||
resolve(data)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
dragZoomOut({ commit }, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
dragZoomOut(params).then(response => {
|
||||
const { data } = response
|
||||
resolve(data)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
queryRecord({ commit }, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
queryRecord(params).then(response => {
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
changeChannelAudio,
|
||||
deleteDevice,
|
||||
deviceRecord, getKeepaliveTimeStatistics, getRegisterTimeStatistics,
|
||||
homePosition,
|
||||
queryBasicParam,
|
||||
queryChannelOne,
|
||||
queryChannels,
|
||||
@ -83,6 +84,16 @@ const actions = {
|
||||
})
|
||||
})
|
||||
},
|
||||
homePosition({ commit }, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
homePosition(params).then(response => {
|
||||
const { data } = response
|
||||
resolve(data)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
subscribeCatalog({ commit }, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
subscribeCatalog(params).then(response => {
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
startScan, stopCruise,
|
||||
stopScan, wiper
|
||||
} from '@/api/frontEnd'
|
||||
import { dragZoomIn, dragZoomOut } from '@/api/device'
|
||||
|
||||
const actions = {
|
||||
setSpeedForScan({ commit }, params) {
|
||||
@ -208,6 +209,26 @@ const actions = {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
dragZoomIn({ commit }, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
dragZoomIn(params).then(response => {
|
||||
const { data } = response
|
||||
resolve(data)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
dragZoomOut({ commit }, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
dragZoomOut(params).then(response => {
|
||||
const { data } = response
|
||||
resolve(data)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 1291092 */
|
||||
src: url('iconfont.woff2?t=1769409737891') format('woff2')
|
||||
src: url('iconfont.woff2?t=1780559263294') format('woff2');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@ -11,6 +11,54 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-dingshirenwuguanli:before {
|
||||
content: "\e800";
|
||||
}
|
||||
|
||||
.icon-zhongqi:before {
|
||||
content: "\e801";
|
||||
}
|
||||
|
||||
.icon-yunfuyangjiaojiao:before {
|
||||
content: "\e7ff";
|
||||
}
|
||||
|
||||
.icon-free:before {
|
||||
content: "\e7fd";
|
||||
}
|
||||
|
||||
.icon-Union-32:before {
|
||||
content: "\e7fe";
|
||||
}
|
||||
|
||||
.icon-ruanxianwei:before {
|
||||
content: "\e7fc";
|
||||
}
|
||||
|
||||
.icon-sudu:before {
|
||||
content: "\e7fb";
|
||||
}
|
||||
|
||||
.icon-shuipingxuanzhuan:before {
|
||||
content: "\e7fa";
|
||||
}
|
||||
|
||||
.icon-cengdie:before {
|
||||
content: "\e7f9";
|
||||
}
|
||||
|
||||
.icon-yinpin:before {
|
||||
content: "\e7f6";
|
||||
}
|
||||
|
||||
.icon-xiangjishezhi2:before {
|
||||
content: "\e7f7";
|
||||
}
|
||||
|
||||
.icon-cengdie3:before {
|
||||
content: "\e815";
|
||||
}
|
||||
|
||||
.icon-xintiao:before {
|
||||
content: "\e7f4";
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -161,11 +161,11 @@
|
||||
<i class="el-icon-warning-outline" style="font-size: 32px;" />
|
||||
<div style="margin-top: 10px;">{{ playbackError }}</div>
|
||||
</div>
|
||||
<div v-else-if="playbackStreamInfo">
|
||||
<h265web
|
||||
<div v-else-if="playbackStreamInfo" style="height: 400px;">
|
||||
<playerTabs
|
||||
ref="playbackPlayer"
|
||||
:height="'400px'"
|
||||
:show-button="false"
|
||||
:showTab="true"
|
||||
:has-audio="true"
|
||||
/>
|
||||
</div>
|
||||
@ -177,7 +177,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import h265web from '../common/h265web.vue'
|
||||
import playerTabs from '../common/playerTabs.vue'
|
||||
|
||||
const ALARM_TYPE_OPTIONS = [
|
||||
{ value: 'VideoLoss', label: '视频丢失报警' },
|
||||
@ -221,7 +221,7 @@ function formatDatetime(ts) {
|
||||
|
||||
export default {
|
||||
name: 'AlarmManage',
|
||||
components: { h265web },
|
||||
components: { playerTabs },
|
||||
data() {
|
||||
return {
|
||||
alarmList: [],
|
||||
@ -238,7 +238,6 @@ export default {
|
||||
playbackLoading: false,
|
||||
playbackError: null,
|
||||
playbackStreamInfo: null,
|
||||
playbackVideoUrl: null,
|
||||
playbackTitle: '录像回放',
|
||||
currentPlaybackChannelId: null
|
||||
}
|
||||
@ -308,15 +307,10 @@ export default {
|
||||
endTime: endTime
|
||||
}).then(data => {
|
||||
this.playbackStreamInfo = data
|
||||
if (location.protocol === 'https:') {
|
||||
this.playbackVideoUrl = data['wss_flv']
|
||||
} else {
|
||||
this.playbackVideoUrl = data['ws_flv']
|
||||
}
|
||||
this.playbackLoading = false
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.playbackPlayer) {
|
||||
this.$refs.playbackPlayer.play(this.playbackVideoUrl)
|
||||
this.$refs.playbackPlayer.setStreamInfo(data)
|
||||
}
|
||||
})
|
||||
}).catch(err => {
|
||||
@ -326,6 +320,9 @@ export default {
|
||||
})
|
||||
},
|
||||
closePlayback() {
|
||||
if (this.$refs.playbackPlayer) {
|
||||
this.$refs.playbackPlayer.stop()
|
||||
}
|
||||
if (this.playbackStreamInfo && this.currentPlaybackChannelId) {
|
||||
this.$store.dispatch('commonChanel/stopPlayback', {
|
||||
channelId: this.currentPlaybackChannelId,
|
||||
@ -336,7 +333,6 @@ export default {
|
||||
}
|
||||
this.playbackDialogVisible = false
|
||||
this.playbackStreamInfo = null
|
||||
this.playbackVideoUrl = null
|
||||
this.playbackError = null
|
||||
this.currentPlaybackChannelId = null
|
||||
},
|
||||
|
||||
448
web/src/views/channel/audioTalk.vue
Normal file
448
web/src/views/channel/audioTalk.vue
Normal file
@ -0,0 +1,448 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog
|
||||
title="语音对讲"
|
||||
top="10vh"
|
||||
width="65vw"
|
||||
:close-on-click-modal="false"
|
||||
:visible.sync="showDialog"
|
||||
@close="close()"
|
||||
>
|
||||
<div style="display: flex; gap: 16px;">
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div v-if="!showPlayer" class="player-placeholder">
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-video-play"
|
||||
:loading="previewLoading"
|
||||
@click="startPreview"
|
||||
>开启预览</el-button>
|
||||
</div>
|
||||
<playerTabs
|
||||
v-if="showPlayer"
|
||||
ref="playerTabs"
|
||||
style="min-height: 60vh;"
|
||||
:has-audio="hasAudio"
|
||||
:show-button="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="broadcast-panel">
|
||||
<div style="text-align: center;">
|
||||
<video id="audioTalkVideo" controls autoplay style="width: 0; height: 0">
|
||||
Your browser is too old which doesn't support HTML5 video.
|
||||
</video>
|
||||
<el-radio-group v-model="talkMode" size="big" @change="onModeChange">
|
||||
<el-radio-button :label="false">喊话</el-radio-button>
|
||||
<el-radio-button :label="true">对讲</el-radio-button>
|
||||
</el-radio-group>
|
||||
<p style="color: #909399; font-size: 14px; margin-top: 4px;">
|
||||
{{ talkMode ? '双向语音交互,可听到设备声音' : '单向喊话,仅向设备发送语音' }}
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<el-button
|
||||
:type="getTalkButtonType()"
|
||||
:disabled="talkStatus === -2"
|
||||
circle
|
||||
icon="el-icon-microphone"
|
||||
style="font-size: 32px; padding: 24px;"
|
||||
@click="talkButtonClick()"
|
||||
/>
|
||||
<p style="margin-top: 16px; color: #606266;">
|
||||
<span v-if="talkStatus === -2">正在释放资源</span>
|
||||
<span v-if="talkStatus === -1">点击开始{{ talkMode ? '对讲' : '喊话' }}</span>
|
||||
<span v-if="talkStatus === 0">等待接通中...</span>
|
||||
<span v-if="talkStatus === 1 && !talkMode">喊话中</span>
|
||||
<span v-if="talkStatus === 1 && talkMode && !playConnected">等待接通中...</span>
|
||||
<span v-if="talkStatus === 1 && talkMode && playConnected">对讲中</span>
|
||||
</p>
|
||||
<p v-if="talkStatus === 1 && talkMode && talkAudioFailed" style="margin-top: 8px;">
|
||||
<el-button
|
||||
type="warning"
|
||||
size="mini"
|
||||
icon="el-icon-refresh"
|
||||
@click="retryTalkAudio"
|
||||
>重试音频</el-button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import playerTabs from '../common/playerTabs.vue'
|
||||
|
||||
export default {
|
||||
name: 'ChAudioTalk',
|
||||
components: { playerTabs },
|
||||
data() {
|
||||
return {
|
||||
showDialog: false,
|
||||
showPlayer: false,
|
||||
previewLoading: false,
|
||||
channelId: null,
|
||||
hasAudio: false,
|
||||
streamInfo: null,
|
||||
talkMode: false,
|
||||
talkStatus: -1,
|
||||
broadcastRtc: null,
|
||||
talkAudioRtc: null,
|
||||
talkAudioRetryTimer: null,
|
||||
talkAudioFailed: false,
|
||||
talkAudioPlayStream: null,
|
||||
playConnected: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.talkStatus = -1
|
||||
},
|
||||
methods: {
|
||||
openDialog(channelId) {
|
||||
if (this.showDialog) return
|
||||
this.channelId = channelId
|
||||
this.talkMode = false
|
||||
this.showPlayer = false
|
||||
this.streamInfo = null
|
||||
this.showDialog = true
|
||||
},
|
||||
onModeChange() {
|
||||
if (this.talkStatus > -1) {
|
||||
this.stopTalk()
|
||||
}
|
||||
},
|
||||
startPreview() {
|
||||
this.previewLoading = true
|
||||
this.$store.dispatch('commonChanel/playChannel', this.channelId)
|
||||
.then(data => {
|
||||
this.streamInfo = data
|
||||
this.hasAudio = data.hasAudio
|
||||
this.showPlayer = true
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.playerTabs) {
|
||||
this.$refs.playerTabs.setStreamInfo(data.transcodeStream || data)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
this.$message({ showClose: true, message: e, type: 'error' })
|
||||
})
|
||||
.finally(() => {
|
||||
this.previewLoading = false
|
||||
})
|
||||
},
|
||||
getTalkButtonType() {
|
||||
if (this.talkStatus === -2) return 'primary'
|
||||
if (this.talkStatus === -1) return 'primary'
|
||||
if (this.talkStatus === 0) return 'warning'
|
||||
if (this.talkStatus === 1) {
|
||||
if (this.talkMode && !this.playConnected) return 'warning'
|
||||
return 'danger'
|
||||
}
|
||||
},
|
||||
async talkButtonClick() {
|
||||
if (this.talkStatus === -1) {
|
||||
await this.startTalk()
|
||||
} else if (this.talkStatus === 1) {
|
||||
this.stopTalk()
|
||||
}
|
||||
},
|
||||
async startTalk() {
|
||||
try {
|
||||
await this.checkMicrophoneAvailability()
|
||||
} catch (e) {
|
||||
this.$message({ showClose: true, message: this.getMicrophoneErrorMessage(e), type: 'error' })
|
||||
return
|
||||
}
|
||||
|
||||
this.talkStatus = 0
|
||||
|
||||
try {
|
||||
const storeName = 'commonChanel'
|
||||
const actionName = this.talkMode ? 'talkStart' : 'broadcastStart'
|
||||
const data = await this.$store.dispatch(storeName + '/' + actionName, this.channelId)
|
||||
|
||||
const pushStream = data?.pushStream
|
||||
const playStream = data?.playStream
|
||||
|
||||
if (this.talkMode && playStream) {
|
||||
this.talkAudioPlayStream = playStream
|
||||
this.startTalkAudioPlay(playStream)
|
||||
this.muteVideoPlayer()
|
||||
}
|
||||
|
||||
this.startWebrtcPush(pushStream)
|
||||
} catch (e) {
|
||||
this.$message({ showClose: true, message: e, type: 'error' })
|
||||
this.talkStatus = -1
|
||||
}
|
||||
},
|
||||
muteVideoPlayer() {
|
||||
const player = this.$refs.playerTabs
|
||||
if (!player) return
|
||||
if (player.mute) {
|
||||
player.mute()
|
||||
}
|
||||
},
|
||||
unmuteVideoPlayer() {
|
||||
const player = this.$refs.playerTabs
|
||||
if (!player) return
|
||||
if (player.cancelMute) {
|
||||
player.cancelMute()
|
||||
}
|
||||
},
|
||||
getMicrophoneErrorMessage(error) {
|
||||
if (!error || !error.name) return '本地麦克风检测失败,请检查浏览器音频采集权限'
|
||||
if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError' || error.name === 'SecurityError') {
|
||||
return '未授予浏览器麦克风权限,无法发起语音对讲'
|
||||
}
|
||||
if (error.name === 'NotFoundError' || error.name === 'DevicesNotFoundError') {
|
||||
return '未检测到可用麦克风,无法发起语音对讲'
|
||||
}
|
||||
if (error.name === 'NotReadableError' || error.name === 'TrackStartError' || error.name === 'AbortError') {
|
||||
return '本地麦克风被占用或暂不可用,请检查后重试'
|
||||
}
|
||||
if (error.name === 'OverconstrainedError' || error.name === 'ConstraintNotSatisfiedError') {
|
||||
return '当前麦克风不满足采集条件,无法发起语音对讲'
|
||||
}
|
||||
return '本地麦克风检测失败: ' + (error.message || error.name)
|
||||
},
|
||||
async checkMicrophoneAvailability() {
|
||||
if (!window.isSecureContext && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
|
||||
throw new Error('当前页面不是安全上下文,浏览器无法采集麦克风音频')
|
||||
}
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
throw new Error('当前浏览器不支持麦克风采集')
|
||||
}
|
||||
let stream = null
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false })
|
||||
const audioTracks = stream.getAudioTracks()
|
||||
if (!audioTracks.length) throw new Error('未检测到有效的麦克风音轨')
|
||||
if (audioTracks.every(track => track.readyState === 'ended')) {
|
||||
throw new Error('麦克风已断开或不可用')
|
||||
}
|
||||
} finally {
|
||||
if (stream) stream.getTracks().forEach(t => t.stop())
|
||||
}
|
||||
},
|
||||
startWebrtcPush(pushStream) {
|
||||
if (!pushStream) return
|
||||
let url = pushStream.rtc || pushStream.rtcs
|
||||
if (!url) {
|
||||
console.warn('[ChAudioTalk] 未找到RTC推流地址')
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.dispatch('user/getUserInfo').then(user => {
|
||||
if (user && user.pushKey) {
|
||||
url += '&sign=' + user.pushKey
|
||||
} else {
|
||||
console.warn('[ChAudioTalk] 未获取到pushKey,推流鉴权可能失败')
|
||||
}
|
||||
|
||||
if (this.broadcastRtc) {
|
||||
this.broadcastRtc.close()
|
||||
}
|
||||
|
||||
this.broadcastRtc = new ZLMRTCClient.Endpoint({
|
||||
debug: true,
|
||||
zlmsdpUrl: url,
|
||||
simulecast: false,
|
||||
useCamera: false,
|
||||
audioEnable: true,
|
||||
videoEnable: false,
|
||||
recvOnly: false
|
||||
})
|
||||
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_NOT_SUPPORT, () => {
|
||||
this.$message({ showClose: true, message: '不支持WebRTC, 无法进行语音对讲', type: 'error' })
|
||||
this.talkStatus = -1
|
||||
})
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, () => {
|
||||
this.$message({ showClose: true, message: 'ICE协商出错', type: 'error' })
|
||||
this.talkStatus = -1
|
||||
})
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, () => {
|
||||
this.$message({ showClose: true, message: 'offer/answer交换失败', type: 'error' })
|
||||
this.talkStatus = -1
|
||||
})
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, (e) => {
|
||||
if (e === 'connecting') {
|
||||
this.talkStatus = 0
|
||||
} else if (e === 'connected') {
|
||||
this.talkStatus = 1
|
||||
} else if (e === 'disconnected') {
|
||||
this.talkStatus = -1
|
||||
}
|
||||
})
|
||||
}).catch(e => {
|
||||
console.warn('[ChAudioTalk] 获取用户pushKey失败', e)
|
||||
this.talkStatus = -1
|
||||
})
|
||||
},
|
||||
startTalkAudioPlay(playStream) {
|
||||
if (this.talkAudioRtc) {
|
||||
this.talkAudioRtc.close()
|
||||
}
|
||||
if (this.talkAudioRetryTimer) {
|
||||
clearTimeout(this.talkAudioRetryTimer)
|
||||
}
|
||||
|
||||
const url = location.protocol === 'https:' ? playStream.rtcs : playStream.rtc
|
||||
if (!url) {
|
||||
console.warn('[ChAudioTalk] 无可用的设备音频播放地址')
|
||||
return
|
||||
}
|
||||
this.talkAudioRetryTimer = setTimeout(() => {
|
||||
this.pollMediaInfoAndPlay(playStream)
|
||||
}, 800)
|
||||
},
|
||||
async pollMediaInfoAndPlay(playStream) {
|
||||
try {
|
||||
const data = await this.$store.dispatch('server/getMediaInfo', {
|
||||
app: playStream.app,
|
||||
stream: playStream.stream,
|
||||
mediaServerId: playStream.mediaServerId
|
||||
})
|
||||
if (data) {
|
||||
const url = location.protocol === 'https:' ? playStream.rtcs : playStream.rtc
|
||||
this.startTalkAudioByRtc(url)
|
||||
} else {
|
||||
throw new Error('no data')
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.talkStatus === 1 || this.talkStatus === 0) {
|
||||
this.talkAudioRetryTimer = setTimeout(() => {
|
||||
this.pollMediaInfoAndPlay(playStream)
|
||||
}, 800)
|
||||
}
|
||||
}
|
||||
},
|
||||
startTalkAudioByRtc(url) {
|
||||
this.talkAudioFailed = false
|
||||
this.talkAudioRtc = new ZLMRTCClient.Endpoint({
|
||||
debug: false,
|
||||
element: document.getElementById('audioTalkVideo'),
|
||||
zlmsdpUrl: url,
|
||||
simulecast: false,
|
||||
useCamera: false,
|
||||
audioEnable: true,
|
||||
videoEnable: false,
|
||||
recvOnly: true,
|
||||
usedatachannel: false
|
||||
})
|
||||
|
||||
this.talkAudioRtc.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, (e) => {
|
||||
console.warn('[ChAudioTalk] 播放流offer失败:', e?.code, e?.msg)
|
||||
if (e && e.code == -400 && e.msg == '流不存在') {
|
||||
this.talkAudioRetryTimer = setTimeout(() => {
|
||||
this.startTalkAudioByRtc(url)
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
this.talkAudioRtc.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, () => {
|
||||
console.warn('[ChAudioTalk] 设备音频流到达')
|
||||
this.playConnected = true
|
||||
})
|
||||
|
||||
this.talkAudioRtc.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, () => {
|
||||
console.error('[ChAudioTalk] 音频播放ICE协商失败')
|
||||
})
|
||||
|
||||
this.talkAudioRtc.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, (s) => {
|
||||
console.warn('[ChAudioTalk] 音频播放连接状态:', s)
|
||||
if (s === 'connected') {
|
||||
this.playConnected = true
|
||||
} else if (s === 'disconnected' || s === 'failed' || s === 'closed') {
|
||||
this.playConnected = false
|
||||
this.talkAudioFailed = true
|
||||
if (this.talkStatus === 1) {
|
||||
this.talkAudioRetryTimer = setTimeout(() => {
|
||||
this.startTalkAudioByRtc(url)
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async stopTalk() {
|
||||
this.talkStatus = -2
|
||||
|
||||
if (this.broadcastRtc) {
|
||||
this.broadcastRtc.close()
|
||||
this.broadcastRtc = null
|
||||
}
|
||||
|
||||
if (this.talkAudioRtc) {
|
||||
this.talkAudioRtc.close()
|
||||
this.talkAudioRtc = null
|
||||
}
|
||||
if (this.talkAudioRetryTimer) {
|
||||
clearTimeout(this.talkAudioRetryTimer)
|
||||
this.talkAudioRetryTimer = null
|
||||
}
|
||||
|
||||
this.talkAudioFailed = false
|
||||
this.talkAudioPlayStream = null
|
||||
this.playConnected = false
|
||||
this.unmuteVideoPlayer()
|
||||
|
||||
const storeName = 'commonChanel'
|
||||
const actionName = this.talkMode ? 'talkStop' : 'broadcastStop'
|
||||
try {
|
||||
await this.$store.dispatch(storeName + '/' + actionName, this.channelId)
|
||||
} catch (e) {
|
||||
console.warn('停止对讲失败', e)
|
||||
}
|
||||
|
||||
this.talkStatus = -1
|
||||
},
|
||||
retryTalkAudio() {
|
||||
if (this.talkAudioPlayStream) {
|
||||
this.startTalkAudioPlay(this.talkAudioPlayStream)
|
||||
}
|
||||
},
|
||||
close() {
|
||||
if (this.showPlayer && this.$refs.playerTabs) {
|
||||
this.$refs.playerTabs.stop()
|
||||
}
|
||||
this.stopTalk()
|
||||
this.streamInfo = null
|
||||
this.showPlayer = false
|
||||
this.showDialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.player-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 16 / 9;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
.broadcast-panel {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 10px;
|
||||
border-left: 1px solid #ebeef5;
|
||||
}
|
||||
.broadcast-panel > div:first-child {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.broadcast-panel > div:last-child {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
82
web/src/views/channel/common/channelPtzPanel.vue
Normal file
82
web/src/views/channel/common/channelPtzPanel.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="ptz-section">
|
||||
<ptzControls
|
||||
btn-layout="row"
|
||||
@ptz-move="onPtzMove"
|
||||
@ptz-stop="onPtzStop"
|
||||
@focus-move="onFocusMove"
|
||||
@focus-stop="onFocusStop"
|
||||
@iris-move="onIrisMove"
|
||||
@iris-stop="onIrisStop"
|
||||
@toggle-drag-zoom="$emit('drag-zoom-start', 'in')"
|
||||
@toggle-drag-zoom-out="$emit('drag-zoom-start', 'out')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ptzControls from '../../common/ptzControls.vue'
|
||||
|
||||
export default {
|
||||
name: 'ChannelPtzPanel',
|
||||
components: { ptzControls },
|
||||
props: {
|
||||
channelId: { type: String, default: null }
|
||||
},
|
||||
methods: {
|
||||
onPtzMove(e) {
|
||||
this.$store.dispatch('commonChanel/ptz', {
|
||||
channelId: this.channelId,
|
||||
command: e.direction,
|
||||
panSpeed: e.speed,
|
||||
tiltSpeed: e.speed,
|
||||
zoomSpeed: e.speed
|
||||
})
|
||||
},
|
||||
onPtzStop() {
|
||||
this.$store.dispatch('commonChanel/ptz', {
|
||||
channelId: this.channelId,
|
||||
command: 'stop',
|
||||
panSpeed: 0,
|
||||
tiltSpeed: 0,
|
||||
zoomSpeed: 0
|
||||
})
|
||||
},
|
||||
onFocusMove(e) {
|
||||
this.$store.dispatch('commonChanel/focus', {
|
||||
channelId: this.channelId,
|
||||
command: e.command,
|
||||
speed: e.speed
|
||||
})
|
||||
},
|
||||
onFocusStop() {
|
||||
this.$store.dispatch('commonChanel/focus', {
|
||||
channelId: this.channelId,
|
||||
command: 'stop',
|
||||
speed: 0
|
||||
})
|
||||
},
|
||||
onIrisMove(e) {
|
||||
this.$store.dispatch('commonChanel/iris', {
|
||||
channelId: this.channelId,
|
||||
command: e.command,
|
||||
speed: e.speed
|
||||
})
|
||||
},
|
||||
onIrisStop() {
|
||||
this.$store.dispatch('commonChanel/iris', {
|
||||
channelId: this.channelId,
|
||||
command: 'stop',
|
||||
speed: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ptz-section {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
91
web/src/views/channel/common/playerPtzPanel.vue
Normal file
91
web/src/views/channel/common/playerPtzPanel.vue
Normal file
@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="player-ptz-panel">
|
||||
<div class="player-section">
|
||||
<div class="player-wrapper" :style="{ height: playerHeight }">
|
||||
<playerTabs ref="playerTabs" :has-audio="hasAudio" :show-button="true" />
|
||||
</div>
|
||||
</div>
|
||||
<channelPtzPanel
|
||||
style="margin-top: 5vh"
|
||||
:channel-id="channelId"
|
||||
@drag-zoom-start="toggleDragZoom"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import playerTabs from '../../common/playerTabs.vue'
|
||||
import channelPtzPanel from './channelPtzPanel.vue'
|
||||
|
||||
export default {
|
||||
name: 'ChPlayerPtzPanel',
|
||||
components: { playerTabs, channelPtzPanel },
|
||||
props: {
|
||||
channelId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasAudio: false,
|
||||
playerHeight: '40vh',
|
||||
dragZoomDirection: ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.startPlay()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopPlay()
|
||||
},
|
||||
methods: {
|
||||
startPlay() {
|
||||
this.$store.dispatch('commonChanel/playChannel', this.channelId)
|
||||
.then(data => {
|
||||
this.hasAudio = data.hasAudio
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.playerTabs) {
|
||||
this.$refs.playerTabs.setStreamInfo(data.transcodeStream || data)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
this.$message({ showClose: true, message: e || '播放失败', type: 'error' })
|
||||
})
|
||||
},
|
||||
stopPlay() {
|
||||
this.$store.dispatch('commonChanel/stopPlayChannel', this.channelId)
|
||||
.catch(() => {})
|
||||
},
|
||||
toggleDragZoom(direction) {
|
||||
this.dragZoomDirection = direction
|
||||
this.$refs.playerTabs.startDragZoom((params) => {
|
||||
params.deviceId = this.channelId
|
||||
params.channelId = this.channelId
|
||||
const action = this.dragZoomDirection === 'in' ? 'commonChanel/dragZoomIn' : 'commonChanel/dragZoomOut'
|
||||
const successMsg = this.dragZoomDirection === 'in' ? '拉框放大成功' : '拉框缩小成功'
|
||||
const failMsg = this.dragZoomDirection === 'in' ? '拉框放大失败' : '拉框缩小失败'
|
||||
this.$store.dispatch(action, params).then(() => {
|
||||
this.$message({ showClose: true, message: successMsg, type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: failMsg, type: 'error' })
|
||||
})
|
||||
this.dragZoomDirection = ''
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.player-ptz-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
.player-section {
|
||||
flex: 0.8;
|
||||
}
|
||||
.player-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
269
web/src/views/channel/common/ptzCruiseConfig.vue
Normal file
269
web/src/views/channel/common/ptzCruiseConfig.vue
Normal file
@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div id="ptzCruiseConfig" style="height: 100%; display: flex; flex-direction: column;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px;">
|
||||
<div>
|
||||
<el-button type="primary" :disabled="formVisible" @click="openAdd">添加巡航组</el-button>
|
||||
<el-button :loading="clearing" :disabled="clearing" @click="clearCruiseTours">清空</el-button>
|
||||
</div>
|
||||
<el-button icon="el-icon-refresh-right" circle @click="loadPresets" />
|
||||
</div>
|
||||
<div v-if="formVisible" style="margin-bottom: 6px; padding: 16px 8px; border: 1px solid #e6e6e6; border-radius: 4px;">
|
||||
<el-form inline size="small" style="display: flex; align-items: center; margin-top: 15px;">
|
||||
<el-form-item label="序号" style="margin-bottom: 0;">
|
||||
<el-input-number v-model="formId" :min="0" :max="255" controls-position="right" style="width: 120px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="名称" style="margin-bottom: 0;">
|
||||
<el-input v-model="formName" placeholder="名称" style="width: 140px" />
|
||||
</el-form-item>
|
||||
<el-form-item style="margin-bottom: 0;">
|
||||
<el-button type="primary" :loading="submitting" :disabled="submitting" @click="confirmSave">确定</el-button>
|
||||
<el-button @click="cancelForm">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-divider style="margin: 6px 0;" />
|
||||
<div style="margin-bottom: 4px;">
|
||||
<el-button size="mini" type="primary" @click="addPresetRow">添加预置点</el-button>
|
||||
</div>
|
||||
<el-table :data="formPresets" size="mini" stripe border max-height="200px">
|
||||
<el-table-column label="序号" width="50">
|
||||
<template v-slot="{ $index }">{{ $index + 1 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预置点" min-width="100">
|
||||
<template v-slot="{ row }">
|
||||
<el-select v-model="row.presetId" size="mini" style="width: 120px" placeholder="选择预置点">
|
||||
<el-option v-for="p in allPresetList" :key="p.presetId"
|
||||
:label="p.presetName || ('预置点' + p.presetId)"
|
||||
:value="p.presetId" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="停留时间(秒)" min-width="100">
|
||||
<template v-slot="{ row }">
|
||||
<el-input-number v-model="row.dwellTime" :min="15" :max="300" size="mini" controls-position="right" style="width: 90px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="速度" min-width="100">
|
||||
<template v-slot="{ row }">
|
||||
<el-select v-model="row.speed" size="mini" style="width: 90px">
|
||||
<el-option v-for="s in 10" :key="s" :label="s" :value="s" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="60">
|
||||
<template v-slot="{ $index }">
|
||||
<el-button size="mini" type="text" style="color: #F56C6C" @click="removePresetRow($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-if="cruiseTours.length > 0" style="flex: 1; overflow: auto;">
|
||||
<el-table ref="cruiseTable" :data="cruiseTours" size="mini" max-height="100%" stripe border highlight-current-row>
|
||||
<el-table-column prop="id" label="ID" />
|
||||
<el-table-column prop="name" label="巡航名称" />
|
||||
<el-table-column label="操作" min-width="150">
|
||||
<template v-slot:default="scope">
|
||||
<el-button v-if="cruisingCruiseId === scope.row.id" size="mini" type="text" style="color: #F56C6C" :loading="operatingId === scope.row.id" :disabled="operatingId !== null" @click="stopCruise(scope.row)">停用</el-button>
|
||||
<el-button v-else size="mini" type="text" :disabled="cruisingCruiseId !== null || operatingId !== null" style="color: #409EFF" :loading="operatingId === scope.row.id" @click="startCruise(scope.row)">启用</el-button>
|
||||
<el-button size="mini" type="text" style="color: #409EFF" :disabled="operatingId !== null" @click="openEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="mini" type="text" style="color: #F56C6C" :loading="deletingId === scope.row.id" :disabled="operatingId !== null || deletingId !== null" @click="deleteCruise(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-else style="color: #909399; font-size: 12px; margin-bottom: 8px;">暂无巡航路线</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ChPtzCruiseConfig',
|
||||
props: {
|
||||
channelId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
cruiseTours: [],
|
||||
cruisingCruiseId: null,
|
||||
formVisible: false,
|
||||
editingTourId: null,
|
||||
submitting: false,
|
||||
clearing: false,
|
||||
operatingId: null,
|
||||
deletingId: null,
|
||||
formId: 1,
|
||||
formName: '',
|
||||
formPresets: [],
|
||||
allPresetList: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadPresets()
|
||||
},
|
||||
methods: {
|
||||
loadPresets() {
|
||||
this.$store.dispatch('commonChanel/queryPreset', this.channelId)
|
||||
.then(data => {
|
||||
this.allPresetList = data || []
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('[巡航] 加载预置点列表失败', error)
|
||||
})
|
||||
},
|
||||
getNextAvailableId() {
|
||||
const used = new Set((this.cruiseTours || []).map(t => t.id))
|
||||
for (let i = 0; i <= 255; i++) {
|
||||
if (!used.has(i)) return i
|
||||
}
|
||||
return 0
|
||||
},
|
||||
openAdd() {
|
||||
this.editingTourId = null
|
||||
this.formId = this.getNextAvailableId()
|
||||
this.formName = '巡航组' + this.formId
|
||||
this.formPresets = []
|
||||
this.formVisible = true
|
||||
},
|
||||
openEdit(tour) {
|
||||
this.editingTourId = tour.id
|
||||
this.formId = tour.id
|
||||
this.formName = tour.name
|
||||
this.formPresets = (tour.presets || []).map(p => ({
|
||||
presetId: p.presetId,
|
||||
dwellTime: p.dwellTime,
|
||||
speed: p.speed
|
||||
}))
|
||||
if (this.formPresets.length === 0) {
|
||||
this.formPresets.push({ presetId: this.getFirstPresetId(), dwellTime: 15, speed: 7 })
|
||||
}
|
||||
this.formVisible = true
|
||||
},
|
||||
cancelForm() {
|
||||
this.formVisible = false
|
||||
this.editingTourId = null
|
||||
this.formPresets = []
|
||||
},
|
||||
getFirstPresetId() {
|
||||
const first = this.allPresetList[0]
|
||||
return first ? first.presetId : 1
|
||||
},
|
||||
addPresetRow() {
|
||||
this.formPresets.push({
|
||||
presetId: this.getFirstPresetId(),
|
||||
dwellTime: 15,
|
||||
speed: 7
|
||||
})
|
||||
},
|
||||
removePresetRow(index) {
|
||||
this.formPresets.splice(index, 1)
|
||||
},
|
||||
confirmSave() {
|
||||
if (!this.formName.trim()) {
|
||||
this.$message({ showClose: true, message: '请输入巡航组名称', type: 'warning' })
|
||||
return
|
||||
}
|
||||
if (this.formId === null || this.formId < 0 || this.formId > 255) {
|
||||
this.$message({ showClose: true, message: '巡航序号必须在0-255之间', type: 'warning' })
|
||||
return
|
||||
}
|
||||
this.submitting = true
|
||||
const cid = this.channelId
|
||||
const fid = this.formId
|
||||
let chain = Promise.resolve()
|
||||
if (this.editingTourId !== null) {
|
||||
chain = chain.then(() => this.$store.dispatch('commonChanel/deletePointForCruise', { channelId: cid, tourId: fid, presetId: 0 }))
|
||||
}
|
||||
this.formPresets.forEach(p => {
|
||||
chain = chain.then(() => this.$store.dispatch('commonChanel/addPointForCruise', { channelId: cid, tourId: fid, presetId: p.presetId }))
|
||||
})
|
||||
const speed = this.formPresets.length > 0 ? this.formPresets[0].speed : 7
|
||||
const dwellTime = this.formPresets.length > 0 ? this.formPresets[0].dwellTime : 15
|
||||
chain = chain.then(() => this.$store.dispatch('commonChanel/setCruiseSpeed', { channelId: cid, tourId: fid, presetId: 0, speed }))
|
||||
chain = chain.then(() => this.$store.dispatch('commonChanel/setCruiseTime', { channelId: cid, tourId: fid, presetId: 0, time: dwellTime }))
|
||||
chain.then(() => {
|
||||
const idx = this.cruiseTours.findIndex(t => t.id === this.formId)
|
||||
const presets = this.formPresets.map(p => ({
|
||||
presetId: p.presetId,
|
||||
dwellTime: p.dwellTime,
|
||||
speed: p.speed
|
||||
}))
|
||||
const tour = { id: this.formId, name: this.formName, presets }
|
||||
if (idx !== -1) {
|
||||
this.$set(this.cruiseTours, idx, tour)
|
||||
} else {
|
||||
this.cruiseTours.push(tour)
|
||||
}
|
||||
this.cancelForm()
|
||||
this.$message({ showClose: true, message: '保存成功', type: 'success' })
|
||||
}).catch(error => {
|
||||
this.$message({ showClose: true, message: error || '保存失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.submitting = false
|
||||
})
|
||||
},
|
||||
clearCruiseTours() {
|
||||
if (this.cruiseTours.length === 0) return
|
||||
this.$confirm('确定清空所有巡航组?', '提示', {
|
||||
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
|
||||
}).then(() => {
|
||||
this.clearing = true
|
||||
const cid = this.channelId
|
||||
let chain = Promise.resolve()
|
||||
this.cruiseTours.forEach(tour => {
|
||||
chain = chain.then(() => this.$store.dispatch('commonChanel/deletePointForCruise', { channelId: cid, tourId: tour.id, presetId: 0 }))
|
||||
})
|
||||
chain.then(() => {
|
||||
this.cruiseTours = []
|
||||
this.cruisingCruiseId = null
|
||||
this.$message({ showClose: true, message: '清空成功', type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '清空失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.clearing = false
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
startCruise(row) {
|
||||
this.operatingId = row.id
|
||||
this.$store.dispatch('commonChanel/startCruise', { channelId: this.channelId, tourId: row.id })
|
||||
.then(() => {
|
||||
this.cruisingCruiseId = row.id
|
||||
this.$message({ showClose: true, message: '启用成功', type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '启用失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.operatingId = null
|
||||
})
|
||||
},
|
||||
stopCruise(row) {
|
||||
this.operatingId = row.id
|
||||
this.$store.dispatch('commonChanel/stopCruise', { channelId: this.channelId, tourId: row.id })
|
||||
.then(() => {
|
||||
this.cruisingCruiseId = null
|
||||
this.$message({ showClose: true, message: '停止成功', type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '停止失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.operatingId = null
|
||||
})
|
||||
},
|
||||
deleteCruise(row) {
|
||||
this.$confirm('确定删除此巡航组?', '提示', {
|
||||
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
|
||||
}).then(() => {
|
||||
this.deletingId = row.id
|
||||
this.$store.dispatch('commonChanel/deletePointForCruise', { channelId: this.channelId, tourId: row.id, presetId: 0 })
|
||||
.then(() => {
|
||||
const idx = this.cruiseTours.indexOf(row)
|
||||
if (idx !== -1) this.cruiseTours.splice(idx, 1)
|
||||
if (this.cruisingCruiseId === row.id) this.cruisingCruiseId = null
|
||||
this.$message({ showClose: true, message: '删除成功', type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '删除失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.deletingId = null
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
54
web/src/views/channel/common/ptzPreset.vue
Normal file
54
web/src/views/channel/common/ptzPreset.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div id="ptzPreset" style="width: 100%">
|
||||
<el-tag
|
||||
v-for="item in presetList"
|
||||
:key="item.presetId"
|
||||
size="mini"
|
||||
style="margin-right: 1rem; cursor: pointer; margin-bottom: 0.6rem"
|
||||
@click="gotoPreset(item)"
|
||||
>
|
||||
{{ item.presetName || item.presetId }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ChPtzPreset',
|
||||
props: {
|
||||
channelId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
presetList: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getPresetList()
|
||||
},
|
||||
methods: {
|
||||
getPresetList() {
|
||||
this.$store.dispatch('commonChanel/queryPreset', this.channelId)
|
||||
.then(data => {
|
||||
this.presetList = data
|
||||
})
|
||||
},
|
||||
gotoPreset(preset) {
|
||||
this.$store.dispatch('commonChanel/callPreset', { channelId: this.channelId, presetId: preset.presetId })
|
||||
.then(() => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '调用成功',
|
||||
type: 'success'
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
155
web/src/views/channel/common/ptzPresetConfig.vue
Normal file
155
web/src/views/channel/common/ptzPresetConfig.vue
Normal file
@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div style="height: 100%; display: flex; flex-direction: column;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px;">
|
||||
<div>
|
||||
<el-button type="primary" :disabled="showAddForm" @click="openAdd">添加预置点</el-button>
|
||||
<el-button :loading="clearing" :disabled="clearing" @click="clearAll">清空</el-button>
|
||||
</div>
|
||||
<el-button icon="el-icon-refresh-right" circle @click="getPresetList" />
|
||||
</div>
|
||||
<el-form v-if="showAddForm" size="small" inline style="margin-bottom: 6px; padding: 16px 8px; border: 1px solid #e6e6e6; border-radius: 4px; display: flex; align-items: center;">
|
||||
<el-form-item label="序号" style="margin-bottom: 0; margin-right: 2rem">
|
||||
<el-input-number v-model="addPresetId" :min="1" :max="255" controls-position="right" style="width: 180px" />
|
||||
</el-form-item>
|
||||
<el-form-item style="margin-bottom: 0;">
|
||||
<el-button type="primary" :loading="submitting" :disabled="submitting" @click="confirmAdd">确定</el-button>
|
||||
<el-button @click="cancelAdd">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table
|
||||
:data="presetList"
|
||||
border
|
||||
stripe
|
||||
max-height="100%"
|
||||
style="flex: 1"
|
||||
>
|
||||
<el-table-column prop="presetId" label="序号" align="center" />
|
||||
<el-table-column label="名称">
|
||||
<template v-slot="{ row }">
|
||||
<span>{{ row.presetName || ('预置点' + row.presetId) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="140" align="center">
|
||||
<template v-slot="{ row }">
|
||||
<el-button size="mini" type="text" @click="callPreset(row)">调用</el-button>
|
||||
<el-button size="mini" type="text" style="color: #F56C6C" :loading="deletingId === row.presetId" :disabled="deletingId !== null" @click="delPreset(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ChPtzPresetConfig',
|
||||
props: {
|
||||
channelId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
presetList: [],
|
||||
showAddForm: false,
|
||||
addPresetId: 1,
|
||||
submitting: false,
|
||||
clearing: false,
|
||||
deletingId: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getPresetList()
|
||||
},
|
||||
methods: {
|
||||
getPresetList() {
|
||||
this.$store.dispatch('commonChanel/queryPreset', this.channelId)
|
||||
.then(data => {
|
||||
this.presetList = data || []
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
})
|
||||
},
|
||||
openAdd() {
|
||||
this.addPresetId = this.getNextAvailableId()
|
||||
this.showAddForm = true
|
||||
},
|
||||
cancelAdd() {
|
||||
this.showAddForm = false
|
||||
this.addPresetId = 1
|
||||
},
|
||||
confirmAdd() {
|
||||
const exists = this.presetList.some(p => p.presetId === this.addPresetId)
|
||||
if (exists) {
|
||||
this.$message({ showClose: true, message: '序号 ' + this.addPresetId + ' 已存在', type: 'warning' })
|
||||
return
|
||||
}
|
||||
this.submitting = true
|
||||
this.$store.dispatch('commonChanel/addPreset', { channelId: this.channelId, presetId: this.addPresetId, presetName: '' })
|
||||
.then(() => {
|
||||
this.showAddForm = false
|
||||
setTimeout(() => {
|
||||
this.getPresetList()
|
||||
}, 600)
|
||||
}).catch(error => {
|
||||
this.$message({ showClose: true, message: error, type: 'error' })
|
||||
}).finally(() => {
|
||||
this.submitting = false
|
||||
})
|
||||
},
|
||||
callPreset(preset) {
|
||||
this.$store.dispatch('commonChanel/callPreset', { channelId: this.channelId, presetId: preset.presetId })
|
||||
.then(() => {
|
||||
this.$message({ showClose: true, message: '调用成功', type: 'success' })
|
||||
}).catch(error => {
|
||||
this.$message({ showClose: true, message: error, type: 'error' })
|
||||
})
|
||||
},
|
||||
delPreset(preset) {
|
||||
this.$confirm('确定删除此预置位', '提示', {
|
||||
dangerouslyUseHTMLString: true,
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.deletingId = preset.presetId
|
||||
this.$store.dispatch('commonChanel/deletePreset', { channelId: this.channelId, presetId: preset.presetId })
|
||||
.then(() => {
|
||||
this.getPresetList()
|
||||
}).catch(error => {
|
||||
this.$message({ showClose: true, message: error, type: 'error' })
|
||||
}).finally(() => {
|
||||
this.deletingId = null
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
clearAll() {
|
||||
if (this.presetList.length === 0) return
|
||||
this.$confirm('确定清空所有预置点?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.clearing = true
|
||||
const promises = this.presetList.map(p =>
|
||||
this.$store.dispatch('commonChanel/deletePreset', { channelId: this.channelId, presetId: p.presetId })
|
||||
)
|
||||
Promise.all(promises).then(() => {
|
||||
this.presetList = []
|
||||
this.$message({ showClose: true, message: '清空成功', type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '清空失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.clearing = false
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
getNextAvailableId() {
|
||||
if (!this.presetList || this.presetList.length === 0) return 1
|
||||
const used = this.presetList.map(p => Number(p.presetId)).sort((a, b) => a - b)
|
||||
for (let i = 0; i < used.length - 1; i++) {
|
||||
if (used[i + 1] - used[i] > 1) return used[i] + 1
|
||||
}
|
||||
return used[used.length - 1] + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
177
web/src/views/channel/common/ptzScanConfig.vue
Normal file
177
web/src/views/channel/common/ptzScanConfig.vue
Normal file
@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<div id="ptzScanConfig" style="height: 100%; display: flex; flex-direction: column;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px;">
|
||||
<div>
|
||||
<el-button type="primary" :loading="adding" :disabled="adding" @click="addLineScan">添加线扫</el-button>
|
||||
<el-button @click="clearAll">清空</el-button>
|
||||
</div>
|
||||
<el-button icon="el-icon-refresh-right" circle />
|
||||
</div>
|
||||
<div v-if="scanAreas.length > 0" style="flex: 1; overflow: auto;">
|
||||
<el-table :data="scanAreas" max-height="100%" stripe border highlight-current-row height="100%">
|
||||
<el-table-column label="序号" min-width="50">
|
||||
<template v-slot="{ row }">{{ row.index }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="名称" min-width="80">
|
||||
<template v-slot="{ row }">{{ row.name }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="左边界" min-width="90">
|
||||
<template v-slot="{ row }">
|
||||
<el-button type="text"
|
||||
:style="{ color: row.leftBoundary ? '#67C23A' : '#409EFF' }"
|
||||
:loading="boundaryLoading.index === row.index && boundaryLoading.side === 'Left'"
|
||||
:disabled="operatingId !== null"
|
||||
@click="setBoundary(row, 'Left')">
|
||||
{{ row.leftBoundary ? '重新保存' : '待保存' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="右边界" min-width="90">
|
||||
<template v-slot="{ row }">
|
||||
<el-button type="text"
|
||||
:style="{ color: row.rightBoundary ? '#67C23A' : '#409EFF' }"
|
||||
:loading="boundaryLoading.index === row.index && boundaryLoading.side === 'Right'"
|
||||
:disabled="operatingId !== null"
|
||||
@click="setBoundary(row, 'Right')">
|
||||
{{ row.rightBoundary ? '重新保存' : '待保存' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="速度" min-width="90">
|
||||
<template v-slot="{ row }">
|
||||
<el-select v-model="row.speed" :disabled="speedSaving === row.index" @change="onSpeedChange(row)">
|
||||
<el-option v-for="s in 8" :key="s" :label="s" :value="s" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="120">
|
||||
<template v-slot="{ row, $index }">
|
||||
<el-button v-if="$index === cruisingScanIndex" type="text" style="color: #F56C6C" :loading="operatingId === row.index" :disabled="operatingId !== null" @click="stopScan(row)">停用</el-button>
|
||||
<el-button v-else type="text" style="color: #409EFF" :disabled="operatingId !== null" :loading="operatingId === row.index" @click="startScan(row, $index)">启用</el-button>
|
||||
<el-button type="text" style="color: #F56C6C" :disabled="operatingId !== null" @click="deleteScan(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-else style="color: #909399; font-size: 12px; margin-bottom: 8px;">暂无线扫区域</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ChPtzScanConfig',
|
||||
props: {
|
||||
channelId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scanAreas: [],
|
||||
cruisingScanIndex: null,
|
||||
operatingId: null,
|
||||
adding: false,
|
||||
boundaryLoading: { index: null, side: null },
|
||||
speedSaving: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getNextAvailableIndex() {
|
||||
const used = new Set(this.scanAreas.filter(a => a.name && a.name.trim()).map(a => a.index))
|
||||
for (let i = 0; i <= 255; i++) {
|
||||
if (!used.has(i)) return i
|
||||
}
|
||||
return 0
|
||||
},
|
||||
addLineScan() {
|
||||
const nextIndex = this.getNextAvailableIndex()
|
||||
const name = '线扫' + nextIndex
|
||||
this.adding = true
|
||||
this.scanAreas.push({
|
||||
index: nextIndex,
|
||||
name: name,
|
||||
leftBoundary: false,
|
||||
rightBoundary: false,
|
||||
speed: 5
|
||||
})
|
||||
this.$nextTick(() => { this.adding = false })
|
||||
},
|
||||
setBoundary(row, boundary) {
|
||||
this.boundaryLoading = { index: row.index, side: boundary }
|
||||
const action = boundary === 'Left' ? 'setLeftForScan' : 'setRightForScan'
|
||||
this.$store.dispatch('commonChanel/' + action, { channelId: this.channelId, scanId: row.index })
|
||||
.then(() => {
|
||||
this.$message({ showClose: true, message: (boundary === 'Left' ? '左' : '右') + '边界设置成功', type: 'success' })
|
||||
if (boundary === 'Left') {
|
||||
row.leftBoundary = true
|
||||
} else {
|
||||
row.rightBoundary = true
|
||||
}
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '边界设置失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.boundaryLoading = { index: null, side: null }
|
||||
})
|
||||
},
|
||||
onSpeedChange(row) {
|
||||
this.speedSaving = row.index
|
||||
this.$store.dispatch('commonChanel/setSpeedForScan', { channelId: this.channelId, scanId: row.index, speed: row.speed })
|
||||
.then(() => {
|
||||
this.$message({ showClose: true, message: '速度已保存', type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '速度保存失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.speedSaving = null
|
||||
})
|
||||
},
|
||||
startScan(row, index) {
|
||||
this.operatingId = row.index
|
||||
this.$store.dispatch('commonChanel/startScan', { channelId: this.channelId, scanId: row.index })
|
||||
.then(() => {
|
||||
this.$message({ showClose: true, message: '启用成功', type: 'success' })
|
||||
this.cruisingScanIndex = index
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '启用失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.operatingId = null
|
||||
})
|
||||
},
|
||||
stopScan(row) {
|
||||
this.operatingId = row.index
|
||||
this.$store.dispatch('commonChanel/stopScan', { channelId: this.channelId, scanId: row.index })
|
||||
.then(() => {
|
||||
this.$message({ showClose: true, message: '停用成功', type: 'success' })
|
||||
this.cruisingScanIndex = null
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '停用失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.operatingId = null
|
||||
})
|
||||
},
|
||||
deleteScan(row) {
|
||||
this.$confirm('确定删除线扫 ' + row.index + '?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const idx = this.scanAreas.indexOf(row)
|
||||
if (idx !== -1) this.scanAreas.splice(idx, 1)
|
||||
if (this.cruisingScanIndex !== null && this.scanAreas[this.cruisingScanIndex] === undefined) {
|
||||
this.cruisingScanIndex = null
|
||||
}
|
||||
this.$message({ showClose: true, message: '删除成功(仅本地列表,设备端配置需手动清除)', type: 'success' })
|
||||
}).catch(() => {})
|
||||
},
|
||||
clearAll() {
|
||||
if (this.scanAreas.length === 0) return
|
||||
this.$confirm('确定清空所有线扫区域?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.scanAreas = []
|
||||
this.cruisingScanIndex = null
|
||||
this.$message({ showClose: true, message: '清空成功(仅本地列表,设备端配置需手动清除)', type: 'success' })
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
59
web/src/views/channel/common/ptzSwitchConfig.vue
Normal file
59
web/src/views/channel/common/ptzSwitchConfig.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline label-width="120px" size="small">
|
||||
<el-form-item label="开关编号" style="margin-bottom: 0;">
|
||||
<el-input-number v-model="switchId" :min="1" :max="255" controls-position="right" style="width: 140px" />
|
||||
</el-form-item>
|
||||
<el-form-item style="margin-bottom: 0;">
|
||||
<el-button type="primary" :loading="loading" :disabled="loading" @click="control('on')">开启</el-button>
|
||||
<el-button :loading="loading" :disabled="loading" @click="control('off')">关闭</el-button>
|
||||
</el-form-item>
|
||||
<el-divider />
|
||||
|
||||
<el-form-item style="margin-bottom: 0;" label="雨刷">
|
||||
<el-button type="primary" :loading="wiperLoading" :disabled="wiperLoading" @click="wiperControl('on')">开启</el-button>
|
||||
<el-button :loading="wiperLoading" :disabled="wiperLoading" @click="wiperControl('off')">关闭</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ChPtzSwitchConfig',
|
||||
props: {
|
||||
channelId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
switchId: 1,
|
||||
loading: false,
|
||||
wiperLoading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
wiperControl(command) {
|
||||
this.wiperLoading = true
|
||||
this.$store.dispatch('commonChanel/wiper', { channelId: this.channelId, command })
|
||||
.then(() => {
|
||||
this.$message({ showClose: true, message: command === 'on' ? '雨刷已开启' : '雨刷已关闭', type: 'success' })
|
||||
}).catch(error => {
|
||||
this.$message({ showClose: true, message: error, type: 'error' })
|
||||
}).finally(() => {
|
||||
this.wiperLoading = false
|
||||
})
|
||||
},
|
||||
control(command) {
|
||||
this.loading = true
|
||||
this.$store.dispatch('commonChanel/auxiliary', { channelId: this.channelId, command, auxiliaryId: this.switchId })
|
||||
.then(() => {
|
||||
this.$message({ showClose: true, message: command === 'on' ? '开关已开启' : '开关已关闭', type: 'success' })
|
||||
}).catch(error => {
|
||||
this.$message({ showClose: true, message: error, type: 'error' })
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -90,6 +90,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="位置信息" min-width="150">
|
||||
<template v-slot:default="scope">
|
||||
<span v-if="scope.row.gbLongitude && scope.row.gbLatitude">{{ scope.row.gbLongitude }}<br>{{ scope.row.gbLatitude }}</span>
|
||||
<span v-if="!scope.row.gbLongitude || !scope.row.gbLatitude">无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template v-slot:default="scope">
|
||||
<div slot="reference" class="name-wrapper">
|
||||
@ -91,6 +91,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="位置信息" min-width="150">
|
||||
<template v-slot:default="scope">
|
||||
<span v-if="scope.row.gbLongitude && scope.row.gbLatitude">{{ scope.row.gbLongitude }}<br>{{ scope.row.gbLatitude }}</span>
|
||||
<span v-if="!scope.row.gbLongitude || !scope.row.gbLatitude">无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template v-slot:default="scope">
|
||||
<div slot="reference" class="name-wrapper">
|
||||
@ -120,7 +126,7 @@
|
||||
<script>
|
||||
import GroupTree from '../../common/GroupTree.vue'
|
||||
import GbChannelSelect from '../../dialog/GbChannelSelect.vue'
|
||||
import UnusualGroupChannelSelect from '../../dialog/UnusualGroupChannelSelect.vue'
|
||||
import UnusualGroupChannelSelect from './UnusualGroupChannelSelect.vue'
|
||||
|
||||
export default {
|
||||
name: 'Group',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="channelList" class="app-container" style="height: calc(100vh - 124px);">
|
||||
<div v-if="!editId" style="height: 100%">
|
||||
<div v-if="!editId && !showPtzConfig" style="height: 100%">
|
||||
<el-form :inline="true" size="mini">
|
||||
<el-form-item label="搜索">
|
||||
<el-input
|
||||
@ -149,6 +149,10 @@
|
||||
设备录像</el-dropdown-item>
|
||||
<el-dropdown-item command="cloudRecords" :disabled="scope.row.gbStatus !== 'ON'">
|
||||
云端录像</el-dropdown-item>
|
||||
<el-dropdown-item command="ptzConfig" :disabled="scope.row.gbStatus !== 'ON'">
|
||||
云台配置</el-dropdown-item>
|
||||
<el-dropdown-item command="audioTalk" :disabled="scope.row.gbStatus !== 'ON'">
|
||||
语音对讲</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
|
||||
</el-dropdown>
|
||||
@ -168,6 +172,8 @@
|
||||
</div>
|
||||
|
||||
<devicePlayer ref="devicePlayer" />
|
||||
<audioTalk ref="audioTalk" />
|
||||
<ptzConfig v-if="showPtzConfig" :channel-id="ptzConfigChannelId" @close="showPtzConfig = false" />
|
||||
<channel-edit v-if="editId" :id="editId" :close-edit="closeEdit" />
|
||||
<chooseCivilCode ref="chooseCivilCode" />
|
||||
<chooseGroup ref="chooseGroup" />
|
||||
@ -176,18 +182,20 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import devicePlayer from '@/views/common/channelPlayer/index.vue'
|
||||
import devicePlayer from './player.vue'
|
||||
import audioTalk from './audioTalk.vue'
|
||||
import ptzConfig from './ptzConfig.vue'
|
||||
import Edit from './edit.vue'
|
||||
import ChooseCivilCode from '../dialog/chooseCivilCode.vue'
|
||||
import ChooseGroup from '@/views/dialog/chooseGroup.vue'
|
||||
import { MessageBox } from 'element-ui'
|
||||
import store from '@/store'
|
||||
|
||||
export default {
|
||||
name: 'ChannelList',
|
||||
components: {
|
||||
ChooseGroup,
|
||||
devicePlayer,
|
||||
audioTalk,
|
||||
ptzConfig,
|
||||
ChooseCivilCode,
|
||||
ChannelEdit: Edit
|
||||
},
|
||||
@ -243,6 +251,8 @@ export default {
|
||||
total: 0,
|
||||
beforeUrl: '/device',
|
||||
editId: null,
|
||||
showPtzConfig: false,
|
||||
ptzConfigChannelId: null,
|
||||
civilCodeName: null,
|
||||
civilCodeDeviceId: null,
|
||||
|
||||
@ -393,6 +403,11 @@ export default {
|
||||
this.queryRecords(itemData)
|
||||
} else if (command === 'cloudRecords') {
|
||||
this.queryCloudRecords(itemData)
|
||||
} else if (command === 'ptzConfig') {
|
||||
this.ptzConfigChannelId = itemData.gbId
|
||||
this.showPtzConfig = true
|
||||
} else if (command === 'audioTalk') {
|
||||
this.$refs.audioTalk.openDialog(itemData.gbId)
|
||||
}
|
||||
},
|
||||
getCheckIds: function() {
|
||||
|
||||
158
web/src/views/channel/player.vue
Executable file
158
web/src/views/channel/player.vue
Executable file
@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div id="devicePlayer" v-loading="isLoging">
|
||||
|
||||
<el-dialog
|
||||
v-if="showVideoDialog"
|
||||
v-el-drag-dialog
|
||||
title="视频播放"
|
||||
top="10vh"
|
||||
width="65vw"
|
||||
:close-on-click-modal="false"
|
||||
:visible.sync="showVideoDialog"
|
||||
@close="close()"
|
||||
>
|
||||
<div class="dhsdk-player-body">
|
||||
|
||||
<div class="player-side">
|
||||
<div class="player-container" :style="{ height: playerHeight }">
|
||||
<playerTabs ref="playerTabs" :has-audio="hasAudio" :show-button="true"
|
||||
@playerChanged="playerChanged" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-side">
|
||||
<channelPtzPanel
|
||||
:channel-id="channelId"
|
||||
@drag-zoom-start="toggleDragZoom"
|
||||
/>
|
||||
|
||||
<el-tabs v-model="tabActiveName" @tab-click="tabHandleClick" class="control-tabs">
|
||||
<el-tab-pane label="预置位" name="preset">
|
||||
<channelPreset
|
||||
v-if="tabActiveName === 'preset'"
|
||||
:channel-id="channelId"
|
||||
style="margin-top: 8px;"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="实时视频" name="media">
|
||||
<streamMediaPanel v-if="tabActiveName === 'media'" :player-url="playerUrlInfo.playerUrl" :play-url="playerUrlInfo.playUrl" :stream-info="streamInfo" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="编码信息" name="codec">
|
||||
<mediaInfo v-if="tabActiveName === 'codec'" ref="mediaInfo" :app="app" :stream="streamId" :media-server-id="mediaServerId" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import elDragDialog from '@/directive/el-drag-dialog'
|
||||
import playerTabs from '../common/playerTabs.vue'
|
||||
import channelPtzPanel from './common/channelPtzPanel.vue'
|
||||
import channelPreset from './common/ptzPreset.vue'
|
||||
import mediaInfo from '../common/mediaInfo.vue'
|
||||
import streamMediaPanel from '../common/streamMediaPanel.vue'
|
||||
|
||||
export default {
|
||||
name: 'ChannelPlayer',
|
||||
directives: { elDragDialog },
|
||||
components: { playerTabs, channelPtzPanel, channelPreset, mediaInfo, streamMediaPanel },
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
videoUrl: '',
|
||||
streamId: '',
|
||||
app: '',
|
||||
mediaServerId: '',
|
||||
channelId: '',
|
||||
tabActiveName: 'preset',
|
||||
hasAudio: false,
|
||||
isLoging: false,
|
||||
showVideoDialog: false,
|
||||
streamInfo: null,
|
||||
playerHeight: '48vh',
|
||||
playerUrlInfo: {
|
||||
playerUrl: null,
|
||||
playUrl: null
|
||||
},
|
||||
dragZoomDirection: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tabHandleClick(tab) {
|
||||
if (tab.name === 'codec') {
|
||||
this.$refs.mediaInfo && this.$refs.mediaInfo.startTask()
|
||||
} else {
|
||||
this.$refs.mediaInfo && this.$refs.mediaInfo.stopTask()
|
||||
}
|
||||
},
|
||||
openDialog(tab, channelId, param) {
|
||||
if (this.showVideoDialog) return
|
||||
this.tabActiveName = tab || 'preset'
|
||||
this.channelId = channelId
|
||||
this.streamId = ''
|
||||
this.mediaServerId = ''
|
||||
this.app = ''
|
||||
this.videoUrl = ''
|
||||
if (param && param.streamInfo) {
|
||||
this.play(param.streamInfo, param.hasAudio)
|
||||
}
|
||||
},
|
||||
play(streamInfo, hasAudio) {
|
||||
this.streamInfo = streamInfo
|
||||
this.hasAudio = hasAudio
|
||||
this.isLoging = false
|
||||
this.streamId = streamInfo.stream
|
||||
this.app = streamInfo.app
|
||||
this.mediaServerId = streamInfo.mediaServerId
|
||||
this.showVideoDialog = true
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.playerTabs) {
|
||||
this.$refs.playerTabs.setStreamInfo(streamInfo.transcodeStream || streamInfo)
|
||||
}
|
||||
})
|
||||
},
|
||||
playerChanged(playerUrlInfo) {
|
||||
this.playerUrlInfo = playerUrlInfo
|
||||
},
|
||||
close() {
|
||||
if (this.$refs.playerTabs) {
|
||||
this.$refs.playerTabs.stop()
|
||||
}
|
||||
this.videoUrl = ''
|
||||
this.showVideoDialog = false
|
||||
},
|
||||
toggleDragZoom(direction) {
|
||||
this.dragZoomDirection = direction
|
||||
this.$refs.playerTabs.startDragZoom((params) => {
|
||||
params.channelId = this.channelId
|
||||
const action = this.dragZoomDirection === 'in' ? 'commonChanel/dragZoomIn' : 'commonChanel/dragZoomOut'
|
||||
const successMsg = this.dragZoomDirection === 'in' ? '拉框放大成功' : '拉框缩小成功'
|
||||
const failMsg = this.dragZoomDirection === 'in' ? '拉框放大失败' : '拉框缩小失败'
|
||||
this.$store.dispatch(action, params).then(() => {
|
||||
this.$message({ showClose: true, message: successMsg, type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: failMsg, type: 'error' })
|
||||
})
|
||||
this.dragZoomDirection = ''
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#devicePlayer .el-dialog__body { padding: 10px 20px; }
|
||||
.dhsdk-player-body { display: flex; gap: 16px; }
|
||||
.player-side { flex: 3; min-width: 0; }
|
||||
.player-container { width: 100%; }
|
||||
.control-side { flex: 2; min-width: 340px; display: flex; flex-direction: column; }
|
||||
.control-tabs { flex: 1; display: flex; flex-direction: column; min-height: 220px }
|
||||
.control-tabs .el-tabs__content { flex: 1; overflow: auto; }
|
||||
.media-info-content { overflow: auto; }
|
||||
.media-row { display: flex; margin-bottom: 0.5rem; height: 2.5rem; }
|
||||
.media-label { width: 6rem; line-height: 2.5rem; text-align: right; flex-shrink: 0; }
|
||||
</style>
|
||||
105
web/src/views/channel/ptzConfig.vue
Normal file
105
web/src/views/channel/ptzConfig.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div id="dhPtzConfigPage">
|
||||
<el-page-header content="云台设置" @back="$emit('close')" />
|
||||
<div class="ptz-config-body">
|
||||
<div class="config-sidebar">
|
||||
<el-menu :default-active="activeTab" @select="handleMenuSelect">
|
||||
<el-menu-item index="preset">
|
||||
<i class="el-icon-map-location" style="margin-right: 6px" />
|
||||
<span>预置点</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="cruise">
|
||||
<i class="el-icon-s-order" style="margin-right: 6px" />
|
||||
<span>巡航组</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="scan">
|
||||
<i class="iconfont icon-slider-right" style="margin-right: 6px" />
|
||||
<span>线性扫描</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="switch">
|
||||
<i class="el-icon-s-tools" style="margin-right: 6px" />
|
||||
<span>辅助开关</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<div class="player-panel">
|
||||
<playerPtzPanel :channel-id="channelId" />
|
||||
</div>
|
||||
<div class="tab-panel">
|
||||
<ptzPresetConfig v-if="activeTab === 'preset'" :channel-id="channelId" />
|
||||
<ptzCruiseConfig v-if="activeTab === 'cruise'" :channel-id="channelId" />
|
||||
<ptzScanConfig v-if="activeTab === 'scan'" :channel-id="channelId" />
|
||||
<ptzSwitchConfig v-if="activeTab === 'switch'" :channel-id="channelId" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import playerPtzPanel from './common/playerPtzPanel.vue'
|
||||
import ptzPresetConfig from './common/ptzPresetConfig.vue'
|
||||
import ptzCruiseConfig from './common/ptzCruiseConfig.vue'
|
||||
import ptzScanConfig from './common/ptzScanConfig.vue'
|
||||
import ptzSwitchConfig from './common/ptzSwitchConfig.vue'
|
||||
|
||||
export default {
|
||||
name: 'ChPtzConfig',
|
||||
components: { playerPtzPanel, ptzPresetConfig, ptzCruiseConfig, ptzScanConfig, ptzSwitchConfig },
|
||||
props: {
|
||||
channelId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'preset'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleMenuSelect(index) {
|
||||
this.activeTab = index
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#dhPtzConfigPage {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ptz-config-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
padding-top: 16px;
|
||||
}
|
||||
.config-sidebar {
|
||||
width: 140px;
|
||||
flex: none;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.config-sidebar .el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
.player-panel {
|
||||
width: 600px;
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.tab-panel {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 0 12px;
|
||||
}
|
||||
</style>
|
||||
@ -90,6 +90,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="位置信息" min-width="150">
|
||||
<template v-slot:default="scope">
|
||||
<span v-if="scope.row.gbLongitude && scope.row.gbLatitude">{{ scope.row.gbLongitude }}<br>{{ scope.row.gbLatitude }}</span>
|
||||
<span v-if="!scope.row.gbLongitude || !scope.row.gbLatitude">无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template v-slot:default="scope">
|
||||
<div slot="reference" class="name-wrapper">
|
||||
@ -90,6 +90,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="位置信息" min-width="150">
|
||||
<template v-slot:default="scope">
|
||||
<span v-if="scope.row.gbLongitude && scope.row.gbLatitude">{{ scope.row.gbLongitude }}<br>{{ scope.row.gbLatitude }}</span>
|
||||
<span v-if="!scope.row.gbLongitude || !scope.row.gbLatitude">无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template v-slot:default="scope">
|
||||
<div slot="reference" class="name-wrapper">
|
||||
@ -118,7 +124,7 @@
|
||||
<script>
|
||||
import RegionTree from '../..//common/RegionTree.vue'
|
||||
import GbChannelSelect from '../../dialog/GbChannelSelect.vue'
|
||||
import UnusualRegionChannelSelect from '../../dialog/UnusualRegionChannelSelect.vue'
|
||||
import UnusualRegionChannelSelect from './UnusualRegionChannelSelect.vue'
|
||||
|
||||
export default {
|
||||
name: 'Region',
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
import moment from 'moment'
|
||||
import momentDurationFormatSetup from 'moment-duration-format'
|
||||
import screenfull from 'screenfull'
|
||||
import cloudRecordPlayer from './cloudRecordPlayer.vue'
|
||||
import cloudRecordPlayer from './player.vue'
|
||||
|
||||
momentDurationFormatSetup(moment)
|
||||
|
||||
@ -284,12 +284,15 @@ export default {
|
||||
this.$refs.cloudRecordPlayer.setStreamInfo(data, this.detailFiles[this.chooseFileIndex].timeLen, this.detailFiles[this.chooseFileIndex].startTime)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
this.playLoading = false
|
||||
})
|
||||
|
||||
},
|
||||
downloadFile(file) {
|
||||
this.$store.dispatch('cloudRecord/getPlayPath', file.id)
|
||||
|
||||
152
web/src/views/cloudRecord/cloudRecordPlayer.vue → web/src/views/cloudRecord/player.vue
Executable file → Normal file
152
web/src/views/cloudRecord/cloudRecordPlayer.vue → web/src/views/cloudRecord/player.vue
Executable file → Normal file
@ -1,28 +1,14 @@
|
||||
<template>
|
||||
<div id="cloudRecordPlayer" style="height: 100%">
|
||||
<div class="cloud-record-playBox" :style="playBoxStyle">
|
||||
<jessibucaPlayer
|
||||
v-if="playerType === 'Jessibuca'"
|
||||
<playerTabs
|
||||
ref="recordVideoPlayer"
|
||||
:height="'calc(100% - 250px)'"
|
||||
:show-button="false"
|
||||
:showTab="false"
|
||||
@playTimeChange="showPlayTimeChange"
|
||||
@playStatusChange="playingChange"
|
||||
fluent
|
||||
autoplay
|
||||
live
|
||||
@player-changed="onPlayerChanged"
|
||||
/>
|
||||
<rtcPlayer
|
||||
v-if="playerType === 'WebRTC'"
|
||||
ref="recordVideoPlayer"
|
||||
:has-audio="true"
|
||||
:show-controls="false"
|
||||
style="height: calc(100% - 250px)"
|
||||
autoplay
|
||||
@playTimeChange="showPlayTimeChange"
|
||||
@playStatusChange="playingChange"
|
||||
/>
|
||||
<h265web v-if="playerType === 'H265web'" ref="recordVideoPlayer" :height="'calc(100% - 250px)'" :show-button="false" @playTimeChange="showPlayTimeChange" @playStatusChange="playingChange"/>
|
||||
</div>
|
||||
<div class="cloud-record-player-option-box">
|
||||
<div class="cloud-record-show-time">
|
||||
@ -47,8 +33,6 @@
|
||||
<div class="cloud-record-record-play-control" style="background-color: transparent; box-shadow: 0 0 10px transparent">
|
||||
<a v-if="showListCallback" target="_blank" class="cloud-record-record-play-control-item iconfont icon-list" title="列表" @click="sidebarControl()" />
|
||||
<a target="_blank" class="cloud-record-record-play-control-item iconfont icon-camera1196054easyiconnet" title="截图" @click="snap()" />
|
||||
<!-- <a target="_blank" class="cloud-record-record-play-control-item iconfont icon-shuaxin11" title="刷新" @click="refresh()" />-->
|
||||
<!-- <a target="_blank" class="cloud-record-record-play-control-item iconfont icon-xiazai011" title="下载" />-->
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
@ -81,9 +65,7 @@
|
||||
<el-dropdown @command="changePlayerType" :popper-append-to-body='false' >
|
||||
<a target="_blank" class="cloud-record-record-play-control-item record-play-control-speed" title="选择播放器">{{ playerLabel }}</a>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="Jessibuca" >Jessibuca</el-dropdown-item>
|
||||
<el-dropdown-item command="WebRTC" >WebRTC</el-dropdown-item>
|
||||
<el-dropdown-item command="H265web" >H265web</el-dropdown-item>
|
||||
<el-dropdown-item v-for="p in playerList" :key="p.key" :command="p.key">{{ p.label }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
@ -97,9 +79,7 @@
|
||||
|
||||
<script>
|
||||
|
||||
import h265web from '../common/h265web.vue'
|
||||
import jessibucaPlayer from '@/views/common/jessibuca.vue'
|
||||
import rtcPlayer from '../common/rtcPlayer.vue'
|
||||
import playerTabs from '../common/playerTabs.vue'
|
||||
import moment from 'moment'
|
||||
import momentDurationFormatSetup from 'moment-duration-format'
|
||||
import screenfull from 'screenfull'
|
||||
@ -108,14 +88,11 @@ momentDurationFormatSetup(moment)
|
||||
|
||||
export default {
|
||||
name: 'CloudRecordPlayer',
|
||||
components: {
|
||||
jessibucaPlayer, rtcPlayer, h265web
|
||||
},
|
||||
components: { playerTabs },
|
||||
props: ['showListCallback', 'showNextCallback', 'showLastCallback', 'lastDiable', 'nextDiable'],
|
||||
data() {
|
||||
return {
|
||||
showSidebar: false,
|
||||
videoUrl: null,
|
||||
streamInfo: null,
|
||||
timeLen: null,
|
||||
startTime: null,
|
||||
@ -128,12 +105,8 @@ export default {
|
||||
isFullScreen: false,
|
||||
playing: false,
|
||||
initTime: null,
|
||||
playerType: 'Jessibuca',
|
||||
playerUrls: {
|
||||
Jessibuca: ['ws_flv', 'wss_flv'],
|
||||
WebRTC: ['rtc', 'rtcs'],
|
||||
H265web: ['ws_flv', 'wss_flv']
|
||||
},
|
||||
playerList: [],
|
||||
playerLabel: 'Jessibuca',
|
||||
playSpeedRange: [1, 2, 4, 6, 8, 16, 20]
|
||||
}
|
||||
},
|
||||
@ -159,7 +132,7 @@ export default {
|
||||
}
|
||||
},
|
||||
playTimeTotal() {
|
||||
return { left: `calc(${this.playerTime/this.streamInfo.duration * 100}% - 6px)` }
|
||||
return { left: `calc(${this.playerTime/this.streamInfo.duration}*100% - 6px)` }
|
||||
},
|
||||
playTimeTitleStyle() {
|
||||
return { left: (this.showTimeLeft - 16) + 'px' }
|
||||
@ -172,21 +145,31 @@ export default {
|
||||
}else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
playerLabel() {
|
||||
const labels = { Jessibuca: 'Jessibuca', WebRTC: 'WebRTC', H265web: 'H265Web' }
|
||||
return labels[this.playerType] || 'Jessibuca'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
document.addEventListener('mousemove', this.timeProcessMousemove)
|
||||
document.addEventListener('mouseup', this.timeProcessMouseup)
|
||||
},
|
||||
mounted() {},
|
||||
mounted() {
|
||||
this.updatePlayerList()
|
||||
},
|
||||
destroyed() {
|
||||
this.$destroy('recordVideoPlayer')
|
||||
},
|
||||
methods: {
|
||||
updatePlayerList() {
|
||||
if (this.$refs.recordVideoPlayer) {
|
||||
this.playerList = this.$refs.recordVideoPlayer.getPlayerList()
|
||||
const active = this.$refs.recordVideoPlayer.getActivePlayer()
|
||||
const p = this.playerList.find(p => p.key === active)
|
||||
this.playerLabel = p ? p.label : 'Jessibuca'
|
||||
}
|
||||
},
|
||||
onPlayerChanged(key) {
|
||||
const p = this.playerList.find(p => p.key === key)
|
||||
this.playerLabel = p ? p.label : 'Jessibuca'
|
||||
},
|
||||
timeProcessMouseup(event) {
|
||||
this.isMousedown = false
|
||||
},
|
||||
@ -215,11 +198,14 @@ export default {
|
||||
this.showListCallback(this.showSidebar)
|
||||
},
|
||||
snap() {
|
||||
this.$refs.recordVideoPlayer.screenshot()
|
||||
if (this.$refs.recordVideoPlayer) {
|
||||
this.$refs.recordVideoPlayer.screenshot()
|
||||
}
|
||||
},
|
||||
refresh() {
|
||||
this.$refs.recordVideoPlayer.destroy()
|
||||
this.$refs.recordVideoPlayer.playBtnClick()
|
||||
if (this.$refs.recordVideoPlayer) {
|
||||
this.$refs.recordVideoPlayer.destroy()
|
||||
}
|
||||
},
|
||||
playLast() {
|
||||
this.showLastCallback()
|
||||
@ -228,7 +214,6 @@ export default {
|
||||
this.showNextCallback()
|
||||
},
|
||||
changePlaySpeed(speed) {
|
||||
// 倍速播放
|
||||
this.playSpeed = speed
|
||||
this.$store.dispatch('cloudRecord/speed', {
|
||||
mediaServerId: this.streamInfo.mediaServerId,
|
||||
@ -238,32 +223,24 @@ export default {
|
||||
speed: this.playSpeed,
|
||||
schema: 'ts'
|
||||
})
|
||||
this.$refs.recordVideoPlayer.setPlaybackRate(this.playSpeed)
|
||||
if (this.$refs.recordVideoPlayer) {
|
||||
this.$refs.recordVideoPlayer.setPlaybackRate(this.playSpeed)
|
||||
}
|
||||
},
|
||||
changePlayerType(playerType) {
|
||||
if (this.playerType === playerType) {
|
||||
return
|
||||
}
|
||||
this.playerType = playerType
|
||||
if (this.streamInfo) {
|
||||
this.videoUrl = this.getUrlByStreamInfo()
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.recordVideoPlayer) {
|
||||
this.$refs.recordVideoPlayer.play(this.videoUrl)
|
||||
}
|
||||
})
|
||||
if (this.$refs.recordVideoPlayer) {
|
||||
this.$refs.recordVideoPlayer.switchPlayer(playerType)
|
||||
const p = this.playerList.find(p => p.key === playerType)
|
||||
this.playerLabel = p ? p.label : 'Jessibuca'
|
||||
}
|
||||
},
|
||||
seekBackward() {
|
||||
// 快退五秒
|
||||
this.seekRecord(this.playerTime - 5 * 1000)
|
||||
},
|
||||
seekForward() {
|
||||
// 快进五秒
|
||||
this.seekRecord(this.playerTime + 5 * 1000)
|
||||
},
|
||||
stopPLay() {
|
||||
// 停止
|
||||
if (this.$refs.recordVideoPlayer) {
|
||||
this.$refs.recordVideoPlayer.destroy()
|
||||
}
|
||||
@ -272,60 +249,49 @@ export default {
|
||||
this.playSpeed = 1
|
||||
},
|
||||
pausePlay() {
|
||||
// 暂停
|
||||
this.$refs.recordVideoPlayer.pause()
|
||||
// TODO
|
||||
if (this.$refs.recordVideoPlayer) {
|
||||
this.$refs.recordVideoPlayer.pause()
|
||||
}
|
||||
},
|
||||
play() {
|
||||
if (this.$refs.recordVideoPlayer.loaded) {
|
||||
this.$refs.recordVideoPlayer.unPause()
|
||||
} else {
|
||||
this.playRecord()
|
||||
if (this.$refs.recordVideoPlayer) {
|
||||
if (this.$refs.recordVideoPlayer.$refs[this.$refs.recordVideoPlayer.getActivePlayer()] &&
|
||||
this.$refs.recordVideoPlayer.$refs[this.$refs.recordVideoPlayer.getActivePlayer()].loaded) {
|
||||
this.$refs.recordVideoPlayer.$refs[this.$refs.recordVideoPlayer.getActivePlayer()].unPause()
|
||||
} else {
|
||||
this.playRecord()
|
||||
}
|
||||
}
|
||||
},
|
||||
fullScreen() {
|
||||
// 全屏
|
||||
if (this.isFullScreen) {
|
||||
screenfull.exit()
|
||||
this.isFullScreen = false
|
||||
return
|
||||
}
|
||||
const playerWidth = this.$refs.recordVideoPlayer.playerWidth
|
||||
const playerHeight = this.$refs.recordVideoPlayer.playerHeight
|
||||
const playerWrapper = this.$refs.recordVideoPlayer ? this.$refs.recordVideoPlayer.$refs.playerWrapper : null
|
||||
const playerWidth = playerWrapper ? playerWrapper.clientWidth : 0
|
||||
const playerHeight = playerWrapper ? playerWrapper.clientHeight : 0
|
||||
screenfull.request(document.getElementById('cloudRecordPlayer'))
|
||||
screenfull.on('change', (event) => {
|
||||
this.$refs.recordVideoPlayer.resize(playerWidth, playerHeight)
|
||||
if (this.$refs.recordVideoPlayer) {
|
||||
this.$refs.recordVideoPlayer.resize(playerWidth, playerHeight)
|
||||
}
|
||||
this.isFullScreen = screenfull.isFullscreen
|
||||
})
|
||||
this.isFullScreen = true
|
||||
},
|
||||
setStreamInfo(streamInfo, timeLen, startTime) {
|
||||
const keys = this.playerUrls[this.playerType]
|
||||
if (location.protocol === 'https:') {
|
||||
this.videoUrl = streamInfo[keys[1]]
|
||||
} else {
|
||||
this.videoUrl = streamInfo[keys[0]]
|
||||
}
|
||||
console.log(location.protocol)
|
||||
this.streamInfo = streamInfo
|
||||
this.timeLen = timeLen
|
||||
this.startTime = startTime
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.recordVideoPlayer) {
|
||||
this.$refs.recordVideoPlayer.play(this.videoUrl)
|
||||
console.log(streamInfo)
|
||||
this.$refs.recordVideoPlayer.setStreamInfo(streamInfo)
|
||||
}
|
||||
})
|
||||
},
|
||||
getUrlByStreamInfo() {
|
||||
if (!this.streamInfo) return ''
|
||||
const keys = this.playerUrls[this.playerType]
|
||||
if (location.protocol === 'https:') {
|
||||
this.videoUrl = this.streamInfo[keys[1]]
|
||||
} else {
|
||||
this.videoUrl = this.streamInfo[keys[0]]
|
||||
}
|
||||
return this.videoUrl
|
||||
},
|
||||
seekRecord(playSeekValue, callback) {
|
||||
this.$store.dispatch('cloudRecord/seek', {
|
||||
mediaServerId: this.streamInfo.mediaServerId,
|
||||
@ -440,10 +406,10 @@ export default {
|
||||
color: rgb(217, 217, 217);
|
||||
font-size: 14px;
|
||||
text-shadow:
|
||||
-1px -1px 0 black, /* 左上角阴影 */
|
||||
1px -1px 0 black, /* 右上角阴影 */
|
||||
-1px 1px 0 black, /* 左下角阴影 */
|
||||
1px 1px 0 black; /* 右下角阴影 */
|
||||
-1px -1px 0 black,
|
||||
1px -1px 0 black,
|
||||
-1px 1px 0 black,
|
||||
1px 1px 0 black;
|
||||
}
|
||||
.record-play-control-player {
|
||||
width: fit-content;
|
||||
@ -21,7 +21,7 @@
|
||||
<script>
|
||||
|
||||
import elDragDialog from '@/directive/el-drag-dialog'
|
||||
import cloudRecordPlayer from './cloudRecordPlayer.vue'
|
||||
import cloudRecordPlayer from './player.vue'
|
||||
|
||||
export default {
|
||||
name: 'PlayerDialog',
|
||||
|
||||
@ -1,910 +0,0 @@
|
||||
<template>
|
||||
<div id="devicePlayer" v-loading="isLoging">
|
||||
|
||||
<el-dialog
|
||||
v-if="showVideoDialog"
|
||||
v-el-drag-dialog
|
||||
title="视频播放"
|
||||
top="0"
|
||||
:close-on-click-modal="false"
|
||||
:visible.sync="showVideoDialog"
|
||||
@close="close()"
|
||||
>
|
||||
<div style="width: 100%; height: 100%">
|
||||
<el-tabs
|
||||
v-if="Object.keys(this.player).length > 1"
|
||||
v-model="activePlayer"
|
||||
type="card"
|
||||
:stretch="true"
|
||||
@tab-click="changePlayer"
|
||||
>
|
||||
<el-tab-pane label="Jessibuca" name="jessibuca">
|
||||
<jessibucaPlayer
|
||||
v-if="activePlayer === 'jessibuca'"
|
||||
ref="jessibuca"
|
||||
:visible.sync="showVideoDialog"
|
||||
:error="videoError"
|
||||
:message="videoError"
|
||||
:has-audio="hasAudio"
|
||||
fluent
|
||||
autoplay
|
||||
live
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="WebRTC" name="webRTC">
|
||||
<rtc-player
|
||||
v-if="activePlayer === 'webRTC'"
|
||||
ref="webRTC"
|
||||
:visible.sync="showVideoDialog"
|
||||
:error="videoError"
|
||||
:message="videoError"
|
||||
height="100px"
|
||||
:has-audio="hasAudio"
|
||||
fluent
|
||||
autoplay
|
||||
live
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="h265web" name="h265web">
|
||||
<h265web
|
||||
v-if="activePlayer === 'h265web'"
|
||||
ref="h265web"
|
||||
:error="videoError"
|
||||
:message="videoError"
|
||||
:has-audio="hasAudio"
|
||||
fluent
|
||||
autoplay
|
||||
live
|
||||
:show-button="true"
|
||||
/>
|
||||
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<jessibucaPlayer
|
||||
v-if="Object.keys(this.player).length == 1 && this.player.jessibuca"
|
||||
ref="jessibuca"
|
||||
:visible.sync="showVideoDialog"
|
||||
:error="videoError"
|
||||
:message="videoError"
|
||||
:has-audio="hasAudio"
|
||||
fluent
|
||||
autoplay
|
||||
live
|
||||
/>
|
||||
<rtc-player
|
||||
v-if="Object.keys(this.player).length == 1 && this.player.webRTC"
|
||||
ref="rtcPlayer"
|
||||
:visible.sync="showVideoDialog"
|
||||
:error="videoError"
|
||||
:message="videoError"
|
||||
height="100px"
|
||||
:has-audio="hasAudio"
|
||||
fluent
|
||||
autoplay
|
||||
live
|
||||
/>
|
||||
<h265web
|
||||
v-if="Object.keys(this.player).length == 1 && this.player.h265web"
|
||||
ref="h265web"
|
||||
:visible.sync="showVideoDialog"
|
||||
:error="videoError"
|
||||
:message="videoError"
|
||||
height="100px"
|
||||
:has-audio="hasAudio"
|
||||
fluent
|
||||
autoplay
|
||||
live
|
||||
/>
|
||||
</div>
|
||||
<div id="shared" style="text-align: right; margin-top: 1rem;">
|
||||
|
||||
<el-tabs v-model="tabActiveName" @tab-click="tabHandleClick">
|
||||
<el-tab-pane label="实时视频" name="media">
|
||||
<div style="display: flex; margin-bottom: 0.5rem; height: 2.5rem;">
|
||||
<span style="width: 5rem; line-height: 2.5rem; text-align: right;">播放地址:</span>
|
||||
<el-input v-model="getPlayerShared.sharedUrl" :disabled="true">
|
||||
<template slot="append">
|
||||
<i
|
||||
class="cpoy-btn el-icon-document-copy"
|
||||
title="点击拷贝"
|
||||
style="cursor: pointer"
|
||||
@click="copyUrl(getPlayerShared.sharedUrl)"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div style="display: flex; margin-bottom: 0.5rem; height: 2.5rem;">
|
||||
<span style="width: 5rem; line-height: 2.5rem; text-align: right;">iframe:</span>
|
||||
<el-input v-model="getPlayerShared.sharedIframe" :disabled="true">
|
||||
<template slot="append">
|
||||
<i
|
||||
class="cpoy-btn el-icon-document-copy"
|
||||
title="点击拷贝"
|
||||
style="cursor: pointer"
|
||||
@click="copyUrl(getPlayerShared.sharedIframe)"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div style="display: flex; margin-bottom: 0.5rem; height: 2.5rem;">
|
||||
<span style="width: 5rem; line-height: 2.5rem; text-align: right;">资源地址:</span>
|
||||
<el-input v-model="getPlayerShared.sharedRtmp" :disabled="true">
|
||||
<el-button
|
||||
slot="append"
|
||||
icon="el-icon-document-copy"
|
||||
title="点击拷贝"
|
||||
style="cursor: pointer"
|
||||
@click="copyUrl(getPlayerShared.sharedIframe)"
|
||||
/>
|
||||
<el-dropdown v-if="streamInfo" slot="prepend" trigger="click" @command="copyUrl">
|
||||
<el-button>
|
||||
更多地址<i class="el-icon-arrow-down el-icon--right" />
|
||||
</el-button>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="streamInfo.flv" :command="streamInfo.flv">
|
||||
<el-tag>FLV:</el-tag>
|
||||
<span>{{ streamInfo.flv }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.https_flv" :command="streamInfo.https_flv">
|
||||
<el-tag>FLV(https):</el-tag>
|
||||
<span>{{ streamInfo.https_flv }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.ws_flv" :command="streamInfo.ws_flv">
|
||||
<el-tag>FLV(ws):</el-tag>
|
||||
<span>{{ streamInfo.ws_flv }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.wss_flv" :command="streamInfo.wss_flv">
|
||||
<el-tag>FLV(wss):</el-tag>
|
||||
<span>{{ streamInfo.wss_flv }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.fmp4" :command="streamInfo.fmp4">
|
||||
<el-tag>FMP4:</el-tag>
|
||||
<span>{{ streamInfo.fmp4 }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.https_fmp4" :command="streamInfo.https_fmp4">
|
||||
<el-tag>FMP4(https):</el-tag>
|
||||
<span>{{ streamInfo.https_fmp4 }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.ws_fmp4" :command="streamInfo.ws_fmp4">
|
||||
<el-tag>FMP4(ws):</el-tag>
|
||||
<span>{{ streamInfo.ws_fmp4 }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.wss_fmp4" :command="streamInfo.wss_fmp4">
|
||||
<el-tag>FMP4(wss):</el-tag>
|
||||
<span>{{ streamInfo.wss_fmp4 }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.hls" :command="streamInfo.hls">
|
||||
<el-tag>HLS:</el-tag>
|
||||
<span>{{ streamInfo.hls }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.https_hls" :command="streamInfo.https_hls">
|
||||
<el-tag>HLS(https):</el-tag>
|
||||
<span>{{ streamInfo.https_hls }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.ws_hls" :command="streamInfo.ws_hls">
|
||||
<el-tag>HLS(ws):</el-tag>
|
||||
<span>{{ streamInfo.ws_hls }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.wss_hls" :command="streamInfo.wss_hls">
|
||||
<el-tag>HLS(wss):</el-tag>
|
||||
<span>{{ streamInfo.wss_hls }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.ts" :command="streamInfo.ts">
|
||||
<el-tag>TS:</el-tag>
|
||||
<span>{{ streamInfo.ts }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.https_ts" :command="streamInfo.https_ts">
|
||||
<el-tag>TS(https):</el-tag>
|
||||
<span>{{ streamInfo.https_ts }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.ws_ts" :command="streamInfo.ws_ts">
|
||||
<el-tag>TS(ws):</el-tag>
|
||||
<span>{{ streamInfo.ws_ts }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.wss_ts" :command="streamInfo.wss_ts">
|
||||
<el-tag>TS(wss):</el-tag>
|
||||
<span>{{ streamInfo.wss_ts }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtc" :command="streamInfo.rtc">
|
||||
<el-tag>RTC:</el-tag>
|
||||
<span>{{ streamInfo.rtc }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtcs" :command="streamInfo.rtcs">
|
||||
<el-tag>RTCS:</el-tag>
|
||||
<span>{{ streamInfo.rtcs }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtmp" :command="streamInfo.rtmp">
|
||||
<el-tag>RTMP:</el-tag>
|
||||
<span>{{ streamInfo.rtmp }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtmps" :command="streamInfo.rtmps">
|
||||
<el-tag>RTMPS:</el-tag>
|
||||
<span>{{ streamInfo.rtmps }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtsp" :command="streamInfo.rtsp">
|
||||
<el-tag>RTSP:</el-tag>
|
||||
<span>{{ streamInfo.rtsp }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtsps" :command="streamInfo.rtsps">
|
||||
<el-tag>RTSPS:</el-tag>
|
||||
<span>{{ streamInfo.rtsps }}</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</el-input>
|
||||
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<!--{"code":0,"data":{"paths":["22-29-30.mp4"],"rootPath":"/home/kkkkk/Documents/ZLMediaKit/release/linux/Debug/www/record/hls/kkkkk/2020-05-11/"}}-->
|
||||
<!--遥控界面-->
|
||||
<el-tab-pane v-if="showPtz" label="云台控制" name="control">
|
||||
<div style="display: grid; grid-template-columns: 240px auto; height: 180px; overflow: auto">
|
||||
<div style="display: grid; grid-template-columns: 6.25rem auto;">
|
||||
|
||||
<div class="control-wrapper">
|
||||
<div class="control-btn control-top" @mousedown="ptzCamera('up')" @mouseup="ptzCamera('stop')">
|
||||
<i class="el-icon-caret-top" />
|
||||
<div class="control-inner-btn control-inner" />
|
||||
</div>
|
||||
<div class="control-btn control-left" @mousedown="ptzCamera('left')" @mouseup="ptzCamera('stop')">
|
||||
<i class="el-icon-caret-left" />
|
||||
<div class="control-inner-btn control-inner" />
|
||||
</div>
|
||||
<div class="control-btn control-bottom" @mousedown="ptzCamera('down')" @mouseup="ptzCamera('stop')">
|
||||
<i class="el-icon-caret-bottom" />
|
||||
<div class="control-inner-btn control-inner" />
|
||||
</div>
|
||||
<div class="control-btn control-right" @mousedown="ptzCamera('right')" @mouseup="ptzCamera('stop')">
|
||||
<i class="el-icon-caret-right" />
|
||||
<div class="control-inner-btn control-inner" />
|
||||
</div>
|
||||
<div class="control-round">
|
||||
<div class="control-round-inner"><i class="fa fa-pause-circle" /></div>
|
||||
</div>
|
||||
<div class="contro-speed" style="position: absolute; left: 4px; top: 7rem; width: 6.25rem;">
|
||||
<el-slider v-model="controSpeed" :max="100" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="ptz-btn-box">
|
||||
<div style="" title="变倍+" @mousedown="ptzCamera('zoomin')" @mouseup="ptzCamera('stop')">
|
||||
<i class="el-icon-zoom-in control-zoom-btn" style="font-size: 1.5rem;" />
|
||||
</div>
|
||||
<div style="" title="变倍-" @mousedown="ptzCamera('zoomout')" @mouseup="ptzCamera('stop')">
|
||||
<i class="el-icon-zoom-out control-zoom-btn" style="font-size: 1.5rem;" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ptz-btn-box">
|
||||
<div title="聚焦+" @mousedown="focusCamera('near')" @mouseup="focusCamera('stop')">
|
||||
<i class="iconfont icon-bianjiao-fangda control-zoom-btn" style="font-size: 1.5rem;" />
|
||||
</div>
|
||||
<div title="聚焦-" @mousedown="focusCamera('far')" @mouseup="focusCamera('stop')">
|
||||
<i class="iconfont icon-bianjiao-suoxiao control-zoom-btn" style="font-size: 1.5rem;" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ptz-btn-box">
|
||||
<div title="光圈+" @mousedown="irisCamera('in')" @mouseup="irisCamera('stop')">
|
||||
<i class="iconfont icon-guangquan control-zoom-btn" style="font-size: 1.5rem;" />
|
||||
</div>
|
||||
<div title="光圈-" @mousedown="irisCamera('out')" @mouseup="irisCamera('stop')">
|
||||
<i class="iconfont icon-guangquan- control-zoom-btn" style="font-size: 1.5rem;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: left" v-if="tabActiveName === 'control'">
|
||||
<el-select
|
||||
v-model="ptzMethod"
|
||||
style="width: 100%"
|
||||
size="mini"
|
||||
placeholder="请选择云台功能"
|
||||
>
|
||||
<el-option label="预置点" value="preset" />
|
||||
<el-option label="巡航组" value="cruise" />
|
||||
<el-option label="自动扫描" value="scan" />
|
||||
<el-option label="雨刷" value="wiper" />
|
||||
<el-option label="辅助开关" value="switch" />
|
||||
</el-select>
|
||||
|
||||
<ptzPreset v-if="ptzMethod === 'preset'" :channel-id="channelId" style="margin-top: 1rem" />
|
||||
<ptzCruising v-if="ptzMethod === 'cruise'" :channel-id="channelId" style="margin-top: 1rem" />
|
||||
<ptzScan v-if="ptzMethod === 'scan'" :channel-id="channelId" style="margin-top: 1rem" />
|
||||
<ptzWiper v-if="ptzMethod === 'wiper'" :channel-id="channelId" style="margin-top: 1rem" />
|
||||
<ptzSwitch v-if="ptzMethod === 'switch'" :channel-id="channelId" style="margin-top: 1rem" />
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="编码信息" name="codec">
|
||||
<mediaInfo ref="mediaInfo" :app="app" :stream="streamId" :media-server-id="mediaServerId" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane v-if="showBroadcast" label="语音对讲" name="broadcast">
|
||||
<div style="padding: 0 10px">
|
||||
<!-- <el-switch v-model="broadcastMode" :disabled="broadcastStatus !== -1" active-color="#409EFF"-->
|
||||
<!-- active-text="喊话(Broadcast)"-->
|
||||
<!-- inactive-text="对讲(Talk)"></el-switch>-->
|
||||
|
||||
<el-radio-group v-model="broadcastMode" :disabled="broadcastStatus !== -1">
|
||||
<el-radio :label="true">喊话(Broadcast)</el-radio>
|
||||
<el-radio :label="false">对讲(Talk)</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="trank" style="text-align: center;">
|
||||
<el-button
|
||||
:type="getBroadcastStatus()"
|
||||
:disabled="broadcastStatus === -2"
|
||||
circle
|
||||
icon="el-icon-microphone"
|
||||
style="font-size: 32px; padding: 24px;margin-top: 24px;"
|
||||
@click="broadcastStatusClick()"
|
||||
/>
|
||||
<p>
|
||||
<span v-if="broadcastStatus === -2">正在释放资源</span>
|
||||
<span v-if="broadcastStatus === -1">点击开始对讲</span>
|
||||
<span v-if="broadcastStatus === 0">等待接通中...</span>
|
||||
<span v-if="broadcastStatus === 1">请说话</span>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import elDragDialog from '@/directive/el-drag-dialog'
|
||||
import crypto from 'crypto'
|
||||
import rtcPlayer from '../rtcPlayer.vue'
|
||||
import jessibucaPlayer from '../jessibuca.vue'
|
||||
import PtzPreset from './ptzPreset.vue'
|
||||
import PtzCruising from './ptzCruising.vue'
|
||||
import ptzScan from './ptzScan.vue'
|
||||
import ptzWiper from './ptzWiper.vue'
|
||||
import ptzSwitch from './ptzSwitch.vue'
|
||||
import mediaInfo from '../mediaInfo.vue'
|
||||
import H265web from '../h265web.vue'
|
||||
|
||||
export default {
|
||||
name: 'DevicePlayer',
|
||||
directives: { elDragDialog },
|
||||
components: {
|
||||
H265web,
|
||||
PtzPreset, PtzCruising, ptzScan, ptzWiper, ptzSwitch, mediaInfo,
|
||||
jessibucaPlayer, rtcPlayer
|
||||
},
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
video: 'http://lndxyj.iqilu.com/public/upload/2019/10/14/8c001ea0c09cdc59a57829dabc8010fa.mp4',
|
||||
videoUrl: '',
|
||||
activePlayer: 'jessibuca',
|
||||
// 如何你只是用一种播放器,直接注释掉不用的部分即可
|
||||
player: {
|
||||
jessibuca: ['ws_flv', 'wss_flv'],
|
||||
webRTC: ['rtc', 'rtcs'],
|
||||
h265web: ['ws_flv', 'wss_flv']
|
||||
},
|
||||
showVideoDialog: false,
|
||||
channelId: null,
|
||||
streamId: '',
|
||||
ptzMethod: 'preset',
|
||||
ptzPresetId: '',
|
||||
app: '',
|
||||
mediaServerId: '',
|
||||
tabActiveName: 'media',
|
||||
hasAudio: false,
|
||||
loadingRecords: false,
|
||||
recordsLoading: false,
|
||||
isLoging: false,
|
||||
controSpeed: 30,
|
||||
timeVal: 0,
|
||||
timeMin: 0,
|
||||
timeMax: 1440,
|
||||
presetPos: 1,
|
||||
cruisingSpeed: 100,
|
||||
cruisingTime: 5,
|
||||
cruisingGroup: 0,
|
||||
scanSpeed: 100,
|
||||
scanGroup: 0,
|
||||
tracks: [],
|
||||
showPtz: true,
|
||||
showBroadcast: true,
|
||||
showRrecord: true,
|
||||
sliderTime: 0,
|
||||
seekTime: 0,
|
||||
recordStartTime: 0,
|
||||
showTimeText: '00:00:00',
|
||||
streamInfo: null,
|
||||
broadcastMode: true,
|
||||
broadcastRtc: null,
|
||||
broadcastStatus: -1 // -2 正在释放资源 -1 默认状态 0 等待接通 1 接通成功
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getPlayerShared: function() {
|
||||
const typeMap = { jessibuca: 0, webRTC: 1, h265web: 2 }
|
||||
const type = typeMap[this.activePlayer] || 0
|
||||
const baseUrl = window.location.origin + '/#/play/share?type=' + type + '&url=' + encodeURIComponent(this.videoUrl)
|
||||
return {
|
||||
sharedUrl: baseUrl,
|
||||
sharedIframe: '<iframe src="' + baseUrl + '"></iframe>',
|
||||
sharedRtmp: this.videoUrl
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.broadcastStatus = -1
|
||||
if (Object.keys(this.player).length === 1) {
|
||||
this.activePlayer = Object.keys(this.player)[0]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tabHandleClick: function(tab, event) {
|
||||
console.log(tab)
|
||||
this.tracks = []
|
||||
if (tab.name === 'codec') {
|
||||
this.$refs.mediaInfo.startTask()
|
||||
} else {
|
||||
this.$refs.mediaInfo.stopTask()
|
||||
}
|
||||
},
|
||||
changePlayer: function(tab) {
|
||||
console.log(this.player[tab.name][0])
|
||||
this.activePlayer = tab.name
|
||||
this.videoUrl = this.getUrlByStreamInfo()
|
||||
console.log(this.videoUrl)
|
||||
},
|
||||
openDialog: function(tab, channelId, param) {
|
||||
if (this.showVideoDialog) {
|
||||
return
|
||||
}
|
||||
this.tabActiveName = tab
|
||||
this.channelId = channelId
|
||||
this.streamId = ''
|
||||
this.mediaServerId = ''
|
||||
this.app = ''
|
||||
this.videoUrl = ''
|
||||
if (this.$refs[this.activePlayer]) {
|
||||
this.$refs[this.activePlayer].pause()
|
||||
}
|
||||
switch (tab) {
|
||||
case 'media':
|
||||
this.play(param.streamInfo, param.hasAudio)
|
||||
break
|
||||
case 'streamPlay':
|
||||
this.tabActiveName = 'media'
|
||||
this.showRrecord = false
|
||||
this.showPtz = false
|
||||
this.showBroadcast = false
|
||||
this.play(param.streamInfo, param.hasAudio)
|
||||
break
|
||||
case 'control':
|
||||
break
|
||||
}
|
||||
},
|
||||
play: function(streamInfo, hasAudio) {
|
||||
this.streamInfo = streamInfo
|
||||
this.hasAudio = hasAudio
|
||||
this.isLoging = false
|
||||
// this.videoUrl = streamInfo.rtc;
|
||||
this.videoUrl = this.getUrlByStreamInfo()
|
||||
this.streamId = streamInfo.stream
|
||||
this.app = streamInfo.app
|
||||
this.mediaServerId = streamInfo.mediaServerId
|
||||
this.playFromStreamInfo(false, streamInfo)
|
||||
},
|
||||
getUrlByStreamInfo() {
|
||||
console.log(this.streamInfo)
|
||||
let streamInfo = this.streamInfo
|
||||
if (this.streamInfo.transcodeStream) {
|
||||
streamInfo = this.streamInfo.transcodeStream
|
||||
}
|
||||
if (location.protocol === 'https:') {
|
||||
this.videoUrl = streamInfo[this.player[this.activePlayer][1]]
|
||||
} else {
|
||||
this.videoUrl = streamInfo[this.player[this.activePlayer][0]]
|
||||
}
|
||||
return this.videoUrl
|
||||
},
|
||||
|
||||
playFromStreamInfo: function(realHasAudio, streamInfo) {
|
||||
this.showVideoDialog = true
|
||||
this.hasaudio = realHasAudio && this.hasaudio
|
||||
if (this.$refs[this.activePlayer]) {
|
||||
this.$refs[this.activePlayer].play(this.getUrlByStreamInfo(streamInfo))
|
||||
} else {
|
||||
this.$nextTick(() => {
|
||||
this.$refs[this.activePlayer].play(this.getUrlByStreamInfo(streamInfo))
|
||||
})
|
||||
}
|
||||
},
|
||||
close: function() {
|
||||
console.log('关闭视频')
|
||||
if (this.$refs[this.activePlayer]) {
|
||||
this.$refs[this.activePlayer].pause()
|
||||
}
|
||||
this.videoUrl = ''
|
||||
this.showVideoDialog = false
|
||||
this.stopBroadcast()
|
||||
},
|
||||
ptzCamera: function(command) {
|
||||
console.log('云台控制:' + command)
|
||||
this.$store.dispatch('commonChanel/ptz',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
command: command,
|
||||
panSpeed: this.controSpeed,
|
||||
tiltSpeed: this.controSpeed,
|
||||
zoomSpeed: this.controSpeed
|
||||
})
|
||||
},
|
||||
irisCamera: function(command) {
|
||||
this.$store.dispatch('commonChanel/iris',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
command: command,
|
||||
speed: this.controSpeed
|
||||
})
|
||||
},
|
||||
focusCamera: function(command) {
|
||||
this.$store.dispatch('commonChanel/focus',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
command: command,
|
||||
speed: this.controSpeed
|
||||
})
|
||||
},
|
||||
// ////////////////////播放器事件处理//////////////////////////
|
||||
videoError: function(e) {
|
||||
console.log('播放器错误:' + JSON.stringify(e))
|
||||
},
|
||||
copyUrl: function(dropdownItem) {
|
||||
console.log(dropdownItem)
|
||||
this.$copyText(dropdownItem).then((e) => {
|
||||
this.$message.success({
|
||||
showClose: true,
|
||||
message: '成功拷贝到粘贴板'
|
||||
})
|
||||
}, (e) => {
|
||||
|
||||
})
|
||||
},
|
||||
getBroadcastStatus() {
|
||||
if (this.broadcastStatus == -2) {
|
||||
return 'primary'
|
||||
}
|
||||
if (this.broadcastStatus == -1) {
|
||||
return 'primary'
|
||||
}
|
||||
if (this.broadcastStatus == 0) {
|
||||
return 'warning'
|
||||
}
|
||||
if (this.broadcastStatus === 1) {
|
||||
return 'danger'
|
||||
}
|
||||
},
|
||||
broadcastStatusClick() {
|
||||
if (this.broadcastStatus === -1) {
|
||||
// 默认状态, 开始
|
||||
this.broadcastStatus = 0
|
||||
// 发起语音对讲
|
||||
this.$store.dispatch('play/broadcastStart', [ this.channelId, this.broadcastMode])
|
||||
.then(data => {
|
||||
const streamInfo = data.streamInfo
|
||||
if (document.location.protocol.includes('https')) {
|
||||
this.startBroadcast(streamInfo.rtcs)
|
||||
} else {
|
||||
this.startBroadcast(streamInfo.rtc)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
})
|
||||
} else if (this.broadcastStatus === 1) {
|
||||
this.broadcastStatus = -1
|
||||
this.broadcastRtc.close()
|
||||
}
|
||||
},
|
||||
startBroadcast(url) {
|
||||
// 获取推流鉴权Key
|
||||
this.$store.dispatch('user/getUserInfo')
|
||||
.then((data) => {
|
||||
if (data == null) {
|
||||
this.broadcastStatus = -1
|
||||
return
|
||||
}
|
||||
const pushKey = data.pushKey
|
||||
// 获取推流鉴权KEY
|
||||
url += '&sign=' + crypto.createHash('md5').update(pushKey, 'utf8').digest('hex')
|
||||
console.log('开始语音喊话: ' + url)
|
||||
this.broadcastRtc = new ZLMRTCClient.Endpoint({
|
||||
debug: true, // 是否打印日志
|
||||
zlmsdpUrl: url, // 流地址
|
||||
simulecast: false,
|
||||
useCamera: false,
|
||||
audioEnable: true,
|
||||
videoEnable: false,
|
||||
recvOnly: false
|
||||
})
|
||||
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_NOT_SUPPORT, (e) => { // 获取到了本地流
|
||||
console.error('不支持webrtc', e)
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '不支持webrtc, 无法进行语音喊话',
|
||||
type: 'error'
|
||||
})
|
||||
this.broadcastStatus = -1
|
||||
})
|
||||
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, (e) => { // ICE 协商出错
|
||||
console.error('ICE 协商出错')
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: 'ICE 协商出错',
|
||||
type: 'error'
|
||||
})
|
||||
this.broadcastStatus = -1
|
||||
})
|
||||
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, (e) => { // offer anwser 交换失败
|
||||
console.error('offer anwser 交换失败', e)
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: 'offer anwser 交换失败' + e,
|
||||
type: 'error'
|
||||
})
|
||||
this.broadcastStatus = -1
|
||||
})
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, (e) => { // offer anwser 交换失败
|
||||
console.log('状态改变', e)
|
||||
if (e === 'connecting') {
|
||||
this.broadcastStatus = 0
|
||||
} else if (e === 'connected') {
|
||||
this.broadcastStatus = 1
|
||||
} else if (e === 'disconnected') {
|
||||
this.broadcastStatus = -1
|
||||
}
|
||||
})
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.CAPTURE_STREAM_FAILED, (e) => { // offer anwser 交换失败
|
||||
console.log('捕获流失败', e)
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '捕获流失败' + e,
|
||||
type: 'error'
|
||||
})
|
||||
this.broadcastStatus = -1
|
||||
})
|
||||
}).catch(e => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: e,
|
||||
type: 'error'
|
||||
})
|
||||
this.broadcastStatus = -1
|
||||
})
|
||||
},
|
||||
stopBroadcast() {
|
||||
this.broadcastRtc.close()
|
||||
this.broadcastStatus = -1
|
||||
this.$store.dispatch('play/broadcastStop', [this.channelId])
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.control-wrapper {
|
||||
position: relative;
|
||||
width: 6.25rem;
|
||||
height: 6.25rem;
|
||||
max-width: 6.25rem;
|
||||
max-height: 6.25rem;
|
||||
border-radius: 100%;
|
||||
margin-top: 1.5rem;
|
||||
margin-left: 0.5rem;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 5rem;
|
||||
height: 11rem;
|
||||
max-height: 11rem;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
width: 44%;
|
||||
height: 44%;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #78aee4;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s linear;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.control-btn i {
|
||||
font-size: 20px;
|
||||
color: #78aee4;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.control-btn i:hover {
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.control-zoom-btn:hover {
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.control-round {
|
||||
position: absolute;
|
||||
top: 21%;
|
||||
left: 21%;
|
||||
width: 58%;
|
||||
height: 58%;
|
||||
background: #fff;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.control-round-inner {
|
||||
position: absolute;
|
||||
left: 13%;
|
||||
top: 13%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
font-size: 40px;
|
||||
color: #78aee4;
|
||||
border: 1px solid #78aee4;
|
||||
border-radius: 100%;
|
||||
transition: all 0.3s linear;
|
||||
}
|
||||
|
||||
.control-inner-btn {
|
||||
position: absolute;
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.control-top {
|
||||
top: -8%;
|
||||
left: 27%;
|
||||
transform: rotate(-45deg);
|
||||
border-radius: 5px 100% 5px 0;
|
||||
}
|
||||
|
||||
.control-top i {
|
||||
transform: rotate(45deg);
|
||||
border-radius: 5px 100% 5px 0;
|
||||
}
|
||||
|
||||
.control-top .control-inner {
|
||||
left: -1px;
|
||||
bottom: 0;
|
||||
border-top: 1px solid #78aee4;
|
||||
border-right: 1px solid #78aee4;
|
||||
border-radius: 0 100% 0 0;
|
||||
}
|
||||
|
||||
.control-top .fa {
|
||||
transform: rotate(45deg) translateY(-7px);
|
||||
}
|
||||
|
||||
.control-left {
|
||||
top: 27%;
|
||||
left: -8%;
|
||||
transform: rotate(45deg);
|
||||
border-radius: 5px 0 5px 100%;
|
||||
}
|
||||
|
||||
.control-left i {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.control-left .control-inner {
|
||||
right: -1px;
|
||||
top: -1px;
|
||||
border-bottom: 1px solid #78aee4;
|
||||
border-left: 1px solid #78aee4;
|
||||
border-radius: 0 0 0 100%;
|
||||
}
|
||||
|
||||
.control-left .fa {
|
||||
transform: rotate(-45deg) translateX(-7px);
|
||||
}
|
||||
|
||||
.control-right {
|
||||
top: 27%;
|
||||
right: -8%;
|
||||
transform: rotate(45deg);
|
||||
border-radius: 5px 100% 5px 0;
|
||||
}
|
||||
|
||||
.control-right i {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.control-right .control-inner {
|
||||
left: -1px;
|
||||
bottom: -1px;
|
||||
border-top: 1px solid #78aee4;
|
||||
border-right: 1px solid #78aee4;
|
||||
border-radius: 0 100% 0 0;
|
||||
}
|
||||
|
||||
.control-right .fa {
|
||||
transform: rotate(-45deg) translateX(7px);
|
||||
}
|
||||
|
||||
.control-bottom {
|
||||
left: 27%;
|
||||
bottom: -8%;
|
||||
transform: rotate(45deg);
|
||||
border-radius: 0 5px 100% 5px;
|
||||
}
|
||||
|
||||
.control-bottom i {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.control-bottom .control-inner {
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
border-bottom: 1px solid #78aee4;
|
||||
border-right: 1px solid #78aee4;
|
||||
border-radius: 0 0 100% 0;
|
||||
}
|
||||
|
||||
.control-bottom .fa {
|
||||
transform: rotate(-45deg) translateY(7px);
|
||||
}
|
||||
|
||||
.trank {
|
||||
width: 80%;
|
||||
height: 180px;
|
||||
text-align: left;
|
||||
padding: 0 10%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.trankInfo {
|
||||
width: 80%;
|
||||
padding: 0 10%;
|
||||
}
|
||||
.el-dialog__body{
|
||||
padding: 10px 20px;
|
||||
}
|
||||
.ptz-btn-box {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
height: 3rem;
|
||||
line-height: 4rem;
|
||||
}
|
||||
</style>
|
||||
@ -1,88 +0,0 @@
|
||||
<template>
|
||||
<div id="deviceEdit" v-loading="isLoging">
|
||||
<el-dialog
|
||||
title="设备编辑"
|
||||
width="40%"
|
||||
top="2rem"
|
||||
:close-on-click-modal="false"
|
||||
:visible.sync="showDialog"
|
||||
:destroy-on-close="true"
|
||||
@close="close()"
|
||||
>
|
||||
<div id="shared" style="margin-top: 1rem;margin-right: 100px;">
|
||||
<el-form ref="form" :rules="rules" :model="form" label-width="200px" >
|
||||
<el-form-item label="终端手机号" prop="phoneNumber">
|
||||
<el-input v-model="form.phoneNumber" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div style="float: right;">
|
||||
<el-button type="primary" @click="onSubmit" >确认</el-button>
|
||||
<el-button @click="close">取消</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "deviceEdit",
|
||||
props: {},
|
||||
computed: {},
|
||||
created() {},
|
||||
data() {
|
||||
return {
|
||||
listChangeCallback: null,
|
||||
showDialog: false,
|
||||
isLoging: false,
|
||||
form: {},
|
||||
isEdit: false,
|
||||
rules: {
|
||||
deviceId: [{ required: true, message: "请输入设备编号", trigger: "blur" }]
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
openDialog: function (row, callback) {
|
||||
console.log(row)
|
||||
this.showDialog = true;
|
||||
this.isEdit = false;
|
||||
if (row) {
|
||||
this.isEdit = true;
|
||||
}
|
||||
this.form = {};
|
||||
this.listChangeCallback = callback;
|
||||
if (row != null) {
|
||||
this.form = row;
|
||||
}
|
||||
},
|
||||
onSubmit: function () {
|
||||
console.log("onSubmit");
|
||||
this.$axios({
|
||||
method: 'post',
|
||||
url:`/api/jt1078/terminal/${this.isEdit?'update':'add'}/`,
|
||||
params: this.form
|
||||
}).then((res) => {
|
||||
console.log(res.data)
|
||||
if (res.data.code === 0) {
|
||||
this.listChangeCallback()
|
||||
}else {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: res.data.msg,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}).catch(function (error) {
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
close: function () {
|
||||
this.showDialog = false;
|
||||
this.$refs.form.resetFields();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,923 +0,0 @@
|
||||
<template>
|
||||
<div id="devicePlayer" v-loading="isLoging">
|
||||
|
||||
<el-dialog title="视频播放" top="0" :close-on-click-modal="false" :visible.sync="showVideoDialog" @close="close()" v-if="showVideoDialog">
|
||||
<div style="width: 100%; height: 100%">
|
||||
<el-tabs type="card" :stretch="true" v-model="activePlayer" @tab-click="changePlayer"
|
||||
v-if="Object.keys(this.player).length > 1">
|
||||
<el-tab-pane label="Jessibuca" name="jessibuca">
|
||||
<jessibucaPlayer v-if="activePlayer === 'jessibuca'" ref="jessibuca" :visible.sync="showVideoDialog"
|
||||
:error="videoError" :message="videoError"
|
||||
:hasAudio="hasAudio" fluent autoplay live></jessibucaPlayer>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="WebRTC" name="webRTC">
|
||||
<rtc-player v-if="activePlayer === 'webRTC'" ref="webRTC" :visible.sync="showVideoDialog"
|
||||
:error="videoError" :message="videoError" height="100px"
|
||||
:hasAudio="hasAudio" fluent autoplay live></rtc-player>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="h265web">h265web敬请期待</el-tab-pane>
|
||||
</el-tabs>
|
||||
<jessibucaPlayer v-if="Object.keys(this.player).length == 1 && this.player.jessibuca" ref="jessibuca"
|
||||
:visible.sync="showVideoDialog" :error="videoError" :message="videoError"
|
||||
:hasAudio="hasAudio" fluent autoplay live></jessibucaPlayer>
|
||||
<rtc-player v-if="Object.keys(this.player).length == 1 && this.player.webRTC" ref="jessibuca"
|
||||
:visible.sync="showVideoDialog" :error="videoError" :message="videoError"
|
||||
height="100px" :hasAudio="hasAudio" fluent autoplay live></rtc-player>
|
||||
|
||||
</div>
|
||||
<div id="shared" style="text-align: right; margin-top: 1rem;">
|
||||
<el-tabs v-model="tabActiveName" @tab-click="tabHandleClick">
|
||||
<el-tab-pane label="实时视频" name="media">
|
||||
<div style="display: flex; margin-bottom: 0.5rem; height: 2.5rem;">
|
||||
<span style="width: 5rem; line-height: 2.5rem; text-align: right;">播放地址:</span>
|
||||
<el-input v-model="getPlayerShared.sharedUrl" :disabled="true">
|
||||
<template slot="append">
|
||||
<i class="cpoy-btn el-icon-document-copy" title="点击拷贝" v-clipboard="getPlayerShared.sharedUrl"
|
||||
@success="$message({type:'success', message:'成功拷贝到粘贴板'})"></i>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div style="display: flex; margin-bottom: 0.5rem; height: 2.5rem;">
|
||||
<span style="width: 5rem; line-height: 2.5rem; text-align: right;">iframe:</span>
|
||||
<el-input v-model="getPlayerShared.sharedIframe" :disabled="true">
|
||||
<template slot="append">
|
||||
<i class="cpoy-btn el-icon-document-copy" title="点击拷贝" v-clipboard="getPlayerShared.sharedIframe"
|
||||
@success="$message({type:'success', message:'成功拷贝到粘贴板'})"></i>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div style="display: flex; margin-bottom: 0.5rem; height: 2.5rem;">
|
||||
<span style="width: 5rem; line-height: 2.5rem; text-align: right;">资源地址:</span>
|
||||
<el-input v-model="getPlayerShared.sharedRtmp" :disabled="true">
|
||||
<el-button slot="append" icon="el-icon-document-copy" title="点击拷贝"
|
||||
v-clipboard="getPlayerShared.sharedRtmp"
|
||||
@success="$message({type:'success', message:'成功拷贝到粘贴板'})"></el-button>
|
||||
<el-dropdown slot="prepend" v-if="streamInfo" trigger="click" @command="copyUrl">
|
||||
<el-button>
|
||||
更多地址<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item v-if="streamInfo.flv" :command="streamInfo.flv">
|
||||
<el-tag>FLV:</el-tag>
|
||||
<span>{{ streamInfo.flv }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.https_flv" :command="streamInfo.https_flv">
|
||||
<el-tag>FLV(https):</el-tag>
|
||||
<span>{{ streamInfo.https_flv }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.ws_flv" :command="streamInfo.ws_flv">
|
||||
<el-tag>FLV(ws):</el-tag>
|
||||
<span>{{ streamInfo.ws_flv }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.wss_flv" :command="streamInfo.wss_flv">
|
||||
<el-tag>FLV(wss):</el-tag>
|
||||
<span>{{ streamInfo.wss_flv }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.fmp4" :command="streamInfo.fmp4">
|
||||
<el-tag>FMP4:</el-tag>
|
||||
<span>{{ streamInfo.fmp4 }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.https_fmp4" :command="streamInfo.https_fmp4">
|
||||
<el-tag>FMP4(https):</el-tag>
|
||||
<span>{{ streamInfo.https_fmp4 }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.ws_fmp4" :command="streamInfo.ws_fmp4">
|
||||
<el-tag>FMP4(ws):</el-tag>
|
||||
<span>{{ streamInfo.ws_fmp4 }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.wss_fmp4" :command="streamInfo.wss_fmp4">
|
||||
<el-tag>FMP4(wss):</el-tag>
|
||||
<span>{{ streamInfo.wss_fmp4 }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.hls" :command="streamInfo.hls">
|
||||
<el-tag>HLS:</el-tag>
|
||||
<span>{{ streamInfo.hls }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.https_hls" :command="streamInfo.https_hls">
|
||||
<el-tag>HLS(https):</el-tag>
|
||||
<span>{{ streamInfo.https_hls }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.ws_hls" :command="streamInfo.ws_hls">
|
||||
<el-tag>HLS(ws):</el-tag>
|
||||
<span>{{ streamInfo.ws_hls }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.wss_hls" :command="streamInfo.wss_hls">
|
||||
<el-tag>HLS(wss):</el-tag>
|
||||
<span>{{ streamInfo.wss_hls }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.ts" :command="streamInfo.ts">
|
||||
<el-tag>TS:</el-tag>
|
||||
<span>{{ streamInfo.ts }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.https_ts" :command="streamInfo.https_ts">
|
||||
<el-tag>TS(https):</el-tag>
|
||||
<span>{{ streamInfo.https_ts }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.ws_ts" :command="streamInfo.ws_ts">
|
||||
<el-tag>TS(ws):</el-tag>
|
||||
<span>{{ streamInfo.ws_ts }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.wss_ts" :command="streamInfo.wss_ts">
|
||||
<el-tag>TS(wss):</el-tag>
|
||||
<span>{{ streamInfo.wss_ts }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtc" :command="streamInfo.rtc">
|
||||
<el-tag>RTC:</el-tag>
|
||||
<span>{{ streamInfo.rtc }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtcs" :command="streamInfo.rtcs">
|
||||
<el-tag>RTCS:</el-tag>
|
||||
<span>{{ streamInfo.rtcs }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtmp" :command="streamInfo.rtmp">
|
||||
<el-tag>RTMP:</el-tag>
|
||||
<span>{{ streamInfo.rtmp }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtmps" :command="streamInfo.rtmps">
|
||||
<el-tag>RTMPS:</el-tag>
|
||||
<span>{{ streamInfo.rtmps }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtsp" :command="streamInfo.rtsp">
|
||||
<el-tag>RTSP:</el-tag>
|
||||
<span>{{ streamInfo.rtsp }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtsps" :command="streamInfo.rtsps">
|
||||
<el-tag>RTSPS:</el-tag>
|
||||
<span>{{ streamInfo.rtsps }}</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</el-input>
|
||||
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<!--{"code":0,"data":{"paths":["22-29-30.mp4"],"rootPath":"/home/kkkkk/Documents/ZLMediaKit/release/linux/Debug/www/record/hls/kkkkk/2020-05-11/"}}-->
|
||||
<!--遥控界面-->
|
||||
<el-tab-pane label="云台控制" name="control" v-if="showPtz">
|
||||
<div style="display: grid; grid-template-columns: 240px auto; height: 180px; overflow: auto">
|
||||
<div style="display: grid; grid-template-columns: 6.25rem auto;">
|
||||
|
||||
<div class="control-wrapper">
|
||||
<div class="control-btn control-top" @mousedown="ptzCamera('up')" @mouseup="ptzCamera('stop')">
|
||||
<i class="el-icon-caret-top"></i>
|
||||
<div class="control-inner-btn control-inner"></div>
|
||||
</div>
|
||||
<div class="control-btn control-left" @mousedown="ptzCamera('left')" @mouseup="ptzCamera('stop')">
|
||||
<i class="el-icon-caret-left"></i>
|
||||
<div class="control-inner-btn control-inner"></div>
|
||||
</div>
|
||||
<div class="control-btn control-bottom" @mousedown="ptzCamera('down')" @mouseup="ptzCamera('stop')">
|
||||
<i class="el-icon-caret-bottom"></i>
|
||||
<div class="control-inner-btn control-inner"></div>
|
||||
</div>
|
||||
<div class="control-btn control-right" @mousedown="ptzCamera('right')" @mouseup="ptzCamera('stop')">
|
||||
<i class="el-icon-caret-right"></i>
|
||||
<div class="control-inner-btn control-inner"></div>
|
||||
</div>
|
||||
<div class="control-round">
|
||||
<div class="control-round-inner"><i class="fa fa-pause-circle"></i></div>
|
||||
</div>
|
||||
<div class="contro-speed" style="position: absolute; left: 4px; top: 7rem; width: 6.25rem;">
|
||||
<el-slider v-model="controSpeed" :max="100"></el-slider>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="ptz-btn-box">
|
||||
<div style="" @mousedown="ptzCamera('zoomin')" @mouseup="ptzCamera('stop')" title="变倍+">
|
||||
<i class="el-icon-zoom-in control-zoom-btn" style="font-size: 1.5rem;"></i>
|
||||
</div>
|
||||
<div style="" @mousedown="ptzCamera('zoomout')" @mouseup="ptzCamera('stop')" title="变倍-">
|
||||
<i class="el-icon-zoom-out control-zoom-btn" style="font-size: 1.5rem;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ptz-btn-box">
|
||||
<div @mousedown="ptzCamera('focusnear')" @mouseup="ptzCamera('stop')" title="聚焦+">
|
||||
<i class="iconfont icon-bianjiao-fangda control-zoom-btn" style="font-size: 1.5rem;"></i>
|
||||
</div>
|
||||
<div @mousedown="ptzCamera('focusfar')" @mouseup="ptzCamera('stop')" title="聚焦-">
|
||||
<i class="iconfont icon-bianjiao-suoxiao control-zoom-btn" style="font-size: 1.5rem;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ptz-btn-box">
|
||||
<div @mousedown="ptzCamera('irisin')" @mouseup="ptzCamera('stop')" title="光圈+">
|
||||
<i class="iconfont icon-guangquan control-zoom-btn" style="font-size: 1.5rem;"></i>
|
||||
</div>
|
||||
<div @mousedown="ptzCamera('irisout')" @mouseup="ptzCamera('stop')" title="光圈-">
|
||||
<i class="iconfont icon-guangquan- control-zoom-btn" style="font-size: 1.5rem;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: left" >
|
||||
<div style="width: 100%; display: grid; grid-template-rows: 1fr 1fr; grid-row-gap: 10px">
|
||||
<el-button-group>
|
||||
<el-button size="mini" @click="wiper('on')">开启雨刷
|
||||
</el-button>
|
||||
<el-button size="mini" @click="wiper('off')">关闭雨刷
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
<el-button-group>
|
||||
<el-button size="mini" @click="fillLight('on')">开补光灯
|
||||
</el-button>
|
||||
<el-button size="mini" @click="fillLight('off')">关补光灯
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="编码信息" name="codec" v-loading="tracksLoading">
|
||||
<mediaInfo :app="app" :stream="streamId" :mediaServerId="mediaServerId"></mediaInfo>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="语音对讲" name="broadcast">
|
||||
<div class="trank" style="text-align: center;">
|
||||
<el-button @click="broadcastStatusClick()" :type="getBroadcastStatus()" :disabled="broadcastStatus === -2"
|
||||
circle icon="el-icon-microphone" style="font-size: 32px; padding: 24px;margin-top: 24px;"/>
|
||||
<p>
|
||||
<span v-if="broadcastStatus === -2">正在释放资源</span>
|
||||
<span v-if="broadcastStatus === -1">点击开始对讲</span>
|
||||
<span v-if="broadcastStatus === 0">等待接通中...</span>
|
||||
<span v-if="broadcastStatus === 1">请说话</span>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import rtcPlayer from '../dialog/rtcPlayer.vue'
|
||||
import LivePlayer from '@liveqing/liveplayer'
|
||||
import crypto from 'crypto'
|
||||
import jessibucaPlayer from '../common/jessibuca.vue'
|
||||
import mediaInfo from '../common/mediaInfo.vue'
|
||||
|
||||
export default {
|
||||
name: 'devicePlayer',
|
||||
props: {},
|
||||
components: {
|
||||
mediaInfo,
|
||||
LivePlayer, jessibucaPlayer, rtcPlayer,
|
||||
},
|
||||
computed: {
|
||||
getPlayerShared: function () {
|
||||
const typeMap = { jessibuca: 0, webRTC: 1, h265web: 2 }
|
||||
const type = typeMap[this.activePlayer] || 0
|
||||
const baseUrl = window.location.origin + '/#/play/share?type=' + type + '&url=' + encodeURIComponent(this.videoUrl)
|
||||
return {
|
||||
sharedUrl: baseUrl,
|
||||
sharedIframe: '<iframe src="' + baseUrl + '"></iframe>',
|
||||
sharedRtmp: this.videoUrl
|
||||
};
|
||||
}
|
||||
},
|
||||
created() {
|
||||
console.log("created")
|
||||
console.log(this.player)
|
||||
this.broadcastStatus = -1;
|
||||
if (Object.keys(this.player).length === 1) {
|
||||
this.activePlayer = Object.keys(this.player)[0]
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
video: 'http://lndxyj.iqilu.com/public/upload/2019/10/14/8c001ea0c09cdc59a57829dabc8010fa.mp4',
|
||||
videoUrl: '',
|
||||
activePlayer: "jessibuca",
|
||||
// 如何你只是用一种播放器,直接注释掉不用的部分即可
|
||||
player: {
|
||||
jessibuca: ["ws_flv", "wss_flv"],
|
||||
webRTC: ["rtc", "rtcs"],
|
||||
},
|
||||
showVideoDialog: false,
|
||||
streamId: '',
|
||||
app: '',
|
||||
mediaServerId: '',
|
||||
deviceId: '',
|
||||
channelId: '',
|
||||
tabActiveName: 'media',
|
||||
hasAudio: false,
|
||||
loadingRecords: false,
|
||||
recordsLoading: false,
|
||||
isLoging: false,
|
||||
controSpeed: 30,
|
||||
timeVal: 0,
|
||||
timeMin: 0,
|
||||
timeMax: 1440,
|
||||
presetPos: 1,
|
||||
cruisingSpeed: 100,
|
||||
cruisingTime: 5,
|
||||
cruisingGroup: 0,
|
||||
scanSpeed: 100,
|
||||
scanGroup: 0,
|
||||
tracks: [],
|
||||
tracksLoading: false,
|
||||
showPtz: true,
|
||||
showRrecord: true,
|
||||
tracksNotLoaded: false,
|
||||
sliderTime: 0,
|
||||
seekTime: 0,
|
||||
recordStartTime: 0,
|
||||
showTimeText: "00:00:00",
|
||||
streamInfo: null,
|
||||
broadcastMode: true,
|
||||
broadcastRtc: null,
|
||||
broadcastStatus: -1, // -2 正在释放资源 -1 默认状态 0 等待接通 1 接通成功
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
tabHandleClick: function (tab, event) {
|
||||
console.log(tab)
|
||||
var that = this;
|
||||
that.tracks = [];
|
||||
that.tracksLoading = true;
|
||||
that.tracksNotLoaded = false;
|
||||
if (tab.name === "codec") {
|
||||
this.$axios({
|
||||
method: 'get',
|
||||
url: '/zlm/' + this.mediaServerId + '/index/api/getMediaInfo?vhost=__defaultVhost__&schema=rtsp&app=' + this.app + '&stream=' + this.streamId
|
||||
}).then(function (res) {
|
||||
that.tracksLoading = false;
|
||||
if (res.data.code == 0 && res.data.tracks) {
|
||||
that.tracks = res.data.tracks;
|
||||
} else {
|
||||
that.tracksNotLoaded = true;
|
||||
that.$message({
|
||||
showClose: true,
|
||||
message: '获取编码信息失败,',
|
||||
type: 'warning'
|
||||
});
|
||||
}
|
||||
}).catch(function (e) {
|
||||
});
|
||||
}
|
||||
},
|
||||
changePlayer: function (tab) {
|
||||
console.log(this.player[tab.name][0])
|
||||
this.activePlayer = tab.name;
|
||||
this.videoUrl = this.getUrlByStreamInfo()
|
||||
console.log(this.videoUrl)
|
||||
if (this.$refs[this.activePlayer]) {
|
||||
this.$refs[this.activePlayer].play(this.videoUrl)
|
||||
} else {
|
||||
this.$nextTick(() => {
|
||||
this.$refs[this.activePlayer].play(this.videoUrl)
|
||||
})
|
||||
}
|
||||
},
|
||||
openDialog: function (tab, deviceId, channelId, param) {
|
||||
if (this.showVideoDialog) {
|
||||
return;
|
||||
}
|
||||
this.tabActiveName = tab;
|
||||
this.channelId = channelId;
|
||||
this.deviceId = deviceId;
|
||||
this.streamId = "";
|
||||
this.mediaServerId = "";
|
||||
this.app = "";
|
||||
this.videoUrl = ""
|
||||
if (!!this.$refs[this.activePlayer]) {
|
||||
this.$refs[this.activePlayer].pause();
|
||||
}
|
||||
switch (tab) {
|
||||
case "media":
|
||||
this.play(param.streamInfo, param.hasAudio)
|
||||
break;
|
||||
case "streamPlay":
|
||||
this.tabActiveName = "media";
|
||||
this.showRrecord = false;
|
||||
this.showPtz = false;
|
||||
this.play(param.streamInfo, param.hasAudio)
|
||||
break;
|
||||
case "control":
|
||||
break;
|
||||
}
|
||||
},
|
||||
play: function (streamInfo, hasAudio) {
|
||||
this.streamInfo = streamInfo;
|
||||
this.hasAudio = hasAudio;
|
||||
this.isLoging = false;
|
||||
// this.videoUrl = streamInfo.rtc;
|
||||
this.videoUrl = this.getUrlByStreamInfo();
|
||||
this.streamId = streamInfo.stream;
|
||||
this.app = streamInfo.app;
|
||||
this.mediaServerId = streamInfo.mediaServerId;
|
||||
this.playFromStreamInfo(false, streamInfo)
|
||||
},
|
||||
getUrlByStreamInfo() {
|
||||
console.log(this.streamInfo)
|
||||
let streamInfo = this.streamInfo
|
||||
if (this.streamInfo.transcodeStream) {
|
||||
streamInfo = this.streamInfo.transcodeStream;
|
||||
}
|
||||
if (location.protocol === "https:") {
|
||||
this.videoUrl = streamInfo[this.player[this.activePlayer][1]]
|
||||
} else {
|
||||
this.videoUrl = streamInfo[this.player[this.activePlayer][0]]
|
||||
}
|
||||
return this.videoUrl;
|
||||
|
||||
},
|
||||
|
||||
playFromStreamInfo: function (realHasAudio, streamInfo) {
|
||||
this.showVideoDialog = true;
|
||||
this.hasaudio = realHasAudio && this.hasaudio;
|
||||
if (this.$refs[this.activePlayer]) {
|
||||
this.$refs[this.activePlayer].play(this.getUrlByStreamInfo(streamInfo))
|
||||
}else {
|
||||
this.$nextTick(() => {
|
||||
this.$refs[this.activePlayer].play(this.getUrlByStreamInfo(streamInfo))
|
||||
});
|
||||
}
|
||||
},
|
||||
close: function () {
|
||||
console.log('关闭视频');
|
||||
if (!!this.$refs[this.activePlayer]){
|
||||
this.$refs[this.activePlayer].pause();
|
||||
}
|
||||
this.videoUrl = '';
|
||||
this.coverPlaying = false;
|
||||
this.showVideoDialog = false;
|
||||
this.stopBroadcast()
|
||||
},
|
||||
|
||||
copySharedInfo: function (data) {
|
||||
console.log('复制内容:' + data);
|
||||
this.coverPlaying = false;
|
||||
this.tracks = []
|
||||
let _this = this;
|
||||
this.$copyText(data).then(
|
||||
function (e) {
|
||||
_this.$message({
|
||||
showClose: true,
|
||||
message: '复制成功',
|
||||
type: 'success'
|
||||
});
|
||||
},
|
||||
function (e) {
|
||||
_this.$message({
|
||||
showClose: true,
|
||||
message: '复制失败,请手动复制',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
ptzCamera: function (command) {
|
||||
console.log('云台控制:' + command);
|
||||
this.$axios({
|
||||
method: 'get',
|
||||
url: '/api/jt1078/ptz',
|
||||
params: {
|
||||
phoneNumber: this.deviceId,
|
||||
channelId: this.channelId,
|
||||
command: command,
|
||||
speed: this.controSpeed,
|
||||
|
||||
}
|
||||
}).then(function (res) {
|
||||
});
|
||||
},
|
||||
wiper: function (command) {
|
||||
console.log('雨刷控制:' + command);
|
||||
this.$axios({
|
||||
method: 'get',
|
||||
url: '/api/jt1078/wiper',
|
||||
params: {
|
||||
phoneNumber: this.deviceId,
|
||||
channelId: this.channelId,
|
||||
command: command,
|
||||
}
|
||||
}).then(function (res) {
|
||||
});
|
||||
},
|
||||
fillLight: function (command) {
|
||||
console.log('补光灯开关控制:' + command);
|
||||
this.$axios({
|
||||
method: 'get',
|
||||
url: '/api/jt1078/fill-light',
|
||||
params: {
|
||||
phoneNumber: this.deviceId,
|
||||
channelId: this.channelId,
|
||||
command: command,
|
||||
}
|
||||
}).then(function (res) {
|
||||
});
|
||||
},
|
||||
//////////////////////播放器事件处理//////////////////////////
|
||||
videoError: function (e) {
|
||||
console.log("播放器错误:" + JSON.stringify(e));
|
||||
},
|
||||
presetPosition: function (cmdCode, presetPos) {
|
||||
console.log('预置位控制:' + this.presetPos + ' : 0x' + cmdCode.toString(16));
|
||||
let that = this;
|
||||
this.$axios({
|
||||
method: 'post',
|
||||
url: '/api/ptz/front_end_command/' + this.deviceId + '/' + this.channelId + '?cmdCode=' + cmdCode + '¶meter1=0¶meter2=' + presetPos + '&combindCode2=0'
|
||||
}).then(function (res) {
|
||||
});
|
||||
},
|
||||
setSpeedOrTime: function (cmdCode, groupNum, parameter) {
|
||||
let that = this;
|
||||
let parameter2 = parameter % 256;
|
||||
let combindCode2 = Math.floor(parameter / 256) * 16;
|
||||
console.log('前端控制:0x' + cmdCode.toString(16) + ' 0x' + groupNum.toString(16) + ' 0x' + parameter2.toString(16) + ' 0x' + combindCode2.toString(16));
|
||||
this.$axios({
|
||||
method: 'post',
|
||||
url: '/api/ptz/front_end_command/' + this.deviceId + '/' + this.channelId + '?cmdCode=' + cmdCode + '¶meter1=' + groupNum + '¶meter2=' + parameter2 + '&combindCode2=' + combindCode2
|
||||
}).then(function (res) {
|
||||
});
|
||||
},
|
||||
setCommand: function (cmdCode, groupNum, parameter) {
|
||||
let that = this;
|
||||
console.log('前端控制:0x' + cmdCode.toString(16) + ' 0x' + groupNum.toString(16) + ' 0x' + parameter.toString(16) + ' 0x0');
|
||||
this.$axios({
|
||||
method: 'post',
|
||||
url: '/api/ptz/front_end_command/' + this.deviceId + '/' + this.channelId + '?cmdCode=' + cmdCode + '¶meter1=' + groupNum + '¶meter2=' + parameter + '&combindCode2=0'
|
||||
}).then(function (res) {
|
||||
});
|
||||
},
|
||||
copyUrl: function (dropdownItem) {
|
||||
console.log(dropdownItem)
|
||||
this.$copyText(dropdownItem).then((e) => {
|
||||
this.$message.success({
|
||||
showClose: true,
|
||||
message: "成功拷贝到粘贴板"
|
||||
})
|
||||
}, (e) => {
|
||||
|
||||
})
|
||||
},
|
||||
getBroadcastStatus() {
|
||||
if (this.broadcastStatus == -2) {
|
||||
return "primary"
|
||||
}
|
||||
if (this.broadcastStatus == -1) {
|
||||
return "primary"
|
||||
}
|
||||
if (this.broadcastStatus == 0) {
|
||||
return "warning"
|
||||
}
|
||||
if (this.broadcastStatus == 1) {
|
||||
return "danger"
|
||||
}
|
||||
|
||||
},
|
||||
broadcastStatusClick() {
|
||||
if (this.broadcastStatus == -1) {
|
||||
// 默认状态, 开始
|
||||
this.broadcastStatus = 0
|
||||
// 发起语音对讲
|
||||
this.$axios({
|
||||
method: 'get',
|
||||
url: '/api/play/broadcast/' + this.deviceId + '/' + this.channelId + "?timeout=30&broadcastMode=" + this.broadcastMode
|
||||
}).then((res) => {
|
||||
if (res.data.code === 0) {
|
||||
let streamInfo = res.data.data.streamInfo;
|
||||
if (document.location.protocol.includes("https")) {
|
||||
this.startBroadcast(streamInfo.rtcs)
|
||||
} else {
|
||||
this.startBroadcast(streamInfo.rtc)
|
||||
}
|
||||
} else {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: res.data.msg,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (this.broadcastStatus === 1) {
|
||||
this.broadcastStatus = -1;
|
||||
this.broadcastRtc.close()
|
||||
}
|
||||
},
|
||||
startBroadcast(url) {
|
||||
// 获取推流鉴权Key
|
||||
this.$axios({
|
||||
method: 'post',
|
||||
url: '/api/user/userInfo',
|
||||
}).then((res) => {
|
||||
if (res.data.code !== 0) {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: "获取推流鉴权Key失败",
|
||||
type: "error",
|
||||
});
|
||||
this.broadcastStatus = -1;
|
||||
} else {
|
||||
let pushKey = res.data.data.pushKey;
|
||||
// 获取推流鉴权KEY
|
||||
url += "&sign=" + crypto.createHash('md5').update(pushKey, "utf8").digest('hex')
|
||||
console.log("开始语音喊话: " + url)
|
||||
this.broadcastRtc = new ZLMRTCClient.Endpoint({
|
||||
debug: true, // 是否打印日志
|
||||
zlmsdpUrl: url, //流地址
|
||||
simulecast: false,
|
||||
useCamera: false,
|
||||
audioEnable: true,
|
||||
videoEnable: false,
|
||||
recvOnly: false,
|
||||
})
|
||||
|
||||
// webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS,(e)=>{//获取到了远端流,可以播放
|
||||
// console.error('播放成功',e.streams)
|
||||
// this.broadcastStatus = 1;
|
||||
// });
|
||||
//
|
||||
// webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_ON_LOCAL_STREAM,(s)=>{// 获取到了本地流
|
||||
// this.broadcastStatus = 1;
|
||||
// // document.getElementById('selfVideo').srcObject=s;
|
||||
// // this.eventcallbacK("LOCAL STREAM", "获取到了本地流")
|
||||
// });
|
||||
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_NOT_SUPPORT, (e) => {// 获取到了本地流
|
||||
console.error('不支持webrtc', e)
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '不支持webrtc, 无法进行语音喊话',
|
||||
type: 'error'
|
||||
});
|
||||
this.broadcastStatus = -1;
|
||||
});
|
||||
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, (e) => {// ICE 协商出错
|
||||
console.error('ICE 协商出错')
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: 'ICE 协商出错',
|
||||
type: 'error'
|
||||
});
|
||||
this.broadcastStatus = -1;
|
||||
});
|
||||
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, (e) => {// offer anwser 交换失败
|
||||
console.error('offer anwser 交换失败', e)
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: 'offer anwser 交换失败' + e,
|
||||
type: 'error'
|
||||
});
|
||||
this.broadcastStatus = -1;
|
||||
});
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, (e) => {// offer anwser 交换失败
|
||||
console.log('状态改变', e)
|
||||
if (e === "connecting") {
|
||||
this.broadcastStatus = 0;
|
||||
} else if (e === "connected") {
|
||||
this.broadcastStatus = 1;
|
||||
} else if (e === "disconnected") {
|
||||
this.broadcastStatus = -1;
|
||||
}
|
||||
});
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.CAPTURE_STREAM_FAILED, (e) => {// offer anwser 交换失败
|
||||
console.log('捕获流失败', e)
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '捕获流失败' + e,
|
||||
type: 'error'
|
||||
});
|
||||
this.broadcastStatus = -1;
|
||||
});
|
||||
}
|
||||
}).catch((e) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: e,
|
||||
type: 'error'
|
||||
});
|
||||
this.broadcastStatus = -1;
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
stopBroadcast() {
|
||||
this.broadcastRtc.close();
|
||||
this.broadcastStatus = -1;
|
||||
this.$axios({
|
||||
method: 'get',
|
||||
url: '/api/play/broadcast/stop/' + this.deviceId + '/' + this.channelId
|
||||
}).then((res) => {
|
||||
if (res.data.code == 0) {
|
||||
// this.broadcastStatus = -1;
|
||||
// this.broadcastRtc.close()
|
||||
} else {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: res.data.msg,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.control-wrapper {
|
||||
position: relative;
|
||||
width: 6.25rem;
|
||||
height: 6.25rem;
|
||||
max-width: 6.25rem;
|
||||
max-height: 6.25rem;
|
||||
border-radius: 100%;
|
||||
margin-top: 1.5rem;
|
||||
margin-left: 0.5rem;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 5rem;
|
||||
height: 11rem;
|
||||
max-height: 11rem;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
width: 44%;
|
||||
height: 44%;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #78aee4;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s linear;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.control-btn i {
|
||||
font-size: 20px;
|
||||
color: #78aee4;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.control-btn i:hover {
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.control-zoom-btn:hover {
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.control-round {
|
||||
position: absolute;
|
||||
top: 21%;
|
||||
left: 21%;
|
||||
width: 58%;
|
||||
height: 58%;
|
||||
background: #fff;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.control-round-inner {
|
||||
position: absolute;
|
||||
left: 13%;
|
||||
top: 13%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
font-size: 40px;
|
||||
color: #78aee4;
|
||||
border: 1px solid #78aee4;
|
||||
border-radius: 100%;
|
||||
transition: all 0.3s linear;
|
||||
}
|
||||
|
||||
.control-inner-btn {
|
||||
position: absolute;
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.control-top {
|
||||
top: -8%;
|
||||
left: 27%;
|
||||
transform: rotate(-45deg);
|
||||
border-radius: 5px 100% 5px 0;
|
||||
}
|
||||
|
||||
.control-top i {
|
||||
transform: rotate(45deg);
|
||||
border-radius: 5px 100% 5px 0;
|
||||
}
|
||||
|
||||
.control-top .control-inner {
|
||||
left: -1px;
|
||||
bottom: 0;
|
||||
border-top: 1px solid #78aee4;
|
||||
border-right: 1px solid #78aee4;
|
||||
border-radius: 0 100% 0 0;
|
||||
}
|
||||
|
||||
.control-top .fa {
|
||||
transform: rotate(45deg) translateY(-7px);
|
||||
}
|
||||
|
||||
.control-left {
|
||||
top: 27%;
|
||||
left: -8%;
|
||||
transform: rotate(45deg);
|
||||
border-radius: 5px 0 5px 100%;
|
||||
}
|
||||
|
||||
.control-left i {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.control-left .control-inner {
|
||||
right: -1px;
|
||||
top: -1px;
|
||||
border-bottom: 1px solid #78aee4;
|
||||
border-left: 1px solid #78aee4;
|
||||
border-radius: 0 0 0 100%;
|
||||
}
|
||||
|
||||
.control-left .fa {
|
||||
transform: rotate(-45deg) translateX(-7px);
|
||||
}
|
||||
|
||||
.control-right {
|
||||
top: 27%;
|
||||
right: -8%;
|
||||
transform: rotate(45deg);
|
||||
border-radius: 5px 100% 5px 0;
|
||||
}
|
||||
|
||||
.control-right i {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.control-right .control-inner {
|
||||
left: -1px;
|
||||
bottom: -1px;
|
||||
border-top: 1px solid #78aee4;
|
||||
border-right: 1px solid #78aee4;
|
||||
border-radius: 0 100% 0 0;
|
||||
}
|
||||
|
||||
.control-right .fa {
|
||||
transform: rotate(-45deg) translateX(7px);
|
||||
}
|
||||
|
||||
.control-bottom {
|
||||
left: 27%;
|
||||
bottom: -8%;
|
||||
transform: rotate(45deg);
|
||||
border-radius: 0 5px 100% 5px;
|
||||
}
|
||||
|
||||
.control-bottom i {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.control-bottom .control-inner {
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
border-bottom: 1px solid #78aee4;
|
||||
border-right: 1px solid #78aee4;
|
||||
border-radius: 0 0 100% 0;
|
||||
}
|
||||
|
||||
.control-bottom .fa {
|
||||
transform: rotate(-45deg) translateY(7px);
|
||||
}
|
||||
|
||||
.trank {
|
||||
width: 80%;
|
||||
height: 180px;
|
||||
text-align: left;
|
||||
padding: 0 10%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.trankInfo {
|
||||
width: 80%;
|
||||
padding: 0 10%;
|
||||
}
|
||||
.el-dialog__body{
|
||||
padding: 10px 20px;
|
||||
}
|
||||
.ptz-btn-box {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
height: 3rem;
|
||||
line-height: 4rem;
|
||||
}
|
||||
</style>
|
||||
@ -1,363 +0,0 @@
|
||||
<template>
|
||||
<div id="ptzCruising">
|
||||
<div style="display: grid; grid-template-columns: 80px auto; line-height: 28px">
|
||||
<span>巡航组号: </span>
|
||||
<el-input
|
||||
v-model="tourId"
|
||||
min="1"
|
||||
max="255"
|
||||
placeholder="巡航组号"
|
||||
addon-before="巡航组号"
|
||||
addon-after="(1-255)"
|
||||
size="mini"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
<el-tag
|
||||
v-for="(item, index) in presetList"
|
||||
:key="item.presetId"
|
||||
closable
|
||||
style="margin-right: 1rem; cursor: pointer"
|
||||
@close="delPreset(item, index)"
|
||||
>
|
||||
{{ item.presetName ? item.presetName : item.presetId }}
|
||||
</el-tag>
|
||||
</p>
|
||||
|
||||
<el-form v-if="selectPresetVisible" size="mini" :inline="true">
|
||||
<el-form-item>
|
||||
<el-select v-model="selectPreset" value-key="presetId" placeholder="请选择预置点">
|
||||
<el-option
|
||||
v-for="item in allPresetList"
|
||||
:key="item.presetId"
|
||||
:label="item.presetName ? item.presetName : item.presetId"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="addCruisePoint">保存</el-button>
|
||||
<el-button type="primary" @click="cancelAddCruisePoint">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button v-else size="mini" @click="selectPresetVisible=true">添加巡航点</el-button>
|
||||
|
||||
<el-form v-if="setSpeedVisible" size="mini" :inline="true">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-if="setSpeedVisible"
|
||||
v-model="cruiseSpeed"
|
||||
min="1"
|
||||
max="4095"
|
||||
placeholder="巡航速度"
|
||||
addon-before="巡航速度"
|
||||
addon-after="(1-4095)"
|
||||
size="mini"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="setCruiseSpeed">保存</el-button>
|
||||
<el-button @click="cancelSetCruiseSpeed">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button v-else size="mini" @click="setSpeedVisible = true">设置巡航速度</el-button>
|
||||
<el-form v-if="setTimeVisible" size="mini" :inline="true">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="cruiseTime"
|
||||
min="1"
|
||||
max="4095"
|
||||
placeholder="巡航停留时间(秒)"
|
||||
addon-before="巡航停留时间(秒)"
|
||||
addon-after="(1-4095)"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="setCruiseTime">保存</el-button>
|
||||
<el-button @click="cancelSetCruiseTime">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button v-else size="mini" @click="setTimeVisible = true">设置巡航时间</el-button>
|
||||
<el-button size="mini" @click="startCruise">开始巡航</el-button>
|
||||
<el-button size="mini" @click="stopCruise">停止巡航</el-button>
|
||||
<el-button size="mini" type="danger" @click="deleteCruise">删除巡航</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'PtzCruising',
|
||||
components: {},
|
||||
props: ['channelId'],
|
||||
data() {
|
||||
return {
|
||||
tourId: 1,
|
||||
presetList: [],
|
||||
allPresetList: [],
|
||||
selectPreset: '',
|
||||
inputVisible: false,
|
||||
selectPresetVisible: false,
|
||||
setSpeedVisible: false,
|
||||
setTimeVisible: false,
|
||||
cruiseSpeed: '',
|
||||
cruiseTime: ''
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getPresetList()
|
||||
},
|
||||
methods: {
|
||||
getPresetList: function() {
|
||||
this.$store.dispatch('commonChanel/queryPreset', this.channelId)
|
||||
.then((data) => {
|
||||
this.allPresetList = data
|
||||
})
|
||||
},
|
||||
addCruisePoint: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/addPointForCruise',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
tourId: this.tourId,
|
||||
presetId: this.selectPreset.presetId
|
||||
})
|
||||
.then((data) => {
|
||||
this.presetList.push(this.selectPreset)
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.selectPreset = ''
|
||||
this.selectPresetVisible = false
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
cancelAddCruisePoint: function() {
|
||||
this.selectPreset = ''
|
||||
this.selectPresetVisible = false
|
||||
},
|
||||
delPreset: function(preset, index) {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/deletePointForCruise',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
tourId: this.tourId,
|
||||
presetId: preset.presetId
|
||||
})
|
||||
.then((data) => {
|
||||
this.presetList.splice(index, 1)
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
deleteCruise: function(preset, index) {
|
||||
this.$confirm('确定删除此巡航组', '提示', {
|
||||
dangerouslyUseHTMLString: true,
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/deletePointForCruise',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
tourId: this.tourId,
|
||||
presetId: 0
|
||||
})
|
||||
.then((data) => {
|
||||
this.presetList = []
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
})
|
||||
},
|
||||
setCruiseSpeed: function() {
|
||||
if (this.presetList.length === 0) {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '请添加巡航点',
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/setCruiseSpeed',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
tourId: this.tourId,
|
||||
presetId: this.presetList.at(-1).presetId,
|
||||
speed: this.cruiseSpeed
|
||||
})
|
||||
.then((data) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '保存成功',
|
||||
type: 'success'
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.cruiseSpeed = ''
|
||||
this.setSpeedVisible = false
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
cancelSetCruiseSpeed: function() {
|
||||
this.cruiseSpeed = ''
|
||||
this.setSpeedVisible = false
|
||||
},
|
||||
setCruiseTime: function() {
|
||||
if (this.presetList.length === 0) {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '请添加巡航点',
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/setCruiseTime',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
tourId: this.tourId,
|
||||
time: this.cruiseTime,
|
||||
presetId: this.presetList.at(-1).presetId
|
||||
})
|
||||
.then((data) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '保存成功',
|
||||
type: 'success'
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.setTimeVisible = false
|
||||
this.cruiseTime = ''
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
cancelSetCruiseTime: function() {
|
||||
this.setTimeVisible = false
|
||||
this.cruiseTime = ''
|
||||
},
|
||||
startCruise: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/startCruise',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
tourId: this.tourId
|
||||
})
|
||||
.then((data) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '发送成功',
|
||||
type: 'success'
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.setTimeVisible = false
|
||||
this.cruiseTime = ''
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
stopCruise: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/stopCruise',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
tourId: this.tourId
|
||||
})
|
||||
.then((data) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '发送成功',
|
||||
type: 'success'
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.setTimeVisible = false
|
||||
this.cruiseTime = ''
|
||||
loading.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,162 +0,0 @@
|
||||
<template>
|
||||
<div id="ptzPreset" style="width: 100%">
|
||||
<el-tag
|
||||
v-for="item in presetList"
|
||||
:key="item.presetId"
|
||||
closable
|
||||
size="mini"
|
||||
style="margin-right: 1rem; cursor: pointer; margin-bottom: 0.6rem"
|
||||
@close="delPreset(item)"
|
||||
@click="gotoPreset(item)"
|
||||
>
|
||||
{{ item.presetName?item.presetName:item.presetId }}
|
||||
</el-tag>
|
||||
<el-input
|
||||
v-if="inputVisible"
|
||||
ref="saveTagInput"
|
||||
v-model="ptzPresetId"
|
||||
min="1"
|
||||
max="255"
|
||||
placeholder="预置位编号"
|
||||
addon-before="预置位编号"
|
||||
addon-after="(1-255)"
|
||||
style="width: 300px; vertical-align: bottom;"
|
||||
size="small"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<el-button @click="addPreset()">保存</el-button>
|
||||
<el-button @click="cancel()">取消</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button v-else size="small" @click="showInput">+ 添加</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'PtzPreset',
|
||||
components: {},
|
||||
props: ['channelId'],
|
||||
data() {
|
||||
return {
|
||||
presetList: [],
|
||||
inputVisible: false,
|
||||
ptzPresetId: ''
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getPresetList()
|
||||
},
|
||||
methods: {
|
||||
getPresetList: function() {
|
||||
this.$store.dispatch('commonChanel/queryPreset', this.channelId)
|
||||
.then(data => {
|
||||
this.presetList = data
|
||||
// 防止出现表格错位
|
||||
this.$nextTick(() => {
|
||||
this.$refs.channelListTable.doLayout()
|
||||
})
|
||||
})
|
||||
},
|
||||
showInput() {
|
||||
this.inputVisible = true
|
||||
this.$nextTick(_ => {
|
||||
this.$refs.saveTagInput.$refs.input.focus()
|
||||
})
|
||||
},
|
||||
addPreset: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/addPreset', {
|
||||
channelId: this.channelId,
|
||||
presetId: this.ptzPresetId,
|
||||
presetName: this.ptzPresetId
|
||||
})
|
||||
.then(data => {
|
||||
setTimeout(() => {
|
||||
this.inputVisible = false
|
||||
this.ptzPresetId = ''
|
||||
this.getPresetList()
|
||||
}, 1000)
|
||||
}).catch((error) => {
|
||||
loading.close()
|
||||
this.inputVisible = false
|
||||
this.ptzPresetId = ''
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
cancel: function() {
|
||||
this.inputVisible = false
|
||||
this.ptzPresetId = ''
|
||||
},
|
||||
gotoPreset: function(preset) {
|
||||
console.log(preset)
|
||||
this.$store.dispatch('commonChanel/callPreset', {
|
||||
channelId: this.channelId,
|
||||
presetId: preset.presetId
|
||||
})
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '调用成功',
|
||||
type: 'success'
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
})
|
||||
},
|
||||
delPreset: function(preset) {
|
||||
this.$confirm('确定删除此预置位', '提示', {
|
||||
dangerouslyUseHTMLString: true,
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/deletePreset', {
|
||||
channelId: this.channelId,
|
||||
presetId: preset.presetId
|
||||
})
|
||||
.then(data => {
|
||||
setTimeout(() => {
|
||||
this.getPresetList()
|
||||
}, 1000)
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
}).catch(() => {
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,233 +0,0 @@
|
||||
<template>
|
||||
<div id="ptzScan">
|
||||
<div style="display: grid; grid-template-columns: 80px auto; line-height: 28px">
|
||||
<span>扫描组号: </span>
|
||||
<el-input
|
||||
v-model="scanId"
|
||||
min="1"
|
||||
max="255"
|
||||
placeholder="扫描组号"
|
||||
addon-before="扫描组号"
|
||||
addon-after="(1-255)"
|
||||
size="mini"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-button size="mini" @click="setScanLeft">设置左边界</el-button>
|
||||
<el-button size="mini" @click="setScanRight">设置右边界</el-button>
|
||||
|
||||
<el-form v-if="setSpeedVisible" size="mini" :inline="true">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-if="setSpeedVisible"
|
||||
v-model="speed"
|
||||
min="1"
|
||||
max="4095"
|
||||
placeholder="巡航速度"
|
||||
addon-before="巡航速度"
|
||||
addon-after="(1-4095)"
|
||||
size="mini"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="setSpeed">保存</el-button>
|
||||
<el-button @click="cancelSetSpeed">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button v-else size="mini" @click="setSpeedVisible = true">设置扫描速度</el-button>
|
||||
|
||||
<el-button size="mini" @click="startScan">开始自动扫描</el-button>
|
||||
<el-button size="mini" @click="stopScan">停止自动扫描</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'PtzScan',
|
||||
components: {},
|
||||
props: ['channelId'],
|
||||
data() {
|
||||
return {
|
||||
scanId: 1,
|
||||
setSpeedVisible: false,
|
||||
speed: ''
|
||||
}
|
||||
},
|
||||
created() {
|
||||
},
|
||||
methods: {
|
||||
setSpeed: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/setSpeedForScan',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
scanId: this.scanId,
|
||||
speed: this.speed
|
||||
})
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '保存成功',
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.speed = ''
|
||||
this.setSpeedVisible = false
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
cancelSetSpeed: function() {
|
||||
this.speed = ''
|
||||
this.setSpeedVisible = false
|
||||
},
|
||||
setScanLeft: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/setLeftForScan',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
scanId: this.scanId
|
||||
})
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '保存成功',
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.speed = ''
|
||||
this.setSpeedVisible = false
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
setScanRight: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/setRightForScan',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
scanId: this.scanId
|
||||
})
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '保存成功',
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.speed = ''
|
||||
this.setSpeedVisible = false
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
startScan: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/startScan',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
scanId: this.scanId
|
||||
})
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '发送成功',
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
stopScan: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/stopScan',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
scanId: this.scanId
|
||||
})
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '发送成功',
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.channel-form {
|
||||
display: grid;
|
||||
background-color: #FFFFFF;
|
||||
padding: 1rem 2rem 0 2rem;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -1,72 +0,0 @@
|
||||
<template>
|
||||
<div id="ptzScan">
|
||||
<el-form size="mini" :inline="true">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="auxiliaryId"
|
||||
min="1"
|
||||
max="4095"
|
||||
placeholder="开关编号"
|
||||
addon-before="开关编号"
|
||||
addon-after="(2-255)"
|
||||
size="mini"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button size="mini" @click="open('on')">开启</el-button>
|
||||
<el-button size="mini" @click="open('off')">关闭</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'PtzScan',
|
||||
components: {},
|
||||
props: ['channelId'],
|
||||
data() {
|
||||
return {
|
||||
auxiliaryId: 1
|
||||
}
|
||||
},
|
||||
created() {
|
||||
},
|
||||
methods: {
|
||||
open: function(command) {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/auxiliary',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
command: command,
|
||||
auxiliaryId: this.auxiliaryId
|
||||
})
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '保存成功',
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<div id="ptzWiper">
|
||||
<el-button size="mini" @click="open('on')">开启</el-button>
|
||||
<el-button size="mini" @click="open('off')">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'PtzWiper',
|
||||
components: {},
|
||||
props: ['channelId'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
created() {
|
||||
},
|
||||
methods: {
|
||||
open: function(command) {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('commonChanel/wiper',
|
||||
{
|
||||
channelId: this.channelId,
|
||||
command: command
|
||||
})
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '保存成功',
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.channel-form {
|
||||
display: grid;
|
||||
background-color: #FFFFFF;
|
||||
padding: 1rem 2rem 0 2rem;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div id="h265Player" ref="container" style="background-color: #000000; " @dblclick="fullscreenSwich">
|
||||
<div id="glplayer" ref="playerBox" style="width: 100%; height: 100%; margin: 0 auto;" >
|
||||
<div :id="'h265Player-' + _uid" ref="container" style="background-color: #000000; position: relative; display: flex; align-items: center; justify-content: center;" @dblclick="fullscreenSwich" @mouseenter="showBar = true" @mouseleave="showBar = false">
|
||||
<div :id="'glplayer-' + _uid" ref="playerBox" style="width: 100%; height: 100%; margin: 0 auto;">
|
||||
<div v-if="playerLoading" class="play-loading">
|
||||
<i class="el-icon-loading" />
|
||||
视频加载中
|
||||
<span style="margin-left: 5px">视频加载中</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showButton" id="buttonsBox" class="buttons-box">
|
||||
<div v-if="showButton" id="buttonsBox" class="buttons-box" :style="{ opacity: showBar ? 1 : 0, pointerEvents: showBar ? 'auto' : 'none' }">
|
||||
<div class="buttons-box-left">
|
||||
<i v-if="!playing" class="iconfont icon-play h265web-btn" @click="unPause" />
|
||||
<i v-if="playing" class="iconfont icon-pause h265web-btn" @click="pause" />
|
||||
@ -38,8 +38,10 @@ const h265webPlayer = {}
|
||||
* @see https://github.com/numberwolf/h265web.js/blob/master/example_normal/index.js
|
||||
*/
|
||||
const token = 'base64:QXV0aG9yOmNoYW5neWFubG9uZ3xudW1iZXJ3b2xmLEdpdGh1YjpodHRwczovL2dpdGh1Yi5jb20vbnVtYmVyd29sZixFbWFpbDpwb3JzY2hlZ3QyM0Bmb3htYWlsLmNvbSxRUTo1MzEzNjU4NzIsSG9tZVBhZ2U6aHR0cDovL3h2aWRlby52aWRlbyxEaXNjb3JkOm51bWJlcndvbGYjODY5NCx3ZWNoYXI6bnVtYmVyd29sZjExLEJlaWppbmcsV29ya0luOkJhaWR1'
|
||||
import dragZoom from '../../mixins/dragZoom'
|
||||
export default {
|
||||
name: 'H265web',
|
||||
mixins: [dragZoom],
|
||||
props: ['videoUrl', 'error', 'hasAudio', 'height', 'showButton'],
|
||||
data() {
|
||||
return {
|
||||
@ -60,7 +62,8 @@ export default {
|
||||
playerHeight: 0,
|
||||
inited: false,
|
||||
playerLoading: false,
|
||||
mediaInfo: null
|
||||
mediaInfo: null,
|
||||
showBar: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -119,7 +122,7 @@ export default {
|
||||
const options = {}
|
||||
h265webPlayer[this._uid] = new window.new265webjs(url, Object.assign(
|
||||
{
|
||||
player: 'glplayer', // 播放器容器id
|
||||
player: 'glplayer-' + this._uid,
|
||||
width: this.playerWidth,
|
||||
height: this.playerHeight,
|
||||
token: token,
|
||||
@ -246,6 +249,12 @@ export default {
|
||||
},
|
||||
setPlaybackRate: function(speed) {
|
||||
h265webPlayer[this._uid].setPlaybackRate(speed)
|
||||
},
|
||||
getVideoElement() {
|
||||
return this.$refs.playerBox
|
||||
},
|
||||
getVideoRect() {
|
||||
return this.getVideoElement().getBoundingClientRect()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -264,12 +273,15 @@ export default {
|
||||
}
|
||||
.buttons-box {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
background-color: rgba(43, 51, 63, 0.7);
|
||||
height: 56px;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 1), rgba(0, 0, 0, 0));
|
||||
position: absolute;
|
||||
transition: opacity 0.3s ease;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding-bottom: 10px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
user-select: none;
|
||||
@ -279,7 +291,6 @@ export default {
|
||||
.h265web-btn {
|
||||
width: 20px;
|
||||
color: rgb(255, 255, 255);
|
||||
line-height: 27px;
|
||||
margin: 0px 10px;
|
||||
padding: 0px 2px;
|
||||
cursor: pointer;
|
||||
@ -290,6 +301,7 @@ export default {
|
||||
.buttons-box-right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 10px;
|
||||
}
|
||||
.player-loading {
|
||||
width: fit-content;
|
||||
|
||||
@ -1,38 +1,41 @@
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
style="width:100%; height: 100%; background-color: #000000;margin:0 auto;position: relative;"
|
||||
@dblclick="fullscreenSwich"
|
||||
>
|
||||
<div id="buttonsBox" class="buttons-box" v-if="showButton === undefined || showButton">
|
||||
<div class="buttons-box-left">
|
||||
<i v-if="!playing" class="iconfont icon-play jessibuca-btn" @click="playBtnClick" />
|
||||
<i v-if="playing" class="iconfont icon-pause jessibuca-btn" @click="pause" />
|
||||
<i class="iconfont icon-stop jessibuca-btn" @click="stop" />
|
||||
<i v-if="isNotMute" class="iconfont icon-audio-high jessibuca-btn" @click="mute()" />
|
||||
<i v-if="!isNotMute" class="iconfont icon-audio-mute jessibuca-btn" @click="cancelMute()" />
|
||||
</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"
|
||||
style="font-size: 1rem !important"
|
||||
@click="screenshot"
|
||||
/>
|
||||
<i class="iconfont icon-shuaxin11 jessibuca-btn" @click="playBtnClick" />
|
||||
<i v-if="!fullscreen" class="iconfont icon-weibiaoti10 jessibuca-btn" @click="fullscreenSwich" />
|
||||
<i v-if="fullscreen" class="iconfont icon-weibiaoti11 jessibuca-btn" @click="fullscreenSwich" />
|
||||
<div
|
||||
ref="container"
|
||||
style="width:100%; height: 100%; background-color: #000000;margin:0 auto;position: relative;"
|
||||
@dblclick="fullscreenSwich"
|
||||
@mouseenter="showBar = true" @mouseleave="showBar = false"
|
||||
>
|
||||
<div id="buttonsBox" class="buttons-box" v-if="showButton === undefined || showButton" :style="{ opacity: showBar ? 1 : 0, pointerEvents: showBar ? 'auto' : 'none' }">
|
||||
<div class="buttons-box-left">
|
||||
<i v-if="!playing" class="iconfont icon-play jessibuca-btn" @click="playBtnClick" />
|
||||
<i v-if="playing" class="iconfont icon-pause jessibuca-btn" @click="pause" />
|
||||
<i class="iconfont icon-stop jessibuca-btn" @click="stop" />
|
||||
<i v-if="isNotMute" class="iconfont icon-audio-high jessibuca-btn" @click="mute()" />
|
||||
<i v-if="!isNotMute" class="iconfont icon-audio-mute jessibuca-btn" @click="cancelMute()" />
|
||||
</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"
|
||||
style="font-size: 1rem !important"
|
||||
@click="screenshot"
|
||||
/>
|
||||
<i class="iconfont icon-shuaxin11 jessibuca-btn" @click="playBtnClick" />
|
||||
<i v-if="!fullscreen" class="iconfont icon-weibiaoti10 jessibuca-btn" @click="fullscreenSwich" />
|
||||
<i v-if="fullscreen" class="iconfont icon-weibiaoti11 jessibuca-btn" @click="fullscreenSwich" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const jessibucaPlayer = {}
|
||||
import dragZoom from '../../mixins/dragZoom'
|
||||
export default {
|
||||
name: 'Jessibuca',
|
||||
mixins: [dragZoom],
|
||||
props: ['videoUrl', 'error', 'hasAudio', 'height', 'showButton'],
|
||||
data() {
|
||||
return {
|
||||
@ -51,7 +54,8 @@ export default {
|
||||
rotate: 0,
|
||||
vod: true, // 点播
|
||||
forceNoOffscreen: false,
|
||||
localVideoUrl: this.videoUrl
|
||||
localVideoUrl: this.videoUrl,
|
||||
showBar: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@ -278,6 +282,14 @@ export default {
|
||||
if (jessibucaPlayer[this._uid]) {
|
||||
jessibucaPlayer[this._uid].resize()
|
||||
}
|
||||
},
|
||||
getVideoElement() {
|
||||
return this.$refs.container.querySelector('canvas')
|
||||
},
|
||||
getVideoRect() {
|
||||
const container = this.$refs.container
|
||||
const canvas = this.getVideoElement()
|
||||
return canvas ? canvas.getBoundingClientRect() : container.getBoundingClientRect()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -289,8 +301,7 @@ export default {
|
||||
height: 28px;
|
||||
background-color: rgba(43, 51, 63, 0.7);
|
||||
position: absolute;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
transition: opacity 0.3s ease;
|
||||
display: flex;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
@ -302,7 +313,7 @@ export default {
|
||||
width: 20px;
|
||||
color: rgb(255, 255, 255);
|
||||
line-height: 27px;
|
||||
margin: 0px 10px;
|
||||
margin: 0px 20px;
|
||||
padding: 0px 2px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
|
||||
@ -91,11 +91,6 @@ export default {
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.channel-form {
|
||||
display: grid;
|
||||
background-color: #FFFFFF;
|
||||
padding: 1rem 2rem 0 2rem;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
#mediaInfo { position: relative; }
|
||||
#mediaInfo >>> .el-descriptions__title { font-size: 14px; color: #606266; font-weight: 600; }
|
||||
</style>
|
||||
|
||||
185
web/src/views/common/playerTabs.vue
Normal file
185
web/src/views/common/playerTabs.vue
Normal file
@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="player-tabs-wrapper" ref="playerWrapper">
|
||||
<el-tabs v-if="showTab && playerList.length > 1" v-model="activePlayer" type="card" :stretch="true" @tab-click="changePlayer">
|
||||
<el-tab-pane v-for="p in playerList" :key="p.key" :label="p.label" :name="p.key"></el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="player-video-area" :style="{ height: showTab ? 'calc(100% - 36px)' : '100%' }">
|
||||
<jessibucaPlayer
|
||||
v-if="activePlayer === 'jessibuca'"
|
||||
ref="jessibuca"
|
||||
style="width: 100%; height: 100%;"
|
||||
:has-audio="hasAudio"
|
||||
:show-button="showButton"
|
||||
fluent autoplay live
|
||||
@playTimeChange="$emit('playTimeChange', $event)"
|
||||
@playStatusChange="$emit('playStatusChange', $event)"
|
||||
/>
|
||||
<rtc-player
|
||||
v-if="activePlayer === 'webRTC'"
|
||||
ref="webRTC"
|
||||
style="width: 100%; height: 100%;"
|
||||
:has-audio="hasAudio"
|
||||
:show-button="showButton"
|
||||
fluent autoplay live
|
||||
@playTimeChange="$emit('playTimeChange', $event)"
|
||||
@playStatusChange="$emit('playStatusChange', $event)"
|
||||
/>
|
||||
<h265web
|
||||
v-if="activePlayer === 'h265web'"
|
||||
ref="h265web"
|
||||
style="width: 100%; height: 100%;"
|
||||
:has-audio="hasAudio"
|
||||
:show-button="showButton"
|
||||
fluent autoplay live
|
||||
@playTimeChange="$emit('playTimeChange', $event)"
|
||||
@playStatusChange="$emit('playStatusChange', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import jessibucaPlayer from './jessibuca.vue'
|
||||
import rtcPlayer from './rtcPlayer.vue'
|
||||
import h265web from './h265web.vue'
|
||||
|
||||
export default {
|
||||
name: 'PlayerTabs',
|
||||
components: { jessibucaPlayer, rtcPlayer, h265web },
|
||||
props: {
|
||||
hasAudio: { type: Boolean, default: false },
|
||||
showButton: { type: Boolean, default: true },
|
||||
showTab: { type: Boolean, default: true }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
streamInfo: null,
|
||||
activePlayer: 'jessibuca',
|
||||
player: { jessibuca: ['ws_flv', 'wss_flv'], webRTC: ['rtc', 'rtcs'], h265web: ['ws_flv', 'wss_flv'] },
|
||||
allPlayerList: [
|
||||
{ key: 'jessibuca', label: 'Jessibuca' },
|
||||
{ key: 'webRTC', label: 'WebRTC' },
|
||||
{ key: 'h265web', label: 'H265web' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
playerList() {
|
||||
return this.allPlayerList
|
||||
},
|
||||
playerCount() {
|
||||
return this.playerList.length
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.playerCount === 1) {
|
||||
this.activePlayer = this.playerList[0].key
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getPlayerList() {
|
||||
return this.playerList
|
||||
},
|
||||
getActivePlayer() {
|
||||
return this.activePlayer
|
||||
},
|
||||
switchPlayer(key) {
|
||||
if (this.activePlayer === key) return
|
||||
this.activePlayer = key
|
||||
if (this.streamInfo) {
|
||||
this.play()
|
||||
}
|
||||
},
|
||||
getUrlByStreamInfo() {
|
||||
if (!this.streamInfo) return ''
|
||||
if (location.protocol === 'https:') {
|
||||
return this.streamInfo[this.player[this.activePlayer][1]]
|
||||
}
|
||||
return this.streamInfo[this.player[this.activePlayer][0]]
|
||||
},
|
||||
changePlayer(tab) {
|
||||
this.activePlayer = tab.name
|
||||
this.play()
|
||||
this.$emit('player-changed', this.activePlayer)
|
||||
},
|
||||
setStreamInfo(streamInfo) {
|
||||
this.streamInfo = streamInfo
|
||||
this.play()
|
||||
},
|
||||
play() {
|
||||
let playUrl = this.getUrlByStreamInfo()
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs[this.activePlayer]) {
|
||||
this.$refs[this.activePlayer].play(playUrl)
|
||||
}
|
||||
})
|
||||
const typeMap = { jessibuca: 0, webRTC: 1, h265web: 2 }
|
||||
const type = typeMap[this.activePlayer] || 0
|
||||
const playerUrl = window.location.origin + '/#/play/share?type=' + type + '&url=' + encodeURIComponent(playUrl)
|
||||
this.$emit('playerChanged', { playUrl, playerUrl })
|
||||
},
|
||||
stop() {
|
||||
if (this.$refs[this.activePlayer]) {
|
||||
this.$refs[this.activePlayer].pause()
|
||||
}
|
||||
},
|
||||
pause() {
|
||||
if (this.$refs[this.activePlayer]) {
|
||||
this.$refs[this.activePlayer].pause()
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
const player = this.$refs[this.activePlayer]
|
||||
if (player && player.destroy) {
|
||||
player.destroy()
|
||||
}
|
||||
},
|
||||
setPlaybackRate(rate) {
|
||||
const player = this.$refs[this.activePlayer]
|
||||
if (player && player.setPlaybackRate) {
|
||||
player.setPlaybackRate(rate)
|
||||
}
|
||||
},
|
||||
resize(width, height) {
|
||||
const player = this.$refs[this.activePlayer]
|
||||
if (player && player.resize) {
|
||||
player.resize(width, height)
|
||||
}
|
||||
},
|
||||
screenshot() {
|
||||
const player = this.$refs[this.activePlayer]
|
||||
if (player && player.screenshot) {
|
||||
return player.screenshot()
|
||||
}
|
||||
},
|
||||
getVideoRect() {
|
||||
const player = this.$refs[this.activePlayer]
|
||||
return player && player.getVideoRect ? player.getVideoRect() : null
|
||||
},
|
||||
startDragZoom(callback) {
|
||||
const player = this.$refs[this.activePlayer]
|
||||
if (player && player.startDragZoom) {
|
||||
player.startDragZoom(callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.player-tabs-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.player-tabs-wrapper .el-tabs {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.player-tabs-wrapper .el-tabs >>> .el-tabs__header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.player-video-area {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
}
|
||||
</style>
|
||||
305
web/src/views/common/ptzControls.vue
Normal file
305
web/src/views/common/ptzControls.vue
Normal file
@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<div class="ptz-section-inner">
|
||||
<div class="ptz-top">
|
||||
<div v-if="hasPtzDirection" class="ptz-dpad">
|
||||
<div class="dpad-ring"></div>
|
||||
<button class="dpad-btn card card-up" @mousedown.prevent="handlePtzMove('up')" @mouseup.prevent="handlePtzStop()">▲</button>
|
||||
<button class="dpad-btn card card-right" @mousedown.prevent="handlePtzMove('right')" @mouseup.prevent="handlePtzStop()">▶</button>
|
||||
<button class="dpad-btn card card-down" @mousedown.prevent="handlePtzMove('down')" @mouseup.prevent="handlePtzStop()">▼</button>
|
||||
<button class="dpad-btn card card-left" @mousedown.prevent="handlePtzMove('left')" @mouseup.prevent="handlePtzStop()">◀</button>
|
||||
<button v-if="showDiagonals" class="dpad-btn diag diag-upright" @mousedown.prevent="handlePtzMove('upright')" @mouseup.prevent="handlePtzStop()"><span style="display:inline-block;transform:rotate(45deg)">▲</span></button>
|
||||
<button v-if="showDiagonals" class="dpad-btn diag diag-downright" @mousedown.prevent="handlePtzMove('downright')" @mouseup.prevent="handlePtzStop()"><span style="display:inline-block;transform:rotate(135deg)">▲</span></button>
|
||||
<button v-if="showDiagonals" class="dpad-btn diag diag-downleft" @mousedown.prevent="handlePtzMove('downleft')" @mouseup.prevent="handlePtzStop()"><span style="display:inline-block;transform:rotate(225deg)">▲</span></button>
|
||||
<button v-if="showDiagonals" class="dpad-btn diag diag-upleft" @mousedown.prevent="handlePtzMove('upleft')" @mouseup.prevent="handlePtzStop()"><span style="display:inline-block;transform:rotate(-45deg)">▲</span></button>
|
||||
<button class="dpad-btn dpad-center" title="停止" @click="$emit('ptz-stop')">⏹</button>
|
||||
</div>
|
||||
<div class="ptz-func-col">
|
||||
<div class="ptz-func-group" :class="{ row: btnLayout === 'row' }">
|
||||
<div class="ptz-func-row" v-if="homePosition && hasGuard">
|
||||
<div class="ptz-func-row">
|
||||
<div class="ptz-func-btn" title="看守位" @click.prevent="$emit('ptz-guard')">
|
||||
<i class="el-icon-s-home" /><span>看守位</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasPtzDirection" class="ptz-func-row">
|
||||
<div class="ptz-func-btn" title="变倍+" @mousedown.prevent="handlePtzMove('zoomin')" @mouseup.prevent="handlePtzStop()">
|
||||
<i class="el-icon-zoom-in" /><span>变倍+</span>
|
||||
</div>
|
||||
<div class="ptz-func-btn" title="变倍-" @mousedown.prevent="handlePtzMove('zoomout')" @mouseup.prevent="handlePtzStop()">
|
||||
<i class="el-icon-zoom-out" /><span>变倍-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasFocus" class="ptz-func-row">
|
||||
<div class="ptz-func-btn" title="聚焦+" @mousedown.prevent="$emit('focus-move', { command: 'near', speed: controSpeed })" @mouseup.prevent="$emit('focus-stop')">
|
||||
<i class="iconfont icon-bianjiao-fangda" /><span>聚焦+</span>
|
||||
</div>
|
||||
<div class="ptz-func-btn" title="聚焦-" @mousedown.prevent="$emit('focus-move', { command: 'far', speed: controSpeed })" @mouseup.prevent="$emit('focus-stop')">
|
||||
<i class="iconfont icon-bianjiao-suoxiao" /><span>聚焦-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasIris" class="ptz-func-row">
|
||||
<div class="ptz-func-btn" title="光圈+" @mousedown.prevent="$emit('iris-move', { command: 'in', speed: controSpeed })" @mouseup.prevent="$emit('iris-stop')">
|
||||
<i class="iconfont icon-guangquan" /><span>光圈+</span>
|
||||
</div>
|
||||
<div class="ptz-func-btn" title="光圈-" @mousedown.prevent="$emit('iris-move', { command: 'out', speed: controSpeed })" @mouseup.prevent="$emit('iris-stop')">
|
||||
<i class="iconfont icon-guangquan-" /><span>光圈-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasDragZoom" class="ptz-func-row">
|
||||
<div class="ptz-func-btn" title="拉框放大" @click="$emit('toggle-drag-zoom')">
|
||||
<i class="iconfont icon-guangquan" /><span>拉框放大</span>
|
||||
</div>
|
||||
<div class="ptz-func-btn" title="拉框缩小" @click="$emit('toggle-drag-zoom-out')">
|
||||
<i class="iconfont icon-guangquan-" /><span>拉框缩小</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasAnyPtz" class="ptz-bottom">
|
||||
<div class="slider-with-controls">
|
||||
<span class="slider-label">速度</span>
|
||||
<el-button type="text" icon="el-icon-minus" class="slider-btn" @click="adjustSpeed(-1)" />
|
||||
<el-slider v-model="controSpeed" :max="100" :min="1" />
|
||||
<el-button type="text" icon="el-icon-plus" class="slider-btn" @click="adjustSpeed(1)" />
|
||||
<span class="slider-value">{{ controSpeed }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PtzControls',
|
||||
props: {
|
||||
btnLayout: { type: String, default: 'column' },
|
||||
homePosition: { type: Boolean, default: false },
|
||||
showDiagonals: { type: Boolean, default: true }
|
||||
},
|
||||
computed: {
|
||||
hasPtzDirection() { return 'ptz-move' in this.$listeners },
|
||||
hasFocus() { return 'focus-move' in this.$listeners },
|
||||
hasIris() { return 'iris-move' in this.$listeners },
|
||||
hasDragZoom() { return 'toggle-drag-zoom' in this.$listeners || 'toggle-drag-zoom-out' in this.$listeners },
|
||||
hasGuard() { return 'ptz-guard' in this.$listeners },
|
||||
hasAnyPtz() { return this.hasPtzDirection || this.hasFocus || this.hasIris || this.hasDragZoom || this.hasGuard }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
controSpeed: 50,
|
||||
currentCommand: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('mouseup', this.onWindowMouseUp)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('mouseup', this.onWindowMouseUp)
|
||||
},
|
||||
methods: {
|
||||
adjustSpeed(delta) {
|
||||
const newVal = this.controSpeed + delta
|
||||
if (newVal >= 1 && newVal <= 100) {
|
||||
this.controSpeed = newVal
|
||||
}
|
||||
},
|
||||
handlePtzMove(direction) {
|
||||
this.currentCommand = direction
|
||||
this.$emit('ptz-move', { direction, speed: this.controSpeed })
|
||||
},
|
||||
handlePtzStop() {
|
||||
this.$emit('ptz-stop', { direction: this.currentCommand })
|
||||
this.currentCommand = null
|
||||
},
|
||||
onWindowMouseUp() {
|
||||
if (this.currentCommand) {
|
||||
this.handlePtzStop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ptz-section-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 4px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.ptz-top {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.ptz-dpad {
|
||||
position: relative;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
flex: none;
|
||||
}
|
||||
.dpad-ring {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
border-radius: 50%;
|
||||
background: #f5f7fa;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dpad-btn {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
user-select: none;
|
||||
transition: all 0.15s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.card {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
font-size: 18px;
|
||||
color: #303133;
|
||||
}
|
||||
.card:hover {
|
||||
background: #409EFF;
|
||||
color: #fff;
|
||||
box-shadow: 0 3px 10px rgba(64,158,255,0.4);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.card:active {
|
||||
background: #337ecc;
|
||||
transform: scale(0.92);
|
||||
}
|
||||
.card-up { top: 18px; left: 67px; }
|
||||
.card-right { top: 67px; left: 116px; }
|
||||
.card-down { top: 116px; left: 67px; }
|
||||
.card-left { top: 67px; left: 18px; }
|
||||
.diag {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
color: #a8abb2;
|
||||
}
|
||||
.diag:hover {
|
||||
background: #409EFF;
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(64,158,255,0.35);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.diag:active {
|
||||
background: #337ecc;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
.diag-upright { top: 40px; left: 110px; }
|
||||
.diag-downright { top: 110px; left: 110px; }
|
||||
.diag-downleft { top: 110px; left: 34px; }
|
||||
.diag-upleft { top: 40px; left: 34px; }
|
||||
.dpad-center {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #eef0f4, #e0e3e8);
|
||||
font-size: 20px;
|
||||
color: #909399;
|
||||
line-height: 1;
|
||||
}
|
||||
.dpad-center:hover {
|
||||
background: #409EFF;
|
||||
color: #fff;
|
||||
box-shadow: 0 3px 10px rgba(64,158,255,0.4);
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
}
|
||||
.dpad-center:active {
|
||||
background: #337ecc;
|
||||
transform: translate(-50%, -50%) scale(0.92);
|
||||
}
|
||||
.ptz-func-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.ptz-func-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
.ptz-func-row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
.ptz-func-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 44px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
user-select: none;
|
||||
font-size: 12px;
|
||||
}
|
||||
.ptz-func-btn:hover {
|
||||
background: #409EFF;
|
||||
color: #fff;
|
||||
}
|
||||
.ptz-func-btn:active {
|
||||
background: #337ecc;
|
||||
}
|
||||
.ptz-func-btn i { font-size: 14px; margin-bottom: 2px; }
|
||||
.ptz-func-group.row .ptz-func-btn {
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
}
|
||||
.ptz-func-group.row .ptz-func-btn i {
|
||||
margin-bottom: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.ptz-bottom {
|
||||
margin-top: 12px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.slider-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.slider-btn {
|
||||
font-weight: bold;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.slider-with-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.slider-with-controls .el-slider {
|
||||
flex: 1;
|
||||
}
|
||||
.slider-value {
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
@ -1,328 +0,0 @@
|
||||
<template>
|
||||
<div id="ptzCruising">
|
||||
<div style="display: grid; grid-template-columns: 80px auto; line-height: 28px">
|
||||
<span>巡航组号: </span>
|
||||
<el-input
|
||||
v-model="cruiseId"
|
||||
min="1"
|
||||
max="255"
|
||||
placeholder="巡航组号"
|
||||
addon-before="巡航组号"
|
||||
addon-after="(1-255)"
|
||||
size="mini"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
<el-tag
|
||||
v-for="(item, index) in presetList"
|
||||
:key="item.presetId"
|
||||
closable
|
||||
style="margin-right: 1rem; cursor: pointer"
|
||||
@close="delPreset(item, index)"
|
||||
>
|
||||
{{ item.presetName ? item.presetName : item.presetId }}
|
||||
</el-tag>
|
||||
</p>
|
||||
|
||||
<el-form v-if="selectPresetVisible" size="mini" :inline="true">
|
||||
<el-form-item>
|
||||
<el-select v-model="selectPreset" value-key="presetId" placeholder="请选择预置点">
|
||||
<el-option
|
||||
v-for="item in allPresetList"
|
||||
:key="item.presetId"
|
||||
:label="item.presetName ? item.presetName : item.presetId"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="addCruisePoint">保存</el-button>
|
||||
<el-button type="primary" @click="cancelAddCruisePoint">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button v-else size="mini" @click="selectPresetVisible=true">添加巡航点</el-button>
|
||||
|
||||
<el-form v-if="setSpeedVisible" size="mini" :inline="true">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-if="setSpeedVisible"
|
||||
v-model="cruiseSpeed"
|
||||
min="1"
|
||||
max="4095"
|
||||
placeholder="巡航速度"
|
||||
addon-before="巡航速度"
|
||||
addon-after="(1-4095)"
|
||||
size="mini"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="setCruiseSpeed">保存</el-button>
|
||||
<el-button @click="cancelSetCruiseSpeed">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button v-else size="mini" @click="setSpeedVisible = true">设置巡航速度</el-button>
|
||||
<el-form v-if="setTimeVisible" size="mini" :inline="true">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="cruiseTime"
|
||||
min="1"
|
||||
max="4095"
|
||||
placeholder="巡航停留时间(秒)"
|
||||
addon-before="巡航停留时间(秒)"
|
||||
addon-after="(1-4095)"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="setCruiseTime">保存</el-button>
|
||||
<el-button @click="cancelSetCruiseTime">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button v-else size="mini" @click="setTimeVisible = true">设置巡航时间</el-button>
|
||||
<el-button size="mini" @click="startCruise">开始巡航</el-button>
|
||||
<el-button size="mini" @click="stopCruise">停止巡航</el-button>
|
||||
<el-button size="mini" type="danger" @click="deleteCruise">删除巡航</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'PtzCruising',
|
||||
components: {},
|
||||
props: ['channelDeviceId', 'deviceId'],
|
||||
data() {
|
||||
return {
|
||||
cruiseId: 1,
|
||||
presetList: [],
|
||||
allPresetList: [],
|
||||
selectPreset: '',
|
||||
inputVisible: false,
|
||||
selectPresetVisible: false,
|
||||
setSpeedVisible: false,
|
||||
setTimeVisible: false,
|
||||
cruiseSpeed: '',
|
||||
cruiseTime: ''
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getPresetList()
|
||||
},
|
||||
methods: {
|
||||
getPresetList: function() {
|
||||
this.$store.dispatch('frontEnd/queryPreset', [this.deviceId, this.channelDeviceId])
|
||||
.then((data) => {
|
||||
this.allPresetList = data
|
||||
})
|
||||
},
|
||||
addCruisePoint: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/addPointForCruise',
|
||||
[this.deviceId, this.channelDeviceId, this.cruiseId, this.selectPreset.presetId])
|
||||
.then((data) => {
|
||||
this.presetList.push(this.selectPreset)
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.selectPreset = ''
|
||||
this.selectPresetVisible = false
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
cancelAddCruisePoint: function() {
|
||||
this.selectPreset = ''
|
||||
this.selectPresetVisible = false
|
||||
},
|
||||
delPreset: function(preset, index) {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/deletePointForCruise',
|
||||
[this.deviceId, this.channelDeviceId, this.cruiseId, preset.presetId])
|
||||
.then((data) => {
|
||||
this.presetList.splice(index, 1)
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
deleteCruise: function(preset, index) {
|
||||
this.$confirm('确定删除此巡航组', '提示', {
|
||||
dangerouslyUseHTMLString: true,
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/deletePointForCruise',
|
||||
[this.deviceId, this.channelDeviceId, this.cruiseId, 0])
|
||||
.then((data) => {
|
||||
this.presetList = []
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
})
|
||||
},
|
||||
setCruiseSpeed: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/setCruiseSpeed',
|
||||
[this.deviceId, this.channelDeviceId, this.cruiseId, this.cruiseSpeed])
|
||||
.then((data) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '保存成功',
|
||||
type: 'success'
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.cruiseSpeed = ''
|
||||
this.setSpeedVisible = false
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
cancelSetCruiseSpeed: function() {
|
||||
this.cruiseSpeed = ''
|
||||
this.setSpeedVisible = false
|
||||
},
|
||||
setCruiseTime: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/setCruiseTime',
|
||||
[this.deviceId, this.channelDeviceId, this.cruiseId, this.cruiseTime])
|
||||
.then((data) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '保存成功',
|
||||
type: 'success'
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.setTimeVisible = false
|
||||
this.cruiseTime = ''
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
cancelSetCruiseTime: function() {
|
||||
this.setTimeVisible = false
|
||||
this.cruiseTime = ''
|
||||
},
|
||||
startCruise: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/startCruise',
|
||||
[this.deviceId, this.channelDeviceId, this.cruiseId])
|
||||
.then((data) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '发送成功',
|
||||
type: 'success'
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.setTimeVisible = false
|
||||
this.cruiseTime = ''
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
stopCruise: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/stopCruise',
|
||||
[this.deviceId, this.channelDeviceId, this.cruiseId])
|
||||
.then((data) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '发送成功',
|
||||
type: 'success'
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.setTimeVisible = false
|
||||
this.cruiseTime = ''
|
||||
loading.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.channel-form {
|
||||
display: grid;
|
||||
background-color: #FFFFFF;
|
||||
padding: 1rem 2rem 0 2rem;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
@ -1,152 +0,0 @@
|
||||
<template>
|
||||
<div id="ptzPreset" style="width: 100%">
|
||||
<el-tag
|
||||
v-for="item in presetList"
|
||||
:key="item.presetId"
|
||||
closable
|
||||
size="mini"
|
||||
style="margin-right: 1rem; cursor: pointer; margin-bottom: 0.6rem"
|
||||
@close="delPreset(item)"
|
||||
@click="gotoPreset(item)"
|
||||
>
|
||||
{{ item.presetName?item.presetName:item.presetId }}
|
||||
</el-tag>
|
||||
<el-input
|
||||
v-if="inputVisible"
|
||||
ref="saveTagInput"
|
||||
v-model="ptzPresetId"
|
||||
min="1"
|
||||
max="255"
|
||||
placeholder="预置位编号"
|
||||
addon-before="预置位编号"
|
||||
addon-after="(1-255)"
|
||||
style="width: 300px; vertical-align: bottom;"
|
||||
size="small"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<el-button @click="addPreset()">保存</el-button>
|
||||
<el-button @click="cancel()">取消</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button v-else size="small" @click="showInput">+ 添加</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'PtzPreset',
|
||||
components: {},
|
||||
props: ['channelDeviceId', 'deviceId'],
|
||||
data() {
|
||||
return {
|
||||
presetList: [],
|
||||
inputVisible: false,
|
||||
ptzPresetId: ''
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getPresetList()
|
||||
},
|
||||
methods: {
|
||||
getPresetList: function() {
|
||||
this.$store.dispatch('frontEnd/queryPreset', [this.deviceId, this.channelDeviceId])
|
||||
.then(data => {
|
||||
this.presetList = data
|
||||
// 防止出现表格错位
|
||||
this.$nextTick(() => {
|
||||
this.$refs.channelListTable.doLayout()
|
||||
})
|
||||
})
|
||||
},
|
||||
showInput() {
|
||||
this.inputVisible = true
|
||||
this.$nextTick(_ => {
|
||||
this.$refs.saveTagInput.$refs.input.focus()
|
||||
})
|
||||
},
|
||||
addPreset: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/addPreset', [this.deviceId, this.channelDeviceId, this.ptzPresetId])
|
||||
.then(data => {
|
||||
setTimeout(() => {
|
||||
this.inputVisible = false
|
||||
this.ptzPresetId = ''
|
||||
this.getPresetList()
|
||||
}, 1000)
|
||||
}).catch((error) => {
|
||||
loading.close()
|
||||
this.inputVisible = false
|
||||
this.ptzPresetId = ''
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
cancel: function() {
|
||||
this.inputVisible = false
|
||||
this.ptzPresetId = ''
|
||||
},
|
||||
gotoPreset: function(preset) {
|
||||
console.log(preset)
|
||||
this.$store.dispatch('frontEnd/callPreset', [this.deviceId, this.channelDeviceId, preset.presetId])
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '调用成功',
|
||||
type: 'success'
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
})
|
||||
},
|
||||
delPreset: function(preset) {
|
||||
this.$confirm('确定删除此预置位', '提示', {
|
||||
dangerouslyUseHTMLString: true,
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/deletePreset', [this.deviceId, this.channelDeviceId, preset.presetId])
|
||||
.then(data => {
|
||||
setTimeout(() => {
|
||||
this.getPresetList()
|
||||
}, 1000)
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
}).catch(() => {
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,212 +0,0 @@
|
||||
<template>
|
||||
<div id="ptzScan">
|
||||
<div style="display: grid; grid-template-columns: 80px auto; line-height: 28px">
|
||||
<span>扫描组号: </span>
|
||||
<el-input
|
||||
v-model="scanId"
|
||||
min="1"
|
||||
max="255"
|
||||
placeholder="扫描组号"
|
||||
addon-before="扫描组号"
|
||||
addon-after="(1-255)"
|
||||
size="mini"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-button size="mini" @click="setScanLeft">设置左边界</el-button>
|
||||
<el-button size="mini" @click="setScanRight">设置右边界</el-button>
|
||||
|
||||
<el-form v-if="setSpeedVisible" size="mini" :inline="true">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-if="setSpeedVisible"
|
||||
v-model="speed"
|
||||
min="1"
|
||||
max="4095"
|
||||
placeholder="巡航速度"
|
||||
addon-before="巡航速度"
|
||||
addon-after="(1-4095)"
|
||||
size="mini"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="setSpeed">保存</el-button>
|
||||
<el-button @click="cancelSetSpeed">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button v-else size="mini" @click="setSpeedVisible = true">设置扫描速度</el-button>
|
||||
|
||||
<el-button size="mini" @click="startScan">开始自动扫描</el-button>
|
||||
<el-button size="mini" @click="stopScan">停止自动扫描</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'PtzScan',
|
||||
components: {},
|
||||
props: ['channelDeviceId', 'deviceId'],
|
||||
data() {
|
||||
return {
|
||||
scanId: 1,
|
||||
setSpeedVisible: false,
|
||||
speed: ''
|
||||
}
|
||||
},
|
||||
created() {
|
||||
},
|
||||
methods: {
|
||||
setSpeed: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/setSpeedForScan', [this.deviceId, this.channelDeviceId, this.scanId, this.speed])
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '保存成功',
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.speed = ''
|
||||
this.setSpeedVisible = false
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
cancelSetSpeed: function() {
|
||||
this.speed = ''
|
||||
this.setSpeedVisible = false
|
||||
},
|
||||
setScanLeft: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/setLeftForScan', [this.deviceId, this.channelDeviceId, this.scanId])
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '保存成功',
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.speed = ''
|
||||
this.setSpeedVisible = false
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
setScanRight: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/setRightForScan', [this.deviceId, this.channelDeviceId, this.scanId])
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '保存成功',
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
this.speed = ''
|
||||
this.setSpeedVisible = false
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
startScan: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/startScan', [this.deviceId, this.channelDeviceId, this.scanId])
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '发送成功',
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
stopScan: function() {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/stopScan', [this.deviceId, this.channelDeviceId, this.scanId])
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '发送成功',
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.channel-form {
|
||||
display: grid;
|
||||
background-color: #FFFFFF;
|
||||
padding: 1rem 2rem 0 2rem;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -1,76 +0,0 @@
|
||||
<template>
|
||||
<div id="ptzScan">
|
||||
<el-form size="mini" :inline="true">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="switchId"
|
||||
min="1"
|
||||
max="4095"
|
||||
placeholder="开关编号"
|
||||
addon-before="开关编号"
|
||||
addon-after="(2-255)"
|
||||
size="mini"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button size="mini" @click="open('on')">开启</el-button>
|
||||
<el-button size="mini" @click="open('off')">关闭</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'PtzScan',
|
||||
components: {},
|
||||
props: ['channelDeviceId', 'deviceId'],
|
||||
data() {
|
||||
return {
|
||||
switchId: 1
|
||||
}
|
||||
},
|
||||
created() {
|
||||
},
|
||||
methods: {
|
||||
open: function(command) {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/auxiliary', [this.deviceId, this.channelDeviceId, command, this.switchId])
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '保存成功',
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.channel-form {
|
||||
display: grid;
|
||||
background-color: #FFFFFF;
|
||||
padding: 1rem 2rem 0 2rem;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<div id="ptzWiper">
|
||||
<el-button size="mini" @click="open('on')">开启</el-button>
|
||||
<el-button size="mini" @click="open('off')">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'PtzWiper',
|
||||
components: {},
|
||||
props: ['channelDeviceId', 'deviceId'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
created() {
|
||||
},
|
||||
methods: {
|
||||
open: function(command) {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
fullscreen: true,
|
||||
text: '正在发送指令',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
this.$store.dispatch('frontEnd/wiper', [this.deviceId, this.channelDeviceId, command])
|
||||
.then(data => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '保存成功',
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.channel-form {
|
||||
display: grid;
|
||||
background-color: #FFFFFF;
|
||||
padding: 1rem 2rem 0 2rem;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -1,15 +1,17 @@
|
||||
<template>
|
||||
<div id="rtcPlayer">
|
||||
<video id="webRtcPlayerBox" :controls="showControls" autoplay style="text-align:left;">
|
||||
<div :id="'rtcPlayer-' + _uid" class="rtc-player-wrapper">
|
||||
<video :id="'webRtcPlayerBox-' + _uid" class="rtc-player-video" :controls="showControls" autoplay style="text-align:left;">
|
||||
Your browser is too old which doesn't support HTML5 video.
|
||||
</video>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
let webrtcPlayer = null
|
||||
const webrtcPlayer = {}
|
||||
import dragZoom from '../../mixins/dragZoom'
|
||||
export default {
|
||||
name: 'RtcPlayer',
|
||||
mixins: [dragZoom],
|
||||
props: {
|
||||
videoUrl: { type: String, default: '' },
|
||||
error: { default: '' },
|
||||
@ -21,20 +23,20 @@ export default {
|
||||
timer: null
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {},
|
||||
destroyed() {
|
||||
clearTimeout(this.timer)
|
||||
this.pause()
|
||||
},
|
||||
methods: {
|
||||
play: function(url) {
|
||||
if (webrtcPlayer != null) {
|
||||
if (webrtcPlayer[this._uid]) {
|
||||
this.pause()
|
||||
}
|
||||
webrtcPlayer = new ZLMRTCClient.Endpoint({
|
||||
element: document.getElementById('webRtcPlayerBox'), // video 标签
|
||||
debug: true, // 是否打印日志
|
||||
zlmsdpUrl: url, // 流地址
|
||||
webrtcPlayer[this._uid] = new ZLMRTCClient.Endpoint({
|
||||
element: document.getElementById('webRtcPlayerBox-' + this._uid),
|
||||
debug: true,
|
||||
zlmsdpUrl: url,
|
||||
simulecast: false,
|
||||
useCamera: false,
|
||||
audioEnable: true,
|
||||
@ -42,37 +44,37 @@ export default {
|
||||
recvOnly: true,
|
||||
usedatachannel: false
|
||||
})
|
||||
webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, (e) => { // ICE 协商出错
|
||||
const player = webrtcPlayer[this._uid]
|
||||
player.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, (e) => {
|
||||
console.error('ICE 协商出错')
|
||||
this.eventcallbacK('ICE ERROR', 'ICE 协商出错')
|
||||
})
|
||||
|
||||
webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, (e) => { // 获取到了远端流,可以播放
|
||||
player.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, (e) => {
|
||||
console.log('播放成功', e.streams)
|
||||
this.eventcallbacK('playing', '播放成功')
|
||||
})
|
||||
|
||||
webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, (e) => { // offer anwser 交换失败
|
||||
player.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, (e) => {
|
||||
console.error('offer anwser 交换失败', e)
|
||||
this.eventcallbacK('OFFER ANSWER ERROR ', 'offer anwser 交换失败')
|
||||
if (e.code == -400 && e.msg == '流不存在') {
|
||||
console.log('流不存在')
|
||||
this.timer = setTimeout(() => {
|
||||
this.webrtcPlayer.close()
|
||||
player.close()
|
||||
this.play(url)
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
|
||||
webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_ON_LOCAL_STREAM, (s) => { // 获取到了本地流
|
||||
// document.getElementById('selfVideo').srcObject=s;
|
||||
player.on(ZLMRTCClient.Events.WEBRTC_ON_LOCAL_STREAM, (s) => {
|
||||
this.eventcallbacK('LOCAL STREAM', '获取到了本地流')
|
||||
})
|
||||
},
|
||||
pause: function() {
|
||||
if (webrtcPlayer != null) {
|
||||
webrtcPlayer.close()
|
||||
webrtcPlayer = null
|
||||
if (webrtcPlayer[this._uid]) {
|
||||
webrtcPlayer[this._uid].close()
|
||||
webrtcPlayer[this._uid] = null
|
||||
}
|
||||
},
|
||||
stop: function() {
|
||||
@ -82,6 +84,35 @@ export default {
|
||||
console.log('player 事件回调')
|
||||
console.log(type)
|
||||
console.log(message)
|
||||
},
|
||||
getVideoElement() {
|
||||
return document.getElementById('webRtcPlayerBox-' + this._uid)
|
||||
},
|
||||
getVideoRect() {
|
||||
const video = this.getVideoElement()
|
||||
const rect = video.getBoundingClientRect()
|
||||
if (video.videoWidth && video.videoHeight) {
|
||||
const natRatio = video.videoWidth / video.videoHeight
|
||||
const disRatio = rect.width / rect.height
|
||||
let w, h, x, y
|
||||
if (natRatio > disRatio) {
|
||||
w = rect.width
|
||||
h = w / natRatio
|
||||
x = 0
|
||||
y = (rect.height - h) / 2
|
||||
} else {
|
||||
h = rect.height
|
||||
w = h * natRatio
|
||||
x = (rect.width - w) / 2
|
||||
y = 0
|
||||
}
|
||||
return {
|
||||
left: rect.left + x, top: rect.top + y,
|
||||
right: rect.left + x + w, bottom: rect.top + y + h,
|
||||
width: w, height: h
|
||||
}
|
||||
}
|
||||
return rect
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -91,11 +122,12 @@ export default {
|
||||
.LodingTitle {
|
||||
min-width: 70px;
|
||||
}
|
||||
#rtcPlayer{
|
||||
.rtc-player-wrapper{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
#webRtcPlayerBox{
|
||||
.rtc-player-video{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
75
web/src/views/common/streamMediaPanel.vue
Normal file
75
web/src/views/common/streamMediaPanel.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="media-info-content">
|
||||
<el-form label-width="90px" size="small">
|
||||
<el-form-item label="播放地址">
|
||||
<el-input v-model="playerUrl" :disabled="true">
|
||||
<template slot="append">
|
||||
<i class="cpoy-btn el-icon-document-copy" title="点击拷贝" style="cursor: pointer" @click="copyUrl(playerUrl)" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="iframe">
|
||||
<el-input v-model="sharedIframe" :disabled="true" >
|
||||
<template slot="append">
|
||||
<i class="cpoy-btn el-icon-document-copy" title="点击拷贝" style="cursor: pointer" @click="copyUrl(sharedIframe)" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="资源地址">
|
||||
<el-input v-model="playUrl" :disabled="true" size="mini">
|
||||
<el-button slot="append" icon="el-icon-document-copy" title="点击拷贝" style="cursor: pointer" @click="copyUrl(playUrl)" />
|
||||
<el-dropdown v-if="streamInfo" slot="prepend" trigger="click" @command="copyUrl">
|
||||
<el-button>更多地址<i class="el-icon-arrow-down el-icon--right" size="mini"/></el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item v-if="streamInfo.flv" :command="streamInfo.flv"><el-tag>FLV:</el-tag><span>{{ streamInfo.flv }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.https_flv" :command="streamInfo.https_flv"><el-tag>FLV(https):</el-tag><span>{{ streamInfo.https_flv }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.ws_flv" :command="streamInfo.ws_flv"><el-tag>FLV(ws):</el-tag><span>{{ streamInfo.ws_flv }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.wss_flv" :command="streamInfo.wss_flv"><el-tag>FLV(wss):</el-tag><span>{{ streamInfo.wss_flv }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.fmp4" :command="streamInfo.fmp4"><el-tag>FMP4:</el-tag><span>{{ streamInfo.fmp4 }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.https_fmp4" :command="streamInfo.https_fmp4"><el-tag>FMP4(https):</el-tag><span>{{ streamInfo.https_fmp4 }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.ws_fmp4" :command="streamInfo.ws_fmp4"><el-tag>FMP4(ws):</el-tag><span>{{ streamInfo.ws_fmp4 }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.wss_fmp4" :command="streamInfo.wss_fmp4"><el-tag>FMP4(wss):</el-tag><span>{{ streamInfo.wss_fmp4 }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.hls" :command="streamInfo.hls"><el-tag>HLS:</el-tag><span>{{ streamInfo.hls }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.https_hls" :command="streamInfo.https_hls"><el-tag>HLS(https):</el-tag><span>{{ streamInfo.https_hls }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.ws_hls" :command="streamInfo.ws_hls"><el-tag>HLS(ws):</el-tag><span>{{ streamInfo.ws_hls }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.wss_hls" :command="streamInfo.wss_hls"><el-tag>HLS(wss):</el-tag><span>{{ streamInfo.wss_hls }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.ts" :command="streamInfo.ts"><el-tag>TS:</el-tag><span>{{ streamInfo.ts }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.https_ts" :command="streamInfo.https_ts"><el-tag>TS(https):</el-tag><span>{{ streamInfo.https_ts }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.ws_ts" :command="streamInfo.ws_ts"><el-tag>TS(ws):</el-tag><span>{{ streamInfo.ws_ts }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.wss_ts" :command="streamInfo.wss_ts"><el-tag>TS(wss):</el-tag><span>{{ streamInfo.wss_ts }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtc" :command="streamInfo.rtc"><el-tag>RTC:</el-tag><span>{{ streamInfo.rtc }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtcs" :command="streamInfo.rtcs"><el-tag>RTCS:</el-tag><span>{{ streamInfo.rtcs }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtmp" :command="streamInfo.rtmp"><el-tag>RTMP:</el-tag><span>{{ streamInfo.rtmp }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtmps" :command="streamInfo.rtmps"><el-tag>RTMPS:</el-tag><span>{{ streamInfo.rtmps }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtsp" :command="streamInfo.rtsp"><el-tag>RTSP:</el-tag><span>{{ streamInfo.rtsp }}</span></el-dropdown-item>
|
||||
<el-dropdown-item v-if="streamInfo.rtsps" :command="streamInfo.rtsps"><el-tag>RTSPS:</el-tag><span>{{ streamInfo.rtsps }}</span></el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'StreamMediaPanel',
|
||||
props: {
|
||||
playerUrl: { type: String, default: '' },
|
||||
playUrl: { type: String, default: '' },
|
||||
streamInfo: { type: Object, default: null }
|
||||
},
|
||||
computed: {
|
||||
sharedIframe() {
|
||||
return `<iframe src="${this.playerUrl}"></iframe>`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyUrl(text) {
|
||||
this.$copyText(text).then(() => {
|
||||
this.$message.success({ showClose: true, message: '成功拷贝到粘贴板' })
|
||||
}, () => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -9,7 +9,7 @@
|
||||
|
||||
<script>
|
||||
import veHistogram from 'v-charts/lib/histogram'
|
||||
import HasStreamChannel from "@/views/dialog/hasStreamChannel";
|
||||
import HasStreamChannel from "@/views/dashboard/dialog/hasStreamChannel.vue";
|
||||
|
||||
export default {
|
||||
name: 'ConsoleNodeLoad',
|
||||
|
||||
@ -71,7 +71,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import devicePlayer from "@/views/dialog/devicePlayer";
|
||||
import devicePlayer from "@/views/device/dialog/devicePlayer.vue";
|
||||
import elDragDialog from "@/directive/el-drag-dialog";
|
||||
|
||||
export default {
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="channelList" style="height: calc(100vh - 124px);">
|
||||
<div v-if="!editId" style="height: 100%">
|
||||
<div v-if="!editId && !ptzConfigChannelDeviceId" style="height: 100%">
|
||||
<el-form :inline="true" size="mini">
|
||||
<el-form-item style="margin-right: 2rem">
|
||||
<el-page-header content="通道列表" @back="showDevice" />
|
||||
@ -189,6 +189,10 @@
|
||||
设备录像控制-开始</el-dropdown-item>
|
||||
<el-dropdown-item command="stopRecord" :disabled="device == null || device.online === 0">
|
||||
设备录像控制-停止</el-dropdown-item>
|
||||
<el-dropdown-item command="ptzConfig" :disabled="device == null || device.online === 0">
|
||||
云台配置</el-dropdown-item>
|
||||
<el-dropdown-item command="audioTalk" :disabled="device == null || device.online === 0">
|
||||
语音对讲</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
|
||||
</el-dropdown>
|
||||
@ -209,19 +213,25 @@
|
||||
|
||||
<devicePlayer ref="devicePlayer" />
|
||||
<channel-edit v-if="editId" :id="editId" :close-edit="closeEdit" />
|
||||
<ptzConfig v-if="ptzConfigChannelDeviceId" :device-id="ptzConfigDeviceId" :channel-device-id="ptzConfigChannelDeviceId" @close="closePtzConfig" />
|
||||
<audioTalk ref="audioTalk" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import devicePlayer from '../../dialog/devicePlayer.vue'
|
||||
import devicePlayer from '../dialog/devicePlayer.vue'
|
||||
import audioTalk from '../dialog/audioTalk.vue'
|
||||
import Edit from './edit.vue'
|
||||
import ptzConfig from '@/views/device/channel/ptzConfig.vue'
|
||||
|
||||
export default {
|
||||
name: 'ChannelList',
|
||||
components: {
|
||||
devicePlayer,
|
||||
ChannelEdit: Edit
|
||||
audioTalk,
|
||||
ChannelEdit: Edit,
|
||||
ptzConfig
|
||||
},
|
||||
props: {
|
||||
defaultPage: {
|
||||
@ -258,6 +268,8 @@ export default {
|
||||
total: 0,
|
||||
beforeUrl: '/device',
|
||||
editId: null,
|
||||
ptzConfigDeviceId: null,
|
||||
ptzConfigChannelDeviceId: null,
|
||||
loadSnap: {},
|
||||
ptzTypes: {
|
||||
0: '未知',
|
||||
@ -278,7 +290,6 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log(23222)
|
||||
if (this.deviceId) {
|
||||
this.$store.dispatch('device/queryDeviceOne', this.deviceId)
|
||||
.then(data => {
|
||||
@ -372,6 +383,10 @@ export default {
|
||||
itemData.playLoading = false
|
||||
})
|
||||
},
|
||||
closePtzConfig: function() {
|
||||
this.ptzConfigDeviceId = null
|
||||
this.ptzConfigChannelDeviceId = null
|
||||
},
|
||||
moreClick: function(command, itemData) {
|
||||
if (command === 'records') {
|
||||
this.queryRecords(itemData)
|
||||
@ -381,6 +396,12 @@ export default {
|
||||
this.startRecord(itemData)
|
||||
} else if (command === 'stopRecord') {
|
||||
this.stopRecord(itemData)
|
||||
} else if (command === 'ptzConfig') {
|
||||
console.log(itemData.channelId)
|
||||
this.ptzConfigDeviceId = this.deviceId
|
||||
this.ptzConfigChannelDeviceId = itemData.deviceId
|
||||
} else if (command === 'audioTalk') {
|
||||
this.$refs.audioTalk.openDialog(this.deviceId, itemData.deviceId)
|
||||
}
|
||||
},
|
||||
queryRecords: function(itemData) {
|
||||
|
||||
106
web/src/views/device/channel/ptzConfig.vue
Normal file
106
web/src/views/device/channel/ptzConfig.vue
Normal file
@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div id="dhPtzConfigPage">
|
||||
<el-page-header content="云台设置" @back="$emit('close')" />
|
||||
<div class="ptz-config-body">
|
||||
<div class="config-sidebar">
|
||||
<el-menu :default-active="activeTab" @select="handleMenuSelect">
|
||||
<el-menu-item index="preset">
|
||||
<i class="el-icon-map-location" style="margin-right: 6px" />
|
||||
<span>预置点</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="cruise">
|
||||
<i class="el-icon-s-order" style="margin-right: 6px" />
|
||||
<span>巡航组</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="scan">
|
||||
<i class="iconfont icon-slider-right" style="margin-right: 6px" />
|
||||
<span>线性扫描</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="switch">
|
||||
<i class="el-icon-s-tools" style="margin-right: 6px" />
|
||||
<span>辅助开关</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<div class="player-panel">
|
||||
<playerPtzPanel :device-id="deviceId" :channel-device-id="channelDeviceId" />
|
||||
</div>
|
||||
<div class="tab-panel">
|
||||
<ptzPresetConfig v-if="activeTab === 'preset'" :device-id="deviceId" :channel-device-id="channelDeviceId" />
|
||||
<ptzCruiseConfig v-if="activeTab === 'cruise'" :device-id="deviceId" :channel-device-id="channelDeviceId" />
|
||||
<ptzScanConfig v-if="activeTab === 'scan'" :device-id="deviceId" :channel-device-id="channelDeviceId" />
|
||||
<ptzSwitchConfig v-if="activeTab === 'switch'" :device-id="deviceId" :channel-device-id="channelDeviceId" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import playerPtzPanel from '../common/playerPtzPanel.vue'
|
||||
import ptzPresetConfig from '../common/ptzPresetConfig.vue'
|
||||
import ptzCruiseConfig from '../common/ptzCruiseConfig.vue'
|
||||
import ptzScanConfig from '../common/ptzScanConfig.vue'
|
||||
import ptzSwitchConfig from '../common/ptzSwitchConfig.vue'
|
||||
|
||||
export default {
|
||||
name: 'PtzConfigPage',
|
||||
components: { playerPtzPanel, ptzPresetConfig, ptzCruiseConfig, ptzScanConfig, ptzSwitchConfig },
|
||||
props: {
|
||||
deviceId: { type: String, default: null },
|
||||
channelDeviceId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'preset'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleMenuSelect(index) {
|
||||
this.activeTab = index
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#dhPtzConfigPage {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ptz-config-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
padding-top: 16px;
|
||||
}
|
||||
.config-sidebar {
|
||||
width: 140px;
|
||||
flex: none;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.config-sidebar .el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
.player-panel {
|
||||
width: 600px;
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.tab-panel {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 0 12px;
|
||||
}
|
||||
</style>
|
||||
75
web/src/views/device/common/devicePtzPanel.vue
Normal file
75
web/src/views/device/common/devicePtzPanel.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="ptz-section">
|
||||
<ptzControls
|
||||
btn-layout="row"
|
||||
@ptz-move="onPtzMove"
|
||||
@ptz-stop="onPtzStop"
|
||||
@focus-move="onFocusMove"
|
||||
@focus-stop="onFocusStop"
|
||||
@iris-move="onIrisMove"
|
||||
@iris-stop="onIrisStop"
|
||||
@toggle-drag-zoom="$emit('drag-zoom-start', 'in')"
|
||||
@toggle-drag-zoom-out="$emit('drag-zoom-start', 'out')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ptzControls from '../../common/ptzControls.vue'
|
||||
|
||||
export default {
|
||||
name: 'DevicePtzPanel',
|
||||
components: { ptzControls },
|
||||
props: {
|
||||
deviceId: { type: String, default: null },
|
||||
channelId: { type: String, default: null }
|
||||
},
|
||||
methods: {
|
||||
ptzSpeed(speed) {
|
||||
return parseInt(speed * 255 / 100)
|
||||
},
|
||||
onPtzMove(e) {
|
||||
const speedVal = this.ptzSpeed(e.speed)
|
||||
this.$store.dispatch('frontEnd/ptz', {
|
||||
deviceId: this.deviceId,
|
||||
channelId: this.channelId,
|
||||
command: e.direction,
|
||||
horizonSpeed: speedVal,
|
||||
verticalSpeed: speedVal,
|
||||
zoomSpeed: parseInt(e.speed * 15 / 100)
|
||||
})
|
||||
},
|
||||
onPtzStop() {
|
||||
this.$store.dispatch('frontEnd/ptz', {
|
||||
deviceId: this.deviceId,
|
||||
channelId: this.channelId,
|
||||
command: 'stop',
|
||||
horizonSpeed: 0,
|
||||
verticalSpeed: 0,
|
||||
zoomSpeed: 0
|
||||
})
|
||||
},
|
||||
onFocusMove(e) {
|
||||
const speedVal = this.ptzSpeed(e.speed)
|
||||
this.$store.dispatch('frontEnd/focus', [this.deviceId, this.channelId, e.command, speedVal])
|
||||
},
|
||||
onFocusStop() {
|
||||
this.$store.dispatch('frontEnd/focus', [this.deviceId, this.channelId, 'stop', 0])
|
||||
},
|
||||
onIrisMove(e) {
|
||||
const speedVal = this.ptzSpeed(e.speed)
|
||||
this.$store.dispatch('frontEnd/iris', [this.deviceId, this.channelId, e.command, speedVal])
|
||||
},
|
||||
onIrisStop() {
|
||||
this.$store.dispatch('frontEnd/iris', [this.deviceId, this.channelId, 'stop', 0])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ptz-section {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
93
web/src/views/device/common/playerPtzPanel.vue
Normal file
93
web/src/views/device/common/playerPtzPanel.vue
Normal file
@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="player-ptz-panel">
|
||||
<div class="player-section">
|
||||
<div class="player-wrapper" :style="{ height: playerHeight }">
|
||||
<playerTabs ref="playerTabs" :has-audio="hasAudio" :show-button="true" />
|
||||
</div>
|
||||
</div>
|
||||
<devicePtzPanel
|
||||
style="margin-top: 5vh"
|
||||
:device-id="deviceId"
|
||||
:channel-id="channelDeviceId"
|
||||
@drag-zoom-start="toggleDragZoom"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import playerTabs from '../../common/playerTabs.vue'
|
||||
import devicePtzPanel from './devicePtzPanel.vue'
|
||||
|
||||
export default {
|
||||
name: 'PlayerPtzPanel',
|
||||
components: { playerTabs, devicePtzPanel },
|
||||
props: {
|
||||
deviceId: { type: String, default: null },
|
||||
channelDeviceId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasAudio: false,
|
||||
playerHeight: '40vh',
|
||||
dragZoomDirection: ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.startPlay()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopPlay()
|
||||
},
|
||||
methods: {
|
||||
startPlay() {
|
||||
this.$store.dispatch('play/play', [this.deviceId, this.channelDeviceId])
|
||||
.then(data => {
|
||||
this.hasAudio = data.hasAudio
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.playerTabs) {
|
||||
this.$refs.playerTabs.setStreamInfo(data.transcodeStream || data)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
this.$message({ showClose: true, message: e || '播放失败', type: 'error' })
|
||||
})
|
||||
},
|
||||
stopPlay() {
|
||||
this.$store.dispatch('play/stop', { deviceId: this.deviceId, channelId: this.channelDeviceId })
|
||||
.catch(() => {})
|
||||
},
|
||||
toggleDragZoom(direction) {
|
||||
this.dragZoomDirection = direction
|
||||
this.$refs.playerTabs.startDragZoom((params) => {
|
||||
params.deviceId = this.deviceId
|
||||
params.channelId = this.channelDeviceId
|
||||
const action = this.dragZoomDirection === 'in' ? 'frontEnd/dragZoomIn' : 'frontEnd/dragZoomOut'
|
||||
const successMsg = this.dragZoomDirection === 'in' ? '拉框放大成功' : '拉框缩小成功'
|
||||
const failMsg = this.dragZoomDirection === 'in' ? '拉框放大失败' : '拉框缩小失败'
|
||||
this.$store.dispatch(action, params).then(() => {
|
||||
this.$message({ showClose: true, message: successMsg, type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: failMsg, type: 'error' })
|
||||
})
|
||||
this.dragZoomDirection = ''
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.player-ptz-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
.player-section {
|
||||
flex: 0.8;
|
||||
}
|
||||
.player-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
267
web/src/views/device/common/ptzCruiseConfig.vue
Normal file
267
web/src/views/device/common/ptzCruiseConfig.vue
Normal file
@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<div id="ptzCruiseConfig" style="height: 100%; display: flex; flex-direction: column;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px;">
|
||||
<div>
|
||||
<el-button type="primary" :disabled="formVisible" @click="openAdd">添加巡航组</el-button>
|
||||
<el-button :loading="clearing" :disabled="clearing" @click="clearCruiseTours">清空</el-button>
|
||||
</div>
|
||||
<el-button icon="el-icon-refresh-right" circle @click="loadPresets" />
|
||||
</div>
|
||||
<div v-if="formVisible" style="margin-bottom: 6px; padding: 16px 8px; border: 1px solid #e6e6e6; border-radius: 4px;">
|
||||
<el-form inline size="small" style="display: flex; align-items: center; margin-top: 15px;">
|
||||
<el-form-item label="序号" style="margin-bottom: 0;">
|
||||
<el-input-number v-model="formId" :min="0" :max="255" controls-position="right" style="width: 120px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="名称" style="margin-bottom: 0;">
|
||||
<el-input v-model="formName" placeholder="名称" style="width: 140px" />
|
||||
</el-form-item>
|
||||
<el-form-item style="margin-bottom: 0;">
|
||||
<el-button type="primary" :loading="submitting" :disabled="submitting" @click="confirmSave">确定</el-button>
|
||||
<el-button @click="cancelForm">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-divider style="margin: 6px 0;" />
|
||||
<div style="margin-bottom: 4px;">
|
||||
<el-button size="mini" type="primary" @click="addPresetRow">添加预置点</el-button>
|
||||
</div>
|
||||
<el-table :data="formPresets" size="mini" stripe border max-height="200px">
|
||||
<el-table-column label="序号" width="50">
|
||||
<template v-slot="{ $index }">{{ $index + 1 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预置点" min-width="100">
|
||||
<template v-slot="{ row }">
|
||||
<el-select v-model="row.presetId" size="mini" style="width: 120px" placeholder="选择预置点">
|
||||
<el-option v-for="p in allPresetList" :key="p.presetId"
|
||||
:label="p.presetName || ('预置点' + p.presetId)"
|
||||
:value="p.presetId" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="停留时间(秒)" min-width="100">
|
||||
<template v-slot="{ row }">
|
||||
<el-input-number v-model="row.dwellTime" :min="15" :max="300" size="mini" controls-position="right" style="width: 90px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="速度" min-width="100">
|
||||
<template v-slot="{ row }">
|
||||
<el-select v-model="row.speed" size="mini" style="width: 90px">
|
||||
<el-option v-for="s in 10" :key="s" :label="s" :value="s" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="60">
|
||||
<template v-slot="{ $index }">
|
||||
<el-button size="mini" type="text" style="color: #F56C6C" @click="removePresetRow($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-if="cruiseTours.length > 0" style="flex: 1; overflow: auto;">
|
||||
<el-table ref="cruiseTable" :data="cruiseTours" size="mini" max-height="100%" stripe border highlight-current-row>
|
||||
<el-table-column prop="id" label="ID" />
|
||||
<el-table-column prop="name" label="巡航名称" />
|
||||
<el-table-column label="操作" min-width="150">
|
||||
<template v-slot:default="scope">
|
||||
<el-button v-if="cruisingCruiseId === scope.row.id" size="mini" type="text" style="color: #F56C6C" :loading="operatingId === scope.row.id" :disabled="operatingId !== null" @click="stopCruise(scope.row)">停用</el-button>
|
||||
<el-button v-else size="mini" type="text" :disabled="cruisingCruiseId !== null || operatingId !== null" style="color: #409EFF" :loading="operatingId === scope.row.id" @click="startCruise(scope.row)">启用</el-button>
|
||||
<el-button size="mini" type="text" style="color: #409EFF" :disabled="operatingId !== null" @click="openEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="mini" type="text" style="color: #F56C6C" :loading="deletingId === scope.row.id" :disabled="operatingId !== null || deletingId !== null" @click="deleteCruise(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-else style="color: #909399; font-size: 12px; margin-bottom: 8px;">暂无巡航路线</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PtzCruiseConfig',
|
||||
props: {
|
||||
deviceId: { type: String, default: null },
|
||||
channelDeviceId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
cruiseTours: [],
|
||||
cruisingCruiseId: null,
|
||||
formVisible: false,
|
||||
editingTourId: null,
|
||||
submitting: false,
|
||||
clearing: false,
|
||||
operatingId: null,
|
||||
deletingId: null,
|
||||
formId: 1,
|
||||
formName: '',
|
||||
formPresets: [],
|
||||
allPresetList: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadPresets()
|
||||
},
|
||||
methods: {
|
||||
loadPresets() {
|
||||
this.$store.dispatch('frontEnd/queryPreset', [this.deviceId, this.channelDeviceId])
|
||||
.then(data => {
|
||||
this.allPresetList = data || []
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('[巡航] 加载预置点列表失败', error)
|
||||
})
|
||||
},
|
||||
getNextAvailableId() {
|
||||
const used = new Set((this.cruiseTours || []).map(t => t.id))
|
||||
for (let i = 0; i <= 255; i++) {
|
||||
if (!used.has(i)) return i
|
||||
}
|
||||
return 0
|
||||
},
|
||||
openAdd() {
|
||||
this.editingTourId = null
|
||||
this.formId = this.getNextAvailableId()
|
||||
this.formName = '巡航组' + this.formId
|
||||
this.formPresets = []
|
||||
this.formVisible = true
|
||||
},
|
||||
openEdit(tour) {
|
||||
this.editingTourId = tour.id
|
||||
this.formId = tour.id
|
||||
this.formName = tour.name
|
||||
this.formPresets = (tour.presets || []).map(p => ({
|
||||
presetId: p.presetId,
|
||||
dwellTime: p.dwellTime,
|
||||
speed: p.speed
|
||||
}))
|
||||
if (this.formPresets.length === 0) {
|
||||
this.formPresets.push({ presetId: this.getFirstPresetId(), dwellTime: 15, speed: 7 })
|
||||
}
|
||||
this.formVisible = true
|
||||
},
|
||||
cancelForm() {
|
||||
this.formVisible = false
|
||||
this.editingTourId = null
|
||||
this.formPresets = []
|
||||
},
|
||||
getFirstPresetId() {
|
||||
const first = this.allPresetList[0]
|
||||
return first ? first.presetId : 1
|
||||
},
|
||||
addPresetRow() {
|
||||
this.formPresets.push({
|
||||
presetId: this.getFirstPresetId(),
|
||||
dwellTime: 15,
|
||||
speed: 7
|
||||
})
|
||||
},
|
||||
removePresetRow(index) {
|
||||
this.formPresets.splice(index, 1)
|
||||
},
|
||||
confirmSave() {
|
||||
if (!this.formName.trim()) {
|
||||
this.$message({ showClose: true, message: '请输入巡航组名称', type: 'warning' })
|
||||
return
|
||||
}
|
||||
if (this.formId == null || this.formId < 0 || this.formId > 255) {
|
||||
this.$message({ showClose: true, message: '巡航序号必须在0-255之间', type: 'warning' })
|
||||
return
|
||||
}
|
||||
this.submitting = true
|
||||
let chain = Promise.resolve()
|
||||
if (this.editingTourId !== null) {
|
||||
chain = chain.then(() => this.$store.dispatch('frontEnd/deletePointForCruise', [this.deviceId, this.channelDeviceId, this.formId, 0]))
|
||||
}
|
||||
this.formPresets.forEach(p => {
|
||||
chain = chain.then(() => this.$store.dispatch('frontEnd/addPointForCruise', [this.deviceId, this.channelDeviceId, this.formId, p.presetId]))
|
||||
})
|
||||
const speed = this.formPresets.length > 0 ? this.formPresets[0].speed : 7
|
||||
const dwellTime = this.formPresets.length > 0 ? this.formPresets[0].dwellTime : 15
|
||||
chain = chain.then(() => this.$store.dispatch('frontEnd/setCruiseSpeed', [this.deviceId, this.channelDeviceId, this.formId, speed]))
|
||||
chain = chain.then(() => this.$store.dispatch('frontEnd/setCruiseTime', [this.deviceId, this.channelDeviceId, this.formId, dwellTime]))
|
||||
chain.then(() => {
|
||||
const idx = this.cruiseTours.findIndex(t => t.id === this.formId)
|
||||
const presets = this.formPresets.map(p => ({
|
||||
presetId: p.presetId,
|
||||
dwellTime: p.dwellTime,
|
||||
speed: p.speed
|
||||
}))
|
||||
const tour = { id: this.formId, name: this.formName, presets }
|
||||
if (idx !== -1) {
|
||||
this.$set(this.cruiseTours, idx, tour)
|
||||
} else {
|
||||
this.cruiseTours.push(tour)
|
||||
}
|
||||
this.cancelForm()
|
||||
this.$message({ showClose: true, message: '保存成功', type: 'success' })
|
||||
}).catch(error => {
|
||||
this.$message({ showClose: true, message: error || '保存失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.submitting = false
|
||||
})
|
||||
},
|
||||
clearCruiseTours() {
|
||||
if (this.cruiseTours.length === 0) return
|
||||
this.$confirm('确定清空所有巡航组?', '提示', {
|
||||
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
|
||||
}).then(() => {
|
||||
this.clearing = true
|
||||
let chain = Promise.resolve()
|
||||
this.cruiseTours.forEach(tour => {
|
||||
chain = chain.then(() => this.$store.dispatch('frontEnd/deletePointForCruise', [this.deviceId, this.channelDeviceId, tour.id, 0]))
|
||||
})
|
||||
chain.then(() => {
|
||||
this.cruiseTours = []
|
||||
this.cruisingCruiseId = null
|
||||
this.$message({ showClose: true, message: '清空成功', type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '清空失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.clearing = false
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
startCruise(row) {
|
||||
this.operatingId = row.id
|
||||
this.$store.dispatch('frontEnd/startCruise', [this.deviceId, this.channelDeviceId, row.id])
|
||||
.then(() => {
|
||||
this.cruisingCruiseId = row.id
|
||||
this.$message({ showClose: true, message: '启用成功', type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '启用失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.operatingId = null
|
||||
})
|
||||
},
|
||||
stopCruise(row) {
|
||||
this.operatingId = row.id
|
||||
this.$store.dispatch('frontEnd/stopCruise', [this.deviceId, this.channelDeviceId, row.id])
|
||||
.then(() => {
|
||||
this.cruisingCruiseId = null
|
||||
this.$message({ showClose: true, message: '停止成功', type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '停止失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.operatingId = null
|
||||
})
|
||||
},
|
||||
deleteCruise(row) {
|
||||
this.$confirm('确定删除此巡航组?', '提示', {
|
||||
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
|
||||
}).then(() => {
|
||||
this.deletingId = row.id
|
||||
this.$store.dispatch('frontEnd/deletePointForCruise', [this.deviceId, this.channelDeviceId, row.id, 0])
|
||||
.then(() => {
|
||||
const idx = this.cruiseTours.indexOf(row)
|
||||
if (idx !== -1) this.cruiseTours.splice(idx, 1)
|
||||
if (this.cruisingCruiseId === row.id) this.cruisingCruiseId = null
|
||||
this.$message({ showClose: true, message: '删除成功', type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '删除失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.deletingId = null
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
156
web/src/views/device/common/ptzPresetConfig.vue
Normal file
156
web/src/views/device/common/ptzPresetConfig.vue
Normal file
@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div style="height: 100%; display: flex; flex-direction: column;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px;">
|
||||
<div>
|
||||
<el-button type="primary" :disabled="showAddForm" @click="openAdd">添加预置点</el-button>
|
||||
<el-button :loading="clearing" :disabled="clearing" @click="clearAll">清空</el-button>
|
||||
</div>
|
||||
<el-button icon="el-icon-refresh-right" circle @click="getPresetList" />
|
||||
</div>
|
||||
<el-form v-if="showAddForm" size="small" inline style="margin-bottom: 6px; padding: 16px 8px; border: 1px solid #e6e6e6; border-radius: 4px; display: flex; align-items: center;">
|
||||
<el-form-item label="序号" style="margin-bottom: 0; margin-right: 2rem">
|
||||
<el-input-number v-model="addPresetId" :min="1" :max="255" controls-position="right" style="width: 180px" />
|
||||
</el-form-item>
|
||||
<el-form-item style="margin-bottom: 0;">
|
||||
<el-button type="primary" :loading="submitting" :disabled="submitting" @click="confirmAdd">确定</el-button>
|
||||
<el-button @click="cancelAdd">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table
|
||||
:data="presetList"
|
||||
border
|
||||
stripe
|
||||
max-height="100%"
|
||||
style="flex: 1"
|
||||
>
|
||||
<el-table-column prop="presetId" label="序号" align="center" />
|
||||
<el-table-column label="名称">
|
||||
<template v-slot="{ row }">
|
||||
<span>{{ row.presetName || ('预置点' + row.presetId) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="140" align="center">
|
||||
<template v-slot="{ row }">
|
||||
<el-button size="mini" type="text" @click="callPreset(row)">调用</el-button>
|
||||
<el-button size="mini" type="text" style="color: #F56C6C" :loading="deletingId === row.presetId" :disabled="deletingId !== null" @click="delPreset(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PtzPresetConfig',
|
||||
props: {
|
||||
deviceId: { type: String, default: null },
|
||||
channelDeviceId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
presetList: [],
|
||||
showAddForm: false,
|
||||
addPresetId: 1,
|
||||
submitting: false,
|
||||
clearing: false,
|
||||
deletingId: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getPresetList()
|
||||
},
|
||||
methods: {
|
||||
getPresetList() {
|
||||
this.$store.dispatch('frontEnd/queryPreset', [this.deviceId, this.channelDeviceId])
|
||||
.then(data => {
|
||||
this.presetList = data || []
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
})
|
||||
},
|
||||
openAdd() {
|
||||
this.addPresetId = this.getNextAvailableId()
|
||||
this.showAddForm = true
|
||||
},
|
||||
cancelAdd() {
|
||||
this.showAddForm = false
|
||||
this.addPresetId = 1
|
||||
},
|
||||
confirmAdd() {
|
||||
const exists = this.presetList.some(p => p.presetId === this.addPresetId)
|
||||
if (exists) {
|
||||
this.$message({ showClose: true, message: '序号 ' + this.addPresetId + ' 已存在', type: 'warning' })
|
||||
return
|
||||
}
|
||||
this.submitting = true
|
||||
this.$store.dispatch('frontEnd/addPreset', [this.deviceId, this.channelDeviceId, this.addPresetId])
|
||||
.then(() => {
|
||||
this.showAddForm = false
|
||||
setTimeout(() => {
|
||||
this.getPresetList()
|
||||
}, 600)
|
||||
}).catch(error => {
|
||||
this.$message({ showClose: true, message: error, type: 'error' })
|
||||
}).finally(() => {
|
||||
this.submitting = false
|
||||
})
|
||||
},
|
||||
callPreset(preset) {
|
||||
this.$store.dispatch('frontEnd/callPreset', [this.deviceId, this.channelDeviceId, preset.presetId])
|
||||
.then(() => {
|
||||
this.$message({ showClose: true, message: '调用成功', type: 'success' })
|
||||
}).catch(error => {
|
||||
this.$message({ showClose: true, message: error, type: 'error' })
|
||||
})
|
||||
},
|
||||
delPreset(preset) {
|
||||
this.$confirm('确定删除此预置位', '提示', {
|
||||
dangerouslyUseHTMLString: true,
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.deletingId = preset.presetId
|
||||
this.$store.dispatch('frontEnd/deletePreset', [this.deviceId, this.channelDeviceId, preset.presetId])
|
||||
.then(() => {
|
||||
this.getPresetList()
|
||||
}).catch(error => {
|
||||
this.$message({ showClose: true, message: error, type: 'error' })
|
||||
}).finally(() => {
|
||||
this.deletingId = null
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
clearAll() {
|
||||
if (this.presetList.length === 0) return
|
||||
this.$confirm('确定清空所有预置点?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.clearing = true
|
||||
const promises = this.presetList.map(p =>
|
||||
this.$store.dispatch('frontEnd/deletePreset', [this.deviceId, this.channelDeviceId, p.presetId])
|
||||
)
|
||||
Promise.all(promises).then(() => {
|
||||
this.presetList = []
|
||||
this.$message({ showClose: true, message: '清空成功', type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '清空失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.clearing = false
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
getNextAvailableId() {
|
||||
if (!this.presetList || this.presetList.length === 0) return 1
|
||||
const used = this.presetList.map(p => Number(p.presetId)).sort((a, b) => a - b)
|
||||
for (let i = 0; i < used.length - 1; i++) {
|
||||
if (used[i + 1] - used[i] > 1) return used[i] + 1
|
||||
}
|
||||
return used[used.length - 1] + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
178
web/src/views/device/common/ptzScanConfig.vue
Normal file
178
web/src/views/device/common/ptzScanConfig.vue
Normal file
@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<div id="ptzScanConfig" style="height: 100%; display: flex; flex-direction: column;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px;">
|
||||
<div>
|
||||
<el-button type="primary" :loading="adding" :disabled="adding" @click="addLineScan">添加线扫</el-button>
|
||||
<el-button @click="clearAll">清空</el-button>
|
||||
</div>
|
||||
<el-button icon="el-icon-refresh-right" circle />
|
||||
</div>
|
||||
<div v-if="scanAreas.length > 0" style="flex: 1; overflow: auto;">
|
||||
<el-table :data="scanAreas" max-height="100%" stripe border highlight-current-row height="100%">
|
||||
<el-table-column label="序号" min-width="50">
|
||||
<template v-slot="{ row }">{{ row.index }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="名称" min-width="80">
|
||||
<template v-slot="{ row }">{{ row.name }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="左边界" min-width="90">
|
||||
<template v-slot="{ row }">
|
||||
<el-button type="text"
|
||||
:style="{ color: row.leftBoundary ? '#67C23A' : '#409EFF' }"
|
||||
:loading="boundaryLoading.index === row.index && boundaryLoading.side === 'Left'"
|
||||
:disabled="operatingId !== null"
|
||||
@click="setBoundary(row, 'Left')">
|
||||
{{ row.leftBoundary ? '重新保存' : '待保存' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="右边界" min-width="90">
|
||||
<template v-slot="{ row }">
|
||||
<el-button type="text"
|
||||
:style="{ color: row.rightBoundary ? '#67C23A' : '#409EFF' }"
|
||||
:loading="boundaryLoading.index === row.index && boundaryLoading.side === 'Right'"
|
||||
:disabled="operatingId !== null"
|
||||
@click="setBoundary(row, 'Right')">
|
||||
{{ row.rightBoundary ? '重新保存' : '待保存' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="速度" min-width="90">
|
||||
<template v-slot="{ row }">
|
||||
<el-select v-model="row.speed" :disabled="speedSaving === row.index" @change="onSpeedChange(row)">
|
||||
<el-option v-for="s in 8" :key="s" :label="s" :value="s" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="120">
|
||||
<template v-slot="{ row, $index }">
|
||||
<el-button v-if="$index === cruisingScanIndex" type="text" style="color: #F56C6C" :loading="operatingId === row.index" :disabled="operatingId !== null" @click="stopScan(row)">停用</el-button>
|
||||
<el-button v-else type="text" style="color: #409EFF" :disabled="operatingId !== null" :loading="operatingId === row.index" @click="startScan(row, $index)">启用</el-button>
|
||||
<el-button type="text" style="color: #F56C6C" :disabled="operatingId !== null" @click="deleteScan(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-else style="color: #909399; font-size: 12px; margin-bottom: 8px;">暂无线扫区域</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PtzScanConfig',
|
||||
props: {
|
||||
deviceId: { type: String, default: null },
|
||||
channelDeviceId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scanAreas: [],
|
||||
cruisingScanIndex: null,
|
||||
operatingId: null,
|
||||
adding: false,
|
||||
boundaryLoading: { index: null, side: null },
|
||||
speedSaving: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getNextAvailableIndex() {
|
||||
const used = new Set(this.scanAreas.filter(a => a.name && a.name.trim()).map(a => a.index))
|
||||
for (let i = 0; i <= 255; i++) {
|
||||
if (!used.has(i)) return i
|
||||
}
|
||||
return 0
|
||||
},
|
||||
addLineScan() {
|
||||
const nextIndex = this.getNextAvailableIndex()
|
||||
const name = '线扫' + nextIndex
|
||||
this.adding = true
|
||||
this.scanAreas.push({
|
||||
index: nextIndex,
|
||||
name: name,
|
||||
leftBoundary: false,
|
||||
rightBoundary: false,
|
||||
speed: 5
|
||||
})
|
||||
this.$nextTick(() => { this.adding = false })
|
||||
},
|
||||
setBoundary(row, boundary) {
|
||||
this.boundaryLoading = { index: row.index, side: boundary }
|
||||
const action = boundary === 'Left' ? 'setLeftForScan' : 'setRightForScan'
|
||||
this.$store.dispatch('frontEnd/' + action, [this.deviceId, this.channelDeviceId, row.index])
|
||||
.then(() => {
|
||||
this.$message({ showClose: true, message: (boundary === 'Left' ? '左' : '右') + '边界设置成功', type: 'success' })
|
||||
if (boundary === 'Left') {
|
||||
row.leftBoundary = true
|
||||
} else {
|
||||
row.rightBoundary = true
|
||||
}
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '边界设置失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.boundaryLoading = { index: null, side: null }
|
||||
})
|
||||
},
|
||||
onSpeedChange(row) {
|
||||
this.speedSaving = row.index
|
||||
this.$store.dispatch('frontEnd/setSpeedForScan', [this.deviceId, this.channelDeviceId, row.index, row.speed])
|
||||
.then(() => {
|
||||
this.$message({ showClose: true, message: '速度已保存', type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '速度保存失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.speedSaving = null
|
||||
})
|
||||
},
|
||||
startScan(row, index) {
|
||||
this.operatingId = row.index
|
||||
this.$store.dispatch('frontEnd/startScan', [this.deviceId, this.channelDeviceId, row.index])
|
||||
.then(() => {
|
||||
this.$message({ showClose: true, message: '启用成功', type: 'success' })
|
||||
this.cruisingScanIndex = index
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '启用失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.operatingId = null
|
||||
})
|
||||
},
|
||||
stopScan(row) {
|
||||
this.operatingId = row.index
|
||||
this.$store.dispatch('frontEnd/stopScan', [this.deviceId, this.channelDeviceId, row.index])
|
||||
.then(() => {
|
||||
this.$message({ showClose: true, message: '停用成功', type: 'success' })
|
||||
this.cruisingScanIndex = null
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: '停用失败', type: 'error' })
|
||||
}).finally(() => {
|
||||
this.operatingId = null
|
||||
})
|
||||
},
|
||||
deleteScan(row) {
|
||||
this.$confirm('确定删除线扫 ' + row.index + '?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const idx = this.scanAreas.indexOf(row)
|
||||
if (idx !== -1) this.scanAreas.splice(idx, 1)
|
||||
if (this.cruisingScanIndex !== null && this.scanAreas[this.cruisingScanIndex] === undefined) {
|
||||
this.cruisingScanIndex = null
|
||||
}
|
||||
this.$message({ showClose: true, message: '删除成功(仅本地列表,设备端配置需手动清除)', type: 'success' })
|
||||
}).catch(() => {})
|
||||
},
|
||||
clearAll() {
|
||||
if (this.scanAreas.length === 0) return
|
||||
this.$confirm('确定清空所有线扫区域?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.scanAreas = []
|
||||
this.cruisingScanIndex = null
|
||||
this.$message({ showClose: true, message: '清空成功(仅本地列表,设备端配置需手动清除)', type: 'success' })
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
60
web/src/views/device/common/ptzSwitchConfig.vue
Normal file
60
web/src/views/device/common/ptzSwitchConfig.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline label-width="120px" size="small">
|
||||
<el-form-item label="开关编号" style="margin-bottom: 0;">
|
||||
<el-input-number v-model="switchId" :min="1" :max="255" controls-position="right" style="width: 140px" />
|
||||
</el-form-item>
|
||||
<el-form-item style="margin-bottom: 0;">
|
||||
<el-button type="primary" :loading="loading" :disabled="loading" @click="control('on')">开启</el-button>
|
||||
<el-button :loading="loading" :disabled="loading" @click="control('off')">关闭</el-button>
|
||||
</el-form-item>
|
||||
<el-divider />
|
||||
|
||||
<el-form-item style="margin-bottom: 0;" label="雨刷">
|
||||
<el-button type="primary" :loading="wiperLoading" :disabled="wiperLoading" @click="wiperControl('on')">开启</el-button>
|
||||
<el-button :loading="wiperLoading" :disabled="wiperLoading" @click="wiperControl('off')">关闭</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PtzSwitchConfig',
|
||||
props: {
|
||||
deviceId: { type: String, default: null },
|
||||
channelDeviceId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
switchId: 1,
|
||||
loading: false,
|
||||
wiperLoading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
wiperControl(command) {
|
||||
this.wiperLoading = true
|
||||
this.$store.dispatch('frontEnd/wiper', [this.deviceId, this.channelDeviceId, command])
|
||||
.then(() => {
|
||||
this.$message({ showClose: true, message: command === 'on' ? '雨刷已开启' : '雨刷已关闭', type: 'success' })
|
||||
}).catch(error => {
|
||||
this.$message({ showClose: true, message: error, type: 'error' })
|
||||
}).finally(() => {
|
||||
this.wiperLoading = false
|
||||
})
|
||||
},
|
||||
control(command) {
|
||||
this.loading = true
|
||||
this.$store.dispatch('frontEnd/auxiliary', [this.deviceId, this.channelDeviceId, command, this.switchId])
|
||||
.then(() => {
|
||||
this.$message({ showClose: true, message: command === 'on' ? '开关已开启' : '开关已关闭', type: 'success' })
|
||||
}).catch(error => {
|
||||
this.$message({ showClose: true, message: error, type: 'error' })
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
34
web/src/views/device/common/ptzWiperConfig.vue
Normal file
34
web/src/views/device/common/ptzWiperConfig.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button size="small" :loading="loading" :disabled="loading" @click="control('on')">开启</el-button>
|
||||
<el-button size="small" :loading="loading" :disabled="loading" @click="control('off')">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PtzWiperConfig',
|
||||
props: {
|
||||
deviceId: { type: String, default: null },
|
||||
channelDeviceId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
control(command) {
|
||||
this.loading = true
|
||||
this.$store.dispatch('frontEnd/wiper', [this.deviceId, this.channelDeviceId, command])
|
||||
.then(() => {
|
||||
this.$message({ showClose: true, message: command === 'on' ? '雨刷已开启' : '雨刷已关闭', type: 'success' })
|
||||
}).catch(error => {
|
||||
this.$message({ showClose: true, message: error, type: 'error' })
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
373
web/src/views/device/dialog/audioTalk.vue
Normal file
373
web/src/views/device/dialog/audioTalk.vue
Normal file
@ -0,0 +1,373 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog
|
||||
title="语音对讲"
|
||||
top="10vh"
|
||||
width="61.5vw"
|
||||
:close-on-click-modal="false"
|
||||
:visible.sync="showDialog"
|
||||
@close="close()"
|
||||
>
|
||||
<div style="display: flex; gap: 16px;">
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div v-if="!showPlayer" class="player-placeholder">
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-video-play"
|
||||
:loading="previewLoading"
|
||||
@click="startPreview"
|
||||
>开启预览</el-button>
|
||||
</div>
|
||||
<playerTabs
|
||||
v-if="showPlayer"
|
||||
ref="playerTabs"
|
||||
style="min-height: 60vh;"
|
||||
:has-audio="hasAudio"
|
||||
:show-button="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="broadcast-panel">
|
||||
<div style="text-align: center;">
|
||||
<video id="audioTalkVideo" controls autoplay style="width: 0; height: 0">
|
||||
Your browser is too old which doesn't support HTML5 video.
|
||||
</video>
|
||||
<el-radio-group v-model="talkMode" size="big" @change="onModeChange">
|
||||
<el-radio-button :label="true">喊话</el-radio-button>
|
||||
<el-radio-button :label="false">对讲</el-radio-button>
|
||||
</el-radio-group>
|
||||
<p style="color: #909399; font-size: 14px; margin-top: 4px;">
|
||||
{{ talkMode ? '单向喊话,仅向设备发送语音' : '双向语音交互,可听到设备声音' }}
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<el-button
|
||||
:type="getTalkButtonType()"
|
||||
:disabled="talkStatus === -2"
|
||||
circle
|
||||
icon="el-icon-microphone"
|
||||
style="font-size: 32px; padding: 24px;"
|
||||
@click="talkButtonClick()"
|
||||
/>
|
||||
<p style="margin-top: 16px; color: #606266;">
|
||||
<span v-if="talkStatus === -2">正在释放资源</span>
|
||||
<span v-if="talkStatus === -1">点击开始{{ talkMode ? '喊话' : '对讲' }}</span>
|
||||
<span v-if="talkStatus === 0">等待接通中...</span>
|
||||
<span v-if="talkStatus === 1 && talkMode">喊话中</span>
|
||||
<span v-if="talkStatus === 1 && !talkMode && !playConnected">等待接通中...</span>
|
||||
<span v-if="talkStatus === 1 && !talkMode && playConnected">对讲中</span>
|
||||
</p>
|
||||
<p v-if="talkStatus === 1 && !talkMode && talkAudioFailed" style="margin-top: 8px;">
|
||||
<el-button
|
||||
type="warning"
|
||||
size="mini"
|
||||
icon="el-icon-refresh"
|
||||
@click="retryTalkAudio"
|
||||
>重试音频</el-button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import playerTabs from '../../common/playerTabs.vue'
|
||||
|
||||
export default {
|
||||
name: 'AudioTalk',
|
||||
components: { playerTabs },
|
||||
data() {
|
||||
return {
|
||||
showDialog: false,
|
||||
showPlayer: false,
|
||||
previewLoading: false,
|
||||
deviceId: null,
|
||||
channelId: null,
|
||||
hasAudio: false,
|
||||
streamInfo: null,
|
||||
talkMode: true,
|
||||
talkStatus: -1,
|
||||
broadcastRtc: null,
|
||||
talkAudioRtc: null,
|
||||
talkAudioRetryTimer: null,
|
||||
talkAudioFailed: false,
|
||||
talkAudioPlayStream: null,
|
||||
playConnected: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.talkStatus = -1
|
||||
},
|
||||
methods: {
|
||||
openDialog(deviceId, channelId) {
|
||||
if (this.showDialog) return
|
||||
this.deviceId = deviceId
|
||||
this.channelId = channelId
|
||||
this.talkMode = false
|
||||
this.showPlayer = false
|
||||
this.streamInfo = null
|
||||
this.showDialog = true
|
||||
},
|
||||
onModeChange() {
|
||||
if (this.talkStatus > -1) {
|
||||
this.stopTalk()
|
||||
}
|
||||
},
|
||||
startPreview() {
|
||||
this.previewLoading = true
|
||||
this.$store.dispatch('play/play', [this.deviceId, this.channelId])
|
||||
.then(data => {
|
||||
this.streamInfo = data
|
||||
this.hasAudio = data.hasAudio
|
||||
this.showPlayer = true
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.playerTabs) {
|
||||
this.$refs.playerTabs.setStreamInfo(data.transcodeStream || data)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
this.$message({ showClose: true, message: e, type: 'error' })
|
||||
})
|
||||
.finally(() => {
|
||||
this.previewLoading = false
|
||||
})
|
||||
},
|
||||
getTalkButtonType() {
|
||||
if (this.talkStatus === -2) return 'primary'
|
||||
if (this.talkStatus === -1) return 'primary'
|
||||
if (this.talkStatus === 0) return 'warning'
|
||||
if (this.talkStatus === 1) {
|
||||
if (!this.talkMode && !this.playConnected) return 'warning'
|
||||
return 'danger'
|
||||
}
|
||||
},
|
||||
async talkButtonClick() {
|
||||
if (this.talkStatus === -1) {
|
||||
await this.startTalk()
|
||||
} else if (this.talkStatus === 1) {
|
||||
this.stopTalk()
|
||||
}
|
||||
},
|
||||
async startTalk() {
|
||||
this.talkStatus = 0
|
||||
try {
|
||||
const data = await this.$store.dispatch('play/broadcastStart', [this.deviceId, this.channelId, this.talkMode])
|
||||
const si = data.streamInfo
|
||||
const url = document.location.protocol.includes('https') ? si.rtcs : si.rtc
|
||||
this.startWebrtcPush(url)
|
||||
|
||||
const playStreamInfo = data?.playStreamInfo
|
||||
if (!this.talkMode && playStreamInfo) {
|
||||
this.talkAudioPlayStream = playStreamInfo
|
||||
this.startTalkAudioPlay(playStreamInfo)
|
||||
this.muteVideoPlayer()
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message({ showClose: true, message: e, type: 'error' })
|
||||
this.talkStatus = -1
|
||||
}
|
||||
},
|
||||
startWebrtcPush(url) {
|
||||
this.$store.dispatch('user/getUserInfo')
|
||||
.then((data) => {
|
||||
if (data === null) { this.talkStatus = -1; return }
|
||||
const pushKey = data.pushKey
|
||||
url += '&sign=' + pushKey
|
||||
|
||||
if (this.broadcastRtc) {
|
||||
this.broadcastRtc.close()
|
||||
}
|
||||
this.broadcastRtc = new ZLMRTCClient.Endpoint({
|
||||
debug: true,
|
||||
zlmsdpUrl: url,
|
||||
simulecast: false,
|
||||
useCamera: false,
|
||||
audioEnable: true,
|
||||
videoEnable: false,
|
||||
recvOnly: false
|
||||
})
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_NOT_SUPPORT, () => { this.talkStatus = -1 })
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, () => { this.talkStatus = -1 })
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, () => { this.talkStatus = -1 })
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, (e) => {
|
||||
if (e === 'connecting') this.talkStatus = 0
|
||||
else if (e === 'connected') this.talkStatus = 1
|
||||
else if (e === 'disconnected') this.talkStatus = -1
|
||||
})
|
||||
this.broadcastRtc.on(ZLMRTCClient.Events.CAPTURE_STREAM_FAILED, () => { this.talkStatus = -1 })
|
||||
})
|
||||
.catch(() => { this.talkStatus = -1 })
|
||||
},
|
||||
muteVideoPlayer() {
|
||||
const player = this.$refs.playerTabs
|
||||
if (!player) return
|
||||
if (player.mute) {
|
||||
player.mute()
|
||||
}
|
||||
},
|
||||
unmuteVideoPlayer() {
|
||||
const player = this.$refs.playerTabs
|
||||
if (!player) return
|
||||
if (player.cancelMute) {
|
||||
player.cancelMute()
|
||||
}
|
||||
},
|
||||
startTalkAudioPlay(playStreamInfo) {
|
||||
if (this.talkAudioRtc) {
|
||||
this.talkAudioRtc.close()
|
||||
}
|
||||
if (this.talkAudioRetryTimer) {
|
||||
clearTimeout(this.talkAudioRetryTimer)
|
||||
}
|
||||
|
||||
const url = location.protocol === 'https:' ? playStreamInfo.rtcs : playStreamInfo.rtc
|
||||
if (!url) {
|
||||
console.warn('[AudioTalk] 无可用的设备音频播放地址')
|
||||
return
|
||||
}
|
||||
this.talkAudioRetryTimer = setTimeout(() => {
|
||||
this.pollMediaInfoAndPlay(playStreamInfo)
|
||||
}, 800)
|
||||
},
|
||||
async pollMediaInfoAndPlay(playStreamInfo) {
|
||||
try {
|
||||
const data = await this.$store.dispatch('server/getMediaInfo', {
|
||||
app: playStreamInfo.app,
|
||||
stream: playStreamInfo.stream,
|
||||
mediaServerId: playStreamInfo.mediaServerId
|
||||
})
|
||||
if (data) {
|
||||
const url = location.protocol === 'https:' ? playStreamInfo.rtcs : playStreamInfo.rtc
|
||||
this.startTalkAudioByRtc(url)
|
||||
} else {
|
||||
throw new Error('no data')
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.talkStatus === 1 || this.talkStatus === 0) {
|
||||
this.talkAudioRetryTimer = setTimeout(() => {
|
||||
this.pollMediaInfoAndPlay(playStreamInfo)
|
||||
}, 800)
|
||||
}
|
||||
}
|
||||
},
|
||||
startTalkAudioByRtc(url) {
|
||||
this.talkAudioFailed = false
|
||||
this.talkAudioRtc = new ZLMRTCClient.Endpoint({
|
||||
debug: false,
|
||||
element: document.getElementById('audioTalkVideo'),
|
||||
zlmsdpUrl: url,
|
||||
simulecast: false,
|
||||
useCamera: false,
|
||||
audioEnable: true,
|
||||
videoEnable: false,
|
||||
recvOnly: true,
|
||||
usedatachannel: false
|
||||
})
|
||||
|
||||
this.talkAudioRtc.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, (e) => {
|
||||
console.warn('[AudioTalk] 播放流offer失败:', e?.code, e?.msg)
|
||||
if (e && e.code == -400 && e.msg == '流不存在') {
|
||||
this.talkAudioRetryTimer = setTimeout(() => {
|
||||
this.startTalkAudioByRtc(url)
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
this.talkAudioRtc.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, () => {
|
||||
console.warn('[AudioTalk] 设备音频流到达')
|
||||
this.playConnected = true
|
||||
})
|
||||
|
||||
this.talkAudioRtc.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, () => {
|
||||
console.error('[AudioTalk] 音频播放ICE协商失败')
|
||||
})
|
||||
|
||||
this.talkAudioRtc.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, (s) => {
|
||||
console.warn('[AudioTalk] 音频播放连接状态:', s)
|
||||
if (s === 'connected') {
|
||||
this.playConnected = true
|
||||
} else if (s === 'disconnected' || s === 'failed' || s === 'closed') {
|
||||
this.playConnected = false
|
||||
this.talkAudioFailed = true
|
||||
if (this.talkStatus === 1) {
|
||||
this.talkAudioRetryTimer = setTimeout(() => {
|
||||
this.startTalkAudioByRtc(url)
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async stopTalk() {
|
||||
this.talkStatus = -2
|
||||
if (this.broadcastRtc) {
|
||||
this.broadcastRtc.close()
|
||||
this.broadcastRtc = null
|
||||
}
|
||||
if (this.talkAudioRtc) {
|
||||
this.talkAudioRtc.close()
|
||||
this.talkAudioRtc = null
|
||||
}
|
||||
if (this.talkAudioRetryTimer) {
|
||||
clearTimeout(this.talkAudioRetryTimer)
|
||||
this.talkAudioRetryTimer = null
|
||||
}
|
||||
this.talkAudioFailed = false
|
||||
this.talkAudioPlayStream = null
|
||||
this.playConnected = false
|
||||
this.unmuteVideoPlayer()
|
||||
try {
|
||||
await this.$store.dispatch('play/broadcastStop', [this.deviceId, this.channelId])
|
||||
} catch (e) {
|
||||
console.warn('停止对讲失败', e)
|
||||
}
|
||||
this.talkStatus = -1
|
||||
},
|
||||
retryTalkAudio() {
|
||||
if (this.talkAudioPlayStream) {
|
||||
this.startTalkAudioPlay(this.talkAudioPlayStream)
|
||||
}
|
||||
},
|
||||
close() {
|
||||
if (this.showPlayer && this.$refs.playerTabs) {
|
||||
this.$refs.playerTabs.stop()
|
||||
}
|
||||
this.stopTalk()
|
||||
this.streamInfo = null
|
||||
this.showPlayer = false
|
||||
this.showDialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.player-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 16 / 9;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
.broadcast-panel {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 10px;
|
||||
border-left: 1px solid #ebeef5;
|
||||
}
|
||||
.broadcast-panel > div:first-child {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.broadcast-panel > div:last-child {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
164
web/src/views/device/dialog/devicePlayer.vue
Executable file
164
web/src/views/device/dialog/devicePlayer.vue
Executable file
@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div id="devicePlayer" v-loading="isLoging">
|
||||
|
||||
<el-dialog
|
||||
v-if="showVideoDialog"
|
||||
v-el-drag-dialog
|
||||
title="视频播放"
|
||||
top="10vh"
|
||||
width="65vw"
|
||||
:close-on-click-modal="false"
|
||||
:visible.sync="showVideoDialog"
|
||||
@close="close()"
|
||||
>
|
||||
<div class="dhsdk-player-body">
|
||||
|
||||
<div class="player-side">
|
||||
<div class="player-container" :style="{ height: playerHeight }">
|
||||
<playerTabs ref="playerTabs" :has-audio="hasAudio" :show-button="true"
|
||||
@playerChanged="playerChanged" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-side">
|
||||
<devicePtzPanel
|
||||
:device-id="deviceId"
|
||||
:channel-id="channelId"
|
||||
@drag-zoom-start="toggleDragZoom"
|
||||
/>
|
||||
|
||||
<el-tabs v-model="tabActiveName" @tab-click="tabHandleClick" class="control-tabs">
|
||||
<el-tab-pane label="预置位" name="preset">
|
||||
<ptzPreset
|
||||
v-if="tabActiveName === 'preset'"
|
||||
:device-id="deviceId"
|
||||
:channel-device-id="channelId"
|
||||
style="margin-top: 8px;"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="实时视频" name="media">
|
||||
<streamMediaPanel v-if="tabActiveName === 'media'" :player-url="playerUrlInfo.playerUrl" :play-url="playerUrlInfo.playUrl" :stream-info="streamInfo" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="编码信息" name="codec">
|
||||
<mediaInfo v-if="tabActiveName === 'codec'" ref="mediaInfo" :app="app" :stream="streamId" :media-server-id="mediaServerId" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import elDragDialog from '@/directive/el-drag-dialog'
|
||||
import playerTabs from '../../common/playerTabs.vue'
|
||||
import devicePtzPanel from '../common/devicePtzPanel.vue'
|
||||
import PtzPreset from './ptzPreset.vue'
|
||||
import mediaInfo from '../../common/mediaInfo.vue'
|
||||
import streamMediaPanel from '../../common/streamMediaPanel.vue'
|
||||
|
||||
export default {
|
||||
name: 'DevicePlayer',
|
||||
directives: { elDragDialog },
|
||||
components: { playerTabs, devicePtzPanel, PtzPreset, mediaInfo, streamMediaPanel },
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
videoUrl: '',
|
||||
streamId: '',
|
||||
app: '',
|
||||
mediaServerId: '',
|
||||
deviceId: '',
|
||||
channelId: '',
|
||||
tabActiveName: 'preset',
|
||||
hasAudio: false,
|
||||
isLoging: false,
|
||||
showVideoDialog: false,
|
||||
streamInfo: null,
|
||||
playerHeight: '48vh',
|
||||
playerUrlInfo: {
|
||||
playerUrl: null,
|
||||
playUrl: null,
|
||||
},
|
||||
dragZoomDirection: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tabHandleClick: function(tab) {
|
||||
if (tab.name === 'codec') {
|
||||
this.$refs.mediaInfo && this.$refs.mediaInfo.startTask()
|
||||
} else {
|
||||
this.$refs.mediaInfo && this.$refs.mediaInfo.stopTask()
|
||||
}
|
||||
},
|
||||
openDialog: function(tab, deviceId, channelId, param) {
|
||||
if (this.showVideoDialog) return
|
||||
this.tabActiveName = tab === 'streamPlay' ? 'media' : (tab || 'preset')
|
||||
this.deviceId = deviceId
|
||||
this.channelId = channelId
|
||||
this.streamId = ''
|
||||
this.mediaServerId = ''
|
||||
this.app = ''
|
||||
this.videoUrl = ''
|
||||
if (param && param.streamInfo) {
|
||||
this.play(param.streamInfo, param.hasAudio)
|
||||
}
|
||||
},
|
||||
play: function(streamInfo, hasAudio) {
|
||||
this.streamInfo = streamInfo
|
||||
this.hasAudio = hasAudio
|
||||
this.isLoging = false
|
||||
this.streamId = streamInfo.stream
|
||||
this.app = streamInfo.app
|
||||
this.mediaServerId = streamInfo.mediaServerId
|
||||
this.showVideoDialog = true
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.playerTabs) {
|
||||
this.$refs.playerTabs.setStreamInfo(streamInfo.transcodeStream || streamInfo)
|
||||
}
|
||||
})
|
||||
},
|
||||
playerChanged: function(playerUrlInfo) {
|
||||
this.playerUrlInfo = playerUrlInfo
|
||||
},
|
||||
close: function() {
|
||||
if (this.$refs.playerTabs) {
|
||||
this.$refs.playerTabs.stop()
|
||||
}
|
||||
this.videoUrl = ''
|
||||
this.showVideoDialog = false
|
||||
},
|
||||
toggleDragZoom(direction) {
|
||||
this.dragZoomDirection = direction
|
||||
this.$refs.playerTabs.startDragZoom((params) => {
|
||||
params.deviceId = this.deviceId
|
||||
params.channelId = this.channelId
|
||||
const action = this.dragZoomDirection === 'in' ? 'frontEnd/dragZoomIn' : 'frontEnd/dragZoomOut'
|
||||
const successMsg = this.dragZoomDirection === 'in' ? '拉框放大成功' : '拉框缩小成功'
|
||||
const failMsg = this.dragZoomDirection === 'in' ? '拉框放大失败' : '拉框缩小失败'
|
||||
this.$store.dispatch(action, params).then(() => {
|
||||
this.$message({ showClose: true, message: successMsg, type: 'success' })
|
||||
}).catch(() => {
|
||||
this.$message({ showClose: true, message: failMsg, type: 'error' })
|
||||
})
|
||||
this.dragZoomDirection = ''
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#devicePlayer .el-dialog__body { padding: 10px 20px; }
|
||||
.dhsdk-player-body { display: flex; gap: 16px; }
|
||||
.player-side { flex: 3; min-width: 0; }
|
||||
.player-container { width: 100%; }
|
||||
.control-side { flex: 2; min-width: 340px; display: flex; flex-direction: column; }
|
||||
.control-tabs { flex: 1; display: flex; flex-direction: column; min-height: 220px}
|
||||
.control-tabs .el-tabs__content { flex: 1; overflow: auto; }
|
||||
.media-info-content { overflow: auto; }
|
||||
.media-row { display: flex; margin-bottom: 0.5rem; height: 2.5rem; }
|
||||
.media-label { width: 6rem; line-height: 2.5rem; text-align: right; flex-shrink: 0; }
|
||||
.trank { width: 80%; height: 180px; text-align: left; padding: 0 10%; overflow: auto; }
|
||||
</style>
|
||||
52
web/src/views/device/dialog/ptzPreset.vue
Normal file
52
web/src/views/device/dialog/ptzPreset.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div id="ptzPreset" style="width: 100%">
|
||||
<el-tag
|
||||
v-for="item in presetList"
|
||||
:key="item.presetId"
|
||||
size="mini"
|
||||
style="margin-right: 1rem; cursor: pointer; margin-bottom: 0.6rem"
|
||||
@click="gotoPreset(item)"
|
||||
>
|
||||
{{ item.presetName || item.presetId }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PtzPreset',
|
||||
props: ['channelDeviceId', 'deviceId'],
|
||||
data() {
|
||||
return {
|
||||
presetList: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getPresetList()
|
||||
},
|
||||
methods: {
|
||||
getPresetList: function() {
|
||||
this.$store.dispatch('frontEnd/queryPreset', [this.deviceId, this.channelDeviceId])
|
||||
.then(data => {
|
||||
this.presetList = data
|
||||
})
|
||||
},
|
||||
gotoPreset: function(preset) {
|
||||
this.$store.dispatch('frontEnd/callPreset', [this.deviceId, this.channelDeviceId, preset.presetId])
|
||||
.then(() => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '调用成功',
|
||||
type: 'success'
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -195,9 +195,9 @@
|
||||
|
||||
<script>
|
||||
import deviceEdit from './edit.vue'
|
||||
import syncChannelProgress from '../dialog/SyncChannelProgress.vue'
|
||||
import syncChannelProgress from './dialog/SyncChannelProgress.vue'
|
||||
import configInfo from '../dialog/configInfo.vue'
|
||||
import timeStatistics from './timeStatistics.vue'
|
||||
import timeStatistics from './dialog/timeStatistics.vue'
|
||||
import Vue from 'vue'
|
||||
|
||||
export default {
|
||||
|
||||
@ -75,6 +75,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="位置信息" min-width="150">
|
||||
<template v-slot:default="scope">
|
||||
<span v-if="scope.row.gbLongitude && scope.row.gbLatitude">{{ scope.row.gbLongitude }}<br>{{ scope.row.gbLatitude }}</span>
|
||||
<span v-if="!scope.row.gbLongitude || !scope.row.gbLatitude">无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template v-slot:default="scope">
|
||||
<div slot="reference" class="name-wrapper">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user