Compare commits

...

26 Commits

Author SHA1 Message Date
田朝盛
dcdee0263a
Pre Merge pull request !39 from 田朝盛/N/A 2026-06-16 08:08:35 +00:00
lin
1fc8848cf9 修改错别字 2026-06-16 16:08:14 +08:00
lin
2f1ec0e33f 修复错别字 2026-06-16 11:26:54 +08:00
lin
cd293bc116 增加位置信息的展示 2026-06-16 10:46:14 +08:00
lin
5f50d4c96f 报警回放支持播放器切换 2026-06-16 10:31:34 +08:00
lin
0c7701d36f 修复地图页面视频播放 2026-06-16 10:21:03 +08:00
lin
6ade3060b5 支持通用通道对讲,支持全双工对讲 2026-06-16 10:08:04 +08:00
lin
f6ca930492 支持通用通道喊话 2026-06-15 16:24:30 +08:00
lin
58cc8a8baf 调整前端代码结构,完善多屏页的云台控制 2026-06-15 15:14:53 +08:00
lin
9a52d29ded 支持多屏播放的播放器切换 2026-06-13 07:27:17 +08:00
lin
1b7661039b 支持通用通道的云台配置和拉框控制 2026-06-12 17:05:11 +08:00
lin
a78599d1d3 Merge branch 'master' into dev/前端新结构 2026-06-12 14:02:04 +08:00
lin
232bca91c2 修复拉流代理ffmpeg模式拉流失败的BUG 2026-06-12 13:09:49 +08:00
lin
9f25d0ce37 推流、部标、拉流代理适配新的播放器结构 2026-06-12 13:04:17 +08:00
lin
79a7e82656 优化云端录像播放结构 2026-06-12 10:55:15 +08:00
lin
d3e89786c2 优化云端录像播放结构 2026-06-12 10:55:04 +08:00
panlinlin
c59bacdc2c 重构设备播放器和PTZ面板组件,优化导入路径并实现拉框缩放功能 2026-06-11 23:26:10 +08:00
lin
6931a95ecf 云台配置 优化巡航组配置 2026-06-11 22:30:27 +08:00
lin
fb6c84de29 对讲使用单独的弹窗组件 2026-06-11 17:39:55 +08:00
lin
70bc01bd90 支持拉框放大和拉框缩小 2026-06-11 17:29:20 +08:00
lin
3417244705 优化播放器基础组件按钮样式 2026-06-11 12:36:13 +08:00
lin
5eea3a19f5 优化播放弹窗 2026-06-11 11:34:27 +08:00
lin
f5494c0b95 拆分播放器组件和云台控制组件 2026-06-10 18:15:45 +08:00
648540858
ab371e00df
Merge pull request #2167 from ym6009/master
修复通道列表页面不能播放的问题
2026-06-10 16:57:09 +08:00
ym
042c78fcf6 修复通道列表页面不能播放的问题 2026-06-04 09:25:12 +08:00
田朝盛
8dd624ccd4
update src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java.
Signed-off-by: 田朝盛 <1753182616@qq.com>
2025-07-14 08:04:09 +00:00
134 changed files with 6037 additions and 6310 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,18 +136,13 @@ public class SourcePTZServiceForGbImpl implements ISourcePTZService {
log.error("[FI失败] 未知的光圈指令 {}", frontEndControlCode.getIris());
callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null);
}
}
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);
}catch (Exception e) {
@ -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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View 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() {
if (this.$refs.recordVideoPlayer) {
this.$refs.recordVideoPlayer.screenshot()
}
},
refresh() {
if (this.$refs.recordVideoPlayer) {
this.$refs.recordVideoPlayer.destroy()
this.$refs.recordVideoPlayer.playBtnClick()
}
},
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'
})
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)
}
})
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() {
//
if (this.$refs.recordVideoPlayer) {
this.$refs.recordVideoPlayer.pause()
// TODO
}
},
play() {
if (this.$refs.recordVideoPlayer.loaded) {
this.$refs.recordVideoPlayer.unPause()
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) => {
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;

View File

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

View File

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

View File

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

View File

@ -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 + '&parameter1=0&parameter2=' + 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 + '&parameter1=' + groupNum + '&parameter2=' + 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 + '&parameter1=' + groupNum + '&parameter2=' + 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,8 +3,9 @@
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">
<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" />
@ -31,8 +32,10 @@
<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;

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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