Commit 26bdf2e7768ee5dfc400c3970a5aa129fed49453
Merge branch '级联' into main-dev
# Conflicts: # src/main/java/com/genersoft/iot/vmp/gb28181/bean/AudioBroadcastCatch.java # src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java # src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java # src/main/java/com/genersoft/iot/vmp/service/IPlayService.java # src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java # src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java # web_src/src/components/dialog/devicePlayer.vue
Showing
28 changed files
with
970 additions
and
174 deletions
doc/README.md
doc/_content/theory/broadcast_cascade.md
0 → 100644
| 1 | +<!-- 点播流程 --> | |
| 2 | + | |
| 3 | +# 点播流程 | |
| 4 | +> 以下为WVP-PRO级联语音喊话流程。 | |
| 5 | + | |
| 6 | +```plantuml | |
| 7 | +@startuml | |
| 8 | +"上级平台" -> "下级平台": 1. 发起语音喊话请求 | |
| 9 | +"上级平台" <-- "下级平台": 2. 200OK | |
| 10 | +"上级平台" <- "下级平台": 3. 回复Result OK | |
| 11 | +"上级平台" --> "下级平台": 4. 200OK | |
| 12 | + | |
| 13 | +"下级平台" -> "设备": 5. 发起语音喊话请求 | |
| 14 | +"下级平台" <-- "设备": 6. 200OK | |
| 15 | +"下级平台" <- "设备": 7. 回复Result OK | |
| 16 | +"下级平台" --> "设备": 8. 200OK | |
| 17 | + | |
| 18 | +"下级平台" <- "设备": 9. invite(broadcast) | |
| 19 | +"下级平台" --> "设备": 10. 100 trying | |
| 20 | +"下级平台" --> "设备": 11. 200OK SDP | |
| 21 | +"下级平台" <-- "设备": 12. ack | |
| 22 | + | |
| 23 | +"上级平台" <- "下级平台": 13. invite(broadcast) | |
| 24 | +"上级平台" --> "下级平台": 14. 100 trying | |
| 25 | +"上级平台" --> "下级平台": 15. 200OK SDP | |
| 26 | +"上级平台" <-- "下级平台": 16. ack | |
| 27 | + | |
| 28 | +"上级平台" -> "下级平台": 17. 推送RTP | |
| 29 | +"下级平台" -> "设备": 18. 推送RTP | |
| 30 | + | |
| 31 | +@enduml | |
| 32 | +``` | |
| 33 | + | |
| 34 | + | |
| 35 | +## 注册流程描述如下: | |
| 36 | +1. 用户从网页或调用接口发起点播请求; | |
| 37 | +2. WVP-PRO向摄像机发送Invite消息,消息头域中携带 Subject字段,表明点播的视频源ID、发送方媒体流序列号、ZLMediaKit接收流使用的IP、端口号、 | |
| 38 | + 接收端媒体流序列号等参数,SDP消息体中 s字段为“Play”代表实时点播,y字段描述SSRC值,f字段描述媒体参数。 | |
| 39 | +3. 摄像机向WVP-PRO回复200OK,消息体中描述了媒体流发送者发送媒体流的IP、端口、媒体格式、SSRC字段等内容。 | |
| 40 | +4. WVP-PRO向设备回复Ack, 会话建立成功。 | |
| 41 | +5. 设备向ZLMediaKit发送实时流。 | |
| 42 | +6. ZLMediaKit向WVP-PRO发送流改变事件。 | |
| 43 | +7. WVP-PRO向WEB用户回复播放地址。 | |
| 44 | +8. ZLMediaKit向WVP发送流无人观看事件。 | |
| 45 | +9. WVP-PRO向设备回复Bye, 结束会话。 | |
| 46 | +10. 设备回复200OK,会话结束成功。 | ... | ... |
doc/_sidebar.md
src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
| ... | ... | @@ -55,6 +55,8 @@ public class UserSetting { |
| 55 | 55 | |
| 56 | 56 | private String thirdPartyGBIdReg = "[\\s\\S]*"; |
| 57 | 57 | |
| 58 | + private String broadcastForPlatform = "UDP"; | |
| 59 | + | |
| 58 | 60 | private List<String> interfaceAuthenticationExcludes = new ArrayList<>(); |
| 59 | 61 | |
| 60 | 62 | public Boolean getSavePositionHistory() { |
| ... | ... | @@ -205,6 +207,14 @@ public class UserSetting { |
| 205 | 207 | this.syncChannelOnDeviceOnline = syncChannelOnDeviceOnline; |
| 206 | 208 | } |
| 207 | 209 | |
| 210 | + public String getBroadcastForPlatform() { | |
| 211 | + return broadcastForPlatform; | |
| 212 | + } | |
| 213 | + | |
| 214 | + public void setBroadcastForPlatform(String broadcastForPlatform) { | |
| 215 | + this.broadcastForPlatform = broadcastForPlatform; | |
| 216 | + } | |
| 217 | + | |
| 208 | 218 | public Boolean getPushStreamAfterAck() { |
| 209 | 219 | return pushStreamAfterAck; |
| 210 | 220 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/bean/AudioBroadcastCatch.java
| ... | ... | @@ -2,6 +2,7 @@ package com.genersoft.iot.vmp.gb28181.bean; |
| 2 | 2 | |
| 3 | 3 | |
| 4 | 4 | import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem; |
| 5 | +import com.genersoft.iot.vmp.vmanager.gb28181.play.bean.AudioBroadcastEvent; | |
| 5 | 6 | import gov.nist.javax.sip.message.SIPResponse; |
| 6 | 7 | |
| 7 | 8 | /** |
| ... | ... | @@ -11,18 +12,24 @@ import gov.nist.javax.sip.message.SIPResponse; |
| 11 | 12 | public class AudioBroadcastCatch { |
| 12 | 13 | |
| 13 | 14 | |
| 14 | - public AudioBroadcastCatch(String deviceId, | |
| 15 | - String channelId, | |
| 16 | - AudioBroadcastCatchStatus status, | |
| 17 | - MediaServerItem mediaServerItem, | |
| 18 | - String app, | |
| 19 | - String stream) { | |
| 15 | + public AudioBroadcastCatch( | |
| 16 | + String deviceId, | |
| 17 | + String channelId, | |
| 18 | + MediaServerItem mediaServerItem, | |
| 19 | + String app, | |
| 20 | + String stream, | |
| 21 | + AudioBroadcastEvent event, | |
| 22 | + AudioBroadcastCatchStatus status, | |
| 23 | + boolean isFromPlatform | |
| 24 | + ) { | |
| 20 | 25 | this.deviceId = deviceId; |
| 21 | 26 | this.channelId = channelId; |
| 22 | 27 | this.status = status; |
| 23 | - this.mediaServerItem = mediaServerItem; | |
| 28 | + this.event = event; | |
| 29 | + this.isFromPlatform = isFromPlatform; | |
| 24 | 30 | this.app = app; |
| 25 | 31 | this.stream = stream; |
| 32 | + this.mediaServerItem = mediaServerItem; | |
| 26 | 33 | } |
| 27 | 34 | |
| 28 | 35 | public AudioBroadcastCatch() { |
| ... | ... | @@ -39,21 +46,26 @@ public class AudioBroadcastCatch { |
| 39 | 46 | private String channelId; |
| 40 | 47 | |
| 41 | 48 | /** |
| 42 | - * 使用的流媒体 | |
| 49 | + * 流媒体信息 | |
| 43 | 50 | */ |
| 44 | 51 | private MediaServerItem mediaServerItem; |
| 45 | 52 | |
| 46 | 53 | /** |
| 47 | - * 待推送给设备的流应用名 | |
| 54 | + * 关联的流APP | |
| 48 | 55 | */ |
| 49 | 56 | private String app; |
| 50 | 57 | |
| 51 | 58 | /** |
| 52 | - * 待推送给设备的流ID | |
| 59 | + * 关联的流STREAM | |
| 53 | 60 | */ |
| 54 | 61 | private String stream; |
| 55 | 62 | |
| 56 | 63 | /** |
| 64 | + * 是否是级联语音喊话 | |
| 65 | + */ | |
| 66 | + private boolean isFromPlatform; | |
| 67 | + | |
| 68 | + /** | |
| 57 | 69 | * 语音广播状态 |
| 58 | 70 | */ |
| 59 | 71 | private AudioBroadcastCatchStatus status; |
| ... | ... | @@ -63,6 +75,11 @@ public class AudioBroadcastCatch { |
| 63 | 75 | */ |
| 64 | 76 | private SipTransactionInfo sipTransactionInfo; |
| 65 | 77 | |
| 78 | + /** | |
| 79 | + * 请求结果回调 | |
| 80 | + */ | |
| 81 | + private AudioBroadcastEvent event; | |
| 82 | + | |
| 66 | 83 | |
| 67 | 84 | public String getDeviceId() { |
| 68 | 85 | return deviceId; |
| ... | ... | @@ -123,4 +140,44 @@ public class AudioBroadcastCatch { |
| 123 | 140 | public void setMediaServerItem(MediaServerItem mediaServerItem) { |
| 124 | 141 | this.mediaServerItem = mediaServerItem; |
| 125 | 142 | } |
| 143 | + | |
| 144 | + public AudioBroadcastEvent getEvent() { | |
| 145 | + return event; | |
| 146 | + } | |
| 147 | + | |
| 148 | + public void setEvent(AudioBroadcastEvent event) { | |
| 149 | + this.event = event; | |
| 150 | + } | |
| 151 | + | |
| 152 | + public String getApp() { | |
| 153 | + return app; | |
| 154 | + } | |
| 155 | + | |
| 156 | + public void setApp(String app) { | |
| 157 | + this.app = app; | |
| 158 | + } | |
| 159 | + | |
| 160 | + public String getStream() { | |
| 161 | + return stream; | |
| 162 | + } | |
| 163 | + | |
| 164 | + public void setStream(String stream) { | |
| 165 | + this.stream = stream; | |
| 166 | + } | |
| 167 | + | |
| 168 | + public boolean isFromPlatform() { | |
| 169 | + return isFromPlatform; | |
| 170 | + } | |
| 171 | + | |
| 172 | + public void setFromPlatform(boolean fromPlatform) { | |
| 173 | + isFromPlatform = fromPlatform; | |
| 174 | + } | |
| 175 | + | |
| 176 | + public MediaServerItem getMediaServerItem() { | |
| 177 | + return mediaServerItem; | |
| 178 | + } | |
| 179 | + | |
| 180 | + public void setMediaServerItem(MediaServerItem mediaServerItem) { | |
| 181 | + this.mediaServerItem = mediaServerItem; | |
| 182 | + } | |
| 126 | 183 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamType.java
src/main/java/com/genersoft/iot/vmp/gb28181/bean/ParentPlatform.java
| ... | ... | @@ -66,7 +66,7 @@ public class ParentPlatform { |
| 66 | 66 | * 设备端口 |
| 67 | 67 | */ |
| 68 | 68 | @Schema(description = "设备端口") |
| 69 | - private String devicePort; | |
| 69 | + private int devicePort; | |
| 70 | 70 | |
| 71 | 71 | /** |
| 72 | 72 | * SIP认证用户名(默认使用设备国标编号) |
| ... | ... | @@ -261,11 +261,11 @@ public class ParentPlatform { |
| 261 | 261 | this.deviceIp = deviceIp; |
| 262 | 262 | } |
| 263 | 263 | |
| 264 | - public String getDevicePort() { | |
| 264 | + public int getDevicePort() { | |
| 265 | 265 | return devicePort; |
| 266 | 266 | } |
| 267 | 267 | |
| 268 | - public void setDevicePort(String devicePort) { | |
| 268 | + public void setDevicePort(int devicePort) { | |
| 269 | 269 | this.devicePort = devicePort; |
| 270 | 270 | } |
| 271 | 271 | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java
| ... | ... | @@ -86,6 +86,11 @@ public class SipSubscribe { |
| 86 | 86 | public String callId; |
| 87 | 87 | public EventObject event; |
| 88 | 88 | |
| 89 | + public EventResult(int statusCode, String msg) { | |
| 90 | + this.statusCode = statusCode; | |
| 91 | + this.msg = msg; | |
| 92 | + } | |
| 93 | + | |
| 89 | 94 | public EventResult(EventObject event) { |
| 90 | 95 | this.event = event; |
| 91 | 96 | if (event instanceof ResponseEvent) { | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommanderForPlatform.java
| 1 | 1 | package com.genersoft.iot.vmp.gb28181.transmit.cmd; |
| 2 | 2 | |
| 3 | +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; | |
| 3 | 4 | import com.genersoft.iot.vmp.gb28181.bean.*; |
| 4 | 5 | import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; |
| 6 | +import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe; | |
| 7 | +import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem; | |
| 5 | 8 | import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; |
| 9 | +import com.genersoft.iot.vmp.service.bean.SSRCInfo; | |
| 6 | 10 | |
| 7 | 11 | import javax.sip.InvalidArgumentException; |
| 8 | 12 | import javax.sip.SipException; |
| ... | ... | @@ -14,42 +18,56 @@ public interface ISIPCommanderForPlatform { |
| 14 | 18 | |
| 15 | 19 | /** |
| 16 | 20 | * 向上级平台注册 |
| 21 | + * | |
| 17 | 22 | * @param parentPlatform |
| 18 | 23 | * @return |
| 19 | 24 | */ |
| 20 | - void register(ParentPlatform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException; | |
| 21 | - void register(ParentPlatform parentPlatform, String callId, WWWAuthenticateHeader www, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent, boolean registerAgain, boolean isRegister) throws SipException, InvalidArgumentException, ParseException; | |
| 25 | + void register(ParentPlatform parentPlatform, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) | |
| 26 | + throws InvalidArgumentException, ParseException, SipException; | |
| 27 | + | |
| 28 | + void register(ParentPlatform parentPlatform, String callId, WWWAuthenticateHeader www, | |
| 29 | + SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent, boolean registerAgain, boolean isRegister) | |
| 30 | + throws SipException, InvalidArgumentException, ParseException; | |
| 22 | 31 | |
| 23 | 32 | /** |
| 24 | 33 | * 向上级平台注销 |
| 34 | + * | |
| 25 | 35 | * @param parentPlatform |
| 26 | 36 | * @return |
| 27 | 37 | */ |
| 28 | - void unregister(ParentPlatform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException; | |
| 38 | + void unregister(ParentPlatform parentPlatform, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) | |
| 39 | + throws InvalidArgumentException, ParseException, SipException; | |
| 29 | 40 | |
| 30 | 41 | |
| 31 | 42 | /** |
| 32 | 43 | * 向上级平发送心跳信息 |
| 44 | + * | |
| 33 | 45 | * @param parentPlatform |
| 34 | 46 | * @return callId(作为接受回复的判定) |
| 35 | 47 | */ |
| 36 | - String keepalive(ParentPlatform parentPlatform,SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException; | |
| 48 | + String keepalive(ParentPlatform parentPlatform, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) | |
| 49 | + throws SipException, InvalidArgumentException, ParseException; | |
| 37 | 50 | |
| 38 | 51 | |
| 39 | 52 | /** |
| 40 | 53 | * 向上级回复通道信息 |
| 41 | - * @param channel 通道信息 | |
| 54 | + * | |
| 55 | + * @param channel 通道信息 | |
| 42 | 56 | * @param parentPlatform 平台信息 |
| 43 | 57 | * @param sn |
| 44 | 58 | * @param fromTag |
| 45 | 59 | * @param size |
| 46 | 60 | * @return |
| 47 | 61 | */ |
| 48 | - void catalogQuery(DeviceChannel channel, ParentPlatform parentPlatform, String sn, String fromTag, int size) throws SipException, InvalidArgumentException, ParseException; | |
| 49 | - void catalogQuery(List<DeviceChannel> channels, ParentPlatform parentPlatform, String sn, String fromTag) throws InvalidArgumentException, ParseException, SipException; | |
| 62 | + void catalogQuery(DeviceChannel channel, ParentPlatform parentPlatform, String sn, String fromTag, int size) | |
| 63 | + throws SipException, InvalidArgumentException, ParseException; | |
| 64 | + | |
| 65 | + void catalogQuery(List<DeviceChannel> channels, ParentPlatform parentPlatform, String sn, String fromTag) | |
| 66 | + throws InvalidArgumentException, ParseException, SipException; | |
| 50 | 67 | |
| 51 | 68 | /** |
| 52 | 69 | * 向上级回复DeviceInfo查询信息 |
| 70 | + * | |
| 53 | 71 | * @param parentPlatform 平台信息 |
| 54 | 72 | * @param sn SN |
| 55 | 73 | * @param fromTag FROM头的tag信息 |
| ... | ... | @@ -59,32 +77,37 @@ public interface ISIPCommanderForPlatform { |
| 59 | 77 | |
| 60 | 78 | /** |
| 61 | 79 | * 向上级回复DeviceStatus查询信息 |
| 80 | + * | |
| 62 | 81 | * @param parentPlatform 平台信息 |
| 63 | 82 | * @param sn |
| 64 | 83 | * @param fromTag |
| 65 | 84 | * @return |
| 66 | 85 | */ |
| 67 | - void deviceStatusResponse(ParentPlatform parentPlatform,String channelId, String sn, String fromTag,int status) throws SipException, InvalidArgumentException, ParseException; | |
| 86 | + void deviceStatusResponse(ParentPlatform parentPlatform,String channelId, String sn, String fromTag,int status) throws SipException, InvalidArgumentException, ParseException ; | |
| 68 | 87 | |
| 69 | 88 | /** |
| 70 | 89 | * 向上级回复移动位置订阅消息 |
| 90 | + * | |
| 71 | 91 | * @param parentPlatform 平台信息 |
| 72 | - * @param gpsMsgInfo GPS信息 | |
| 73 | - * @param subscribeInfo 订阅相关的信息 | |
| 92 | + * @param gpsMsgInfo GPS信息 | |
| 93 | + * @param subscribeInfo 订阅相关的信息 | |
| 74 | 94 | * @return |
| 75 | 95 | */ |
| 76 | - void sendNotifyMobilePosition(ParentPlatform parentPlatform, GPSMsgInfo gpsMsgInfo, SubscribeInfo subscribeInfo) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException; | |
| 96 | + void sendNotifyMobilePosition(ParentPlatform parentPlatform, GPSMsgInfo gpsMsgInfo, SubscribeInfo subscribeInfo) | |
| 97 | + throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException; | |
| 77 | 98 | |
| 78 | 99 | /** |
| 79 | 100 | * 向上级回复报警消息 |
| 101 | + * | |
| 80 | 102 | * @param parentPlatform 平台信息 |
| 81 | - * @param deviceAlarm 报警信息信息 | |
| 103 | + * @param deviceAlarm 报警信息信息 | |
| 82 | 104 | * @return |
| 83 | 105 | */ |
| 84 | 106 | void sendAlarmMessage(ParentPlatform parentPlatform, DeviceAlarm deviceAlarm) throws SipException, InvalidArgumentException, ParseException; |
| 85 | 107 | |
| 86 | 108 | /** |
| 87 | 109 | * 回复catalog事件-增加/更新 |
| 110 | + * | |
| 88 | 111 | * @param parentPlatform |
| 89 | 112 | * @param deviceChannels |
| 90 | 113 | */ |
| ... | ... | @@ -92,22 +115,28 @@ public interface ISIPCommanderForPlatform { |
| 92 | 115 | |
| 93 | 116 | /** |
| 94 | 117 | * 回复catalog事件-删除 |
| 118 | + * | |
| 95 | 119 | * @param parentPlatform |
| 96 | 120 | * @param deviceChannels |
| 97 | 121 | */ |
| 98 | - void sendNotifyForCatalogOther(String type, ParentPlatform parentPlatform, List<DeviceChannel> deviceChannels, SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException; | |
| 122 | + void sendNotifyForCatalogOther(String type, ParentPlatform parentPlatform, List<DeviceChannel> deviceChannels, | |
| 123 | + SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, | |
| 124 | + ParseException, NoSuchFieldException, SipException, IllegalAccessException; | |
| 99 | 125 | |
| 100 | 126 | /** |
| 101 | 127 | * 回复recordInfo |
| 102 | - * @param deviceChannel 通道信息 | |
| 128 | + * | |
| 129 | + * @param deviceChannel 通道信息 | |
| 103 | 130 | * @param parentPlatform 平台信息 |
| 104 | - * @param fromTag fromTag | |
| 105 | - * @param recordInfo 录像信息 | |
| 131 | + * @param fromTag fromTag | |
| 132 | + * @param recordInfo 录像信息 | |
| 106 | 133 | */ |
| 107 | - void recordInfo(DeviceChannel deviceChannel, ParentPlatform parentPlatform, String fromTag, RecordInfo recordInfo) throws SipException, InvalidArgumentException, ParseException; | |
| 134 | + void recordInfo(DeviceChannel deviceChannel, ParentPlatform parentPlatform, String fromTag, RecordInfo recordInfo) | |
| 135 | + throws SipException, InvalidArgumentException, ParseException; | |
| 108 | 136 | |
| 109 | 137 | /** |
| 110 | 138 | * 录像播放推送完成时发送MediaStatus消息 |
| 139 | + * | |
| 111 | 140 | * @param platform |
| 112 | 141 | * @param sendRtpItem |
| 113 | 142 | * @return |
| ... | ... | @@ -116,9 +145,19 @@ public interface ISIPCommanderForPlatform { |
| 116 | 145 | |
| 117 | 146 | /** |
| 118 | 147 | * 向发起点播的上级回复bye |
| 148 | + * | |
| 119 | 149 | * @param platform 平台信息 |
| 120 | - * @param callId callId | |
| 150 | + * @param callId callId | |
| 121 | 151 | */ |
| 122 | 152 | void streamByeCmd(ParentPlatform platform, String callId) throws SipException, InvalidArgumentException, ParseException; |
| 153 | + | |
| 123 | 154 | void streamByeCmd(ParentPlatform platform, SendRtpItem sendRtpItem) throws SipException, InvalidArgumentException, ParseException; |
| 155 | + | |
| 156 | + void streamByeCmd(ParentPlatform platform, String channelId, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException; | |
| 157 | + | |
| 158 | + void broadcastInviteCmd(ParentPlatform platform, String channelId, MediaServerItem mediaServerItem, | |
| 159 | + SSRCInfo ssrcInfo, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, | |
| 160 | + SipSubscribe.Event errorEvent) throws ParseException, SipException, InvalidArgumentException; | |
| 161 | + | |
| 162 | + void broadcastResultCmd(ParentPlatform platform, DeviceChannel deviceChannel, String sn, boolean result, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException; | |
| 124 | 163 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java
| ... | ... | @@ -4,6 +4,7 @@ import com.genersoft.iot.vmp.conf.SipConfig; |
| 4 | 4 | import com.genersoft.iot.vmp.gb28181.SipLayer; |
| 5 | 5 | import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform; |
| 6 | 6 | import com.genersoft.iot.vmp.gb28181.bean.SendRtpItem; |
| 7 | +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; | |
| 7 | 8 | import com.genersoft.iot.vmp.gb28181.bean.SubscribeInfo; |
| 8 | 9 | import com.genersoft.iot.vmp.gb28181.utils.SipUtils; |
| 9 | 10 | import com.genersoft.iot.vmp.storager.IRedisCatchStorage; |
| ... | ... | @@ -14,7 +15,8 @@ import org.springframework.beans.factory.annotation.Autowired; |
| 14 | 15 | import org.springframework.stereotype.Component; |
| 15 | 16 | import org.springframework.util.DigestUtils; |
| 16 | 17 | |
| 17 | -import javax.sip.*; | |
| 18 | +import javax.sip.InvalidArgumentException; | |
| 19 | +import javax.sip.PeerUnavailableException; | |
| 18 | 20 | import javax.sip.address.Address; |
| 19 | 21 | import javax.sip.address.SipURI; |
| 20 | 22 | import javax.sip.header.*; |
| ... | ... | @@ -22,7 +24,6 @@ import javax.sip.message.Request; |
| 22 | 24 | import javax.validation.constraints.NotNull; |
| 23 | 25 | import java.text.ParseException; |
| 24 | 26 | import java.util.ArrayList; |
| 25 | -import java.util.List; | |
| 26 | 27 | import java.util.UUID; |
| 27 | 28 | |
| 28 | 29 | /** |
| ... | ... | @@ -175,7 +176,7 @@ public class SIPRequestHeaderPlarformProvider { |
| 175 | 176 | SipURI requestURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), serverAddress); |
| 176 | 177 | // via |
| 177 | 178 | ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>(); |
| 178 | - ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), Integer.parseInt(parentPlatform.getDevicePort()), | |
| 179 | + ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), parentPlatform.getDevicePort(), | |
| 179 | 180 | parentPlatform.getTransport(), viaTag); |
| 180 | 181 | viaHeader.setRPort(); |
| 181 | 182 | viaHeaders.add(viaHeader); |
| ... | ... | @@ -212,7 +213,7 @@ public class SIPRequestHeaderPlarformProvider { |
| 212 | 213 | SipURI requestURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), parentPlatform.getServerIP()+ ":" + parentPlatform.getServerPort()); |
| 213 | 214 | // via |
| 214 | 215 | ArrayList<ViaHeader> viaHeaders = new ArrayList<>(); |
| 215 | - ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), Integer.parseInt(parentPlatform.getDevicePort()), | |
| 216 | + ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), parentPlatform.getDevicePort(), | |
| 216 | 217 | parentPlatform.getTransport(), SipUtils.getNewViaTag()); |
| 217 | 218 | viaHeader.setRPort(); |
| 218 | 219 | viaHeaders.add(viaHeader); |
| ... | ... | @@ -272,7 +273,7 @@ public class SIPRequestHeaderPlarformProvider { |
| 272 | 273 | SipURI requestURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(platform.getServerGBId(), platform.getServerIP()+ ":" + platform.getServerPort()); |
| 273 | 274 | // via |
| 274 | 275 | ArrayList<ViaHeader> viaHeaders = new ArrayList<>(); |
| 275 | - ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(platform.getDeviceIp(), Integer.parseInt(platform.getDevicePort()), | |
| 276 | + ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(platform.getDeviceIp(), platform.getDevicePort(), | |
| 276 | 277 | platform.getTransport(), SipUtils.getNewViaTag()); |
| 277 | 278 | viaHeader.setRPort(); |
| 278 | 279 | viaHeaders.add(viaHeader); |
| ... | ... | @@ -308,4 +309,80 @@ public class SIPRequestHeaderPlarformProvider { |
| 308 | 309 | |
| 309 | 310 | return request; |
| 310 | 311 | } |
| 312 | + | |
| 313 | + public Request createInviteRequest(ParentPlatform platform, String channelId, String content, String viaTag, String fromTag, String ssrc, CallIdHeader callIdHeader) throws PeerUnavailableException, ParseException, InvalidArgumentException { | |
| 314 | + Request request = null; | |
| 315 | + //请求行 | |
| 316 | + String platformHostAddress = platform.getServerIP() + ":" + platform.getServerPort(); | |
| 317 | + String localHostAddress = sipLayer.getLocalIp(platform.getDeviceIp())+":"+ platform.getDevicePort(); | |
| 318 | + SipURI requestLine = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, platformHostAddress); | |
| 319 | + //via | |
| 320 | + ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>(); | |
| 321 | + ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(platform.getDeviceIp()), platform.getDevicePort(), platform.getTransport(), viaTag); | |
| 322 | + viaHeader.setRPort(); | |
| 323 | + viaHeaders.add(viaHeader); | |
| 324 | + | |
| 325 | + //from | |
| 326 | + SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(platform.getDeviceGBId(), sipConfig.getDomain()); | |
| 327 | + Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI); | |
| 328 | + FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack | |
| 329 | + //to | |
| 330 | + SipURI toSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, platformHostAddress); | |
| 331 | + Address toAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(toSipURI); | |
| 332 | + ToHeader toHeader = sipLayer.getSipFactory().createHeaderFactory().createToHeader(toAddress,null); | |
| 333 | + | |
| 334 | + //Forwards | |
| 335 | + MaxForwardsHeader maxForwards = sipLayer.getSipFactory().createHeaderFactory().createMaxForwardsHeader(70); | |
| 336 | + | |
| 337 | + //ceq | |
| 338 | + CSeqHeader cSeqHeader = sipLayer.getSipFactory().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INVITE); | |
| 339 | + request = sipLayer.getSipFactory().createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); | |
| 340 | + | |
| 341 | + request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); | |
| 342 | + | |
| 343 | + Address concatAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(),localHostAddress)); | |
| 344 | + request.addHeader(sipLayer.getSipFactory().createHeaderFactory().createContactHeader(concatAddress)); | |
| 345 | + // Subject | |
| 346 | + SubjectHeader subjectHeader = sipLayer.getSipFactory().createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", channelId, ssrc, sipConfig.getId(), 0)); | |
| 347 | + request.addHeader(subjectHeader); | |
| 348 | + ContentTypeHeader contentTypeHeader = sipLayer.getSipFactory().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP"); | |
| 349 | + request.setContent(content, contentTypeHeader); | |
| 350 | + return request; | |
| 351 | + } | |
| 352 | + | |
| 353 | + public Request createByteRequest(ParentPlatform platform, String channelId, SipTransactionInfo transactionInfo) throws PeerUnavailableException, ParseException, InvalidArgumentException { | |
| 354 | + String deviceHostAddress = platform.getDeviceIp() + ":" + platform.getDevicePort(); | |
| 355 | + Request request = null; | |
| 356 | + SipURI requestLine = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, deviceHostAddress); | |
| 357 | + | |
| 358 | + // via | |
| 359 | + ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>(); | |
| 360 | + ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(platform.getDeviceIp()), platform.getDevicePort(), platform.getTransport(), SipUtils.getNewViaTag()); | |
| 361 | + viaHeaders.add(viaHeader); | |
| 362 | + //from | |
| 363 | + SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain()); | |
| 364 | + Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI); | |
| 365 | + FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.isAsSender()?transactionInfo.getFromTag():transactionInfo.getToTag()); | |
| 366 | + //to | |
| 367 | + SipURI toSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, deviceHostAddress); | |
| 368 | + Address toAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(toSipURI); | |
| 369 | + ToHeader toHeader = sipLayer.getSipFactory().createHeaderFactory().createToHeader(toAddress,transactionInfo.isAsSender()?transactionInfo.getToTag():transactionInfo.getFromTag()); | |
| 370 | + | |
| 371 | + //Forwards | |
| 372 | + MaxForwardsHeader maxForwards = sipLayer.getSipFactory().createHeaderFactory().createMaxForwardsHeader(70); | |
| 373 | + | |
| 374 | + //ceq | |
| 375 | + CSeqHeader cSeqHeader = sipLayer.getSipFactory().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.BYE); | |
| 376 | + CallIdHeader callIdHeader = sipLayer.getSipFactory().createHeaderFactory().createCallIdHeader(transactionInfo.getCallId()); | |
| 377 | + request = sipLayer.getSipFactory().createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); | |
| 378 | + | |
| 379 | + request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); | |
| 380 | + | |
| 381 | + Address concatAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(), sipLayer.getLocalIp(platform.getDeviceIp())+":"+ platform.getDevicePort())); | |
| 382 | + request.addHeader(sipLayer.getSipFactory().createHeaderFactory().createContactHeader(concatAddress)); | |
| 383 | + | |
| 384 | + request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); | |
| 385 | + | |
| 386 | + return request; | |
| 387 | + } | |
| 311 | 388 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
| ... | ... | @@ -591,12 +591,12 @@ public class SIPCommander implements ISIPCommander { |
| 591 | 591 | return; |
| 592 | 592 | } |
| 593 | 593 | if (!mediaServerItem.isRtpEnable()) { |
| 594 | - // 单端口暂不支持语音对讲 | |
| 595 | - logger.info("[语音对讲] 单端口暂不支持此操作"); | |
| 594 | + // 单端口暂不支持语音喊话 | |
| 595 | + logger.info("[语音喊话] 单端口暂不支持此操作"); | |
| 596 | 596 | return; |
| 597 | 597 | } |
| 598 | 598 | |
| 599 | - logger.info("[语音对讲] {} 分配的ZLM为: {} [{}:{}]", stream, mediaServerItem.getId(), mediaServerItem.getIp(), sendRtpItem.getPort()); | |
| 599 | + logger.info("[语音喊话] {} 分配的ZLM为: {} [{}:{}]", stream, mediaServerItem.getId(), mediaServerItem.getIp(), sendRtpItem.getPort()); | |
| 600 | 600 | HookSubscribeForStreamChange hookSubscribeForStreamChange = HookSubscribeFactory.on_stream_changed("rtp", stream, true, "rtsp", mediaServerItem.getId()); |
| 601 | 601 | subscribe.addSubscribe(hookSubscribeForStreamChange, (MediaServerItem mediaServerItemInUse, JSONObject json) -> { |
| 602 | 602 | if (event != null) { | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderFroPlatform.java
| 1 | 1 | package com.genersoft.iot.vmp.gb28181.transmit.cmd.impl; |
| 2 | 2 | |
| 3 | 3 | import com.alibaba.fastjson2.JSON; |
| 4 | +import com.alibaba.fastjson2.JSONObject; | |
| 4 | 5 | import com.genersoft.iot.vmp.conf.DynamicTask; |
| 6 | +import com.genersoft.iot.vmp.conf.UserSetting; | |
| 7 | +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; | |
| 5 | 8 | import com.genersoft.iot.vmp.gb28181.SipLayer; |
| 6 | 9 | import com.genersoft.iot.vmp.gb28181.bean.*; |
| 7 | 10 | import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; |
| 11 | +import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager; | |
| 8 | 12 | import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; |
| 9 | 13 | import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; |
| 10 | 14 | import com.genersoft.iot.vmp.gb28181.transmit.cmd.SIPRequestHeaderPlarformProvider; |
| 11 | 15 | import com.genersoft.iot.vmp.gb28181.utils.SipUtils; |
| 12 | 16 | import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory; |
| 17 | +import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe; | |
| 18 | +import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeFactory; | |
| 19 | +import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeForStreamChange; | |
| 13 | 20 | import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem; |
| 14 | 21 | import com.genersoft.iot.vmp.service.IMediaServerService; |
| 15 | 22 | import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; |
| 23 | +import com.genersoft.iot.vmp.service.bean.SSRCInfo; | |
| 16 | 24 | import com.genersoft.iot.vmp.storager.IRedisCatchStorage; |
| 17 | 25 | import com.genersoft.iot.vmp.storager.dao.dto.PlatformRegisterInfo; |
| 18 | 26 | import com.genersoft.iot.vmp.utils.DateUtil; |
| 19 | 27 | import gov.nist.javax.sip.message.MessageFactoryImpl; |
| 20 | 28 | import gov.nist.javax.sip.message.SIPRequest; |
| 29 | +import gov.nist.javax.sip.message.SIPResponse; | |
| 21 | 30 | import org.slf4j.Logger; |
| 22 | 31 | import org.slf4j.LoggerFactory; |
| 23 | 32 | import org.springframework.beans.factory.annotation.Autowired; |
| ... | ... | @@ -27,6 +36,7 @@ import org.springframework.stereotype.Component; |
| 27 | 36 | import org.springframework.util.ObjectUtils; |
| 28 | 37 | |
| 29 | 38 | import javax.sip.InvalidArgumentException; |
| 39 | +import javax.sip.ResponseEvent; | |
| 30 | 40 | import javax.sip.SipException; |
| 31 | 41 | import javax.sip.header.CallIdHeader; |
| 32 | 42 | import javax.sip.header.WWWAuthenticateHeader; |
| ... | ... | @@ -63,6 +73,16 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform { |
| 63 | 73 | private SIPSender sipSender; |
| 64 | 74 | |
| 65 | 75 | @Autowired |
| 76 | + private ZlmHttpHookSubscribe subscribe; | |
| 77 | + | |
| 78 | + @Autowired | |
| 79 | + private UserSetting userSetting; | |
| 80 | + | |
| 81 | + | |
| 82 | + @Autowired | |
| 83 | + private VideoStreamSessionManager streamSession; | |
| 84 | + | |
| 85 | + @Autowired | |
| 66 | 86 | private DynamicTask dynamicTask; |
| 67 | 87 | |
| 68 | 88 | @Override |
| ... | ... | @@ -336,6 +356,7 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform { |
| 336 | 356 | sipSender.transmitRequest(parentPlatform.getDeviceIp(), request); |
| 337 | 357 | } |
| 338 | 358 | |
| 359 | + | |
| 339 | 360 | /** |
| 340 | 361 | * 向上级回复DeviceStatus查询信息 |
| 341 | 362 | * @param parentPlatform 平台信息 |
| ... | ... | @@ -702,4 +723,107 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform { |
| 702 | 723 | } |
| 703 | 724 | sipSender.transmitRequest(platform.getDeviceIp(),byeRequest); |
| 704 | 725 | } |
| 726 | + | |
| 727 | + @Override | |
| 728 | + public void streamByeCmd(ParentPlatform platform, String channelId, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException { | |
| 729 | + SsrcTransaction ssrcTransaction = streamSession.getSsrcTransaction(platform.getServerGBId(), channelId, callId, stream); | |
| 730 | + if (ssrcTransaction == null) { | |
| 731 | + throw new SsrcTransactionNotFoundException(platform.getServerGBId(), channelId, callId, stream); | |
| 732 | + } | |
| 733 | + | |
| 734 | + mediaServerService.releaseSsrc(ssrcTransaction.getMediaServerId(), ssrcTransaction.getSsrc()); | |
| 735 | + mediaServerService.closeRTPServer(ssrcTransaction.getMediaServerId(), ssrcTransaction.getStream()); | |
| 736 | + streamSession.remove(ssrcTransaction.getDeviceId(), ssrcTransaction.getChannelId(), ssrcTransaction.getStream()); | |
| 737 | + | |
| 738 | + Request byteRequest = headerProviderPlatformProvider.createByteRequest(platform, channelId, ssrcTransaction.getSipTransactionInfo()); | |
| 739 | + sipSender.transmitRequest(sipLayer.getLocalIp(platform.getDeviceIp()), byteRequest, null, okEvent); | |
| 740 | + } | |
| 741 | + | |
| 742 | + @Override | |
| 743 | + public void broadcastResultCmd(ParentPlatform platform, DeviceChannel deviceChannel, String sn, boolean result, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException { | |
| 744 | + if (platform == null || deviceChannel == null) { | |
| 745 | + return; | |
| 746 | + } | |
| 747 | + String characterSet = platform.getCharacterSet(); | |
| 748 | + StringBuffer mediaStatusXml = new StringBuffer(200); | |
| 749 | + mediaStatusXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n"); | |
| 750 | + mediaStatusXml.append("<Response>\r\n"); | |
| 751 | + mediaStatusXml.append("<CmdType>Broadcast</CmdType>\r\n"); | |
| 752 | + mediaStatusXml.append("<SN>" + sn + "</SN>\r\n"); | |
| 753 | + mediaStatusXml.append("<DeviceID>" + deviceChannel.getChannelId() + "</DeviceID>\r\n"); | |
| 754 | + mediaStatusXml.append("<Result>" + (result?"OK":"ERROR") + "</Result>\r\n"); | |
| 755 | + mediaStatusXml.append("</Response>\r\n"); | |
| 756 | + | |
| 757 | + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(platform.getDeviceIp(), platform.getTransport()); | |
| 758 | + | |
| 759 | + SIPRequest messageRequest = (SIPRequest)headerProviderPlatformProvider.createMessageRequest(platform, mediaStatusXml.toString(), | |
| 760 | + SipUtils.getNewFromTag(), SipUtils.getNewViaTag(), callIdHeader); | |
| 761 | + | |
| 762 | + sipSender.transmitRequest(platform.getDeviceIp(),messageRequest, errorEvent, okEvent); | |
| 763 | + } | |
| 764 | + | |
| 765 | + @Override | |
| 766 | + public void broadcastInviteCmd(ParentPlatform platform, String channelId, MediaServerItem mediaServerItem, | |
| 767 | + SSRCInfo ssrcInfo, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, | |
| 768 | + SipSubscribe.Event errorEvent) throws ParseException, SipException, InvalidArgumentException { | |
| 769 | + String stream = ssrcInfo.getStream(); | |
| 770 | + | |
| 771 | + if (platform == null) { | |
| 772 | + return; | |
| 773 | + } | |
| 774 | + | |
| 775 | + logger.info("{} 分配的ZLM为: {} [{}:{}]", stream, mediaServerItem.getId(), mediaServerItem.getIp(), ssrcInfo.getPort()); | |
| 776 | + HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", stream, true, "rtsp", mediaServerItem.getId()); | |
| 777 | + subscribe.addSubscribe(hookSubscribe, (MediaServerItem mediaServerItemInUse, JSONObject json) -> { | |
| 778 | + if (event != null) { | |
| 779 | + event.response(mediaServerItemInUse, json); | |
| 780 | + subscribe.removeSubscribe(hookSubscribe); | |
| 781 | + } | |
| 782 | + }); | |
| 783 | + String sdpIp = mediaServerItem.getSdpIp(); | |
| 784 | + | |
| 785 | + StringBuffer content = new StringBuffer(200); | |
| 786 | + content.append("v=0\r\n"); | |
| 787 | + content.append("o=" + channelId + " 0 0 IN IP4 " + sdpIp + "\r\n"); | |
| 788 | + content.append("s=Play\r\n"); | |
| 789 | + content.append("c=IN IP4 " + sdpIp + "\r\n"); | |
| 790 | + content.append("t=0 0\r\n"); | |
| 791 | + | |
| 792 | + if ("TCP-PASSIVE".equalsIgnoreCase(userSetting.getBroadcastForPlatform())) { | |
| 793 | + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 8 96\r\n"); | |
| 794 | + } else if ("TCP-ACTIVE".equalsIgnoreCase(userSetting.getBroadcastForPlatform())) { | |
| 795 | + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 8 96\r\n"); | |
| 796 | + } else if ("UDP".equalsIgnoreCase(userSetting.getBroadcastForPlatform())) { | |
| 797 | + content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 8 96\r\n"); | |
| 798 | + } | |
| 799 | + | |
| 800 | + content.append("a=recvonly\r\n"); | |
| 801 | + content.append("a=rtpmap:8 PCMA/8000\r\n"); | |
| 802 | + content.append("a=rtpmap:96 PS/90000\r\n"); | |
| 803 | + if ("TCP-PASSIVE".equalsIgnoreCase(userSetting.getBroadcastForPlatform())) { | |
| 804 | + content.append("a=setup:passive\r\n"); | |
| 805 | + content.append("a=connection:new\r\n"); | |
| 806 | + }else if ("TCP-ACTIVE".equalsIgnoreCase(userSetting.getBroadcastForPlatform())) { | |
| 807 | + content.append("a=setup:active\r\n"); | |
| 808 | + content.append("a=connection:new\r\n"); | |
| 809 | + } | |
| 810 | + | |
| 811 | + content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc | |
| 812 | + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(sipLayer.getLocalIp(platform.getDeviceIp()), platform.getTransport()); | |
| 813 | + | |
| 814 | + Request request = headerProviderPlatformProvider.createInviteRequest(platform, channelId, | |
| 815 | + content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), ssrcInfo.getSsrc(), | |
| 816 | + callIdHeader); | |
| 817 | + sipSender.transmitRequest(sipLayer.getLocalIp(platform.getDeviceIp()), request, (e -> { | |
| 818 | + streamSession.remove(platform.getServerGBId(), channelId, ssrcInfo.getStream()); | |
| 819 | + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); | |
| 820 | + errorEvent.response(e); | |
| 821 | + }), e -> { | |
| 822 | + // 这里为例避免一个通道的点播只有一个callID这个参数使用一个固定值 | |
| 823 | + ResponseEvent responseEvent = (ResponseEvent) e.event; | |
| 824 | + SIPResponse response = (SIPResponse) responseEvent.getResponse(); | |
| 825 | + streamSession.put(platform.getServerGBId(), channelId, callIdHeader.getCallId(), stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, VideoStreamSessionManager.SessionType.play); | |
| 826 | + okEvent.response(e); | |
| 827 | + }); | |
| 828 | + } | |
| 705 | 829 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/AckRequestProcessor.java
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
| ... | ... | @@ -202,7 +202,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements |
| 202 | 202 | |
| 203 | 203 | MediaServerItem mediaServerItem = null; |
| 204 | 204 | StreamPushItem streamPushItem = null; |
| 205 | - StreamProxyItem proxyByAppAndStream =null; | |
| 205 | + StreamProxyItem proxyByAppAndStream = null; | |
| 206 | 206 | // 不是通道可能是直播流 |
| 207 | 207 | if (channel != null && gbStream == null) { |
| 208 | 208 | // 通道存在,发100,TRYING |
| ... | ... | @@ -1018,7 +1018,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements |
| 1018 | 1018 | |
| 1019 | 1019 | |
| 1020 | 1020 | CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME); |
| 1021 | - sendRtpItem.setPlayType(InviteStreamType.TALK); | |
| 1021 | + sendRtpItem.setPlayType(InviteStreamType.BROADCAST); | |
| 1022 | 1022 | sendRtpItem.setCallId(callIdHeader.getCallId()); |
| 1023 | 1023 | sendRtpItem.setPlatformId(requesterId); |
| 1024 | 1024 | sendRtpItem.setStatus(1); |
| ... | ... | @@ -1113,7 +1113,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements |
| 1113 | 1113 | } |
| 1114 | 1114 | |
| 1115 | 1115 | } catch (SipException | InvalidArgumentException | ParseException | SdpParseException e) { |
| 1116 | - logger.error("[命令发送失败] 语音对讲 回复200OK(SDP): {}", e.getMessage()); | |
| 1116 | + logger.error("[命令发送失败] 语音喊话 回复200OK(SDP): {}", e.getMessage()); | |
| 1117 | 1117 | } |
| 1118 | 1118 | return sipResponse; |
| 1119 | 1119 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/BroadcastNotifyMessageHandler.java
0 → 100644
| 1 | +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd; | |
| 2 | + | |
| 3 | +import com.alibaba.fastjson2.JSONObject; | |
| 4 | +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; | |
| 5 | +import com.genersoft.iot.vmp.gb28181.bean.*; | |
| 6 | +import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; | |
| 7 | +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; | |
| 8 | +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; | |
| 9 | +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; | |
| 10 | +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler; | |
| 11 | +import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory; | |
| 12 | +import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem; | |
| 13 | +import com.genersoft.iot.vmp.service.IDeviceService; | |
| 14 | +import com.genersoft.iot.vmp.service.IMediaServerService; | |
| 15 | +import com.genersoft.iot.vmp.service.IPlatformService; | |
| 16 | +import com.genersoft.iot.vmp.service.IPlayService; | |
| 17 | +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; | |
| 18 | +import com.genersoft.iot.vmp.storager.IVideoManagerStorage; | |
| 19 | +import gov.nist.javax.sip.message.SIPRequest; | |
| 20 | +import org.dom4j.Element; | |
| 21 | +import org.slf4j.Logger; | |
| 22 | +import org.slf4j.LoggerFactory; | |
| 23 | +import org.springframework.beans.factory.InitializingBean; | |
| 24 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 25 | +import org.springframework.stereotype.Component; | |
| 26 | + | |
| 27 | +import javax.sip.InvalidArgumentException; | |
| 28 | +import javax.sip.RequestEvent; | |
| 29 | +import javax.sip.SipException; | |
| 30 | +import javax.sip.message.Response; | |
| 31 | +import java.text.ParseException; | |
| 32 | + | |
| 33 | +/** | |
| 34 | + * 状态信息(心跳)报送 | |
| 35 | + */ | |
| 36 | +@Component | |
| 37 | +public class BroadcastNotifyMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { | |
| 38 | + | |
| 39 | + private Logger logger = LoggerFactory.getLogger(BroadcastNotifyMessageHandler.class); | |
| 40 | + private final static String cmdType = "Broadcast"; | |
| 41 | + | |
| 42 | + @Autowired | |
| 43 | + private NotifyMessageHandler notifyMessageHandler; | |
| 44 | + | |
| 45 | + @Autowired | |
| 46 | + private IVideoManagerStorage storage; | |
| 47 | + | |
| 48 | + @Autowired | |
| 49 | + private ISIPCommanderForPlatform commanderForPlatform; | |
| 50 | + | |
| 51 | + @Autowired | |
| 52 | + private IMediaServerService mediaServerService; | |
| 53 | + | |
| 54 | + @Autowired | |
| 55 | + private IPlayService playService; | |
| 56 | + | |
| 57 | + @Autowired | |
| 58 | + private IDeviceService deviceService; | |
| 59 | + | |
| 60 | + @Autowired | |
| 61 | + private IPlatformService platformService; | |
| 62 | + | |
| 63 | + @Autowired | |
| 64 | + private AudioBroadcastManager audioBroadcastManager; | |
| 65 | + | |
| 66 | + @Autowired | |
| 67 | + private ZLMRTPServerFactory zlmrtpServerFactory; | |
| 68 | + | |
| 69 | + @Autowired | |
| 70 | + private IRedisCatchStorage redisCatchStorage; | |
| 71 | + | |
| 72 | + @Override | |
| 73 | + public void afterPropertiesSet() throws Exception { | |
| 74 | + notifyMessageHandler.addHandler(cmdType, this); | |
| 75 | + } | |
| 76 | + | |
| 77 | + @Override | |
| 78 | + public void handForDevice(RequestEvent evt, Device device, Element element) { | |
| 79 | + | |
| 80 | + } | |
| 81 | + | |
| 82 | + @Override | |
| 83 | + public void handForPlatform(RequestEvent evt, ParentPlatform platform, Element rootElement) { | |
| 84 | + // 来自上级平台的语音喊话请求 | |
| 85 | + SIPRequest request = (SIPRequest) evt.getRequest(); | |
| 86 | + try { | |
| 87 | + Element snElement = rootElement.element("SN"); | |
| 88 | + if (snElement == null) { | |
| 89 | + responseAck(request, Response.BAD_REQUEST, "sn must not null"); | |
| 90 | + return; | |
| 91 | + } | |
| 92 | + String sn = snElement.getText(); | |
| 93 | + Element targetIDElement = rootElement.element("TargetID"); | |
| 94 | + if (targetIDElement == null) { | |
| 95 | + responseAck(request, Response.BAD_REQUEST, "TargetID must not null"); | |
| 96 | + return; | |
| 97 | + } | |
| 98 | + String targetId = targetIDElement.getText(); | |
| 99 | + | |
| 100 | + | |
| 101 | + logger.info("[国标级联 语音喊话] platform: {}, channel: {}", platform.getServerGBId(), targetId); | |
| 102 | + | |
| 103 | + DeviceChannel deviceChannel = storage.queryChannelInParentPlatform(platform.getServerGBId(), targetId); | |
| 104 | + if (deviceChannel == null) { | |
| 105 | + responseAck(request, Response.NOT_FOUND, "TargetID not found"); | |
| 106 | + return; | |
| 107 | + } | |
| 108 | + // 向下级发送语音的喊话请求 | |
| 109 | + Device device = deviceService.getDevice(deviceChannel.getDeviceId()); | |
| 110 | + if (device == null) { | |
| 111 | + responseAck(request, Response.NOT_FOUND, "device not found"); | |
| 112 | + return; | |
| 113 | + } | |
| 114 | + responseAck(request, Response.OK); | |
| 115 | + | |
| 116 | + // 查看语音通道是否已经建立并且已经在使用 | |
| 117 | + if (playService.audioBroadcastInUse(device, targetId)) { | |
| 118 | + commanderForPlatform.broadcastResultCmd(platform, deviceChannel, sn, false,null, null); | |
| 119 | + return; | |
| 120 | + } | |
| 121 | + | |
| 122 | + MediaServerItem mediaServerForMinimumLoad = mediaServerService.getMediaServerForMinimumLoad(null); | |
| 123 | + commanderForPlatform.broadcastResultCmd(platform, deviceChannel, sn, true, eventResult->{ | |
| 124 | + logger.info("[国标级联] 语音喊话 回复失败 platform: {}, 错误:{}/{}", platform.getServerGBId(), eventResult.statusCode, eventResult.msg); | |
| 125 | + }, eventResult->{ | |
| 126 | + // 消息发送成功, 向上级发送invite,获取推流 | |
| 127 | + try { | |
| 128 | + platformService.broadcastInvite(platform, deviceChannel.getChannelId(), mediaServerForMinimumLoad, (mediaServerItem, response)->{ | |
| 129 | + // 上级平台推流成功 | |
| 130 | + String app = response.getString("app"); | |
| 131 | + String stream = response.getString("stream"); | |
| 132 | + AudioBroadcastCatch broadcastCatch = audioBroadcastManager.get(device.getDeviceId(), targetId); | |
| 133 | + if (broadcastCatch != null ) { | |
| 134 | + if (playService.audioBroadcastInUse(device, targetId)) { | |
| 135 | + logger.info("[国标级联] 语音喊话 设备正正在使用中 platform: {}, channel: {}", | |
| 136 | + platform.getServerGBId(), deviceChannel.getChannelId()); | |
| 137 | + // 查看语音通道已经建立且已经占用 回复BYE | |
| 138 | + try { | |
| 139 | + platformService.stopBroadcast(platform, deviceChannel.getChannelId(), stream); | |
| 140 | + } catch (InvalidArgumentException | ParseException | SsrcTransactionNotFoundException | | |
| 141 | + SipException e) { | |
| 142 | + logger.info("[消息发送失败] 国标级联 语音喊话 platform: {}, channel: {}", platform.getServerGBId(), deviceChannel.getChannelId()); | |
| 143 | + } | |
| 144 | + }else { | |
| 145 | + // 查看语音通道已经建立但是未占用 | |
| 146 | + broadcastCatch.setApp(app); | |
| 147 | + broadcastCatch.setStream(stream); | |
| 148 | + broadcastCatch.setMediaServerItem(mediaServerItem); | |
| 149 | + audioBroadcastManager.update(broadcastCatch); | |
| 150 | + // 推流到设备 | |
| 151 | + SendRtpItem sendRtpItem = redisCatchStorage.querySendRTPServer(null, targetId, stream, null); | |
| 152 | + if (sendRtpItem == null) { | |
| 153 | + logger.warn("[国标级联] 语音喊话 异常,未找到发流信息, channelId: {}, stream: {}", targetId, stream); | |
| 154 | + logger.info("[国标级联] 语音喊话 重新开始,channelId: {}, stream: {}", targetId, stream); | |
| 155 | + try { | |
| 156 | + playService.audioBroadcastCmd(device, targetId, mediaServerItem, app, stream, 60, true, msg -> { | |
| 157 | + logger.info("[语音喊话] 通道建立成功, device: {}, channel: {}", device.getDeviceId(), targetId); | |
| 158 | + }); | |
| 159 | + } catch (SipException | InvalidArgumentException | ParseException e) { | |
| 160 | + logger.info("[消息发送失败] 国标级联 语音喊话 platform: {}", platform.getServerGBId()); | |
| 161 | + } | |
| 162 | + }else { | |
| 163 | + // 发流 | |
| 164 | + JSONObject jsonObject = zlmrtpServerFactory.startSendRtp(mediaServerItem, sendRtpItem); | |
| 165 | + if (jsonObject != null && jsonObject.getInteger("code") == 0 ) { | |
| 166 | + logger.info("[语音喊话] 自动推流成功, device: {}, channel: {}", device.getDeviceId(), targetId); | |
| 167 | + }else { | |
| 168 | + logger.info("[语音喊话] 推流失败, 结果: {}", jsonObject); | |
| 169 | + } | |
| 170 | + } | |
| 171 | + } | |
| 172 | + }else { | |
| 173 | + try { | |
| 174 | + playService.audioBroadcastCmd(device, targetId, mediaServerItem, app, stream, 60, true, msg -> { | |
| 175 | + logger.info("[语音喊话] 通道建立成功, device: {}, channel: {}", device.getDeviceId(), targetId); | |
| 176 | + }); | |
| 177 | + } catch (SipException | InvalidArgumentException | ParseException e) { | |
| 178 | + logger.info("[消息发送失败] 国标级联 语音喊话 platform: {}", platform.getServerGBId()); | |
| 179 | + } | |
| 180 | + } | |
| 181 | + | |
| 182 | + }, eventResultForBroadcastInvite -> { | |
| 183 | + // 收到错误 | |
| 184 | + logger.info("[国标级联-语音喊话] 与下级通道建立失败 device: {}, channel: {}, 错误:{}/{}", device.getDeviceId(), | |
| 185 | + targetId, eventResultForBroadcastInvite.statusCode, eventResultForBroadcastInvite.msg); | |
| 186 | + }, (code, msg)->{ | |
| 187 | + // 超时 | |
| 188 | + logger.info("[国标级联-语音喊话] 与下级通道建立超时 device: {}, channel: {}, 错误:{}/{}", device.getDeviceId(), | |
| 189 | + targetId, code, msg); | |
| 190 | + }); | |
| 191 | + } catch (SipException | InvalidArgumentException | ParseException e) { | |
| 192 | + logger.info("[消息发送失败] 国标级联 语音喊话 invite消息 platform: {}", platform.getServerGBId()); | |
| 193 | + } | |
| 194 | + }); | |
| 195 | + } catch (SipException | InvalidArgumentException | ParseException e) { | |
| 196 | + logger.info("[消息发送失败] 国标级联 语音喊话 platform: {}", platform.getServerGBId()); | |
| 197 | + } | |
| 198 | + | |
| 199 | + } | |
| 200 | +} | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/BroadcastResponseMessageHandler.java
| ... | ... | @@ -7,6 +7,7 @@ import com.genersoft.iot.vmp.gb28181.bean.AudioBroadcastCatchStatus; |
| 7 | 7 | import com.genersoft.iot.vmp.gb28181.bean.Device; |
| 8 | 8 | import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform; |
| 9 | 9 | import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; |
| 10 | +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; | |
| 10 | 11 | import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; |
| 11 | 12 | import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; |
| 12 | 13 | import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; |
| ... | ... | @@ -53,9 +54,9 @@ public class BroadcastResponseMessageHandler extends SIPRequestProcessorParent i |
| 53 | 54 | @Override |
| 54 | 55 | public void handForDevice(RequestEvent evt, Device device, Element rootElement) { |
| 55 | 56 | |
| 56 | - String channelId = getText(rootElement, "DeviceID"); | |
| 57 | 57 | SIPRequest request = (SIPRequest) evt.getRequest(); |
| 58 | 58 | try { |
| 59 | + String channelId = getText(rootElement, "DeviceID"); | |
| 59 | 60 | if (!audioBroadcastManager.exit(device.getDeviceId(), channelId)) { |
| 60 | 61 | // 回复410 |
| 61 | 62 | responseAck((SIPRequest) evt.getRequest(), Response.GONE); | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java
| 1 | 1 | package com.genersoft.iot.vmp.gb28181.transmit.event.response.impl; |
| 2 | 2 | |
| 3 | -import com.genersoft.iot.vmp.conf.SipConfig; | |
| 4 | 3 | import com.genersoft.iot.vmp.gb28181.SipLayer; |
| 5 | -import com.genersoft.iot.vmp.gb28181.bean.Device; | |
| 6 | -import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction; | |
| 7 | -import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager; | |
| 8 | 4 | import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; |
| 9 | 5 | import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; |
| 10 | -import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; | |
| 11 | 6 | import com.genersoft.iot.vmp.gb28181.transmit.cmd.SIPRequestHeaderProvider; |
| 12 | 7 | import com.genersoft.iot.vmp.gb28181.transmit.event.response.SIPResponseProcessorAbstract; |
| 13 | -import com.genersoft.iot.vmp.gb28181.utils.SipUtils; | |
| 14 | -import com.genersoft.iot.vmp.service.IDeviceService; | |
| 15 | -import com.genersoft.iot.vmp.utils.GitUtil; | |
| 16 | 8 | import gov.nist.javax.sip.ResponseEventExt; |
| 17 | -import gov.nist.javax.sip.SipProviderImpl; | |
| 18 | 9 | import gov.nist.javax.sip.message.SIPResponse; |
| 19 | -import gov.nist.javax.sip.stack.SIPClientTransaction; | |
| 20 | -import gov.nist.javax.sip.stack.SIPDialog; | |
| 21 | -import gov.nist.javax.sip.stack.SIPTransaction; | |
| 22 | -import gov.nist.javax.sip.stack.SIPTransactionImpl; | |
| 23 | 10 | import org.slf4j.Logger; |
| 24 | 11 | import org.slf4j.LoggerFactory; |
| 25 | 12 | import org.springframework.beans.factory.annotation.Autowired; |
| 26 | -import org.springframework.beans.factory.annotation.Qualifier; | |
| 27 | 13 | import org.springframework.stereotype.Component; |
| 28 | 14 | |
| 29 | 15 | import javax.sdp.SdpFactory; |
| 30 | 16 | import javax.sdp.SdpParseException; |
| 31 | 17 | import javax.sdp.SessionDescription; |
| 32 | -import javax.sip.*; | |
| 33 | -import javax.sip.address.Address; | |
| 18 | +import javax.sip.InvalidArgumentException; | |
| 19 | +import javax.sip.ResponseEvent; | |
| 20 | +import javax.sip.SipException; | |
| 34 | 21 | import javax.sip.address.SipURI; |
| 35 | -import javax.sip.header.CSeqHeader; | |
| 36 | -import javax.sip.header.UserAgentHeader; | |
| 37 | 22 | import javax.sip.message.Request; |
| 38 | 23 | import javax.sip.message.Response; |
| 39 | 24 | import java.text.ParseException; |
| ... | ... | @@ -104,6 +89,7 @@ public class InviteResponseProcessor extends SIPResponseProcessorAbstract { |
| 104 | 89 | } else { |
| 105 | 90 | sdp = SdpFactory.getInstance().createSessionDescription(contentString); |
| 106 | 91 | } |
| 92 | + // 查看是否是来自设备的,此是回复 | |
| 107 | 93 | |
| 108 | 94 | SipURI requestUri = sipLayer.getSipFactory().createAddressFactory().createSipURI(sdp.getOrigin().getUsername(), event.getRemoteIpAddress() + ":" + event.getRemotePort()); |
| 109 | 95 | Request reqAck = headerProvider.createAckRequest(response.getLocalAddress().getHostAddress(), requestUri, response); | ... | ... |
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
| ... | ... | @@ -280,6 +280,9 @@ public class ZLMHttpHookListener { |
| 280 | 280 | logger.info("[ZLM HOOK] 流注销, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream()); |
| 281 | 281 | } |
| 282 | 282 | |
| 283 | + JSONObject ret = new JSONObject(); | |
| 284 | + ret.put("code", 0); | |
| 285 | + ret.put("msg", "success"); | |
| 283 | 286 | MediaServerItem mediaInfo = mediaServerService.getOne(param.getMediaServerId()); |
| 284 | 287 | JSONObject json = (JSONObject) JSON.toJSON(param); |
| 285 | 288 | taskExecutor.execute(() -> { |
| ... | ... | @@ -335,34 +338,34 @@ public class ZLMHttpHookListener { |
| 335 | 338 | } |
| 336 | 339 | }else if ("broadcast".equals(param.getApp())){ |
| 337 | 340 | // 语音对讲推流 stream需要满足格式deviceId_channelId |
| 338 | - if (param.getStream().indexOf("_") > 0) { | |
| 339 | - String[] streamArray = param.getStream().split("_"); | |
| 340 | - if (streamArray.length == 2) { | |
| 341 | - String deviceId = streamArray[0]; | |
| 342 | - String channelId = streamArray[1]; | |
| 343 | - Device device = deviceService.getDevice(deviceId); | |
| 344 | - if (device != null) { | |
| 345 | - if (param.isRegist()) { | |
| 346 | - if (audioBroadcastManager.exit(deviceId, channelId)) { | |
| 347 | - playService.stopAudioBroadcast(deviceId, channelId); | |
| 348 | - } | |
| 349 | - // 开启语音对讲通道 | |
| 350 | - try { | |
| 351 | - playService.audioBroadcastCmd(device, channelId, 60, mediaInfo, param.getApp(), param.getStream(), (msg)->{ | |
| 352 | - logger.info("[语音对讲] 通道建立成功, device: {}, channel: {}", deviceId, channelId); | |
| 353 | - }); | |
| 354 | - } catch (InvalidArgumentException | ParseException | SipException e) { | |
| 355 | - logger.error("[命令发送失败] 语音对讲: {}", e.getMessage()); | |
| 356 | - } | |
| 357 | - }else { | |
| 358 | - // 流注销 | |
| 359 | - playService.stopAudioBroadcast(deviceId, channelId); | |
| 360 | - } | |
| 361 | - } else{ | |
| 362 | - logger.info("[语音对讲] 未找到设备:{}", deviceId); | |
| 363 | - } | |
| 364 | - } | |
| 365 | - } | |
| 341 | + if (param.getStream().indexOf("_") > 0) { | |
| 342 | + String[] streamArray = param.getStream().split("_"); | |
| 343 | + if (streamArray.length == 2) { | |
| 344 | + String deviceId = streamArray[0]; | |
| 345 | + String channelId = streamArray[1]; | |
| 346 | + Device device = deviceService.getDevice(deviceId); | |
| 347 | + if (device != null) { | |
| 348 | + if (param.isRegist()) { | |
| 349 | + if (audioBroadcastManager.exit(deviceId, channelId)) { | |
| 350 | + playService.stopAudioBroadcast(deviceId, channelId); | |
| 351 | + } | |
| 352 | + // 开启语音对讲通道 | |
| 353 | + try { | |
| 354 | + playService.audioBroadcastCmd(device, channelId, mediaInfo, param.getApp(), param.getStream(), 60, false, (msg)->{ | |
| 355 | + logger.info("[语音对讲] 通道建立成功, device: {}, channel: {}", deviceId, channelId); | |
| 356 | + }); | |
| 357 | + } catch (InvalidArgumentException | ParseException | SipException e) { | |
| 358 | + logger.error("[命令发送失败] 语音对讲: {}", e.getMessage()); | |
| 359 | + } | |
| 360 | + }else { | |
| 361 | + // 流注销 | |
| 362 | + playService.stopAudioBroadcast(deviceId, channelId); | |
| 363 | + } | |
| 364 | + } else{ | |
| 365 | + logger.info("[语音对讲] 未找到设备:{}", deviceId); | |
| 366 | + } | |
| 367 | + } | |
| 368 | + } | |
| 366 | 369 | }else if ("talk".equals(param.getApp())){ |
| 367 | 370 | // 语音对讲推流 stream需要满足格式deviceId_channelId |
| 368 | 371 | if (param.getStream().indexOf("_") > 0) { | ... | ... |
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java
| ... | ... | @@ -92,7 +92,7 @@ public class ZLMRTPServerFactory { |
| 92 | 92 | return result; |
| 93 | 93 | } |
| 94 | 94 | |
| 95 | - public int createRTPServer(MediaServerItem mediaServerItem, String streamId, int ssrc, Integer port) { | |
| 95 | + public int createRTPServer(MediaServerItem mediaServerItem, String streamId, int ssrc, Integer port, Boolean onlyAuto) { | |
| 96 | 96 | int result = -1; |
| 97 | 97 | // 查询此rtp server 是否已经存在 |
| 98 | 98 | JSONObject rtpInfo = zlmresTfulUtils.getRtpInfo(mediaServerItem, streamId); |
| ... | ... | @@ -108,7 +108,7 @@ public class ZLMRTPServerFactory { |
| 108 | 108 | JSONObject jsonObject = zlmresTfulUtils.closeRtpServer(mediaServerItem, param); |
| 109 | 109 | if (jsonObject != null ) { |
| 110 | 110 | if (jsonObject.getInteger("code") == 0) { |
| 111 | - return createRTPServer(mediaServerItem, streamId, ssrc, port); | |
| 111 | + return createRTPServer(mediaServerItem, streamId, ssrc, port, onlyAuto); | |
| 112 | 112 | }else { |
| 113 | 113 | logger.warn("[开启rtpServer], 重启RtpServer错误"); |
| 114 | 114 | } |
| ... | ... | @@ -131,6 +131,9 @@ public class ZLMRTPServerFactory { |
| 131 | 131 | param.put("port", port); |
| 132 | 132 | } |
| 133 | 133 | param.put("ssrc", ssrc); |
| 134 | + if (onlyAuto != null) { | |
| 135 | + param.put("only_audio", onlyAuto?"1":"0"); | |
| 136 | + } | |
| 134 | 137 | JSONObject openRtpServerResultJson = zlmresTfulUtils.openRtpServer(mediaServerItem, param); |
| 135 | 138 | logger.info(JSONObject.toJSONString(openRtpServerResultJson)); |
| 136 | 139 | if (openRtpServerResultJson != null) { |
| ... | ... | @@ -352,4 +355,52 @@ public class ZLMRTPServerFactory { |
| 352 | 355 | return result; |
| 353 | 356 | } |
| 354 | 357 | |
| 358 | + public JSONObject startSendRtp(MediaServerItem mediaInfo, SendRtpItem sendRtpItem) { | |
| 359 | + String is_Udp = sendRtpItem.isTcp() ? "0" : "1"; | |
| 360 | + logger.info("rtp/{}开始向上级推流, 目标={}:{},SSRC={}", sendRtpItem.getStreamId(), sendRtpItem.getIp(), sendRtpItem.getPort(), sendRtpItem.getSsrc()); | |
| 361 | + Map<String, Object> param = new HashMap<>(12); | |
| 362 | + param.put("vhost","__defaultVhost__"); | |
| 363 | + param.put("app",sendRtpItem.getApp()); | |
| 364 | + param.put("stream",sendRtpItem.getStreamId()); | |
| 365 | + param.put("ssrc", sendRtpItem.getSsrc()); | |
| 366 | + param.put("src_port", sendRtpItem.getLocalPort()); | |
| 367 | + param.put("pt", sendRtpItem.getPt()); | |
| 368 | + param.put("use_ps", sendRtpItem.isUsePs() ? "1" : "0"); | |
| 369 | + param.put("only_audio", sendRtpItem.isOnlyAudio() ? "1" : "0"); | |
| 370 | + if (!sendRtpItem.isTcp()) { | |
| 371 | + // udp模式下开启rtcp保活 | |
| 372 | + param.put("udp_rtcp_timeout", sendRtpItem.isRtcp()? "1":"0"); | |
| 373 | + } | |
| 374 | + | |
| 375 | + if (mediaInfo == null) { | |
| 376 | + return null; | |
| 377 | + } | |
| 378 | + // 如果是非严格模式,需要关闭端口占用 | |
| 379 | + JSONObject startSendRtpStreamResult = null; | |
| 380 | + if (sendRtpItem.getLocalPort() != 0) { | |
| 381 | + HookSubscribeForRtpServerTimeout hookSubscribeForRtpServerTimeout = HookSubscribeFactory.on_rtp_server_timeout(sendRtpItem.getSsrc(), null, mediaInfo.getId()); | |
| 382 | + hookSubscribe.removeSubscribe(hookSubscribeForRtpServerTimeout); | |
| 383 | + if (releasePort(mediaInfo, sendRtpItem.getSsrc())) { | |
| 384 | + if (sendRtpItem.isTcpActive()) { | |
| 385 | + startSendRtpStreamResult = startSendRtpPassive(mediaInfo, param); | |
| 386 | + System.out.println(JSON.toJSON(param)); | |
| 387 | + }else { | |
| 388 | + param.put("is_udp", is_Udp); | |
| 389 | + param.put("dst_url", sendRtpItem.getIp()); | |
| 390 | + param.put("dst_port", sendRtpItem.getPort()); | |
| 391 | + startSendRtpStreamResult = startSendRtpStream(mediaInfo, param); | |
| 392 | + } | |
| 393 | + } | |
| 394 | + }else { | |
| 395 | + if (sendRtpItem.isTcpActive()) { | |
| 396 | + startSendRtpStreamResult = startSendRtpPassive(mediaInfo, param); | |
| 397 | + }else { | |
| 398 | + param.put("is_udp", is_Udp); | |
| 399 | + param.put("dst_url", sendRtpItem.getIp()); | |
| 400 | + param.put("dst_port", sendRtpItem.getPort()); | |
| 401 | + startSendRtpStreamResult = startSendRtpStream(mediaInfo, param); | |
| 402 | + } | |
| 403 | + } | |
| 404 | + return startSendRtpStreamResult; | |
| 405 | + } | |
| 355 | 406 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java
| ... | ... | @@ -47,7 +47,7 @@ public interface IMediaServerService { |
| 47 | 47 | |
| 48 | 48 | SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, String ssrc, boolean ssrcCheck, boolean isPlayback); |
| 49 | 49 | |
| 50 | - SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, String ssrc, boolean ssrcCheck, boolean isPlayback, Integer port); | |
| 50 | + SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, String ssrc, boolean ssrcCheck, boolean isPlayback, Integer port, Boolean onlyAuto); | |
| 51 | 51 | |
| 52 | 52 | void closeRTPServer(MediaServerItem mediaServerItem, String streamId); |
| 53 | 53 | ... | ... |
src/main/java/com/genersoft/iot/vmp/service/IPlatformService.java
| 1 | 1 | package com.genersoft.iot.vmp.service; |
| 2 | 2 | |
| 3 | +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; | |
| 3 | 4 | import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform; |
| 5 | +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; | |
| 6 | +import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe; | |
| 7 | +import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem; | |
| 8 | +import com.genersoft.iot.vmp.service.bean.InviteTimeOutCallback; | |
| 4 | 9 | import com.github.pagehelper.PageInfo; |
| 5 | 10 | |
| 11 | +import javax.sip.InvalidArgumentException; | |
| 12 | +import javax.sip.SipException; | |
| 13 | +import java.text.ParseException; | |
| 14 | + | |
| 6 | 15 | /** |
| 7 | 16 | * 国标平台的业务类 |
| 8 | 17 | * @author lin |
| ... | ... | @@ -48,4 +57,23 @@ public interface IPlatformService { |
| 48 | 57 | * @param platformId 平台 |
| 49 | 58 | */ |
| 50 | 59 | void sendNotifyMobilePosition(String platformId); |
| 60 | + | |
| 61 | + /** | |
| 62 | + * 向上级发送语音喊话的消息 | |
| 63 | + * @param platform 平台 | |
| 64 | + * @param channelId 通道 | |
| 65 | + * @param hookEvent hook事件 | |
| 66 | + * @param errorEvent 信令错误事件 | |
| 67 | + * @param timeoutCallback 超时事件 | |
| 68 | + */ | |
| 69 | + void broadcastInvite(ParentPlatform platform, String channelId, MediaServerItem mediaServerItem, ZlmHttpHookSubscribe.Event hookEvent, | |
| 70 | + SipSubscribe.Event errorEvent, InviteTimeOutCallback timeoutCallback) throws InvalidArgumentException, ParseException, SipException; | |
| 71 | + | |
| 72 | + /** | |
| 73 | + * 语音喊话回复BYE | |
| 74 | + * @param platform 平台 | |
| 75 | + * @param channelId 通道 | |
| 76 | + * @param stream 流信息 | |
| 77 | + */ | |
| 78 | + void stopBroadcast(ParentPlatform platform, String channelId, String stream )throws InvalidArgumentException, ParseException, SsrcTransactionNotFoundException, SipException; | |
| 51 | 79 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/service/IPlayService.java
| ... | ... | @@ -12,7 +12,9 @@ import com.genersoft.iot.vmp.service.bean.PlayBackCallback; |
| 12 | 12 | import com.genersoft.iot.vmp.service.bean.SSRCInfo; |
| 13 | 13 | import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult; |
| 14 | 14 | import com.genersoft.iot.vmp.vmanager.gb28181.play.bean.AudioBroadcastEvent; |
| 15 | +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; | |
| 15 | 16 | import gov.nist.javax.sip.message.SIPResponse; |
| 17 | +import org.springframework.web.context.request.async.DeferredResult; | |
| 16 | 18 | |
| 17 | 19 | import javax.sip.InvalidArgumentException; |
| 18 | 20 | import javax.sip.SipException; |
| ... | ... | @@ -54,6 +56,11 @@ public interface IPlayService { |
| 54 | 56 | void zlmServerOnline(String mediaServerId); |
| 55 | 57 | |
| 56 | 58 | AudioBroadcastResult audioBroadcast(Device device, String channelId, Boolean broadcastMode); |
| 59 | + | |
| 60 | + boolean audioBroadcastCmd(Device device, String channelId, MediaServerItem mediaServerItem, String app, String stream, int timeout, boolean isFromPlatform, AudioBroadcastEvent event) throws InvalidArgumentException, ParseException, SipException; | |
| 61 | + | |
| 62 | + boolean audioBroadcastInUse(Device device, String channelId); | |
| 63 | + | |
| 57 | 64 | void stopAudioBroadcast(String deviceId, String channelId); |
| 58 | 65 | |
| 59 | 66 | void audioBroadcastCmd(Device device, String channelId, int timeout, MediaServerItem mediaServerItem, String sourceApp, String sourceStream, AudioBroadcastEvent event) throws InvalidArgumentException, ParseException, SipException; | ... | ... |
src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java
| 1 | 1 | package com.genersoft.iot.vmp.service.impl; |
| 2 | 2 | |
| 3 | -import java.time.LocalDateTime; | |
| 4 | -import java.util.ArrayList; | |
| 5 | -import java.util.Collections; | |
| 6 | -import java.util.HashMap; | |
| 7 | -import java.util.List; | |
| 8 | -import java.util.Map; | |
| 9 | -import java.util.Set; | |
| 10 | - | |
| 11 | -import com.genersoft.iot.vmp.media.zlm.ZLMRunner; | |
| 12 | -import com.genersoft.iot.vmp.service.IStreamProxyService; | |
| 13 | -import com.genersoft.iot.vmp.storager.IVideoManagerStorage; | |
| 14 | 3 | import com.alibaba.fastjson2.JSON; |
| 15 | 4 | import com.alibaba.fastjson2.JSONArray; |
| 16 | 5 | import com.alibaba.fastjson2.JSONObject; |
| ... | ... | @@ -39,8 +28,6 @@ import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; |
| 39 | 28 | import okhttp3.OkHttpClient; |
| 40 | 29 | import okhttp3.Request; |
| 41 | 30 | import okhttp3.Response; |
| 42 | -import java.time.LocalDateTime; | |
| 43 | -import java.util.*; | |
| 44 | 31 | import org.slf4j.Logger; |
| 45 | 32 | import org.slf4j.LoggerFactory; |
| 46 | 33 | import org.springframework.beans.factory.annotation.Autowired; |
| ... | ... | @@ -134,7 +121,8 @@ public class MediaServerServiceImpl implements IMediaServerService { |
| 134 | 121 | } |
| 135 | 122 | |
| 136 | 123 | @Override |
| 137 | - public SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, String presetSsrc, boolean ssrcCheck, boolean isPlayback, Integer port) { | |
| 124 | + public SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, String presetSsrc, boolean ssrcCheck, | |
| 125 | + boolean isPlayback, Integer port, Boolean onlyAuto) { | |
| 138 | 126 | if (mediaServerItem == null || mediaServerItem.getId() == null) { |
| 139 | 127 | logger.info("[openRTPServer] 失败, mediaServerItem == null || mediaServerItem.getId() == null"); |
| 140 | 128 | return null; |
| ... | ... | @@ -163,7 +151,7 @@ public class MediaServerServiceImpl implements IMediaServerService { |
| 163 | 151 | } |
| 164 | 152 | int rtpServerPort; |
| 165 | 153 | if (mediaServerItem.isRtpEnable()) { |
| 166 | - rtpServerPort = zlmrtpServerFactory.createRTPServer(mediaServerItem, streamId, ssrcCheck?Integer.parseInt(ssrc):0, port); | |
| 154 | + rtpServerPort = zlmrtpServerFactory.createRTPServer(mediaServerItem, streamId, ssrcCheck?Integer.parseInt(ssrc):0, port, onlyAuto); | |
| 167 | 155 | } else { |
| 168 | 156 | rtpServerPort = mediaServerItem.getRtpProxyPort(); |
| 169 | 157 | } |
| ... | ... | @@ -174,7 +162,7 @@ public class MediaServerServiceImpl implements IMediaServerService { |
| 174 | 162 | |
| 175 | 163 | @Override |
| 176 | 164 | public SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, String ssrc, boolean ssrcCheck, boolean isPlayback) { |
| 177 | - return openRTPServer(mediaServerItem, streamId, ssrc, ssrcCheck, isPlayback, null); | |
| 165 | + return openRTPServer(mediaServerItem, streamId, ssrc, ssrcCheck, isPlayback, null, null); | |
| 178 | 166 | } |
| 179 | 167 | |
| 180 | 168 | @Override | ... | ... |
src/main/java/com/genersoft/iot/vmp/service/impl/PlatformServiceImpl.java
| 1 | 1 | package com.genersoft.iot.vmp.service.impl; |
| 2 | 2 | |
| 3 | +import com.alibaba.fastjson2.JSONObject; | |
| 4 | +import com.genersoft.iot.vmp.common.StreamInfo; | |
| 3 | 5 | import com.genersoft.iot.vmp.conf.DynamicTask; |
| 4 | 6 | import com.genersoft.iot.vmp.conf.UserSetting; |
| 7 | +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; | |
| 5 | 8 | import com.genersoft.iot.vmp.gb28181.bean.*; |
| 6 | 9 | import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; |
| 10 | +import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager; | |
| 7 | 11 | import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform; |
| 8 | 12 | import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory; |
| 13 | +import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe; | |
| 14 | +import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeFactory; | |
| 15 | +import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeForStreamChange; | |
| 9 | 16 | import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem; |
| 10 | 17 | import com.genersoft.iot.vmp.service.IMediaServerService; |
| 11 | 18 | import com.genersoft.iot.vmp.service.IPlatformService; |
| 19 | +import com.genersoft.iot.vmp.service.IPlayService; | |
| 12 | 20 | import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; |
| 21 | +import com.genersoft.iot.vmp.service.bean.InviteTimeOutCallback; | |
| 22 | +import com.genersoft.iot.vmp.service.bean.SSRCInfo; | |
| 13 | 23 | import com.genersoft.iot.vmp.storager.IRedisCatchStorage; |
| 14 | 24 | import com.genersoft.iot.vmp.storager.dao.GbStreamMapper; |
| 15 | 25 | import com.genersoft.iot.vmp.storager.dao.ParentPlatformMapper; |
| ... | ... | @@ -21,11 +31,13 @@ import org.springframework.beans.factory.annotation.Autowired; |
| 21 | 31 | import org.springframework.stereotype.Service; |
| 22 | 32 | |
| 23 | 33 | import javax.sip.InvalidArgumentException; |
| 34 | +import javax.sip.ResponseEvent; | |
| 24 | 35 | import javax.sip.SipException; |
| 25 | 36 | import java.text.ParseException; |
| 26 | 37 | import java.util.HashMap; |
| 27 | 38 | import java.util.List; |
| 28 | 39 | import java.util.Map; |
| 40 | +import java.util.UUID; | |
| 29 | 41 | |
| 30 | 42 | /** |
| 31 | 43 | * @author lin |
| ... | ... | @@ -65,6 +77,16 @@ public class PlatformServiceImpl implements IPlatformService { |
| 65 | 77 | @Autowired |
| 66 | 78 | private UserSetting userSetting; |
| 67 | 79 | |
| 80 | + @Autowired | |
| 81 | + private ZlmHttpHookSubscribe subscribe; | |
| 82 | + | |
| 83 | + @Autowired | |
| 84 | + private VideoStreamSessionManager streamSession; | |
| 85 | + | |
| 86 | + | |
| 87 | + @Autowired | |
| 88 | + private IPlayService playService; | |
| 89 | + | |
| 68 | 90 | |
| 69 | 91 | |
| 70 | 92 | @Override |
| ... | ... | @@ -310,4 +332,138 @@ public class PlatformServiceImpl implements IPlatformService { |
| 310 | 332 | } |
| 311 | 333 | } |
| 312 | 334 | } |
| 335 | + | |
| 336 | + @Override | |
| 337 | + public void broadcastInvite(ParentPlatform platform, String channelId, MediaServerItem mediaServerItem, ZlmHttpHookSubscribe.Event hookEvent, | |
| 338 | + SipSubscribe.Event errorEvent, InviteTimeOutCallback timeoutCallback) throws InvalidArgumentException, ParseException, SipException { | |
| 339 | + | |
| 340 | + if (mediaServerItem == null) { | |
| 341 | + logger.info("[国标级联] 语音喊话未找到可用的zlm. platform: {}", platform.getServerGBId()); | |
| 342 | + return; | |
| 343 | + } | |
| 344 | + StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(platform.getServerGBId(), channelId); | |
| 345 | + if (streamInfo != null) { | |
| 346 | + // 如果zlm不存在这个流,则删除数据即可 | |
| 347 | + MediaServerItem mediaServerItemForStreamInfo = mediaServerService.getOne(streamInfo.getMediaServerId()); | |
| 348 | + if (mediaServerItemForStreamInfo != null) { | |
| 349 | + Boolean ready = zlmrtpServerFactory.isStreamReady(mediaServerItemForStreamInfo, streamInfo.getApp(), streamInfo.getStream()); | |
| 350 | + if (!ready) { | |
| 351 | + // 错误存在于redis中的数据 | |
| 352 | + redisCatchStorage.stopPlay(streamInfo); | |
| 353 | + }else { | |
| 354 | + // 流确实尚在推流,直接回调结果 | |
| 355 | + JSONObject json = new JSONObject(); | |
| 356 | + json.put("app", streamInfo.getApp()); | |
| 357 | + json.put("stream", streamInfo.getStream()); | |
| 358 | + hookEvent.response(mediaServerItemForStreamInfo, json); | |
| 359 | + return; | |
| 360 | + } | |
| 361 | + } | |
| 362 | + } | |
| 363 | + | |
| 364 | + String streamId = null; | |
| 365 | + if (mediaServerItem.isRtpEnable()) { | |
| 366 | + streamId = String.format("%s_%s", platform.getServerGBId(), channelId); | |
| 367 | + } | |
| 368 | + // 默认不进行SSRC校验, TODO 后续可改为配置 | |
| 369 | + boolean ssrcCheck = false; | |
| 370 | + SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, null, ssrcCheck, false, null, true); | |
| 371 | + if (ssrcInfo == null || ssrcInfo.getPort() < 0) { | |
| 372 | + logger.info("[国标级联] 发起语音喊话 开启端口监听失败, platform: {}, channel: {}", platform.getServerGBId(), channelId); | |
| 373 | + errorEvent.response(new SipSubscribe.EventResult(-1, "端口监听失败")); | |
| 374 | + return; | |
| 375 | + } | |
| 376 | + logger.info("[国标级联] 发起语音喊话 deviceId: {}, channelId: {},收流端口: {}, 收流模式:{}, SSRC: {}, SSRC校验:{}", | |
| 377 | + platform.getServerGBId(), channelId, ssrcInfo.getPort(), userSetting.getBroadcastForPlatform(), ssrcInfo.getSsrc(), ssrcCheck); | |
| 378 | + | |
| 379 | + String timeOutTaskKey = UUID.randomUUID().toString(); | |
| 380 | + dynamicTask.startDelay(timeOutTaskKey, () -> { | |
| 381 | + // 执行超时任务时查询是否已经成功,成功了则不执行超时任务,防止超时任务取消失败的情况 | |
| 382 | + if (redisCatchStorage.queryPlayByDevice(platform.getServerGBId(), channelId) == null) { | |
| 383 | + logger.info("[国标级联] 发起语音喊话 收流超时 deviceId: {}, channelId: {},端口:{}, SSRC: {}", platform.getServerGBId(), channelId, ssrcInfo.getPort(), ssrcInfo.getSsrc()); | |
| 384 | + // 点播超时回复BYE 同时释放ssrc以及此次点播的资源 | |
| 385 | + try { | |
| 386 | + commanderForPlatform.streamByeCmd(platform, channelId, ssrcInfo.getStream(), null, null); | |
| 387 | + } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { | |
| 388 | + logger.error("[点播超时], 发送BYE失败 {}", e.getMessage()); | |
| 389 | + } finally { | |
| 390 | + timeoutCallback.run(1, "收流超时"); | |
| 391 | + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); | |
| 392 | + mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); | |
| 393 | + streamSession.remove(platform.getServerGBId(), channelId, ssrcInfo.getStream()); | |
| 394 | + mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); | |
| 395 | + } | |
| 396 | + } | |
| 397 | + }, userSetting.getPlayTimeout()); | |
| 398 | + commanderForPlatform.broadcastInviteCmd(platform, channelId, mediaServerItem, ssrcInfo, (mediaServerItemForInvite, response)->{ | |
| 399 | + dynamicTask.stop(timeOutTaskKey); | |
| 400 | + // hook响应 | |
| 401 | + playService.onPublishHandlerForPlay(mediaServerItemForInvite, response, platform.getServerGBId(), channelId); | |
| 402 | + // 收到流 | |
| 403 | + if (hookEvent != null) { | |
| 404 | + hookEvent.response(mediaServerItem, response); | |
| 405 | + } | |
| 406 | + }, event -> { | |
| 407 | + // 收到200OK 检测ssrc是否有变化,防止上级自定义了ssrc | |
| 408 | + ResponseEvent responseEvent = (ResponseEvent) event.event; | |
| 409 | + String contentString = new String(responseEvent.getResponse().getRawContent()); | |
| 410 | + // 获取ssrc | |
| 411 | + int ssrcIndex = contentString.indexOf("y="); | |
| 412 | + // 检查是否有y字段 | |
| 413 | + if (ssrcIndex >= 0) { | |
| 414 | + //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段 TODO 后续对不规范的非10位ssrc兼容 | |
| 415 | + String ssrcInResponse = contentString.substring(ssrcIndex + 2, ssrcIndex + 12); | |
| 416 | + // 查询到ssrc不一致且开启了ssrc校验则需要针对处理 | |
| 417 | + if (ssrcInfo.getSsrc().equals(ssrcInResponse) || ssrcCheck) { | |
| 418 | + return; | |
| 419 | + } | |
| 420 | + logger.info("[点播消息] 收到invite 200, 发现下级自定义了ssrc: {}", ssrcInResponse); | |
| 421 | + if (!mediaServerItem.isRtpEnable()) { | |
| 422 | + logger.info("[点播消息] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse); | |
| 423 | + | |
| 424 | + if (!mediaServerItem.getSsrcConfig().checkSsrc(ssrcInResponse)) { | |
| 425 | + // ssrc 不可用 | |
| 426 | + // 释放ssrc | |
| 427 | + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); | |
| 428 | + streamSession.remove(platform.getServerGBId(), channelId, ssrcInfo.getStream()); | |
| 429 | + event.msg = "下级自定义了ssrc,但是此ssrc不可用"; | |
| 430 | + event.statusCode = 400; | |
| 431 | + errorEvent.response(event); | |
| 432 | + return; | |
| 433 | + } | |
| 434 | + | |
| 435 | + // 单端口模式streamId也有变化,需要重新设置监听 | |
| 436 | + if (!mediaServerItem.isRtpEnable()) { | |
| 437 | + // 添加订阅 | |
| 438 | + HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", ssrcInfo.getStream(), true, "rtsp", mediaServerItem.getId()); | |
| 439 | + subscribe.removeSubscribe(hookSubscribe); | |
| 440 | + hookSubscribe.getContent().put("stream", String.format("%08x", Integer.parseInt(ssrcInResponse)).toUpperCase()); | |
| 441 | + subscribe.addSubscribe(hookSubscribe, (MediaServerItem mediaServerItemInUse, JSONObject response) -> { | |
| 442 | + logger.info("[ZLM HOOK] ssrc修正后收到订阅消息: " + response.toJSONString()); | |
| 443 | + dynamicTask.stop(timeOutTaskKey); | |
| 444 | + // hook响应 | |
| 445 | + playService.onPublishHandlerForPlay(mediaServerItemInUse, response, platform.getServerGBId(), channelId); | |
| 446 | + hookEvent.response(mediaServerItemInUse, response); | |
| 447 | + }); | |
| 448 | + } | |
| 449 | + // 关闭rtp server | |
| 450 | + mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); | |
| 451 | + // 重新开启ssrc server | |
| 452 | + mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, false, false, ssrcInfo.getPort(), true); | |
| 453 | + | |
| 454 | + | |
| 455 | + } | |
| 456 | + } | |
| 457 | + }, eventResult -> { | |
| 458 | + // 收到错误回复 | |
| 459 | + if (errorEvent != null) { | |
| 460 | + errorEvent.response(eventResult); | |
| 461 | + } | |
| 462 | + }); | |
| 463 | + } | |
| 464 | + | |
| 465 | + @Override | |
| 466 | + public void stopBroadcast(ParentPlatform platform, String channelId, String stream) throws InvalidArgumentException, ParseException, SsrcTransactionNotFoundException, SipException { | |
| 467 | + commanderForPlatform.streamByeCmd(platform, channelId, stream, null, null); | |
| 468 | + } | |
| 313 | 469 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
| ... | ... | @@ -490,7 +490,7 @@ public class PlayServiceImpl implements IPlayService { |
| 490 | 490 | // 关闭rtp server |
| 491 | 491 | mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); |
| 492 | 492 | // 重新开启ssrc server |
| 493 | - mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), false, ssrcInfo.getPort()); | |
| 493 | + mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), false, ssrcInfo.getPort(), false); | |
| 494 | 494 | |
| 495 | 495 | } |
| 496 | 496 | } |
| ... | ... | @@ -731,7 +731,7 @@ public class PlayServiceImpl implements IPlayService { |
| 731 | 731 | // 关闭rtp server |
| 732 | 732 | mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); |
| 733 | 733 | // 重新开启ssrc server |
| 734 | - mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), true, ssrcInfo.getPort()); | |
| 734 | + mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), true, ssrcInfo.getPort(), false); | |
| 735 | 735 | } |
| 736 | 736 | } |
| 737 | 737 | } |
| ... | ... | @@ -966,16 +966,16 @@ public class PlayServiceImpl implements IPlayService { |
| 966 | 966 | } |
| 967 | 967 | |
| 968 | 968 | @Override |
| 969 | - public void audioBroadcastCmd(Device device, String channelId, int timeout, MediaServerItem mediaServerItem, String sourceApp, String sourceStream, AudioBroadcastEvent event) throws InvalidArgumentException, ParseException, SipException { | |
| 969 | + public boolean audioBroadcastCmd(Device device, String channelId, MediaServerItem mediaServerItem, String app, String stream, int timeout, boolean isFromPlatform, AudioBroadcastEvent event) throws InvalidArgumentException, ParseException, SipException { | |
| 970 | 970 | if (device == null || channelId == null) { |
| 971 | - return; | |
| 971 | + return false; | |
| 972 | 972 | } |
| 973 | 973 | logger.info("[语音喊话] device: {}, channel: {}", device.getDeviceId(), channelId); |
| 974 | 974 | DeviceChannel deviceChannel = storager.queryChannel(device.getDeviceId(), channelId); |
| 975 | 975 | if (deviceChannel == null) { |
| 976 | 976 | logger.warn("开启语音广播的时候未找到通道: {}", channelId); |
| 977 | 977 | event.call("开启语音广播的时候未找到通道"); |
| 978 | - return; | |
| 978 | + return false; | |
| 979 | 979 | } |
| 980 | 980 | // 查询通道使用状态 |
| 981 | 981 | if (audioBroadcastManager.exit(device.getDeviceId(), channelId)) { |
| ... | ... | @@ -986,7 +986,7 @@ public class PlayServiceImpl implements IPlayService { |
| 986 | 986 | if (streamReady) { |
| 987 | 987 | logger.warn("语音广播已经开启: {}", channelId); |
| 988 | 988 | event.call("语音广播已经开启"); |
| 989 | - return; | |
| 989 | + return false; | |
| 990 | 990 | } else { |
| 991 | 991 | stopAudioBroadcast(device.getDeviceId(), channelId); |
| 992 | 992 | } |
| ... | ... | @@ -1008,8 +1008,7 @@ public class PlayServiceImpl implements IPlayService { |
| 1008 | 1008 | // 发送通知 |
| 1009 | 1009 | cmder.audioBroadcastCmd(device, channelId, eventResultForOk -> { |
| 1010 | 1010 | // 发送成功 |
| 1011 | - AudioBroadcastCatch audioBroadcastCatch = new AudioBroadcastCatch(device.getDeviceId(), channelId, | |
| 1012 | - AudioBroadcastCatchStatus.Ready, mediaServerItem, sourceApp, sourceStream); | |
| 1011 | + AudioBroadcastCatch audioBroadcastCatch = new AudioBroadcastCatch(device.getDeviceId(), channelId, mediaServerItem, app, stream, event, AudioBroadcastCatchStatus.Ready, isFromPlatform); | |
| 1013 | 1012 | audioBroadcastManager.update(audioBroadcastCatch); |
| 1014 | 1013 | }, eventResultForError -> { |
| 1015 | 1014 | // 发送失败 |
| ... | ... | @@ -1017,6 +1016,24 @@ public class PlayServiceImpl implements IPlayService { |
| 1017 | 1016 | event.call("语音广播发送失败"); |
| 1018 | 1017 | stopAudioBroadcast(device.getDeviceId(), channelId); |
| 1019 | 1018 | }); |
| 1019 | + return true; | |
| 1020 | + } | |
| 1021 | + | |
| 1022 | + @Override | |
| 1023 | + public boolean audioBroadcastInUse(Device device, String channelId) { | |
| 1024 | + if (audioBroadcastManager.exit(device.getDeviceId(), channelId)) { | |
| 1025 | + SendRtpItem sendRtpItem = redisCatchStorage.querySendRTPServer(device.getDeviceId(), channelId, null, null); | |
| 1026 | + if (sendRtpItem != null && sendRtpItem.isOnlyAudio()) { | |
| 1027 | + // 查询流是否存在,不存在则认为是异常状态 | |
| 1028 | + MediaServerItem mediaServerServiceOne = mediaServerService.getOne(sendRtpItem.getMediaServerId()); | |
| 1029 | + Boolean streamReady = zlmrtpServerFactory.isStreamReady(mediaServerServiceOne, sendRtpItem.getApp(), sendRtpItem.getStreamId()); | |
| 1030 | + if (streamReady) { | |
| 1031 | + logger.warn("语音广播通道使用中: {}", channelId); | |
| 1032 | + return true; | |
| 1033 | + } | |
| 1034 | + } | |
| 1035 | + } | |
| 1036 | + return false; | |
| 1020 | 1037 | } |
| 1021 | 1038 | |
| 1022 | 1039 | ... | ... |
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java
| ... | ... | @@ -250,10 +250,10 @@ public class PlayController { |
| 250 | 250 | @GetMapping("/broadcast/{deviceId}/{channelId}") |
| 251 | 251 | @PostMapping("/broadcast/{deviceId}/{channelId}") |
| 252 | 252 | public AudioBroadcastResult broadcastApi(@PathVariable String deviceId, @PathVariable String channelId, Integer timeout, Boolean broadcastMode) { |
| 253 | - if (logger.isDebugEnabled()) { | |
| 254 | - logger.debug("语音广播API调用"); | |
| 255 | - } | |
| 256 | - Device device = storager.queryVideoDevice(deviceId); | |
| 253 | + if (logger.isDebugEnabled()) { | |
| 254 | + logger.debug("语音广播API调用"); | |
| 255 | + } | |
| 256 | + Device device = storager.queryVideoDevice(deviceId); | |
| 257 | 257 | if (device == null) { |
| 258 | 258 | throw new ControllerException(ErrorCode.ERROR400.getCode(), "未找到设备: " + deviceId); |
| 259 | 259 | } |
| ... | ... | @@ -265,7 +265,6 @@ public class PlayController { |
| 265 | 265 | |
| 266 | 266 | } |
| 267 | 267 | |
| 268 | - | |
| 269 | 268 | @Operation(summary = "停止语音广播") |
| 270 | 269 | @Parameter(name = "deviceId", description = "设备Id", required = true) |
| 271 | 270 | @Parameter(name = "channelId", description = "通道Id", required = true) | ... | ... |
src/main/resources/all-application.yml
| ... | ... | @@ -195,6 +195,8 @@ user-settings: |
| 195 | 195 | gb-send-stream-strict: false |
| 196 | 196 | # 设备上线时是否自动同步通道 |
| 197 | 197 | sync-channel-on-device-online: false |
| 198 | + # 国标级联语音喊话发流模式 * UDP:udp传输 TCP-ACTIVE:tcp主动模式 TCP-PASSIVE:tcp被动模式 | |
| 199 | + broadcast-for-platform: UDP | |
| 198 | 200 | # 是否使用设备来源Ip作为回复IP, 不设置则为 false |
| 199 | 201 | sip-use-source-ip-as-remote-address: false |
| 200 | 202 | # 是否开启sip日志 | ... | ... |
web_src/src/components/dialog/devicePlayer.vue
| ... | ... | @@ -663,46 +663,46 @@ export default { |
| 663 | 663 | this.startBroadcast(streamInfo.rtc) |
| 664 | 664 | } |
| 665 | 665 | |
| 666 | - }else { | |
| 667 | - this.$message({ | |
| 668 | - showClose: true, | |
| 669 | - message: res.data.msg, | |
| 670 | - type: "error", | |
| 671 | - }); | |
| 672 | - } | |
| 673 | - }); | |
| 674 | - }else if (this.broadcastStatus === 1) { | |
| 675 | - this.broadcastStatus = -1; | |
| 676 | - this.broadcastRtc.close() | |
| 677 | - } | |
| 678 | - }, | |
| 679 | - startBroadcast(url) { | |
| 680 | - // 获取推流鉴权Key | |
| 681 | - this.$axios({ | |
| 682 | - method: 'post', | |
| 683 | - url: '/api/user/userInfo', | |
| 684 | - }).then((res) => { | |
| 685 | - if (res.data.code !== 0) { | |
| 686 | - this.$message({ | |
| 687 | - showClose: true, | |
| 688 | - message: "获取推流鉴权Key失败", | |
| 689 | - type: "error", | |
| 690 | - }); | |
| 691 | - this.broadcastStatus = -1; | |
| 692 | - } else { | |
| 693 | - let pushKey = res.data.data.pushKey; | |
| 694 | - // 获取推流鉴权KEY | |
| 695 | - url += "&sign=" + crypto.createHash('md5').update(pushKey, "utf8").digest('hex') | |
| 696 | - console.log("开始语音对讲: " + url) | |
| 697 | - this.broadcastRtc = new ZLMRTCClient.Endpoint({ | |
| 698 | - debug: true, // 是否打印日志 | |
| 699 | - zlmsdpUrl: url, //流地址 | |
| 700 | - simulecast: false, | |
| 701 | - useCamera: false, | |
| 702 | - audioEnable: true, | |
| 703 | - videoEnable: false, | |
| 704 | - recvOnly: false, | |
| 705 | - }) | |
| 666 | + }else { | |
| 667 | + this.$message({ | |
| 668 | + showClose: true, | |
| 669 | + message: res.data.msg, | |
| 670 | + type: "error", | |
| 671 | + }); | |
| 672 | + } | |
| 673 | + }); | |
| 674 | + }else if (this.broadcastStatus === 1) { | |
| 675 | + this.broadcastStatus = -1; | |
| 676 | + this.broadcastRtc.close() | |
| 677 | + } | |
| 678 | + }, | |
| 679 | + startBroadcast(url){ | |
| 680 | + // 获取推流鉴权Key | |
| 681 | + this.$axios({ | |
| 682 | + method: 'post', | |
| 683 | + url: '/api/user/userInfo', | |
| 684 | + }).then( (res)=> { | |
| 685 | + if (res.data.code !== 0) { | |
| 686 | + this.$message({ | |
| 687 | + showClose: true, | |
| 688 | + message: "获取推流鉴权Key失败", | |
| 689 | + type: "error", | |
| 690 | + }); | |
| 691 | + this.broadcastStatus = -1; | |
| 692 | + }else { | |
| 693 | + let pushKey = res.data.data.pushKey; | |
| 694 | + // 获取推流鉴权KEY | |
| 695 | + url += "&sign=" + crypto.createHash('md5').update(pushKey, "utf8").digest('hex') | |
| 696 | + console.log("开始语音喊话: " + url) | |
| 697 | + this.broadcastRtc = new ZLMRTCClient.Endpoint({ | |
| 698 | + debug: true, // 是否打印日志 | |
| 699 | + zlmsdpUrl: url, //流地址 | |
| 700 | + simulecast: false, | |
| 701 | + useCamera: false, | |
| 702 | + audioEnable: true, | |
| 703 | + videoEnable: false, | |
| 704 | + recvOnly: false, | |
| 705 | + }) | |
| 706 | 706 | |
| 707 | 707 | // webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS,(e)=>{//获取到了远端流,可以播放 |
| 708 | 708 | // console.error('播放成功',e.streams) |
| ... | ... | @@ -715,15 +715,15 @@ export default { |
| 715 | 715 | // // this.eventcallbacK("LOCAL STREAM", "获取到了本地流") |
| 716 | 716 | // }); |
| 717 | 717 | |
| 718 | - this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_NOT_SUPPORT, (e) => {// 获取到了本地流 | |
| 719 | - console.error('不支持webrtc', e) | |
| 720 | - this.$message({ | |
| 721 | - showClose: true, | |
| 722 | - message: '不支持webrtc, 无法进行语音对讲', | |
| 723 | - type: 'error' | |
| 724 | - }); | |
| 725 | - this.broadcastStatus = -1; | |
| 726 | - }); | |
| 718 | + this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_NOT_SUPPORT,(e)=>{// 获取到了本地流 | |
| 719 | + console.error('不支持webrtc',e) | |
| 720 | + this.$message({ | |
| 721 | + showClose: true, | |
| 722 | + message: '不支持webrtc, 无法进行语音喊话', | |
| 723 | + type: 'error' | |
| 724 | + }); | |
| 725 | + this.broadcastStatus = -1; | |
| 726 | + }); | |
| 727 | 727 | |
| 728 | 728 | this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, (e) => {// ICE 协商出错 |
| 729 | 729 | console.error('ICE 协商出错') | ... | ... |