mirror of
https://gitee.com/pan648540858/wvp-GB28181-pro.git
synced 2026-06-22 11:07:48 +08:00
支持通用通道喊话
This commit is contained in:
parent
58cc8a8baf
commit
f6ca930492
@ -16,6 +16,7 @@ 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) {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import com.genersoft.iot.vmp.gb28181.utils.VectorTileCatch;
|
|||||||
import com.genersoft.iot.vmp.service.bean.ErrorCallback;
|
import com.genersoft.iot.vmp.service.bean.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;
|
||||||
@ -352,6 +353,42 @@ 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)
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import com.genersoft.iot.vmp.gb28181.bean.CommonRecordInfo;
|
|||||||
import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo;
|
import com.genersoft.iot.vmp.gb28181.bean.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;
|
||||||
|
|
||||||
@ -41,4 +42,12 @@ 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.genersoft.iot.vmp.gb28181.service;
|
||||||
|
|
||||||
|
import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
|
||||||
|
import com.genersoft.iot.vmp.vmanager.bean.AudioTalkResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源能力接入-语音对讲
|
||||||
|
*/
|
||||||
|
public interface ISourceBroadcastService {
|
||||||
|
|
||||||
|
AudioTalkResult startTalk(CommonGBChannel channel);
|
||||||
|
|
||||||
|
void stopTalk(CommonGBChannel channel);
|
||||||
|
|
||||||
|
AudioTalkResult startBroadcast(CommonGBChannel channel);
|
||||||
|
|
||||||
|
void stopBroadcast(CommonGBChannel channel);
|
||||||
|
}
|
||||||
@ -7,9 +7,11 @@ 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;
|
||||||
@ -39,6 +41,9 @@ 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) {
|
||||||
@ -247,4 +252,52 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,101 @@
|
|||||||
|
package com.genersoft.iot.vmp.gb28181.service.impl;
|
||||||
|
|
||||||
|
import com.genersoft.iot.vmp.common.enums.ChannelDataType;
|
||||||
|
import com.genersoft.iot.vmp.common.enums.MediaStreamUtil;
|
||||||
|
import com.genersoft.iot.vmp.conf.UserSetting;
|
||||||
|
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.media.bean.MediaServer;
|
||||||
|
import com.genersoft.iot.vmp.media.service.IMediaServerService;
|
||||||
|
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 com.genersoft.iot.vmp.vmanager.bean.StreamContent;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IMediaServerService mediaServerService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserSetting userSetting;
|
||||||
|
|
||||||
|
@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);
|
||||||
|
MediaServer mediaServer = mediaServerService.getMediaServerForMinimumLoad(null);
|
||||||
|
StreamContent playStream = new StreamContent(
|
||||||
|
mediaServerService.getStreamInfoByAppAndStream(mediaServer,
|
||||||
|
MediaStreamUtil.GB28181_TALK, abResult.getStream() + "_talk",
|
||||||
|
null, null, null, false));
|
||||||
|
AudioTalkResult result = new AudioTalkResult();
|
||||||
|
result.setPushStream(abResult.getStreamInfo());
|
||||||
|
result.setPlayStream(playStream);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.genersoft.iot.vmp.vmanager.bean;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "对讲信息")
|
||||||
|
public class AudioTalkResult {
|
||||||
|
|
||||||
|
@Schema(description = "推流地址(浏览器 WebRTC推流到ZLM)")
|
||||||
|
private StreamContent pushStream;
|
||||||
|
|
||||||
|
@Schema(description = "播放地址(设备音频通过ZLM播放给浏览器),喊话时为null")
|
||||||
|
private StreamContent playStream;
|
||||||
|
}
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
package com.genersoft.iot.vmp.gb28181.bean;
|
||||||
|
|
||||||
|
import org.dom4j.DocumentHelper;
|
||||||
|
import org.dom4j.Element;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class DeviceAlarmNotifyTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromXml_withoutAlarmType_shouldNotThrowNpe() throws Exception {
|
||||||
|
String xml = """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Notify>
|
||||||
|
<DeviceID>55123456781381000010</DeviceID>
|
||||||
|
<AlarmPriority>1</AlarmPriority>
|
||||||
|
<AlarmMethod>7</AlarmMethod>
|
||||||
|
<AlarmTime>2026-06-05T09:46:05</AlarmTime>
|
||||||
|
<AlarmDescription>1001,1780623964994529058,55123456781381000010,25123456781381000050,55LCPCweb10</AlarmDescription>
|
||||||
|
<Longitude>0.0</Longitude>
|
||||||
|
<Latitude>0.0</Latitude>
|
||||||
|
</Notify>
|
||||||
|
""";
|
||||||
|
|
||||||
|
Element root = DocumentHelper.parseText(xml).getRootElement();
|
||||||
|
DeviceAlarmNotify notify = DeviceAlarmNotify.fromXml(root);
|
||||||
|
|
||||||
|
assertNotNull(notify);
|
||||||
|
assertEquals(Integer.valueOf(7), notify.getAlarmMethod());
|
||||||
|
assertNull(notify.getAlarmType(), "AlarmType should be null when not present in XML");
|
||||||
|
|
||||||
|
// Simulate the exact code path from AlarmNotifyMessageHandler.executeTaskQueue lines 131-141
|
||||||
|
// which was causing the NPE
|
||||||
|
AlarmChannelMessage alarmChannelMessage = new AlarmChannelMessage();
|
||||||
|
assertDoesNotThrow(() -> {
|
||||||
|
alarmChannelMessage.setAlarmType(notify.getAlarmType());
|
||||||
|
alarmChannelMessage.setAlarmSn(notify.getAlarmMethod());
|
||||||
|
alarmChannelMessage.setAlarmDescription(notify.getAlarmDescription());
|
||||||
|
alarmChannelMessage.setGbId(notify.getChannelId());
|
||||||
|
}, "setAlarmType(null) should not throw NPE when field type is Integer");
|
||||||
|
assertNull(alarmChannelMessage.getAlarmType());
|
||||||
|
assertEquals(Integer.valueOf(7), alarmChannelMessage.getAlarmSn());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromXml_withAlarmType_shouldParseCorrectly() throws Exception {
|
||||||
|
String xml = """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Notify>
|
||||||
|
<DeviceID>34020000001320000001</DeviceID>
|
||||||
|
<AlarmPriority>1</AlarmPriority>
|
||||||
|
<AlarmMethod>2</AlarmMethod>
|
||||||
|
<AlarmTime>2026-06-05T10:30:00</AlarmTime>
|
||||||
|
<AlarmDescription>Video loss alarm</AlarmDescription>
|
||||||
|
<Longitude>116.397</Longitude>
|
||||||
|
<Latitude>39.908</Latitude>
|
||||||
|
<AlarmType>1</AlarmType>
|
||||||
|
</Notify>
|
||||||
|
""";
|
||||||
|
|
||||||
|
Element root = DocumentHelper.parseText(xml).getRootElement();
|
||||||
|
DeviceAlarmNotify notify = DeviceAlarmNotify.fromXml(root);
|
||||||
|
|
||||||
|
assertNotNull(notify);
|
||||||
|
assertEquals(Integer.valueOf(2), notify.getAlarmMethod());
|
||||||
|
assertEquals(Integer.valueOf(1), notify.getAlarmType());
|
||||||
|
|
||||||
|
AlarmChannelMessage msg = new AlarmChannelMessage();
|
||||||
|
assertDoesNotThrow(() -> msg.setAlarmType(notify.getAlarmType()));
|
||||||
|
assertEquals(Integer.valueOf(1), msg.getAlarmType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromXml_withAlarmTypeInInfo_shouldUseInfoValue() throws Exception {
|
||||||
|
String xml = """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Notify>
|
||||||
|
<DeviceID>34020000001320000001</DeviceID>
|
||||||
|
<AlarmPriority>1</AlarmPriority>
|
||||||
|
<AlarmMethod>5</AlarmMethod>
|
||||||
|
<AlarmTime>2026-06-05T10:30:00</AlarmTime>
|
||||||
|
<AlarmDescription>Motion detection</AlarmDescription>
|
||||||
|
<Longitude>116.397</Longitude>
|
||||||
|
<Latitude>39.908</Latitude>
|
||||||
|
<AlarmType>9</AlarmType>
|
||||||
|
<Info>
|
||||||
|
<AlarmType>2</AlarmType>
|
||||||
|
</Info>
|
||||||
|
</Notify>
|
||||||
|
""";
|
||||||
|
|
||||||
|
Element root = DocumentHelper.parseText(xml).getRootElement();
|
||||||
|
DeviceAlarmNotify notify = DeviceAlarmNotify.fromXml(root);
|
||||||
|
|
||||||
|
assertNotNull(notify);
|
||||||
|
assertEquals(Integer.valueOf(2), notify.getAlarmType(),
|
||||||
|
"AlarmType should use Info/AlarmType value when present");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
package com.genersoft.iot.vmp.jt1078.dao.provider;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class JTChannelProviderTest {
|
||||||
|
|
||||||
|
private final JTChannelProvider provider = new JTChannelProvider();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectAll_withQuery_shouldUseBindVariable() {
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("terminalDbId", 1);
|
||||||
|
params.put("query", "test-channel");
|
||||||
|
String sql = provider.selectAll(params);
|
||||||
|
assertTrue(sql.contains("#{query}"), "should use #{query} bind variable");
|
||||||
|
assertFalse(sql.contains("test-channel"), "should not contain raw query value");
|
||||||
|
assertTrue(sql.contains("concat('%',#{query},'%')"), "should use concat with bind variable");
|
||||||
|
assertTrue(sql.contains("#{terminalDbId}"), "should use #{terminalDbId} bind variable");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectAll_withoutQuery_shouldNotContainLike() {
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("terminalDbId", 1);
|
||||||
|
String sql = provider.selectAll(params);
|
||||||
|
assertFalse(sql.contains("LIKE"), "should not contain LIKE clause when no query");
|
||||||
|
assertTrue(sql.contains("#{terminalDbId}"), "should still have terminalDbId condition");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectChannelByChannelId_shouldUseBindVariables() {
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("terminalDbId", 5);
|
||||||
|
params.put("channelId", 100);
|
||||||
|
String sql = provider.selectChannelByChannelId(params);
|
||||||
|
assertTrue(sql.contains("#{terminalDbId}"), "should use #{terminalDbId}");
|
||||||
|
assertTrue(sql.contains("#{channelId}"), "should use #{channelId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectChannelById_shouldUseBindVariable() {
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("id", 42);
|
||||||
|
String sql = provider.selectChannelById(params);
|
||||||
|
assertTrue(sql.contains("#{id}"), "should use #{id} bind variable");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectAll_shouldOrderByChannelId() {
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("terminalDbId", 1);
|
||||||
|
String sql = provider.selectAll(params);
|
||||||
|
assertTrue(sql.contains("ORDER BY jc.channel_id"), "should order by channel_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void baseSql_shouldHaveJoins() {
|
||||||
|
assertTrue(JTChannelProvider.BASE_SQL.contains("LEFT join wvp_device_channel"), "should have LEFT JOIN");
|
||||||
|
assertTrue(JTChannelProvider.BASE_SQL.contains("wvp_jt_channel"), "should query from jt_channel");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
package com.genersoft.iot.vmp.streamProxy.dao.provider;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class StreamProxyProviderTest {
|
||||||
|
|
||||||
|
private final StreamProxyProvider provider = new StreamProxyProvider();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void select_shouldUseBindVariable() {
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("id", 123);
|
||||||
|
String sql = provider.select(params);
|
||||||
|
assertTrue(sql.contains("#{id}"), "should use #{id} bind variable");
|
||||||
|
assertFalse(sql.contains("123"), "should not contain raw value");
|
||||||
|
assertTrue(sql.contains("WHERE st.id = #{id}"), "should have proper WHERE clause");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectOneByAppAndStream_shouldUseBindVariables() {
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("app", "testApp");
|
||||||
|
params.put("stream", "testStream");
|
||||||
|
String sql = provider.selectOneByAppAndStream(params);
|
||||||
|
assertTrue(sql.contains("#{app}"), "should use #{app} bind variable");
|
||||||
|
assertTrue(sql.contains("#{stream}"), "should use #{stream} bind variable");
|
||||||
|
assertFalse(sql.contains("testApp"), "should not contain raw app value");
|
||||||
|
assertFalse(sql.contains("testStream"), "should not contain raw stream value");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectForPushingInMediaServer_shouldUseBindVariable() {
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("mediaServerId", "server-001");
|
||||||
|
String sql = provider.selectForPushingInMediaServer(params);
|
||||||
|
assertTrue(sql.contains("#{mediaServerId}"), "should use #{mediaServerId} bind variable");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectAll_withQuery_shouldUseBindVariable() {
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("query", "test-query");
|
||||||
|
String sql = provider.selectAll(params);
|
||||||
|
assertTrue(sql.contains("#{query}"), "should use #{query} bind variable");
|
||||||
|
assertFalse(sql.contains("test-query"), "should not contain raw query value");
|
||||||
|
assertTrue(sql.contains("LIKE concat('%',#{query},'%')"), "should use concat with bind variable");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectAll_withMediaServerId_shouldUseBindVariable() {
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("mediaServerId", "server-001");
|
||||||
|
String sql = provider.selectAll(params);
|
||||||
|
assertTrue(sql.contains("#{mediaServerId}"), "should use #{mediaServerId} bind variable");
|
||||||
|
assertFalse(sql.contains("server-001"), "should not contain raw server id");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectAll_withPullingTrue() {
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("pulling", true);
|
||||||
|
String sql = provider.selectAll(params);
|
||||||
|
assertTrue(sql.contains("st.pulling=1"), "should filter by pulling=1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectAll_withPullingFalse() {
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("pulling", false);
|
||||||
|
String sql = provider.selectAll(params);
|
||||||
|
assertTrue(sql.contains("st.pulling=0"), "should filter by pulling=0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectAll_withoutParams_shouldReturnBaseQuery() {
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
String sql = provider.selectAll(params);
|
||||||
|
assertTrue(sql.contains("FROM wvp_stream_proxy"), "should have FROM clause");
|
||||||
|
assertTrue(sql.contains("LEFT join wvp_device_channel"), "should have JOIN clause");
|
||||||
|
assertTrue(sql.contains("order by"), "should have ORDER BY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getBaseSelectSql_shouldReturnValidSql() {
|
||||||
|
String sql = provider.getBaseSelectSql();
|
||||||
|
assertTrue(sql.contains("SELECT"), "should start with SELECT");
|
||||||
|
assertTrue(sql.contains("FROM wvp_stream_proxy"), "should have FROM");
|
||||||
|
assertTrue(sql.contains("LEFT join wvp_device_channel"), "should have LEFT JOIN");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -270,6 +270,38 @@ export function stopPlayChannel(channelId) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function talkStart(channelId) {
|
||||||
|
return request({
|
||||||
|
method: 'get',
|
||||||
|
url: '/api/common/channel/talk/start',
|
||||||
|
params: { channelId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function talkStop(channelId) {
|
||||||
|
return request({
|
||||||
|
method: 'get',
|
||||||
|
url: '/api/common/channel/talk/stop',
|
||||||
|
params: { channelId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function broadcastStart(channelId) {
|
||||||
|
return request({
|
||||||
|
method: 'get',
|
||||||
|
url: '/api/common/channel/broadcast/start',
|
||||||
|
params: { channelId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function broadcastStop(channelId) {
|
||||||
|
return request({
|
||||||
|
method: 'get',
|
||||||
|
url: '/api/common/channel/broadcast/stop',
|
||||||
|
params: { channelId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 前端控制
|
// 前端控制
|
||||||
|
|
||||||
|
|||||||
@ -49,7 +49,8 @@ import {
|
|||||||
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
|
dragZoomIn, dragZoomOut,
|
||||||
|
talkStart, talkStop, broadcastStart, broadcastStop
|
||||||
} from '@/api/commonChannel'
|
} from '@/api/commonChannel'
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
@ -283,6 +284,46 @@ const actions = {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
talkStart({ commit }, channelId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
talkStart(channelId).then(response => {
|
||||||
|
const { data } = response
|
||||||
|
resolve(data)
|
||||||
|
}).catch(error => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
talkStop({ commit }, channelId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
talkStop(channelId).then(response => {
|
||||||
|
const { data } = response
|
||||||
|
resolve(data)
|
||||||
|
}).catch(error => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
broadcastStart({ commit }, channelId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
broadcastStart(channelId).then(response => {
|
||||||
|
const { data } = response
|
||||||
|
resolve(data)
|
||||||
|
}).catch(error => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
broadcastStop({ commit }, channelId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
broadcastStop(channelId).then(response => {
|
||||||
|
const { data } = response
|
||||||
|
resolve(data)
|
||||||
|
}).catch(error => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
getList({ commit }, param) {
|
getList({ commit }, param) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
getList(param).then(response => {
|
getList(param).then(response => {
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<el-dialog
|
<el-dialog
|
||||||
title="语音对讲"
|
title="语音对讲"
|
||||||
top="10vh"
|
top="10vh"
|
||||||
width="61.5vw"
|
width="65vw"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:visible.sync="showDialog"
|
:visible.sync="showDialog"
|
||||||
@close="close()"
|
@close="close()"
|
||||||
@ -33,8 +33,8 @@
|
|||||||
Your browser is too old which doesn't support HTML5 video.
|
Your browser is too old which doesn't support HTML5 video.
|
||||||
</video>
|
</video>
|
||||||
<el-radio-group v-model="talkMode" size="big" @change="onModeChange">
|
<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-button :label="false">对讲</el-radio-button>
|
<el-radio-button :label="true">对讲</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
<p style="color: #909399; font-size: 14px; margin-top: 4px;">
|
<p style="color: #909399; font-size: 14px; margin-top: 4px;">
|
||||||
{{ talkMode ? '双向语音交互,可听到设备声音' : '单向喊话,仅向设备发送语音' }}
|
{{ talkMode ? '双向语音交互,可听到设备声音' : '单向喊话,仅向设备发送语音' }}
|
||||||
@ -53,7 +53,17 @@
|
|||||||
<span v-if="talkStatus === -2">正在释放资源</span>
|
<span v-if="talkStatus === -2">正在释放资源</span>
|
||||||
<span v-if="talkStatus === -1">点击开始{{ talkMode ? '对讲' : '喊话' }}</span>
|
<span v-if="talkStatus === -1">点击开始{{ talkMode ? '对讲' : '喊话' }}</span>
|
||||||
<span v-if="talkStatus === 0">等待接通中...</span>
|
<span v-if="talkStatus === 0">等待接通中...</span>
|
||||||
<span v-if="talkStatus === 1">请说话</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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -127,7 +137,10 @@ export default {
|
|||||||
if (this.talkStatus === -2) return 'primary'
|
if (this.talkStatus === -2) return 'primary'
|
||||||
if (this.talkStatus === -1) return 'primary'
|
if (this.talkStatus === -1) return 'primary'
|
||||||
if (this.talkStatus === 0) return 'warning'
|
if (this.talkStatus === 0) return 'warning'
|
||||||
if (this.talkStatus === 1) return 'danger'
|
if (this.talkStatus === 1) {
|
||||||
|
if (this.talkMode && !this.playConnected) return 'warning'
|
||||||
|
return 'danger'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async talkButtonClick() {
|
async talkButtonClick() {
|
||||||
if (this.talkStatus === -1) {
|
if (this.talkStatus === -1) {
|
||||||
@ -137,54 +150,230 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async startTalk() {
|
async startTalk() {
|
||||||
this.talkStatus = 0
|
|
||||||
try {
|
try {
|
||||||
const data = await this.$store.dispatch('play/broadcastStart', [this.channelId, this.channelId, this.talkMode])
|
await this.checkMicrophoneAvailability()
|
||||||
const si = data.streamInfo
|
} catch (e) {
|
||||||
const url = document.location.protocol.includes('https') ? si.rtcs : si.rtc
|
this.$message({ showClose: true, message: this.getMicrophoneErrorMessage(e), type: 'error' })
|
||||||
this.startWebrtcPush(url)
|
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) {
|
} catch (e) {
|
||||||
this.$message({ showClose: true, message: e, type: 'error' })
|
this.$message({ showClose: true, message: e, type: 'error' })
|
||||||
this.talkStatus = -1
|
this.talkStatus = -1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startWebrtcPush(url) {
|
muteVideoPlayer() {
|
||||||
this.$store.dispatch('user/getUserInfo')
|
const player = this.$refs.playerTabs
|
||||||
.then((data) => {
|
if (!player) return
|
||||||
if (data === null) { this.talkStatus = -1; return }
|
if (player.mute) {
|
||||||
const pushKey = data.pushKey
|
player.mute()
|
||||||
url += '&sign=' + pushKey
|
}
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
if (this.broadcastRtc) {
|
this.$store.dispatch('user/getUserInfo').then(user => {
|
||||||
this.broadcastRtc.close()
|
if (user && user.pushKey) {
|
||||||
}
|
url += '&sign=' + user.pushKey
|
||||||
this.broadcastRtc = new ZLMRTCClient.Endpoint({
|
} else {
|
||||||
debug: true,
|
console.warn('[ChAudioTalk] 未获取到pushKey,推流鉴权可能失败')
|
||||||
zlmsdpUrl: url,
|
}
|
||||||
simulecast: false,
|
|
||||||
useCamera: false,
|
if (this.broadcastRtc) {
|
||||||
audioEnable: true,
|
this.broadcastRtc.close()
|
||||||
videoEnable: false,
|
}
|
||||||
recvOnly: false
|
|
||||||
})
|
this.broadcastRtc = new ZLMRTCClient.Endpoint({
|
||||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_NOT_SUPPORT, () => { this.talkStatus = -1 })
|
debug: true,
|
||||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, () => { this.talkStatus = -1 })
|
zlmsdpUrl: url,
|
||||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, () => { this.talkStatus = -1 })
|
simulecast: false,
|
||||||
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, (e) => {
|
useCamera: false,
|
||||||
if (e === 'connecting') this.talkStatus = 0
|
audioEnable: true,
|
||||||
else if (e === 'connected') this.talkStatus = 1
|
videoEnable: false,
|
||||||
else if (e === 'disconnected') this.talkStatus = -1
|
recvOnly: false
|
||||||
})
|
|
||||||
this.broadcastRtc.on(ZLMRTCClient.Events.CAPTURE_STREAM_FAILED, () => { this.talkStatus = -1 })
|
|
||||||
})
|
})
|
||||||
.catch(() => { this.talkStatus = -1 })
|
|
||||||
|
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 === 'disconnected' || s === 'failed' || s === 'closed') {
|
||||||
|
this.playConnected = false
|
||||||
|
this.talkAudioFailed = true
|
||||||
|
if (this.talkStatus === 1) {
|
||||||
|
this.talkAudioRetryTimer = setTimeout(() => {
|
||||||
|
this.startTalkAudioByRtc(url)
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
async stopTalk() {
|
async stopTalk() {
|
||||||
this.talkStatus = -2
|
this.talkStatus = -2
|
||||||
|
|
||||||
if (this.broadcastRtc) {
|
if (this.broadcastRtc) {
|
||||||
this.broadcastRtc.close()
|
this.broadcastRtc.close()
|
||||||
this.broadcastRtc = null
|
this.broadcastRtc = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.talkAudioRtc) {
|
if (this.talkAudioRtc) {
|
||||||
this.talkAudioRtc.close()
|
this.talkAudioRtc.close()
|
||||||
this.talkAudioRtc = null
|
this.talkAudioRtc = null
|
||||||
@ -193,16 +382,27 @@ export default {
|
|||||||
clearTimeout(this.talkAudioRetryTimer)
|
clearTimeout(this.talkAudioRetryTimer)
|
||||||
this.talkAudioRetryTimer = null
|
this.talkAudioRetryTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
this.talkAudioFailed = false
|
this.talkAudioFailed = false
|
||||||
this.talkAudioPlayStream = null
|
this.talkAudioPlayStream = null
|
||||||
this.playConnected = false
|
this.playConnected = false
|
||||||
|
this.unmuteVideoPlayer()
|
||||||
|
|
||||||
|
const storeName = 'commonChanel'
|
||||||
|
const actionName = this.talkMode ? 'talkStop' : 'broadcastStop'
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('play/broadcastStop', [this.channelId, this.channelId])
|
await this.$store.dispatch(storeName + '/' + actionName, this.channelId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('停止对讲失败', e)
|
console.warn('停止对讲失败', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.talkStatus = -1
|
this.talkStatus = -1
|
||||||
},
|
},
|
||||||
|
retryTalkAudio() {
|
||||||
|
if (this.talkAudioPlayStream) {
|
||||||
|
this.startTalkAudioPlay(this.talkAudioPlayStream)
|
||||||
|
}
|
||||||
|
},
|
||||||
close() {
|
close() {
|
||||||
if (this.showPlayer && this.$refs.playerTabs) {
|
if (this.showPlayer && this.$refs.playerTabs) {
|
||||||
this.$refs.playerTabs.stop()
|
this.$refs.playerTabs.stop()
|
||||||
|
|||||||
@ -37,7 +37,7 @@
|
|||||||
<el-radio-button :label="false">对讲</el-radio-button>
|
<el-radio-button :label="false">对讲</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
<p style="color: #909399; font-size: 14px; margin-top: 4px;">
|
<p style="color: #909399; font-size: 14px; margin-top: 4px;">
|
||||||
{{ talkMode ? '双向语音交互,可听到设备声音' : '单向喊话,仅向设备发送语音' }}
|
{{ talkMode ? '单向喊话,仅向设备发送语音' : '双向语音交互,可听到设备声音' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
@ -77,7 +77,7 @@ export default {
|
|||||||
channelId: null,
|
channelId: null,
|
||||||
hasAudio: false,
|
hasAudio: false,
|
||||||
streamInfo: null,
|
streamInfo: null,
|
||||||
talkMode: false,
|
talkMode: true,
|
||||||
talkStatus: -1,
|
talkStatus: -1,
|
||||||
broadcastRtc: null,
|
broadcastRtc: null,
|
||||||
talkAudioRtc: null,
|
talkAudioRtc: null,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user