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
f6ca930492
commit
6ade3060b5
@ -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());
|
||||||
|
|||||||
@ -1229,6 +1229,12 @@ 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
package com.genersoft.iot.vmp.gb28181.service.impl;
|
package com.genersoft.iot.vmp.gb28181.service.impl;
|
||||||
|
|
||||||
import com.genersoft.iot.vmp.common.enums.ChannelDataType;
|
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.conf.exception.ControllerException;
|
||||||
import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
|
import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
|
||||||
import com.genersoft.iot.vmp.gb28181.bean.Device;
|
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.IDeviceService;
|
||||||
import com.genersoft.iot.vmp.gb28181.service.IPlayService;
|
import com.genersoft.iot.vmp.gb28181.service.IPlayService;
|
||||||
import com.genersoft.iot.vmp.gb28181.service.ISourceBroadcastService;
|
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.AudioBroadcastResult;
|
||||||
import com.genersoft.iot.vmp.vmanager.bean.AudioTalkResult;
|
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 lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -34,12 +29,6 @@ public class SourceBroadcastServiceForGbImpl implements ISourceBroadcastService
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IDeviceChannelService deviceChannelService;
|
private IDeviceChannelService deviceChannelService;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private IMediaServerService mediaServerService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserSetting userSetting;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AudioTalkResult startBroadcast(CommonGBChannel channel) {
|
public AudioTalkResult startBroadcast(CommonGBChannel channel) {
|
||||||
Device device = deviceService.getDevice(channel.getDataDeviceId());
|
Device device = deviceService.getDevice(channel.getDataDeviceId());
|
||||||
@ -79,14 +68,9 @@ public class SourceBroadcastServiceForGbImpl implements ISourceBroadcastService
|
|||||||
}
|
}
|
||||||
AudioBroadcastResult abResult = playService.audioBroadcast(
|
AudioBroadcastResult abResult = playService.audioBroadcast(
|
||||||
device.getDeviceId(), deviceChannel.getDeviceId(), false);
|
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();
|
AudioTalkResult result = new AudioTalkResult();
|
||||||
result.setPushStream(abResult.getStreamInfo());
|
result.setPushStream(abResult.getStreamInfo());
|
||||||
result.setPlayStream(playStream);
|
result.setPlayStream(abResult.getPlayStreamInfo());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -496,7 +496,7 @@ public class SIPCommander implements ISIPCommander {
|
|||||||
}
|
}
|
||||||
if (!mediaServerItem.isRtpEnable()) {
|
if (!mediaServerItem.isRtpEnable()) {
|
||||||
// 单端口暂不支持语音喊话
|
// 单端口暂不支持语音喊话
|
||||||
log.info("[语音喊话] 单端口暂不支持此操作");
|
log.warn("[语音喊话] 单端口暂不支持此操作");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
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 {
|
||||||
/**
|
/**
|
||||||
* 推流的各个方式流地址
|
* 推流的各个方式流地址
|
||||||
@ -24,36 +29,10 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -355,7 +355,9 @@ export default {
|
|||||||
|
|
||||||
this.talkAudioRtc.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, (s) => {
|
this.talkAudioRtc.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, (s) => {
|
||||||
console.warn('[ChAudioTalk] 音频播放连接状态:', 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.playConnected = false
|
||||||
this.talkAudioFailed = true
|
this.talkAudioFailed = true
|
||||||
if (this.talkStatus === 1) {
|
if (this.talkStatus === 1) {
|
||||||
|
|||||||
@ -51,9 +51,19 @@
|
|||||||
/>
|
/>
|
||||||
<p style="margin-top: 16px; color: #606266;">
|
<p style="margin-top: 16px; color: #606266;">
|
||||||
<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>
|
||||||
@ -129,7 +139,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) {
|
||||||
@ -145,6 +158,13 @@ export default {
|
|||||||
const si = data.streamInfo
|
const si = data.streamInfo
|
||||||
const url = document.location.protocol.includes('https') ? si.rtcs : si.rtc
|
const url = document.location.protocol.includes('https') ? si.rtcs : si.rtc
|
||||||
this.startWebrtcPush(url)
|
this.startWebrtcPush(url)
|
||||||
|
|
||||||
|
const playStreamInfo = data?.playStreamInfo
|
||||||
|
if (!this.talkMode && playStreamInfo) {
|
||||||
|
this.talkAudioPlayStream = playStreamInfo
|
||||||
|
this.startTalkAudioPlay(playStreamInfo)
|
||||||
|
this.muteVideoPlayer()
|
||||||
|
}
|
||||||
} 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
|
||||||
@ -181,6 +201,105 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch(() => { 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() {
|
async stopTalk() {
|
||||||
this.talkStatus = -2
|
this.talkStatus = -2
|
||||||
if (this.broadcastRtc) {
|
if (this.broadcastRtc) {
|
||||||
@ -198,6 +317,7 @@ export default {
|
|||||||
this.talkAudioFailed = false
|
this.talkAudioFailed = false
|
||||||
this.talkAudioPlayStream = null
|
this.talkAudioPlayStream = null
|
||||||
this.playConnected = false
|
this.playConnected = false
|
||||||
|
this.unmuteVideoPlayer()
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('play/broadcastStop', [this.deviceId, this.channelId])
|
await this.$store.dispatch('play/broadcastStop', [this.deviceId, this.channelId])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -205,6 +325,11 @@ export default {
|
|||||||
}
|
}
|
||||||
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()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user