Compare commits

..

1 Commits

Author SHA1 Message Date
阿斌
1f8137051b
Pre Merge pull request !41 from 阿斌/N/A 2026-06-10 02:42:47 +00:00
133 changed files with 6311 additions and 6038 deletions

View File

@ -16,7 +16,6 @@ public class ChannelDataType {
public final static String DOWNLOAD_SERVICE = "sourceChannelDownloadService"; public final static String DOWNLOAD_SERVICE = "sourceChannelDownloadService";
public final static String PTZ_SERVICE = "sourceChannelPTZService"; public final static String PTZ_SERVICE = "sourceChannelPTZService";
public final static String OTHER_SERVICE = "sourceChannelOtherService"; public final static String OTHER_SERVICE = "sourceChannelOtherService";
public final static String BROADCAST_SERVICE = "sourceChannelBroadcastService";
public static String getDateTypeDesc(Integer dataType) { public static String getDateTypeDesc(Integer dataType) {

View File

@ -1,70 +0,0 @@
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 { public enum FrontEndControlType {
PTZ,FI,PRESET,TOUR,SCAN,AUXILIARY,DRAG_ZOOM PTZ,FI,PRESET,TOUR,SCAN,AUXILIARY
} }

View File

@ -12,7 +12,6 @@ import com.genersoft.iot.vmp.gb28181.utils.VectorTileCatch;
import com.genersoft.iot.vmp.service.bean.ErrorCallback; import com.genersoft.iot.vmp.service.bean.ErrorCallback;
import com.genersoft.iot.vmp.service.bean.InviteErrorCode; import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
import com.genersoft.iot.vmp.utils.DateUtil; 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.ErrorCode;
import com.genersoft.iot.vmp.vmanager.bean.StreamContent; import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
import com.genersoft.iot.vmp.vmanager.bean.WVPResult; import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
@ -353,42 +352,6 @@ public class ChannelController {
channelPlayService.stopPlay(channel); 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)) @Operation(summary = "录像查询", security = @SecurityRequirement(name = JwtUtils.HEADER))
@Parameter(name = "channelId", description = "通道ID", required = true) @Parameter(name = "channelId", description = "通道ID", required = true)
@Parameter(name = "startTime", description = "开始时间", required = true) @Parameter(name = "startTime", description = "开始时间", required = true)

View File

@ -598,94 +598,4 @@ public class ChannelFrontEndController {
channelControlService.auxiliary(channel, controlCode, callback); channelControlService.auxiliary(channel, controlCode, callback);
return result; 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,50 +165,66 @@ public class DeviceControl {
@Parameter(name = "channelId", description = "通道国标编号", required = true) @Parameter(name = "channelId", description = "通道国标编号", required = true)
@Parameter(name = "length", description = "播放窗口长度像素值", required = true) @Parameter(name = "length", description = "播放窗口长度像素值", required = true)
@Parameter(name = "width", description = "播放窗口宽度像素值", required = true) @Parameter(name = "width", description = "播放窗口宽度像素值", required = true)
@Parameter(name = "midPointX", description = "拉框中心的横轴坐标像素值", required = true) @Parameter(name = "midpointx", description = "拉框中心的横轴坐标像素值", required = true)
@Parameter(name = "midPointY", description = "拉框中心的纵轴坐标像素值", required = true) @Parameter(name = "midpointy", description = "拉框中心的纵轴坐标像素值", required = true)
@Parameter(name = "lengthX", description = "拉框长度像素值", required = true) @Parameter(name = "lengthx", description = "拉框长度像素值", required = true)
@Parameter(name = "lengthY", description = "拉框宽度像素值", required = true) @Parameter(name = "lengthy", description = "拉框宽度像素值", required = true)
@GetMapping("drag_zoom/zoom_in") @GetMapping("drag_zoom/zoom_in")
public void dragZoomIn(@RequestParam String deviceId, String channelId, public DeferredResult<WVPResult<String>> dragZoomIn(@RequestParam String deviceId, String channelId,
@RequestParam int length, @RequestParam int length,
@RequestParam int width, @RequestParam int width,
@RequestParam int midPointX, @RequestParam int midpointx,
@RequestParam int midPointY, @RequestParam int midpointy,
@RequestParam int lengthX, @RequestParam int lengthx,
@RequestParam int lengthY) { @RequestParam int lengthy) {
if (log.isDebugEnabled()) { 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); Device device = deviceService.getDeviceByDeviceId(deviceId);
Assert.notNull(device, "设备不存在"); Assert.notNull(device, "设备不存在");
deviceService.dragZoomIn(device, channelId, length, width, midPointX, midPointY, lengthX, lengthY); 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;
} }
@Operation(summary = "拉框缩小", security = @SecurityRequirement(name = JwtUtils.HEADER)) @Operation(summary = "拉框缩小", security = @SecurityRequirement(name = JwtUtils.HEADER))
@Parameter(name = "deviceId", description = "设备国标编号", required = true) @Parameter(name = "deviceId", description = "设备国标编号", required = true)
@Parameter(name = "channelId", description = "通道国标编号") @Parameter(name = "channelId", description = "通道国标编号")
@Parameter(name = "length", description = "播放窗口长度像素值", required = true) @Parameter(name = "length", description = "播放窗口长度像素值", required = true)
@Parameter(name = "width", description = "播放窗口宽像素值", required = true) @Parameter(name = "width", description = "拉框中心的横轴坐标像素值", required = true)
@Parameter(name = "midPointX", description = "拉框中心的横轴坐标像素值", required = true) @Parameter(name = "midpointx", description = "拉框中心的横轴坐标像素值", required = true)
@Parameter(name = "midPointY", description = "拉框中心的纵轴坐标像素值", required = true) @Parameter(name = "midpointy", description = "拉框中心的纵轴坐标像素值", required = true)
@Parameter(name = "lengthX", description = "拉框长度像素值", required = true) @Parameter(name = "lengthx", description = "拉框长度像素值", required = true)
@Parameter(name = "lengthY", description = "拉框宽度像素值", required = true) @Parameter(name = "lengthy", description = "拉框宽度像素值", required = true)
@GetMapping("/drag_zoom/zoom_out") @GetMapping("/drag_zoom/zoom_out")
public void dragZoomOut(@RequestParam String deviceId, public DeferredResult<WVPResult<String>> dragZoomOut(@RequestParam String deviceId,
@RequestParam(required = false) String channelId, @RequestParam(required = false) String channelId,
@RequestParam int length, @RequestParam int length,
@RequestParam int width, @RequestParam int width,
@RequestParam int midPointX, @RequestParam int midpointx,
@RequestParam int midPointY, @RequestParam int midpointy,
@RequestParam int lengthX, @RequestParam int lengthx,
@RequestParam int lengthY){ @RequestParam int lengthy){
if (log.isDebugEnabled()) { 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); Device device = deviceService.getDeviceByDeviceId(deviceId);
Assert.notNull(device, "设备不存在"); Assert.notNull(device, "设备不存在");
deviceService.dragZoomOut(device, channelId, length, width, midPointX, midPointY, lengthX,lengthY); 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;
} }
} }

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 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); void dragZoomIn(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 dragZoomOut(Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy, ErrorCallback<String> callback);
void deviceStatus(Device device, ErrorCallback<String> callback); void deviceStatus(Device device, ErrorCallback<String> callback);

View File

@ -16,5 +16,4 @@ public interface IGbChannelControlService {
void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper controlCode, ErrorCallback<String> callback); void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper controlCode, ErrorCallback<String> callback);
void auxiliary(CommonGBChannel channel, FrontEndControlCodeForAuxiliary frontEndControlCode, ErrorCallback<String> callback); void auxiliary(CommonGBChannel channel, FrontEndControlCodeForAuxiliary frontEndControlCode, ErrorCallback<String> callback);
void queryPreset(CommonGBChannel channel, ErrorCallback<List<Preset>> callback); void queryPreset(CommonGBChannel channel, ErrorCallback<List<Preset>> callback);
void dragZoom(CommonGBChannel channel, FrontEndControlCodeForDragZoom frontEndControlCode, ErrorCallback<String> callback);
} }

View File

@ -7,7 +7,6 @@ import com.genersoft.iot.vmp.gb28181.bean.CommonRecordInfo;
import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo; import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo;
import com.genersoft.iot.vmp.gb28181.bean.Platform; import com.genersoft.iot.vmp.gb28181.bean.Platform;
import com.genersoft.iot.vmp.service.bean.ErrorCallback; import com.genersoft.iot.vmp.service.bean.ErrorCallback;
import com.genersoft.iot.vmp.vmanager.bean.AudioTalkResult;
import java.util.List; import java.util.List;
@ -42,12 +41,4 @@ public interface IGbChannelPlayService {
void getSnap(CommonGBChannel channel, ErrorCallback<byte[]> callback); 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,8 +18,4 @@ public interface IPTZService {
void queryPresetList(CommonGBChannel channel, ErrorCallback<List<Preset>> callback); 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

@ -1,18 +0,0 @@
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,6 +25,4 @@ public interface ISourcePTZService {
void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper frontEndControlCode, ErrorCallback<String> callback); void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper frontEndControlCode, ErrorCallback<String> callback);
void queryPreset(CommonGBChannel channel, ErrorCallback<List<Preset>> 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 @Override
public boolean removeCatalogSubscribe(@NotNull Device device, CommonCallback<Boolean> callback) { public boolean removeCatalogSubscribe(@NotNull Device device, CommonCallback<Boolean> callback) {
log.info("[移除目录订阅]: {}", device.getDeviceId());
String key = SubscribeTaskForCatalog.getKey(device); String key = SubscribeTaskForCatalog.getKey(device);
if (subscribeTaskRunner.containsKey(key)) { if (subscribeTaskRunner.containsKey(key)) {
log.info("[移除目录订阅]: {}", device.getDeviceId());
SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key); SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key);
if (transactionInfo == null) { if (transactionInfo == null) {
log.warn("[移除目录订阅] 未找到事务信息,{}", device.getDeviceId()); log.warn("[移除目录订阅] 未找到事务信息,{}", device.getDeviceId());
@ -638,9 +638,9 @@ public class DeviceServiceImpl implements IDeviceService {
@Override @Override
public boolean removeMobilePositionSubscribe(Device device, CommonCallback<Boolean> callback) { public boolean removeMobilePositionSubscribe(Device device, CommonCallback<Boolean> callback) {
log.info("[移除移动位置订阅]: {}", device.getDeviceId());
String key = SubscribeTaskForMobilPosition.getKey(device); String key = SubscribeTaskForMobilPosition.getKey(device);
if (subscribeTaskRunner.containsKey(key)) { if (subscribeTaskRunner.containsKey(key)) {
log.info("[移除移动位置订阅]: {}", device.getDeviceId());
SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key); SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key);
if (transactionInfo == null) { if (transactionInfo == null) {
log.warn("[移除移动位置订阅] 未找到事务信息,{}", device.getDeviceId()); log.warn("[移除移动位置订阅] 未找到事务信息,{}", device.getDeviceId());
@ -703,9 +703,9 @@ public class DeviceServiceImpl implements IDeviceService {
@Override @Override
public boolean removeAlarmSubscribe(Device device, CommonCallback<Boolean> callback) { public boolean removeAlarmSubscribe(Device device, CommonCallback<Boolean> callback) {
log.info("[移除报警订阅]: {}", device.getDeviceId());
String key = SubscribeTaskForAlarm.getKey(device); String key = SubscribeTaskForAlarm.getKey(device);
if (subscribeTaskRunner.containsKey(key)) { if (subscribeTaskRunner.containsKey(key)) {
log.info("[移除报警订阅]: {}", device.getDeviceId());
SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key); SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key);
if (transactionInfo == null) { if (transactionInfo == null) {
log.warn("[移除报警订阅] 未找到事务信息,{}", device.getDeviceId()); log.warn("[移除报警订阅] 未找到事务信息,{}", device.getDeviceId());
@ -1276,9 +1276,9 @@ public class DeviceServiceImpl implements IDeviceService {
} }
@Override @Override
public void dragZoomIn(Device device, String channelId, int length, int width, int midPointX, int midPointY, int lengthX, int lengthY) { public void dragZoomIn(Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy, ErrorCallback<String> callback) {
if (!userSetting.getServerId().equals(device.getServerId())) { 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; return;
} }
@ -1286,23 +1286,24 @@ public class DeviceServiceImpl implements IDeviceService {
cmdXml.append("<DragZoomIn>\r\n"); cmdXml.append("<DragZoomIn>\r\n");
cmdXml.append("<Length>" + length+ "</Length>\r\n"); cmdXml.append("<Length>" + length+ "</Length>\r\n");
cmdXml.append("<Width>" + width+ "</Width>\r\n"); cmdXml.append("<Width>" + width+ "</Width>\r\n");
cmdXml.append("<MidPointX>" + midPointX+ "</MidPointX>\r\n"); cmdXml.append("<MidPointX>" + midpointx+ "</MidPointX>\r\n");
cmdXml.append("<MidPointY>" + midPointY+ "</MidPointY>\r\n"); cmdXml.append("<MidPointY>" + midpointy+ "</MidPointY>\r\n");
cmdXml.append("<LengthX>" + lengthX+ "</LengthX>\r\n"); cmdXml.append("<LengthX>" + lengthx+ "</LengthX>\r\n");
cmdXml.append("<LengthY>" + lengthY+ "</LengthY>\r\n"); cmdXml.append("<LengthY>" + lengthy+ "</LengthY>\r\n");
cmdXml.append("</DragZoomIn>\r\n"); cmdXml.append("</DragZoomIn>\r\n");
try { try {
sipCommander.dragZoomCmd(device, channelId, cmdXml.toString()); sipCommander.dragZoomCmd(device, channelId, cmdXml.toString(), callback);
} catch (InvalidArgumentException | SipException | ParseException e) { } catch (InvalidArgumentException | SipException | ParseException e) {
log.error("[命令发送失败] 拉框放大: {}", e.getMessage()); log.error("[命令发送失败] 拉框放大: {}", e.getMessage());
callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null);
throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
} }
} }
@Override @Override
public void dragZoomOut(Device device, String channelId, int length, int width, int midPointX, int midPointY, int lengthX, int lengthY) { public void dragZoomOut(Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy, ErrorCallback<String> callback) {
if (!userSetting.getServerId().equals(device.getServerId())) { 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; return;
} }
@ -1310,15 +1311,16 @@ public class DeviceServiceImpl implements IDeviceService {
cmdXml.append("<DragZoomOut>\r\n"); cmdXml.append("<DragZoomOut>\r\n");
cmdXml.append("<Length>" + length+ "</Length>\r\n"); cmdXml.append("<Length>" + length+ "</Length>\r\n");
cmdXml.append("<Width>" + width+ "</Width>\r\n"); cmdXml.append("<Width>" + width+ "</Width>\r\n");
cmdXml.append("<MidPointX>" + midPointX+ "</MidPointX>\r\n"); cmdXml.append("<MidPointX>" + midpointx+ "</MidPointX>\r\n");
cmdXml.append("<MidPointY>" + midPointY+ "</MidPointY>\r\n"); cmdXml.append("<MidPointY>" + midpointy+ "</MidPointY>\r\n");
cmdXml.append("<LengthX>" + lengthX+ "</LengthX>\r\n"); cmdXml.append("<LengthX>" + lengthx+ "</LengthX>\r\n");
cmdXml.append("<LengthY>" + lengthY+ "</LengthY>\r\n"); cmdXml.append("<LengthY>" + lengthy+ "</LengthY>\r\n");
cmdXml.append("</DragZoomOut>\r\n"); cmdXml.append("</DragZoomOut>\r\n");
try { try {
sipCommander.dragZoomCmd(device, channelId, cmdXml.toString()); sipCommander.dragZoomCmd(device, channelId, cmdXml.toString(), callback);
} catch (InvalidArgumentException | SipException | ParseException e) { } catch (InvalidArgumentException | SipException | ParseException e) {
log.error("[命令发送失败] 拉框放大: {}", e.getMessage()); log.error("[命令发送失败] 拉框放大: {}", e.getMessage());
callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null);
throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
} }
} }

View File

@ -125,17 +125,4 @@ public class GbChannelControlServiceImpl implements IGbChannelControlService {
} }
sourcePTZService.queryPreset(channel, callback); 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,11 +7,9 @@ import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.gb28181.bean.*; import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper; import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper;
import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService; 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.ISourceDownloadService;
import com.genersoft.iot.vmp.gb28181.service.ISourcePlayService; import com.genersoft.iot.vmp.gb28181.service.ISourcePlayService;
import com.genersoft.iot.vmp.gb28181.service.ISourcePlaybackService; 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.ErrorCallback;
import com.genersoft.iot.vmp.service.bean.InviteErrorCode; import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -41,9 +39,6 @@ public class GbChannelPlayServiceImpl implements IGbChannelPlayService {
@Autowired @Autowired
private Map<String, ISourceDownloadService> sourceDownloadServiceMap; private Map<String, ISourceDownloadService> sourceDownloadServiceMap;
@Autowired
private Map<String, ISourceBroadcastService> sourceBroadcastServiceMap;
@Override @Override
public void startInvite(CommonGBChannel channel, InviteMessageInfo inviteInfo, Platform platform, ErrorCallback<StreamInfo> callback) { public void startInvite(CommonGBChannel channel, InviteMessageInfo inviteInfo, Platform platform, ErrorCallback<StreamInfo> callback) {
@ -252,52 +247,4 @@ public class GbChannelPlayServiceImpl implements IGbChannelPlayService {
} }
sourceChannelPlayService.getSnap(channel, callback); 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,38 +87,10 @@ public class PTZServiceImpl implements IPTZService {
if (device == null) { if (device == null) {
throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备ID"); throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备ID");
} }
DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); DeviceChannel deviceChannel = deviceChannelService.getOneById(channel.getGbId());
frontEndCommand(device, deviceChannel.getDeviceId(), cmdCode, parameter1, parameter2, combindCode2); 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 @Override
public void queryPresetList(CommonGBChannel channel, ErrorCallback<List<Preset>> callback) { public void queryPresetList(CommonGBChannel channel, ErrorCallback<List<Preset>> callback) {
if (channel.getDataType() != ChannelDataType.GB28181) { if (channel.getDataType() != ChannelDataType.GB28181) {
@ -136,6 +108,4 @@ public class PTZServiceImpl implements IPTZService {
} }
deviceService.queryPreset(device, deviceChannel.getDeviceId(), callback); deviceService.queryPreset(device, deviceChannel.getDeviceId(), callback);
} }
} }

View File

@ -1229,12 +1229,6 @@ public class PlayServiceImpl implements IPlayService {
audioBroadcastResult.setApp(app); audioBroadcastResult.setApp(app);
audioBroadcastResult.setStream(stream); audioBroadcastResult.setStream(stream);
audioBroadcastResult.setStreamInfo(new StreamContent(mediaServerService.getStreamInfoByAppAndStream(mediaServerItem, app, stream, null, null, null, false))); 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"); audioBroadcastResult.setCodec("G.711");
return audioBroadcastResult; return audioBroadcastResult;
} }

View File

@ -1,85 +0,0 @@
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,11 +121,6 @@ public class SourcePTZServiceForGbImpl implements ISourcePTZService {
log.error("[FI失败] 未知的聚焦指令 {}", frontEndControlCode.getFocus()); log.error("[FI失败] 未知的聚焦指令 {}", frontEndControlCode.getFocus());
callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null); 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() != null) {
if (frontEndControlCode.getIris() == 0) { if (frontEndControlCode.getIris() == 0) {
@ -136,13 +131,18 @@ public class SourcePTZServiceForGbImpl implements ISourcePTZService {
log.error("[FI失败] 未知的光圈指令 {}", frontEndControlCode.getIris()); log.error("[FI失败] 未知的光圈指令 {}", frontEndControlCode.getIris());
callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null); callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null);
} }
}
if (frontEndControlCode.getFocusSpeed() == null) {
callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null);
return;
}
if (frontEndControlCode.getIrisSpeed() == null) { if (frontEndControlCode.getIrisSpeed() == null) {
callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null);
return; return;
} }
focusSpeed = frontEndControlCode.getFocusSpeed();
irisSpeed = frontEndControlCode.getIrisSpeed(); irisSpeed = frontEndControlCode.getIrisSpeed();
} }
}
ptzService.frontEndCommand(channel, cmdCode, focusSpeed, irisSpeed, parameter3); ptzService.frontEndCommand(channel, cmdCode, focusSpeed, irisSpeed, parameter3);
callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null);
}catch (Exception e) { }catch (Exception e) {
@ -348,16 +348,4 @@ public class SourcePTZServiceForGbImpl implements ISourcePTZService {
public void queryPreset(CommonGBChannel channel, ErrorCallback<List<Preset>> callback) { public void queryPreset(CommonGBChannel channel, ErrorCallback<List<Preset>> callback) {
ptzService.queryPresetList(channel, 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 channelId 通道id
* @param cmdString 前端控制指令串 * @param cmdString 前端控制指令串
*/ */
void dragZoomCmd(Device device, String channelId, String cmdString) throws InvalidArgumentException, SipException, ParseException; void dragZoomCmd(Device device, String channelId, String cmdString, ErrorCallback<String> callback) throws InvalidArgumentException, SipException, ParseException;
void playbackControlCmd(Device device, DeviceChannel channel, String stream, String content, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, 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()) { if (!mediaServerItem.isRtpEnable()) {
// 单端口暂不支持语音喊话 // 单端口暂不支持语音喊话
log.warn("[语音喊话] 单端口暂不支持此操作"); log.info("[语音喊话] 单端口暂不支持此操作");
return; return;
} }
@ -1292,7 +1292,7 @@ public class SIPCommander implements ISIPCommander {
} }
@Override @Override
public void dragZoomCmd(Device device, String channelId, String cmdString) throws InvalidArgumentException, SipException, ParseException { public void dragZoomCmd(Device device, String channelId, String cmdString, ErrorCallback<String> callback) throws InvalidArgumentException, SipException, ParseException {
String cmdType = "DeviceControl"; String cmdType = "DeviceControl";
int sn = (int) ((Math.random() * 9 + 1) * 100000); int sn = (int) ((Math.random() * 9 + 1) * 100000);
@ -1311,6 +1311,9 @@ public class SIPCommander implements ISIPCommander {
dragXml.append(cmdString); dragXml.append(cmdString);
dragXml.append("</Control>\r\n"); 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())); 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); sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()),request);
} }

View File

@ -241,19 +241,6 @@ public class DeviceControlQueryMessageHandler extends SIPRequestProcessorParent
} }
})); }));
break; 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: default:
log.info("[INFO 消息] 设备不支持的控制方式"); log.info("[INFO 消息] 设备不支持的控制方式");
try { try {
@ -403,7 +390,9 @@ public class DeviceControlQueryMessageHandler extends SIPRequestProcessorParent
cmdXml.append("<LengthX>" + dragZoom.getLengthX() + "</LengthX>\r\n"); cmdXml.append("<LengthX>" + dragZoom.getLengthX() + "</LengthX>\r\n");
cmdXml.append("<LengthY>" + dragZoom.getLengthY() + "</LengthY>\r\n"); cmdXml.append("<LengthY>" + dragZoom.getLengthY() + "</LengthY>\r\n");
cmdXml.append("</" + type.getVal() + ">\r\n"); cmdXml.append("</" + type.getVal() + ">\r\n");
cmder.dragZoomCmd(device, deviceChannel.getDeviceId(), cmdXml.toString()); cmder.dragZoomCmd(device, deviceChannel.getDeviceId(), cmdXml.toString(), (code, msg, data) -> {
});
responseAck(request, Response.OK); responseAck(request, Response.OK);
} catch (Exception e) { } catch (Exception e) {
log.error("[命令发送失败] 拉框控制: {}", e.getMessage()); log.error("[命令发送失败] 拉框控制: {}", e.getMessage());

View File

@ -155,9 +155,4 @@ public class SourcePTZServiceForJTImpl implements ISourcePTZService {
public void queryPreset(CommonGBChannel channel, ErrorCallback<List<Preset>> callback) { public void queryPreset(CommonGBChannel channel, ErrorCallback<List<Preset>> callback) {
callback.run(ErrorCode.ERROR486.getCode(), ErrorCode.ERROR486.getMsg(), null); 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,8 +4,10 @@ import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference; 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.bean.MediaServer;
import com.genersoft.iot.vmp.media.zlm.dto.*; import com.genersoft.iot.vmp.media.zlm.dto.*;
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import okhttp3.*; import okhttp3.*;
import okhttp3.logging.HttpLoggingInterceptor; import okhttp3.logging.HttpLoggingInterceptor;
@ -15,9 +17,11 @@ import org.springframework.stereotype.Component;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.net.ConnectException; import java.net.ConnectException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.URLEncoder;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -330,6 +334,11 @@ public class ZLMRESTfulUtils {
public ZLMResult<StreamProxyResult> addFFmpegSource(MediaServer mediaServer, String src_url, String dst_url, Integer timeout_sec, 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){ 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<>(); Map<String, Object> param = new HashMap<>();
param.put("src_url", src_url); 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); 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); WVPResult<String> deviceStatus(String serverId, Device device);

View File

@ -318,10 +318,10 @@ public class RedisRpcDeviceController extends RpcController {
String channelId = paramJson.getString("channelId"); String channelId = paramJson.getString("channelId");
Integer length = paramJson.getInteger("length"); Integer length = paramJson.getInteger("length");
Integer width = paramJson.getInteger("width"); Integer width = paramJson.getInteger("width");
Integer midPointX = paramJson.getInteger("midPointX"); Integer midpointx = paramJson.getInteger("midpointx");
Integer midPointY = paramJson.getInteger("midPointY"); Integer midpointy = paramJson.getInteger("midpointy");
Integer lengthX = paramJson.getInteger("lengthX"); Integer lengthx = paramJson.getInteger("lengthx");
Integer lengthY = paramJson.getInteger("lengthY"); Integer lengthy = paramJson.getInteger("lengthy");
Device device = deviceService.getDeviceByDeviceId(deviceId); Device device = deviceService.getDeviceByDeviceId(deviceId);
@ -332,15 +332,18 @@ public class RedisRpcDeviceController extends RpcController {
return response; return response;
} }
try { try {
deviceService.dragZoomIn(device, channelId, length, width, midPointX, midPointY, lengthX, lengthY); 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);
});
}catch (ControllerException e) { }catch (ControllerException e) {
response.setStatusCode(e.getCode()); response.setStatusCode(e.getCode());
response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg()));
return response; sendResponse(response);
} }
response.setStatusCode(ErrorCode.SUCCESS.getCode()); return null;
response.setBody(WVPResult.success());
return response;
} }
@RedisRpcMapping("dragZoomOut") @RedisRpcMapping("dragZoomOut")
@ -350,10 +353,10 @@ public class RedisRpcDeviceController extends RpcController {
String channelId = paramJson.getString("channelId"); String channelId = paramJson.getString("channelId");
Integer length = paramJson.getInteger("length"); Integer length = paramJson.getInteger("length");
Integer width = paramJson.getInteger("width"); Integer width = paramJson.getInteger("width");
Integer midPointX = paramJson.getInteger("midPointX"); Integer midpointx = paramJson.getInteger("midpointx");
Integer midPointY = paramJson.getInteger("midPointY"); Integer midpointy = paramJson.getInteger("midpointy");
Integer lengthX = paramJson.getInteger("lengthX"); Integer lengthx = paramJson.getInteger("lengthx");
Integer lengthY = paramJson.getInteger("lengthY"); Integer lengthy = paramJson.getInteger("lengthy");
Device device = deviceService.getDeviceByDeviceId(deviceId); Device device = deviceService.getDeviceByDeviceId(deviceId);
@ -364,15 +367,18 @@ public class RedisRpcDeviceController extends RpcController {
return response; return response;
} }
try { try {
deviceService.dragZoomOut(device, channelId, length, width, midPointX, midPointY, lengthX, lengthY); 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);
});
}catch (ControllerException e) { }catch (ControllerException e) {
response.setStatusCode(e.getCode()); response.setStatusCode(e.getCode());
response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg()));
return response; sendResponse(response);
} }
response.setStatusCode(ErrorCode.SUCCESS.getCode()); return null;
response.setBody(WVPResult.success());
return response;
} }
@RedisRpcMapping("alarm") @RedisRpcMapping("alarm")

View File

@ -433,33 +433,33 @@ public class RedisRpcServiceImpl implements IRedisRpcService {
} }
@Override @Override
public void dragZoomIn(String serverId, Device device, String channelId, int length, int width, int midPointX, public void dragZoomIn(String serverId, Device device, String channelId, int length, int width, int midpointx,
int midPointY, int lengthX, int lengthY) { int midpointy, int lengthx, int lengthy) {
JSONObject jsonObject = new JSONObject(); JSONObject jsonObject = new JSONObject();
jsonObject.put("device", device.getDeviceId()); jsonObject.put("device", device.getDeviceId());
jsonObject.put("channelId", channelId); jsonObject.put("channelId", channelId);
jsonObject.put("length", length); jsonObject.put("length", length);
jsonObject.put("width", width); jsonObject.put("width", width);
jsonObject.put("midPointX", midPointX); jsonObject.put("midpointx", midpointx);
jsonObject.put("midPointY", midPointY); jsonObject.put("midpointy", midpointy);
jsonObject.put("lengthX", lengthX); jsonObject.put("lengthx", lengthx);
jsonObject.put("lengthY", lengthY); jsonObject.put("lengthy", lengthy);
RedisRpcRequest request = buildRequest("device/dragZoomIn", jsonObject); RedisRpcRequest request = buildRequest("device/dragZoomIn", jsonObject);
request.setToId(serverId); request.setToId(serverId);
redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS);
} }
@Override @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 jsonObject = new JSONObject();
jsonObject.put("device", device.getDeviceId()); jsonObject.put("device", device.getDeviceId());
jsonObject.put("channelId", channelId); jsonObject.put("channelId", channelId);
jsonObject.put("length", length); jsonObject.put("length", length);
jsonObject.put("width", width); jsonObject.put("width", width);
jsonObject.put("midPointX", midPointX); jsonObject.put("midpointx", midpointx);
jsonObject.put("midPointY", midPointY); jsonObject.put("midpointy", midpointy);
jsonObject.put("lengthX", lengthX); jsonObject.put("lengthx", lengthx);
jsonObject.put("lengthY", lengthY); jsonObject.put("lengthy", lengthy);
RedisRpcRequest request = buildRequest("device/dragZoomOut", jsonObject); RedisRpcRequest request = buildRequest("device/dragZoomOut", jsonObject);
request.setToId(serverId); request.setToId(serverId);
redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS);

View File

@ -1,13 +1,8 @@
package com.genersoft.iot.vmp.vmanager.bean; package com.genersoft.iot.vmp.vmanager.bean;
import lombok.Getter;
import lombok.Setter;
/** /**
* @author lin * @author lin
*/ */
@Setter
@Getter
public class AudioBroadcastResult { public class AudioBroadcastResult {
/** /**
* 推流的各个方式流地址 * 推流的各个方式流地址
@ -29,10 +24,36 @@ public class AudioBroadcastResult {
*/ */
private String stream; 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

@ -1,15 +0,0 @@
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

@ -1,100 +0,0 @@
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

@ -1,66 +0,0 @@
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

@ -1,95 +0,0 @@
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,38 +270,6 @@ 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 }
})
}
// 前端控制 // 前端控制
@ -544,20 +512,6 @@ 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 }) { export function queryRecord({ channelId, startTime, endTime }) {
return request({ return request({
method: 'get', method: 'get',

View File

@ -69,14 +69,6 @@ export function resetGuard(deviceId) {
}) })
} }
export function homePosition(params) {
return request({
method: 'get',
url: '/api/device/control/home_position',
params
})
}
export function subscribeCatalog(params) { export function subscribeCatalog(params) {
const { id, cycle } = params const { id, cycle } = params
return request({ return request({
@ -283,19 +275,3 @@ 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({ return request({
method: 'get', method: 'get',
url: `/api/front-end/ptz/${deviceId}/${channelId}`, url: `/api/front-end/ptz/${deviceId}/${channelId}`,

View File

@ -1,208 +0,0 @@
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,9 +48,7 @@ import {
stopPlayback, stopPlayback,
pausePlayback, pausePlayback,
resumePlayback, 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' } from '@/api/commonChannel'
const actions = { const actions = {
@ -284,46 +282,6 @@ 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) { getList({ commit }, param) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getList(param).then(response => { getList(param).then(response => {
@ -534,26 +492,6 @@ 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) { queryRecord({ commit }, params) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
queryRecord(params).then(response => { queryRecord(params).then(response => {

View File

@ -3,7 +3,6 @@ import {
changeChannelAudio, changeChannelAudio,
deleteDevice, deleteDevice,
deviceRecord, getKeepaliveTimeStatistics, getRegisterTimeStatistics, deviceRecord, getKeepaliveTimeStatistics, getRegisterTimeStatistics,
homePosition,
queryBasicParam, queryBasicParam,
queryChannelOne, queryChannelOne,
queryChannels, queryChannels,
@ -84,16 +83,6 @@ 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) { subscribeCatalog({ commit }, params) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
subscribeCatalog(params).then(response => { subscribeCatalog(params).then(response => {

View File

@ -7,7 +7,6 @@ import {
startScan, stopCruise, startScan, stopCruise,
stopScan, wiper stopScan, wiper
} from '@/api/frontEnd' } from '@/api/frontEnd'
import { dragZoomIn, dragZoomOut } from '@/api/device'
const actions = { const actions = {
setSpeedForScan({ commit }, params) { setSpeedForScan({ commit }, params) {
@ -209,26 +208,6 @@ const actions = {
reject(error) 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-face {
font-family: "iconfont"; /* Project id 1291092 */ font-family: "iconfont"; /* Project id 1291092 */
src: url('iconfont.woff2?t=1780559263294') format('woff2'); src: url('iconfont.woff2?t=1769409737891') format('woff2')
} }
.iconfont { .iconfont {
@ -11,54 +11,6 @@
-moz-osx-font-smoothing: grayscale; -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 { .icon-xintiao:before {
content: "\e7f4"; content: "\e7f4";
} }

Binary file not shown.

View File

@ -161,11 +161,11 @@
<i class="el-icon-warning-outline" style="font-size: 32px;" /> <i class="el-icon-warning-outline" style="font-size: 32px;" />
<div style="margin-top: 10px;">{{ playbackError }}</div> <div style="margin-top: 10px;">{{ playbackError }}</div>
</div> </div>
<div v-else-if="playbackStreamInfo" style="height: 400px;"> <div v-else-if="playbackStreamInfo">
<playerTabs <h265web
ref="playbackPlayer" ref="playbackPlayer"
:height="'400px'"
:show-button="false" :show-button="false"
:showTab="true"
:has-audio="true" :has-audio="true"
/> />
</div> </div>
@ -177,7 +177,7 @@
</template> </template>
<script> <script>
import playerTabs from '../common/playerTabs.vue' import h265web from '../common/h265web.vue'
const ALARM_TYPE_OPTIONS = [ const ALARM_TYPE_OPTIONS = [
{ value: 'VideoLoss', label: '视频丢失报警' }, { value: 'VideoLoss', label: '视频丢失报警' },
@ -221,7 +221,7 @@ function formatDatetime(ts) {
export default { export default {
name: 'AlarmManage', name: 'AlarmManage',
components: { playerTabs }, components: { h265web },
data() { data() {
return { return {
alarmList: [], alarmList: [],
@ -238,6 +238,7 @@ export default {
playbackLoading: false, playbackLoading: false,
playbackError: null, playbackError: null,
playbackStreamInfo: null, playbackStreamInfo: null,
playbackVideoUrl: null,
playbackTitle: '录像回放', playbackTitle: '录像回放',
currentPlaybackChannelId: null currentPlaybackChannelId: null
} }
@ -307,10 +308,15 @@ export default {
endTime: endTime endTime: endTime
}).then(data => { }).then(data => {
this.playbackStreamInfo = data this.playbackStreamInfo = data
if (location.protocol === 'https:') {
this.playbackVideoUrl = data['wss_flv']
} else {
this.playbackVideoUrl = data['ws_flv']
}
this.playbackLoading = false this.playbackLoading = false
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.playbackPlayer) { if (this.$refs.playbackPlayer) {
this.$refs.playbackPlayer.setStreamInfo(data) this.$refs.playbackPlayer.play(this.playbackVideoUrl)
} }
}) })
}).catch(err => { }).catch(err => {
@ -320,9 +326,6 @@ export default {
}) })
}, },
closePlayback() { closePlayback() {
if (this.$refs.playbackPlayer) {
this.$refs.playbackPlayer.stop()
}
if (this.playbackStreamInfo && this.currentPlaybackChannelId) { if (this.playbackStreamInfo && this.currentPlaybackChannelId) {
this.$store.dispatch('commonChanel/stopPlayback', { this.$store.dispatch('commonChanel/stopPlayback', {
channelId: this.currentPlaybackChannelId, channelId: this.currentPlaybackChannelId,
@ -333,6 +336,7 @@ export default {
} }
this.playbackDialogVisible = false this.playbackDialogVisible = false
this.playbackStreamInfo = null this.playbackStreamInfo = null
this.playbackVideoUrl = null
this.playbackError = null this.playbackError = null
this.currentPlaybackChannelId = null this.currentPlaybackChannelId = null
}, },

View File

@ -1,448 +0,0 @@
<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

@ -1,82 +0,0 @@
<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

@ -1,91 +0,0 @@
<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

@ -1,269 +0,0 @@
<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

@ -1,54 +0,0 @@
<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

@ -1,155 +0,0 @@
<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

@ -1,177 +0,0 @@
<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

@ -1,59 +0,0 @@
<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

@ -91,12 +91,6 @@
</div> </div>
</template> </template>
</el-table-column> </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"> <el-table-column label="状态" min-width="100">
<template v-slot:default="scope"> <template v-slot:default="scope">
<div slot="reference" class="name-wrapper"> <div slot="reference" class="name-wrapper">
@ -126,7 +120,7 @@
<script> <script>
import GroupTree from '../../common/GroupTree.vue' import GroupTree from '../../common/GroupTree.vue'
import GbChannelSelect from '../../dialog/GbChannelSelect.vue' import GbChannelSelect from '../../dialog/GbChannelSelect.vue'
import UnusualGroupChannelSelect from './UnusualGroupChannelSelect.vue' import UnusualGroupChannelSelect from '../../dialog/UnusualGroupChannelSelect.vue'
export default { export default {
name: 'Group', name: 'Group',

View File

@ -1,6 +1,6 @@
<template> <template>
<div id="channelList" class="app-container" style="height: calc(100vh - 124px);"> <div id="channelList" class="app-container" style="height: calc(100vh - 124px);">
<div v-if="!editId && !showPtzConfig" style="height: 100%"> <div v-if="!editId" style="height: 100%">
<el-form :inline="true" size="mini"> <el-form :inline="true" size="mini">
<el-form-item label="搜索"> <el-form-item label="搜索">
<el-input <el-input
@ -149,10 +149,6 @@
设备录像</el-dropdown-item> 设备录像</el-dropdown-item>
<el-dropdown-item command="cloudRecords" :disabled="scope.row.gbStatus !== 'ON'"> <el-dropdown-item command="cloudRecords" :disabled="scope.row.gbStatus !== 'ON'">
云端录像</el-dropdown-item> 云端录像</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-menu>
</el-dropdown> </el-dropdown>
@ -172,8 +168,6 @@
</div> </div>
<devicePlayer ref="devicePlayer" /> <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" /> <channel-edit v-if="editId" :id="editId" :close-edit="closeEdit" />
<chooseCivilCode ref="chooseCivilCode" /> <chooseCivilCode ref="chooseCivilCode" />
<chooseGroup ref="chooseGroup" /> <chooseGroup ref="chooseGroup" />
@ -182,20 +176,18 @@
</template> </template>
<script> <script>
import devicePlayer from './player.vue' import devicePlayer from '@/views/common/channelPlayer/index.vue'
import audioTalk from './audioTalk.vue'
import ptzConfig from './ptzConfig.vue'
import Edit from './edit.vue' import Edit from './edit.vue'
import ChooseCivilCode from '../dialog/chooseCivilCode.vue' import ChooseCivilCode from '../dialog/chooseCivilCode.vue'
import ChooseGroup from '@/views/dialog/chooseGroup.vue' import ChooseGroup from '@/views/dialog/chooseGroup.vue'
import { MessageBox } from 'element-ui'
import store from '@/store'
export default { export default {
name: 'ChannelList', name: 'ChannelList',
components: { components: {
ChooseGroup, ChooseGroup,
devicePlayer, devicePlayer,
audioTalk,
ptzConfig,
ChooseCivilCode, ChooseCivilCode,
ChannelEdit: Edit ChannelEdit: Edit
}, },
@ -251,8 +243,6 @@ export default {
total: 0, total: 0,
beforeUrl: '/device', beforeUrl: '/device',
editId: null, editId: null,
showPtzConfig: false,
ptzConfigChannelId: null,
civilCodeName: null, civilCodeName: null,
civilCodeDeviceId: null, civilCodeDeviceId: null,
@ -403,11 +393,6 @@ export default {
this.queryRecords(itemData) this.queryRecords(itemData)
} else if (command === 'cloudRecords') { } else if (command === 'cloudRecords') {
this.queryCloudRecords(itemData) 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() { getCheckIds: function() {

View File

@ -1,158 +0,0 @@
<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

@ -1,105 +0,0 @@
<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,12 +90,6 @@
</div> </div>
</template> </template>
</el-table-column> </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"> <el-table-column label="状态" min-width="100">
<template v-slot:default="scope"> <template v-slot:default="scope">
<div slot="reference" class="name-wrapper"> <div slot="reference" class="name-wrapper">
@ -124,7 +118,7 @@
<script> <script>
import RegionTree from '../..//common/RegionTree.vue' import RegionTree from '../..//common/RegionTree.vue'
import GbChannelSelect from '../../dialog/GbChannelSelect.vue' import GbChannelSelect from '../../dialog/GbChannelSelect.vue'
import UnusualRegionChannelSelect from './UnusualRegionChannelSelect.vue' import UnusualRegionChannelSelect from '../../dialog/UnusualRegionChannelSelect.vue'
export default { export default {
name: 'Region', name: 'Region',

View File

@ -1,14 +1,28 @@
<template> <template>
<div id="cloudRecordPlayer" style="height: 100%"> <div id="cloudRecordPlayer" style="height: 100%">
<div class="cloud-record-playBox" :style="playBoxStyle"> <div class="cloud-record-playBox" :style="playBoxStyle">
<playerTabs <jessibucaPlayer
v-if="playerType === 'Jessibuca'"
ref="recordVideoPlayer" ref="recordVideoPlayer"
:height="'calc(100% - 250px)'"
:show-button="false" :show-button="false"
:showTab="false"
@playTimeChange="showPlayTimeChange" @playTimeChange="showPlayTimeChange"
@playStatusChange="playingChange" @playStatusChange="playingChange"
@player-changed="onPlayerChanged" fluent
autoplay
live
/> />
<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>
<div class="cloud-record-player-option-box"> <div class="cloud-record-player-option-box">
<div class="cloud-record-show-time"> <div class="cloud-record-show-time">
@ -33,6 +47,8 @@
<div class="cloud-record-record-play-control" style="background-color: transparent; box-shadow: 0 0 10px transparent"> <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 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-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> </div>
<div style="text-align: center;"> <div style="text-align: center;">
@ -65,7 +81,9 @@
<el-dropdown @command="changePlayerType" :popper-append-to-body='false' > <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> <a target="_blank" class="cloud-record-record-play-control-item record-play-control-speed" title="选择播放器">{{ playerLabel }}</a>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="p in playerList" :key="p.key" :command="p.key">{{ p.label }}</el-dropdown-item> <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-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</div> </div>
@ -79,7 +97,9 @@
<script> <script>
import playerTabs from '../common/playerTabs.vue' import h265web from '../common/h265web.vue'
import jessibucaPlayer from '@/views/common/jessibuca.vue'
import rtcPlayer from '../common/rtcPlayer.vue'
import moment from 'moment' import moment from 'moment'
import momentDurationFormatSetup from 'moment-duration-format' import momentDurationFormatSetup from 'moment-duration-format'
import screenfull from 'screenfull' import screenfull from 'screenfull'
@ -88,11 +108,14 @@ momentDurationFormatSetup(moment)
export default { export default {
name: 'CloudRecordPlayer', name: 'CloudRecordPlayer',
components: { playerTabs }, components: {
jessibucaPlayer, rtcPlayer, h265web
},
props: ['showListCallback', 'showNextCallback', 'showLastCallback', 'lastDiable', 'nextDiable'], props: ['showListCallback', 'showNextCallback', 'showLastCallback', 'lastDiable', 'nextDiable'],
data() { data() {
return { return {
showSidebar: false, showSidebar: false,
videoUrl: null,
streamInfo: null, streamInfo: null,
timeLen: null, timeLen: null,
startTime: null, startTime: null,
@ -105,8 +128,12 @@ export default {
isFullScreen: false, isFullScreen: false,
playing: false, playing: false,
initTime: null, initTime: null,
playerList: [], playerType: 'Jessibuca',
playerLabel: 'Jessibuca', playerUrls: {
Jessibuca: ['ws_flv', 'wss_flv'],
WebRTC: ['rtc', 'rtcs'],
H265web: ['ws_flv', 'wss_flv']
},
playSpeedRange: [1, 2, 4, 6, 8, 16, 20] playSpeedRange: [1, 2, 4, 6, 8, 16, 20]
} }
}, },
@ -132,7 +159,7 @@ export default {
} }
}, },
playTimeTotal() { playTimeTotal() {
return { left: `calc(${this.playerTime/this.streamInfo.duration}*100% - 6px)` } return { left: `calc(${this.playerTime/this.streamInfo.duration * 100}% - 6px)` }
}, },
playTimeTitleStyle() { playTimeTitleStyle() {
return { left: (this.showTimeLeft - 16) + 'px' } return { left: (this.showTimeLeft - 16) + 'px' }
@ -145,31 +172,21 @@ export default {
}else { }else {
return '' return ''
} }
},
playerLabel() {
const labels = { Jessibuca: 'Jessibuca', WebRTC: 'WebRTC', H265web: 'H265Web' }
return labels[this.playerType] || 'Jessibuca'
} }
}, },
created() { created() {
document.addEventListener('mousemove', this.timeProcessMousemove) document.addEventListener('mousemove', this.timeProcessMousemove)
document.addEventListener('mouseup', this.timeProcessMouseup) document.addEventListener('mouseup', this.timeProcessMouseup)
}, },
mounted() { mounted() {},
this.updatePlayerList()
},
destroyed() { destroyed() {
this.$destroy('recordVideoPlayer') this.$destroy('recordVideoPlayer')
}, },
methods: { 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) { timeProcessMouseup(event) {
this.isMousedown = false this.isMousedown = false
}, },
@ -198,14 +215,11 @@ export default {
this.showListCallback(this.showSidebar) this.showListCallback(this.showSidebar)
}, },
snap() { snap() {
if (this.$refs.recordVideoPlayer) {
this.$refs.recordVideoPlayer.screenshot() this.$refs.recordVideoPlayer.screenshot()
}
}, },
refresh() { refresh() {
if (this.$refs.recordVideoPlayer) {
this.$refs.recordVideoPlayer.destroy() this.$refs.recordVideoPlayer.destroy()
} this.$refs.recordVideoPlayer.playBtnClick()
}, },
playLast() { playLast() {
this.showLastCallback() this.showLastCallback()
@ -214,6 +228,7 @@ export default {
this.showNextCallback() this.showNextCallback()
}, },
changePlaySpeed(speed) { changePlaySpeed(speed) {
//
this.playSpeed = speed this.playSpeed = speed
this.$store.dispatch('cloudRecord/speed', { this.$store.dispatch('cloudRecord/speed', {
mediaServerId: this.streamInfo.mediaServerId, mediaServerId: this.streamInfo.mediaServerId,
@ -223,24 +238,32 @@ export default {
speed: this.playSpeed, speed: this.playSpeed,
schema: 'ts' schema: 'ts'
}) })
if (this.$refs.recordVideoPlayer) {
this.$refs.recordVideoPlayer.setPlaybackRate(this.playSpeed) this.$refs.recordVideoPlayer.setPlaybackRate(this.playSpeed)
}
}, },
changePlayerType(playerType) { changePlayerType(playerType) {
if (this.playerType === playerType) {
return
}
this.playerType = playerType
if (this.streamInfo) {
this.videoUrl = this.getUrlByStreamInfo()
this.$nextTick(() => {
if (this.$refs.recordVideoPlayer) { if (this.$refs.recordVideoPlayer) {
this.$refs.recordVideoPlayer.switchPlayer(playerType) this.$refs.recordVideoPlayer.play(this.videoUrl)
const p = this.playerList.find(p => p.key === playerType) }
this.playerLabel = p ? p.label : 'Jessibuca' })
} }
}, },
seekBackward() { seekBackward() {
// 退
this.seekRecord(this.playerTime - 5 * 1000) this.seekRecord(this.playerTime - 5 * 1000)
}, },
seekForward() { seekForward() {
//
this.seekRecord(this.playerTime + 5 * 1000) this.seekRecord(this.playerTime + 5 * 1000)
}, },
stopPLay() { stopPLay() {
//
if (this.$refs.recordVideoPlayer) { if (this.$refs.recordVideoPlayer) {
this.$refs.recordVideoPlayer.destroy() this.$refs.recordVideoPlayer.destroy()
} }
@ -249,49 +272,60 @@ export default {
this.playSpeed = 1 this.playSpeed = 1
}, },
pausePlay() { pausePlay() {
if (this.$refs.recordVideoPlayer) { //
this.$refs.recordVideoPlayer.pause() this.$refs.recordVideoPlayer.pause()
} // TODO
}, },
play() { play() {
if (this.$refs.recordVideoPlayer) { if (this.$refs.recordVideoPlayer.loaded) {
if (this.$refs.recordVideoPlayer.$refs[this.$refs.recordVideoPlayer.getActivePlayer()] && this.$refs.recordVideoPlayer.unPause()
this.$refs.recordVideoPlayer.$refs[this.$refs.recordVideoPlayer.getActivePlayer()].loaded) {
this.$refs.recordVideoPlayer.$refs[this.$refs.recordVideoPlayer.getActivePlayer()].unPause()
} else { } else {
this.playRecord() this.playRecord()
} }
}
}, },
fullScreen() { fullScreen() {
//
if (this.isFullScreen) { if (this.isFullScreen) {
screenfull.exit() screenfull.exit()
this.isFullScreen = false this.isFullScreen = false
return return
} }
const playerWrapper = this.$refs.recordVideoPlayer ? this.$refs.recordVideoPlayer.$refs.playerWrapper : null const playerWidth = this.$refs.recordVideoPlayer.playerWidth
const playerWidth = playerWrapper ? playerWrapper.clientWidth : 0 const playerHeight = this.$refs.recordVideoPlayer.playerHeight
const playerHeight = playerWrapper ? playerWrapper.clientHeight : 0
screenfull.request(document.getElementById('cloudRecordPlayer')) screenfull.request(document.getElementById('cloudRecordPlayer'))
screenfull.on('change', (event) => { screenfull.on('change', (event) => {
if (this.$refs.recordVideoPlayer) {
this.$refs.recordVideoPlayer.resize(playerWidth, playerHeight) this.$refs.recordVideoPlayer.resize(playerWidth, playerHeight)
}
this.isFullScreen = screenfull.isFullscreen this.isFullScreen = screenfull.isFullscreen
}) })
this.isFullScreen = true this.isFullScreen = true
}, },
setStreamInfo(streamInfo, timeLen, startTime) { 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.streamInfo = streamInfo
this.timeLen = timeLen this.timeLen = timeLen
this.startTime = startTime this.startTime = startTime
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.recordVideoPlayer) { if (this.$refs.recordVideoPlayer) {
console.log(streamInfo) this.$refs.recordVideoPlayer.play(this.videoUrl)
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) { seekRecord(playSeekValue, callback) {
this.$store.dispatch('cloudRecord/seek', { this.$store.dispatch('cloudRecord/seek', {
mediaServerId: this.streamInfo.mediaServerId, mediaServerId: this.streamInfo.mediaServerId,
@ -406,10 +440,10 @@ export default {
color: rgb(217, 217, 217); color: rgb(217, 217, 217);
font-size: 14px; font-size: 14px;
text-shadow: 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 { .record-play-control-player {
width: fit-content; width: fit-content;

View File

@ -58,7 +58,7 @@
import moment from 'moment' import moment from 'moment'
import momentDurationFormatSetup from 'moment-duration-format' import momentDurationFormatSetup from 'moment-duration-format'
import screenfull from 'screenfull' import screenfull from 'screenfull'
import cloudRecordPlayer from './player.vue' import cloudRecordPlayer from './cloudRecordPlayer.vue'
momentDurationFormatSetup(moment) momentDurationFormatSetup(moment)
@ -284,15 +284,12 @@ export default {
this.$refs.cloudRecordPlayer.setStreamInfo(data, this.detailFiles[this.chooseFileIndex].timeLen, this.detailFiles[this.chooseFileIndex].startTime) this.$refs.cloudRecordPlayer.setStreamInfo(data, this.detailFiles[this.chooseFileIndex].timeLen, this.detailFiles[this.chooseFileIndex].startTime)
}) })
.catch((error) => { .catch((error) => {
this.$message({ console.log(error)
showClose: true,
message: error,
type: 'error'
})
}) })
.finally(() => { .finally(() => {
this.playLoading = false this.playLoading = false
}) })
}, },
downloadFile(file) { downloadFile(file) {
this.$store.dispatch('cloudRecord/getPlayPath', file.id) this.$store.dispatch('cloudRecord/getPlayPath', file.id)

View File

@ -21,7 +21,7 @@
<script> <script>
import elDragDialog from '@/directive/el-drag-dialog' import elDragDialog from '@/directive/el-drag-dialog'
import cloudRecordPlayer from './player.vue' import cloudRecordPlayer from './cloudRecordPlayer.vue'
export default { export default {
name: 'PlayerDialog', name: 'PlayerDialog',

View File

@ -0,0 +1,910 @@
<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

@ -0,0 +1,88 @@
<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

@ -0,0 +1,923 @@
<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

@ -0,0 +1,363 @@
<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

@ -0,0 +1,162 @@
<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

@ -0,0 +1,233 @@
<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

@ -0,0 +1,72 @@
<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

@ -0,0 +1,62 @@
<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> <template>
<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="h265Player" ref="container" style="background-color: #000000; " @dblclick="fullscreenSwich">
<div :id="'glplayer-' + _uid" ref="playerBox" style="width: 100%; height: 100%; margin: 0 auto;"> <div id="glplayer" ref="playerBox" style="width: 100%; height: 100%; margin: 0 auto;" >
<div v-if="playerLoading" class="play-loading"> <div v-if="playerLoading" class="play-loading">
<i class="el-icon-loading" /> <i class="el-icon-loading" />
<span style="margin-left: 5px">视频加载中</span> 视频加载中
</div> </div>
</div> </div>
<div v-if="showButton" id="buttonsBox" class="buttons-box" :style="{ opacity: showBar ? 1 : 0, pointerEvents: showBar ? 'auto' : 'none' }"> <div v-if="showButton" id="buttonsBox" class="buttons-box">
<div class="buttons-box-left"> <div class="buttons-box-left">
<i v-if="!playing" class="iconfont icon-play h265web-btn" @click="unPause" /> <i v-if="!playing" class="iconfont icon-play h265web-btn" @click="unPause" />
<i v-if="playing" class="iconfont icon-pause h265web-btn" @click="pause" /> <i v-if="playing" class="iconfont icon-pause h265web-btn" @click="pause" />
@ -38,10 +38,8 @@ const h265webPlayer = {}
* @see https://github.com/numberwolf/h265web.js/blob/master/example_normal/index.js * @see https://github.com/numberwolf/h265web.js/blob/master/example_normal/index.js
*/ */
const token = 'base64:QXV0aG9yOmNoYW5neWFubG9uZ3xudW1iZXJ3b2xmLEdpdGh1YjpodHRwczovL2dpdGh1Yi5jb20vbnVtYmVyd29sZixFbWFpbDpwb3JzY2hlZ3QyM0Bmb3htYWlsLmNvbSxRUTo1MzEzNjU4NzIsSG9tZVBhZ2U6aHR0cDovL3h2aWRlby52aWRlbyxEaXNjb3JkOm51bWJlcndvbGYjODY5NCx3ZWNoYXI6bnVtYmVyd29sZjExLEJlaWppbmcsV29ya0luOkJhaWR1' const token = 'base64:QXV0aG9yOmNoYW5neWFubG9uZ3xudW1iZXJ3b2xmLEdpdGh1YjpodHRwczovL2dpdGh1Yi5jb20vbnVtYmVyd29sZixFbWFpbDpwb3JzY2hlZ3QyM0Bmb3htYWlsLmNvbSxRUTo1MzEzNjU4NzIsSG9tZVBhZ2U6aHR0cDovL3h2aWRlby52aWRlbyxEaXNjb3JkOm51bWJlcndvbGYjODY5NCx3ZWNoYXI6bnVtYmVyd29sZjExLEJlaWppbmcsV29ya0luOkJhaWR1'
import dragZoom from '../../mixins/dragZoom'
export default { export default {
name: 'H265web', name: 'H265web',
mixins: [dragZoom],
props: ['videoUrl', 'error', 'hasAudio', 'height', 'showButton'], props: ['videoUrl', 'error', 'hasAudio', 'height', 'showButton'],
data() { data() {
return { return {
@ -62,8 +60,7 @@ export default {
playerHeight: 0, playerHeight: 0,
inited: false, inited: false,
playerLoading: false, playerLoading: false,
mediaInfo: null, mediaInfo: null
showBar: true
} }
}, },
watch: { watch: {
@ -122,7 +119,7 @@ export default {
const options = {} const options = {}
h265webPlayer[this._uid] = new window.new265webjs(url, Object.assign( h265webPlayer[this._uid] = new window.new265webjs(url, Object.assign(
{ {
player: 'glplayer-' + this._uid, player: 'glplayer', // id
width: this.playerWidth, width: this.playerWidth,
height: this.playerHeight, height: this.playerHeight,
token: token, token: token,
@ -249,12 +246,6 @@ export default {
}, },
setPlaybackRate: function(speed) { setPlaybackRate: function(speed) {
h265webPlayer[this._uid].setPlaybackRate(speed) h265webPlayer[this._uid].setPlaybackRate(speed)
},
getVideoElement() {
return this.$refs.playerBox
},
getVideoRect() {
return this.getVideoElement().getBoundingClientRect()
} }
} }
} }
@ -273,15 +264,12 @@ export default {
} }
.buttons-box { .buttons-box {
width: 100%; width: 100%;
height: 56px; height: 28px;
background: linear-gradient(to top, rgba(0, 0, 0, 1), rgba(0, 0, 0, 0)); background-color: rgba(43, 51, 63, 0.7);
position: absolute; position: absolute;
transition: opacity 0.3s ease;
display: -webkit-box; display: -webkit-box;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
align-items: flex-end;
padding-bottom: 10px;
left: 0; left: 0;
bottom: 0; bottom: 0;
user-select: none; user-select: none;
@ -291,6 +279,7 @@ export default {
.h265web-btn { .h265web-btn {
width: 20px; width: 20px;
color: rgb(255, 255, 255); color: rgb(255, 255, 255);
line-height: 27px;
margin: 0px 10px; margin: 0px 10px;
padding: 0px 2px; padding: 0px 2px;
cursor: pointer; cursor: pointer;
@ -301,7 +290,6 @@ export default {
.buttons-box-right { .buttons-box-right {
position: absolute; position: absolute;
right: 0; right: 0;
bottom: 10px;
} }
.player-loading { .player-loading {
width: fit-content; width: fit-content;

View File

@ -3,9 +3,8 @@
ref="container" ref="container"
style="width:100%; height: 100%; background-color: #000000;margin:0 auto;position: relative;" style="width:100%; height: 100%; background-color: #000000;margin:0 auto;position: relative;"
@dblclick="fullscreenSwich" @dblclick="fullscreenSwich"
@mouseenter="showBar = true" @mouseleave="showBar = false"
> >
<div id="buttonsBox" class="buttons-box" v-if="showButton === undefined || showButton" :style="{ opacity: showBar ? 1 : 0, pointerEvents: showBar ? 'auto' : 'none' }"> <div id="buttonsBox" class="buttons-box" v-if="showButton === undefined || showButton">
<div class="buttons-box-left"> <div class="buttons-box-left">
<i v-if="!playing" class="iconfont icon-play jessibuca-btn" @click="playBtnClick" /> <i v-if="!playing" class="iconfont icon-play jessibuca-btn" @click="playBtnClick" />
<i v-if="playing" class="iconfont icon-pause jessibuca-btn" @click="pause" /> <i v-if="playing" class="iconfont icon-pause jessibuca-btn" @click="pause" />
@ -32,10 +31,8 @@
<script> <script>
const jessibucaPlayer = {} const jessibucaPlayer = {}
import dragZoom from '../../mixins/dragZoom'
export default { export default {
name: 'Jessibuca', name: 'Jessibuca',
mixins: [dragZoom],
props: ['videoUrl', 'error', 'hasAudio', 'height', 'showButton'], props: ['videoUrl', 'error', 'hasAudio', 'height', 'showButton'],
data() { data() {
return { return {
@ -54,8 +51,7 @@ export default {
rotate: 0, rotate: 0,
vod: true, // vod: true, //
forceNoOffscreen: false, forceNoOffscreen: false,
localVideoUrl: this.videoUrl, localVideoUrl: this.videoUrl
showBar: true
} }
}, },
created() { created() {
@ -282,14 +278,6 @@ export default {
if (jessibucaPlayer[this._uid]) { if (jessibucaPlayer[this._uid]) {
jessibucaPlayer[this._uid].resize() 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()
} }
} }
} }
@ -301,7 +289,8 @@ export default {
height: 28px; height: 28px;
background-color: rgba(43, 51, 63, 0.7); background-color: rgba(43, 51, 63, 0.7);
position: absolute; position: absolute;
transition: opacity 0.3s ease; display: -webkit-box;
display: -ms-flexbox;
display: flex; display: flex;
left: 0; left: 0;
bottom: 0; bottom: 0;
@ -313,7 +302,7 @@ export default {
width: 20px; width: 20px;
color: rgb(255, 255, 255); color: rgb(255, 255, 255);
line-height: 27px; line-height: 27px;
margin: 0px 20px; margin: 0px 10px;
padding: 0px 2px; padding: 0px 2px;
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;

View File

@ -91,6 +91,11 @@ export default {
} }
</script> </script>
<style> <style>
#mediaInfo { position: relative; } .channel-form {
#mediaInfo >>> .el-descriptions__title { font-size: 14px; color: #606266; font-weight: 600; } display: grid;
background-color: #FFFFFF;
padding: 1rem 2rem 0 2rem;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
}
</style> </style>

View File

@ -1,185 +0,0 @@
<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

@ -1,305 +0,0 @@
<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

@ -0,0 +1,328 @@
<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

@ -0,0 +1,152 @@
<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

@ -0,0 +1,212 @@
<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

@ -0,0 +1,76 @@
<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

@ -0,0 +1,58 @@
<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,17 +1,15 @@
<template> <template>
<div :id="'rtcPlayer-' + _uid" class="rtc-player-wrapper"> <div id="rtcPlayer">
<video :id="'webRtcPlayerBox-' + _uid" class="rtc-player-video" :controls="showControls" autoplay style="text-align:left;"> <video id="webRtcPlayerBox" :controls="showControls" autoplay style="text-align:left;">
Your browser is too old which doesn't support HTML5 video. Your browser is too old which doesn't support HTML5 video.
</video> </video>
</div> </div>
</template> </template>
<script> <script>
const webrtcPlayer = {} let webrtcPlayer = null
import dragZoom from '../../mixins/dragZoom'
export default { export default {
name: 'RtcPlayer', name: 'RtcPlayer',
mixins: [dragZoom],
props: { props: {
videoUrl: { type: String, default: '' }, videoUrl: { type: String, default: '' },
error: { default: '' }, error: { default: '' },
@ -23,20 +21,20 @@ export default {
timer: null timer: null
} }
}, },
mounted() {}, mounted() {},
destroyed() { destroyed() {
clearTimeout(this.timer) clearTimeout(this.timer)
this.pause()
}, },
methods: { methods: {
play: function(url) { play: function(url) {
if (webrtcPlayer[this._uid]) { if (webrtcPlayer != null) {
this.pause() this.pause()
} }
webrtcPlayer[this._uid] = new ZLMRTCClient.Endpoint({ webrtcPlayer = new ZLMRTCClient.Endpoint({
element: document.getElementById('webRtcPlayerBox-' + this._uid), element: document.getElementById('webRtcPlayerBox'), // video
debug: true, debug: true, //
zlmsdpUrl: url, zlmsdpUrl: url, //
simulecast: false, simulecast: false,
useCamera: false, useCamera: false,
audioEnable: true, audioEnable: true,
@ -44,37 +42,37 @@ export default {
recvOnly: true, recvOnly: true,
usedatachannel: false usedatachannel: false
}) })
const player = webrtcPlayer[this._uid] webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, (e) => { // ICE
player.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, (e) => {
console.error('ICE 协商出错') console.error('ICE 协商出错')
this.eventcallbacK('ICE ERROR', 'ICE 协商出错') this.eventcallbacK('ICE ERROR', 'ICE 协商出错')
}) })
player.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, (e) => { webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, (e) => { //
console.log('播放成功', e.streams) console.log('播放成功', e.streams)
this.eventcallbacK('playing', '播放成功') this.eventcallbacK('playing', '播放成功')
}) })
player.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, (e) => { webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, (e) => { // offer anwser
console.error('offer anwser 交换失败', e) console.error('offer anwser 交换失败', e)
this.eventcallbacK('OFFER ANSWER ERROR ', 'offer anwser 交换失败') this.eventcallbacK('OFFER ANSWER ERROR ', 'offer anwser 交换失败')
if (e.code == -400 && e.msg == '流不存在') { if (e.code == -400 && e.msg == '流不存在') {
console.log('流不存在') console.log('流不存在')
this.timer = setTimeout(() => { this.timer = setTimeout(() => {
player.close() this.webrtcPlayer.close()
this.play(url) this.play(url)
}, 100) }, 100)
} }
}) })
player.on(ZLMRTCClient.Events.WEBRTC_ON_LOCAL_STREAM, (s) => { webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_ON_LOCAL_STREAM, (s) => { //
// document.getElementById('selfVideo').srcObject=s;
this.eventcallbacK('LOCAL STREAM', '获取到了本地流') this.eventcallbacK('LOCAL STREAM', '获取到了本地流')
}) })
}, },
pause: function() { pause: function() {
if (webrtcPlayer[this._uid]) { if (webrtcPlayer != null) {
webrtcPlayer[this._uid].close() webrtcPlayer.close()
webrtcPlayer[this._uid] = null webrtcPlayer = null
} }
}, },
stop: function() { stop: function() {
@ -84,35 +82,6 @@ export default {
console.log('player 事件回调') console.log('player 事件回调')
console.log(type) console.log(type)
console.log(message) 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
} }
} }
} }
@ -122,12 +91,11 @@ export default {
.LodingTitle { .LodingTitle {
min-width: 70px; min-width: 70px;
} }
.rtc-player-wrapper{ #rtcPlayer{
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative;
} }
.rtc-player-video{ #webRtcPlayerBox{
width: 100%; width: 100%;
height: 100%; height: 100%;
max-height: 100%; max-height: 100%;

View File

@ -1,75 +0,0 @@
<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> <script>
import veHistogram from 'v-charts/lib/histogram' import veHistogram from 'v-charts/lib/histogram'
import HasStreamChannel from "@/views/dashboard/dialog/hasStreamChannel.vue"; import HasStreamChannel from "@/views/dialog/hasStreamChannel";
export default { export default {
name: 'ConsoleNodeLoad', name: 'ConsoleNodeLoad',

View File

@ -1,6 +1,6 @@
<template> <template>
<div id="channelList" style="height: calc(100vh - 124px);"> <div id="channelList" style="height: calc(100vh - 124px);">
<div v-if="!editId && !ptzConfigChannelDeviceId" style="height: 100%"> <div v-if="!editId" style="height: 100%">
<el-form :inline="true" size="mini"> <el-form :inline="true" size="mini">
<el-form-item style="margin-right: 2rem"> <el-form-item style="margin-right: 2rem">
<el-page-header content="通道列表" @back="showDevice" /> <el-page-header content="通道列表" @back="showDevice" />
@ -189,10 +189,6 @@
设备录像控制-开始</el-dropdown-item> 设备录像控制-开始</el-dropdown-item>
<el-dropdown-item command="stopRecord" :disabled="device == null || device.online === 0"> <el-dropdown-item command="stopRecord" :disabled="device == null || device.online === 0">
设备录像控制-停止</el-dropdown-item> 设备录像控制-停止</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-menu>
</el-dropdown> </el-dropdown>
@ -213,25 +209,19 @@
<devicePlayer ref="devicePlayer" /> <devicePlayer ref="devicePlayer" />
<channel-edit v-if="editId" :id="editId" :close-edit="closeEdit" /> <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> </div>
</template> </template>
<script> <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 Edit from './edit.vue'
import ptzConfig from '@/views/device/channel/ptzConfig.vue'
export default { export default {
name: 'ChannelList', name: 'ChannelList',
components: { components: {
devicePlayer, devicePlayer,
audioTalk, ChannelEdit: Edit
ChannelEdit: Edit,
ptzConfig
}, },
props: { props: {
defaultPage: { defaultPage: {
@ -268,8 +258,6 @@ export default {
total: 0, total: 0,
beforeUrl: '/device', beforeUrl: '/device',
editId: null, editId: null,
ptzConfigDeviceId: null,
ptzConfigChannelDeviceId: null,
loadSnap: {}, loadSnap: {},
ptzTypes: { ptzTypes: {
0: '未知', 0: '未知',
@ -290,6 +278,7 @@ export default {
} }
}, },
mounted() { mounted() {
console.log(23222)
if (this.deviceId) { if (this.deviceId) {
this.$store.dispatch('device/queryDeviceOne', this.deviceId) this.$store.dispatch('device/queryDeviceOne', this.deviceId)
.then(data => { .then(data => {
@ -383,10 +372,6 @@ export default {
itemData.playLoading = false itemData.playLoading = false
}) })
}, },
closePtzConfig: function() {
this.ptzConfigDeviceId = null
this.ptzConfigChannelDeviceId = null
},
moreClick: function(command, itemData) { moreClick: function(command, itemData) {
if (command === 'records') { if (command === 'records') {
this.queryRecords(itemData) this.queryRecords(itemData)
@ -396,12 +381,6 @@ export default {
this.startRecord(itemData) this.startRecord(itemData)
} else if (command === 'stopRecord') { } else if (command === 'stopRecord') {
this.stopRecord(itemData) 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) { queryRecords: function(itemData) {

View File

@ -1,106 +0,0 @@
<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

@ -1,75 +0,0 @@
<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

@ -1,93 +0,0 @@
<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

@ -1,267 +0,0 @@
<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

@ -1,156 +0,0 @@
<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

@ -1,178 +0,0 @@
<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

@ -1,60 +0,0 @@
<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

@ -1,34 +0,0 @@
<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

@ -1,373 +0,0 @@
<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

@ -1,164 +0,0 @@
<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

@ -1,52 +0,0 @@
<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> <script>
import deviceEdit from './edit.vue' import deviceEdit from './edit.vue'
import syncChannelProgress from './dialog/SyncChannelProgress.vue' import syncChannelProgress from '../dialog/SyncChannelProgress.vue'
import configInfo from '../dialog/configInfo.vue' import configInfo from '../dialog/configInfo.vue'
import timeStatistics from './dialog/timeStatistics.vue' import timeStatistics from './timeStatistics.vue'
import Vue from 'vue' import Vue from 'vue'
export default { export default {

View File

@ -75,12 +75,6 @@
</div> </div>
</template> </template>
</el-table-column> </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"> <el-table-column label="状态" min-width="100">
<template v-slot:default="scope"> <template v-slot:default="scope">
<div slot="reference" class="name-wrapper"> <div slot="reference" class="name-wrapper">

View File

@ -90,12 +90,6 @@
</div> </div>
</template> </template>
</el-table-column> </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"> <el-table-column label="状态" min-width="100">
<template v-slot:default="scope"> <template v-slot:default="scope">
<div slot="reference" class="name-wrapper"> <div slot="reference" class="name-wrapper">

View File

@ -90,12 +90,6 @@
</div> </div>
</template> </template>
</el-table-column> </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"> <el-table-column label="状态" min-width="100">
<template v-slot:default="scope"> <template v-slot:default="scope">
<div slot="reference" class="name-wrapper"> <div slot="reference" class="name-wrapper">

Some files were not shown because too many files have changed in this diff Show More