支持通用通道对讲,支持全双工对讲

This commit is contained in:
lin 2026-06-16 10:08:04 +08:00
parent f6ca930492
commit 6ade3060b5
7 changed files with 151 additions and 55 deletions

View File

@ -574,9 +574,9 @@ public class DeviceServiceImpl implements IDeviceService {
@Override
public boolean removeCatalogSubscribe(@NotNull Device device, CommonCallback<Boolean> callback) {
log.info("[移除目录订阅]: {}", device.getDeviceId());
String key = SubscribeTaskForCatalog.getKey(device);
if (subscribeTaskRunner.containsKey(key)) {
log.info("[移除目录订阅]: {}", device.getDeviceId());
SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key);
if (transactionInfo == null) {
log.warn("[移除目录订阅] 未找到事务信息,{}", device.getDeviceId());
@ -638,9 +638,9 @@ public class DeviceServiceImpl implements IDeviceService {
@Override
public boolean removeMobilePositionSubscribe(Device device, CommonCallback<Boolean> callback) {
log.info("[移除移动位置订阅]: {}", device.getDeviceId());
String key = SubscribeTaskForMobilPosition.getKey(device);
if (subscribeTaskRunner.containsKey(key)) {
log.info("[移除移动位置订阅]: {}", device.getDeviceId());
SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key);
if (transactionInfo == null) {
log.warn("[移除移动位置订阅] 未找到事务信息,{}", device.getDeviceId());
@ -703,9 +703,9 @@ public class DeviceServiceImpl implements IDeviceService {
@Override
public boolean removeAlarmSubscribe(Device device, CommonCallback<Boolean> callback) {
log.info("[移除报警订阅]: {}", device.getDeviceId());
String key = SubscribeTaskForAlarm.getKey(device);
if (subscribeTaskRunner.containsKey(key)) {
log.info("[移除报警订阅]: {}", device.getDeviceId());
SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key);
if (transactionInfo == null) {
log.warn("[移除报警订阅] 未找到事务信息,{}", device.getDeviceId());

View File

@ -1229,6 +1229,12 @@ public class PlayServiceImpl implements IPlayService {
audioBroadcastResult.setApp(app);
audioBroadcastResult.setStream(stream);
audioBroadcastResult.setStreamInfo(new StreamContent(mediaServerService.getStreamInfoByAppAndStream(mediaServerItem, app, stream, null, null, null, false)));
if (!broadcastMode) {
audioBroadcastResult.setPlayStreamInfo(new StreamContent(
mediaServerService.getStreamInfoByAppAndStream(mediaServerItem,
MediaStreamUtil.GB28181_TALK, stream + "_talk",
null, null, null, true)));
}
audioBroadcastResult.setCodec("G.711");
return audioBroadcastResult;
}

View File

@ -1,8 +1,6 @@
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;
@ -11,12 +9,9 @@ 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;
@ -34,12 +29,6 @@ public class SourceBroadcastServiceForGbImpl implements ISourceBroadcastService
@Autowired
private IDeviceChannelService deviceChannelService;
@Autowired
private IMediaServerService mediaServerService;
@Autowired
private UserSetting userSetting;
@Override
public AudioTalkResult startBroadcast(CommonGBChannel channel) {
Device device = deviceService.getDevice(channel.getDataDeviceId());
@ -79,14 +68,9 @@ public class SourceBroadcastServiceForGbImpl implements ISourceBroadcastService
}
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);
result.setPlayStream(abResult.getPlayStreamInfo());
return result;
}

View File

@ -496,7 +496,7 @@ public class SIPCommander implements ISIPCommander {
}
if (!mediaServerItem.isRtpEnable()) {
// 单端口暂不支持语音喊话
log.info("[语音喊话] 单端口暂不支持此操作");
log.warn("[语音喊话] 单端口暂不支持此操作");
return;
}

View File

@ -1,8 +1,13 @@
package com.genersoft.iot.vmp.vmanager.bean;
import lombok.Getter;
import lombok.Setter;
/**
* @author lin
*/
@Setter
@Getter
public class AudioBroadcastResult {
/**
* 推流的各个方式流地址
@ -24,36 +29,10 @@ public class AudioBroadcastResult {
*/
private String stream;
/**
* 播放流地址设备音频通过ZLM播放给浏览器对讲时设置
*/
private StreamContent playStreamInfo;
public StreamContent getStreamInfo() {
return streamInfo;
}
public void setStreamInfo(StreamContent streamInfo) {
this.streamInfo = streamInfo;
}
public String getCodec() {
return codec;
}
public void setCodec(String codec) {
this.codec = codec;
}
public String getApp() {
return app;
}
public void setApp(String app) {
this.app = app;
}
public String getStream() {
return stream;
}
public void setStream(String stream) {
this.stream = stream;
}
}

View File

@ -355,7 +355,9 @@ export default {
this.talkAudioRtc.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, (s) => {
console.warn('[ChAudioTalk] 音频播放连接状态:', s)
if (s === 'disconnected' || s === 'failed' || s === 'closed') {
if (s === 'connected') {
this.playConnected = true
} else if (s === 'disconnected' || s === 'failed' || s === 'closed') {
this.playConnected = false
this.talkAudioFailed = true
if (this.talkStatus === 1) {

View File

@ -51,9 +51,19 @@
/>
<p style="margin-top: 16px; color: #606266;">
<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 === 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>
</div>
</div>
@ -129,7 +139,10 @@ export default {
if (this.talkStatus === -2) return 'primary'
if (this.talkStatus === -1) return 'primary'
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() {
if (this.talkStatus === -1) {
@ -145,6 +158,13 @@ export default {
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
@ -181,6 +201,105 @@ export default {
})
.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) {
@ -198,6 +317,7 @@ export default {
this.talkAudioFailed = false
this.talkAudioPlayStream = null
this.playConnected = false
this.unmuteVideoPlayer()
try {
await this.$store.dispatch('play/broadcastStop', [this.deviceId, this.channelId])
} catch (e) {
@ -205,6 +325,11 @@ export default {
}
this.talkStatus = -1
},
retryTalkAudio() {
if (this.talkAudioPlayStream) {
this.startTalkAudioPlay(this.talkAudioPlayStream)
}
},
close() {
if (this.showPlayer && this.$refs.playerTabs) {
this.$refs.playerTabs.stop()