上饶市公安局wvp平台
zhongrj
2023-06-27 2e98b20bea4463e4465e3c19059d0744a09aec06
gb28181版本升级-补充
556 files added
86112 ■■■■■ changed files
.gitignore 30 ●●●●● patch | view | raw | blame | history
.gitmodules 3 ●●●●● patch | view | raw | blame | history
DOCKERFILE 130 ●●●●● patch | view | raw | blame | history
LICENSE 21 ●●●●● patch | view | raw | blame | history
README.md 171 ●●●●● patch | view | raw | blame | history
bootstrap.sh 91 ●●●●● patch | view | raw | blame | history
doc/README.md 102 ●●●●● patch | view | raw | blame | history
doc/_content/ability/_media/cascade1.png patch | view | raw | blame | history
doc/_content/ability/_media/cascade2.png patch | view | raw | blame | history
doc/_content/ability/_media/cascade3.png patch | view | raw | blame | history
doc/_content/ability/_media/cascade4.png patch | view | raw | blame | history
doc/_content/ability/_media/img.png patch | view | raw | blame | history
doc/_content/ability/_media/img_1.png patch | view | raw | blame | history
doc/_content/ability/_media/img_10.png patch | view | raw | blame | history
doc/_content/ability/_media/img_11.png patch | view | raw | blame | history
doc/_content/ability/_media/img_12.png patch | view | raw | blame | history
doc/_content/ability/_media/img_13.png patch | view | raw | blame | history
doc/_content/ability/_media/img_14.png patch | view | raw | blame | history
doc/_content/ability/_media/img_15.png patch | view | raw | blame | history
doc/_content/ability/_media/img_16.png patch | view | raw | blame | history
doc/_content/ability/_media/img_17.png patch | view | raw | blame | history
doc/_content/ability/_media/img_18.png patch | view | raw | blame | history
doc/_content/ability/_media/img_2.png patch | view | raw | blame | history
doc/_content/ability/_media/img_3.png patch | view | raw | blame | history
doc/_content/ability/_media/img_4.png patch | view | raw | blame | history
doc/_content/ability/_media/img_5.png patch | view | raw | blame | history
doc/_content/ability/_media/img_6.png patch | view | raw | blame | history
doc/_content/ability/_media/img_7.png patch | view | raw | blame | history
doc/_content/ability/_media/img_8.png patch | view | raw | blame | history
doc/_content/ability/_media/img_9.png patch | view | raw | blame | history
doc/_content/ability/auto_play.md 2 ●●●●● patch | view | raw | blame | history
doc/_content/ability/cascade.md 34 ●●●●● patch | view | raw | blame | history
doc/_content/ability/cascade2.md 18 ●●●●● patch | view | raw | blame | history
doc/_content/ability/cloud_record.md 8 ●●●●● patch | view | raw | blame | history
doc/_content/ability/continuous_recording.md 14 ●●●●● patch | view | raw | blame | history
doc/_content/ability/device.md 36 ●●●●● patch | view | raw | blame | history
doc/_content/ability/device_use.md 35 ●●●●● patch | view | raw | blame | history
doc/_content/ability/gis.md 37 ●●●●● patch | view | raw | blame | history
doc/_content/ability/node_manger.md 9 ●●●●● patch | view | raw | blame | history
doc/_content/ability/online_doc.md 2 ●●●●● patch | view | raw | blame | history
doc/_content/ability/proxy.md 24 ●●●●● patch | view | raw | blame | history
doc/_content/ability/push.md 41 ●●●●● patch | view | raw | blame | history
doc/_content/ability/user.md 2 ●●●●● patch | view | raw | blame | history
doc/_content/about_doc.md 5 ●●●●● patch | view | raw | blame | history
doc/_content/disclaimers.md 2 ●●●●● patch | view | raw | blame | history
doc/_content/introduction/_media/img.png patch | view | raw | blame | history
doc/_content/introduction/_media/img_1.png patch | view | raw | blame | history
doc/_content/introduction/_media/img_2.png patch | view | raw | blame | history
doc/_content/introduction/compile.md 95 ●●●●● patch | view | raw | blame | history
doc/_content/introduction/config.md 119 ●●●●● patch | view | raw | blame | history
doc/_content/introduction/deployment.md 36 ●●●●● patch | view | raw | blame | history
doc/_content/qa/_media/img.png patch | view | raw | blame | history
doc/_content/qa/_media/img_1.png patch | view | raw | blame | history
doc/_content/qa/_media/img_2.png patch | view | raw | blame | history
doc/_content/qa/_media/img_3.png patch | view | raw | blame | history
doc/_content/qa/_media/img_4.png patch | view | raw | blame | history
doc/_content/qa/_media/img_5.png patch | view | raw | blame | history
doc/_content/qa/bug.md 19 ●●●●● patch | view | raw | blame | history
doc/_content/qa/development.md 15 ●●●●● patch | view | raw | blame | history
doc/_content/qa/img.png patch | view | raw | blame | history
doc/_content/qa/play_error.md 57 ●●●●● patch | view | raw | blame | history
doc/_content/qa/regiser_error.md 8 ●●●●● patch | view | raw | blame | history
doc/_content/qa/start_error.md 24 ●●●●● patch | view | raw | blame | history
doc/_content/skill/_media/img.png patch | view | raw | blame | history
doc/_content/skill/_media/img_1.png patch | view | raw | blame | history
doc/_content/skill/_media/img_2.png patch | view | raw | blame | history
doc/_content/skill/tcpdump.md 62 ●●●●● patch | view | raw | blame | history
doc/_content/theory/_media/img.png patch | view | raw | blame | history
doc/_content/theory/_media/img_1.png patch | view | raw | blame | history
doc/_content/theory/_media/img_2.png patch | view | raw | blame | history
doc/_content/theory/_media/img_3.png patch | view | raw | blame | history
doc/_content/theory/_media/img_4.png patch | view | raw | blame | history
doc/_content/theory/_media/img_5.png patch | view | raw | blame | history
doc/_content/theory/_media/img_6.png patch | view | raw | blame | history
doc/_content/theory/_media/img_7.png patch | view | raw | blame | history
doc/_content/theory/_media/img_8.png patch | view | raw | blame | history
doc/_content/theory/_media/img_9.png patch | view | raw | blame | history
doc/_content/theory/channel_tree.md 14 ●●●●● patch | view | raw | blame | history
doc/_content/theory/code.md 25 ●●●●● patch | view | raw | blame | history
doc/_content/theory/img.png patch | view | raw | blame | history
doc/_content/theory/play.md 33 ●●●●● patch | view | raw | blame | history
doc/_content/theory/register.md 21 ●●●●● patch | view | raw | blame | history
doc/_coverpage.md 17 ●●●●● patch | view | raw | blame | history
doc/_media/favicon.ico patch | view | raw | blame | history
doc/_media/logo-mini.png patch | view | raw | blame | history
doc/_media/logo.png patch | view | raw | blame | history
doc/_media/weixin.jpg patch | view | raw | blame | history
doc/_media/zhifubao.jpg patch | view | raw | blame | history
doc/_navbar.md 1 ●●●● patch | view | raw | blame | history
doc/_sidebar.md 32 ●●●●● patch | view | raw | blame | history
doc/index.html 59 ●●●●● patch | view | raw | blame | history
docker/docker-compose.yml 49 ●●●●● patch | view | raw | blame | history
docker/redis/redis.conf 2 ●●●●● patch | view | raw | blame | history
docker/wvp/Dockerfile 81 ●●●●● patch | view | raw | blame | history
pom.xml 305 ●●●●● patch | view | raw | blame | history
snap/34020000001320000001_34020000001320000001.jpg 5 ●●●●● patch | view | raw | blame | history
snap/34020000002000000003_34020000001320000001.jpg 5 ●●●●● patch | view | raw | blame | history
sql/clean.sql 13 ●●●●● patch | view | raw | blame | history
sql/mysql.sql 576 ●●●●● patch | view | raw | blame | history
sql/update.sql 29 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java 46 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/common/ApiSaveConstant.java 198 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java 359 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/common/SystemInfoDto.java 22 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/common/VersionPo.java 136 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java 139 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/ApiAccessFilter.java 116 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/DynamicTask.java 138 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/GlobalExceptionHandler.java 57 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/GlobalResponseAdvice.java 69 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java 234 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/MediaStatusTimerTask.java 16 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/ProxyServletConfig.java 276 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/SipConfig.java 117 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/SipPlatformRunner.java 57 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/SpringDocConfig.java 89 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/SystemInfoTimerTask.java 39 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/ThreadPoolTaskConfig.java 68 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java 159 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/VersionConfig.java 37 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/VersionInfo.java 25 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/WVPTimerTask.java 33 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/druid/DruidConfiguration.java 64 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/druid/EnableDruidSupport.java 24 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/exception/ControllerException.java 37 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/exception/ServiceException.java 27 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/exception/SsrcTransactionNotFoundException.java 49 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/redis/RedisConfig.java 89 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/security/AnonymousAuthenticationEntryPoint.java 46 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/security/DefaultUserDetailsServiceImpl.java 48 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/security/InvalidSessionHandler.java 24 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/security/LoginFailureHandler.java 65 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/security/LoginSuccessHandler.java 28 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/security/LogoutHandler.java 27 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/security/SecurityUtils.java 78 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/security/UrlTokenHandler.java 24 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java 181 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/conf/security/dto/LoginUser.java 102 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java 96 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/auth/DigestServerAuthenticationHelper.java 295 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/AlarmChannelMessage.java 46 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/BaiduPoint.java 24 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogData.java 81 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/ChannelIdType.java 23 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/CmdSendFailEvent.java 27 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/CmdType.java 8 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java 421 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarm.java 187 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarmMethod.java 40 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannel.java 591 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannelInPlatform.java 23 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceNotFoundEvent.java 27 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbStream.java 125 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/HandlerCatchData.java 44 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/Host.java 35 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamCallback.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamInfo.java 61 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamType.java 8 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/MobilePosition.java 205 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/ParentPlatform.java 431 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/ParentPlatformCatch.java 58 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformCatalog.java 116 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformGbStream.java 39 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformRegister.java 15 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/PresetQuerySipReq.java 28 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordInfo.java 82 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordItem.java 133 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SDPInfo.java 14 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SendRtpItem.java 284 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipMsgInfo.java 56 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipTransactionInfo.java 54 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SsrcTransaction.java 81 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeHolder.java 106 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeInfo.java 99 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SyncStatus.java 51 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/TreeType.java 10 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/bean/WvpSipDate.java 149 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/conf/DefaultProperties.java 46 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/conf/SipLoggerPass.java 107 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java 118 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java 172 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEvent.java 31 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEventListener.java 57 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/event/device/RequestTimeoutEvent.java 25 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/event/device/RequestTimeoutEventImpl.java 42 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEvent.java 32 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEventListener.java 47 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEvent.java 85 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEventLister.java 194 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/session/CatalogDataCatch.java 140 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/session/RecordDataCatch.java 87 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/session/SsrcConfig.java 150 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/session/VideoStreamSessionManager.java 143 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/task/ISubscribeTask.java 10 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/task/SipDeviceRunner.java 51 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/task/impl/CatalogSubscribeTask.java 105 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/task/impl/MobilePositionSubscribeHandlerTask.java 44 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/task/impl/MobilePositionSubscribeTask.java 103 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/ISIPProcessorObserver.java 6 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPProcessorObserver.java 202 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java 110 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java 125 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/RequestMessage.java 39 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java 366 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommanderForPlatform.java 124 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java 310 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java 321 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java 1402 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderFroPlatform.java 648 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/ISIPRequestProcessor.java 14 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/SIPRequestProcessorParent.java 265 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/AckRequestProcessor.java 157 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java 162 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/CancelRequestProcessor.java 40 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java 932 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java 460 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java 176 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/SubscribeRequestProcessor.java 201 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/info/InfoRequestProcessor.java 139 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/IMessageHandler.java 23 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageHandlerAbstract.java 40 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageRequestProcessor.java 142 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/ControlMessageHandler.java 27 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/cmd/DeviceControlQueryMessageHandler.java 142 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/NotifyMessageHandler.java 26 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/AlarmNotifyMessageHandler.java 275 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java 93 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MediaStatusNotifyMessageHandler.java 119 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MobilePositionNotifyMessageHandler.java 175 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/QueryMessageHandler.java 25 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/AlarmQueryMessageHandler.java 68 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/CatalogQueryMessageHandler.java 130 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceInfoQueryMessageHandler.java 63 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceStatusQueryMessageHandler.java 76 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java 146 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/ResponseMessageHandler.java 25 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/AlarmResponseMessageHandler.java 58 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/BroadcastResponseMessageHandler.java 73 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/CatalogResponseMessageHandler.java 177 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/ConfigDownloadResponseMessageHandler.java 79 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceConfigResponseMessageHandler.java 58 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceControlResponseMessageHandler.java 71 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceInfoResponseMessageHandler.java 112 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceStatusResponseMessageHandler.java 92 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/MobilePositionResponseMessageHandler.java 153 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/PresetQueryResponseMessageHandler.java 120 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java 177 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/ISIPResponseProcessor.java 15 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/SIPResponseProcessorAbstract.java 8 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/ByeResponseProcessor.java 48 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/CancelResponseProcessor.java 47 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java 117 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/RegisterResponseProcessor.java 111 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/timeout/ITimeoutProcessor.java 7 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/timeout/impl/TimeoutProcessorImpl.java 47 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/utils/Coordtransform.java 126 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/utils/NumericUtil.java 46 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java 122 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/utils/XmlUtil.java 413 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/gb28181/vo/DeviceChannelVO.java 46 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/AssistRESTfulUtils.java 148 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java 783 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaListManager.java 136 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java 346 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java 344 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRunner.java 162 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMServerConfig.java 816 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/ZlmHttpHookSubscribe.java 156 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ChannelOnlineEvent.java 11 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookParam.java 17 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeFactory.java 33 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeForServerStarted.java 44 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeForStreamChange.java 43 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookType.java 23 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/IHookSubscribe.java 36 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaItem.java 446 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaServerItem.java 342 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/OnPlayHookParam.java 82 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/OnPublishHookParam.java 82 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/OriginType.java 23 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamAuthorityInfo.java 114 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamProxyItem.java 190 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamPushItem.java 329 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMRunInfo.java 33 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/event/ZLMEventAbstract.java 25 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/event/ZLMOfflineEvent.java 11 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/event/ZLMOnlineEvent.java 11 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/media/zlm/event/ZLMStatusEventListener.java 59 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/netty/business/entity/TalkBackEquipment.java 101 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/netty/business/entity/TalkBackEquipmentRecord.java 91 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/netty/business/mapper/TalkBackEquipmentMapper.java 41 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/netty/business/mapper/TalkBackEquipmentMapper.xml 68 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/netty/business/service/TalkBackEquipmentService.java 19 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/netty/business/service/impl/TalkBackEquipmentServiceImpl.java 64 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/netty/config/MyDecoder.java 132 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/netty/config/SysConfig.java 26 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/netty/event/StartupEvent.java 37 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/netty/handle/UdpServerHandler.java 191 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/netty/server/UdpServer.java 93 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/netty/util/Hex.java 551 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/netty/util/HexStringTool.java 303 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/netty/util/Utils.java 4 ●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/policeBodyCamera/entity/DeviceChannelPoliceCamera.java 452 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/policeBodyCamera/mapper/DeviceChannelPoliceCameraMapper.java 360 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/policeBodyCamera/service/DeviceChannelPoliceCameraService.java 32 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/policeBodyCamera/service/impl/DeviceChannelPoliceCameraServiceImpl.java 96 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/policeBodyCamera/vo/DeviceChannelPoliceCameraVO.java 14 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/IDeviceAlarmService.java 43 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/IDeviceChannelService.java 35 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/IDeviceService.java 158 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/IGbStreamService.java 58 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/ILogService.java 34 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java 90 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/IMediaService.java 44 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/IPlatformChannelService.java 22 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/IPlatformService.java 56 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/IPlayService.java 54 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/IRecordInfoServer.java 8 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/IRoleService.java 18 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/IStreamProxyService.java 105 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/IStreamPushService.java 109 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/IUserService.java 29 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/bean/GPSMsgInfo.java 106 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/bean/InviteTimeOutCallback.java 6 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannel.java 131 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannelResponse.java 71 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackCallback.java 10 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackResult.java 69 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/bean/PushStreamStatusChangeFromRedisDto.java 41 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/bean/RequestPushStreamMsg.java 170 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/bean/RequestSendItemMsg.java 173 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/bean/ResponseSendItemMsg.java 31 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/bean/SSRCInfo.java 38 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/bean/StreamPushItemFromRedis.java 34 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/bean/ThirdPartyGB.java 23 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsg.java 116 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsgCmd.java 12 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/DeviceAlarmServiceImpl.java 36 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java 221 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java 633 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/GbStreamServiceImpl.java 233 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/LogServiceImpl.java 36 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java 694 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java 133 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/PlatformChannelServiceImpl.java 108 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/PlatformServiceImpl.java 299 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java 873 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/RecordInfoServerImpl.java 25 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/RoleServerImpl.java 41 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/StreamProxyServiceImpl.java 457 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/StreamPushServiceImpl.java 512 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/StreamPushUploadFileHandler.java 177 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java 82 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisAlarmMsgListener.java 118 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGbPlayMsgListener.java 398 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGpsMsgListener.java 76 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamResponseListener.java 75 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamStatusListMsgListener.java 103 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamStatusMsgListener.java 100 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisStreamMsgListener.java 91 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java 243 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/IVideoManagerStorage.java 419 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceAlarmMapper.java 48 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java 749 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java 266 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMobilePositionMapper.java 75 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/GbStreamMapper.java 172 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/LogMapper.java 37 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java 132 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/ParentPlatformMapper.java 97 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformCatalogMapper.java 56 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformChannelMapper.java 102 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformGbStreamMapper.java 108 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/RecordInfoDao.java 25 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/RoleMapper.java 35 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/StreamProxyMapper.java 78 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/StreamPushMapper.java 174 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/UserMapper.java 65 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/dto/ChannelSourceInfo.java 22 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/dto/LogDto.java 86 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/dto/PlatformRegisterInfo.java 41 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/dto/RecordInfo.java 133 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/dto/Role.java 50 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/dao/dto/User.java 68 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java 774 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java 1116 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/CollectionUtil.java 13 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/ConfigConst.java 8 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/Coordtransform.java 126 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/DateUtil.java 90 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/GitUtil.java 59 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/GpsUtil.java 41 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/ObjectUtils.java 41 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/SpringBeanFactory.java 50 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/SystemInfoUtils.java 91 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/node/BaseNode.java 74 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/node/ForestNode.java 31 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/node/ForestNodeManager.java 68 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/node/ForestNodeMerger.java 51 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/node/INode.java 42 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/node/TreeNode.java 42 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/redis/FastJsonRedisSerializer.java 43 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil.java 894 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/bean/BaseTree.java 87 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/bean/BatchGBStreamParam.java 23 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeviceChannelTree.java 119 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeviceChannelTreeNode.java 56 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java 31 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/bean/PlayTypeEnum.java 23 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/bean/StreamPushExcelDto.java 88 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/bean/WVPResult.java 66 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/MobilePosition/MobilePositionController.java 168 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/SseController/SseController.java 37 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/alarm/AlarmController.java 195 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceConfig.java 160 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceControl.java 405 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceQuery.java 692 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/gbStream/GbStreamController.java 103 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/gbStream/bean/GbStreamParam.java 43 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/media/MediaController.java 116 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/platform/PlatformController.java 570 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/platform/bean/ChannelReduce.java 137 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/platform/bean/UpdateChannelParam.java 46 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java 304 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/bean/PlayResult.java 39 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/playback/PlaybackController.java 188 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/ptz/PtzController.java 178 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/record/GBRecordController.java 174 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/vo/PageVO.java 80 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/log/LogController.java 97 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/record/RecordController.java 51 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java 205 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/streamProxy/StreamProxyController.java 135 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/streamPush/StreamPushController.java 288 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/user/RoleController.java 79 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java 203 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiControlController.java 105 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiController.java 107 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiDeviceController.java 165 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java 217 ●●●●● patch | view | raw | blame | history
src/main/java/com/genersoft/iot/vmp/web/gb28181/AuthController.java 25 ●●●●● patch | view | raw | blame | history
src/main/resources/all-application.yml 199 ●●●●● patch | view | raw | blame | history
src/main/resources/application.yml 3 ●●●●● patch | view | raw | blame | history
src/main/resources/banner.txt 7 ●●●●● patch | view | raw | blame | history
src/main/resources/logback-spring-local.xml 132 ●●●●● patch | view | raw | blame | history
src/main/resources/wvpssl.jks patch | view | raw | blame | history
web_src/.babelrc 12 ●●●●● patch | view | raw | blame | history
web_src/.editorconfig 9 ●●●●● patch | view | raw | blame | history
web_src/.gitignore 14 ●●●●● patch | view | raw | blame | history
web_src/.postcssrc.js 14 ●●●●● patch | view | raw | blame | history
web_src/README.md 21 ●●●●● patch | view | raw | blame | history
web_src/build/build.js 41 ●●●●● patch | view | raw | blame | history
web_src/build/check-versions.js 54 ●●●●● patch | view | raw | blame | history
web_src/build/logo.png patch | view | raw | blame | history
web_src/build/utils.js 101 ●●●●● patch | view | raw | blame | history
web_src/build/vue-loader.conf.js 22 ●●●●● patch | view | raw | blame | history
web_src/build/webpack.base.conf.js 83 ●●●●● patch | view | raw | blame | history
web_src/build/webpack.dev.conf.js 101 ●●●●● patch | view | raw | blame | history
web_src/build/webpack.prod.conf.js 150 ●●●●● patch | view | raw | blame | history
web_src/config/dev.env.js 8 ●●●●● patch | view | raw | blame | history
web_src/config/index.js 87 ●●●●● patch | view | raw | blame | history
web_src/config/prod.env.js 4 ●●●● patch | view | raw | blame | history
web_src/index.html 19 ●●●●● patch | view | raw | blame | history
web_src/package-lock.json 12114 ●●●●● patch | view | raw | blame | history
web_src/package.json 78 ●●●●● patch | view | raw | blame | history
web_src/src/App.vue 108 ●●●●● patch | view | raw | blame | history
web_src/src/assets/icons.png patch | view | raw | blame | history
web_src/src/assets/loading.png patch | view | raw | blame | history
web_src/src/assets/login-bg.jpg patch | view | raw | blame | history
web_src/src/assets/login-cloud.png patch | view | raw | blame | history
web_src/src/assets/logo.png patch | view | raw | blame | history
web_src/src/assets/play.png patch | view | raw | blame | history
web_src/src/assets/zlm-log.png patch | view | raw | blame | history
web_src/src/components/CloudRecord.vue 211 ●●●●● patch | view | raw | blame | history
web_src/src/components/CloudRecordDetail.vue 577 ●●●●● patch | view | raw | blame | history
web_src/src/components/DeviceList.vue 353 ●●●●● patch | view | raw | blame | history
web_src/src/components/GeoConvertTools.js 250 ●●●●● patch | view | raw | blame | history
web_src/src/components/Login.vue 128 ●●●●● patch | view | raw | blame | history
web_src/src/components/MediaServerManger.vue 181 ●●●●● patch | view | raw | blame | history
web_src/src/components/ParentPlatformList.vue 193 ●●●●● patch | view | raw | blame | history
web_src/src/components/PushVideoList.vue 353 ●●●●● patch | view | raw | blame | history
web_src/src/components/StreamProxyList.vue 379 ●●●●● patch | view | raw | blame | history
web_src/src/components/UserManager.vue 238 ●●●●● patch | view | raw | blame | history
web_src/src/components/channelList.vue 479 ●●●●● patch | view | raw | blame | history
web_src/src/components/common/DeviceTree.vue 189 ●●●●● patch | view | raw | blame | history
web_src/src/components/common/DeviceTreeForZtree.vue 112 ●●●●● patch | view | raw | blame | history
web_src/src/components/common/MapComponent.vue 263 ●●●●● patch | view | raw | blame | history
web_src/src/components/common/h265web.vue 327 ●●●●● patch | view | raw | blame | history
web_src/src/components/common/jessibuca.vue 327 ●●●●● patch | view | raw | blame | history
web_src/src/components/control.vue 714 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/MediaServerEdit.vue 377 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/StreamProxyEdit.vue 291 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/SyncChannelProgress.vue 120 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/addUser.vue 154 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/catalogEdit.vue 149 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/changePassword.vue 131 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/changePasswordForAdmin.vue 121 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/changePushKey.vue 101 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/channelMapInfobox.vue 65 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/chooseChannel.vue 128 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/chooseChannelForCatalog.vue 316 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/chooseChannelForGb.vue 323 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/chooseChannelForStream.vue 255 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/deviceEdit.vue 154 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/devicePlayer.vue 1014 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/easyPlayer.vue 67 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/getCatalog.vue 167 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/importChannel.vue 125 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/importChannelShowErrorData.vue 64 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/onvifEdit.vue 121 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/platformEdit.vue 531 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/pushStreamEdit.vue 180 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/queryTrace.vue 104 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/recordDownload.vue 202 ●●●●● patch | view | raw | blame | history
web_src/src/components/dialog/rtcPlayer.vue 114 ●●●●● patch | view | raw | blame | history
web_src/src/components/live.vue 305 ●●●●● patch | view | raw | blame | history
web_src/src/components/map.vue 393 ●●●●● patch | view | raw | blame | history
web_src/src/components/service/DeviceService.js 177 ●●●●● patch | view | raw | blame | history
web_src/src/components/service/MediaServer.js 99 ●●●●● patch | view | raw | blame | history
web_src/src/components/setting/Media.vue 111 ●●●●● patch | view | raw | blame | history
web_src/src/components/setting/Sip.vue 70 ●●●●● patch | view | raw | blame | history
web_src/src/components/setting/Web.vue 74 ●●●●● patch | view | raw | blame | history
web_src/src/layout/UiHeader.vue 166 ●●●●● patch | view | raw | blame | history
web_src/src/layout/index.vue 66 ●●●●● patch | view | raw | blame | history
web_src/src/main.js 63 ●●●●● patch | view | raw | blame | history
web_src/src/router/index.js 135 ●●●●● patch | view | raw | blame | history
web_src/static/.gitkeep patch | view | raw | blame | history
web_src/static/EasyPlayer.swf patch | view | raw | blame | history
web_src/static/css/iconfont.css 1987 ●●●●● patch | view | raw | blame | history
web_src/static/css/iconfont.woff2 patch | view | raw | blame | history
web_src/static/css/login.css 420 ●●●●● patch | view | raw | blame | history
web_src/static/favicon.ico patch | view | raw | blame | history
web_src/static/fonts/poppins/Poppins-Bold.ttf patch | view | raw | blame | history
web_src/static/fonts/poppins/Poppins-Medium.ttf patch | view | raw | blame | history
web_src/static/fonts/poppins/Poppins-Regular.ttf patch | view | raw | blame | history
web_src/static/fonts/poppins/Poppins-SemiBold.ttf patch | view | raw | blame | history
web_src/static/images/arrow.png patch | view | raw | blame | history
web_src/static/images/gis/camera-offline.png patch | view | raw | blame | history
web_src/static/images/gis/camera.png patch | view | raw | blame | history
web_src/static/images/gis/camera1-offline.png patch | view | raw | blame | history
web_src/static/images/gis/camera1.png patch | view | raw | blame | history
web_src/static/images/gis/camera2-offline.png patch | view | raw | blame | history
web_src/static/images/gis/camera2.png patch | view | raw | blame | history
web_src/static/images/gis/camera3-offline.png patch | view | raw | blame | history
web_src/static/images/gis/camera3.png patch | view | raw | blame | history
web_src/static/images/zlm-logo.png patch | view | raw | blame | history
web_src/static/js/EasyWasmPlayer.js 19 ●●●●● patch | view | raw | blame | history
web_src/static/js/ZLMRTCClient.js 7718 ●●●●● patch | view | raw | blame | history
web_src/static/js/ZLMRTCClient.js.map 1 ●●●● patch | view | raw | blame | history
web_src/static/js/jessibuca/decoder.js 1 ●●●● patch | view | raw | blame | history
web_src/static/js/jessibuca/decoder.wasm patch | view | raw | blame | history
web_src/static/js/jessibuca/jessibuca.d.ts 637 ●●●●● patch | view | raw | blame | history
web_src/static/js/jessibuca/jessibuca.js 1 ●●●● patch | view | raw | blame | history
web_src/static/js/mapConfig.js 19 ●●●●● patch | view | raw | blame | history
web_src/static/libDecoder.wasm patch | view | raw | blame | history
web_src/static/logo.png patch | view | raw | blame | history
.gitignore
New file
@@ -0,0 +1,30 @@
# Compiled class file
*.class
# Log file
*.log
logs/*
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
src/main/resources/application-*.yml
# Package Files #
#*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
*.iml
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
/.idea/*
/target/*
/.idea/
/target/
/src/main/resources/static/
certificates
.gitmodules
New file
@@ -0,0 +1,3 @@
[submodule "be.teletask.onvif-java"]
    path = be.teletask.onvif-java
    url = https://gitee.com/pan648540858/be.teletask.onvif-java.git
DOCKERFILE
New file
@@ -0,0 +1,130 @@
#很久没维护了,已经与定前版本不匹配
FROM ubuntu:20.04 AS build
ARG DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Shanghai
EXPOSE 18080/tcp
EXPOSE 5060/tcp
EXPOSE 5060/udp
EXPOSE 6379/tcp
EXPOSE 18081/tcp
EXPOSE 80/tcp
EXPOSE 1935/tcp
EXPOSE 554/tcp
EXPOSE 554/udp
EXPOSE 30000-30500/tcp
EXPOSE 30000-30500/udp
ENV LC_ALL zh_CN.UTF-8
# 使用了自己的settings.xml作为maven的源,加快打包速度
RUN apt-get update && \
        DEBIAN_FRONTEND="noninteractive" && \
         apt-get install -y --no-install-recommends openjdk-11-jre git maven nodejs npm build-essential tcl language-pack-zh-hans \
         cmake curl  vim ca-certificates  tzdata libmysqlclient-dev  redis-server libssl-dev libx264-dev libfaac-dev ffmpeg
WORKDIR /home
RUN      git clone https://gitee.com/pan648540858/maven.git && \
         cp maven/settings.xml /usr/share/maven/conf/ && \
         git clone https://gitee.com/pan648540858/wvp-GB28181.git && \
         git clone https://gitee.com/pan648540858/wvp-pro-assist.git
         # 编译前端界面
WORKDIR /home/wvp-GB28181/web_src
RUN      npm install && \
         npm run build && \
         mkdir -p /opt/wvp/config && \
         mkdir -p /opt/assist/config && \
         cp /home/wvp-GB28181/src/main/resources/application-dev.yml /opt/wvp/config/application.yml && \
         cp /home/wvp-pro-assist/src/main/resources/application-dev.yml /opt/assist/config/application.yml
         # wvp打包
WORKDIR /home/wvp-GB28181
RUN      mvn compile && \
         mvn package && \
         cp /home/wvp-GB28181/target/wvp*.jar /opt/wvp/
         # wvp 录像管理打包
WORKDIR /home/wvp-pro-assist
RUN      mvn compile && \
         mvn package && \
         cp /home/wvp-pro-assist/target/*.jar /opt/assist/
         # zlm打包
WORKDIR /home
RUN     mkdir -p /opt/media && \
        git clone --depth=1 https://gitee.com/xia-chu/ZLMediaKit && \
        cd ZLMediaKit && git submodule update --init --recursive && \
        mkdir -p build release/linux/Release/ &&\
        cd build && \
        cmake -DCMAKE_BUILD_TYPE=Release .. && \
        make -j4 && \
        rm -rf ../release/linux/Release/config.ini && \
        cp -r ../release/linux/Release/* /opt/media && \
        mkdir -p /opt/media/www/record
         # 清理
 RUN     rm -rf /home/wiki && \
         rm -rf /home/wvp-GB28181 && \
         apt-get autoremove -y git maven nodejs npm && \
         apt-get clean -y && \
         rm -rf /var/lib/apt/lists/*dic
WORKDIR /opt/wvp
RUN     echo '#!/bin/bash' > run.sh && \
        echo 'echo ${WVP_IP}' >> run.sh && \
        echo 'echo ${WVP_CONFIG}' >> run.sh && \
        echo 'redis-server --daemonize yes --bind 0.0.0.0' >> run.sh && \
        echo 'cd /opt/assist' >> run.sh && \
        echo 'nohup java -jar *.jar --userSettings.record=/opt/media/www/record/ &' >> run.sh && \
        echo 'nohup /opt/media/MediaServer -d -m 3 &' >> run.sh && \
        echo 'cd /opt/wvp' >> run.sh && \
        echo 'if [-n "${WVP_CONFIG}"]; then' >> run.sh && \
        echo '        java -jar *.jar --spring.config.location=/opt/wvp/config/application.yml --media.record-assist-port=18081 ${WVP_CONFIG}' >> run.sh && \
        echo 'else' >> run.sh && \
        echo '        java -jar *.jar --spring.config.location=/opt/wvp/config/application.yml --media.record-assist-port=18081 --media.ip=127.0.0.1 --media.sdp-ip=${WVP_IP} --sip.ip=${WVP_IP} --media.stream-ip=${WVP_IP}' >> run.sh  && \
        echo 'fi' >> run.sh
RUN chmod +x run.sh
FROM ubuntu:20.04
ARG DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Shanghai
EXPOSE 18080/tcp
EXPOSE 5060/tcp
EXPOSE 5060/udp
EXPOSE 6379/tcp
EXPOSE 18081/tcp
EXPOSE 80/tcp
EXPOSE 1935/tcp
EXPOSE 554/tcp
EXPOSE 554/udp
EXPOSE 30000-30500/tcp
EXPOSE 30000-30500/udp
ENV LC_ALL zh_CN.UTF-8
RUN apt-get update && \
        DEBIAN_FRONTEND="noninteractive" && \
        apt-get install -y --no-install-recommends openjdk-11-jre tcl language-pack-zh-hans \
        ca-certificates  tzdata libmysqlclient21  redis-server libssl1.1 libx264-155 libfaac0 ffmpeg && \
        apt-get autoremove -y && \
        apt-get clean -y && \
        rm -rf /var/lib/apt/lists/*dic
WORKDIR /opt/wvp
COPY --from=build /opt /opt
CMD ["sh", "run.sh"]
LICENSE
New file
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 swwhaha
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
README.md
New file
@@ -0,0 +1,171 @@
![logo](https://raw.githubusercontent.com/648540858/wvp-GB28181-pro/wvp-28181-2.0/web_src/static/logo.png)
# 开箱即用的28181协议视频平台
[![Build Status](https://travis-ci.org/xia-chu/ZLMediaKit.svg?branch=master)](https://travis-ci.org/xia-chu/ZLMediaKit)
[![license](http://img.shields.io/badge/license-MIT-green.svg)](https://github.com/xia-chu/ZLMediaKit/blob/master/LICENSE)
[![JAVA](https://img.shields.io/badge/language-java-red.svg)](https://en.cppreference.com/)
[![platform](https://img.shields.io/badge/platform-linux%20|%20macos%20|%20windows-blue.svg)](https://github.com/xia-chu/ZLMediaKit)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-yellow.svg)](https://github.com/xia-chu/ZLMediaKit/pulls)
WEB VIDEO PLATFORM是一个基于GB28181-2016标准实现的开箱即用的网络视频平台,负责实现核心信令与设备管理后台部分,支持NAT穿透,支持海康、大华、宇视等品牌的IPC、NVR接入。支持国标级联,支持将不带国标功能的摄像机/直播流/直播推流转发到其他国标平台。
流媒体服务基于@夏楚 ZLMediaKit [https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit)
播放器使用@dexter jessibuca [https://github.com/langhuihui/jessibuca/tree/v3](https://github.com/langhuihui/jessibuca/tree/v3)
前端页面基于@Kyle MediaServerUI [https://gitee.com/kkkkk5G/MediaServerUI](https://gitee.com/kkkkk5G/MediaServerUI) 进行修改.
# 应用场景:
支持浏览器无插件播放摄像头视频。
支持摄像机、平台、NVR等设备接入。
支持国标级联。
支持rtsp/rtmp等视频流转发到国标平台。
支持rtsp/rtmp等推流转发到国标平台。
# 项目目标
旨在打造一个易配置,易使用,便于维护的28181国标信令系统, 依托优秀的开源流媒体服务框架ZLMediaKit, 实现一个完整易用GB28181平台.
# 部署文档
[doc.wvp-pro.cn](https://doc.wvp-pro.cn)
# gitee同步仓库
https://gitee.com/pan648540858/wvp-GB28181-pro.git
# 截图
![build_1.png](https://images.gitee.com/uploads/images/2022/0304/101513_79632720_1018729.png "2022-03-04_09-51.png")
![build_1.png](https://images.gitee.com/uploads/images/2022/0304/103025_5df016f9_1018729.png "2022-03-04_10-27.png")
![build_1.png](https://images.gitee.com/uploads/images/2022/0304/101706_088fbafa_1018729.png "2022-03-04_09-52_1.png")
![build_1.png](https://images.gitee.com/uploads/images/2022/0304/101756_3d662828_1018729.png "2022-03-04_10-00_1.png")
![build_1.png](https://images.gitee.com/uploads/images/2022/0304/101823_19050c66_1018729.png "2022-03-04_10-12_1.png")
![build_1.png](https://images.gitee.com/uploads/images/2022/0304/101848_e5a39557_1018729.png "2022-03-04_10-12_2.png")
![build_1.png](https://images.gitee.com/uploads/images/2022/0304/101919_ee5b8c79_1018729.png "2022-03-04_10-13.png")
# 1.0 基础特性
1. 视频预览;
2. 云台控制(方向、缩放控制);
3. 视频设备信息同步;
4. 离在线监控;
5. 录像查询与回放(基于NVR\DVR,暂不支持快进、seek操作);
6. 无人观看自动断流;
7. 支持UDP和TCP两种国标信令传输模式;
8. 集成web界面, 不需要单独部署前端服务, 直接利用wvp内置文件服务部署, 随wvp一起部署;
9. 支持平台接入, 针对大平台大量设备的情况进行优化;
10. 支持检索,通道筛选;
11. 支持自动配置ZLM媒体服务, 减少因配置问题所出现的问题;
12. 支持启用udp多端口模式, 提高udp模式下媒体传输性能;
13. 支持通道是否含有音频的设置;
14. 支持通道子目录查询;
15. 支持udp/tcp国标流传输模式;
16. 支持直接输出RTSP、RTMP、HTTP-FLV、Websocket-FLV、HLS多种协议流地址
17. 支持国标网络校时
18. 支持公网部署, 支持wvp与zlm分开部署
19. 支持播放h265, g.711格式的流(需要将closeWaitRTPInfo设为false)
20. 报警信息处理,支持向前端推送报警信息
# 1.0 新支持特性
1. 集成web界面, 不需要单独部署前端服务, 直接利用wvp内置文件服务部署, 随wvp一起部署;
2. 支持平台接入, 针对大平台大量设备的情况进行优化;
3. 支持检索,通道筛选;
4. 支持自动配置ZLM媒体服务, 减少因配置问题所出现的问题;
5. 支持启用udp多端口模式, 提高udp模式下媒体传输性能;
6. 支持通道是否含有音频的设置;
7. 支持通道子目录查询;
8. 支持udp/tcp国标流传输模式;
9. 支持直接输出RTSP、RTMP、HTTP-FLV、Websocket-FLV、HLS多种协议流地址
10. 支持国标网络校时
11. 支持公网部署, 支持wvp与zlm分开部署
12. 支持播放h265, g.711格式的流
13. 支持固定流地址和自动点播,同时支持未点播时直接播放流地址,代码自动发起点播.  ( [查看WIKI](https://github.com/648540858/wvp-GB28181-pro/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E5%9B%BA%E5%AE%9A%E6%92%AD%E6%94%BE%E5%9C%B0%E5%9D%80%E4%B8%8E%E8%87%AA%E5%8A%A8%E7%82%B9%E6%92%AD))
14. 报警信息处理,支持向前端推送报警信息
15. 支持订阅与通知方法
   -  [X] 移动位置订阅
   -  [X] 移动位置通知处理
   -  [X] 报警事件订阅
   -  [X] 报警事件通知处理
   -  [X] 设备目录订阅
   -  [X] 设备目录通知处理
16. 移动位置查询和显示,可通过配置文件设置移动位置历史是否存储
# 2.0 支持特性
- [X] 国标通道向上级联
    - [X] WEB添加上级平台
    - [X] 注册
    - [X] 心跳保活
    - [X] 通道选择
    - [X] 通道推送
    - [X] 点播
    - [X] 云台控制
    - [X] 平台状态查询
    - [X] 平台信息查询
    - [X] 平台远程启动
    - [X] 每个级联平台可自定义的虚拟目录
    - [X] 目录订阅与通知
    - [X] 录像查看与播放
    - [X] GPS订阅与通知(直播推流)
- [X] 支持手动添加设备和给设备设置单独的密码
- [X] 添加RTSP视频
- [X] 添加接口鉴权
- [X] 添加RTMP视频
- [X] 云端录像(需要部署单独服务配合使用)
- [X] 多流媒体节点,自动选择负载最低的节点使用。
- [X] WEB端支持播放H264与H265,音频支持G.711A/G.711U/AAC,覆盖国标常用编码格式。
- [X] 支持电子地图。
- [X] 支持接入WGS84和GCJ02两种坐标系。
[//]: # (# docker快速体验)
[//]: # (目前作者的docker-compose因为时间有限维护不及时,这里提供第三方提供的供大家使用,维护不易,大家记得给这位小伙伴点个star。  )
[//]: # (https://github.com/SaltFish001/wvp_pro_compose)
[//]: # ([https://github.com/SaltFish001/wvp_pro_compose](https://github.com/SaltFish001/wvp_pro_compose))
[//]: # (这是作者维护的一个镜像,可能存在不及时的问题。)
[//]: # (```shell)
[//]: # (docker pull 648540858/wvp_pro)
[//]: # ()
[//]: # (docker run  --env WVP_IP="你的IP" -it -p 18080:18080 -p 30000-30500:30000-30500/udp -p 30000-30500:30000-30500/tcp -p 80:80 -p 5060:5060 -p 5060:5060/udp 648540858/wvp_pro)
[//]: # (```)
[//]: # (docker使用详情查看:[https://hub.docker.com/r/648540858/wvp_pro](https://hub.docker.com/r/648540858/wvp_pro))
# gitee同步仓库
https://gitee.com/pan648540858/wvp-GB28181-pro.git
# 遇到问题
国标最麻烦的地方在于设备的兼容性,所以需要大量的设备来测试,目前作者手里的设备有限,再加上作者水平有限,所以遇到问题在所难免;
1. 查看wiki,仔细的阅读可以帮你避免几乎所有的问题
2. 搜索issues,这里有大部分的答案
3. 加QQ群,这里有大量热心的小伙伴,但是前提新希望你已经仔细阅读了wiki和搜索了issues。
4. 你可以请作者为你解答,但是我不是免费的。
5. 你可以把遇到问题的设备寄给我,可以更容易的复现问题。
# 合作
目前很多打着合作的幌子来私聊的,其实大家大可不必,目前作者没有精力,你有问题可以付费找我解答,也可以提PR
,如果对代码有建议可以提ISSUE;也可以加群一起聊聊。我们欢迎所有有兴趣参与到项目中来的人。
# 使用帮助
QQ群: 901799015, ZLM使用文档[https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit)
QQ私信一般不回, 精力有限.欢迎大家在群里讨论.觉得项目对你有帮助,欢迎star和提交pr。
# 授权协议
本项目自有代码使用宽松的MIT协议,在保留版权信息的情况下可以自由应用于各自商用、非商业的项目。 但是本项目也零碎的使用了一些其他的开源代码,在商用的情况下请自行替代或剔除; 由于使用本项目而产生的商业纠纷或侵权行为一概与本项目及开发者无关,请自行承担法律风险。 在使用本项目代码时,也应该在授权协议中同时表明本项目依赖的第三方库的协议
# 致谢
感谢作者[夏楚](https://github.com/xia-chu) 提供这么棒的开源流媒体服务框架,并在开发过程中给予支持与帮助。
感谢作者[dexter langhuihui](https://github.com/langhuihui) 开源这么好用的WEB播放器。
感谢作者[Kyle](https://gitee.com/kkkkk5G) 开源了好用的前端页面
感谢各位大佬的赞助以及对项目的指正与帮助。包括但不限于代码贡献、问题反馈、资金捐赠等各种方式的支持!以下排名不分先后:
[lawrencehj](https://github.com/lawrencehj) [Smallwhitepig](https://github.com/Smallwhitepig) [swwhaha](https://github.com/swwheihei)
[hotcoffie](https://github.com/hotcoffie) [xiaomu](https://github.com/nikmu) [TristingChen](https://github.com/TristingChen)
[chenparty](https://github.com/chenparty) [Hotleave](https://github.com/hotleave) [ydwxb](https://github.com/ydwxb)
[ydpd](https://github.com/ydpd) [szy833](https://github.com/szy833) [ydwxb](https://github.com/ydwxb) [Albertzhu666](https://github.com/Albertzhu666)
[mk1990](https://github.com/mk1990) [SaltFish001](https://github.com/SaltFish001)
ps: 刚增加了这个名单,肯定遗漏了一些大佬,欢迎大佬联系我添加。
bootstrap.sh
New file
@@ -0,0 +1,91 @@
#!/bin/bash
######################################################
# Copyright 2019 Pham Ngoc Hoai
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Repo: https://github.com/tyrion9/spring-boot-startup-script
#
######### PARAM ######################################
JAVA_OPT=-Xmx1024m
JARFILE=`ls -1r *.jar 2>/dev/null | head -n 1`
PID_FILE=pid.file
RUNNING=N
PWD=`pwd`
######### DO NOT MODIFY ########
if [ -f $PID_FILE ]; then
        PID=`cat $PID_FILE`
        if [ ! -z "$PID" ] && kill -0 $PID 2>/dev/null; then
                RUNNING=Y
        fi
fi
start()
{
        if [ $RUNNING == "Y" ]; then
                echo "Application already started"
        else
                if [ -z "$JARFILE" ]
                then
                        echo "ERROR: jar file not found"
                else
                        nohup java  $JAVA_OPT -Djava.security.egd=file:/dev/./urandom -jar $PWD/$JARFILE > nohup.out 2>&1  &
                        echo $! > $PID_FILE
                        echo "Application $JARFILE starting..."
                        tail -f nohup.out
                fi
        fi
}
stop()
{
        if [ $RUNNING == "Y" ]; then
                kill -9 $PID
                rm -f $PID_FILE
                echo "Application stopped"
        else
                echo "Application not running"
        fi
}
restart()
{
        stop
        start
}
case "$1" in
        'start')
                start
                ;;
        'stop')
                stop
                ;;
        'restart')
                restart
                ;;
        *)
                echo "Usage: $0 {  start | stop | restart  }"
                exit 1
                ;;
esac
exit 0
doc/README.md
New file
@@ -0,0 +1,102 @@
# 介绍
> 开箱即用的28181协议视频平台
# 概述
- WVP-PRO基于GB/T 28181-2016标准实现的流媒体平台,依托优秀的开源流媒体服务[ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit),提供完善丰富的功能。
- GB/T 28181-2016 中文标准名称是《公共安全视频监控联网系统信息传输、交换、控制技术要求》是监控领域的国家标准。大量应用于政府视频平台。
- 通过28181协议你可以将IPC摄像头接入平台,可以观看也可以使用28181/rtsp/rtmp/flv等协议将视频流分发到其他平台。
# 特性
- 实现标准的28181信令,兼容常见的品牌设备,比如海康、大华、宇视等品牌的IPC、NVR以及平台。
- 支持将国标设备级联到其他国标平台,也支持将不支持国标的设备的图像或者直播推送到其他国标平台
- 前端完善,自带完整前端页面,无需二次开发可直接部署使用。
- 完全开源,且使用MIT许可协议。保留版权的情况下可以用于商业项目。
- 支持多流媒体节点负载均衡。
# 我们实现了哪些国标功能
**作为上级平台**
- [X] 注册
- [X] 注销
- [X] 实时视音频点播
- [X] 设备控制
  - [X] 云台控制
  - [ ] 远程启动
  - [ ] 录像控制
  - [ ] 报警布防/撤防
  - [ ] 报警复位
  - [X] 强制关键帧
  - [ ] 拉框放大
  - [ ] 拉框缩小
  - [ ] 看守位控制
  - [ ] 设备配置
- [X] 报警事件通知和分发
- [X] 设备目录订阅
- [X] 网络设备信息查询
  - [X] 设备目录查询
  - [X] 设备状态查询
  - [ ] 设备配置查询
  - [ ] 设备预置位查询
- [X] 状态信息报送
- [X] 设备视音频文件检索
- [X] 历史视音频的回放
  - [X] 播放
  - [X] 暂停
  - [X] 进/退
  - [X] 停止
- [X] 视音频文件下载
- [X] 校时
- [X] 订阅和通知
  - [X] 事件订阅
    - [X] 移动设备位置订阅
    - [X] 报警订阅
    - [X] 目录订阅
- [ ] 语音广播
- [ ] 语音对讲
**作为下级平台**
- [X] 注册
- [X] 注销
- [X] 实时视音频点播
- [ ] 设备控制
  - [ ] 云台控制
  - [ ] 远程启动
  - [ ] 录像控制
  - [ ] 报警布防/撤防
  - [ ] 报警复位
  - [ ] 强制关键帧
  - [ ] 拉框放大
  - [ ] 拉框缩小
  - [ ] 看守位控制
  - [ ] 设备配置
- [ ] 报警事件通知和分发
- [X] 设备目录订阅
- [X] 网络设备信息查询
  - [X] 设备目录查询
  - [X] 设备状态查询
  - [ ] 设备配置查询
  - [ ] 设备预置位查询
- [X] 状态信息报送
- [X] 设备视音频文件检索
- [X] 历史视音频的回放
  - [X] 播放
  - [x] 暂停
  - [x] 进/退
  - [x] 停止
- [ ] 视音频文件下载
- [ ] ~~校时~~
- [X] 订阅和通知
  - [X] 事件订阅
    - [X] 移动设备位置订阅
    - [ ] 报警订阅
    - [X] 目录订阅
- [ ] 语音广播
- [ ] 语音对讲
# 社区
代码目前托管在GitHub和Gitee,Gitee目前作为加速仓库使用,不接受issue。
GitHub: [https://github.com/648540858/wvp-GB28181-pro](https://github.com/648540858/wvp-GB28181-pro)
Gitee: [https://gitee.com/pan648540858/wvp-GB28181-pro](https://gitee.com/pan648540858/wvp-GB28181-pro)
doc/_content/ability/_media/cascade1.png
doc/_content/ability/_media/cascade2.png
doc/_content/ability/_media/cascade3.png
doc/_content/ability/_media/cascade4.png
doc/_content/ability/_media/img.png
doc/_content/ability/_media/img_1.png
doc/_content/ability/_media/img_10.png
doc/_content/ability/_media/img_11.png
doc/_content/ability/_media/img_12.png
doc/_content/ability/_media/img_13.png
doc/_content/ability/_media/img_14.png
doc/_content/ability/_media/img_15.png
doc/_content/ability/_media/img_16.png
doc/_content/ability/_media/img_17.png
doc/_content/ability/_media/img_18.png
doc/_content/ability/_media/img_2.png
doc/_content/ability/_media/img_3.png
doc/_content/ability/_media/img_4.png
doc/_content/ability/_media/img_5.png
doc/_content/ability/_media/img_6.png
doc/_content/ability/_media/img_7.png
doc/_content/ability/_media/img_8.png
doc/_content/ability/_media/img_9.png
doc/_content/ability/auto_play.md
New file
@@ -0,0 +1,2 @@
<!-- 自动点播 -->
# 自动点播
doc/_content/ability/cascade.md
New file
@@ -0,0 +1,34 @@
<!-- 国标级联的使用 -->
# 国标级联的使用
国标28181不同平台之间支持两种连接方式,平级和上下级,WVP目前支持向上级级联。
## 1 接入平台
### 1.1 wvp-pro
#### 1.1.1 wvp-pro管理页面点击添加
   ![cascade1](_media/cascade1.png)
#### 1.1.2 填入wvp-pro上级平台信息
   ![cascade1](_media/img_4.png)
   ![cascade1](_media/img_5.png)
#### 1.1.3 编辑wvp-pro上级设备信息,开启订阅
   ![cascade1](_media/img_6.png)
### 1.2 大华平台
### 1.3 海康平台
### 1.4 liveGBS
#### 1.4.1. wvp-pro管理页面点击添加
   ![添加](_media/cascade1.png)
#### 1.4.2. 填入liveGBS平台信息
   ![填入liveGBS平台信息1](_media/cascade2.png)
   ![填入liveGBS平台信息2](_media/cascade3.png)
#### 1.4.3. 编辑liveGBS设备信息,开启目录订阅
   ![cascade1](_media/cascade4.png)
#### 1.4.4. 编辑liveGBS设备信息,开启GPS订阅
   ![cascade1](_media/img_7.png)
## 2 添加目录与通道
1. 级联平台添加目录信息
   ![cascade1](_media/img_1.png)
2. 为目录添加通道
   ![cascade1](_media/img_2.png)
3. 设置默认流目录
如果需要后续自动生成的流信息都在某一个节点下,可以在对应节点右键设置为默认
   ![cascade1](_media/img_3.png)
doc/_content/ability/cascade2.md
New file
@@ -0,0 +1,18 @@
<!-- 国标级联的使用 -->
# 国标级联的使用
国标28181不同平台之间支持两种连接方式,平级和上下级,WVP目前支持向上级级联。
## 添加上级平台
在国标级联页面点击“添加”按钮,以推送到上级WVP为例子,参看[接入设备](./_content/ability/device.md)
![cascade17](_media/img_17.png)
点击保存可以在上级的国标通道列表看到新增加的设备;
国标级联列表出现了级联的这个平台;同时状态显示为在线,如果状态为离线那么可能是你的服务信息配置有误或者网络不通。
订阅信息列有三个图标,表示上级开启订阅,从左到右依次是:报警订阅,目录订阅,移动位置订阅。
## 推送通道
点击你要推送的平台的“选择通道”按钮。
![cascade18](_media/img_18.png)
- **页面结构**
  - 左侧为目录结构
  选择未分配,则右侧显示待分配的通道,可以点击“添加按钮”,在弹窗中选择要放置的位置,保存后即可添加通道成功
  选择其他的目录可以看到已经分配在这个目录下的通道,可以对其进行删除后重新在未分配中去分配。
  - 右侧为数据展示以及操作
  国标通道栏内为来自其他国标设备/平台的通道;直播流通道为来自推流/拉流代理的通道。
doc/_content/ability/cloud_record.md
New file
@@ -0,0 +1,8 @@
<!-- 云端录像 -->
# 云端录像
云端录像是对录制在zlm服务下的录像文件的管理,录像的文件路径默认在ZLM/www/record下,使用云端录像功能必须部署wvp-pro-assist,主要通过调用wvp-pro-assist的接口完成各种功能。
如果你需要24小时的录像,目前有一个这种方案,可以参考[7*24不间断录像](./_content/ability/continuous_recording.md)。
1. 云段录像支持录像文件的查看,播放(可能因为编码的原因导致无法播放);
2. 支持录像的下载;
3. 支持录像的合并下载;
功能没有太多特殊的地方就不一一介绍了,大家自行体验吧。
doc/_content/ability/continuous_recording.md
New file
@@ -0,0 +1,14 @@
<!-- 7*24不间断录像 -->
# 7*24不间断录像
目前如果要实现不间断录像如果只是关闭无人观看停止推流是不够的,设备可能经历断网,重启,都会导致录像的中断,目前给大家提供一种可用的临时方案。
**原理:** wvp支持使用流地址自动点播,即你拿到一个流地址直接去播放,即使设备处于未点播状态,wvp会自动帮你点播;ZLM
的拉流代理成功后会无限重试,只要流一恢复就可以拉起来,基于这两个原理。
**方案如下:**
1. wvp的配置中user-settings->auto-apply-play设置为团true,开启自动点播;
2. 点击你要录像的通道,点击播放页面左下角的“更多地址”,点击rtsp,此时复制了rtsp地址到剪贴板;
3. 在拉流代理中添加一路流,地址填写你复制的地址,启用成功即可。
**前提:**
1. wvp使用多端口收流,不然你无法得到一个固定的流地址,也就无法实现自动点播。
doc/_content/ability/device.md
New file
@@ -0,0 +1,36 @@
<!-- 接入设备 -->
# 接入设备
设备接入主要是需要在设备上配置28181上级也就是WVP-PRO的信息,只有信息一致的情况才可以注册成功。设备注册成功后打开WVP->国标设备,可以看到新增加的设备;[设备使用](./_content/ability/device_use.md),
主要有以下字段需要配置:
- sip->ip
本机IP,不要使用127.0.0.1/0.0.0.0, 除非你对项目及其熟悉
- sip->port
28181服务监听的端口
- sip->domain
domain宜采用ID统一编码的前十位编码。
- sip->id
28181服务ID
- sip->password
28181服务密码
- 配置信息在如下位置
![_media/img_16.png](_media/img_16.png)
***
## 大华摄像头
![_media/img_10.png](_media/img_10.png)
## 大华NVR
![_media/img_11.png](_media/img_11.png)
## 艾科威视摄像头
![_media/img_15.png](_media/img_15.png)
## 水星摄像头
![_media/img_12.png](_media/img_12.png)
## 海康摄像头
![_media/img_9.png](_media/img_9.png)
[设备使用](_content/ability/device_use.md)
doc/_content/ability/device_use.md
New file
@@ -0,0 +1,35 @@
<!-- 设备使用 -->
# 设备使用
### 更新设备通道
  点击列表末尾的“刷新”按钮,可以看到一个圆形进度条,等进度结束提示成功后即可更新完成,如果通道数量有变化你可以看点击左上角的![刷新](_media/img_14.png)即可看到通道数量的变化;如果通道数量仍未0,那么可能时对方尚未推送通道给你。
### 查看设备通道
  点击列表末尾的“通道”按钮,
### 查看设备定位
  点击列表末尾的“定位”按钮,即可跳转到地图页面看到设备的位置
### 编辑设备在WVP中一些功能
点击列表末尾的“编辑”按钮,即可在打开的弹窗中对设备功能进行修改
- 设备名称
  如何未能从设备里读取到设备名称或者需要自己重命名,那么可以修改此选项。
- 字符集
  修改读取设备数据时使用的字符集,默认为GB2312,但是GB2312收录的汉字不全,所以有时候回遇到乱码,可以修改为UTF-8来解决。
- 地理坐标系
  展示此设备定位信息时使用的设用什么坐标系来解析经纬度,一般不用修改,如果遇到定位不准,可以修改尝试修改此选项解决。
- 目录结构
  展示设备的通道信息时,使用设备作为树形结构的依据,国标28181定义了两种树形结构,详情查看[国标28181的树形结构](./_content/theory/channel_tree.md);
- 目录订阅
  填写订阅周期即可对设备开启目录订阅,设备如果支持目录订阅那么设备在通道信息发生变化时就会通知WVP哪些通道发生了那些变化,包括通道增加/删除/更新/上线/下线/视频丢失/故障。0为取消订阅。
  一般NVR和平台对接可以开启此选项,直接接摄像机开启此选项意义不大。
- 移动位置订阅
  对设备开启移动位置订阅,设备如果支持目录订阅那么设备位置发生变化时会通知到WVP,一般执法记录仪可以开启此选项,对固定位置的设备意义不大。
- SSRC校验
  为了解决部分设备出现的串流问题,可以打开此选项。ZLM会严格按照给定的ssrc处理视频流。部分设备流信息不标准,开启可能导致无法点播。
### 删除设备
  可以删除WVP中的设备信息,如果设备28181配置未更改,那么设备在下一次注册后仍然会注册上来。
### 点播视频
  进入通道列表后,点击列表末尾的“播放”按钮,稍等即可弹出播放页面
### 设备录像
  进入通道列表后,点击列表末尾的“设备录像”按钮,也可以在播放页面点击录像查询进入录像查看页面,选择要查看的日期即可对录像进行播放和下载。
### 云台控制
  可以对支持云台功能的设备进行上下左右的转动以及拉近拉远的操作。
### 获取视频的播放器地址
  视频点播成功后在实时视频页面,点击“更多地址”可以看到所有的播放地址,地址是否可以播放与你是否完整编译启用zlm功能有关,更与网络有关。
doc/_content/ability/gis.md
New file
@@ -0,0 +1,37 @@
<!-- 电子地图 -->
# 电子地图
WVP提供了简单的电子地图用于设备的定位以及移动设备的轨迹信息,电子地图基于开源的地图引擎openlayers开发。
### 查看设备定位
1. 可以在设备列表点击“定位”按钮,自动跳转到电子地图页面;
2. 在电子地图页面在设备上右键点击“定位”获取设备/平台下的所有通道位置。
3. 单击通道信息可以定位到具体的通道
### 查询设备轨迹
查询轨迹需要提前配置save-position-history选项开启轨迹信息的保存,目前WVP此处未支持分库分表,对于大数据量的轨迹信息无法胜任,有需求请自行二次开发或者定制开发。
在电子地图页面在设备上右键点击“查询轨迹”获取设备轨迹信息。
PS: 目前的底图仅用用作演示和学习,商用情况请自行购买授权使用。
### 更换底图以及底图配置
目前WVP支持使用了更换底图,配置文件在web_src/static/js/mapConfig.js,请修改后重新编译前端文件。
```javascript
window.mapParam = {
  // 开启/关闭地图功能
  enable: true,
  // 坐标系 GCJ-02 WGS-84,
  coordinateSystem: "GCJ-02",
  // 地图瓦片地址
  tilesUrl: "http://webrd0{1-4}.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8",
  // 瓦片大小
  tileSize: 256,
  // 默认层级
  zoom:10,
  // 默认地图中心点
  center:[116.41020, 39.915119],
  // 地图最大层级
  maxZoom:18,
  // 地图最小层级
  minZoom: 3
}
```
doc/_content/ability/node_manger.md
New file
@@ -0,0 +1,9 @@
<!-- 节点管理 -->
# 节点管理
WVP支持单个WVP多个ZLM的方案来扩展WVP的视频并发能力,并发点播是因为带宽和性能的原因,单个ZLM节点能支持的路数有限,所以WVP增加了ZLM集群来扩展并发并且保证ZLM的高可用。
## 默认节点
WVP中为了保证功能的完整性,ZLM节点至少要有一个默认节点,这个节点不是在管理页面添加的,而是在WVP的配置文件中配置的,这个节点不可在页面删除。每次启动会自动从配置文件中读取配置写入数据库备用。
## 新增节点
启动你要添加的zlm节点,然后点击“添加节点”按钮输入zlm的ip, http端口,SECRET。点击测试测试完成则开始对节点进行详细的设置,如果你的zlm是使用docker启动的,可能存在zlm使用的端口与宿主机端口不一致的情况,需要在这里一一配置。
## wvp使用多个节点的原理
wvp会把连接的节点统一记录在redis中,并记录zlm的负载情况,当新的请求到来时,会取出负载最低的那个zlm进行使用。以此保证节点负载均衡。
doc/_content/ability/online_doc.md
New file
@@ -0,0 +1,2 @@
<!-- 在线文档 -->
# 在线文档
doc/_content/ability/proxy.md
New file
@@ -0,0 +1,24 @@
<!-- 拉流代理 -->
# 拉流代理
不是所有的摄像机都支持国标或者推流的,但是这些设备可以得到一个视频播放地址,通常为rtsp协议,
以大华为例:
```text
rtsp://{user}:{passwd}@{ipc_ip}:{rtsp_port}/cam/realmonitor?channel=1&subtype=0
```
可以得到这样一个流地址,可以直接用vlc进行播放,此时我们可以通过拉流代理功能将这个设备推送给其他国标平台了。
流程如下:
```plantuml
@startuml
"摄像机"  <- "ZLMediaKit": 1. 流去流信息到ZLM
"ZLMediaKit"  -> "WVP-PRO": 2. 收到hook通知得到流信息
"上级国标平台"  -> "WVP-PRO": 3. 点播这路视频
"WVP-PRO" -> "ZLMediaKit": 4. 通知推流到上级国标平台
@enduml
```
## 添加代理
拉流代理支持两种方式:
1. ZLM中直接代理流,支持RTSP/RTMP,不支持转码;
2. 借助ffmpeg完成拉转,可以通过修改ffmpeg拉转参数完成转码。
点击页面的“添加代理”,安装提示操作即可,保存并启用成功后,可以在国标级联中[添加通道推送给上级平台](./_content/ability/cascade?id=_2-%e6%b7%bb%e5%8a%a0%e7%9b%ae%e5%bd%95%e4%b8%8e%e9%80%9a%e9%81%93)
PS: ffmpeg默认模板不需修改,需要修改参数自行去ZLM配置文件中添加一个即可。
doc/_content/ability/push.md
New file
@@ -0,0 +1,41 @@
<!-- 推流列表 -->
# 推流列表
## 功能说明
WVP支持三种图像输入方式,直播,[拉流代理](_content/ability/proxy.md),[国标](_content/ability/device.md),直播设备接入流程如下
```plantuml
@startuml
"直播设备"  -> "ZLMediaKit": 1. 发起推流
"ZLMediaKit"  -> "WVP-PRO": 2. 收到hook通知得到流信息
"上级国标平台"  -> "WVP-PRO": 3. 点播这路视频
"WVP-PRO" -> "ZLMediaKit": 4. 通知推流到上级国标平台
@enduml
```
1. 默认情况下WVP收到推流信息后,列表中出现这条推流信息,此时你可以点击“加入国标”按钮为此路推流配置名称以及国标编号,只有有国标编号的推流才可以添加到级联平台,保存成功后可以在国标级联中[添加通道推送给上级平台](_content/ability/cascade?id=_2-%e6%b7%bb%e5%8a%a0%e7%9b%ae%e5%bd%95%e4%b8%8e%e9%80%9a%e9%81%93)
2. WVP也支持推流前导入大量通道直接推送给上级,点击“下载模板”按钮,根据示例修改模板后,点击“通道导入”按钮导入通道数据,保存成功后可以在国标级联中[添加通道推送给上级平台](_content/ability/cascade?id=_2-%e6%b7%bb%e5%8a%a0%e7%9b%ae%e5%bd%95%e4%b8%8e%e9%80%9a%e9%81%93)
## 推拉流鉴权规则
为了保护服务器的WVP默认开启推流鉴权(目前不支持关闭此功能)
### 推流规则
推流时需要携带推流鉴权的签名sign,sign=md5(pushKey),pushKey来自用户表,每个用户会有一个不同的pushKey.
例如app=test,stream=live,pushKey=1000,ip=192.168.1.4, port=10554 那么推流地址为:
```
rtsp://192.168.1.4:10554/test/live?sign=a9b7ba70783b617e9998dc4dd82eb3c5
```
支持推流时自定义播放鉴权Id,参数名为callId,此时sign=md5(callId_pushKey)
例如app=test,stream=live,pushKey=1000,callId=12345678, ip=192.168.1.4, port=10554 那么推流地址为:
```
rtsp://192.168.1.4:10554/test/live?callId=12345678&sign=c8e6e01dde2d60c66dcea8d2498ffef1
```
### 播放规则
默认情况播放不需要鉴权,但是如果推流时携带了callId,那么播放时必须携带callId
例如app=test,stream=live,无callId, ip=192.168.1.4, port=10554 那么播放地址为:
```
rtsp://192.168.1.4:10554/test/live
```
例如app=test,stream=live,callId=12345678, ip=192.168.1.4, port=10554 那么播放地址为:
```
rtsp://192.168.1.4:10554/test/live?callId=12345678
```
doc/_content/ability/user.md
New file
@@ -0,0 +1,2 @@
<!-- 用户管理 -->
# 用户管理
doc/_content/about_doc.md
New file
@@ -0,0 +1,5 @@
<!-- 关于本文档 -->
# 关于本文档
本文档开源在gitee上,[https://gitee.com/pan648540858/wvp-pro-doc.git](https://gitee.com/pan648540858/wvp-pro-doc.git),如果文档出现任何错误或者不易理解的语句,请大家提ISSUE帮助我及时更正。欢迎大家提交PR一起维护这份文档,让更多的人可以使用到这个开源的视频平台。
doc/_content/disclaimers.md
New file
@@ -0,0 +1,2 @@
# 免责声明
WVP-PRO自有代码使用宽松的MIT协议,在保留版权信息的情况下可以自由应用于各自商用、非商业的项目。 但是本项目也零碎的使用了一些其他的开源代码,在商用的情况下请自行替代或剔除; 由于使用本项目而产生的商业纠纷或侵权行为一概与本项目及开发者无关,请自行承担法律风险。 在使用本项目代码时,也应该在授权协议中同时表明本项目依赖的第三方库的协议
doc/_content/introduction/_media/img.png
doc/_content/introduction/_media/img_1.png
doc/_content/introduction/_media/img_2.png
doc/_content/introduction/compile.md
New file
@@ -0,0 +1,95 @@
<!-- 编译 -->
# 编译
WVP-PRO不只是实现了国标28181的协议,本身也是一个完整的视频平台。所以对于新手来说,你可能需要一些耐心来完成。遇到问题不要焦躁,你可以
1. 百度
2. 如果身边有熟悉java的朋友,可以咨询下朋友;
3. 来群里(901799015)咨询群友;
4. 向作者发送邮件648540858@qq.com;
5. 作者远程支持(有偿)。
   如果这些仍不能解决你的问题,那么你可能需要与作者我一起合作完成这个项目,解决你遇到的问题。
WVP-PRO使用Spring boot开发,maven管理依赖。对于熟悉spring开发的朋友是很容易进行编译部署以及运行的。
下面将提供一种通用方法方便大家运行项目。
## 1 服务介绍
| 服务             | 作用                                       | 是否必须                    |
|----------------|------------------------------------------|-------------------------|
| WVP-PRO        | 实现国标28181的信令以及视频平台相关的功能                  | 是                       |
| ZLMediaKit     | 为WVP-PRO提供国标28181的媒体部分的实现,以及各种视频流格式的分发支持 | 是                       |
| wvp-pro-assist | wvp的辅助录像程序,也可单独跟zlm一起使用,提供录像控制,录像合并下载接口  | 否(不安装只是影响云端录像功能和国标录像下载) |
## 2 安装依赖
| 依赖     | 版本         | 用途          | 开发环境需要 | 生产环境需要 |
|--------|------------|-------------|--------|--------|
| jdk    | >=1.8      | 运行与编译java代码 | 是      | 是      |
| maven  | >=3.3      | 管理java代码依赖  | 否      | 否      |
| git    || 下载/更新/提交代码 | 否           | 否      |
| nodejs || 编译于运行前端文件  | 否           | 否      |
| npm    || 管理前端文件依赖   | 否           | 否      |
如果你是一个新手,建议你使用linux或者macOS平台。windows不推荐。
ubuntu环境,以ubuntu 18为例:
``` bash
apt-get install -y openjdk-11-jre git maven nodejs npm
```
centos环境,以centos 8为例:
```bash
yum install -y java-1.8.0-openjdk.x86_64 git maven nodejs npm
```
window环境,以windows10为例:
```bash
这里不细说了,百度或者谷歌一搜一大把,基本都是下一步下一步,然后配置环境变量。
```
## 3 安装mysql以及redis
这里依然是参考网上教程,自行安装吧。
## 4 编译ZLMediaKit
参考ZLMediaKit[WIKI](https://github.com/ZLMediaKit/ZLMediaKit/wiki),截取一下关键步骤:
```bash
# 国内用户推荐从同步镜像网站gitee下载
git clone --depth 1 https://gitee.com/xia-chu/ZLMediaKit
cd ZLMediaKit
# 千万不要忘记执行这句命令
git submodule update --init
```
## 5 编译WVP-PRO
### 5.1 可以通过git克隆,也可以在项目下载点击下载
![点击下载](_media/img_1.png)
![点击下载](_media/img_2.png)
从gitee克隆
```bash
git clone https://gitee.com/pan648540858/wvp-GB28181-pro.git
```
从github克隆
```bash
git clone https://github.com/648540858/wvp-GB28181-pro.git
```
### 5.2 编译前端页面
```shell script
cd wvp-GB28181-pro/web_src/
npm --registry=https://registry.npm.taobao.org install
npm run build
```
编译如果报错, 一般都是网络问题, 导致的依赖包下载失败
编译完成后在src/main/resources下出现static目录
**编译完成一般是这个样子,中间没有报红的错误信息**
![编译成功](_media/img.png)
### 5.3 打包项目, 生成可执行jar
```bash
cd wvp-GB28181-pro
mvn package
```
编译如果报错, 一般都是网络问题, 导致的依赖包下载失败
编译完成后在target目录下出现wvp-pro-***.jar。
接下来[配置服务](./_content/introduction/config.md)
doc/_content/introduction/config.md
New file
@@ -0,0 +1,119 @@
<!-- 配置 -->
# 配置
对于首次测试或者新手同学,我建议在局域网测试,并且关闭服务器与客户机的防火墙测试。建议部署在linux进行测试。
```plantuml
@startuml
"WVP-PRO" -> "ZLMediaKit": RESTful 接口
"WVP-PRO" <-- "ZLMediaKit": Web Hook 接口
@enduml
```
WVP-PRO通过调用ZLMediaKit的RESTful接口实现对ZLMediaKit行为的控制; ZLMediaKit通过Web Hook 接口把消息通知WVP-PRO。通过这种方式,实现了两者的互通。
对于最简单的配置,你不需要修改ZLMediaKit的任何默认配置。你只需要在WVP-PRO中配置的ZLMediaKit信息即可
## 1 WVP配置文件位置
基于spring boot的开发方式,配置文件的加载是很灵活的。默认在src/main/resources/application.yml,部分配置项是可选,你不需要全部配置在配置文件中,
完全的配置说明可以参看all-application.yml。
### 1.1 默认加载配置文件方式
使用maven打包后的jar包里,已经存在了配置文件,但是每次打开jar包修改配置文件或者修改后再打包都是比较麻烦的,所以大家可通过指定配置文件路径来加载指定位置的配置文件。
```shell
cd wvp-GB28181-pro/target
java -jar wvp-pro-*.jar --spring.config.location=../src/main/resources/application.yml
```
### 1.2 迁移配置文件以方便启动
由于配置文件的命令比较长,所以为了启动方便通常我会把配置文件放到jar包的同级目录,类似这样,
移除jar包内/BOOT-INF/classes/下所有以application开头的文件,使用解压缩工具打开jar即可,不需要解压出来。
```shell
cd wvp-GB28181-pro/target
mv ../src/main/resources/application-dev.yml application.yml
java -jar wvp-pro-*.jar
```
这也是我自己最常用的方式。
## 2 配置WVP-PRO
### 2.1 Mysql数据库配置
首先你需要创建一个名为wvp(也可使用其他名字)的数据库,并使用sql/mysql.sql导入数据库,初始化数据库结构。
在application-dev.yml中配置(使用1.2方式的是在jar包的同级目录的application.yml)配置数据库连接,包括数据库连接信息,密码。
### 2.2 Redis数据库配置
配置wvp中的redis连接信息,建议wvp自己单独使用一个db。
### 2.3 配置服务启动端口(可直接使用默认配置)
```yaml
# [可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口
server:
    port: 18080
```
### 2.4 配置28181相关信息(可直接使用默认配置)
```yaml
# 作为28181服务器的配置
sip:
    # [必须修改] 本机的IP
    ip: 192.168.1.3
    # [可选] 28181服务监听的端口
    port: 5060
    # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007)
    # 后两位为行业编码,定义参照附录D.3
    # 3701020049标识山东济南历下区 信息行业接入
    # [可选]
    domain: 3402000000
    # [可选]
    id: 34020000002000000001
    # [可选] 默认设备认证密码,后续扩展使用设备单独密码, 移除密码将不进行校验
    password: 12345678
```
### 2.5 配置ZLMediaKit连接信息
```yaml
#zlm 默认服务器配置
media:
    # ZLMediaKit的服务ID,必须配置
    id: FQ3TF8yT83wh5Wvz
    # [必须修改] zlm服务器的内网IP,sdp-ip与stream-ip使用默认值的情况下,这里不要使用127.0.0.1/0.0.0.0
    ip: 192.168.1.3
    # [必须修改] zlm服务器的http.port
    http-port: 6080
    # [可选] zlm服务器的hook.admin_params=secret
    secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
    # 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试
    rtp:
        # [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输
        enable: true
        # [可选] 在此范围内选择端口用于媒体流传输,
        port-range: 30000,30500 # 端口范围
        # [可选] 国标级联在此范围内选择端口发送媒体流,
        send-port-range: 30000,30500 # 端口范围
    # 录像辅助服务, 部署此服务可以实现zlm录像的管理与下载, 0 表示不使用
    record-assist-port: 18081
```
### 2.4 个性化定制信息配置
```yaml
# [根据业务需求配置]
user-settings:
    # [可选] 服务ID,不写则为000000
    server-id:
    # [可选] 自动点播, 使用固定流地址进行播放时,如果未点播则自动进行点播, 需要rtp.enable=true
    auto-apply-play: false
    # [可选] 部分设备需要扩展SDP,需要打开此设置
    senior-sdp: false
    # 保存移动位置历史轨迹:true:保留历史数据,false:仅保留最后的位置(默认)
    save-position-history: false
    # 点播等待超时时间,单位:毫秒
    play-timeout: 3000
    # 等待音视频编码信息再返回, true: 可以根据编码选择合适的播放器,false: 可以更快点播
    wait-track: false
    # 是否开启接口鉴权
    interface-authentication: true
    # 自动配置redis 可以过期事件
    redis-config: true
    # 接口鉴权例外的接口, 即不进行接口鉴权的接口,尽量详细书写,尽量不用/**,至少两级目录
    interface-authentication-excludes:
        - /api/v1/**
    # 推流直播是否录制
    record-push-live: true
    # 国标是否录制
    record-sip: true
    # 是否将日志存储进数据库
    logInDatebase: true
    # 第三方匹配,用于从stream钟获取有效信息
    thirdPartyGBIdReg: [\s\S]*
```
如果配置信息无误,你可以启动zlm,再启动wvp来测试了,启动成功的话,你可以在wvp的日志下看到zlm已连接的提示。
接下来[部署到服务器](./_content/introduction/deployment.md), 如何你只是本地运行直接再本地运行即可。
doc/_content/introduction/deployment.md
New file
@@ -0,0 +1,36 @@
<!-- 部署 -->
# 部署
**请仔细阅读以下内容**
1. WVP-PRO与ZLM支持分开部署,但是wvp-pro-assist必须与zlm部署在同一台主机;
2. 需要开放的端口
| 服务  | 端口                       | 类型          | 必选    |
|-----|:-------------------------|-------------|-------|
| wvp | server.port              | tcp         | 是     |
| wvp | sip.port                 | udp and tcp | 是     |
| zlm | http.port                | tcp         | 是     |
| zlm | http.sslport             | tcp         | 否     |
| zlm | rtmp.port                | tcp         | 否     |
| zlm | rtmp.sslport             | tcp         | 否     |
| zlm | rtsp.port                | udp and tcp | 否     |
| zlm | rtsp.sslport             | udp and tcp | 否     |
| zlm | rtp_proxy.port           | udp and tcp | 单端口开放 |
| zlm | rtp.port-range(在wvp中配置)  | udp and tcp | 多端口开放 |
3. 测试环境部署建议所有服务部署在一台主机,关闭防火墙,减少因网络出现问题的可能;
4. WVP-PRO与ZLM支持分开部署,但是wvp-pro-assist必须与zlm部署在同一台主机;
5. 生产环境按需开放端口,但是建议修改默认端口,尤其是5060端口,易受到攻击;
6. zlm使用docker部署的情况,要求端口映射一致,比如映射5060,应将外部端口也映射为5060端口;
7. 启动服务,以linux为例
**启动WVP-PRO**
```shell
nohup java -jar wvp-pro-*.jar &
```
**启动ZLM**
```shell
nohup ./MediaServer -d -m 3 &
```
[接入设备](./_content/ability/device.md)
doc/_content/qa/_media/img.png
doc/_content/qa/_media/img_1.png
doc/_content/qa/_media/img_2.png
doc/_content/qa/_media/img_3.png
doc/_content/qa/_media/img_4.png
doc/_content/qa/_media/img_5.png
doc/_content/qa/bug.md
New file
@@ -0,0 +1,19 @@
<!-- 反馈bug -->
# 反馈bug
代码是在不断的完善的,不断修改会修复旧的问题也有可能引入新的问题,所以遇到BUG是很正常的一件事。所以遇到问题不要烦燥,咱们就事论事就好了。
## 如何反馈
1. 更新代码,很可能你遇到问题别人已经更早的遇到了,或者是作者自己发现了,已经解决了,所以你可以更新代码再次进行测试;
2. 可以在github提ISSUE,我几乎每天都会去看issue,你的问题我会尽快给予答复;
3. 你可以来我的QQ群里,询问群友看看是否遇到了同样的问题;
4. 你可以私聊我的QQ,如果我有时间我会给你答复,但是除非你有明确的复现步骤或者修复方案,否则你可能等不到我的答复。
## 如何快速解决BUG
目前解决BUG有三种方式:
1. 作者验证以及修复;
2. 热心开发者提来的PR;
3. 使用运维手段屏蔽BUG的影响。
- 对于第一种:详细的复现步骤,完整的抓包文件,有条理的错误分析都可以帮助作者复现问题,进而解决问题。解决问题往往不是最难的,复现才是。
- 对于第二种:如果你是开发者,你已经发现了造成BUG的原因以及知道如何正确的修复,那么我很希望你PR,SRS的大佬经常说的,开源不是一个人的事。所以你的参与就是最大的鼓励。
- 对于第三种:如果你有一个有经验的运维伙伴,那么部分问题是可以通过运维的手段暂时屏蔽的,在等待修复的这段时间了以保证项目的运行。
doc/_content/qa/development.md
New file
@@ -0,0 +1,15 @@
<!-- 参与开发 -->
# 参与到开发中来
非常欢迎有兴趣的小伙伴一起来维护这个项目
## 与开发有关的信息
- 开发语言:后端java + 前端vue;
- jdk版本: 1.8;
- 作者自用开发ide: jetbrains intellij idea;
- nodejs/npm版本:v10.19.0/6.14.4;
- 后端使用Spring boot框架开发;
- 项目大量使用了异步操作;
- 跟代码学流程需要参考28181文档,只看代码你会很懵的;
- 必须学会[抓包](_content/skill/tcpdump.md),这是必须的
## 提交代码
大家可以通过fork项目的方式提交自己的代码,然后提交PR,我来合并到主线。提交代码的过程中我们需要遵循“**阿里编码规约**”,现有代码也有很多代码没有做到,但是我们在朝这个方向努力。
doc/_content/qa/img.png
doc/_content/qa/play_error.md
New file
@@ -0,0 +1,57 @@
<!-- 点播错误 -->
# 点播错误
排查点播错误你首先要清除[点播的基本流程](_content/theory/play.md),一般的流程如下:
```plantuml
@startuml
"WEB用户"  -> "WVP-PRO": 1. 发起点播请求
"设备" <-  "WVP-PRO": 2. Invite(携带SDP消息体)
"设备" --> "WVP-PRO": 3. 200OK(携带SDP消息体)
"设备" <-- "WVP-PRO": 4. Ack
"设备" -> "ZLMediaKit": 5. 发送实时流
"WVP-PRO" <- "ZLMediaKit": 6. 流改变事件
"WEB用户"  <-- "WVP-PRO": 7. 回复流播放地址(携带流地址)
"WVP-PRO" <- "ZLMediaKit": 8. 无人观看事件
"设备" <-  "WVP-PRO": 9 Bye消息
"设备" -->  "WVP-PRO": 10 200OK
@enduml
```
针对几种常见的错误,我们来分析一下,也方便大家对号入座解决常见的问题
## 点播收到错误码
这个错误一般表现为点击"播放"按钮后很快得到一个错误。
1. **400错误码**
出现400错误玛时一般是这样的流程是这样的
```plantuml
@startuml
"WEB用户"  -> "WVP-PRO": 1. 发起点播请求
"设备" <-  "WVP-PRO": 2. Invite(携带SDP消息体)
"设备" --> "WVP-PRO": 3. 400错误
@enduml
```
此时通常是设备认为WVP发送了错误的消息给它,它认为消息不全或者错误所以直接返回400错误,此时我们需要[抓包](_content/skill/tcpdump.md)来分析是否缺失了内容,也可以直接联系对方询问为什么返回了400。
WVP不能保证兼容所有的设备,有些实现不规范的设备可能在对接时就会出现上述问题,你可以联系作者帮忙对接。
2. **500错误码**
500或者大于500小于600的错误码一般多是设备内部出了问题,解决方式有两个,第一种直接联系设备/平台客服寻求解决;第二种,如果你有确定可以对接这个设备的平台那么可以把对接这个平台的抓包和对接wvp的抓包同时发送给我,我来尝试解决。
## 点播超时
点播超时的情况大致分为两种:点播超时和收流超时
1. **点播超时**
点播超时错误一般为信令的超时,比如长时间为收到对方的回复,可能出现在流程中 “3. 200OK(携带SDP消息体)”这个位置,即我们发送点播消息,但是设备没有回复,可能的原因:
> 1. 设备内部错误,未能回复消息
> 2. 网络原因消息未到到达设备
大部分时候是原因2,所以遇到这个错误我们首先要排查我们我的网路,如果你是公网部署,那么也可能时心跳周期太长,导致的路由NAT失效,WVP的消息无法通道原来的IP端口号发送给设备。
2. **收流超时**
收流超时可能发生在流程中的5和6,可能的原因有:
> 1. 设备发送了流但是发送到了错误的ip和端口上,而这个信息是在invite消息的sdp中指定的,就是流程2Invite(携带SDP消息体)中,而这个错误很可能来自你的配置错误,比如你设置了127.0.0.1导致设备网127.0.0.1上发流,或者是你WVP在公网,但是你给设备了一个内网ip,导致设备无法把流发送过来;
> 2. 设备内部错误未发送流;
> 2. 设备发送了流,但是流无法识别,可能存在于流不规范和网络很差的情况下;
> 3. 设备发送了流,zlm也收到了,但是zlm无法通过hook通知到wvp,此时原因是你可以检查zlm的配置文件中的hook配置,看看是否无法从zlm连接到wvp;
> 4. 设备发送了流,但是开启SSRC校验,设备的流不够规范采用错误的ssrc,导致zlm选择丢弃;
针对这些可能的错误原因我建议的排查顺序:
- 关闭ssrc校验;
- 查看zlm配置的hook是否可以连接到zlm;
- 查看zlm日志是否有流注册;
- 抓包查看流的信息,看看流是否正常发送,甚至可以导出发送原始流,用vlc播放,看看是否可以播放。
doc/_content/qa/regiser_error.md
New file
@@ -0,0 +1,8 @@
<!-- 设备注册不上来的解决办法 -->
# 设备注册不上来的解决办法
一般的原因有两个
1. 信息填写错误,比如密码错误;
2. 网络不通导致注册消息无法发送到WVP;
遇到问题首先仔细校验填写信息,例如海康可能需要勾选鉴权才可以输入密码。网络问题请自行测试。
doc/_content/qa/start_error.md
New file
@@ -0,0 +1,24 @@
<!-- 启动时报错 -->
# 启动时报错
启动时的报错大部分时候是因为你的配置有问题,比如mysql没连接上,redis没连接上,18080/15060端口占用了,这些都会导致启动是报错,修改配置配置之后都可以解决;
下面我整理的一些常见的错误,大家可以先对号入座的简单排查下。
> **常见错误**
![_media/img.png](_media/img.png)
**错误原因:** redis配置错误,可能原因: redis未启动/ip错误/端口错误/网络不通
---
![_media/img_1.png](_media/img_1.png)
**错误原因:** redis配置错误,可能原因: 密码错误
---
![_media/img_2.png](_media/img_2.png)
**错误原因:** mysql配置错误,可能原因: mysql未启动/ip错误/端口错误/网络不通
---
![_media/img_3.png](_media/img_3.png)
**错误原因:** mysql配置错误,可能原因: 用户名/密码错误
---
![_media/img_4.png](_media/img_4.png)
**错误原因:** SIP配置错误,可能原因: SIP端口被占用
---
![_media/img_5.png](_media/img_5.png)
**错误原因:** WVP Tomcat端口配置错误,可能原因: server.port端口被占用
---
doc/_content/skill/_media/img.png
doc/_content/skill/_media/img_1.png
doc/_content/skill/_media/img_2.png
doc/_content/skill/tcpdump.md
New file
@@ -0,0 +1,62 @@
<!-- 抓包 -->
# 抓包
如果说对于网络编程,有什么工具是必会的,我觉得抓包肯定是其中之一了。作为GB/T 28181调试过程中最重要的手段,我觉得如果你真对他有兴趣,或者系统遇到问题可以最快的得到解决,那么抓包你就一定要学会了。
## 抓包工具的选择
### 1. Wireshark
在具备图形界面的系统上,比如windows,linux发行版ubuntu,opensuse等,我一般直接使用Wireshark直接进行抓包,也方便进行内容的查看。
### 2. Tcpdump
在使用命令行的系统,比如linux服务器,我一般使用Tcpdump进行抓包,无需额外安装,系统一般自带,抓包的到的文件,可以使用Wireshark打开,在图形界面下方便查看内容。
## 工具安装
Wireshark的安装很简单,根据提示一步步点击就好了,在linux需要解决权限的问题,如果和我一样使用图形界面的linux发行版的话,可以参看如下步骤; windows的小伙伴直接略过即可
```shell
# 1. 添加wireshark用户组
sudo groupadd wireshark
# 2. 将dumpcap更改为wireshark用户组
sudo chgrp wireshark /usr/bin/dumpcap
# 3. 让wireshark用户组有root权限使用dumpcap
sudo chmod 4755 /usr/bin/dumpcap
# 4. 将需要使用的用户名加入wireshark用户组
sudo gpasswd -a $USER wireshark
```
tcpdump一般linux都是自带,无需安装,可以这样验证;显示版本信息即是已安装
```shell
tcpdump --version
```
## 开始抓包
### 使用Wireshark
在28181中我一般只关注sip包和rtp包,所以我一般是直接过滤sip和rtp,可以输入框输入 `sip or rtp`这样即可,如果设备来源比较多还可以加上ip和端口号的过滤`(sip or rtp )and ip.addr==192.168.1.3 and udp.port==5060`
详细的过滤规则可以自行百度,我可以提供一些常用的给大家参考
![img.png](_media/img.png)
**只过滤SIP:**
```shell
sip
```
**只获取rtp数据:**
```shell
rtp
```
**默认方式:**
```shell
sip or rtp
```
**过滤IP:**
```shell
 sip and ip.addr==192.168.1.3
```
**过滤端口:**
```shell
 sip and udp.port==5060
```
输入命令开启抓包后,此时可以进行操作,比如点播,录像回访等,操作完成回到Wireshark点击红色的停止即可,需要保存文件可以点击`文件->导出特定分组`导出过滤后的数据,也可以直接`文件->另存为`保存未过滤的数据。
### 使用tcpdump
对于服务器抓包,为了得到足够完整的数据,我一般会要求直接抓取网卡数据而不过滤,如下:
抓取网卡首先需要获取网卡名,在linux我一般使用`ip addr`获取网卡信息,如下所示:
![img_1.png](_media/img_1.png)
```shell
sudo tcpdump -i wlp3s0 -w demo.pcap
```
![img_2.png](_media/img_2.png)
命令行会停留在这个位置,此时可以进行操作,比如点播,录像回放等,操作完成回到命令行使用`Ctrl+C`结束命令行,在当前目录下得到demo.pcap,将这个文件下载到图形界面操作系统里,即可使用Wireshark查看了
更多的操作可以参考: [https://www.cnblogs.com/jiujuan/p/9017495.html](https://www.cnblogs.com/jiujuan/p/9017495.html)
doc/_content/theory/_media/img.png
doc/_content/theory/_media/img_1.png
doc/_content/theory/_media/img_2.png
doc/_content/theory/_media/img_3.png
doc/_content/theory/_media/img_4.png
doc/_content/theory/_media/img_5.png
doc/_content/theory/_media/img_6.png
doc/_content/theory/_media/img_7.png
doc/_content/theory/_media/img_8.png
doc/_content/theory/_media/img_9.png
doc/_content/theory/channel_tree.md
New file
@@ -0,0 +1,14 @@
<!-- 通道的树形结构 -->
# 通道的树形结构
国标28181规定了两种组织设备树的方式
1. **行政区划**
   行政区划模式下主要是以行政区划作为目录节点例如:河北省->邯郸市->广平县
   ![_media/img_8.png](_media/img_8.png)
2. **业务分组**
   业务分组主要自定义的目录树的一种组织形式,但是对定义的目录的国标编号有一定的要求。
   第一级别需要是业务分组类型,即国标编码中的11、12、13是215,例如:65010200002150000001;
   业务分组下是虚拟组织,即国标编码中的11、12、13是216,例如:65010200002160000002。
   虚拟组织下不可是业务分组,虚拟组织下可以继续添加虚拟组织。
   ![_media/img_9.png](_media/img_9.png)
doc/_content/theory/code.md
New file
@@ -0,0 +1,25 @@
<!-- 统一编码规则 -->
# 统一编码规则
## D.1 编码规则 A
>&emsp;&emsp;编码规则 A 由中心编码(8位)、行业编码(2位)、类型编码(3位)和序号(7位)四个码段共20位十
>进制数字字符构成,即系统编码 =中心编码 + 行业编码 + 类型编码 + 序号。
>&emsp;&emsp;编码规则 A 的详细说明见表 D.1。其中,中心编码指用户或设备所归属的监控中心的编码,按照监控中心所在地的行政区划代码确定,
> 当不是基层单位时空余位为0。行政区划代码采用 GB/T2260— 2007规定的行政区划代码表示。行业编码是指用户或设备所归属的行业,行业编码对照表见 D.3。
> 类型编码指定了设备或用户的具体类型,其中的前端设备包含公安系统和非公安系统的前端设备,终端用 户包含公安系统和非公安系统的终端用户。
![img_7.png](_media/img_7.png)
![img_1.png](_media/img_1.png)
![img_2.png](_media/img_2.png)
## D.2 编码规则 B
>&emsp;&emsp;编码规则 B由中心编码(8位)、行业编码(2位)、序号(4位)和类型编码(2位)四个码段构成,即系
>统编码 =中心编码 + 行业编码 +序号+类型编码。编码规则 B的详细说明见表 D.2。
![img_3.png](_media/img_3.png)
![img_4.png](_media/img_4.png)
## D.3 行业编码对照表
>&emsp;&emsp;行业编码对照表见表 D.3。
![img_5.png](_media/img_5.png)
![img_6.png](_media/img_6.png)
doc/_content/theory/img.png
doc/_content/theory/play.md
New file
@@ -0,0 +1,33 @@
<!-- 点播流程 -->
# 点播流程
> 以下为WVP-PRO点播流程。点播成功前的任何一个环节出现问题都可能出现点播超时,这也是排查点播超时的依据。
```plantuml
@startuml
"WEB用户"  -> "WVP-PRO": 1. 发起点播请求
"设备" <-  "WVP-PRO": 2. Invite(携带SDP消息体)
"设备" --> "WVP-PRO": 3. 200OK(携带SDP消息体)
"设备" <-- "WVP-PRO": 4. Ack
"设备" -> "ZLMediaKit": 5. 发送实时流
"WVP-PRO" <- "ZLMediaKit": 6. 流改变事件
"WEB用户"  <-- "WVP-PRO": 7. 回复流播放地址(携带流地址)
"WVP-PRO" <- "ZLMediaKit": 8. 无人观看事件
"设备" <-  "WVP-PRO": 9 Bye消息
"设备" -->  "WVP-PRO": 10 200OK
@enduml
```
##  注册流程描述如下:
1. 用户从网页或调用接口发起点播请求;
2. WVP-PRO向摄像机发送Invite消息,消息头域中携带 Subject字段,表明点播的视频源ID、发送方媒体流序列号、ZLMediaKit接收流使用的IP、端口号、
 接收端媒体流序列号等参数,SDP消息体中 s字段为“Play”代表实时点播,y字段描述SSRC值,f字段描述媒体参数。
3. 摄像机向WVP-PRO回复200OK,消息体中描述了媒体流发送者发送媒体流的IP、端口、媒体格式、SSRC字段等内容。
4. WVP-PRO向设备回复Ack, 会话建立成功。
5. 设备向ZLMediaKit发送实时流。
6. ZLMediaKit向WVP-PRO发送流改变事件。
7. WVP-PRO向WEB用户回复播放地址。
8. ZLMediaKit向WVP发送流无人观看事件。
9. WVP-PRO向设备回复Bye, 结束会话。
10. 设备回复200OK,会话结束成功。
doc/_content/theory/register.md
New file
@@ -0,0 +1,21 @@
<!-- 注册流程 -->
# 注册流程
WVP-PRO目前仅支持国标中描述的基本注册流程,也是最常用的,
> 基本注册即采用IETFRFC3261规定的基于数字摘要的挑战应答式安全技术进行注册.
```plantuml
@startuml
"设备"  -> "WVP-PRO": 1. Register
"设备" <-- "WVP-PRO": 2. 401 Unauthorized
"设备"  -> "WVP-PRO": 3. Register
"设备" <-- "WVP-PRO": 4. 200 OK
@enduml
```
> 注册流程描述如下:
> 1. 摄像机向WVP-PRO服务器发送 Register请求;
> 2. WVP-PRO向摄像机发送响应401,并在响应的消息头 WWW_Authenticate字段中给出适合摄像机的认证体制和参数;
> 3. 摄像机重新向WVP-PRO发送 Register请求,在请求的 Authorization字段给出信任书, 包含认证信息;
> 4. WVP-PRO对请求进行验证,如果检查出 摄像机身份合法,向摄像机发送成功响应 200OK,如果身份不合法则发送拒绝服务应答。
doc/_coverpage.md
New file
@@ -0,0 +1,17 @@
<!-- 封面 -->
![logo](_media/logo-mini.png)
# WVP-PRO <small>2.0</small>
> 开箱即用的28181协议视频平台。
- 基于GB/T28181-2016标准信令实现,兼容GB/T28181-2011。
- 自带完整前端页面,开箱即用。
- 完全开源,且使用MIT许可协议。可以在保留版权信息的基础上商用。
[GitHub](https://github.com/648540858/wvp-GB28181-pro)
[Gitee](https://gitee.com/pan648540858/wvp-GB28181-pro)
<!-- 背景色 -->
[//]: # ([comment]: <> &#40;![color]&#40;#f0f0f0&#41;&#41;)
doc/_media/favicon.ico
doc/_media/logo-mini.png
doc/_media/logo.png
doc/_media/weixin.jpg
doc/_media/zhifubao.jpg
doc/_navbar.md
New file
@@ -0,0 +1 @@
<!-- 导航栏 -->
doc/_sidebar.md
New file
@@ -0,0 +1,32 @@
<!-- 侧边栏 -->
* **编译与部署**
  * [编译](_content/introduction/compile.md)
  * [配置](_content/introduction/config.md)
  * [部署](_content/introduction/deployment.md)
* **功能与使用**
  * [接入设备](_content/ability/device.md)
  * [设备使用](_content/ability/device_use.md)
  * [国标级联](_content/ability/cascade2.md)
  * [推流列表](_content/ability/push.md)
  * [拉流代理](_content/ability/proxy.md)
  * [电子地图](_content/ability/gis.md)
  * [节点管理](_content/ability/node_manger.md)
  * [云端录像](_content/ability/cloud_record.md)
  * [不间断录像](_content/ability/continuous_recording.md)
* **流程与原理**
  * [统一编码规则](_content/theory/code.md)
  * [树形结构](_content/theory/channel_tree.md)
  * [注册流程](_content/theory/register.md)
  * [点播流程](_content/theory/play.md)
* **必备技巧**
  * [抓包](_content/skill/tcpdump.md)
* **常见问答**
  - [如何反馈BUG](_content/qa/bug.md)
  - [如何参与开发](_content/qa/development.md)
  - [启动报错的解决办法](_content/qa/start_error.md)
  - [设备注册不上来的解决办法](_content/qa/regiser_error.md)
  - [点播超时/报错的解决办法](_content/qa/play_error.md)
* [**免责声明**](_content/disclaimers.md)
* [**关于本文档**](_content/about_doc.md)
doc/index.html
New file
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>WVP-PRO文档</title>
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
  <meta name="description" content="Description">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
  <link rel="icon" href="_media/favicon.ico" type="image/x-icon" />
  <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css">
<!--  <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify/lib/themes/dark.css">-->
  <style>
    .cover{
      background: linear-gradient(to left bottom, hsl(82, 100%, 85%) 0%,hsl(199, 100%, 85%) 100%) !important;
    }
  </style>
</head>
<body>
  <div id="app">加载中</div>
  <script>
    window.$docsify = {
      name: 'WVP-RPO使用文档',
      repo: 'https://github.com/648540858/wvp-GB28181-pro',
      loadSidebar: true, // 开启侧边栏
      loadNavbar: true, // 开启导航栏
      coverpage: true, // 开启封面
      subMaxLevel: 3,
      plantuml: {
        skin: 'default',
      },
      search: {
        maxAge: 86400000, // 过期时间,单位毫秒,默认一天
        paths: 'auto', // or 'auto'
        placeholder: '搜索',
        noData: '找不到结果',
        // 搜索标题的最大层级, 1 - 6
        depth: 4,
        hideOtherSidebarContent: false, // 是否隐藏其他侧边栏内容
      },
      copyCode: {
        buttonText : '复制',
        errorText  : '错误',
        successText: '已复制'
      },
      // disqus: 'shortname'
    }
  </script>
  <!-- Docsify v4 -->
  <script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
  <script src="//unpkg.com/docsify-plantuml/dist/docsify-plantuml.min.js"></script>
  <script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
  <script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/zoom-image.min.js"></script>
  <script src="//cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script>
  </script>
<!--  <script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/disqus.min.js"></script>-->
</body>
</html>
docker/docker-compose.yml
New file
@@ -0,0 +1,49 @@
version: '3'
services:
  redis:
    image: redis
    restart: always
    volumes:
      - ./redis/redis.conf:/etc/redis/redis_default.conf
      - ./redis/data/:/data
    environment:
      TZ: "Asia/Shanghai"
    command: redis-server /etc/redis/redis_default.conf --appendonly yes
  wvp:
    build:
      context: ./wvp
      args:
        gitUrl: "https://gitee.com/pan648540858"
        zlmGitUrl: "https://gitee.com/xia-chu/ZLMediaKit"
    restart: always
    ports:
      - "5060:5060"
      - "5060:5060/udp"
      - "18080:18080"
      - "80:80"
      - "10000:10000/tcp"
      - "10000:10000/udp"
      - "30000-30500:30000-30500/tcp"
      - "30000-30500:30000-30500/udp"
    volumes:
      - ./video:/opt/media/www/record/
      - ./logs/wvp:/opt/wvp/logs/
      - ./logs/assist:/opt/assist/logs/
      - ./logs/media:/opt/media/log/
    environment:
      TZ: "Asia/Shanghai"
      # [必须修改] 本机的IP
      WVP_HOST: 172.18.0.61
      WVP_PWD: aseqw_+hiy123
      WVP_DOMAIN: 6101130049
      WVP_ID: 61011300490000000001
      REDIS_HOST: redis
      REDIS_PORT: 6379
      REDIS_DB: 6
      REDIS_PWD: root
      ASSIST_JVM_CONFIG: -Xms128m -Xmx256m
      WVP_JVM_CONFIG: -Xms128m -Xmx256m
      ASSIST_CONFIG:
      WVP_CONFIG:
    depends_on:
      - redis
docker/redis/redis.conf
New file
@@ -0,0 +1,2 @@
requirepass root
bind 0.0.0.0
docker/wvp/Dockerfile
New file
@@ -0,0 +1,81 @@
FROM ubuntu:20.04   as   build
ARG gitUrl="https://gitee.com/pan648540858"
ARG zlmGitUrl="https://gitee.com/xia-chu/ZLMediaKit"
RUN export DEBIAN_FRONTEND=noninteractive &&\
        apt-get update && \
        apt-get install -y --no-install-recommends openjdk-11-jre git maven nodejs npm build-essential \
        cmake ca-certificates openssl ffmpeg &&\
        mkdir -p /opt/wvp/config /opt/wvp/heapdump /opt/wvp/config /opt/assist/config /opt/assist/heapdump /opt/media/www/record
RUN cd /home && \
        git clone "${gitUrl}/maven.git" && \
        cp maven/settings.xml /usr/share/maven/conf/
RUN cd /home && \
        git clone "${gitUrl}/wvp-GB28181-pro.git"
RUN cd /home/wvp-GB28181-pro/web_src && \
        npm install && \
        npm run build
RUN cd /home/wvp-GB28181-pro && \
        mvn clean package -Dmaven.test.skip=true && \
        cp /home/wvp-GB28181-pro/target/*.jar /opt/wvp/ && \
        cp /home/wvp-GB28181-pro/src/main/resources/application-docker.yml /opt/wvp/config/application.yml
RUN cd /home && \
        git clone "${gitUrl}/wvp-pro-assist.git"
RUN cd /home/wvp-pro-assist && \
    git reset --hard 58f1a79136a55a7cd1593c95b56ddadcc2225b61 && \
        mvn clean package -Dmaven.test.skip=true && \
        cp /home/wvp-pro-assist/target/*.jar /opt/assist/ && \
        cp /home/wvp-pro-assist/src/main/resources/application-dev.yml /opt/assist/config/application.yml
RUN cd /home && \
        git clone --depth=1 "${zlmGitUrl}"
RUN cd /home/ZLMediaKit && \
        git submodule update --init --recursive && \
        mkdir -p build release/linux/Release/ &&\
        cd build && \
        cmake -DCMAKE_BUILD_TYPE=Release .. && \
        make -j4 && \
        rm -rf ../release/linux/Release/config.ini && \
        cp -r ../release/linux/Release/* /opt/media
RUN cd /opt/wvp && \
        echo '#!/bin/bash' > run.sh && \
        echo 'echo ${WVP_IP}' >> run.sh && \
        echo 'echo ${WVP_CONFIG}' >> run.sh && \
        echo 'cd /opt/assist' >> run.sh && \
        echo 'nohup java ${ASSIST_JVM_CONFIG} -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/assist/heapdump/ -jar *.jar --spring.config.location=/opt/assist/config/application.yml --userSettings.record=/opt/media/www/record/  --media.record-assist-port=18081 ${ASSIST_CONFIG} &' >> run.sh && \
        echo 'nohup /opt/media/MediaServer -d -m 3 &' >> run.sh && \
        echo 'cd /opt/wvp' >> run.sh && \
        echo 'java ${WVP_JVM_CONFIG} -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/wvp/heapdump/ -jar *.jar --spring.config.location=/opt/wvp/config/application.yml --media.record-assist-port=18081 ${WVP_CONFIG}' >> run.sh && \
        chmod +x run.sh
FROM ubuntu:20.04
EXPOSE 18080/tcp
EXPOSE 5060/tcp
EXPOSE 5060/udp
EXPOSE 6379/tcp
EXPOSE 18081/tcp
EXPOSE 80/tcp
EXPOSE 1935/tcp
EXPOSE 554/tcp
EXPOSE 554/udp
EXPOSE 30000-30500/tcp
EXPOSE 30000-30500/udp
ENV LC_ALL zh_CN.UTF-8
RUN export DEBIAN_FRONTEND=noninteractive &&\
        apt-get update && \
        apt-get install -y --no-install-recommends openjdk-11-jre ca-certificates ffmpeg language-pack-zh-hans && \
        apt-get autoremove -y && \
        apt-get clean -y && \
        rm -rf /var/lib/apt/lists/*dic
COPY --from=build /opt /opt
WORKDIR /opt/wvp
CMD ["sh", "run.sh"]
pom.xml
New file
@@ -0,0 +1,305 @@
<?xml version="1.0"?>
<project
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
    xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.2</version>
    </parent>
    <groupId>com.genersoft</groupId>
    <artifactId>wvp-pro</artifactId>
    <version>2.3.2</version>
    <name>web video platform</name>
    <description>国标28181视频平台</description>
    <repositories>
        <repository>
            <id>nexus-aliyun</id>
            <name>Nexus aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
            <layout>default</layout>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
            <releases>
                <enabled>true</enabled>
            </releases>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>nexus-aliyun</id>
            <name>Nexus aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
            <releases>
                <enabled>true</enabled>
            </releases>
        </pluginRepository>
    </pluginRepositories>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.build.timestamp.format>MMddHHmm</maven.build.timestamp.format>
        <maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
        <!-- 依赖版本 -->
        <snippetsDirectory>${project.build.directory}/generated-snippets</snippetsDirectory>
        <asciidoctor.input.directory>${project.basedir}/docs/asciidoc</asciidoctor.input.directory>
        <generated.asciidoc.directory>${project.build.directory}/asciidoc</generated.asciidoc.directory>
        <asciidoctor.html.output.directory>${project.build.directory}/asciidoc/html</asciidoctor.html.output.directory>
        <asciidoctor.pdf.output.directory>${project.build.directory}/asciidoc/pdf</asciidoctor.pdf.output.directory>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
            <exclusions>
                <exclusion>
                    <groupId>com.zaxxer</groupId>
                    <artifactId>HikariCP</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- druid数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.11</version>
        </dependency>
        <!-- mysql数据库 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
        <!--Mybatis分页插件 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.4.3</version>
        </dependency>
        <!--在线文档 -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-ui</artifactId>
            <version>1.6.10</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-springdoc-ui</artifactId>
            <version>3.0.3</version>
        </dependency>
        <!--参数校验 -->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
        </dependency>
        <!-- 日志相关 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- sip协议栈 -->
        <dependency>
            <groupId>javax.sip</groupId>
            <artifactId>jain-sip-ri</artifactId>
            <version>1.3.0-91</version>
        </dependency>
        <!-- 取代log4j -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
            <version>1.7.36</version>
        </dependency>
        <!-- xml解析库 -->
        <dependency>
            <groupId>org.dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>2.1.3</version>
        </dependency>
        <!-- json解析库fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
        <!-- okhttp -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.10.0</version>
        </dependency>
        <!-- okhttp 调试日志 -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>logging-interceptor</artifactId>
            <version>4.10.0</version>
        </dependency>
        <!-- okhttp-digest -->
        <dependency>
            <groupId>io.github.rburgst</groupId>
            <artifactId>okhttp-digest</artifactId>
            <version>2.7</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/net.sf.kxml/kxml2 -->
        <dependency>
            <groupId>net.sf.kxml</groupId>
            <artifactId>kxml2</artifactId>
            <version>2.3.0</version>
        </dependency>
        <!--反向代理-->
        <dependency>
            <groupId>org.mitre.dsmiley.httpproxy</groupId>
            <artifactId>smiley-http-proxy-servlet</artifactId>
            <version>1.12.1</version>
        </dependency>
        <!--excel解析库-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>3.1.1</version>
        </dependency>
        <!-- 获取系统信息 -->
        <dependency>
            <groupId>com.github.oshi</groupId>
            <artifactId>oshi-core</artifactId>
            <version>6.2.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-core</artifactId>
        </dependency>
<!--        &lt;!&ndash; 检测文件编码 &ndash;&gt;-->
<!--        &lt;!&ndash; https://mvnrepository.com/artifact/cpdetector/cpdetector &ndash;&gt;-->
<!--        <dependency>-->
<!--            <groupId>cpdetector</groupId>-->
<!--            <artifactId>cpdetector</artifactId>-->
<!--            <version>1.0.8</version>-->
<!--        </dependency>-->
        <!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>31.1-jre</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
<!--            <scope>test</scope>-->
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>
    </dependencies>
    <build>
        <finalName>videos</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.5.RELEASE</version>
                <configuration>
                    <includeSystemScope>true</includeSystemScope>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>pl.project13.maven</groupId>
                <artifactId>git-commit-id-plugin</artifactId>
                <version>3.0.1</version>
                <configuration>
                    <offline>true</offline>
                    <failOnNoGitDirectory>false</failOnNoGitDirectory>
                    <dateFormat>yyyyMMdd</dateFormat>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
        </resources>
    </build>
</project>
snap/34020000001320000001_34020000001320000001.jpg
New file
@@ -0,0 +1,5 @@
#### pid=6480,cmd=/usr/bin/ffmpeg -i http://192.168.0.108:8880/rtp/34020000001320000001_34020000001320000001.live.mp4 -y -f mjpeg -t 0.001 /home/videoPlatform/ZLMediaKit/release/linux/Debug/www/snap/5be529f0537c77269a9a2dfd06de96fa/1665543460.jpeg.tmp #####
execv process failed:-2(no such file or directory)
snap/34020000002000000003_34020000001320000001.jpg
New file
@@ -0,0 +1,5 @@
#### pid=1977,cmd=/usr/bin/ffmpeg -i http://192.168.0.160:8880/rtp/34020000002000000003_34020000001320000001.live.mp4 -y -f mjpeg -t 0.001 /home/ZLMediaKit/release/linux/Debug/www/snap/bb6842151ae0f34378e1a9b45a1adbbb/1679390098.jpeg.tmp #####
execv process failed:-2(no such file or directory)
sql/clean.sql
New file
@@ -0,0 +1,13 @@
delete from  device;
delete from  device_alarm;
delete from  device_channel;
delete from  device_mobile_position;
delete from  gb_stream;
delete from  log;
delete from  media_server;
delete from  parent_platform;
delete from  platform_catalog;
delete from  platform_gb_channel;
delete from  platform_gb_stream;
delete from  stream_proxy;
delete from  stream_push;
sql/mysql.sql
New file
@@ -0,0 +1,576 @@
-- MySQL dump 10.13  Distrib 8.0.30, for Linux (x86_64)
--
-- Host: 127.0.0.1    Database: wvp
-- ------------------------------------------------------
-- Server version    8.0.30
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `device`
--
DROP TABLE IF EXISTS `device`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `device` (
                          `id` int NOT NULL AUTO_INCREMENT,
                          `deviceId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                          `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                          `manufacture` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                          `model` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                          `firmware` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                          `transport` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                          `streamMode` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                          `online` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                          `registerTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                          `keepaliveTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                          `ip` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL,
                          `createTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                          `updateTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                          `port` int DEFAULT NULL,
                          `expires` int DEFAULT NULL,
                          `subscribeCycleForCatalog` int DEFAULT NULL,
                          `hostAddress` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL,
                          `charset` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                          `subscribeCycleForMobilePosition` int DEFAULT NULL,
                          `mobilePositionSubmissionInterval` int DEFAULT '5',
                          `subscribeCycleForAlarm` int DEFAULT NULL,
                          `ssrcCheck` int DEFAULT '0',
                          `geoCoordSys` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                          `treeType` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                          `mediaServerId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT 'auto',
                          `custom_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
                          `password` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
                          PRIMARY KEY (`id`),
                          UNIQUE KEY `device_deviceId_uindex` (`deviceId`)
) ENGINE=InnoDB AUTO_INCREMENT=47 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `device`
--
LOCK TABLES `device` WRITE;
/*!40000 ALTER TABLE `device` DISABLE KEYS */;
/*!40000 ALTER TABLE `device` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `device_alarm`
--
DROP TABLE IF EXISTS `device_alarm`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `device_alarm` (
                                `id` int NOT NULL AUTO_INCREMENT,
                                `deviceId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `channelId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `alarmPriority` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `alarmMethod` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                `alarmTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `alarmDescription` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                `longitude` double DEFAULT NULL,
                                `latitude` double DEFAULT NULL,
                                `alarmType` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                `createTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `device_alarm`
--
LOCK TABLES `device_alarm` WRITE;
/*!40000 ALTER TABLE `device_alarm` DISABLE KEYS */;
/*!40000 ALTER TABLE `device_alarm` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `device_channel`
--
DROP TABLE IF EXISTS `device_channel`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `device_channel` (
                                  `id` int NOT NULL AUTO_INCREMENT,
                                  `channelId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `manufacture` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `model` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `owner` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `civilCode` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `block` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `address` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `parentId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `safetyWay` int DEFAULT NULL,
                                  `registerWay` int DEFAULT NULL,
                                  `certNum` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `certifiable` int DEFAULT NULL,
                                  `errCode` int DEFAULT NULL,
                                  `endTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `secrecy` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `ipAddress` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `port` int DEFAULT NULL,
                                  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `PTZType` int DEFAULT NULL,
                                  `status` int DEFAULT NULL,
                                  `longitude` double DEFAULT NULL,
                                  `latitude` double DEFAULT NULL,
                                  `streamId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `deviceId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                  `parental` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `hasAudio` bit(1) DEFAULT NULL,
                                  `createTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                  `updateTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                  `subCount` int DEFAULT '0',
                                  `longitudeGcj02` double DEFAULT NULL,
                                  `latitudeGcj02` double DEFAULT NULL,
                                  `longitudeWgs84` double DEFAULT NULL,
                                  `latitudeWgs84` double DEFAULT NULL,
                                  `businessGroupId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  `gpsTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                  PRIMARY KEY (`id`),
                                  UNIQUE KEY `device_channel_id_uindex` (`id`),
                                  UNIQUE KEY `device_channel_pk` (`channelId`,`deviceId`)
) ENGINE=InnoDB AUTO_INCREMENT=60301 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `device_channel`
--
LOCK TABLES `device_channel` WRITE;
/*!40000 ALTER TABLE `device_channel` DISABLE KEYS */;
/*!40000 ALTER TABLE `device_channel` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `device_mobile_position`
--
DROP TABLE IF EXISTS `device_mobile_position`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `device_mobile_position` (
                                          `id` int NOT NULL AUTO_INCREMENT,
                                          `deviceId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                          `channelId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                          `deviceName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                          `time` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                          `longitude` double NOT NULL,
                                          `latitude` double NOT NULL,
                                          `altitude` double DEFAULT NULL,
                                          `speed` double DEFAULT NULL,
                                          `direction` double DEFAULT NULL,
                                          `reportSource` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                          `longitudeGcj02` double DEFAULT NULL,
                                          `latitudeGcj02` double DEFAULT NULL,
                                          `longitudeWgs84` double DEFAULT NULL,
                                          `latitudeWgs84` double DEFAULT NULL,
                                          `createTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                          PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=55589 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `device_mobile_position`
--
LOCK TABLES `device_mobile_position` WRITE;
/*!40000 ALTER TABLE `device_mobile_position` DISABLE KEYS */;
/*!40000 ALTER TABLE `device_mobile_position` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `gb_stream`
--
DROP TABLE IF EXISTS `gb_stream`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `gb_stream` (
                             `gbStreamId` int NOT NULL AUTO_INCREMENT,
                             `app` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                             `stream` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                             `gbId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                             `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                             `longitude` double DEFAULT NULL,
                             `latitude` double DEFAULT NULL,
                             `streamType` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                             `mediaServerId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                             `createTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                             PRIMARY KEY (`gbStreamId`) USING BTREE,
                             UNIQUE KEY `app` (`app`,`stream`) USING BTREE,
                             UNIQUE KEY `gbId` (`gbId`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=301059 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `gb_stream`
--
LOCK TABLES `gb_stream` WRITE;
/*!40000 ALTER TABLE `gb_stream` DISABLE KEYS */;
/*!40000 ALTER TABLE `gb_stream` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `log`
--
DROP TABLE IF EXISTS `log`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `log` (
                       `id` int NOT NULL AUTO_INCREMENT,
                       `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                       `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                       `uri` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                       `address` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                       `result` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                       `timing` bigint NOT NULL,
                       `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                       `createTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                       PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=733627 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `log`
--
LOCK TABLES `log` WRITE;
/*!40000 ALTER TABLE `log` DISABLE KEYS */;
/*!40000 ALTER TABLE `log` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `media_server`
--
DROP TABLE IF EXISTS `media_server`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media_server` (
                                `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `hookIp` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `sdpIp` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `streamIp` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `httpPort` int NOT NULL,
                                `httpSSlPort` int NOT NULL,
                                `rtmpPort` int NOT NULL,
                                `rtmpSSlPort` int NOT NULL,
                                `rtpProxyPort` int NOT NULL,
                                `rtspPort` int NOT NULL,
                                `rtspSSLPort` int NOT NULL,
                                `autoConfig` int NOT NULL,
                                `secret` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `rtpEnable` int NOT NULL,
                                `rtpPortRange` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `sendRtpPortRange` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `recordAssistPort` int NOT NULL,
                                `defaultServer` int NOT NULL,
                                `createTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `updateTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `hookAliveInterval` int NOT NULL,
                                PRIMARY KEY (`id`) USING BTREE,
                                UNIQUE KEY `media_server_i` (`ip`,`httpPort`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `media_server`
--
LOCK TABLES `media_server` WRITE;
/*!40000 ALTER TABLE `media_server` DISABLE KEYS */;
/*!40000 ALTER TABLE `media_server` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `parent_platform`
--
DROP TABLE IF EXISTS `parent_platform`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `parent_platform` (
                                   `id` int NOT NULL AUTO_INCREMENT,
                                   `enable` int DEFAULT NULL,
                                   `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                   `serverGBId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                   `serverGBDomain` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                   `serverIP` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                   `serverPort` int DEFAULT NULL,
                                   `deviceGBId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                   `deviceIp` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                   `devicePort` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                   `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                   `password` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                   `expires` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                   `keepTimeout` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                   `transport` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                   `characterSet` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                   `catalogId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                   `ptz` int DEFAULT NULL,
                                   `rtcp` int DEFAULT NULL,
                                   `status` bit(1) DEFAULT NULL,
                                   `startOfflinePush` int DEFAULT '0',
                                   `administrativeDivision` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                   `catalogGroup` int DEFAULT '1',
                                   `createTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                   `updateTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                   `treeType` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                   PRIMARY KEY (`id`),
                                   UNIQUE KEY `parent_platform_id_uindex` (`id`),
                                   UNIQUE KEY `parent_platform_pk` (`serverGBId`)
) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `parent_platform`
--
LOCK TABLES `parent_platform` WRITE;
/*!40000 ALTER TABLE `parent_platform` DISABLE KEYS */;
/*!40000 ALTER TABLE `parent_platform` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `platform_catalog`
--
DROP TABLE IF EXISTS `platform_catalog`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `platform_catalog` (
                                    `id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                    `platformId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                    `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                    `parentId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                    `civilCode` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                    `businessGroupId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                    PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `platform_catalog`
--
LOCK TABLES `platform_catalog` WRITE;
/*!40000 ALTER TABLE `platform_catalog` DISABLE KEYS */;
/*!40000 ALTER TABLE `platform_catalog` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `platform_gb_channel`
--
DROP TABLE IF EXISTS `platform_gb_channel`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `platform_gb_channel` (
                                       `id` int NOT NULL AUTO_INCREMENT,
                                       `platformId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                       `catalogId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                       `deviceChannelId` int NOT NULL,
                                       PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=102 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `platform_gb_channel`
--
LOCK TABLES `platform_gb_channel` WRITE;
/*!40000 ALTER TABLE `platform_gb_channel` DISABLE KEYS */;
/*!40000 ALTER TABLE `platform_gb_channel` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `platform_gb_stream`
--
DROP TABLE IF EXISTS `platform_gb_stream`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `platform_gb_stream` (
                                      `platformId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                      `catalogId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                      `gbStreamId` int NOT NULL,
                                      `id` int NOT NULL AUTO_INCREMENT,
                                      PRIMARY KEY (`id`),
                                      UNIQUE KEY `platform_gb_stream_pk` (`platformId`,`catalogId`,`gbStreamId`)
) ENGINE=InnoDB AUTO_INCREMENT=301766 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `platform_gb_stream`
--
LOCK TABLES `platform_gb_stream` WRITE;
/*!40000 ALTER TABLE `platform_gb_stream` DISABLE KEYS */;
/*!40000 ALTER TABLE `platform_gb_stream` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `stream_proxy`
--
DROP TABLE IF EXISTS `stream_proxy`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `stream_proxy` (
                                `id` int NOT NULL AUTO_INCREMENT,
                                `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `app` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `stream` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                `src_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                `dst_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                `timeout_ms` int DEFAULT NULL,
                                `ffmpeg_cmd_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                `rtp_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                `mediaServerId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                `enable_hls` bit(1) DEFAULT NULL,
                                `enable_mp4` bit(1) DEFAULT NULL,
                                `enable` bit(1) NOT NULL,
                                `status` bit(1) NOT NULL,
                                `enable_remove_none_reader` bit(1) NOT NULL,
                                `createTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                `updateTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                                `enable_disable_none_reader` bit(1) DEFAULT NULL,
                                PRIMARY KEY (`id`),
                                UNIQUE KEY `stream_proxy_pk` (`app`,`stream`)
) ENGINE=InnoDB AUTO_INCREMENT=548 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `stream_proxy`
--
LOCK TABLES `stream_proxy` WRITE;
/*!40000 ALTER TABLE `stream_proxy` DISABLE KEYS */;
/*!40000 ALTER TABLE `stream_proxy` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `stream_push`
--
DROP TABLE IF EXISTS `stream_push`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `stream_push` (
                               `id` int NOT NULL AUTO_INCREMENT,
                               `app` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                               `stream` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                               `totalReaderCount` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                               `originType` int DEFAULT NULL,
                               `originTypeStr` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                               `createTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                               `aliveSecond` int DEFAULT NULL,
                               `mediaServerId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                               `serverId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                               `pushTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                               `status` int DEFAULT NULL,
                               `updateTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                               `pushIng` int DEFAULT NULL,
                               `self` int DEFAULT NULL,
                               PRIMARY KEY (`id`),
                               UNIQUE KEY `stream_push_pk` (`app`,`stream`)
) ENGINE=InnoDB AUTO_INCREMENT=310558 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `stream_push`
--
LOCK TABLES `stream_push` WRITE;
/*!40000 ALTER TABLE `stream_push` DISABLE KEYS */;
/*!40000 ALTER TABLE `stream_push` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `user`
--
DROP TABLE IF EXISTS `user`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `user` (
                        `id` int NOT NULL AUTO_INCREMENT,
                        `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                        `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                        `roleId` int NOT NULL,
                        `createTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                        `updateTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                        `pushKey` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                        PRIMARY KEY (`id`) USING BTREE,
                        UNIQUE KEY `user_username_uindex` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `user`
--
LOCK TABLES `user` WRITE;
/*!40000 ALTER TABLE `user` DISABLE KEYS */;
INSERT INTO `user` VALUES (1,'admin','21232f297a57a5a743894a0e4a801fc3',1,'2021 - 04 - 13 14:14:57','2021 - 04 - 13 14:14:57','3e80d1762a324d5b0ff636e0bd16f1e3');
/*!40000 ALTER TABLE `user` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `user_role`
--
DROP TABLE IF EXISTS `user_role`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `user_role` (
                             `id` int NOT NULL AUTO_INCREMENT,
                             `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                             `authority` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                             `createTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                             `updateTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                             PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `user_role`
--
LOCK TABLES `user_role` WRITE;
/*!40000 ALTER TABLE `user_role` DISABLE KEYS */;
INSERT INTO `user_role` VALUES (1,'admin','0','2021-04-13 14:14:57','2021-04-13 14:14:57');
/*!40000 ALTER TABLE `user_role` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2022-10-18 17:00:02
sql/update.sql
New file
@@ -0,0 +1,29 @@
alter table media_server
    drop column streamNoneReaderDelayMS;
alter table stream_proxy
    add enable_disable_none_reader bit(1) default null;
alter table device
    add mediaServerId varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT 'auto';
alter table device
    add custom_name varchar(255) default null;
alter table device
    add password varchar(255) default null;
alter table device
    modify ip varchar(50) null;
alter table device
    modify port int null;
alter table device
    modify expires int null;
alter table device
    modify subscribeCycleForCatalog int null;
alter table device
    modify hostAddress varchar(50) null;
src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java
New file
@@ -0,0 +1,46 @@
package com.genersoft.iot.vmp;
import java.util.logging.LogManager;
import com.genersoft.iot.vmp.conf.druid.EnableDruidSupport;
import com.genersoft.iot.vmp.storager.impl.RedisCatchStorageImpl;
import com.genersoft.iot.vmp.utils.GitUtil;
import com.genersoft.iot.vmp.utils.SpringBeanFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
 * 启动类
 */
@ServletComponentScan("com.genersoft.iot.vmp.conf")
@SpringBootApplication
@EnableScheduling
@EnableDruidSupport
public class VManageBootstrap extends LogManager {
    private final static Logger logger = LoggerFactory.getLogger(VManageBootstrap.class);
    private static String[] args;
    private static ConfigurableApplicationContext context;
    public static void main(String[] args) {
        VManageBootstrap.args = args;
        VManageBootstrap.context = SpringApplication.run(VManageBootstrap.class, args);
        GitUtil gitUtil1 = SpringBeanFactory.getBean("gitUtil");
        logger.info("构建版本: {}", gitUtil1.getBuildVersion());
        logger.info("构建时间: {}", gitUtil1.getBuildDate());
        logger.info("GIT最后提交时间: {}", gitUtil1.getCommitTime());
    }
    // 项目重启
    public static void restart() {
        context.close();
        VManageBootstrap.context = SpringApplication.run(VManageBootstrap.class, args);
    }
}
src/main/java/com/genersoft/iot/vmp/common/ApiSaveConstant.java
New file
@@ -0,0 +1,198 @@
package com.genersoft.iot.vmp.common;
/**
 * 为API重命名, 方便向数据库记录数据的时候展示
 * @author lin
 */
public class ApiSaveConstant {
    public static String getVal(String key) {
        String[] keyItemArray = key.split("/");
        if (keyItemArray.length <= 1 || !"api".equals(keyItemArray[1])) {
            return null;
        }
        if (keyItemArray.length >= 4) {
            switch (keyItemArray[2]) {
                case "alarm":
                    if ("delete".equals(keyItemArray[3])) {
                        return "删除报警";
                    }
                    break;
                case "device":
                    switch (keyItemArray[3]) {
                        case "config":
                            if (keyItemArray.length >= 5 && "basicParam".equals(keyItemArray[4])) {
                                return "[设备配置] 基本配置设置命令";
                            }
                            break;
                        case "control":
                            switch (keyItemArray[4]) {
                                case "teleboot":
                                    return "[设备控制] 远程启动";
                                case "record":
                                    return "[设备控制] 录像控制";
                                case "guard":
                                    return "[设备控制] 布防/撤防命令";
                                case "reset_alarm":
                                    return "[设备控制] 报警复位";
                                case "i_frame":
                                    return "[设备控制] 强制关键帧";
                                case "home_position":
                                    return "[设备控制] 看守位控制";
                                default:
                                    return "";
                            }
                            case "query":
                                if (keyItemArray.length <= 5) {
                                    return null;
                                }
                                switch (keyItemArray[4]) {
                                    case "devices":
                                        if (keyItemArray.length < 7) {
                                            return null;
                                        }
                                        switch (keyItemArray[6]) {
                                            case "sync":
                                                return "[设备查询] 同步设备通道";
                                            case "delete":
                                                return "[设备查询] 移除设备";
                                            default:
                                                return "";
                                        }
                                    case "channel":
                                        return "[设备查询] 更新通道信息";
                                    case "transport":
                                        return "[设备查询] 修改数据流传输模式";
                                    default:
                                        return "";
                                }
                        default:
                            return "";
                            }
                    break;
                case "gbStream":
                    switch (keyItemArray[3]) {
                        case "del":
                            return "移除通道与国标的关联";
                        case "add":
                            return "添加通道与国标的关联";
                        default:
                            return "";
                    }
                case "media":
                    break;
                case "position":
                    if ("subscribe".equals(keyItemArray[3])) {
                        return "订阅位置信息";
                    }
                    break;
                case "platform":
                    switch (keyItemArray[3]) {
                        case "save":
                            return "添加上级平台";
                        case "delete":
                            return "移除上级平台";
                        case "update_channel_for_gb":
                            return "向上级平台添加国标通道";
                        case "del_channel_for_gb":
                            return "从上级平台移除国标通道";
                        default:
                            return "";
                    }
                case "platform_gb_stream":
                    break;
                case "play":
                    switch (keyItemArray[3]) {
                        case "start":
                            return "开始点播";
                        case "stop":
                            return "停止点播";
                        case "convert":
                            return "转码";
                        case "convertStop":
                            return "结束转码";
                        case "broadcast":
                            return "语音广播";
                        default:
                            return "";
                    }
                case "download":
                    switch (keyItemArray[3]) {
                        case "start":
                            return "开始历史媒体下载";
                        case "stop":
                            return "停止历史媒体下载";
                        default:
                            return "";
                    }
                case "playback":
                    switch (keyItemArray[3]) {
                        case "start":
                            return "开始视频回放";
                        case "stop":
                            return "停止视频回放";
                        default:
                            return "";
                    }
                case "ptz":
                    switch (keyItemArray[3]) {
                        case "control":
                            return "云台控制";
                        case "front_end_command":
                            return "通用前端控制命令";
                        default:
                            return "";
                    }
                case "gb_record":
                    break;
                case "onvif":
                    break;
                case "server":
                    if ("restart".equals(keyItemArray[3])) {
                        return "重启流媒体服务";
                    }
                    break;
                case "proxy":
                    switch (keyItemArray[3]) {
                        case "save":
                            return "保存代理";
                        case "del":
                            return "移除代理";
                        case "start":
                            return "启用代理";
                        case "stop":
                            return "停用代理";
                        default:
                            return "";
                    }
                case "push":
                    switch (keyItemArray[3]) {
                        case "save_to_gb":
                            return "将推流添加到国标";
                        case "remove_form_gb":
                            return "将推流移出到国标";
                        default:
                            return "";
                    }
                case "user":
                    switch (keyItemArray[3]) {
                        case "login":
                            return "登录";
                        case "changePassword":
                            return "修改密码";
                        case "add":
                            return "添加用户";
                        case "delete":
                            return "删除用户";
                        default:
                            return "";
                    }
                default:
                    return "";
            }
        }
        return null;
    }
}
src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java
New file
@@ -0,0 +1,359 @@
package com.genersoft.iot.vmp.common;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "流信息")
public class StreamInfo {
    @Schema(description = "应用名")
    private String app;
    @Schema(description = "流ID")
    private String stream;
    @Schema(description = "设备编号")
    private String deviceID;
    @Schema(description = "通道编号")
    private String channelId;
    @Schema(description = "HTTP-FLV流地址")
    private String flv;
    @Schema(description = "IP")
    private String ip;
    @Schema(description = "HTTPS-FLV流地址")
    private String https_flv;
    @Schema(description = "Websocket-FLV流地址")
    private String ws_flv;
    @Schema(description = "Websockets-FLV流地址")
    private String wss_flv;
    @Schema(description = "HTTP-FMP4流地址")
    private String fmp4;
    @Schema(description = "HTTPS-FMP4流地址")
    private String https_fmp4;
    @Schema(description = "Websocket-FMP4流地址")
    private String ws_fmp4;
    @Schema(description = "Websockets-FMP4流地址")
    private String wss_fmp4;
    @Schema(description = "HLS流地址")
    private String hls;
    @Schema(description = "HTTPS-HLS流地址")
    private String https_hls;
    @Schema(description = "Websocket-HLS流地址")
    private String ws_hls;
    @Schema(description = "Websockets-HLS流地址")
    private String wss_hls;
    @Schema(description = "HTTP-TS流地址")
    private String ts;
    @Schema(description = "HTTPS-TS流地址")
    private String https_ts;
    @Schema(description = "Websocket-TS流地址")
    private String ws_ts;
    @Schema(description = "Websockets-TS流地址")
    private String wss_ts;
    @Schema(description = "RTMP流地址")
    private String rtmp;
    @Schema(description = "RTMPS流地址")
    private String rtmps;
    @Schema(description = "RTSP流地址")
    private String rtsp;
    @Schema(description = "RTSPS流地址")
    private String rtsps;
    @Schema(description = "RTC流地址")
    private String rtc;
    @Schema(description = "RTCS流地址")
    private String rtcs;
    @Schema(description = "流媒体ID")
    private String mediaServerId;
    @Schema(description = "流编码信息")
    private Object tracks;
    @Schema(description = "开始时间")
    private String startTime;
    @Schema(description = "结束时间")
    private String endTime;
    @Schema(description = "进度(录像下载使用)")
    private double progress;
    @Schema(description = "是否暂停(录像回放使用)")
    private boolean pause;
    public static class TransactionInfo{
        public String callId;
        public String localTag;
        public String remoteTag;
        public String branch;
    }
    private TransactionInfo transactionInfo;
    public String getApp() {
        return app;
    }
    public void setApp(String app) {
        this.app = app;
    }
    public String getDeviceID() {
        return deviceID;
    }
    public void setDeviceID(String deviceID) {
        this.deviceID = deviceID;
    }
    public String getChannelId() {
        return channelId;
    }
    public void setChannelId(String channelId) {
        this.channelId = channelId;
    }
    public String getFlv() {
        return flv;
    }
    public void setFlv(String flv) {
        this.flv = flv;
    }
    public String getWs_flv() {
        return ws_flv;
    }
    public void setWs_flv(String ws_flv) {
        this.ws_flv = ws_flv;
    }
    public String getRtmp() {
        return rtmp;
    }
    public void setRtmp(String rtmp) {
        this.rtmp = rtmp;
    }
    public String getHls() {
        return hls;
    }
    public void setHls(String hls) {
        this.hls = hls;
    }
    public String getRtsp() {
        return rtsp;
    }
    public void setRtsp(String rtsp) {
        this.rtsp = rtsp;
    }
    public Object getTracks() {
        return tracks;
    }
    public void setTracks(Object tracks) {
        this.tracks = tracks;
    }
    public String getFmp4() {
        return fmp4;
    }
    public void setFmp4(String fmp4) {
        this.fmp4 = fmp4;
    }
    public String getWs_fmp4() {
        return ws_fmp4;
    }
    public void setWs_fmp4(String ws_fmp4) {
        this.ws_fmp4 = ws_fmp4;
    }
    public String getWs_hls() {
        return ws_hls;
    }
    public void setWs_hls(String ws_hls) {
        this.ws_hls = ws_hls;
    }
    public String getTs() {
        return ts;
    }
    public void setTs(String ts) {
        this.ts = ts;
    }
    public String getWs_ts() {
        return ws_ts;
    }
    public void setWs_ts(String ws_ts) {
        this.ws_ts = ws_ts;
    }
    public String getStream() {
        return stream;
    }
    public void setStream(String stream) {
        this.stream = stream;
    }
    public String getRtc() {
        return rtc;
    }
    public void setRtc(String rtc) {
        this.rtc = rtc;
    }
    public TransactionInfo getTransactionInfo() {
        return transactionInfo;
    }
    public void setTransactionInfo(TransactionInfo transactionInfo) {
        this.transactionInfo = transactionInfo;
    }
    public String getMediaServerId() {
        return mediaServerId;
    }
    public void setMediaServerId(String mediaServerId) {
        this.mediaServerId = mediaServerId;
    }
    public String getHttps_flv() {
        return https_flv;
    }
    public void setHttps_flv(String https_flv) {
        this.https_flv = https_flv;
    }
    public String getWss_flv() {
        return wss_flv;
    }
    public void setWss_flv(String wss_flv) {
        this.wss_flv = wss_flv;
    }
    public String getWss_fmp4() {
        return wss_fmp4;
    }
    public void setWss_fmp4(String wss_fmp4) {
        this.wss_fmp4 = wss_fmp4;
    }
    public String getWss_hls() {
        return wss_hls;
    }
    public void setWss_hls(String wss_hls) {
        this.wss_hls = wss_hls;
    }
    public String getWss_ts() {
        return wss_ts;
    }
    public void setWss_ts(String wss_ts) {
        this.wss_ts = wss_ts;
    }
    public String getRtmps() {
        return rtmps;
    }
    public void setRtmps(String rtmps) {
        this.rtmps = rtmps;
    }
    public String getRtsps() {
        return rtsps;
    }
    public void setRtsps(String rtsps) {
        this.rtsps = rtsps;
    }
    public String getHttps_hls() {
        return https_hls;
    }
    public void setHttps_hls(String https_hls) {
        this.https_hls = https_hls;
    }
    public String getHttps_fmp4() {
        return https_fmp4;
    }
    public void setHttps_fmp4(String https_fmp4) {
        this.https_fmp4 = https_fmp4;
    }
    public String getHttps_ts() {
        return https_ts;
    }
    public void setHttps_ts(String https_ts) {
        this.https_ts = https_ts;
    }
    public String getStartTime() {
        return startTime;
    }
    public void setStartTime(String startTime) {
        this.startTime = startTime;
    }
    public String getEndTime() {
        return endTime;
    }
    public void setEndTime(String endTime) {
        this.endTime = endTime;
    }
    public double getProgress() {
        return progress;
    }
    public void setProgress(double progress) {
        this.progress = progress;
    }
    public String getIp() {
        return ip;
    }
    public void setIp(String ip) {
        this.ip = ip;
    }
    public String getRtcs() {
        return rtcs;
    }
    public void setRtcs(String rtcs) {
        this.rtcs = rtcs;
    }
    public boolean isPause() {
        return pause;
    }
    public void setPause(boolean pause) {
        this.pause = pause;
    }
}
src/main/java/com/genersoft/iot/vmp/common/SystemInfoDto.java
New file
@@ -0,0 +1,22 @@
package com.genersoft.iot.vmp.common;
public class SystemInfoDto<T> {
    private String time;
    private T data;
    public String getTime() {
        return time;
    }
    public void setTime(String time) {
        this.time = time;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}
src/main/java/com/genersoft/iot/vmp/common/VersionPo.java
New file
@@ -0,0 +1,136 @@
package com.genersoft.iot.vmp.common;
import com.alibaba.fastjson.annotation.JSONField;
public class VersionPo {
    /**
     * git的全版本号
     */
    @JSONField(name="GIT-Revision")
    private String GIT_Revision;
    /**
     * maven版本
     */
    @JSONField(name = "Create-By")
    private String Create_By;
    /**
     * git的分支
     */
    @JSONField(name = "GIT-BRANCH")
    private String GIT_BRANCH;
    /**
     * git的url
     */
    @JSONField(name = "GIT-URL")
    private String GIT_URL;
    /**
     * 构建日期
     */
    @JSONField(name = "BUILD-DATE")
    private String BUILD_DATE;
    /**
     * 项目名称 配合pom使用
     */
    @JSONField(name = "artifactId")
    private String artifactId;
    /**
     * git局部版本号
     */
    @JSONField(name = "GIT-Revision-SHORT")
    private String GIT_Revision_SHORT;
    /**
     * 项目的版本如2.0.1.0 配合pom使用
     */
    @JSONField(name = "version")
    private String version;
    /**
     * 子系统名称
     */
    @JSONField(name = "project")
    private String project;
    /**
     * jdk版本
     */
    @JSONField(name="Build_Jdk")
    private String Build_Jdk;
    public void setGIT_Revision(String GIT_Revision) {
        this.GIT_Revision = GIT_Revision;
    }
    public void setCreate_By(String create_By) {
        Create_By = create_By;
    }
    public void setGIT_BRANCH(String GIT_BRANCH) {
        this.GIT_BRANCH = GIT_BRANCH;
    }
    public void setGIT_URL(String GIT_URL) {
        this.GIT_URL = GIT_URL;
    }
    public void setBUILD_DATE(String BUILD_DATE) {
        this.BUILD_DATE = BUILD_DATE;
    }
    public void setArtifactId(String artifactId) {
        this.artifactId = artifactId;
    }
    public void setGIT_Revision_SHORT(String GIT_Revision_SHORT) {
        this.GIT_Revision_SHORT = GIT_Revision_SHORT;
    }
    public void setVersion(String version) {
        this.version = version;
    }
    public void setProject(String project) {
        this.project = project;
    }
    public void setBuild_Jdk(String build_Jdk) {
        Build_Jdk = build_Jdk;
    }
    public String getGIT_Revision() {
        return GIT_Revision;
    }
    public String getCreate_By() {
        return Create_By;
    }
    public String getGIT_BRANCH() {
        return GIT_BRANCH;
    }
    public String getGIT_URL() {
        return GIT_URL;
    }
    public String getBUILD_DATE() {
        return BUILD_DATE;
    }
    public String getArtifactId() {
        return artifactId;
    }
    public String getGIT_Revision_SHORT() {
        return GIT_Revision_SHORT;
    }
    public String getVersion() {
        return version;
    }
    public String getProject() {
        return project;
    }
    public String getBuild_Jdk() {
        return Build_Jdk;
    }
}
src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java
New file
@@ -0,0 +1,139 @@
package com.genersoft.iot.vmp.common;
/**
 * @description: 定义常量
 * @author: swwheihei
 * @date:   2019年5月30日 下午3:04:04
 *
 */
public class VideoManagerConstants {
    public static final String WVP_SERVER_PREFIX = "VMP_SIGNALLING_SERVER_INFO_";
    public static final String WVP_SERVER_STREAM_PREFIX = "VMP_SIGNALLING_STREAM_";
    public static final String MEDIA_SERVER_PREFIX = "VMP_MEDIA_SERVER_";
    public static final String MEDIA_SERVERS_ONLINE_PREFIX = "VMP_MEDIA_ONLINE_SERVERS_";
    public static final String MEDIA_STREAM_PREFIX = "VMP_MEDIA_STREAM";
    public static final String DEVICE_PREFIX = "VMP_DEVICE_";
    // 设备同步完成
    public static final String DEVICE_SYNC_PREFIX = "VMP_DEVICE_SYNC_";
    public static final String CACHEKEY_PREFIX = "VMP_CHANNEL_";
    public static final String KEEPLIVEKEY_PREFIX = "VMP_KEEPALIVE_";
    // 此处多了一个_,暂不修改
    public static final String PLAYER_PREFIX = "VMP_PLAYER_";
    public static final String PLAY_BLACK_PREFIX = "VMP_PLAYBACK_";
    public static final String PLAY_INFO_PREFIX = "VMP_PLAY_INFO_";
    public static final String DOWNLOAD_PREFIX = "VMP_DOWNLOAD_";
    public static final String PLATFORM_KEEPALIVE_PREFIX = "VMP_PLATFORM_KEEPALIVE_";
    public static final String PLATFORM_CATCH_PREFIX = "VMP_PLATFORM_CATCH_";
    public static final String PLATFORM_REGISTER_PREFIX = "VMP_PLATFORM_REGISTER_";
    public static final String PLATFORM_REGISTER_INFO_PREFIX = "VMP_PLATFORM_REGISTER_INFO_";
    public static final String PLATFORM_SEND_RTP_INFO_PREFIX = "VMP_PLATFORM_SEND_RTP_INFO_";
    public static final String EVENT_ONLINE_REGISTER = "1";
    public static final String EVENT_ONLINE_MESSAGE = "3";
    public static final String EVENT_OUTLINE_UNREGISTER = "1";
    public static final String EVENT_OUTLINE_TIMEOUT = "2";
    public static final String MEDIA_SSRC_USED_PREFIX = "VMP_MEDIA_USED_SSRC_";
    public static final String MEDIA_TRANSACTION_USED_PREFIX = "VMP_MEDIA_TRANSACTION_";
    public static final String MEDIA_STREAM_AUTHORITY = "MEDIA_STREAM_AUTHORITY_";
    public static final String SIP_CSEQ_PREFIX = "VMP_SIP_CSEQ_";
    public static final String SIP_SN_PREFIX = "VMP_SIP_SN_";
    public static final String SIP_SUBSCRIBE_PREFIX = "VMP_SIP_SUBSCRIBE_";
    public static final String SYSTEM_INFO_CPU_PREFIX = "VMP_SYSTEM_INFO_CPU_";
    public static final String SYSTEM_INFO_MEM_PREFIX = "VMP_SYSTEM_INFO_MEM_";
    public static final String SYSTEM_INFO_NET_PREFIX = "VMP_SYSTEM_INFO_NET_";
    //************************** redis 消息*********************************
    /**
     * 流变化的通知
     */
    public static final String WVP_MSG_STREAM_CHANGE_PREFIX = "WVP_MSG_STREAM_CHANGE_";
    /**
     * 接收推流设备的GPS变化通知
     */
    public static final String VM_MSG_GPS = "VM_MSG_GPS";
    /**
     * 接收推流设备的GPS变化通知
     */
    public static final String VM_MSG_PUSH_STREAM_STATUS_CHANGE = "VM_MSG_PUSH_STREAM_STATUS_CHANGE";
    /**
     * 接收推流设备列表更新变化通知
     */
    public static final String VM_MSG_PUSH_STREAM_LIST_CHANGE = "VM_MSG_PUSH_STREAM_LIST_CHANGE";
    /**
     * redis 消息通知设备推流到平台
     */
    public static final String VM_MSG_STREAM_PUSH_REQUESTED = "VM_MSG_STREAM_PUSH_REQUESTED";
    /**
     * redis 消息通知平台通知设备推流结果
     */
    public static final String VM_MSG_STREAM_PUSH_RESPONSE = "VM_MSG_STREAM_PUSH_RESPONSE";
    /**
     * redis 消息请求所有的在线通道
     */
    public static final String VM_MSG_GET_ALL_ONLINE_REQUESTED = "VM_MSG_GET_ALL_ONLINE_REQUESTED";
    /**
     * 移动位置订阅通知
     */
    public static final String VM_MSG_SUBSCRIBE_MOBILE_POSITION = "mobileposition";
    /**
     * 报警订阅的通知(收到报警向redis发出通知)
     */
    public static final String VM_MSG_SUBSCRIBE_ALARM = "alarm";
    /**
     * 报警通知的发送 (收到redis发出的通知,转发给其他平台)
     */
    public static final String VM_MSG_SUBSCRIBE_ALARM_RECEIVE= "alarm_receive";
    /**
     * 设备状态订阅的通知
     */
    public static final String VM_MSG_SUBSCRIBE_DEVICE_STATUS = "device";
    //**************************    第三方  ****************************************
    public static final String WVP_STREAM_GB_ID_PREFIX = "memberNo_";
    public static final String WVP_STREAM_GPS_MSG_PREFIX = "WVP_STREAM_GPS_MSG_";
}
src/main/java/com/genersoft/iot/vmp/conf/ApiAccessFilter.java
New file
@@ -0,0 +1,116 @@
package com.genersoft.iot.vmp.conf;
import com.genersoft.iot.vmp.common.ApiSaveConstant;
import com.genersoft.iot.vmp.conf.security.SecurityUtils;
import com.genersoft.iot.vmp.service.ILogService;
import com.genersoft.iot.vmp.storager.dao.dto.LogDto;
import com.genersoft.iot.vmp.utils.DateUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * @author lin
 */
@WebFilter(filterName = "ApiAccessFilter", urlPatterns = "/api/*", asyncSupported=true)
public class ApiAccessFilter extends OncePerRequestFilter {
    private final static Logger logger = LoggerFactory.getLogger(ApiAccessFilter.class);
    @Autowired
    private UserSetting userSetting;
    @Autowired
    private ILogService logService;
    @Override
    protected void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
        String username = null;
        if (SecurityUtils.getUserInfo() == null) {
            username = servletRequest.getParameter("username");
        }else {
            username = SecurityUtils.getUserInfo().getUsername();
        }
        long start = System.currentTimeMillis(); // 请求进入时间
        String uriName = ApiSaveConstant.getVal(servletRequest.getRequestURI());
        filterChain.doFilter(servletRequest, servletResponse);
        if (uriName != null && userSetting.getLogInDatebase()) {
            LogDto logDto = new LogDto();
            logDto.setName(uriName);
            logDto.setUsername(username);
            logDto.setAddress(servletRequest.getRemoteAddr());
            logDto.setResult(HttpStatus.valueOf(servletResponse.getStatus()).toString());
            logDto.setTiming(System.currentTimeMillis() - start);
            logDto.setType(servletRequest.getMethod());
            logDto.setUri(servletRequest.getRequestURI());
            logDto.setCreateTime(DateUtil.getNow());
            logService.add(logDto);
//            logger.warn("[Api Access]  [{}] [{}] [{}] [{}] [{}] {}ms",
//                    uriName, servletRequest.getMethod(), servletRequest.getRequestURI(), servletRequest.getRemoteAddr(), HttpStatus.valueOf(servletResponse.getStatus()),
//                    System.currentTimeMillis() - start);
        }
    }
    /**
     * 获取IP地址
     *
     * @param request 请求
     * @return request发起客户端的IP地址
     */
    private String getIP(HttpServletRequest request) {
        if (request == null) {
            return "0.0.0.0";
        }
        String Xip = request.getHeader("X-Real-IP");
        String XFor = request.getHeader("X-Forwarded-For");
        String UNKNOWN_IP = "unknown";
        if (StringUtils.isNotEmpty(XFor) && !UNKNOWN_IP.equalsIgnoreCase(XFor)) {
            //多次反向代理后会有多个ip值,第一个ip才是真实ip
            int index = XFor.indexOf(",");
            if (index != -1) {
                return XFor.substring(0, index);
            } else {
                return XFor;
            }
        }
        XFor = Xip;
        if (StringUtils.isNotEmpty(XFor) && !UNKNOWN_IP.equalsIgnoreCase(XFor)) {
            return XFor;
        }
        if (StringUtils.isBlank(XFor) || UNKNOWN_IP.equalsIgnoreCase(XFor)) {
            XFor = request.getHeader("Proxy-Client-IP");
        }
        if (StringUtils.isBlank(XFor) || UNKNOWN_IP.equalsIgnoreCase(XFor)) {
            XFor = request.getHeader("WL-Proxy-Client-IP");
        }
        if (StringUtils.isBlank(XFor) || UNKNOWN_IP.equalsIgnoreCase(XFor)) {
            XFor = request.getHeader("HTTP_CLIENT_IP");
        }
        if (StringUtils.isBlank(XFor) || UNKNOWN_IP.equalsIgnoreCase(XFor)) {
            XFor = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (StringUtils.isBlank(XFor) || UNKNOWN_IP.equalsIgnoreCase(XFor)) {
            XFor = request.getRemoteAddr();
        }
        return XFor;
    }
}
src/main/java/com/genersoft/iot/vmp/conf/DynamicTask.java
New file
@@ -0,0 +1,138 @@
package com.genersoft.iot.vmp.conf;
import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.time.Instant;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
 * 动态定时任务
 * @author lin
 */
@Component
public class DynamicTask {
    private final Logger logger = LoggerFactory.getLogger(DynamicTask.class);
    private ThreadPoolTaskScheduler threadPoolTaskScheduler;
    private final Map<String, ScheduledFuture<?>> futureMap = new ConcurrentHashMap<>();
    private final Map<String, Runnable> runnableMap = new ConcurrentHashMap<>();
    @PostConstruct
    public void DynamicTask() {
        threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        threadPoolTaskScheduler.setPoolSize(300);
        threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true);
        threadPoolTaskScheduler.setAwaitTerminationSeconds(10);
        threadPoolTaskScheduler.initialize();
    }
    /**
     * 循环执行的任务
     * @param key 任务ID
     * @param task 任务
     * @param cycleForCatalog 间隔 毫秒
     * @return
     */
    public void startCron(String key, Runnable task, int cycleForCatalog) {
        ScheduledFuture<?> future = futureMap.get(key);
        if (future != null) {
            if (future.isCancelled()) {
                logger.debug("任务【{}】已存在但是关闭状态!!!", key);
            } else {
                logger.debug("任务【{}】已存在且已启动!!!", key);
                return;
            }
        }
        // scheduleWithFixedDelay 必须等待上一个任务结束才开始计时period, cycleForCatalog表示执行的间隔
        future = threadPoolTaskScheduler.scheduleAtFixedRate(task, cycleForCatalog);
        if (future != null){
            futureMap.put(key, future);
            runnableMap.put(key, task);
            logger.debug("任务【{}】启动成功!!!", key);
        }else {
            logger.debug("任务【{}】启动失败!!!", key);
        }
    }
    /**
     * 延时任务
     * @param key 任务ID
     * @param task 任务
     * @param delay 延时 /毫秒
     * @return
     */
    public void startDelay(String key, Runnable task, int delay) {
        stop(key);
        // 获取执行的时刻
        Instant startInstant = Instant.now().plusMillis(TimeUnit.MILLISECONDS.toMillis(delay));
        ScheduledFuture future = futureMap.get(key);
        if (future != null) {
            if (future.isCancelled()) {
                logger.debug("任务【{}】已存在但是关闭状态!!!", key);
            } else {
                logger.debug("任务【{}】已存在且已启动!!!", key);
                return;
            }
        }
        // scheduleWithFixedDelay 必须等待上一个任务结束才开始计时period, cycleForCatalog表示执行的间隔
        future = threadPoolTaskScheduler.schedule(task, startInstant);
        if (future != null){
            futureMap.put(key, future);
            runnableMap.put(key, task);
            logger.debug("任务【{}】启动成功!!!", key);
        }else {
            logger.debug("任务【{}】启动失败!!!", key);
        }
    }
    public void stop(String key) {
        if (futureMap.get(key) != null && !futureMap.get(key).isCancelled()) {
            futureMap.get(key).cancel(false);
            futureMap.remove(key);
            runnableMap.remove(key);
        }
    }
    public boolean contains(String key) {
        return futureMap.get(key) != null;
    }
    public Set<String> getAllKeys() {
        return futureMap.keySet();
    }
    public Runnable get(String key) {
        return runnableMap.get(key);
    }
    /**
     * 每五分钟检查失效的任务,并移除
     */
    @Scheduled(cron="0 0/5 * * * ?")
    public void execute(){
        if (futureMap.size() > 0) {
            for (String key : futureMap.keySet()) {
                if (futureMap.get(key).isDone()) {
                    futureMap.remove(key);
                    runnableMap.remove(key);
                }
            }
        }
    }
}
src/main/java/com/genersoft/iot/vmp/conf/GlobalExceptionHandler.java
New file
@@ -0,0 +1,57 @@
package com.genersoft.iot.vmp.conf;
import com.genersoft.iot.vmp.conf.exception.ControllerException;
import com.genersoft.iot.vmp.gb28181.event.alarm.AlarmEventListener;
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
 * 全局异常处理
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    private final static Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    /**
     * 默认异常处理
     * @param e 异常
     * @return 统一返回结果
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public WVPResult<String> exceptionHandler(Exception e) {
        logger.error("[全局异常]: ", e);
        return WVPResult.fail(ErrorCode.ERROR500.getCode(), e.getMessage());
    }
    /**
     * 自定义异常处理, 处理controller中返回的错误
     * @param e 异常
     * @return 统一返回结果
     */
    @ExceptionHandler(ControllerException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public WVPResult<String> exceptionHandler(ControllerException e) {
        return WVPResult.fail(e.getCode(), e.getMsg());
    }
    /**
     * 登陆失败
     * @param e 异常
     * @return 统一返回结果
     */
    @ExceptionHandler(BadCredentialsException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public WVPResult<String> exceptionHandler(BadCredentialsException e) {
        return WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMessage());
    }
}
src/main/java/com/genersoft/iot/vmp/conf/GlobalResponseAdvice.java
New file
@@ -0,0 +1,69 @@
package com.genersoft.iot.vmp.conf;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
import org.jetbrains.annotations.NotNull;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.List;
/**
 * 全局统一返回结果
 * @author lin
 */
@RestControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(@NotNull MethodParameter returnType, @NotNull Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }
    @Override
    public Object beforeBodyWrite(Object body, @NotNull MethodParameter returnType, @NotNull MediaType selectedContentType, @NotNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response) {
        // 排除api文档的接口,这个接口不需要统一
        String[] excludePath = {"/v3/api-docs","/api/v1","/index/hook"};
        for (String path : excludePath) {
            if (request.getURI().getPath().startsWith(path)) {
                return body;
            }
        }
        if (body instanceof WVPResult) {
            return body;
        }
        if (body instanceof ErrorCode) {
            ErrorCode errorCode = (ErrorCode) body;
            return new WVPResult<>(errorCode.getCode(), errorCode.getMsg(), null);
        }
        if (body instanceof String) {
            return JSON.toJSONString(WVPResult.success(body));
        }
        return WVPResult.success(body);
    }
    /**
     * 防止返回string时出错
     * @return
     */
    @Bean
    public HttpMessageConverters custHttpMessageConverter() {
        return new HttpMessageConverters(new FastJsonHttpMessageConverter());
    }
}
src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java
New file
@@ -0,0 +1,234 @@
package com.genersoft.iot.vmp.conf;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.utils.DateUtil;
import com.genersoft.iot.vmp.vmanager.gb28181.device.DeviceQuery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.regex.Pattern;
@Configuration("mediaConfig")
public class MediaConfig{
    private final static Logger logger = LoggerFactory.getLogger(MediaConfig.class);
    // 修改必须配置,不再支持自动获取
    @Value("${media.id}")
    private String id;
    @Value("${media.ip}")
    private String ip;
    @Value("${media.hook-ip:${sip.ip}}")
    private String hookIp;
    @Value("${sip.ip}")
    private String sipIp;
    @Value("${sip.domain}")
    private String sipDomain;
    @Value("${media.sdp-ip:${media.ip}}")
    private String sdpIp;
    @Value("${media.stream-ip:${media.ip}}")
    private String streamIp;
    @Value("${media.http-port}")
    private Integer httpPort;
    @Value("${media.http-ssl-port:0}")
    private Integer httpSSlPort = 0;
    @Value("${media.rtmp-port:0}")
    private Integer rtmpPort = 0;
    @Value("${media.rtmp-ssl-port:0}")
    private Integer rtmpSSlPort = 0;
    @Value("${media.rtp-proxy-port:0}")
    private Integer rtpProxyPort = 0;
    @Value("${media.rtsp-port:0}")
    private Integer rtspPort = 0;
    @Value("${media.rtsp-ssl-port:0}")
    private Integer rtspSSLPort = 0;
    @Value("${media.auto-config:true}")
    private boolean autoConfig = true;
    @Value("${media.secret}")
    private String secret;
    @Value("${media.rtp.enable}")
    private boolean rtpEnable;
    @Value("${media.rtp.port-range}")
    private String rtpPortRange;
    @Value("${media.rtp.send-port-range}")
    private String sendRtpPortRange;
    @Value("${media.record-assist-port:0}")
    private Integer recordAssistPort = 0;
    public String getId() {
        return id;
    }
    public String getIp() {
        return ip;
    }
    public String getHookIp() {
        if (ObjectUtils.isEmpty(hookIp)){
            return sipIp;
        }else {
            return hookIp;
        }
    }
    public String getSipIp() {
        if (sipIp == null) {
            return this.ip;
        }else {
            return sipIp;
        }
    }
    public int getHttpPort() {
        return httpPort;
    }
    public int getHttpSSlPort() {
        return httpSSlPort;
    }
    public int getRtmpPort() {
        return rtmpPort;
    }
    public int getRtmpSSlPort() {
        return rtmpSSlPort;
    }
    public int getRtpProxyPort() {
        if (rtpProxyPort == null) {
            return 0;
        }else {
            return rtpProxyPort;
        }
    }
    public int getRtspPort() {
        return rtspPort;
    }
    public int getRtspSSLPort() {
        return rtspSSLPort;
    }
    public boolean isAutoConfig() {
        return autoConfig;
    }
    public String getSecret() {
        return secret;
    }
    public boolean isRtpEnable() {
        return rtpEnable;
    }
    public String getRtpPortRange() {
        return rtpPortRange;
    }
    public int getRecordAssistPort() {
        return recordAssistPort;
    }
    public String getSdpIp() {
        if (ObjectUtils.isEmpty(sdpIp)){
            return ip;
        }else {
            if (isValidIPAddress(sdpIp)) {
                return sdpIp;
            }else {
                // 按照域名解析
                String hostAddress = null;
                try {
                    hostAddress = InetAddress.getByName(sdpIp).getHostAddress();
                } catch (UnknownHostException e) {
                    logger.error("[获取SDP IP]: 域名解析失败");
                }
                return hostAddress;
            }
        }
    }
    public String getStreamIp() {
        if (ObjectUtils.isEmpty(streamIp)){
            return ip;
        }else {
            return streamIp;
        }
    }
    public String getSipDomain() {
        return sipDomain;
    }
    public String getSendRtpPortRange() {
        return sendRtpPortRange;
    }
    public MediaServerItem getMediaSerItem(){
        MediaServerItem mediaServerItem = new MediaServerItem();
        mediaServerItem.setId(id);
        mediaServerItem.setIp(ip);
        mediaServerItem.setDefaultServer(true);
        mediaServerItem.setHookIp(getHookIp());
        mediaServerItem.setSdpIp(getSdpIp());
        mediaServerItem.setStreamIp(getStreamIp());
        mediaServerItem.setHttpPort(httpPort);
        mediaServerItem.setHttpSSlPort(httpSSlPort);
        mediaServerItem.setRtmpPort(rtmpPort);
        mediaServerItem.setRtmpSSlPort(rtmpSSlPort);
        mediaServerItem.setRtpProxyPort(getRtpProxyPort());
        mediaServerItem.setRtspPort(rtspPort);
        mediaServerItem.setRtspSSLPort(rtspSSLPort);
        mediaServerItem.setAutoConfig(autoConfig);
        mediaServerItem.setSecret(secret);
        mediaServerItem.setRtpEnable(rtpEnable);
        mediaServerItem.setRtpPortRange(rtpPortRange);
        mediaServerItem.setSendRtpPortRange(sendRtpPortRange);
        mediaServerItem.setRecordAssistPort(recordAssistPort);
        mediaServerItem.setHookAliveInterval(120);
        mediaServerItem.setCreateTime(DateUtil.getNow());
        mediaServerItem.setUpdateTime(DateUtil.getNow());
        return mediaServerItem;
    }
    private boolean isValidIPAddress(String ipAddress) {
        if ((ipAddress != null) && (!ipAddress.isEmpty())) {
            return Pattern.matches("^([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}$", ipAddress);
        }
        return false;
    }
}
src/main/java/com/genersoft/iot/vmp/conf/MediaStatusTimerTask.java
New file
@@ -0,0 +1,16 @@
package com.genersoft.iot.vmp.conf;
import com.alibaba.fastjson.JSONObject;
import org.springframework.scheduling.annotation.Scheduled;
/**
 * 定时向zlm同步媒体流状态
 */
public class MediaStatusTimerTask {
    @Scheduled(fixedRate = 2 * 1000)   //每3秒执行一次
    public void execute(){
    }
}
src/main/java/com/genersoft/iot/vmp/conf/ProxyServletConfig.java
New file
@@ -0,0 +1,276 @@
package com.genersoft.iot.vmp.conf;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.service.IMediaServerService;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.mitre.dsmiley.httpproxy.ProxyServlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.ConnectException;
/**
 * @author lin
 */
@SuppressWarnings(value = {"rawtypes", "unchecked"})
@Configuration
public class ProxyServletConfig {
    private final static Logger logger = LoggerFactory.getLogger(ProxyServletConfig.class);
    @Autowired
    private IMediaServerService mediaServerService;
    @Value("${server.port}")
    private int serverPort;
    @Bean
    public ServletRegistrationBean zlmServletRegistrationBean(){
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new ZlmProxyServlet(),"/zlm/*");
        servletRegistrationBean.setName("zlm_Proxy");
        servletRegistrationBean.addInitParameter("targetUri", "http://127.0.0.1:6080");
        servletRegistrationBean.addUrlMappings();
        if (logger.isDebugEnabled()) {
            servletRegistrationBean.addInitParameter("log", "true");
        }
        return servletRegistrationBean;
    }
    class ZlmProxyServlet extends ProxyServlet{
        @Override
        protected String rewriteQueryStringFromRequest(HttpServletRequest servletRequest, String queryString) {
            String queryStr = super.rewriteQueryStringFromRequest(servletRequest, queryString);
            MediaServerItem mediaInfo = getMediaInfoByUri(servletRequest.getRequestURI());
            if (mediaInfo != null) {
                if (!ObjectUtils.isEmpty(queryStr)) {
                    queryStr += "&secret=" + mediaInfo.getSecret();
                }else {
                    queryStr = "secret=" + mediaInfo.getSecret();
                }
            }
            return queryStr;
        }
        /**
         * 异常处理
         */
        @Override
        protected void handleRequestException(HttpRequest proxyRequest, HttpResponse proxyResonse, Exception e){
            try {
                super.handleRequestException(proxyRequest, proxyResonse, e);
            } catch (ServletException servletException) {
                logger.error("zlm 代理失败: ", e);
            } catch (IOException ioException) {
                if (ioException instanceof ConnectException) {
                    logger.error("zlm 连接失败");
                } else if (ioException instanceof ClientAbortException) {
                    logger.error("zlm: 用户已中断连接,代理终止");
                } else {
                    logger.error("zlm 代理失败: ", e);
                }
            } catch (RuntimeException exception){
                logger.error("zlm 代理失败: ", e);
            }
        }
        /**
         * 对于为按照格式请求的可以直接返回404
         */
        @Override
        protected String getTargetUri(HttpServletRequest servletRequest) {
            String requestURI = servletRequest.getRequestURI();
            MediaServerItem mediaInfo = getMediaInfoByUri(requestURI);
            String uri = null;
            if (mediaInfo != null) {
//                String realRequestURI = requestURI.substring(requestURI.indexOf(mediaInfo.getId())+ mediaInfo.getId().length());
                uri = String.format("http://%s:%s", mediaInfo.getIp(), mediaInfo.getHttpPort());
            }else {
                uri = "http://127.0.0.1:" + serverPort +"/index/hook/null"; // 只是一个能返回404的请求而已, 其他的也可以
            }
            return uri;
        }
        /**
         * 动态替换请求目标
         */
        @Override
        protected HttpHost getTargetHost(HttpServletRequest servletRequest) {
            String requestURI = servletRequest.getRequestURI();
            MediaServerItem mediaInfo = getMediaInfoByUri(requestURI);
            HttpHost host;
            if (mediaInfo != null) {
                host = new HttpHost(mediaInfo.getIp(), mediaInfo.getHttpPort());
            }else {
                host = new HttpHost("127.0.0.1", serverPort);
            }
            return host;
        }
        /**
         * 根据uri获取流媒体信息
         */
        MediaServerItem getMediaInfoByUri(String uri){
            String[] split = uri.split("/");
            String mediaServerId = split[2];
            if ("default".equalsIgnoreCase(mediaServerId)) {
                return mediaServerService.getDefaultMediaServer();
            }else {
                return mediaServerService.getOne(mediaServerId);
            }
        }
        /**
         * 去掉url中的标志信息
         */
        @Override
        protected String rewriteUrlFromRequest(HttpServletRequest servletRequest) {
            String requestURI = servletRequest.getRequestURI();
            MediaServerItem mediaInfo = getMediaInfoByUri(requestURI);
            String url = super.rewriteUrlFromRequest(servletRequest);
            if (mediaInfo == null) {
                logger.error("[ZLM服务访问代理],错误:处理url信息时未找到流媒体信息=>{}", requestURI);
                return  url;
            }
            if (!ObjectUtils.isEmpty(mediaInfo.getId())) {
                url = url.replace(mediaInfo.getId() + "/", "");
            }
            return url.replace("default/", "");
        }
    }
    @Bean
    public ServletRegistrationBean recordServletRegistrationBean(){
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new RecordProxyServlet(),"/record_proxy/*");
        servletRegistrationBean.setName("record_proxy");
        servletRegistrationBean.addInitParameter("targetUri", "http://127.0.0.1:18081");
        servletRegistrationBean.addUrlMappings();
        if (logger.isDebugEnabled()) {
            servletRegistrationBean.addInitParameter("log", "true");
        }
        return servletRegistrationBean;
    }
    class RecordProxyServlet extends ProxyServlet{
        @Override
        protected String rewriteQueryStringFromRequest(HttpServletRequest servletRequest, String queryString) {
            String queryStr = super.rewriteQueryStringFromRequest(servletRequest, queryString);
            MediaServerItem mediaInfo = getMediaInfoByUri(servletRequest.getRequestURI());
            String remoteHost = String.format("http://%s:%s", mediaInfo.getIp(), mediaInfo.getHttpPort());
            if (mediaInfo != null) {
                if (!ObjectUtils.isEmpty(queryStr)) {
                    queryStr += "&remoteHost=" + remoteHost;
                }else {
                    queryStr = "remoteHost=" + remoteHost;
                }
            }
            return queryStr;
        }
        /**
         * 异常处理
         */
        @Override
        protected void handleRequestException(HttpRequest proxyRequest, HttpResponse proxyResponse, Exception e){
            try {
                super.handleRequestException(proxyRequest, proxyResponse, e);
            } catch (ServletException servletException) {
                logger.error("录像服务 代理失败: ", e);
            } catch (IOException ioException) {
                if (ioException instanceof ConnectException) {
                    logger.error("录像服务 连接失败");
                } else if (ioException instanceof ClientAbortException) {
                    logger.error("录像服务:用户已中断连接,代理终止");
                } else {
                    logger.error("录像服务 代理失败: ", e);
                }
            } catch (RuntimeException exception){
                logger.error("录像服务 代理失败: ", e);
            }
        }
        /**
         * 对于为按照格式请求的可以直接返回404
         */
        @Override
        protected String getTargetUri(HttpServletRequest servletRequest) {
            String requestURI = servletRequest.getRequestURI();
            MediaServerItem mediaInfo = getMediaInfoByUri(requestURI);
            String uri = null;
            if (mediaInfo != null) {
//                String realRequestURI = requestURI.substring(requestURI.indexOf(mediaInfo.getId())+ mediaInfo.getId().length());
                uri = String.format("http://%s:%s", mediaInfo.getIp(), mediaInfo.getRecordAssistPort());
            }else {
                uri = "http://127.0.0.1:" + serverPort +"/index/hook/null"; // 只是一个能返回404的请求而已, 其他的也可以
            }
            return uri;
        }
        /**
         * 动态替换请求目标
         */
        @Override
        protected HttpHost getTargetHost(HttpServletRequest servletRequest) {
            String requestURI = servletRequest.getRequestURI();
            MediaServerItem mediaInfo = getMediaInfoByUri(requestURI);
            HttpHost host;
            if (mediaInfo != null) {
                host = new HttpHost(mediaInfo.getIp(), mediaInfo.getRecordAssistPort());
            }else {
                host = new HttpHost("127.0.0.1", serverPort);
            }
            return host;
        }
        /**
         * 根据uri获取流媒体信息
         */
        MediaServerItem getMediaInfoByUri(String uri){
            String[] split = uri.split("/");
            String mediaServerId = split[2];
            if ("default".equalsIgnoreCase(mediaServerId)) {
                return mediaServerService.getDefaultMediaServer();
            }else {
                return mediaServerService.getOne(mediaServerId);
            }
        }
        /**
         * 去掉url中的标志信息
         */
        @Override
        protected String rewriteUrlFromRequest(HttpServletRequest servletRequest) {
            String requestURI = servletRequest.getRequestURI();
            MediaServerItem mediaInfo = getMediaInfoByUri(requestURI);
            String url = super.rewriteUrlFromRequest(servletRequest);
            if (mediaInfo == null) {
                logger.error("[录像服务访问代理],错误:处理url信息时未找到流媒体信息=>{}", requestURI);
                return  url;
            }
            if (!ObjectUtils.isEmpty(mediaInfo.getId())) {
                url = url.replace(mediaInfo.getId() + "/", "");
            }
            return url.replace("default/", "");
        }
    }
}
src/main/java/com/genersoft/iot/vmp/conf/SipConfig.java
New file
@@ -0,0 +1,117 @@
package com.genersoft.iot.vmp.conf;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "sip", ignoreInvalidFields = true)
public class SipConfig {
    private String ip;
    /**
     * 默认使用 0.0.0.0
     */
    private String monitorIp = "0.0.0.0";
    private Integer port;
    private String domain;
    private String id;
    private String password;
    Integer ptzSpeed = 50;
    Integer keepaliveTimeOut = 255;
    Integer registerTimeInterval = 120;
    private boolean alarm;
    public void setIp(String ip) {
        this.ip = ip;
    }
    public void setMonitorIp(String monitorIp) {
        this.monitorIp = monitorIp;
    }
    public void setPort(Integer port) {
        this.port = port;
    }
    public void setDomain(String domain) {
        this.domain = domain;
    }
    public void setId(String id) {
        this.id = id;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public void setPtzSpeed(Integer ptzSpeed) {
        this.ptzSpeed = ptzSpeed;
    }
    public void setKeepaliveTimeOut(Integer keepaliveTimeOut) {
        this.keepaliveTimeOut = keepaliveTimeOut;
    }
    public void setRegisterTimeInterval(Integer registerTimeInterval) {
        this.registerTimeInterval = registerTimeInterval;
    }
    public String getMonitorIp() {
        return monitorIp;
    }
    public String getIp() {
        return ip;
    }
    public Integer getPort() {
        return port;
    }
    public String getDomain() {
        return domain;
    }
    public String getId() {
        return id;
    }
    public String getPassword() {
        return password;
    }
    public Integer getPtzSpeed() {
        return ptzSpeed;
    }
    public Integer getKeepaliveTimeOut() {
        return keepaliveTimeOut;
    }
    public Integer getRegisterTimeInterval() {
        return registerTimeInterval;
    }
    public boolean isAlarm() {
        return alarm;
    }
    public void setAlarm(boolean alarm) {
        this.alarm = alarm;
    }
}
src/main/java/com/genersoft/iot/vmp/conf/SipPlatformRunner.java
New file
@@ -0,0 +1,57 @@
package com.genersoft.iot.vmp.conf;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatformCatch;
import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform;
import com.genersoft.iot.vmp.service.IPlatformService;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.List;
/**
 * 系统启动时控制上级平台重新注册
 * @author lin
 */
@Component
@Order(value=3)
public class SipPlatformRunner implements CommandLineRunner {
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Autowired
    private IPlatformService platformService;
    @Autowired
    private ISIPCommanderForPlatform sipCommanderForPlatform;
    @Override
    public void run(String... args) throws Exception {
        // 获取所有启用的平台
        List<ParentPlatform> parentPlatforms = storager.queryEnableParentPlatformList(true);
        for (ParentPlatform parentPlatform : parentPlatforms) {
            // 更新缓存
            ParentPlatformCatch parentPlatformCatch = new ParentPlatformCatch();
            parentPlatformCatch.setParentPlatform(parentPlatform);
            parentPlatformCatch.setId(parentPlatform.getServerGBId());
            redisCatchStorage.updatePlatformCatchInfo(parentPlatformCatch);
            // 设置所有平台离线
            platformService.offline(parentPlatform);
            // 取消订阅
            sipCommanderForPlatform.unregister(parentPlatform, null, (eventResult)->{
                platformService.login(parentPlatform);
            });
        }
    }
}
src/main/java/com/genersoft/iot/vmp/conf/SpringDocConfig.java
New file
@@ -0,0 +1,89 @@
package com.genersoft.iot.vmp.conf;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.HeaderParameter;
import org.springdoc.core.GroupedOpenApi;
import org.springdoc.core.SpringDocConfigProperties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * @author lin
 */
@Configuration
public class SpringDocConfig {
    @Value("${doc.enabled: true}")
    private boolean enable;
    @Bean
    public OpenAPI springShopOpenApi() {
        Contact contact = new Contact();
        contact.setName("pan");
        contact.setEmail("648540858@qq.com");
        return new OpenAPI()
                .info(new Info().title("WVP-PRO 接口文档")
                        .contact(contact)
                        .description("开箱即用的28181协议视频平台")
                        .version("v2.0")
                        .license(new License().name("Apache 2.0").url("http://springdoc.org")));
    }
    /**
     * 添加分组
     * @return
     */
    @Bean
    public GroupedOpenApi publicApi() {
        return GroupedOpenApi.builder()
                .group("1. 全部")
                .packagesToScan("com.genersoft.iot.vmp.vmanager")
                .build();
    }
    @Bean
    public GroupedOpenApi publicApi2() {
        return GroupedOpenApi.builder()
                .group("2. 国标28181")
                .packagesToScan("com.genersoft.iot.vmp.vmanager.gb28181")
                .build();
    }
    @Bean
    public GroupedOpenApi publicApi3() {
        return GroupedOpenApi.builder()
                .group("3. 拉流转发")
                .packagesToScan("com.genersoft.iot.vmp.vmanager.streamProxy")
                .build();
    }
    @Bean
    public GroupedOpenApi publicApi4() {
        return GroupedOpenApi.builder()
                .group("4. 推流管理")
                .packagesToScan("com.genersoft.iot.vmp.vmanager.streamPush")
                .build();
    }
    @Bean
    public GroupedOpenApi publicApi5() {
        return GroupedOpenApi.builder()
                .group("4. 服务管理")
                .packagesToScan("com.genersoft.iot.vmp.vmanager.server")
                .build();
    }
    @Bean
    public GroupedOpenApi publicApi6() {
        return GroupedOpenApi.builder()
                .group("5. 用户管理")
                .packagesToScan("com.genersoft.iot.vmp.vmanager.user")
                .build();
    }
}
src/main/java/com/genersoft/iot/vmp/conf/SystemInfoTimerTask.java
New file
@@ -0,0 +1,39 @@
package com.genersoft.iot.vmp.conf;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd.AlarmQueryMessageHandler;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.utils.SystemInfoUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
 * 获取系统信息写入redis
 */
@Component
public class SystemInfoTimerTask {
    private Logger logger = LoggerFactory.getLogger(SystemInfoTimerTask.class);
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Scheduled(fixedRate = 1000)   //每1秒执行一次
    public void execute(){
        try {
            double cpuInfo = SystemInfoUtils.getCpuInfo();
            redisCatchStorage.addCpuInfo(cpuInfo);
            double memInfo = SystemInfoUtils.getMemInfo();
            redisCatchStorage.addMemInfo(memInfo);
            Map<String, String> networkInterfaces = SystemInfoUtils.getNetworkInterfaces();
            redisCatchStorage.addNetInfo(networkInterfaces);
        } catch (InterruptedException e) {
            logger.error("[获取系统信息失败] {}", e.getMessage());
        }
    }
}
src/main/java/com/genersoft/iot/vmp/conf/ThreadPoolTaskConfig.java
New file
@@ -0,0 +1,68 @@
package com.genersoft.iot.vmp.conf;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
 *  ThreadPoolTask 配置类
 * @author lin
 */
@Configuration
@EnableAsync(proxyTargetClass = true)
public class ThreadPoolTaskConfig {
    public static final int cpuNum = Runtime.getRuntime().availableProcessors();
    /**
     *   默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,
     *    当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
     *  当队列满了,就继续创建线程,当线程数量大于等于maxPoolSize后,开始使用拒绝策略拒绝
     */
    /**
     * 核心线程数(默认线程数)
     */
    private static final int corePoolSize = cpuNum;
    /**
     * 最大线程数
     */
    private static final int maxPoolSize = cpuNum*2;
    /**
     * 允许线程空闲时间(单位:默认为秒)
     */
    private static final int keepAliveTime = 30;
    /**
     * 缓冲队列大小
     */
    private static final int queueCapacity = 10000;
    /**
     * 线程池名前缀
     */
    private static final String threadNamePrefix = "wvp-";
    /**
     *
     * @return
     */
    @Bean("taskExecutor") // bean的名称,默认为首字母小写的方法名
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveTime);
        executor.setThreadNamePrefix(threadNamePrefix);
        // 线程池对拒绝任务的处理策略
        // CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 初始化
        executor.initialize();
        return executor;
    }
}
src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
New file
@@ -0,0 +1,159 @@
package com.genersoft.iot.vmp.conf;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
 * 配置文件 user-settings 映射的配置信息
 */
@Component
@ConfigurationProperties(prefix = "user-settings", ignoreInvalidFields = true)
public class UserSetting {
    private Boolean savePositionHistory = Boolean.FALSE;
    private Boolean autoApplyPlay = Boolean.FALSE;
    private Boolean seniorSdp = Boolean.FALSE;
    private Integer playTimeout = 18000;
    private int platformPlayTimeout = 60000;
    private Boolean interfaceAuthentication = Boolean.TRUE;
    private Boolean recordPushLive = Boolean.TRUE;
    private Boolean recordSip = Boolean.TRUE;
    private Boolean logInDatebase = Boolean.TRUE;
    private Boolean usePushingAsStatus = Boolean.TRUE;
    private Boolean streamOnDemand = Boolean.TRUE;
    private String serverId = "000000";
    private String thirdPartyGBIdReg = "[\\s\\S]*";
    private List<String> interfaceAuthenticationExcludes = new ArrayList<>();
    public Boolean getSavePositionHistory() {
        return savePositionHistory;
    }
    public Boolean isSavePositionHistory() {
        return savePositionHistory;
    }
    public Boolean isAutoApplyPlay() {
        return autoApplyPlay;
    }
    public Boolean isSeniorSdp() {
        return seniorSdp;
    }
    public Integer getPlayTimeout() {
        return playTimeout;
    }
    public Boolean isInterfaceAuthentication() {
        return interfaceAuthentication;
    }
    public Boolean isRecordPushLive() {
        return recordPushLive;
    }
    public List<String> getInterfaceAuthenticationExcludes() {
        return interfaceAuthenticationExcludes;
    }
    public void setSavePositionHistory(Boolean savePositionHistory) {
        this.savePositionHistory = savePositionHistory;
    }
    public void setAutoApplyPlay(Boolean autoApplyPlay) {
        this.autoApplyPlay = autoApplyPlay;
    }
    public void setSeniorSdp(Boolean seniorSdp) {
        this.seniorSdp = seniorSdp;
    }
    public void setPlayTimeout(Integer playTimeout) {
        this.playTimeout = playTimeout;
    }
    public void setInterfaceAuthentication(boolean interfaceAuthentication) {
        this.interfaceAuthentication = interfaceAuthentication;
    }
    public void setRecordPushLive(Boolean recordPushLive) {
        this.recordPushLive = recordPushLive;
    }
    public void setInterfaceAuthenticationExcludes(List<String> interfaceAuthenticationExcludes) {
        this.interfaceAuthenticationExcludes = interfaceAuthenticationExcludes;
    }
    public Boolean getLogInDatebase() {
        return logInDatebase;
    }
    public void setLogInDatebase(Boolean logInDatebase) {
        this.logInDatebase = logInDatebase;
    }
    public String getServerId() {
        return serverId;
    }
    public void setServerId(String serverId) {
        this.serverId = serverId;
    }
    public String getThirdPartyGBIdReg() {
        return thirdPartyGBIdReg;
    }
    public void setThirdPartyGBIdReg(String thirdPartyGBIdReg) {
        this.thirdPartyGBIdReg = thirdPartyGBIdReg;
    }
    public Boolean getRecordSip() {
        return recordSip;
    }
    public void setRecordSip(Boolean recordSip) {
        this.recordSip = recordSip;
    }
    public int getPlatformPlayTimeout() {
        return platformPlayTimeout;
    }
    public void setPlatformPlayTimeout(int platformPlayTimeout) {
        this.platformPlayTimeout = platformPlayTimeout;
    }
    public Boolean isUsePushingAsStatus() {
        return usePushingAsStatus;
    }
    public void setUsePushingAsStatus(Boolean usePushingAsStatus) {
        this.usePushingAsStatus = usePushingAsStatus;
    }
    public Boolean getStreamOnDemand() {
        return streamOnDemand;
    }
    public void setStreamOnDemand(Boolean streamOnDemand) {
        this.streamOnDemand = streamOnDemand;
    }
}
src/main/java/com/genersoft/iot/vmp/conf/VersionConfig.java
New file
@@ -0,0 +1,37 @@
package com.genersoft.iot.vmp.conf;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "version")
public class VersionConfig {
    private String version;
    private String artifactId;
    private String description;
    public void setVersion(String version) {
        this.version = version;
    }
    public void setArtifactId(String artifactId) {
        this.artifactId = artifactId;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public String getVersion() {
        return version;
    }
    public String getArtifactId() {
        return artifactId;
    }
    public String getDescription() {
        return description;
    }
}
src/main/java/com/genersoft/iot/vmp/conf/VersionInfo.java
New file
@@ -0,0 +1,25 @@
package com.genersoft.iot.vmp.conf;
import com.genersoft.iot.vmp.common.VersionPo;
import com.genersoft.iot.vmp.utils.GitUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class VersionInfo {
    @Autowired
    GitUtil gitUtil;
    public VersionPo getVersion() {
        VersionPo versionPo = new VersionPo();
        versionPo.setGIT_Revision(gitUtil.getGitCommitId());
        versionPo.setGIT_BRANCH(gitUtil.getBranch());
        versionPo.setGIT_URL(gitUtil.getGitUrl());
        versionPo.setBUILD_DATE(gitUtil.getBuildDate());
        versionPo.setGIT_Revision_SHORT(gitUtil.getCommitIdShort());
        versionPo.setVersion(gitUtil.getBuildVersion());
        return versionPo;
    }
}
src/main/java/com/genersoft/iot/vmp/conf/WVPTimerTask.java
New file
@@ -0,0 +1,33 @@
package com.genersoft.iot.vmp.conf;
import com.alibaba.fastjson.JSONObject;
import com.genersoft.iot.vmp.service.IMediaServerService;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class WVPTimerTask {
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Autowired
    private IMediaServerService mediaServerService;
    @Value("${server.port}")
    private int serverPort;
    @Autowired
    private SipConfig sipConfig;
    @Scheduled(fixedRate = 2 * 1000)   //每3秒执行一次
    public void execute(){
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("ip", sipConfig.getIp());
        jsonObject.put("port", serverPort);
        redisCatchStorage.updateWVPInfo(jsonObject, 3);
    }
}
src/main/java/com/genersoft/iot/vmp/conf/druid/DruidConfiguration.java
New file
@@ -0,0 +1,64 @@
package com.genersoft.iot.vmp.conf.druid;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import javax.servlet.Filter;
import javax.servlet.Servlet;
/**
 * druid监控配置
 * @author
 */
public class DruidConfiguration  {
    @Value("${rj-druid-manage.allow:127.0.0.1}")
    private String allow;
    @Value("${rj-druid-manage.deny:}")
    private String deny;
    @Value("${rj-druid-manage.loginUsername:admin}")
    private String loginUsername;
    @Value("${rj-druid-manage.loginPassword:admin}")
    private String loginPassword;
    @Value("${rj-druid-manage.resetEnable:false}")
    private String resetEnable;
    /**
     * druid监控页面开启
     */
    @Bean
    public ServletRegistrationBean druidServlet() {
        ServletRegistrationBean<Servlet> servletRegistrationBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
        // IP白名单
        servletRegistrationBean.addInitParameter("allow", allow);
        // IP黑名单(共同存在时,deny优先于allow)
        servletRegistrationBean.addInitParameter("deny", deny);
        //控制台管理用户
        servletRegistrationBean.addInitParameter("loginUsername", loginUsername);
        servletRegistrationBean.addInitParameter("loginPassword", loginPassword);
        //是否能够重置数据 禁用HTML页面上的“Reset All”功能
        servletRegistrationBean.addInitParameter("resetEnable", resetEnable);
        return servletRegistrationBean;
    }
    /**
     * druid url监控配置
     */
    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(new WebStatFilter());
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
        return filterRegistrationBean;
    }
}
src/main/java/com/genersoft/iot/vmp/conf/druid/EnableDruidSupport.java
New file
@@ -0,0 +1,24 @@
package com.genersoft.iot.vmp.conf.druid;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
 * druid监控支持注解
 *
 * @author
 * {@link DruidConfiguration} druid监控页面安全配置支持
 * {@link ServletComponentScan} druid监控页面需要扫描servlet
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({
        DruidConfiguration.class,
})
@ServletComponentScan
public @interface EnableDruidSupport {
}
src/main/java/com/genersoft/iot/vmp/conf/exception/ControllerException.java
New file
@@ -0,0 +1,37 @@
package com.genersoft.iot.vmp.conf.exception;
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
/**
 * 自定义异常,controller出现错误时直接抛出异常由全局异常捕获并返回结果
 */
public class ControllerException extends RuntimeException{
    private int code;
    private String msg;
    public ControllerException(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public ControllerException(ErrorCode errorCode) {
        this.code = errorCode.getCode();
        this.msg = errorCode.getMsg();
    }
    public int getCode() {
        return code;
    }
    public void setCode(int code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
}
src/main/java/com/genersoft/iot/vmp/conf/exception/ServiceException.java
New file
@@ -0,0 +1,27 @@
package com.genersoft.iot.vmp.conf.exception;
/**
 * @author lin
 */
public class ServiceException extends Exception{
    private String msg;
    public ServiceException(String msg) {
        this.msg = msg;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    @Override
    public String getMessage() {
        return msg;
    }
}
src/main/java/com/genersoft/iot/vmp/conf/exception/SsrcTransactionNotFoundException.java
New file
@@ -0,0 +1,49 @@
package com.genersoft.iot.vmp.conf.exception;
/**
 * @author lin
 */
public class SsrcTransactionNotFoundException extends Exception{
    private String deviceId;
    private String channelId;
    private String callId;
    private String stream;
    public SsrcTransactionNotFoundException(String deviceId, String channelId, String callId, String stream) {
        this.deviceId = deviceId;
        this.channelId = channelId;
        this.callId = callId;
        this.stream = stream;
    }
    public String getDeviceId() {
        return deviceId;
    }
    public String getChannelId() {
        return channelId;
    }
    public String getCallId() {
        return callId;
    }
    public String getStream() {
        return stream;
    }
    @Override
    public String getMessage() {
        StringBuffer msg = new StringBuffer();
        msg.append(String.format("缓存事务信息未找到,device:%s channel: %s ",  deviceId, channelId));
        if (callId != null) {
            msg.append(",callId: " + callId);
        }
        if (stream != null) {
            msg.append(",stream: " + stream);
        }
        return msg.toString();
    }
}
src/main/java/com/genersoft/iot/vmp/conf/redis/RedisConfig.java
New file
@@ -0,0 +1,89 @@
package com.genersoft.iot.vmp.conf.redis;
import com.alibaba.fastjson.parser.ParserConfig;
import com.genersoft.iot.vmp.common.VideoManagerConstants;
import com.genersoft.iot.vmp.service.redisMsg.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.genersoft.iot.vmp.utils.redis.FastJsonRedisSerializer;
/**
 * @description:Redis中间件配置类,使用spring-data-redis集成,自动从application.yml中加载redis配置
 * @author: swwheihei
 * @date: 2019年5月30日 上午10:58:25
 *
 */
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Autowired
    private RedisGpsMsgListener redisGPSMsgListener;
    @Autowired
    private RedisAlarmMsgListener redisAlarmMsgListener;
    @Autowired
    private RedisStreamMsgListener redisStreamMsgListener;
    @Autowired
    private RedisGbPlayMsgListener redisGbPlayMsgListener;
    @Autowired
    private RedisPushStreamStatusMsgListener redisPushStreamStatusMsgListener;
    @Autowired
    private RedisPushStreamStatusListMsgListener redisPushStreamListMsgListener;
    @Autowired
    private RedisPushStreamResponseListener redisPushStreamResponseListener;
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        // 使用fastJson序列化
        FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
        // value值的序列化采用fastJsonRedisSerializer
        redisTemplate.setValueSerializer(fastJsonRedisSerializer);
        redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
        // 全局开启AutoType,不建议使用
         ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        // key的序列化采用StringRedisSerializer
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
    /**
     * redis消息监听器容器 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器
     * 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理
     *
     * @param connectionFactory
     * @return
     */
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(redisGPSMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_GPS));
        container.addMessageListener(redisAlarmMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_SUBSCRIBE_ALARM_RECEIVE));
        container.addMessageListener(redisStreamMsgListener, new PatternTopic(VideoManagerConstants.WVP_MSG_STREAM_CHANGE_PREFIX + "PUSH"));
        container.addMessageListener(redisGbPlayMsgListener, new PatternTopic(RedisGbPlayMsgListener.WVP_PUSH_STREAM_KEY));
        container.addMessageListener(redisPushStreamStatusMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_PUSH_STREAM_STATUS_CHANGE));
        container.addMessageListener(redisPushStreamListMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_PUSH_STREAM_LIST_CHANGE));
        container.addMessageListener(redisPushStreamResponseListener, new PatternTopic(VideoManagerConstants.VM_MSG_STREAM_PUSH_RESPONSE));
        return container;
    }
}
src/main/java/com/genersoft/iot/vmp/conf/security/AnonymousAuthenticationEntryPoint.java
New file
@@ -0,0 +1,46 @@
package com.genersoft.iot.vmp.conf.security;
import com.alibaba.fastjson.JSONObject;
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
import org.apache.poi.hssf.eventmodel.ERFListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * 处理匿名用户访问逻辑
 * @author lin
 */
@Component
public class AnonymousAuthenticationEntryPoint implements AuthenticationEntryPoint {
    private final static Logger logger = LoggerFactory.getLogger(DefaultUserDetailsServiceImpl.class);
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
        // 允许跨域
        response.setHeader("Access-Control-Allow-Origin", "*");
        // 允许自定义请求头token(允许head跨域)
        response.setHeader("Access-Control-Allow-Headers", "token, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");
        response.setHeader("Content-type", "application/json;charset=UTF-8");
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", ErrorCode.ERROR401.getCode());
        jsonObject.put("msg", ErrorCode.ERROR401.getMsg());
        String logUri = "api/user/login";
        if (request.getRequestURI().contains(logUri)){
            jsonObject.put("msg", e.getMessage());
        }
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        try {
            response.getWriter().print(jsonObject.toJSONString());
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
    }
}
src/main/java/com/genersoft/iot/vmp/conf/security/DefaultUserDetailsServiceImpl.java
New file
@@ -0,0 +1,48 @@
package com.genersoft.iot.vmp.conf.security;
import java.time.LocalDateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import com.alibaba.excel.util.StringUtils;
import com.genersoft.iot.vmp.conf.security.dto.LoginUser;
import com.genersoft.iot.vmp.service.IUserService;
import com.genersoft.iot.vmp.storager.dao.dto.User;
/**
 * 用户登录认证逻辑
 */
@Component
public class DefaultUserDetailsServiceImpl implements UserDetailsService {
    private final static Logger logger = LoggerFactory.getLogger(DefaultUserDetailsServiceImpl.class);
    @Autowired
    private IUserService userService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StringUtils.isBlank(username)) {
            logger.info("登录用户:{} 不存在", username);
            throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
        }
        // 查出密码
        User user = userService.getUserByUsername(username);
        if (user == null) {
            logger.info("登录用户:{} 不存在", username);
            throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
        }
        String password = SecurityUtils.encryptPassword(user.getPassword());
        user.setPassword(password);
        return new LoginUser(user, LocalDateTime.now());
    }
}
src/main/java/com/genersoft/iot/vmp/conf/security/InvalidSessionHandler.java
New file
@@ -0,0 +1,24 @@
package com.genersoft.iot.vmp.conf.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.web.session.InvalidSessionStrategy;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * 登录超时的处理
 */
public class InvalidSessionHandler implements InvalidSessionStrategy {
    private final static Logger logger = LoggerFactory.getLogger(InvalidSessionHandler.class);
    @Override
    public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse httpServletResponse) throws IOException, ServletException {
        String username = request.getParameter("username");
        logger.info("[登录超时] - [{}]", username);
    }
}
src/main/java/com/genersoft/iot/vmp/conf/security/LoginFailureHandler.java
New file
@@ -0,0 +1,65 @@
package com.genersoft.iot.vmp.conf.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
    private final static Logger logger = LoggerFactory.getLogger(LoginFailureHandler.class);
    @Autowired
    private ObjectMapper objectMapper;
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        String username = request.getParameter("username");
        if (e instanceof AccountExpiredException) {
            // 账号过期
            logger.info("[登录失败] - 用户[{}]账号过期", username);
        } else if (e instanceof BadCredentialsException) {
            // 密码错误
            logger.info("[登录失败] - 用户[{}]密码/SIP服务器ID 错误", username);
        } else if (e instanceof CredentialsExpiredException) {
            // 密码过期
            logger.info("[登录失败] - 用户[{}]密码过期", username);
        } else if (e instanceof DisabledException) {
            // 用户被禁用
            logger.info("[登录失败] - 用户[{}]被禁用", username);
        } else if (e instanceof LockedException) {
            // 用户被锁定
            logger.info("[登录失败] - 用户[{}]被锁定", username);
        } else if (e instanceof InternalAuthenticationServiceException) {
            // 内部错误
            logger.error(String.format("[登录失败] - [%s]内部错误", username), e);
        } else {
            // 其他错误
            logger.error(String.format("[登录失败] - [%s]其他错误", username), e);
        }
        Map<String, Object> map = new HashMap<>();
        map.put("code","0");
        map.put("msg","登录失败");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(map));
    }
}
src/main/java/com/genersoft/iot/vmp/conf/security/LoginSuccessHandler.java
New file
@@ -0,0 +1,28 @@
package com.genersoft.iot.vmp.conf.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * @author lin
 */
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    private final static Logger logger = LoggerFactory.getLogger(LoginSuccessHandler.class);
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        String username = request.getParameter("username");
        logger.info("[登录成功] - [{}]", username);
    }
}
src/main/java/com/genersoft/iot/vmp/conf/security/LogoutHandler.java
New file
@@ -0,0 +1,27 @@
package com.genersoft.iot.vmp.conf.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * 退出登录成功
 */
@Component
public class LogoutHandler implements LogoutSuccessHandler {
    private final static Logger logger = LoggerFactory.getLogger(LogoutHandler.class);
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        String username = request.getParameter("username");
        logger.info("[退出登录成功] - [{}]", username);
    }
}
src/main/java/com/genersoft/iot/vmp/conf/security/SecurityUtils.java
New file
@@ -0,0 +1,78 @@
package com.genersoft.iot.vmp.conf.security;
import com.genersoft.iot.vmp.conf.security.dto.LoginUser;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import javax.security.sasl.AuthenticationException;
public class SecurityUtils {
    /**
     * 描述根据账号密码进行调用security进行认证授权 主动调
     * 用AuthenticationManager的authenticate方法实现
     * 授权成功后将用户信息存入SecurityContext当中
     * @param username 用户名
     * @param password 密码
     * @param authenticationManager 认证授权管理器,
     * @see  AuthenticationManager
     * @return UserInfo  用户信息
     */
    public static LoginUser login(String username, String password, AuthenticationManager authenticationManager) throws AuthenticationException {
        //使用security框架自带的验证token生成器  也可以自定义。
        UsernamePasswordAuthenticationToken token =new UsernamePasswordAuthenticationToken(username,password);
        Authentication authenticate = authenticationManager.authenticate(token);
        SecurityContextHolder.getContext().setAuthentication(authenticate);
        LoginUser user = (LoginUser) authenticate.getPrincipal();
        return user;
    }
    /**
     * 获取当前登录的所有认证信息
     * @return
     */
    public static Authentication getAuthentication(){
        SecurityContext context = SecurityContextHolder.getContext();
        return context.getAuthentication();
    }
    /**
     * 获取当前登录用户信息
     * @return
     */
    public static LoginUser getUserInfo(){
        Authentication authentication = getAuthentication();
        if(authentication!=null){
            Object principal = authentication.getPrincipal();
            if(principal!=null && !"anonymousUser".equals(principal)){
                LoginUser user = (LoginUser) authentication.getPrincipal();
                return user;
            }
        }
        return null;
    }
    /**
     * 获取当前登录用户ID
     * @return
     */
    public static int getUserId(){
        LoginUser user = getUserInfo();
        return user.getId();
    }
    /**
     * 生成BCryptPasswordEncoder密码
     *
     * @param password 密码
     * @return 加密字符串
     */
    public static String encryptPassword(String password) {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.encode(password);
    }
}
src/main/java/com/genersoft/iot/vmp/conf/security/UrlTokenHandler.java
New file
@@ -0,0 +1,24 @@
package com.genersoft.iot.vmp.conf.security;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.SessionCookieConfig;
import javax.servlet.SessionTrackingMode;
import java.util.Collections;
public class UrlTokenHandler extends SpringBootServletInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        super.onStartup(servletContext);
        servletContext.setSessionTrackingModes(
                Collections.singleton(SessionTrackingMode.COOKIE)
        );
        SessionCookieConfig sessionCookieConfig = servletContext.getSessionCookieConfig();
        sessionCookieConfig.setHttpOnly(true);
    }
}
src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java
New file
@@ -0,0 +1,181 @@
package com.genersoft.iot.vmp.conf.security;
import com.genersoft.iot.vmp.conf.UserSetting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.util.List;
/**
 * 配置Spring Security
 * @author lin
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    private final static Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);
    @Autowired
    private UserSetting userSetting;
    @Autowired
    private DefaultUserDetailsServiceImpl userDetailsService;
    /**
     * 登出成功的处理
     */
    @Autowired
    private LoginFailureHandler loginFailureHandler;
    /**
     * 登录成功的处理
     */
    @Autowired
    private LoginSuccessHandler loginSuccessHandler;
    /**
     * 登出成功的处理
     */
    @Autowired
    private LogoutHandler logoutHandler;
    /**
     * 未登录的处理
     */
    @Autowired
    private AnonymousAuthenticationEntryPoint anonymousAuthenticationEntryPoint;
//    /**
//     * 超时处理
//     */
//    @Autowired
//    private InvalidSessionHandler invalidSessionHandler;
//    /**
//     * 顶号处理
//     */
//    @Autowired
//    private SessionInformationExpiredHandler sessionInformationExpiredHandler;
//    /**
//     * 登录用户没有权限访问资源
//     */
//    @Autowired
//    private LoginUserAccessDeniedHandler accessDeniedHandler;
    /**
     * 描述: 静态资源放行,这里的放行,是不走 Spring Security 过滤器链
     **/
    @Override
    public void configure(WebSecurity web) {
        if (!userSetting.isInterfaceAuthentication()) {
            web.ignoring().antMatchers("**");
        }else {
            // 可以直接访问的静态数据
            web.ignoring()
                    .antMatchers("/")
                    .antMatchers("/#/**")
                    .antMatchers("/static/**")
                    .antMatchers("/index.html")
                    .antMatchers("/doc.html") // "/webjars/**", "/swagger-resources/**", "/v3/api-docs/**"
                    .antMatchers("/webjars/**")
                    .antMatchers("/swagger-resources/**")
                    .antMatchers("/v3/api-docs/**")
                    .antMatchers("/favicon.ico")
                    .antMatchers("/js/**");
            List<String> interfaceAuthenticationExcludes = userSetting.getInterfaceAuthenticationExcludes();
            for (String interfaceAuthenticationExclude : interfaceAuthenticationExcludes) {
                if (interfaceAuthenticationExclude.split("/").length < 4 ) {
                    logger.warn("{}不满足两级目录,已忽略", interfaceAuthenticationExclude);
                }else {
                    web.ignoring().antMatchers(interfaceAuthenticationExclude);
                }
            }
        }
    }
    /**
     * 配置认证方式
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        // 设置不隐藏 未找到用户异常
        provider.setHideUserNotFoundExceptions(true);
        // 用户认证service - 查询数据库的逻辑
        provider.setUserDetailsService(userDetailsService);
        // 设置密码加密算法
        provider.setPasswordEncoder(passwordEncoder());
        auth.authenticationProvider(provider);
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable();
        // 设置允许添加静态文件
        http.headers().contentTypeOptions().disable();
        http.authorizeRequests()
                // 放行接口
                .antMatchers("/api/user/login"
                        ,"/index/hook/**"
                        ,"/api/device/query/**"
                        ,"/api/play/start/**"
                        ,"/api/position/**"
                        ,"/api/position/history/**"
                ).permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                // 异常处理(权限拒绝、登录失效等)
                .and().exceptionHandling()
                //匿名用户访问无权限资源时的异常处理
                .authenticationEntryPoint(anonymousAuthenticationEntryPoint)
//                .accessDeniedHandler(accessDeniedHandler)//登录用户没有权限访问资源
                // 登入 允许所有用户
                .and().formLogin().permitAll()
                //登录成功处理逻辑
                .successHandler(loginSuccessHandler)
                //登录失败处理逻辑
                .failureHandler(loginFailureHandler)
                // 登出
                .and().logout().logoutUrl("/api/user/logout").permitAll()
                //登出成功处理逻辑
                .logoutSuccessHandler(logoutHandler)
                .deleteCookies("JSESSIONID")
                // 会话管理
//                .and().sessionManagement().invalidSessionStrategy(invalidSessionHandler) // 超时处理
//                .maximumSessions(1)//同一账号同时登录最大用户数
//                .expiredSessionStrategy(sessionInformationExpiredHandler) // 顶号处理
        ;
    }
    /**
     * 描述: 密码加密算法 BCrypt 推荐使用
     **/
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    /**
     * 描述: 注入AuthenticationManager管理器
     **/
    @Override
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
}
src/main/java/com/genersoft/iot/vmp/conf/security/dto/LoginUser.java
New file
@@ -0,0 +1,102 @@
package com.genersoft.iot.vmp.conf.security.dto;
import com.genersoft.iot.vmp.storager.dao.dto.Role;
import com.genersoft.iot.vmp.storager.dao.dto.User;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.userdetails.UserDetails;
import java.time.LocalDateTime;
import java.util.Collection;
public class LoginUser implements UserDetails, CredentialsContainer {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    /**
     * 用户
     */
    private User user;
    /**
     * 登录时间
     */
    private LocalDateTime loginTime;
    public LoginUser(User user, LocalDateTime loginTime) {
        this.user = user;
        this.loginTime = loginTime;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }
    @Override
    public String getPassword() {
        return user.getPassword();
    }
    @Override
    public String getUsername() {
        return user.getUsername();
    }
    /**
     * 账户是否未过期,过期无法验证
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    /**
     * 指定用户是否解锁,锁定的用户无法进行身份验证
     * <p>
     * 密码锁定
     * </p>
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    /**
     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    /**
     * 用户是否被启用或禁用。禁用的用户无法进行身份验证。
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
    /**
     * 认证完成后,擦除密码
     */
    @Override
    public void eraseCredentials() {
        user.setPassword(null);
    }
    public int getId() {
        return user.getId();
    }
    public Role getRole() {
        return user.getRole();
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java
New file
@@ -0,0 +1,96 @@
package com.genersoft.iot.vmp.gb28181;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.gb28181.conf.DefaultProperties;
import com.genersoft.iot.vmp.gb28181.transmit.ISIPProcessorObserver;
import gov.nist.javax.sip.SipProviderImpl;
import gov.nist.javax.sip.SipStackImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.sip.*;
import java.util.Properties;
import java.util.TooManyListenersException;
@Configuration
public class SipLayer{
    private final static Logger logger = LoggerFactory.getLogger(SipLayer.class);
    @Autowired
    private SipConfig sipConfig;
    @Autowired
    private ISIPProcessorObserver sipProcessorObserver;
    private SipStackImpl sipStack;
    private SipFactory sipFactory;
    @Bean("sipFactory")
    SipFactory createSipFactory() {
        sipFactory = SipFactory.getInstance();
        sipFactory.setPathName("gov.nist");
        return sipFactory;
    }
    @Bean("sipStack")
    @DependsOn({"sipFactory"})
    SipStackImpl createSipStack() throws PeerUnavailableException {
        sipStack = ( SipStackImpl )sipFactory.createSipStack(DefaultProperties.getProperties(sipConfig.getMonitorIp(), false));
        return sipStack;
    }
    @Bean(name = "tcpSipProvider")
    @DependsOn("sipStack")
    SipProviderImpl startTcpListener() {
        ListeningPoint tcpListeningPoint = null;
        SipProviderImpl tcpSipProvider  = null;
        try {
            tcpListeningPoint = sipStack.createListeningPoint(sipConfig.getMonitorIp(), sipConfig.getPort(), "TCP");
            tcpSipProvider = (SipProviderImpl)sipStack.createSipProvider(tcpListeningPoint);
            tcpSipProvider.setDialogErrorsAutomaticallyHandled();
            tcpSipProvider.addSipListener(sipProcessorObserver);
            logger.info("[Sip Server] TCP 启动成功 {}:{}", sipConfig.getMonitorIp(), sipConfig.getPort());
        } catch (TransportNotSupportedException e) {
            e.printStackTrace();
        } catch (InvalidArgumentException e) {
            logger.error("[Sip Server]  无法使用 [ {}:{} ]作为SIP[ TCP ]服务,可排查: 1. sip.monitor-ip 是否为本机网卡IP; 2. sip.port 是否已被占用"
                    , sipConfig.getMonitorIp(), sipConfig.getPort());
        } catch (TooManyListenersException e) {
            e.printStackTrace();
        } catch (ObjectInUseException e) {
            e.printStackTrace();
        }
        return tcpSipProvider;
    }
    @Bean(name = "udpSipProvider")
    @DependsOn("sipStack")
    SipProviderImpl startUdpListener() {
        ListeningPoint udpListeningPoint = null;
        SipProviderImpl udpSipProvider = null;
        try {
            udpListeningPoint = sipStack.createListeningPoint(sipConfig.getMonitorIp(), sipConfig.getPort(), "UDP");
            udpSipProvider = (SipProviderImpl)sipStack.createSipProvider(udpListeningPoint);
            udpSipProvider.addSipListener(sipProcessorObserver);
        } catch (TransportNotSupportedException e) {
            e.printStackTrace();
        } catch (InvalidArgumentException e) {
            logger.error("[Sip Server]  无法使用 [ {}:{} ]作为SIP[ UDP ]服务,可排查: 1. sip.monitor-ip 是否为本机网卡IP; 2. sip.port 是否已被占用"
                    , sipConfig.getMonitorIp(), sipConfig.getPort());
        } catch (TooManyListenersException e) {
            e.printStackTrace();
        } catch (ObjectInUseException e) {
            e.printStackTrace();
        }
        logger.info("[Sip Server] UDP 启动成功 {}:{}", sipConfig.getMonitorIp(), sipConfig.getPort());
        return udpSipProvider;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/auth/DigestServerAuthenticationHelper.java
New file
@@ -0,0 +1,295 @@
/*
 * Conditions Of Use
 *
 * This software was developed by employees of the National Institute of
 * Standards and Technology (NIST), an agency of the Federal Government.
 * Pursuant to title 15 Untied States Code Section 105, works of NIST
 * employees are not subject to copyright protection in the United States
 * and are considered to be in the public domain.  As a result, a formal
 * license is not needed to use the software.
 *
 * This software is provided by NIST as a service and is expressly
 * provided "AS IS."  NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED
 * OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT
 * AND DATA ACCURACY.  NIST does not warrant or make any representations
 * regarding the use of the software or the results thereof, including but
 * not limited to the correctness, accuracy, reliability or usefulness of
 * the software.
 *
 * Permission to use this software is contingent upon your acceptance
 * of the terms of this agreement
 *
 * .
 *
 */
package com.genersoft.iot.vmp.gb28181.auth;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Random;
import javax.sip.address.URI;
import javax.sip.header.AuthorizationHeader;
import javax.sip.header.HeaderFactory;
import javax.sip.header.WWWAuthenticateHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
import gov.nist.core.InternalErrorHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * Implements the HTTP digest authentication method server side functionality.
 *
 * @author M. Ranganathan
 * @author Marc Bednarek
 */
public class DigestServerAuthenticationHelper  {
    private Logger logger = LoggerFactory.getLogger(DigestServerAuthenticationHelper.class);
    private MessageDigest messageDigest;
    public static final String DEFAULT_ALGORITHM = "MD5";
    public static final String DEFAULT_SCHEME = "Digest";
    /** to hex converter */
    private static final char[] toHex = { '0', '1', '2', '3', '4', '5', '6',
            '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
    /**
     * Default constructor.
     * @throws NoSuchAlgorithmException
     */
    public DigestServerAuthenticationHelper()
            throws NoSuchAlgorithmException {
        messageDigest = MessageDigest.getInstance(DEFAULT_ALGORITHM);
    }
    public static String toHexString(byte b[]) {
        int pos = 0;
        char[] c = new char[b.length * 2];
        for (int i = 0; i < b.length; i++) {
            c[pos++] = toHex[(b[i] >> 4) & 0x0F];
            c[pos++] = toHex[b[i] & 0x0f];
        }
        return new String(c);
    }
    /**
     * Generate the challenge string.
     *
     * @return a generated nonce.
     */
    private String generateNonce() {
        long time = Instant.now().toEpochMilli();
        Random rand = new Random();
        long pad = rand.nextLong();
        String nonceString = Long.valueOf(time).toString()
                + Long.valueOf(pad).toString();
        byte mdbytes[] = messageDigest.digest(nonceString.getBytes());
        return toHexString(mdbytes);
    }
    public Response generateChallenge(HeaderFactory headerFactory, Response response, String realm) {
        try {
            WWWAuthenticateHeader proxyAuthenticate = headerFactory
                    .createWWWAuthenticateHeader(DEFAULT_SCHEME);
            proxyAuthenticate.setParameter("realm", realm);
            proxyAuthenticate.setParameter("qop", "auth");
            proxyAuthenticate.setParameter("nonce", generateNonce());
            proxyAuthenticate.setParameter("algorithm", DEFAULT_ALGORITHM);
            response.setHeader(proxyAuthenticate);
        } catch (Exception ex) {
            InternalErrorHandler.handleException(ex);
        }
        return response;
    }
    /**
     * Authenticate the inbound request.
     *
     * @param request - the request to authenticate.
     * @param hashedPassword -- the MD5 hashed string of username:realm:plaintext password.
     *
     * @return true if authentication succeded and false otherwise.
     */
    public boolean doAuthenticateHashedPassword(Request request, String hashedPassword) {
        AuthorizationHeader authHeader = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME);
        if ( authHeader == null ) {
            return false;
        }
        String realm = authHeader.getRealm();
        String username = authHeader.getUsername();
        if ( username == null || realm == null ) {
            return false;
        }
        String nonce = authHeader.getNonce();
        URI uri = authHeader.getURI();
        if (uri == null) {
            return false;
        }
        String A2 = request.getMethod().toUpperCase() + ":" + uri.toString();
        String HA1 = hashedPassword;
        byte[] mdbytes = messageDigest.digest(A2.getBytes());
        String HA2 = toHexString(mdbytes);
        String cnonce = authHeader.getCNonce();
        String KD = HA1 + ":" + nonce;
        if (cnonce != null) {
            KD += ":" + cnonce;
        }
        KD += ":" + HA2;
        mdbytes = messageDigest.digest(KD.getBytes());
        String mdString = toHexString(mdbytes);
        String response = authHeader.getResponse();
        return mdString.equals(response);
    }
    /**
     * Authenticate the inbound request given plain text password.
     *
     * @param request - the request to authenticate.
     * @param pass -- the plain text password.
     *
     * @return true if authentication succeded and false otherwise.
     */
    public boolean doAuthenticatePlainTextPassword(Request request, String pass) {
        AuthorizationHeader authHeader = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME);
        if ( authHeader == null ) {
            return false;
        }
        String realm = authHeader.getRealm().trim();
        String username = authHeader.getUsername().trim();
        if ( username == null || realm == null ) {
            return false;
        }
        String nonce = authHeader.getNonce();
        URI uri = authHeader.getURI();
        if (uri == null) {
            return false;
        }
        // qop 保护质量 包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略
        String qop = authHeader.getQop();
        // 客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。
        // 这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护
        String cnonce = authHeader.getCNonce();
        // nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量
        int nc = authHeader.getNonceCount();
        String ncStr = String.format("%08x", nc).toUpperCase();
        // String ncStr = new DecimalFormat("00000000").format(nc);
        // String ncStr = new DecimalFormat("00000000").format(Integer.parseInt(nc + "", 16));
        String A1 = username + ":" + realm + ":" + pass;
        String A2 = request.getMethod().toUpperCase() + ":" + uri.toString();
        byte mdbytes[] = messageDigest.digest(A1.getBytes());
        String HA1 = toHexString(mdbytes);
        logger.debug("A1: " + A1);
        logger.debug("A2: " + A2);
        mdbytes = messageDigest.digest(A2.getBytes());
        String HA2 = toHexString(mdbytes);
        logger.debug("HA1: " + HA1);
        logger.debug("HA2: " + HA2);
        // String cnonce = authHeader.getCNonce();
        logger.debug("nonce: " + nonce);
        logger.debug("nc: " + ncStr);
        logger.debug("cnonce: " + cnonce);
        logger.debug("qop: " + qop);
        String KD = HA1 + ":" + nonce;
        if (qop != null && qop.equalsIgnoreCase("auth") ) {
            if (nc != -1) {
                KD += ":" + ncStr;
            }
            if (cnonce != null) {
                KD += ":" + cnonce;
            }
            KD += ":" + qop;
        }
        KD += ":" + HA2;
        logger.debug("KD: " + KD);
        mdbytes = messageDigest.digest(KD.getBytes());
        String mdString = toHexString(mdbytes);
        logger.debug("mdString: " + mdString);
        String response = authHeader.getResponse();
        logger.debug("response: " + response);
        return mdString.equals(response);
    }
//     public static void main(String[] args) throws NoSuchAlgorithmException {
//         String realm = "3402000000";
//         String username = "44010000001180008012";
//         String nonce = "07cab60999fbf643264ace27d3b7de8b";
//         String uri = "sip:34020000002000000001@3402000000";
//         // qop 保护质量 包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略
//         String qop = "auth";
//         // 客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。
//         // 这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护
//         //String cNonce = authHeader.getCNonce();
//         // nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量
//         int nc = 1;
//         String ncStr = new DecimalFormat("00000000").format(nc);
// //        String ncStr = new DecimalFormat("00000000").format(Integer.parseInt(nc + "", 16));
//         MessageDigest messageDigest = MessageDigest.getInstance(DEFAULT_ALGORITHM);
//         String A1 = username + ":" + realm + ":" + "12345678";
//         String A2 = "REGISTER" + ":" + uri;
//         byte mdbytes[] = messageDigest.digest(A1.getBytes());
//         String HA1 = toHexString(mdbytes);
//         System.out.println("A1: " + A1);
//         System.out.println("A2: " + A2);
//         mdbytes = messageDigest.digest(A2.getBytes());
//         String HA2 = toHexString(mdbytes);
//         System.out.println("HA1: " + HA1);
//         System.out.println("HA2: " + HA2);
//         String cnonce = "0a4f113b";
//         System.out.println("nonce: " + nonce);
//         System.out.println("nc: " + ncStr);
//         System.out.println("cnonce: " + cnonce);
//         System.out.println("qop: " + qop);
//         String KD = HA1 + ":" + nonce;
//         if (qop != null && qop.equals("auth") ) {
//             if (nc != -1) {
//                 KD += ":" + ncStr;
//             }
//             if (cnonce != null) {
//                 KD += ":" + cnonce;
//             }
//             KD += ":" + qop;
//         }
//         KD += ":" + HA2;
//         System.out.println("KD: " + KD);
//         mdbytes = messageDigest.digest(KD.getBytes());
//         String mdString = toHexString(mdbytes);
//         System.out.println("mdString: " + mdString);
//         String response = "4f0507d4b87cdecff04bdaf4c96348f0";
//         System.out.println("response: " + response);
//     }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/AlarmChannelMessage.java
New file
@@ -0,0 +1,46 @@
package com.genersoft.iot.vmp.gb28181.bean;
/**
 * 通过redis分发报警消息
 */
public class AlarmChannelMessage {
    /**
     * 国标编号
     */
    private String gbId;
    /**
     * 报警编号
     */
    private int alarmSn;
    /**
     * 报警描述
     */
    private String alarmDescription;
    public String getGbId() {
        return gbId;
    }
    public void setGbId(String gbId) {
        this.gbId = gbId;
    }
    public int getAlarmSn() {
        return alarmSn;
    }
    public void setAlarmSn(int alarmSn) {
        this.alarmSn = alarmSn;
    }
    public String getAlarmDescription() {
        return alarmDescription;
    }
    public void setAlarmDescription(String alarmDescription) {
        this.alarmDescription = alarmDescription;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/BaiduPoint.java
New file
@@ -0,0 +1,24 @@
package com.genersoft.iot.vmp.gb28181.bean;
public class BaiduPoint {
    String bdLng;
    String bdLat;
    public String getBdLng() {
        return bdLng;
    }
    public void setBdLng(String bdLng) {
        this.bdLng = bdLng;
    }
    public String getBdLat() {
        return bdLat;
    }
    public void setBdLat(String bdLat) {
        this.bdLat = bdLat;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogData.java
New file
@@ -0,0 +1,81 @@
package com.genersoft.iot.vmp.gb28181.bean;
import java.time.Instant;
import java.util.List;
/**
 * @author lin
 */
public class CatalogData {
    /**
     * 命令序列号
     */
    private int sn;
    private int total;
    private List<DeviceChannel> channelList;
    private Instant lastTime;
    private Device device;
    private String errorMsg;
    public enum CatalogDataStatus{
        ready, runIng, end
    }
    private CatalogDataStatus status;
    public int getSn() {
        return sn;
    }
    public void setSn(int sn) {
        this.sn = sn;
    }
    public int getTotal() {
        return total;
    }
    public void setTotal(int total) {
        this.total = total;
    }
    public List<DeviceChannel> getChannelList() {
        return channelList;
    }
    public void setChannelList(List<DeviceChannel> channelList) {
        this.channelList = channelList;
    }
    public Instant getLastTime() {
        return lastTime;
    }
    public void setLastTime(Instant lastTime) {
        this.lastTime = lastTime;
    }
    public Device getDevice() {
        return device;
    }
    public void setDevice(Device device) {
        this.device = device;
    }
    public String getErrorMsg() {
        return errorMsg;
    }
    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }
    public CatalogDataStatus getStatus() {
        return status;
    }
    public void setStatus(CatalogDataStatus status) {
        this.status = status;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/ChannelIdType.java
New file
@@ -0,0 +1,23 @@
package com.genersoft.iot.vmp.gb28181.bean;
/**
 * 国标类型编码,国标编码中11-13位为类型编码
 * 详见 附 录 D  编码规则 A
 * @author lin
 */
public class ChannelIdType {
    /**
     * 中心信令控制服务器编码
     */
    public final static String CENTRAL_SIGNALING_CONTROL_SERVER = "200";
    /**
     * 业务分组编码
     */
    public final static String BUSINESS_GROUP = "215";
    /**
     * 虚拟组织编码
     */
    public final static String VIRTUAL_ORGANIZATION = "216";
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/CmdSendFailEvent.java
New file
@@ -0,0 +1,27 @@
package com.genersoft.iot.vmp.gb28181.bean;
import javax.sip.Dialog;
import java.util.EventObject;
public class CmdSendFailEvent extends EventObject {
    private String callId;
    /**
     * Constructs a prototypical Event.
     *
     * @param dialog
     * @throws IllegalArgumentException if source is null.
     */
    public CmdSendFailEvent(Dialog dialog) {
        super(dialog);
    }
    public String getCallId() {
        return callId;
    }
    public void setCallId(String callId) {
        this.callId = callId;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/CmdType.java
New file
@@ -0,0 +1,8 @@
package com.genersoft.iot.vmp.gb28181.bean;
public class CmdType {
    public static final String CATALOG = "Catalog";
    public static final String ALARM = "Alarm";
    public static final String MOBILE_POSITION = "MobilePosition";
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java
New file
@@ -0,0 +1,421 @@
package com.genersoft.iot.vmp.gb28181.bean;
import io.swagger.v3.oas.annotations.media.Schema;
/**
 * 国标设备/平台
 * @author lin
 */
@Schema(description = "国标设备/平台")
public class Device {
    /**
     * 设备国标编号
     */
    @Schema(description = "设备国标编号")
    private String deviceId;
    /**
     * 纬度
     */
    @Schema(description = "纬度")
    private String latitude;
    /**
     * 经度
     */
    @Schema(description = "经度")
    private String longitude;
    /**
     * 设备名
     */
    @Schema(description = "名称")
    private String name;
    /**
     * 生产厂商
     */
    @Schema(description = "生产厂商")
    private String manufacture;
    /**
     * 型号
     */
    @Schema(description = "型号")
    private String model;
    /**
     * 固件版本
     */
    @Schema(description = "固件版本")
    private String firmware;
    /**
     * 传输协议
     * UDP/TCP
     */
    @Schema(description = "传输协议(UDP/TCP)")
    private String transport;
    /**
     * 数据流传输模式
     * UDP:udp传输
     * TCP-ACTIVE:tcp主动模式
     * TCP-PASSIVE:tcp被动模式
     */
    @Schema(description = "数据流传输模式")
    private String streamMode;
    /**
     * wan地址_ip
     */
    @Schema(description = "IP")
    private String  ip;
    /**
     * wan地址_port
     */
    @Schema(description = "端口")
    private int port;
    /**
     * wan地址
     */
    @Schema(description = "wan地址")
    private String  hostAddress;
    /**
     * 在线
     */
    @Schema(description = "是否在线,1为在线,0为离线")
    private int online;
    /**
     * 注册时间
     */
    @Schema(description = "注册时间")
    private String registerTime;
    /**
     * 心跳时间
     */
    @Schema(description = "心跳时间")
    private String keepaliveTime;
    /**
     * 通道个数
     */
    @Schema(description = "通道个数")
    private int channelCount;
    /**
     * 注册有效期
     */
    @Schema(description = "注册有效期")
    private int expires;
    /**
     * 创建时间
     */
    @Schema(description = "创建时间")
    private String createTime;
    /**
     * 更新时间
     */
    @Schema(description = "更新时间")
    private String updateTime;
    /**
     * 设备使用的媒体id, 默认为null
     */
    @Schema(description = "设备使用的媒体id, 默认为null")
    private String mediaServerId;
    /**
     * 字符集, 支持 UTF-8 与 GB2312
     */
    @Schema(description = "符集, 支持 UTF-8 与 GB2312")
    private String charset ;
    /**
     * 目录订阅周期,0为不订阅
     */
    @Schema(description = "目录订阅周期,0为不订阅")
    private int subscribeCycleForCatalog;
    /**
     * 移动设备位置订阅周期,0为不订阅
     */
    @Schema(description = "移动设备位置订阅周期,0为不订阅")
    private int subscribeCycleForMobilePosition;
    /**
     * 移动设备位置信息上报时间间隔,单位:秒,默认值5
     */
    @Schema(description = "移动设备位置信息上报时间间隔,单位:秒,默认值5")
    private int mobilePositionSubmissionInterval = 5;
    /**
     * 报警订阅周期,0为不订阅
     */
    @Schema(description = "报警心跳时间订阅周期,0为不订阅")
    private int subscribeCycleForAlarm;
    /**
     * 是否开启ssrc校验,默认关闭,开启可以防止串流
     */
    @Schema(description = "是否开启ssrc校验,默认关闭,开启可以防止串流")
    private boolean ssrcCheck = true;
    /**
     * 地理坐标系, 目前支持 WGS84,GCJ02
     */
    @Schema(description = "地理坐标系, 目前支持 WGS84,GCJ02")
    private String geoCoordSys;
    /**
     * 树类型 国标规定了两种树的展现方式 行政区划:CivilCode 和业务分组:BusinessGroup
     */
    @Schema(description = "树类型 国标规定了两种树的展现方式 行政区划:CivilCode 和业务分组:BusinessGroup")
    private String treeType;
    @Schema(description = "密码")
    private String password;
    public String getLatitude() {
        return latitude;
    }
    public void setLatitude(String latitude) {
        this.latitude = latitude;
    }
    public String getLongitude() {
        return longitude;
    }
    public void setLongitude(String longitude) {
        this.longitude = longitude;
    }
    public String getDeviceId() {
        return deviceId;
    }
    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getmanufacture() {
        return manufacture;
    }
    public void setmanufacture(String manufacture) {
        this.manufacture = manufacture;
    }
    public String getModel() {
        return model;
    }
    public void setModel(String model) {
        this.model = model;
    }
    public String getFirmware() {
        return firmware;
    }
    public void setFirmware(String firmware) {
        this.firmware = firmware;
    }
    public String getTransport() {
        return transport;
    }
    public void setTransport(String transport) {
        this.transport = transport;
    }
    public String getStreamMode() {
        return streamMode;
    }
    public void setStreamMode(String streamMode) {
        this.streamMode = streamMode;
    }
    public String getIp() {
        return ip;
    }
    public void setIp(String ip) {
        this.ip = ip;
    }
    public int getPort() {
        return port;
    }
    public void setPort(int port) {
        this.port = port;
    }
    public String getHostAddress() {
        return hostAddress;
    }
    public void setHostAddress(String hostAddress) {
        this.hostAddress = hostAddress;
    }
    public int getOnline() {
        return online;
    }
    public void setOnline(int online) {
        this.online = online;
    }
    public int getChannelCount() {
        return channelCount;
    }
    public void setChannelCount(int channelCount) {
        this.channelCount = channelCount;
    }
    public String getRegisterTime() {
        return registerTime;
    }
    public void setRegisterTime(String registerTime) {
        this.registerTime = registerTime;
    }
    public String getKeepaliveTime() {
        return keepaliveTime;
    }
    public void setKeepaliveTime(String keepaliveTime) {
        this.keepaliveTime = keepaliveTime;
    }
    public int getExpires() {
        return expires;
    }
    public void setExpires(int expires) {
        this.expires = expires;
    }
    public String getCreateTime() {
        return createTime;
    }
    public void setCreateTime(String createTime) {
        this.createTime = createTime;
    }
    public String getUpdateTime() {
        return updateTime;
    }
    public void setUpdateTime(String updateTime) {
        this.updateTime = updateTime;
    }
    public String getMediaServerId() {
        return mediaServerId;
    }
    public void setMediaServerId(String mediaServerId) {
        this.mediaServerId = mediaServerId;
    }
    public String getCharset() {
        return charset;
    }
    public void setCharset(String charset) {
        this.charset = charset;
    }
    public int getSubscribeCycleForCatalog() {
        return subscribeCycleForCatalog;
    }
    public void setSubscribeCycleForCatalog(int subscribeCycleForCatalog) {
        this.subscribeCycleForCatalog = subscribeCycleForCatalog;
    }
    public int getSubscribeCycleForMobilePosition() {
        return subscribeCycleForMobilePosition;
    }
    public void setSubscribeCycleForMobilePosition(int subscribeCycleForMobilePosition) {
        this.subscribeCycleForMobilePosition = subscribeCycleForMobilePosition;
    }
    public int getMobilePositionSubmissionInterval() {
        return mobilePositionSubmissionInterval;
    }
    public void setMobilePositionSubmissionInterval(int mobilePositionSubmissionInterval) {
        this.mobilePositionSubmissionInterval = mobilePositionSubmissionInterval;
    }
    public int getSubscribeCycleForAlarm() {
        return subscribeCycleForAlarm;
    }
    public void setSubscribeCycleForAlarm(int subscribeCycleForAlarm) {
        this.subscribeCycleForAlarm = subscribeCycleForAlarm;
    }
    public boolean isSsrcCheck() {
        return ssrcCheck;
    }
    public void setSsrcCheck(boolean ssrcCheck) {
        this.ssrcCheck = ssrcCheck;
    }
    public String getGeoCoordSys() {
        return geoCoordSys;
    }
    public void setGeoCoordSys(String geoCoordSys) {
        this.geoCoordSys = geoCoordSys;
    }
    public String getTreeType() {
        return treeType;
    }
    public void setTreeType(String treeType) {
        this.treeType = treeType;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarm.java
New file
@@ -0,0 +1,187 @@
package com.genersoft.iot.vmp.gb28181.bean;
import io.swagger.v3.oas.annotations.media.Schema;
/**
 * @author lin
 */
@Schema(description = "报警信息")
public class DeviceAlarm {
    /**
     * 数据库id
     */
    @Schema(description = "数据库id")
    private String id;
    /**
     * 设备Id
     */
    @Schema(description = "设备的国标编号")
    private String deviceId;
    /**
     * 通道Id
     */
    @Schema(description = "通道的国标编号")
    private String channelId;
    /**
     * 报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级警情
     */
    @Schema(description = "报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级警情")
    private String alarmPriority;
    /**
     * 报警方式 , 1为电话报警, 2为设备报警, 3为短信报警, 4为 GPS报警, 5为视频报警, 6为设备故障报警,
     * 7其他报警;可以为直接组合如12为电话报警或 设备报警-
     */
    @Schema(description = "报警方式 , 1为电话报警, 2为设备报警, 3为短信报警, 4为 GPS报警, 5为视频报警, 6为设备故障报警,\n" +
            "\t * 7其他报警;可以为直接组合如12为电话报警或设备报警")
    private String alarmMethod;
    /**
     * 报警时间
     */
    @Schema(description = "报警时间")
    private String alarmTime;
    /**
     * 报警内容描述
     */
    @Schema(description = "报警内容描述")
    private String alarmDescription;
    /**
     * 经度
     */
    @Schema(description = "经度")
    private double longitude;
    /**
     * 纬度
     */
    @Schema(description = "纬度")
    private double latitude;
    /**
     * 报警类型,
     * 报警方式为2时,不携带 AlarmType为默认的报警设备报警,
     * 携带 AlarmType取值及对应报警类型如下:
     *         1-视频丢失报警;
     *         2-设备防拆报警;
     *         3-存储设备磁盘满报警;
     *         4-设备高温报警;
     *         5-设备低温报警。
     * 报警方式为5时,取值如下:
     *         1-人工视频报警;
     *         2-运动目标检测报警;
     *         3-遗留物检测报警;
     *         4-物体移除检测报警;
     *         5-绊线检测报警;
     *         6-入侵检测报警;
     *         7-逆行检测报警;
     *         8-徘徊检测报警;
     *         9-流量统计报警;
     *         10-密度检测报警;
     *         11-视频异常检测报警;
     *         12-快速移动报警。
     * 报警方式为6时,取值下:
     *         1-存储设备磁盘故障报警;
     *         2-存储设备风扇故障报警。
     */
    @Schema(description = "报警类型")
    private String alarmType;
    @Schema(description = "创建时间")
    private String createTime;
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getDeviceId() {
        return deviceId;
    }
    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }
    public String getAlarmPriority() {
        return alarmPriority;
    }
    public void setAlarmPriority(String alarmPriority) {
        this.alarmPriority = alarmPriority;
    }
    public String getAlarmMethod() {
        return alarmMethod;
    }
    public void setAlarmMethod(String alarmMethod) {
        this.alarmMethod = alarmMethod;
    }
    public String getAlarmTime() {
        return alarmTime;
    }
    public void setAlarmTime(String alarmTime) {
        this.alarmTime = alarmTime;
    }
    public String getAlarmDescription() {
        return alarmDescription;
    }
    public void setAlarmDescription(String alarmDescription) {
        this.alarmDescription = alarmDescription;
    }
    public double getLongitude() {
        return longitude;
    }
    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }
    public double getLatitude() {
        return latitude;
    }
    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }
    public String getAlarmType() {
        return alarmType;
    }
    public void setAlarmType(String alarmType) {
        this.alarmType = alarmType;
    }
    public String getChannelId() {
        return channelId;
    }
    public void setChannelId(String channelId) {
        this.channelId = channelId;
    }
    public String getCreateTime() {
        return createTime;
    }
    public void setCreateTime(String createTime) {
        this.createTime = createTime;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarmMethod.java
New file
@@ -0,0 +1,40 @@
package com.genersoft.iot.vmp.gb28181.bean;
/**
 * 报警方式
 * @author lin
 * 1为电话报警, 2为设备报警, 3为短信报警, 4为 GPS报警, 5为视频报警, 6为设备故障报警,
 * 7其他报警;可以为直接组合如12为电话报警或 设备报警-
 */
public enum DeviceAlarmMethod {
    // 1为电话报警
    Telephone(1),
    // 2为设备报警
    Device(2),
    // 3为短信报警
    SMS(3),
    // 4为 GPS报警
    GPS(4),
    // 5为视频报警
    Video(5),
    // 6为设备故障报警
    DeviceFailure(6),
    // 7其他报警
    Other(7);
    private final int val;
    DeviceAlarmMethod(int val) {
        this.val=val;
    }
    public int getVal() {
        return val;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannel.java
New file
@@ -0,0 +1,591 @@
package com.genersoft.iot.vmp.gb28181.bean;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "通道信息")
public class DeviceChannel {
    /**
     * 数据库自增ID
     */
    @Schema(description = "数据库自增ID")
    private int id;
    /**
     * 通道国标编号
     */
    @Schema(description = "通道国标编号")
    private String channelId;
    /**
     * 设备国标编号
     */
    @Schema(description = "设备国标编号")
    private String deviceId;
    /**
     * 通道名
     */
    @Schema(description = "名称")
    private String name;
    /**
     * 生产厂商
     */
    @Schema(description = "生产厂商")
    private String manufacture;
    /**
     * 型号
     */
    @Schema(description = "型号")
    private String model;
    /**
     * 设备归属
     */
    @Schema(description = "设备归属")
    private String owner;
    /**
     * 行政区域
     */
    @Schema(description = "行政区域")
    private String civilCode;
    /**
     * 警区
     */
    @Schema(description = "警区")
    private String block;
    /**
     * 安装地址
     */
    @Schema(description = "安装地址")
    private String address;
    /**
     * 是否有子设备 1有, 0没有
     */
    @Schema(description = "是否有子设备 1有, 0没有")
    private int parental;
    /**
     * 父级id
     */
    @Schema(description = "父级id")
    private String parentId;
    /**
     * 信令安全模式  缺省为0; 0:不采用; 2: S/MIME签名方式; 3: S/ MIME加密签名同时采用方式; 4:数字摘要方式
     */
    @Schema(description = "信令安全模式  缺省为0; 0:不采用; 2: S/MIME签名方式; 3: S/ MIME加密签名同时采用方式; 4:数字摘要方式")
    private int safetyWay;
    /**
     * 注册方式 缺省为1;1:符合IETFRFC3261标准的认证注册模 式; 2:基于口令的双向认证注册模式; 3:基于数字证书的双向认证注册模式
     */
    @Schema(description = "注册方式 缺省为1;1:符合IETFRFC3261标准的认证注册模 式; 2:基于口令的双向认证注册模式; 3:基于数字证书的双向认证注册模式")
    private int registerWay;
    /**
     * 证书序列号
     */
    @Schema(description = "证书序列号")
    private String certNum;
    /**
     * 证书有效标识 缺省为0;证书有效标识:0:无效1: 有效
     */
    @Schema(description = "证书有效标识 缺省为0;证书有效标识:0:无效1: 有效")
    private int certifiable;
    /**
     * 证书无效原因码
     */
    @Schema(description = "证书无效原因码")
    private int errCode;
    /**
     * 证书终止有效期
     */
    @Schema(description = "证书终止有效期")
    private String endTime;
    /**
     * 保密属性 缺省为0; 0:不涉密, 1:涉密
     */
    @Schema(description = "保密属性 缺省为0; 0:不涉密, 1:涉密")
    private String secrecy;
    /**
     * IP地址
     */
    @Schema(description = "IP地址")
    private String ipAddress;
    /**
     * 端口号
     */
    @Schema(description = "端口号")
    private int port;
    /**
     * 密码
     */
    @Schema(description = "密码")
    private String password;
    /**
     * 云台类型
     */
    @Schema(description = "云台类型")
    private int PTZType;
    /**
     * 云台类型描述字符串
     */
    @Schema(description = "云台类型描述字符串")
    private String PTZTypeText;
    /**
     * 创建时间
     */
    @Schema(description = "创建时间")
    private String createTime;
    /**
     * 更新时间
     */
    @Schema(description = "更新时间")
    private String updateTime;
    /**
     * 在线/离线
     * 1在线,0离线
     * 默认在线
     * 信令:
     * <Status>ON</Status>
     * <Status>OFF</Status>
     * 遇到过NVR下的IPC下发信令可以推流, 但是 Status 响应 OFF
     */
    @Schema(description = "在线/离线, 1在线,0离线")
    private int status;
    /**
     * 经度
     */
    @Schema(description = "经度")
    private double longitude;
    /**
     * 纬度
     */
    @Schema(description = "纬度")
    private double latitude;
    /**
     * 经度 GCJ02
     */
    @Schema(description = "GCJ02坐标系经度")
    private double longitudeGcj02;
    /**
     * 纬度 GCJ02
     */
    @Schema(description = "GCJ02坐标系纬度")
    private double latitudeGcj02;
    /**
     * 经度 WGS84
     */
    @Schema(description = "WGS84坐标系经度")
    private double longitudeWgs84;
    /**
     * 纬度 WGS84
     */
    @Schema(description = "WGS84坐标系纬度")
    private double latitudeWgs84;
    /**
     * 子设备数
     */
    @Schema(description = "子设备数")
    private int subCount;
    /**
     * 流唯一编号,存在表示正在直播
     */
    @Schema(description = "流唯一编号,存在表示正在直播")
    private String  streamId;
    /**
     *  是否含有音频
     */
    @Schema(description = "是否含有音频")
    private boolean hasAudio;
    /**
     * 标记通道的类型,0->国标通道 1->直播流通道 2->业务分组/虚拟组织/行政区划
     */
    @Schema(description = "标记通道的类型,0->国标通道 1->直播流通道 2->业务分组/虚拟组织/行政区划")
    private int channelType;
    /**
     * 业务分组
     */
    @Schema(description = "业务分组")
    private String businessGroupId;
    /**
     * GPS的更新时间
     */
    @Schema(description = "GPS的更新时间")
    private String gpsTime;
    /**
     * 空间坐标
     */
    @Schema(description = "空间坐标")
    private String coordinate;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getDeviceId() {
        return deviceId;
    }
    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }
    public void setPTZType(int PTZType) {
        this.PTZType = PTZType;
        switch (PTZType) {
            case 0:
                this.PTZTypeText = "未知";
                break;
            case 1:
                this.PTZTypeText = "球机";
                break;
            case 2:
                this.PTZTypeText = "半球";
                break;
            case 3:
                this.PTZTypeText = "固定枪机";
                break;
            case 4:
                this.PTZTypeText = "遥控枪机";
                break;
        }
    }
    public String getChannelId() {
        return channelId;
    }
    public void setChannelId(String channelId) {
        this.channelId = channelId;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getManufacture() {
        return manufacture;
    }
    public void setManufacture(String manufacture) {
        this.manufacture = manufacture;
    }
    public String getModel() {
        return model;
    }
    public void setModel(String model) {
        this.model = model;
    }
    public String getOwner() {
        return owner;
    }
    public void setOwner(String owner) {
        this.owner = owner;
    }
    public String getCivilCode() {
        return civilCode;
    }
    public void setCivilCode(String civilCode) {
        this.civilCode = civilCode;
    }
    public String getBlock() {
        return block;
    }
    public void setBlock(String block) {
        this.block = block;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    public int getParental() {
        return parental;
    }
    public void setParental(int parental) {
        this.parental = parental;
    }
    public String getParentId() {
        return parentId;
    }
    public void setParentId(String parentId) {
        this.parentId = parentId;
    }
    public int getSafetyWay() {
        return safetyWay;
    }
    public void setSafetyWay(int safetyWay) {
        this.safetyWay = safetyWay;
    }
    public int getRegisterWay() {
        return registerWay;
    }
    public void setRegisterWay(int registerWay) {
        this.registerWay = registerWay;
    }
    public String getCertNum() {
        return certNum;
    }
    public void setCertNum(String certNum) {
        this.certNum = certNum;
    }
    public int getCertifiable() {
        return certifiable;
    }
    public void setCertifiable(int certifiable) {
        this.certifiable = certifiable;
    }
    public int getErrCode() {
        return errCode;
    }
    public void setErrCode(int errCode) {
        this.errCode = errCode;
    }
    public String getEndTime() {
        return endTime;
    }
    public void setEndTime(String endTime) {
        this.endTime = endTime;
    }
    public String getSecrecy() {
        return secrecy;
    }
    public void setSecrecy(String secrecy) {
        this.secrecy = secrecy;
    }
    public String getIpAddress() {
        return ipAddress;
    }
    public void setIpAddress(String ipAddress) {
        this.ipAddress = ipAddress;
    }
    public int getPort() {
        return port;
    }
    public void setPort(int port) {
        this.port = port;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public int getPTZType() {
        return PTZType;
    }
    public String getPTZTypeText() {
        return PTZTypeText;
    }
    public void setPTZTypeText(String PTZTypeText) {
        this.PTZTypeText = PTZTypeText;
    }
    public int getStatus() {
        return status;
    }
    public void setStatus(int status) {
        this.status = status;
    }
    public double getLongitude() {
        return longitude;
    }
    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }
    public double getLatitude() {
        return latitude;
    }
    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }
    public double getLongitudeGcj02() {
        return longitudeGcj02;
    }
    public void setLongitudeGcj02(double longitudeGcj02) {
        this.longitudeGcj02 = longitudeGcj02;
    }
    public double getLatitudeGcj02() {
        return latitudeGcj02;
    }
    public void setLatitudeGcj02(double latitudeGcj02) {
        this.latitudeGcj02 = latitudeGcj02;
    }
    public double getLongitudeWgs84() {
        return longitudeWgs84;
    }
    public void setLongitudeWgs84(double longitudeWgs84) {
        this.longitudeWgs84 = longitudeWgs84;
    }
    public double getLatitudeWgs84() {
        return latitudeWgs84;
    }
    public void setLatitudeWgs84(double latitudeWgs84) {
        this.latitudeWgs84 = latitudeWgs84;
    }
    public int getSubCount() {
        return subCount;
    }
    public void setSubCount(int subCount) {
        this.subCount = subCount;
    }
    public boolean isHasAudio() {
        return hasAudio;
    }
    public void setHasAudio(boolean hasAudio) {
        this.hasAudio = hasAudio;
    }
    public String getStreamId() {
        return streamId;
    }
    public void setStreamId(String streamId) {
        this.streamId = streamId;
    }
    public String getCreateTime() {
        return createTime;
    }
    public void setCreateTime(String createTime) {
        this.createTime = createTime;
    }
    public String getUpdateTime() {
        return updateTime;
    }
    public void setUpdateTime(String updateTime) {
        this.updateTime = updateTime;
    }
    public int getChannelType() {
        return channelType;
    }
    public void setChannelType(int channelType) {
        this.channelType = channelType;
    }
    public String getBusinessGroupId() {
        return businessGroupId;
    }
    public void setBusinessGroupId(String businessGroupId) {
        this.businessGroupId = businessGroupId;
    }
    public String getGpsTime() {
        return gpsTime;
    }
    public void setGpsTime(String gpsTime) {
        this.gpsTime = gpsTime;
    }
    public String getCoordinate() {
        return coordinate;
    }
    public void setCoordinate(String coordinate) {
        this.coordinate = coordinate;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannelInPlatform.java
New file
@@ -0,0 +1,23 @@
package com.genersoft.iot.vmp.gb28181.bean;
public class DeviceChannelInPlatform extends DeviceChannel{
    private String platFormId;
    private String catalogId;
    public String getPlatFormId() {
        return platFormId;
    }
    public void setPlatFormId(String platFormId) {
        this.platFormId = platFormId;
    }
    public String getCatalogId() {
        return catalogId;
    }
    public void setCatalogId(String catalogId) {
        this.catalogId = catalogId;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceNotFoundEvent.java
New file
@@ -0,0 +1,27 @@
package com.genersoft.iot.vmp.gb28181.bean;
import javax.sip.Dialog;
import java.util.EventObject;
public class DeviceNotFoundEvent extends EventObject {
    private String callId;
    /**
     * Constructs a prototypical Event.
     *
     * @param dialog
     * @throws IllegalArgumentException if source is null.
     */
    public DeviceNotFoundEvent(Dialog dialog) {
        super(dialog);
    }
    public String getCallId() {
        return callId;
    }
    public void setCallId(String callId) {
        this.callId = callId;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbStream.java
New file
@@ -0,0 +1,125 @@
package com.genersoft.iot.vmp.gb28181.bean;
import io.swagger.v3.oas.annotations.media.Schema;
/**
 * 直播流关联国标上级平台
 * @author lin
 */
@Schema(description = "直播流关联国标上级平台")
public class GbStream extends PlatformGbStream{
    @Schema(description = "ID")
    private Integer gbStreamId;
    @Schema(description = "应用名")
    private String app;
    @Schema(description = "流ID")
    private String stream;
    @Schema(description = "国标ID")
    private String gbId;
    @Schema(description = "名称")
    private String name;
    @Schema(description = "流媒体ID")
    private String mediaServerId;
    @Schema(description = "经度")
    private double longitude;
    @Schema(description = "纬度")
    private double latitude;
    @Schema(description = "流类型(拉流/推流)")
    private String streamType;
    @Schema(description = "状态")
    private boolean status;
    @Schema(description = "创建时间")
    public String createTime;
    @Override
    public Integer getGbStreamId() {
        return gbStreamId;
    }
    @Override
    public void setGbStreamId(Integer gbStreamId) {
        this.gbStreamId = gbStreamId;
    }
    public String getApp() {
        return app;
    }
    public void setApp(String app) {
        this.app = app;
    }
    public String getStream() {
        return stream;
    }
    public void setStream(String stream) {
        this.stream = stream;
    }
    public String getGbId() {
        return gbId;
    }
    public void setGbId(String gbId) {
        this.gbId = gbId;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getLongitude() {
        return longitude;
    }
    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }
    public double getLatitude() {
        return latitude;
    }
    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }
    public String getStreamType() {
        return streamType;
    }
    public void setStreamType(String streamType) {
        this.streamType = streamType;
    }
    public boolean isStatus() {
        return status;
    }
    public void setStatus(boolean status) {
        this.status = status;
    }
    public String getMediaServerId() {
        return mediaServerId;
    }
    public void setMediaServerId(String mediaServerId) {
        this.mediaServerId = mediaServerId;
    }
    public String getCreateTime() {
        return createTime;
    }
    public void setCreateTime(String createTime) {
        this.createTime = createTime;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/HandlerCatchData.java
New file
@@ -0,0 +1,44 @@
package com.genersoft.iot.vmp.gb28181.bean;
import org.dom4j.Element;
import javax.sip.RequestEvent;
/**
 * @author lin
 */
public class HandlerCatchData {
    private RequestEvent evt;
    private Device device;
    private Element rootElement;
    public HandlerCatchData(RequestEvent evt, Device device, Element rootElement) {
        this.evt = evt;
        this.device = device;
        this.rootElement = rootElement;
    }
    public RequestEvent getEvt() {
        return evt;
    }
    public void setEvt(RequestEvent evt) {
        this.evt = evt;
    }
    public Device getDevice() {
        return device;
    }
    public void setDevice(Device device) {
        this.device = device;
    }
    public Element getRootElement() {
        return rootElement;
    }
    public void setRootElement(Element rootElement) {
        this.rootElement = rootElement;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/Host.java
New file
@@ -0,0 +1,35 @@
package com.genersoft.iot.vmp.gb28181.bean;
public class Host {
    private String ip;
    private int port;
    private String address;
    public String getIp() {
        return ip;
    }
    public void setIp(String ip) {
        this.ip = ip;
    }
    public int getPort() {
        return port;
    }
    public void setPort(int port) {
        this.port = port;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamCallback.java
New file
@@ -0,0 +1,5 @@
package com.genersoft.iot.vmp.gb28181.bean;
public interface InviteStreamCallback {
    void call(InviteStreamInfo inviteStreamInfo);
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamInfo.java
New file
@@ -0,0 +1,61 @@
package com.genersoft.iot.vmp.gb28181.bean;
import com.alibaba.fastjson.JSONObject;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
public class InviteStreamInfo {
    public InviteStreamInfo(MediaServerItem mediaServerItem, JSONObject response, String callId, String app, String stream) {
        this.mediaServerItem = mediaServerItem;
        this.response = response;
        this.callId = callId;
        this.app = app;
        this.stream = stream;
    }
    private MediaServerItem mediaServerItem;
    private JSONObject response;
    private String callId;
    private String app;
    private String stream;
    public MediaServerItem getMediaServerItem() {
        return mediaServerItem;
    }
    public void setMediaServerItem(MediaServerItem mediaServerItem) {
        this.mediaServerItem = mediaServerItem;
    }
    public JSONObject getResponse() {
        return response;
    }
    public void setResponse(JSONObject response) {
        this.response = response;
    }
    public String getCallId() {
        return callId;
    }
    public void setCallId(String callId) {
        this.callId = callId;
    }
    public String getApp() {
        return app;
    }
    public void setApp(String app) {
        this.app = app;
    }
    public String getStream() {
        return stream;
    }
    public void setStream(String stream) {
        this.stream = stream;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamType.java
New file
@@ -0,0 +1,8 @@
package com.genersoft.iot.vmp.gb28181.bean;
public enum InviteStreamType {
    PLAY,PLAYBACK,PUSH,PROXY,CLOUD_RECORD_PUSH,CLOUD_RECORD_PROXY
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/MobilePosition.java
New file
@@ -0,0 +1,205 @@
package com.genersoft.iot.vmp.gb28181.bean;
/**
 * @description: 移动位置bean
 * @author: lawrencehj
 * @date: 2021年1月23日
 */
public class MobilePosition {
    /**
     * 设备Id
     */
    private String deviceId;
    /**
     * 通道Id
     */
    private String channelId;
    /**
     * 设备名称
     */
    private String deviceName;
    /**
     * 通知时间
     */
    private String time;
    /**
     * 经度
     */
    private double longitude;
    /**
     * 纬度
     */
    private double latitude;
    /**
     * 海拔高度
     */
    private double altitude;
    /**
     * 速度
     */
    private double speed;
    /**
     * 方向
     */
    private double direction;
    /**
     * 位置信息上报来源(Mobile Position、GPS Alarm)
     */
    private String reportSource;
    /**
     * 国内坐标系:经度坐标
     */
    private double longitudeGcj02;
    /**
     * 国内坐标系:纬度坐标
     */
    private double latitudeGcj02;
    /**
     * 国内坐标系:经度坐标
     */
    private double longitudeWgs84;
    /**
     * 国内坐标系:纬度坐标
     */
    private double latitudeWgs84;
    /**
     * 创建时间
     */
    private String createTime;
    public String getDeviceId() {
        return deviceId;
    }
    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }
    public String getDeviceName() {
        return deviceName;
    }
    public void setDeviceName(String deviceName) {
        this.deviceName = deviceName;
    }
    public String getTime() {
        return time;
    }
    public void setTime(String time) {
        this.time = time;
    }
    public double getLongitude() {
        return longitude;
    }
    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }
    public double getLatitude() {
        return latitude;
    }
    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }
    public double getAltitude() {
        return altitude;
    }
    public void setAltitude(double altitude) {
        this.altitude = altitude;
    }
    public double getSpeed() {
        return speed;
    }
    public void setSpeed(double speed) {
        this.speed = speed;
    }
    public double getDirection() {
        return direction;
    }
    public void setDirection(double direction) {
        this.direction = direction;
    }
    public String getReportSource() {
        return reportSource;
    }
    public void setReportSource(String reportSource) {
        this.reportSource = reportSource;
    }
    public String getChannelId() {
        return channelId;
    }
    public void setChannelId(String channelId) {
        this.channelId = channelId;
    }
    public double getLongitudeGcj02() {
        return longitudeGcj02;
    }
    public void setLongitudeGcj02(double longitudeGcj02) {
        this.longitudeGcj02 = longitudeGcj02;
    }
    public double getLatitudeGcj02() {
        return latitudeGcj02;
    }
    public void setLatitudeGcj02(double latitudeGcj02) {
        this.latitudeGcj02 = latitudeGcj02;
    }
    public double getLongitudeWgs84() {
        return longitudeWgs84;
    }
    public void setLongitudeWgs84(double longitudeWgs84) {
        this.longitudeWgs84 = longitudeWgs84;
    }
    public double getLatitudeWgs84() {
        return latitudeWgs84;
    }
    public void setLatitudeWgs84(double latitudeWgs84) {
        this.latitudeWgs84 = latitudeWgs84;
    }
    public String getCreateTime() {
        return createTime;
    }
    public void setCreateTime(String createTime) {
        this.createTime = createTime;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/ParentPlatform.java
New file
@@ -0,0 +1,431 @@
package com.genersoft.iot.vmp.gb28181.bean;
import io.swagger.v3.oas.annotations.media.Schema;
/**
 * @author lin
 */
@Schema(description = "平台信息")
public class ParentPlatform {
    /**
     * id
     */
    @Schema(description = "ID(数据库中)")
    private Integer id;
    /**
     * 是否启用
     */
    @Schema(description = "是否启用")
    private boolean enable;
    /**
     * 名称
     */
    @Schema(description = "名称")
    private String name;
    /**
     * SIP服务国标编码
     */
    @Schema(description = "SIP服务国标编码")
    private String serverGBId;
    /**
     * SIP服务国标域
     */
    @Schema(description = "SIP服务国标域")
    private String serverGBDomain;
    /**
     * SIP服务IP
     */
    @Schema(description = "SIP服务IP")
    private String serverIP;
    /**
     * SIP服务端口
     */
    @Schema(description = "SIP服务端口")
    private int serverPort;
    /**
     * 设备国标编号
     */
    @Schema(description = "设备国标编号")
    private String deviceGBId;
    /**
     * 设备ip
     */
    @Schema(description = "设备ip")
    private String deviceIp;
    /**
     * 设备端口
     */
    @Schema(description = "设备端口")
    private String devicePort;
    /**
     * SIP认证用户名(默认使用设备国标编号)
     */
    @Schema(description = "SIP认证用户名(默认使用设备国标编号)")
    private String username;
    /**
     * SIP认证密码
     */
    @Schema(description = "SIP认证密码")
    private String password;
    /**
     * 注册周期 (秒)
     */
    @Schema(description = "注册周期 (秒)")
    private int expires;
    /**
     * 心跳周期(秒)
     */
    @Schema(description = "心跳周期(秒)")
    private int keepTimeout;
    /**
     * 传输协议
     * UDP/TCP
     */
    @Schema(description = "传输协议")
    private String transport;
    /**
     * 字符集
     */
    @Schema(description = "字符集")
    private String characterSet;
    /**
     * 允许云台控制
     */
    @Schema(description = "允许云台控制")
    private boolean ptz;
    /**
     * RTCP流保活
     */
    @Schema(description = "RTCP流保活")
    private boolean rtcp;
    /**
     * 在线状态
     */
    @Schema(description = "在线状态")
    private boolean status;
    /**
     * 在线状态
     */
    @Schema(description = "在线状态")
    private int channelCount;
    /**
     * 默认目录Id,自动添加的通道多放在这个目录下
     */
    @Schema(description = "默认目录Id,自动添加的通道多放在这个目录下")
    private String catalogId;
    /**
     * 已被订阅目录信息
     */
    @Schema(description = "已被订阅目录信息")
    private boolean catalogSubscribe;
    /**
     * 已被订阅报警信息
     */
    @Schema(description = "已被订阅报警信息")
    private boolean alarmSubscribe;
    /**
     * 已被订阅移动位置信息
     */
    @Schema(description = "已被订阅移动位置信息")
    private boolean mobilePositionSubscribe;
    /**
     * 点播未推流的设备时是否使用redis通知拉起
     */
    @Schema(description = "点播未推流的设备时是否使用redis通知拉起")
    private boolean startOfflinePush;
    /**
     * 目录分组-每次向上级发送通道信息时单个包携带的通道数量,取值1,2,4,8
     */
    @Schema(description = "目录分组-每次向上级发送通道信息时单个包携带的通道数量,取值1,2,4,8")
    private int catalogGroup;
    /**
     * 行政区划
     */
    @Schema(description = "行政区划")
    private String administrativeDivision;
    /**
     * 更新时间
     */
    @Schema(description = "更新时间")
    private String updateTime;
    /**
     * 创建时间
     */
    @Schema(description = "创建时间")
    private String createTime;
    /**
     * 树类型 国标规定了两种树的展现方式 行政区划 CivilCode 和业务分组:BusinessGroup
     */
    @Schema(description = "树类型 国标规定了两种树的展现方式 行政区划 CivilCode 和业务分组:BusinessGrou")
    private String treeType;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public boolean isEnable() {
        return enable;
    }
    public void setEnable(boolean enable) {
        this.enable = enable;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getServerGBId() {
        return serverGBId;
    }
    public void setServerGBId(String serverGBId) {
        this.serverGBId = serverGBId;
    }
    public String getServerGBDomain() {
        return serverGBDomain;
    }
    public void setServerGBDomain(String serverGBDomain) {
        this.serverGBDomain = serverGBDomain;
    }
    public String getServerIP() {
        return serverIP;
    }
    public void setServerIP(String serverIP) {
        this.serverIP = serverIP;
    }
    public int getServerPort() {
        return serverPort;
    }
    public void setServerPort(int serverPort) {
        this.serverPort = serverPort;
    }
    public String getDeviceGBId() {
        return deviceGBId;
    }
    public void setDeviceGBId(String deviceGBId) {
        this.deviceGBId = deviceGBId;
    }
    public String getDeviceIp() {
        return deviceIp;
    }
    public void setDeviceIp(String deviceIp) {
        this.deviceIp = deviceIp;
    }
    public String getDevicePort() {
        return devicePort;
    }
    public void setDevicePort(String devicePort) {
        this.devicePort = devicePort;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public int getExpires() {
        return expires;
    }
    public void setExpires(int expires) {
        this.expires = expires;
    }
    public int getKeepTimeout() {
        return keepTimeout;
    }
    public void setKeepTimeout(int keepTimeout) {
        this.keepTimeout = keepTimeout;
    }
    public String getTransport() {
        return transport;
    }
    public void setTransport(String transport) {
        this.transport = transport;
    }
    public String getCharacterSet() {
        return characterSet;
    }
    public void setCharacterSet(String characterSet) {
        this.characterSet = characterSet;
    }
    public boolean isPtz() {
        return ptz;
    }
    public void setPtz(boolean ptz) {
        this.ptz = ptz;
    }
    public boolean isRtcp() {
        return rtcp;
    }
    public void setRtcp(boolean rtcp) {
        this.rtcp = rtcp;
    }
    public boolean isStatus() {
        return status;
    }
    public void setStatus(boolean status) {
        this.status = status;
    }
    public int getChannelCount() {
        return channelCount;
    }
    public void setChannelCount(int channelCount) {
        this.channelCount = channelCount;
    }
    public String getCatalogId() {
        return catalogId;
    }
    public void setCatalogId(String catalogId) {
        this.catalogId = catalogId;
    }
    public boolean isCatalogSubscribe() {
        return catalogSubscribe;
    }
    public void setCatalogSubscribe(boolean catalogSubscribe) {
        this.catalogSubscribe = catalogSubscribe;
    }
    public boolean isAlarmSubscribe() {
        return alarmSubscribe;
    }
    public void setAlarmSubscribe(boolean alarmSubscribe) {
        this.alarmSubscribe = alarmSubscribe;
    }
    public boolean isMobilePositionSubscribe() {
        return mobilePositionSubscribe;
    }
    public void setMobilePositionSubscribe(boolean mobilePositionSubscribe) {
        this.mobilePositionSubscribe = mobilePositionSubscribe;
    }
    public boolean isStartOfflinePush() {
        return startOfflinePush;
    }
    public void setStartOfflinePush(boolean startOfflinePush) {
        this.startOfflinePush = startOfflinePush;
    }
    public int getCatalogGroup() {
        return catalogGroup;
    }
    public void setCatalogGroup(int catalogGroup) {
        this.catalogGroup = catalogGroup;
    }
    public String getAdministrativeDivision() {
        return administrativeDivision;
    }
    public void setAdministrativeDivision(String administrativeDivision) {
        this.administrativeDivision = administrativeDivision;
    }
    public String getUpdateTime() {
        return updateTime;
    }
    public void setUpdateTime(String updateTime) {
        this.updateTime = updateTime;
    }
    public String getCreateTime() {
        return createTime;
    }
    public void setCreateTime(String createTime) {
        this.createTime = createTime;
    }
    public String getTreeType() {
        return treeType;
    }
    public void setTreeType(String treeType) {
        this.treeType = treeType;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/ParentPlatformCatch.java
New file
@@ -0,0 +1,58 @@
package com.genersoft.iot.vmp.gb28181.bean;
public class ParentPlatformCatch {
    private String id;
    /**
     * 心跳未回复次数
     */
    private int keepAliveReply;
    // 注册未回复次数
    private int registerAliveReply;
    private String callId;
    private ParentPlatform parentPlatform;
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public int getKeepAliveReply() {
        return keepAliveReply;
    }
    public void setKeepAliveReply(int keepAliveReply) {
        this.keepAliveReply = keepAliveReply;
    }
    public int getRegisterAliveReply() {
        return registerAliveReply;
    }
    public void setRegisterAliveReply(int registerAliveReply) {
        this.registerAliveReply = registerAliveReply;
    }
    public ParentPlatform getParentPlatform() {
        return parentPlatform;
    }
    public void setParentPlatform(ParentPlatform parentPlatform) {
        this.parentPlatform = parentPlatform;
    }
    public String getCallId() {
        return callId;
    }
    public void setCallId(String callId) {
        this.callId = callId;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformCatalog.java
New file
@@ -0,0 +1,116 @@
package com.genersoft.iot.vmp.gb28181.bean;
import io.swagger.v3.oas.annotations.media.Schema;
/**
 * 国标级联-目录
 * @author lin
 */
@Schema(description = "目录信息")
public class PlatformCatalog {
    @Schema(description = "ID")
    private String id;
    @Schema(description = "名称")
    private String name;
    @Schema(description = "平台ID")
    private String platformId;
    @Schema(description = "父级目录ID")
    private String parentId;
    @Schema(description = "行政区划")
    private String civilCode;
    @Schema(description = "目录分组")
    private String businessGroupId;
    /**
     * 子节点数
     */
    @Schema(description = "子节点数")
    private int childrenCount;
    /**
     * 0 目录, 1 国标通道, 2 直播流
     */
    @Schema(description = "类型:0 目录, 1 国标通道, 2 直播流")
    private int type;
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getPlatformId() {
        return platformId;
    }
    public void setPlatformId(String platformId) {
        this.platformId = platformId;
    }
    public String getParentId() {
        return parentId;
    }
    public void setParentId(String parentId) {
        this.parentId = parentId;
    }
    public int getChildrenCount() {
        return childrenCount;
    }
    public void setChildrenCount(int childrenCount) {
        this.childrenCount = childrenCount;
    }
    public int getType() {
        return type;
    }
    public void setType(int type) {
        this.type = type;
    }
    public void setTypeForCatalog() {
        this.type = 0;
    }
    public void setTypeForGb() {
        this.type = 1;
    }
    public void setTypeForStream() {
        this.type = 2;
    }
    public String getCivilCode() {
        return civilCode;
    }
    public void setCivilCode(String civilCode) {
        this.civilCode = civilCode;
    }
    public String getBusinessGroupId() {
        return businessGroupId;
    }
    public void setBusinessGroupId(String businessGroupId) {
        this.businessGroupId = businessGroupId;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformGbStream.java
New file
@@ -0,0 +1,39 @@
package com.genersoft.iot.vmp.gb28181.bean;
import io.swagger.v3.oas.annotations.media.Schema;
public class PlatformGbStream {
    @Schema(description = "ID")
    private Integer gbStreamId;
    @Schema(description = "平台ID")
    private String platformId;
    @Schema(description = "目录ID")
    private String catalogId;
    public Integer getGbStreamId() {
        return gbStreamId;
    }
    public void setGbStreamId(Integer gbStreamId) {
        this.gbStreamId = gbStreamId;
    }
    public String getPlatformId() {
        return platformId;
    }
    public void setPlatformId(String platformId) {
        this.platformId = platformId;
    }
    public String getCatalogId() {
        return catalogId;
    }
    public void setCatalogId(String catalogId) {
        this.catalogId = catalogId;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformRegister.java
New file
@@ -0,0 +1,15 @@
package com.genersoft.iot.vmp.gb28181.bean;
public class PlatformRegister {
    // 未回复次数
    private int reply;
    public int getReply() {
        return reply;
    }
    public void setReply(int reply) {
        this.reply = reply;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/PresetQuerySipReq.java
New file
@@ -0,0 +1,28 @@
package com.genersoft.iot.vmp.gb28181.bean;
/**
 * @author chenjialing
 */
public class PresetQuerySipReq {
    private String presetId;
    private String presetName;
    public String getPresetId() {
        return presetId;
    }
    public void setPresetId(String presetId) {
        this.presetId = presetId;
    }
    public String getPresetName() {
        return presetName;
    }
    public void setPresetName(String presetName) {
        this.presetName = presetName;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordInfo.java
New file
@@ -0,0 +1,82 @@
package com.genersoft.iot.vmp.gb28181.bean;
import java.time.Instant;
import java.util.List;
/**
 * @description:设备录像信息bean
 * @author: swwheihei
 * @date:   2020年5月8日 下午2:05:56
 */
public class RecordInfo {
    private String deviceId;
    private String channelId;
    private String sn;
    private String name;
    private int sumNum;
    private Instant lastTime;
    private List<RecordItem> recordList;
    public String getDeviceId() {
        return deviceId;
    }
    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getSumNum() {
        return sumNum;
    }
    public void setSumNum(int sumNum) {
        this.sumNum = sumNum;
    }
    public List<RecordItem> getRecordList() {
        return recordList;
    }
    public void setRecordList(List<RecordItem> recordList) {
        this.recordList = recordList;
    }
    public String getChannelId() {
        return channelId;
    }
    public void setChannelId(String channelId) {
        this.channelId = channelId;
    }
    public String getSn() {
        return sn;
    }
    public void setSn(String sn) {
        this.sn = sn;
    }
    public Instant getLastTime() {
        return lastTime;
    }
    public void setLastTime(Instant lastTime) {
        this.lastTime = lastTime;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordItem.java
New file
@@ -0,0 +1,133 @@
package com.genersoft.iot.vmp.gb28181.bean;
import com.genersoft.iot.vmp.utils.DateUtil;
import org.jetbrains.annotations.NotNull;
import java.text.ParseException;
import java.time.Instant;
import java.time.temporal.TemporalAccessor;
/**
 * @description:设备录像bean
 * @author: swwheihei
 * @date:   2020年5月8日 下午2:06:54
 */
public class RecordItem  implements Comparable<RecordItem>{
    private String deviceId;
    private String name;
    private String filePath;
    private String fileSize;
    private String address;
    private String startTime;
    private String endTime;
    private int secrecy;
    private String type;
    private String recorderId;
    public String getDeviceId() {
        return deviceId;
    }
    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getFilePath() {
        return filePath;
    }
    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    public String getStartTime() {
        return startTime;
    }
    public void setStartTime(String startTime) {
        this.startTime = startTime;
    }
    public String getEndTime() {
        return endTime;
    }
    public void setEndTime(String endTime) {
        this.endTime = endTime;
    }
    public int getSecrecy() {
        return secrecy;
    }
    public void setSecrecy(int secrecy) {
        this.secrecy = secrecy;
    }
    public String getType() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }
    public String getRecorderId() {
        return recorderId;
    }
    public void setRecorderId(String recorderId) {
        this.recorderId = recorderId;
    }
    public String getFileSize() {
        return fileSize;
    }
    public void setFileSize(String fileSize) {
        this.fileSize = fileSize;
    }
    @Override
    public int compareTo(@NotNull RecordItem recordItem) {
        TemporalAccessor startTimeNow = DateUtil.formatter.parse(startTime);
        TemporalAccessor startTimeParam = DateUtil.formatter.parse(recordItem.getStartTime());
        Instant startTimeParamInstant = Instant.from(startTimeParam);
        Instant startTimeNowInstant = Instant.from(startTimeNow);
        if (startTimeNowInstant.equals(startTimeParamInstant)) {
            return 0;
        }else if (Instant.from(startTimeParam).isAfter(Instant.from(startTimeNow)) ) {
            return -1;
        }else {
            return 1;
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SDPInfo.java
New file
@@ -0,0 +1,14 @@
package com.genersoft.iot.vmp.gb28181.bean;
import javax.sdp.SessionDescription;
public class SDPInfo {
    private byte[] source;
    private SessionDescription sdpSource;
    private String sessionName;
    private Long startTime;
    private Long stopTime;
    private String username;
    private String address;
    private String ssrc;
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SendRtpItem.java
New file
@@ -0,0 +1,284 @@
package com.genersoft.iot.vmp.gb28181.bean;
import gov.nist.javax.sip.message.SIPRequest;
public class SendRtpItem {
    /**
     * 推流ip
     */
    private String ip;
    /**
     * 推流端口
     */
    private int port;
    /**
     * 推流标识
     */
    private String ssrc;
    /**
     * 平台id
     */
    private String platformId;
     /**
     * 对应设备id
     */
    private String deviceId;
    /**
     * 直播流的应用名
     */
    private String app;
   /**
     * 通道id
     */
    private String channelId;
    /**
     * 推流状态
     * 0 等待设备推流上来
     * 1 等待上级平台回复ack
     * 2 推流中
     */
    private int status = 0;
    /**
     * 设备推流的streamId
     */
    private String streamId;
    /**
     * 是否为tcp
     */
    private boolean tcp;
    /**
     * 是否为tcp主动模式
     */
    private boolean tcpActive;
    /**
     * 自己推流使用的端口
     */
    private int localPort;
    /**
     * 使用的流媒体
     */
    private String mediaServerId;
    /**
     * 使用的服务的ID
     */
    private String serverId;
    /**
     *  invite 的 callId
     */
    private String CallId;
    /**
     *  invite 的 fromTag
     */
    private String fromTag;
    /**
     *  invite 的 toTag
     */
    private String toTag;
    /**
     * 发送时,rtp的pt(uint8_t),不传时默认为96
     */
    private int pt = 96;
    /**
     * 发送时,rtp的负载类型。为true时,负载为ps;为false时,为es;
     */
    private boolean usePs = true;
    /**
     * 当usePs 为false时,有效。为1时,发送音频;为0时,发送视频;不传时默认为0
     */
    private boolean onlyAudio = false;
    /**
     * 播放类型
     */
    private InviteStreamType playType;
    public String getIp() {
        return ip;
    }
    public void setIp(String ip) {
        this.ip = ip;
    }
    public int getPort() {
        return port;
    }
    public void setPort(int port) {
        this.port = port;
    }
    public String getSsrc() {
        return ssrc;
    }
    public void setSsrc(String ssrc) {
        this.ssrc = ssrc;
    }
    public String getPlatformId() {
        return platformId;
    }
    public void setPlatformId(String platformId) {
        this.platformId = platformId;
    }
    public String getDeviceId() {
        return deviceId;
    }
    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }
    public String getChannelId() {
        return channelId;
    }
    public void setChannelId(String channelId) {
        this.channelId = channelId;
    }
    public int getStatus() {
        return status;
    }
    public void setStatus(int status) {
        this.status = status;
    }
    public String getApp() {
        return app;
    }
    public void setApp(String app) {
        this.app = app;
    }
    public String getStreamId() {
        return streamId;
    }
    public void setStreamId(String streamId) {
        this.streamId = streamId;
    }
    public boolean isTcp() {
        return tcp;
    }
    public void setTcp(boolean tcp) {
        this.tcp = tcp;
    }
    public int getLocalPort() {
        return localPort;
    }
    public void setLocalPort(int localPort) {
        this.localPort = localPort;
    }
    public boolean isTcpActive() {
        return tcpActive;
    }
    public void setTcpActive(boolean tcpActive) {
        this.tcpActive = tcpActive;
    }
    public String getMediaServerId() {
        return mediaServerId;
    }
    public void setMediaServerId(String mediaServerId) {
        this.mediaServerId = mediaServerId;
    }
    public String getCallId() {
        return CallId;
    }
    public void setCallId(String callId) {
        CallId = callId;
    }
    public InviteStreamType getPlayType() {
        return playType;
    }
    public void setPlayType(InviteStreamType playType) {
        this.playType = playType;
    }
    public int getPt() {
        return pt;
    }
    public void setPt(int pt) {
        this.pt = pt;
    }
    public boolean isUsePs() {
        return usePs;
    }
    public void setUsePs(boolean usePs) {
        this.usePs = usePs;
    }
    public boolean isOnlyAudio() {
        return onlyAudio;
    }
    public void setOnlyAudio(boolean onlyAudio) {
        this.onlyAudio = onlyAudio;
    }
    public String getServerId() {
        return serverId;
    }
    public void setServerId(String serverId) {
        this.serverId = serverId;
    }
    public String getFromTag() {
        return fromTag;
    }
    public void setFromTag(String fromTag) {
        this.fromTag = fromTag;
    }
    public String getToTag() {
        return toTag;
    }
    public void setToTag(String toTag) {
        this.toTag = toTag;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipMsgInfo.java
New file
@@ -0,0 +1,56 @@
package com.genersoft.iot.vmp.gb28181.bean;
import org.dom4j.Element;
import javax.sip.RequestEvent;
public class SipMsgInfo {
    private RequestEvent evt;
    private  Device device;
    private ParentPlatform platform;
    private Element rootElement;
    public SipMsgInfo(RequestEvent evt, Device device, Element rootElement) {
        this.evt = evt;
        this.device = device;
        this.rootElement = rootElement;
    }
    public SipMsgInfo(RequestEvent evt, ParentPlatform platform, Element rootElement) {
        this.evt = evt;
        this.platform = platform;
        this.rootElement = rootElement;
    }
    public RequestEvent getEvt() {
        return evt;
    }
    public void setEvt(RequestEvent evt) {
        this.evt = evt;
    }
    public Device getDevice() {
        return device;
    }
    public void setDevice(Device device) {
        this.device = device;
    }
    public ParentPlatform getPlatform() {
        return platform;
    }
    public void setPlatform(ParentPlatform platform) {
        this.platform = platform;
    }
    public Element getRootElement() {
        return rootElement;
    }
    public void setRootElement(Element rootElement) {
        this.rootElement = rootElement;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipTransactionInfo.java
New file
@@ -0,0 +1,54 @@
package com.genersoft.iot.vmp.gb28181.bean;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
public class SipTransactionInfo {
    private String callId;
    private String fromTag;
    private String toTag;
    private String viaBranch;
    public SipTransactionInfo(SIPResponse response) {
        this.callId = response.getCallIdHeader().getCallId();
        this.fromTag = response.getFromTag();
        this.toTag = response.getToTag();
        this.viaBranch = response.getTopmostViaHeader().getBranch();
    }
    public SipTransactionInfo() {
    }
    public String getCallId() {
        return callId;
    }
    public void setCallId(String callId) {
        this.callId = callId;
    }
    public String getFromTag() {
        return fromTag;
    }
    public void setFromTag(String fromTag) {
        this.fromTag = fromTag;
    }
    public String getToTag() {
        return toTag;
    }
    public void setToTag(String toTag) {
        this.toTag = toTag;
    }
    public String getViaBranch() {
        return viaBranch;
    }
    public void setViaBranch(String viaBranch) {
        this.viaBranch = viaBranch;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SsrcTransaction.java
New file
@@ -0,0 +1,81 @@
package com.genersoft.iot.vmp.gb28181.bean;
import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
public class SsrcTransaction {
    private String deviceId;
    private String channelId;
    private String callId;
    private String stream;
    private String mediaServerId;
    private String ssrc;
    private SipTransactionInfo sipTransactionInfo;
    private VideoStreamSessionManager.SessionType type;
    public String getDeviceId() {
        return deviceId;
    }
    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }
    public String getChannelId() {
        return channelId;
    }
    public void setChannelId(String channelId) {
        this.channelId = channelId;
    }
    public String getCallId() {
        return callId;
    }
    public void setCallId(String callId) {
        this.callId = callId;
    }
    public String getStream() {
        return stream;
    }
    public void setStream(String stream) {
        this.stream = stream;
    }
    public String getMediaServerId() {
        return mediaServerId;
    }
    public void setMediaServerId(String mediaServerId) {
        this.mediaServerId = mediaServerId;
    }
    public String getSsrc() {
        return ssrc;
    }
    public void setSsrc(String ssrc) {
        this.ssrc = ssrc;
    }
    public VideoStreamSessionManager.SessionType getType() {
        return type;
    }
    public void setType(VideoStreamSessionManager.SessionType type) {
        this.type = type;
    }
    public SipTransactionInfo getSipTransactionInfo() {
        return sipTransactionInfo;
    }
    public void setSipTransactionInfo(SipTransactionInfo sipTransactionInfo) {
        this.sipTransactionInfo = sipTransactionInfo;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeHolder.java
New file
@@ -0,0 +1,106 @@
package com.genersoft.iot.vmp.gb28181.bean;
import com.genersoft.iot.vmp.common.VideoManagerConstants;
import com.genersoft.iot.vmp.conf.DynamicTask;
import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask;
import com.genersoft.iot.vmp.gb28181.task.impl.MobilePositionSubscribeHandlerTask;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform;
import com.genersoft.iot.vmp.service.IPlatformService;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
 * @author lin
 */
@Component
public class SubscribeHolder {
    @Autowired
    private DynamicTask dynamicTask;
    private final String taskOverduePrefix = "subscribe_overdue_";
    private static ConcurrentHashMap<String, SubscribeInfo> catalogMap = new ConcurrentHashMap<>();
    private static ConcurrentHashMap<String, SubscribeInfo> mobilePositionMap = new ConcurrentHashMap<>();
    public void putCatalogSubscribe(String platformId, SubscribeInfo subscribeInfo) {
        catalogMap.put(platformId, subscribeInfo);
        // 添加订阅到期
        String taskOverdueKey = taskOverduePrefix +  "catalog_" + platformId;
        // 添加任务处理订阅过期
        dynamicTask.startDelay(taskOverdueKey, () -> removeCatalogSubscribe(subscribeInfo.getId()),
                subscribeInfo.getExpires() * 1000);
    }
    public SubscribeInfo getCatalogSubscribe(String platformId) {
        return catalogMap.get(platformId);
    }
    public void removeCatalogSubscribe(String platformId) {
        catalogMap.remove(platformId);
        String taskOverdueKey = taskOverduePrefix +  "catalog_" + platformId;
        Runnable runnable = dynamicTask.get(taskOverdueKey);
        if (runnable instanceof ISubscribeTask) {
            ISubscribeTask subscribeTask = (ISubscribeTask) runnable;
            subscribeTask.stop();
        }
        // 添加任务处理订阅过期
        dynamicTask.stop(taskOverdueKey);
    }
    public void putMobilePositionSubscribe(String platformId, SubscribeInfo subscribeInfo) {
        mobilePositionMap.put(platformId, subscribeInfo);
        String key = VideoManagerConstants.SIP_SUBSCRIBE_PREFIX +  "MobilePosition_" + platformId;
        // 添加任务处理GPS定时推送
        dynamicTask.startCron(key, new MobilePositionSubscribeHandlerTask(platformId),
                subscribeInfo.getGpsInterval() * 1000);
        String taskOverdueKey = taskOverduePrefix +  "MobilePosition_" + platformId;
        // 添加任务处理订阅过期
        dynamicTask.startDelay(taskOverdueKey, () -> {
                    removeMobilePositionSubscribe(subscribeInfo.getId());
                },
                subscribeInfo.getExpires() * 1000);
    }
    public SubscribeInfo getMobilePositionSubscribe(String platformId) {
        return mobilePositionMap.get(platformId);
    }
    public void removeMobilePositionSubscribe(String platformId) {
        mobilePositionMap.remove(platformId);
        String key = VideoManagerConstants.SIP_SUBSCRIBE_PREFIX +  "MobilePosition_" + platformId;
        // 结束任务处理GPS定时推送
        dynamicTask.stop(key);
        String taskOverdueKey = taskOverduePrefix +  "MobilePosition_" + platformId;
        Runnable runnable = dynamicTask.get(taskOverdueKey);
        if (runnable instanceof ISubscribeTask) {
            ISubscribeTask subscribeTask = (ISubscribeTask) runnable;
            subscribeTask.stop();
        }
        // 添加任务处理订阅过期
        dynamicTask.stop(taskOverdueKey);
    }
    public List<String> getAllCatalogSubscribePlatform() {
        List<String> platforms = new ArrayList<>();
        if(catalogMap.size() > 0) {
            for (String key : catalogMap.keySet()) {
                platforms.add(catalogMap.get(key).getId());
            }
        }
        return platforms;
    }
    public void removeAllSubscribe(String platformId) {
        removeMobilePositionSubscribe(platformId);
        removeCatalogSubscribe(platformId);
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeInfo.java
New file
@@ -0,0 +1,99 @@
package com.genersoft.iot.vmp.gb28181.bean;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import javax.sip.header.*;
public class SubscribeInfo {
    public SubscribeInfo(SIPRequest request, String id) {
        this.id = id;
        this.request = request;
        this.expires = request.getExpires().getExpires();
        EventHeader eventHeader = (EventHeader)request.getHeader(EventHeader.NAME);
        this.eventId = eventHeader.getEventId();
        this.eventType = eventHeader.getEventType();
    }
    private String id;
    private SIPRequest request;
    private int expires;
    private String eventId;
    private String eventType;
    private SIPResponse response;
    /**
     * 以下为可选字段
     * @return
     */
    private String sn;
    private int gpsInterval;
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public SIPRequest getRequest() {
        return request;
    }
    public void setRequest(SIPRequest request) {
        this.request = request;
    }
    public int getExpires() {
        return expires;
    }
    public void setExpires(int expires) {
        this.expires = expires;
    }
    public String getEventId() {
        return eventId;
    }
    public void setEventId(String eventId) {
        this.eventId = eventId;
    }
    public String getEventType() {
        return eventType;
    }
    public void setEventType(String eventType) {
        this.eventType = eventType;
    }
    public SIPResponse getResponse() {
        return response;
    }
    public void setResponse(SIPResponse response) {
        this.response = response;
    }
    public String getSn() {
        return sn;
    }
    public void setSn(String sn) {
        this.sn = sn;
    }
    public int getGpsInterval() {
        return gpsInterval;
    }
    public void setGpsInterval(int gpsInterval) {
        this.gpsInterval = gpsInterval;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SyncStatus.java
New file
@@ -0,0 +1,51 @@
package com.genersoft.iot.vmp.gb28181.bean;
import io.swagger.v3.oas.annotations.media.Schema;
/**
 * 摄像机同步状态
 * @author lin
 */
@Schema(description = "摄像机同步状态")
public class SyncStatus {
    @Schema(description = "总数")
    private int total;
    @Schema(description = "当前更新多少")
    private int current;
    @Schema(description = "错误描述")
    private String errorMsg;
    @Schema(description = "是否同步中")
    private boolean syncIng;
    public int getTotal() {
        return total;
    }
    public void setTotal(int total) {
        this.total = total;
    }
    public int getCurrent() {
        return current;
    }
    public void setCurrent(int current) {
        this.current = current;
    }
    public String getErrorMsg() {
        return errorMsg;
    }
    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }
    public boolean isSyncIng() {
        return syncIng;
    }
    public void setSyncIng(boolean syncIng) {
        this.syncIng = syncIng;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/TreeType.java
New file
@@ -0,0 +1,10 @@
package com.genersoft.iot.vmp.gb28181.bean;
/**
 * 目录结构类型
 * @author lin
 */
public class TreeType {
    public static final String BUSINESS_GROUP = "BusinessGroup";
    public static final String CIVIL_CODE = "CivilCode";
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/WvpSipDate.java
New file
@@ -0,0 +1,149 @@
package com.genersoft.iot.vmp.gb28181.bean;
import gov.nist.core.InternalErrorHandler;
import gov.nist.javax.sip.header.SIPDate;
import java.util.*;
/**
 * 重写jain sip的SIPDate解决与国标时间格式不一致的问题
 */
public class WvpSipDate extends SIPDate {
    /**
     *
     */
    private static final long serialVersionUID = 1L;
    private Calendar javaCal;
    public WvpSipDate(long timeMillis) {
        this.javaCal = new GregorianCalendar(TimeZone.getDefault(), Locale.getDefault());
        Date date = new Date(timeMillis);
        this.javaCal.setTime(date);
        this.wkday = this.javaCal.get(7);
        switch(this.wkday) {
            case 1:
                this.sipWkDay = "Sun";
                break;
            case 2:
                this.sipWkDay = "Mon";
                break;
            case 3:
                this.sipWkDay = "Tue";
                break;
            case 4:
                this.sipWkDay = "Wed";
                break;
            case 5:
                this.sipWkDay = "Thu";
                break;
            case 6:
                this.sipWkDay = "Fri";
                break;
            case 7:
                this.sipWkDay = "Sat";
                break;
            default:
                InternalErrorHandler.handleException("No date map for wkday " + this.wkday);
        }
        this.day = this.javaCal.get(5);
        this.month = this.javaCal.get(2);
        switch(this.month) {
            case 0:
                this.sipMonth = "Jan";
                break;
            case 1:
                this.sipMonth = "Feb";
                break;
            case 2:
                this.sipMonth = "Mar";
                break;
            case 3:
                this.sipMonth = "Apr";
                break;
            case 4:
                this.sipMonth = "May";
                break;
            case 5:
                this.sipMonth = "Jun";
                break;
            case 6:
                this.sipMonth = "Jul";
                break;
            case 7:
                this.sipMonth = "Aug";
                break;
            case 8:
                this.sipMonth = "Sep";
                break;
            case 9:
                this.sipMonth = "Oct";
                break;
            case 10:
                this.sipMonth = "Nov";
                break;
            case 11:
                this.sipMonth = "Dec";
                break;
            default:
                InternalErrorHandler.handleException("No date map for month " + this.month);
        }
        this.year = this.javaCal.get(1);
        this.hour = this.javaCal.get(11);
        this.minute = this.javaCal.get(12);
        this.second = this.javaCal.get(13);
    }
    @Override
    public StringBuilder encode(StringBuilder var1) {
        String var2;
        if (this.month < 9) {
            var2 = "0" + (this.month + 1);
        } else {
            var2 = "" + (this.month + 1);
        }
        String var3;
        if (this.day < 10) {
            var3 = "0" + this.day;
        } else {
            var3 = "" + this.day;
        }
        String var4;
        if (this.hour < 10) {
            var4 = "0" + this.hour;
        } else {
            var4 = "" + this.hour;
        }
        String var5;
        if (this.minute < 10) {
            var5 = "0" + this.minute;
        } else {
            var5 = "" + this.minute;
        }
        String var6;
        if (this.second < 10) {
            var6 = "0" + this.second;
        } else {
            var6 = "" + this.second;
        }
        int var8 = this.javaCal.get(14);
        String var7;
        if (var8 < 10) {
            var7 = "00" + var8;
        } else if (var8 < 100) {
            var7 = "0" + var8;
        } else {
            var7 = "" + var8;
        }
        return var1.append(this.year).append("-").append(var2).append("-").append(var3).append("T").append(var4).append(":").append(var5).append(":").append(var6).append(".").append(var7);
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/conf/DefaultProperties.java
New file
@@ -0,0 +1,46 @@
package com.genersoft.iot.vmp.gb28181.conf;
import java.util.Properties;
/**
 * 获取sip默认配置
 * @author lin
 */
public class DefaultProperties {
    public static Properties getProperties(String ip, boolean isDebug) {
        Properties properties = new Properties();
        properties.setProperty("javax.sip.STACK_NAME", "GB28181_SIP");
        properties.setProperty("javax.sip.IP_ADDRESS", ip);
        properties.setProperty("javax.sip.AUTOMATIC_DIALOG_SUPPORT", "off");
        /**
         * 完整配置参考 gov.nist.javax.sip.SipStackImpl,需要下载源码
         * gov/nist/javax/sip/SipStackImpl.class
         * sip消息的解析在 gov.nist.javax.sip.stack.UDPMessageChannel的processIncomingDataPacket方法
         */
//         * gov/nist/javax/sip/SipStackImpl.class
        if (isDebug) {
            properties.setProperty("gov.nist.javax.sip.LOG_MESSAGE_CONTENT", "false");
        }
        // 接收所有notify请求,即使没有订阅
        properties.setProperty("gov.nist.javax.sip.DELIVER_UNSOLICITED_NOTIFY", "true");
        properties.setProperty("gov.nist.javax.sip.AUTOMATIC_DIALOG_ERROR_HANDLING", "false");
        properties.setProperty("gov.nist.javax.sip.CANCEL_CLIENT_TRANSACTION_CHECKED", "false");
        // 为_NULL _对话框传递_终止的_事件
        properties.setProperty("gov.nist.javax.sip.DELIVER_TERMINATED_EVENT_FOR_NULL_DIALOG", "true");
        // 会话清理策略
        properties.setProperty("gov.nist.javax.sip.RELEASE_REFERENCES_STRATEGY", "Normal");
        // 处理由该服务器处理的基于底层TCP的保持生存超时
        properties.setProperty("gov.nist.javax.sip.RELIABLE_CONNECTION_KEEP_ALIVE_TIMEOUT", "60");
        // 获取实际内容长度,不使用header中的长度信息
        properties.setProperty("gov.nist.javax.sip.COMPUTE_CONTENT_LENGTH_FROM_MESSAGE_BODY", "true");
        /**
         * sip_server_log.log 和 sip_debug_log.log ERROR, INFO, WARNING, OFF, DEBUG, TRACE
         */
        properties.setProperty("gov.nist.javax.sip.TRACE_LEVEL", "ERROR");
        return properties;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/conf/SipLoggerPass.java
New file
@@ -0,0 +1,107 @@
package com.genersoft.iot.vmp.gb28181.conf;
import gov.nist.core.StackLogger;
import java.util.Properties;
/**
 * sip日志格式化
 * 暂不使用
 */
public class SipLoggerPass implements StackLogger {
    @Override
    public void logStackTrace() {
    }
    @Override
    public void logStackTrace(int traceLevel) {
    }
    @Override
    public int getLineCount() {
        return 0;
    }
    @Override
    public void logException(Throwable ex) {
    }
    @Override
    public void logDebug(String message) {
    }
    @Override
    public void logDebug(String message, Exception ex) {
    }
    @Override
    public void logTrace(String message) {
    }
    @Override
    public void logFatalError(String message) {
    }
    @Override
    public void logError(String message) {
    }
    @Override
    public boolean isLoggingEnabled() {
        return false;
    }
    @Override
    public boolean isLoggingEnabled(int logLevel) {
        return false;
    }
    @Override
    public void logError(String message, Exception ex) {
    }
    @Override
    public void logWarning(String string) {
    }
    @Override
    public void logInfo(String string) {
    }
    @Override
    public void disableLogging() {
    }
    @Override
    public void enableLogging() {
    }
    @Override
    public void setBuildTimeStamp(String buildTimeStamp) {
    }
    @Override
    public void setStackProperties(Properties stackProperties) {
    }
    @Override
    public String getLoggerName() {
        return null;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java
New file
@@ -0,0 +1,118 @@
package com.genersoft.iot.vmp.gb28181.event;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.event.device.RequestTimeoutEvent;
import com.genersoft.iot.vmp.gb28181.event.record.RecordEndEvent;
import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent;
import com.genersoft.iot.vmp.media.zlm.event.ZLMOfflineEvent;
import com.genersoft.iot.vmp.media.zlm.event.ZLMOnlineEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import com.genersoft.iot.vmp.gb28181.event.alarm.AlarmEvent;
import javax.sip.TimeoutEvent;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
 * @description:Event事件通知推送器,支持推送在线事件、离线事件
 * @author: swwheihei
 * @date:   2020年5月6日 上午11:30:50
 */
@Component
public class EventPublisher {
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;
    /**
     * 设备报警事件
     * @param deviceAlarm
     */
    public void deviceAlarmEventPublish(DeviceAlarm deviceAlarm) {
        AlarmEvent alarmEvent = new AlarmEvent(this);
        alarmEvent.setAlarmInfo(deviceAlarm);
        applicationEventPublisher.publishEvent(alarmEvent);
    }
    public void zlmOfflineEventPublish(String mediaServerId){
        ZLMOfflineEvent outEvent = new ZLMOfflineEvent(this);
        outEvent.setMediaServerId(mediaServerId);
        applicationEventPublisher.publishEvent(outEvent);
    }
    public void zlmOnlineEventPublish(String mediaServerId) {
        ZLMOnlineEvent outEvent = new ZLMOnlineEvent(this);
        outEvent.setMediaServerId(mediaServerId);
        applicationEventPublisher.publishEvent(outEvent);
    }
    public void catalogEventPublish(String platformId, DeviceChannel deviceChannel, String type) {
        List<DeviceChannel> deviceChannelList = new ArrayList<>();
        deviceChannelList.add(deviceChannel);
        catalogEventPublish(platformId, deviceChannelList, type);
    }
    public void requestTimeOut(TimeoutEvent timeoutEvent) {
        RequestTimeoutEvent requestTimeoutEvent = new RequestTimeoutEvent(this);
        requestTimeoutEvent.setTimeoutEvent(timeoutEvent);
        applicationEventPublisher.publishEvent(requestTimeoutEvent);
    }
    /**
     *
     * @param platformId
     * @param deviceChannels
     * @param type
     */
    public void catalogEventPublish(String platformId, List<DeviceChannel> deviceChannels, String type) {
        CatalogEvent outEvent = new CatalogEvent(this);
        List<DeviceChannel> channels = new ArrayList<>();
        if (deviceChannels.size() > 1) {
            // 数据去重
            Set<String> gbIdSet = new HashSet<>();
            for (DeviceChannel deviceChannel : deviceChannels) {
                if (!gbIdSet.contains(deviceChannel.getChannelId())) {
                    gbIdSet.add(deviceChannel.getChannelId());
                    channels.add(deviceChannel);
                }
            }
        }else {
            channels = deviceChannels;
        }
        outEvent.setDeviceChannels(channels);
        outEvent.setType(type);
        outEvent.setPlatformId(platformId);
        applicationEventPublisher.publishEvent(outEvent);
    }
    public void catalogEventPublishForStream(String platformId, List<GbStream> gbStreams, String type) {
        CatalogEvent outEvent = new CatalogEvent(this);
        outEvent.setGbStreams(gbStreams);
        outEvent.setType(type);
        outEvent.setPlatformId(platformId);
        applicationEventPublisher.publishEvent(outEvent);
    }
    public void catalogEventPublishForStream(String platformId, GbStream gbStream, String type) {
        List<GbStream> gbStreamList = new ArrayList<>();
        gbStreamList.add(gbStream);
        catalogEventPublishForStream(platformId, gbStreamList, type);
    }
    public void recordEndEventPush(RecordInfo recordInfo) {
        RecordEndEvent outEvent = new RecordEndEvent(this);
        outEvent.setRecordInfo(recordInfo);
        applicationEventPublisher.publishEvent(outEvent);
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java
New file
@@ -0,0 +1,172 @@
package com.genersoft.iot.vmp.gb28181.event;
import com.genersoft.iot.vmp.gb28181.bean.DeviceNotFoundEvent;
import gov.nist.javax.sip.message.SIPRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.sip.*;
import javax.sip.header.CallIdHeader;
import javax.sip.message.Response;
import java.text.ParseException;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
 * @author lin
 */
@Component
public class SipSubscribe {
    private final Logger logger = LoggerFactory.getLogger(SipSubscribe.class);
    private Map<String, SipSubscribe.Event> errorSubscribes = new ConcurrentHashMap<>();
    private Map<String, SipSubscribe.Event> okSubscribes = new ConcurrentHashMap<>();
    private Map<String, Instant> okTimeSubscribes = new ConcurrentHashMap<>();
    private Map<String, Instant> errorTimeSubscribes = new ConcurrentHashMap<>();
    //    @Scheduled(cron="*/5 * * * * ?")   //每五秒执行一次
    //    @Scheduled(fixedRate= 100 * 60 * 60 )
    @Scheduled(cron="0 0/5 * * * ?")   //每5分钟执行一次
    public void execute(){
        logger.info("[定时任务] 清理过期的SIP订阅信息");
        Instant instant = Instant.now().minusMillis(TimeUnit.MINUTES.toMillis(5));
        for (String key : okTimeSubscribes.keySet()) {
            if (okTimeSubscribes.get(key).isBefore(instant)){
                okSubscribes.remove(key);
                okTimeSubscribes.remove(key);
            }
        }
        for (String key : errorTimeSubscribes.keySet()) {
            if (errorTimeSubscribes.get(key).isBefore(instant)){
                errorSubscribes.remove(key);
                errorTimeSubscribes.remove(key);
            }
        }
        logger.debug("okTimeSubscribes.size:{}",okTimeSubscribes.size());
        logger.debug("okSubscribes.size:{}",okSubscribes.size());
        logger.debug("errorTimeSubscribes.size:{}",errorTimeSubscribes.size());
        logger.debug("errorSubscribes.size:{}",errorSubscribes.size());
    }
    public interface Event { void response(EventResult eventResult) ;
    }
    /**
     *
     */
    public enum EventResultType{
        // 超时
        timeout,
        // 回复
        response,
        // 事务已结束
        transactionTerminated,
        // 会话已结束
        dialogTerminated,
        // 设备未找到
        deviceNotFoundEvent
    }
    public static class EventResult<EventObject>{
        public int statusCode;
        public EventResultType type;
        public String msg;
        public String callId;
        public EventObject event;
        public EventResult(EventObject event) {
            this.event = event;
            if (event instanceof ResponseEvent) {
                ResponseEvent responseEvent = (ResponseEvent)event;
                Response response = responseEvent.getResponse();
                this.type = EventResultType.response;
                if (response != null) {
                    this.msg = response.getReasonPhrase();
                    this.statusCode = response.getStatusCode();
                }
                this.callId = ((CallIdHeader)response.getHeader(CallIdHeader.NAME)).getCallId();
            }else if (event instanceof TimeoutEvent) {
                TimeoutEvent timeoutEvent = (TimeoutEvent)event;
                this.type = EventResultType.timeout;
                this.msg = "消息超时未回复";
                this.statusCode = -1024;
                if (timeoutEvent.isServerTransaction()) {
                    this.callId = ((SIPRequest)timeoutEvent.getServerTransaction().getRequest()).getCallIdHeader().getCallId();
                }else {
                    this.callId = ((SIPRequest)timeoutEvent.getClientTransaction().getRequest()).getCallIdHeader().getCallId();
                }
            }else if (event instanceof TransactionTerminatedEvent) {
                TransactionTerminatedEvent transactionTerminatedEvent = (TransactionTerminatedEvent)event;
                this.type = EventResultType.transactionTerminated;
                this.msg = "事务已结束";
                this.statusCode = -1024;
                if (transactionTerminatedEvent.isServerTransaction()) {
                    this.callId = ((SIPRequest)transactionTerminatedEvent.getServerTransaction().getRequest()).getCallIdHeader().getCallId();
                }else {
                    this.callId = ((SIPRequest)transactionTerminatedEvent.getClientTransaction().getRequest()).getCallIdHeader().getCallId();
                }
            }else if (event instanceof DialogTerminatedEvent) {
                DialogTerminatedEvent dialogTerminatedEvent = (DialogTerminatedEvent)event;
                this.type = EventResultType.dialogTerminated;
                this.msg = "会话已结束";
                this.statusCode = -1024;
                this.callId = dialogTerminatedEvent.getDialog().getCallId().getCallId();
            }else if (event instanceof DeviceNotFoundEvent) {
                this.type = EventResultType.deviceNotFoundEvent;
                this.msg = "设备未找到";
                this.statusCode = -1024;
                this.callId = ((DeviceNotFoundEvent) event).getCallId();
            }
        }
    }
    public void addErrorSubscribe(String key, SipSubscribe.Event event) {
        errorSubscribes.put(key, event);
        errorTimeSubscribes.put(key, Instant.now());
    }
    public void addOkSubscribe(String key, SipSubscribe.Event event) {
        okSubscribes.put(key, event);
        okTimeSubscribes.put(key, Instant.now());
    }
    public SipSubscribe.Event getErrorSubscribe(String key) {
        return errorSubscribes.get(key);
    }
    public void removeErrorSubscribe(String key) {
        if(key == null){
            return;
        }
        errorSubscribes.remove(key);
        errorTimeSubscribes.remove(key);
    }
    public SipSubscribe.Event getOkSubscribe(String key) {
        return okSubscribes.get(key);
    }
    public void removeOkSubscribe(String key) {
        if(key == null){
            return;
        }
        okSubscribes.remove(key);
        okTimeSubscribes.remove(key);
    }
    public int getErrorSubscribesSize(){
        return errorSubscribes.size();
    }
    public int getOkSubscribesSize(){
        return okSubscribes.size();
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEvent.java
New file
@@ -0,0 +1,31 @@
package com.genersoft.iot.vmp.gb28181.event.alarm;
import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm;
import org.springframework.context.ApplicationEvent;
/**
 * @description: 报警事件
 * @author: lawrencehj
 * @data: 2021-01-20
 */
public class AlarmEvent extends ApplicationEvent {
    /**
     *
     */
    private static final long serialVersionUID = 1L;
    public AlarmEvent(Object source) {
        super(source);
    }
    private DeviceAlarm deviceAlarm;
    public DeviceAlarm getAlarmInfo() {
        return deviceAlarm;
    }
    public void setAlarmInfo(DeviceAlarm deviceAlarm) {
        this.deviceAlarm = deviceAlarm;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEventListener.java
New file
@@ -0,0 +1,57 @@
package com.genersoft.iot.vmp.gb28181.event.alarm;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * @description: 报警事件监听
 * @author: lawrencehj
 * @data: 2021-01-20
 */
@Component
public class AlarmEventListener implements ApplicationListener<AlarmEvent> {
    private final static Logger logger = LoggerFactory.getLogger(AlarmEventListener.class);
    private static Map<String, SseEmitter> sseEmitters = new Hashtable<>();
    public void addSseEmitters(String browserId, SseEmitter sseEmitter) {
        sseEmitters.put(browserId, sseEmitter);
    }
    @Override
    public void onApplicationEvent(AlarmEvent event) {
        if (logger.isDebugEnabled()) {
            logger.debug("设备报警事件触发,deviceId:" + event.getAlarmInfo().getDeviceId() + ", "
                    + event.getAlarmInfo().getAlarmDescription());
        }
        String msg = "<strong>设备编码:</strong> <i>" + event.getAlarmInfo().getDeviceId() + "</i>"
                    + "<br><strong>报警描述:</strong> <i>" + event.getAlarmInfo().getAlarmDescription() + "</i>"
                    + "<br><strong>报警时间:</strong> <i>" + event.getAlarmInfo().getAlarmTime() + "</i>"
                    + "<br><strong>报警位置:</strong> <i>" + event.getAlarmInfo().getLongitude() + "</i>"
                    + ", <i>" + event.getAlarmInfo().getLatitude() + "</i>";
        for (Iterator<Map.Entry<String, SseEmitter>> it = sseEmitters.entrySet().iterator(); it.hasNext();) {
            Map.Entry<String, SseEmitter> emitter = it.next();
            logger.info("推送到SSE连接,浏览器ID: " + emitter.getKey());
            try {
                emitter.getValue().send(msg);
            } catch (IOException | IllegalStateException e) {
                if (logger.isDebugEnabled()) {
                    logger.debug("SSE连接已关闭");
                }
                // 移除已关闭的连接
                it.remove();
            }
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/event/device/RequestTimeoutEvent.java
New file
@@ -0,0 +1,25 @@
package com.genersoft.iot.vmp.gb28181.event.device;
import org.springframework.context.ApplicationEvent;
import javax.sip.TimeoutEvent;
/**
 * @author lin
 */
public class RequestTimeoutEvent extends ApplicationEvent {
    public RequestTimeoutEvent(Object source) {
        super(source);
    }
    private TimeoutEvent timeoutEvent;
    public TimeoutEvent getTimeoutEvent() {
        return timeoutEvent;
    }
    public void setTimeoutEvent(TimeoutEvent timeoutEvent) {
        this.timeoutEvent = timeoutEvent;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/event/device/RequestTimeoutEventImpl.java
New file
@@ -0,0 +1,42 @@
package com.genersoft.iot.vmp.gb28181.event.device;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
import com.genersoft.iot.vmp.service.IDeviceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import javax.sip.ClientTransaction;
import javax.sip.address.SipURI;
import javax.sip.header.CallIdHeader;
import javax.sip.header.ToHeader;
import javax.sip.message.Request;
/**
 * @author lin
 */
@Component
public class RequestTimeoutEventImpl implements ApplicationListener<RequestTimeoutEvent> {
    @Autowired
    private IDeviceService deviceService;
    @Override
    public void onApplicationEvent(RequestTimeoutEvent event) {
        ClientTransaction clientTransaction = event.getTimeoutEvent().getClientTransaction();
        if (clientTransaction != null) {
            Request request = clientTransaction.getRequest();
            if (request != null) {
                String host = ((SipURI) request.getRequestURI()).getHost();
                int port = ((SipURI) request.getRequestURI()).getPort();
                Device device = deviceService.getDeviceByHostAndPort(host, port);
                if (device == null) {
                    return;
                }
                deviceService.offline(device.getDeviceId());
            }
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEvent.java
New file
@@ -0,0 +1,32 @@
package com.genersoft.iot.vmp.gb28181.event.record;
import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm;
import com.genersoft.iot.vmp.gb28181.bean.RecordInfo;
import org.springframework.context.ApplicationEvent;
/**
 * @description: 录像查询结束时间
 * @author: pan
 * @data: 2022-02-23
 */
public class RecordEndEvent extends ApplicationEvent {
    /**
     *
     */
    private static final long serialVersionUID = 1L;
    public RecordEndEvent(Object source) {
        super(source);
    }
    private RecordInfo recordInfo;
    public RecordInfo getRecordInfo() {
        return recordInfo;
    }
    public void setRecordInfo(RecordInfo recordInfo) {
        this.recordInfo = recordInfo;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEventListener.java
New file
@@ -0,0 +1,47 @@
package com.genersoft.iot.vmp.gb28181.event.record;
import com.genersoft.iot.vmp.gb28181.bean.RecordInfo;
import com.genersoft.iot.vmp.gb28181.bean.RecordItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.*;
/**
 * @description: 录像查询结束时间
 * @author: pan
 * @data: 2022-02-23
 */
@Component
public class RecordEndEventListener implements ApplicationListener<RecordEndEvent> {
    private final static Logger logger = LoggerFactory.getLogger(RecordEndEventListener.class);
    private static Map<String, SseEmitter> sseEmitters = new Hashtable<>();
    public interface RecordEndEventHandler{
        void  handler(RecordInfo recordInfo);
    }
    private Map<String, RecordEndEventHandler> handlerMap = new HashMap<>();
    @Override
    public void onApplicationEvent(RecordEndEvent event) {
        logger.info("录像查询完成事件触发,deviceId:{}, channelId: {}, 录像数量{}条", event.getRecordInfo().getDeviceId(),
                event.getRecordInfo().getChannelId(), event.getRecordInfo().getSumNum() );
        if (handlerMap.size() > 0) {
            for (RecordEndEventHandler recordEndEventHandler : handlerMap.values()) {
                recordEndEventHandler.handler(event.getRecordInfo());
            }
        }
    }
    public void addEndEventHandler(String device, String channelId, RecordEndEventHandler recordEndEventHandler) {
        handlerMap.put(device + channelId, recordEndEventHandler);
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEvent.java
New file
@@ -0,0 +1,85 @@
package com.genersoft.iot.vmp.gb28181.event.subscribe.catalog;
import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
import com.genersoft.iot.vmp.gb28181.bean.GbStream;
import org.springframework.context.ApplicationEvent;
import java.util.List;
public class CatalogEvent  extends ApplicationEvent {
    public CatalogEvent(Object source) {
        super(source);
    }
    /**
     * 上线
     */
    public static final String ON = "ON";
    /**
     * 离线
     */
    public static final String OFF = "OFF";
    /**
     * 视频丢失
     */
    public static final String VLOST = "VLOST";
    /**
     * 故障
     */
    public static final String DEFECT = "DEFECT";
    /**
     * 增加
     */
    public static final String ADD = "ADD";
    /**
     * 删除
     */
    public static final String DEL = "DEL";
    /**
     * 更新
     */
    public static final String UPDATE = "UPDATE";
    private List<DeviceChannel> deviceChannels;
    private List<GbStream> gbStreams;
    private String type;
    private String platformId;
    public List<DeviceChannel> getDeviceChannels() {
        return deviceChannels;
    }
    public void setDeviceChannels(List<DeviceChannel> deviceChannels) {
        this.deviceChannels = deviceChannels;
    }
    public String getType() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }
    public String getPlatformId() {
        return platformId;
    }
    public void setPlatformId(String platformId) {
        this.platformId = platformId;
    }
    public List<GbStream> getGbStreams() {
        return gbStreams;
    }
    public void setGbStreams(List<GbStream> gbStreams) {
        this.gbStreams = gbStreams;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEventLister.java
New file
@@ -0,0 +1,194 @@
package com.genersoft.iot.vmp.gb28181.event.subscribe.catalog;
import com.genersoft.iot.vmp.common.VideoManagerConstants;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory;
import com.genersoft.iot.vmp.service.IGbStreamService;
import com.genersoft.iot.vmp.service.IMediaServerService;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.sip.InvalidArgumentException;
import javax.sip.SipException;
import java.text.ParseException;
import java.util.*;
/**
 * catalog事件
 */
@Component
public class CatalogEventLister implements ApplicationListener<CatalogEvent> {
    private final static Logger logger = LoggerFactory.getLogger(CatalogEventLister.class);
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private SIPCommanderFroPlatform sipCommanderFroPlatform;
    @Autowired
    private IGbStreamService gbStreamService;
    @Autowired
    private SubscribeHolder subscribeHolder;
    @Override
    public void onApplicationEvent(CatalogEvent event) {
        SubscribeInfo subscribe = null;
        ParentPlatform parentPlatform = null;
        Map<String, List<ParentPlatform>> parentPlatformMap = new HashMap<>();
        if (!ObjectUtils.isEmpty(event.getPlatformId())) {
            subscribe = subscribeHolder.getCatalogSubscribe(event.getPlatformId());
            if (subscribe == null) {
                return;
            }
            parentPlatform = storager.queryParentPlatByServerGBId(event.getPlatformId());
            if (parentPlatform != null && !parentPlatform.isStatus()) {
                return;
            }
        }else {
            // 获取所用订阅
            List<String> platforms = subscribeHolder.getAllCatalogSubscribePlatform();
            if (event.getDeviceChannels() != null) {
                if (platforms.size() > 0) {
                    for (DeviceChannel deviceChannel : event.getDeviceChannels()) {
                        List<ParentPlatform> parentPlatformsForGB = storager.queryPlatFormListForGBWithGBId(deviceChannel.getChannelId(), platforms);
                        parentPlatformMap.put(deviceChannel.getChannelId(), parentPlatformsForGB);
                    }
                }
            }else if (event.getGbStreams() != null) {
                if (platforms.size() > 0) {
                    for (GbStream gbStream : event.getGbStreams()) {
                        if (gbStream == null || ObjectUtils.isEmpty(gbStream.getGbId())) {
                            continue;
                        }
                        List<ParentPlatform> parentPlatformsForGB = storager.queryPlatFormListForStreamWithGBId(gbStream.getApp(),gbStream.getStream(), platforms);
                        parentPlatformMap.put(gbStream.getGbId(), parentPlatformsForGB);
                    }
                }
            }
        }
        switch (event.getType()) {
            case CatalogEvent.ON:
            case CatalogEvent.OFF:
            case CatalogEvent.DEL:
                if (parentPlatform != null || subscribe != null) {
                    List<DeviceChannel> deviceChannelList = new ArrayList<>();
                    if (event.getDeviceChannels() != null) {
                        deviceChannelList.addAll(event.getDeviceChannels());
                    }
                    if (event.getGbStreams() != null && event.getGbStreams().size() > 0){
                        for (GbStream gbStream : event.getGbStreams()) {
                            DeviceChannel deviceChannelByStream = gbStreamService.getDeviceChannelListByStream(gbStream, gbStream.getCatalogId(), parentPlatform);
                            deviceChannelList.add(deviceChannelByStream);
                        }
                    }
                    if (deviceChannelList.size() > 0) {
                        logger.info("[Catalog事件: {}]平台:{},影响通道{}个", event.getType(), event.getPlatformId(), deviceChannelList.size());
                        try {
                            sipCommanderFroPlatform.sendNotifyForCatalogOther(event.getType(), parentPlatform, deviceChannelList, subscribe, null);
                        } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException |
                                 IllegalAccessException e) {
                            logger.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage());
                        }
                    }
                }else if (parentPlatformMap.keySet().size() > 0) {
                    for (String gbId : parentPlatformMap.keySet()) {
                        List<ParentPlatform> parentPlatforms = parentPlatformMap.get(gbId);
                        if (parentPlatforms != null && parentPlatforms.size() > 0) {
                            for (ParentPlatform platform : parentPlatforms) {
                                SubscribeInfo subscribeInfo = subscribeHolder.getCatalogSubscribe(platform.getServerGBId());
                                if (subscribeInfo == null) {
                                    continue;
                                }
                                logger.info("[Catalog事件: {}]平台:{},影响通道{}", event.getType(), platform.getServerGBId(), gbId);
                                List<DeviceChannel> deviceChannelList = new ArrayList<>();
                                DeviceChannel deviceChannel = new DeviceChannel();
                                deviceChannel.setChannelId(gbId);
                                deviceChannelList.add(deviceChannel);
                                try {
                                    sipCommanderFroPlatform.sendNotifyForCatalogOther(event.getType(), platform, deviceChannelList, subscribeInfo, null);
                                } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException |
                                         IllegalAccessException e) {
                                    logger.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage());
                                }
                            }
                        }
                    }
                }
                break;
            case CatalogEvent.VLOST:
                break;
            case CatalogEvent.DEFECT:
                break;
            case CatalogEvent.ADD:
            case CatalogEvent.UPDATE:
                if (parentPlatform != null || subscribe != null) {
                     List<DeviceChannel> deviceChannelList = new ArrayList<>();
                     if (event.getDeviceChannels() != null) {
                         deviceChannelList.addAll(event.getDeviceChannels());
                     }
                    if (event.getGbStreams() != null && event.getGbStreams().size() > 0){
                        for (GbStream gbStream : event.getGbStreams()) {
                            deviceChannelList.add(
                                    gbStreamService.getDeviceChannelListByStreamWithStatus(gbStream, gbStream.getCatalogId(), parentPlatform));
                        }
                    }
                    if (deviceChannelList.size() > 0) {
                        logger.info("[Catalog事件: {}]平台:{},影响通道{}个", event.getType(), event.getPlatformId(), deviceChannelList.size());
                        try {
                            sipCommanderFroPlatform.sendNotifyForCatalogAddOrUpdate(event.getType(), parentPlatform, deviceChannelList, subscribe, null);
                        } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException |
                                 IllegalAccessException e) {
                            logger.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage());
                        }
                    }
                }else if (parentPlatformMap.keySet().size() > 0) {
                    for (String gbId : parentPlatformMap.keySet()) {
                        List<ParentPlatform> parentPlatforms = parentPlatformMap.get(gbId);
                        if (parentPlatforms != null && parentPlatforms.size() > 0) {
                            for (ParentPlatform platform : parentPlatforms) {
                                SubscribeInfo subscribeInfo = subscribeHolder.getCatalogSubscribe(platform.getServerGBId());
                                if (subscribeInfo == null) {
                                    continue;
                                }
                                logger.info("[Catalog事件: {}]平台:{},影响通道{}", event.getType(), platform.getServerGBId(), gbId);
                                List<DeviceChannel> deviceChannelList = new ArrayList<>();
                                DeviceChannel deviceChannel = storager.queryChannelInParentPlatform(platform.getServerGBId(), gbId);
                                deviceChannelList.add(deviceChannel);
                                GbStream gbStream = storager.queryStreamInParentPlatform(platform.getServerGBId(), gbId);
                                if(gbStream != null){
                                    DeviceChannel deviceChannelByStream = gbStreamService.getDeviceChannelListByStreamWithStatus(gbStream, gbStream.getCatalogId(), platform);
                                    deviceChannelList.add(deviceChannelByStream);
                                }
                                try {
                                    sipCommanderFroPlatform.sendNotifyForCatalogAddOrUpdate(event.getType(), platform, deviceChannelList, subscribeInfo, null);
                                } catch (InvalidArgumentException | ParseException | NoSuchFieldException |
                                         SipException | IllegalAccessException e) {
                                    logger.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage());
                                }
                            }
                        }
                    }
                }
                break;
            default:
                break;
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/session/CatalogDataCatch.java
New file
@@ -0,0 +1,140 @@
package com.genersoft.iot.vmp.gb28181.session;
import com.genersoft.iot.vmp.gb28181.bean.CatalogData;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
import com.genersoft.iot.vmp.gb28181.bean.SyncStatus;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Component
public class CatalogDataCatch {
    public static Map<String, CatalogData> data = new ConcurrentHashMap<>();
    @Autowired
    private IVideoManagerStorage storager;
    public void addReady(Device device, int sn ) {
        CatalogData catalogData = data.get(device.getDeviceId());
        if (catalogData == null || catalogData.getStatus().equals(CatalogData.CatalogDataStatus.end)) {
            catalogData = new CatalogData();
            catalogData.setChannelList(Collections.synchronizedList(new ArrayList<>()));
            catalogData.setDevice(device);
            catalogData.setSn(sn);
            catalogData.setStatus(CatalogData.CatalogDataStatus.ready);
            catalogData.setLastTime(Instant.now());
            data.put(device.getDeviceId(), catalogData);
        }
    }
    public void put(String deviceId, int sn, int total, Device device, List<DeviceChannel> deviceChannelList) {
        CatalogData catalogData = data.get(deviceId);
        if (catalogData == null) {
            catalogData = new CatalogData();
            catalogData.setSn(sn);
            catalogData.setTotal(total);
            catalogData.setDevice(device);
            catalogData.setChannelList(Collections.synchronizedList(new ArrayList<>()));
            catalogData.setStatus(CatalogData.CatalogDataStatus.runIng);
            catalogData.setLastTime(Instant.now());
            data.put(deviceId, catalogData);
        }else {
            // 同一个设备的通道同步请求只考虑一个,其他的直接忽略
            if (catalogData.getSn() != sn) {
                return;
            }
            catalogData.setTotal(total);
            catalogData.setDevice(device);
            catalogData.setStatus(CatalogData.CatalogDataStatus.runIng);
            catalogData.getChannelList().addAll(deviceChannelList);
            catalogData.setLastTime(Instant.now());
        }
    }
    public List<DeviceChannel> get(String deviceId) {
        CatalogData catalogData = data.get(deviceId);
        if (catalogData == null) {
            return null;
        }
        return catalogData.getChannelList();
    }
    public int getTotal(String deviceId) {
        CatalogData catalogData = data.get(deviceId);
        if (catalogData == null) {
            return 0;
        }
        return catalogData.getTotal();
    }
    public SyncStatus getSyncStatus(String deviceId) {
        CatalogData catalogData = data.get(deviceId);
        if (catalogData == null) {
            return null;
        }
        SyncStatus syncStatus = new SyncStatus();
        syncStatus.setCurrent(catalogData.getChannelList().size());
        syncStatus.setTotal(catalogData.getTotal());
        syncStatus.setErrorMsg(catalogData.getErrorMsg());
        if (catalogData.getStatus().equals(CatalogData.CatalogDataStatus.end)) {
            syncStatus.setSyncIng(false);
        }else {
            syncStatus.setSyncIng(true);
        }
        return syncStatus;
    }
    public boolean isSyncRunning(String deviceId) {
        CatalogData catalogData = data.get(deviceId);
        if (catalogData == null) {
            return false;
        }
        return !catalogData.getStatus().equals(CatalogData.CatalogDataStatus.end);
    }
    @Scheduled(fixedRate = 5 * 1000)   //每5秒执行一次, 发现数据5秒未更新则移除数据并认为数据接收超时
    private void timerTask(){
        Set<String> keys = data.keySet();
        Instant instantBefore5S = Instant.now().minusMillis(TimeUnit.SECONDS.toMillis(5));
        Instant instantBefore30S = Instant.now().minusMillis(TimeUnit.SECONDS.toMillis(30));
        for (String deviceId : keys) {
            CatalogData catalogData = data.get(deviceId);
            if ( catalogData.getLastTime().isBefore(instantBefore5S)) { // 超过五秒收不到消息任务超时, 只更新这一部分数据
                if (catalogData.getStatus().equals(CatalogData.CatalogDataStatus.runIng)) {
                    storager.resetChannels(catalogData.getDevice().getDeviceId(), catalogData.getChannelList());
                    if (catalogData.getTotal() != catalogData.getChannelList().size()) {
                        String errorMsg = "更新成功,共" + catalogData.getTotal() + "条,已更新" + catalogData.getChannelList().size() + "条";
                        catalogData.setErrorMsg(errorMsg);
                    }
                }else if (catalogData.getStatus().equals(CatalogData.CatalogDataStatus.ready)) {
                    String errorMsg = "同步失败,等待回复超时";
                    catalogData.setErrorMsg(errorMsg);
                }
                catalogData.setStatus(CatalogData.CatalogDataStatus.end);
            }
            if (catalogData.getStatus().equals(CatalogData.CatalogDataStatus.end) && catalogData.getLastTime().isBefore(instantBefore30S)) { // 超过三十秒,如果标记为end则删除
                data.remove(deviceId);
            }
        }
    }
    public void setChannelSyncEnd(String deviceId, String errorMsg) {
        CatalogData catalogData = data.get(deviceId);
        if (catalogData == null) {
            return;
        }
        catalogData.setStatus(CatalogData.CatalogDataStatus.end);
        catalogData.setErrorMsg(errorMsg);
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/session/RecordDataCatch.java
New file
@@ -0,0 +1,87 @@
package com.genersoft.iot.vmp.gb28181.session;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
 * @author lin
 */
@Component
public class RecordDataCatch {
    public static Map<String, RecordInfo> data = new ConcurrentHashMap<>();
    @Autowired
    private DeferredResultHolder deferredResultHolder;
    public int put(String deviceId, String sn, int sumNum, List<RecordItem> recordItems) {
        String key = deviceId + sn;
        RecordInfo recordInfo = data.get(key);
        if (recordInfo == null) {
            recordInfo = new RecordInfo();
            recordInfo.setDeviceId(deviceId);
            recordInfo.setSn(sn.trim());
            recordInfo.setSumNum(sumNum);
            recordInfo.setRecordList(Collections.synchronizedList(new ArrayList<>()));
            recordInfo.setLastTime(Instant.now());
            recordInfo.getRecordList().addAll(recordItems);
            data.put(key, recordInfo);
        }else {
            // 同一个设备的通道同步请求只考虑一个,其他的直接忽略
            if (!Objects.equals(sn.trim(), recordInfo.getSn())) {
                return 0;
            }
            recordInfo.getRecordList().addAll(recordItems);
            recordInfo.setLastTime(Instant.now());
        }
        return recordInfo.getRecordList().size();
    }
    @Scheduled(fixedRate = 5 * 1000)   //每5秒执行一次, 发现数据5秒未更新则移除数据并认为数据接收超时
    private void timerTask(){
        Set<String> keys = data.keySet();
        // 获取五秒前的时刻
        Instant instantBefore5S = Instant.now().minusMillis(TimeUnit.SECONDS.toMillis(5));
        for (String key : keys) {
            RecordInfo recordInfo = data.get(key);
            // 超过五秒收不到消息任务超时, 只更新这一部分数据
            if ( recordInfo.getLastTime().isBefore(instantBefore5S)) {
                // 处理录像数据, 返回给前端
                String msgKey = DeferredResultHolder.CALLBACK_CMD_RECORDINFO + recordInfo.getDeviceId() + recordInfo.getSn();
                // 对数据进行排序
                Collections.sort(recordInfo.getRecordList());
                RequestMessage msg = new RequestMessage();
                msg.setKey(msgKey);
                msg.setData(recordInfo);
                deferredResultHolder.invokeAllResult(msg);
                data.remove(key);
            }
        }
    }
    public boolean isComplete(String deviceId, String sn) {
        RecordInfo recordInfo = data.get(deviceId + sn);
        return recordInfo != null && recordInfo.getRecordList().size() == recordInfo.getSumNum();
    }
    public RecordInfo getRecordInfo(String deviceId, String sn) {
        return data.get(deviceId + sn);
    }
    public void remove(String deviceId, String sn) {
        data.remove(deviceId + sn);
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/session/SsrcConfig.java
New file
@@ -0,0 +1,150 @@
package com.genersoft.iot.vmp.gb28181.session;
import com.genersoft.iot.vmp.utils.ConfigConst;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Set;
@Schema(description = "ssrc信息")
public class SsrcConfig {
    /**
     * zlm流媒体服务器Id
     */
    @Schema(description = "流媒体服务器Id")
    private String mediaServerId;
    @Schema(description = "SSRC前缀")
    private String ssrcPrefix;
    /**
     * zlm流媒体服务器已用会话句柄
     */
    @Schema(description = "zlm流媒体服务器已用会话句柄")
    private List<String> isUsed;
    /**
     * zlm流媒体服务器可用会话句柄
     */
    @Schema(description = "zlm流媒体服务器可用会话句柄")
    private List<String> notUsed;
    public SsrcConfig() {
    }
    public SsrcConfig(String mediaServerId, Set<String> usedSet, String sipDomain) {
        this.mediaServerId = mediaServerId;
        this.isUsed = new ArrayList<>();
        this.ssrcPrefix = sipDomain.substring(3, 8);
        this.notUsed = new ArrayList<>();
        for (int i = 1; i < ConfigConst.MAX_STRTEAM_COUNT; i++) {
            String ssrc;
            if (i < 10) {
                ssrc = "000" + i;
            } else if (i < 100) {
                ssrc = "00" + i;
            } else if (i < 1000) {
                ssrc = "0" + i;
            } else {
                ssrc = String.valueOf(i);
            }
            if (null == usedSet || !usedSet.contains(ssrc)) {
                this.notUsed.add(ssrc);
            } else {
                this.isUsed.add(ssrc);
            }
        }
    }
    /**
     * 获取视频预览的SSRC值,第一位固定为0
     * @return ssrc
     */
    public String getPlaySsrc() {
        return "0" + getSsrcPrefix() + getSN();
    }
    /**
     * 获取录像回放的SSRC值,第一位固定为1
     *
     */
    public String getPlayBackSsrc() {
        return "1" + getSsrcPrefix() + getSN();
    }
    /**
     * 释放ssrc,主要用完的ssrc一定要释放,否则会耗尽
     * @param ssrc 需要重置的ssrc
     */
    public void releaseSsrc(String ssrc) {
        if (ssrc == null) {
            return;
        }
        String sn = ssrc.substring(6);
        try {
            isUsed.remove(sn);
            notUsed.add(sn);
        }catch (NullPointerException e){
        }
    }
    /**
     * 获取后四位数SN,随机数
     *
     */
    private String getSN() {
        String sn = null;
        int index = 0;
        if (notUsed.size() == 0) {
            throw new RuntimeException("ssrc已经用完");
        } else if (notUsed.size() == 1) {
            sn = notUsed.get(0);
        } else {
            index = new Random().nextInt(notUsed.size() - 1);
            sn = notUsed.get(index);
        }
        notUsed.remove(index);
        isUsed.add(sn);
        return sn;
    }
    public String getSsrcPrefix() {
        return ssrcPrefix;
    }
    public String getMediaServerId() {
        return mediaServerId;
    }
    public void setMediaServerId(String mediaServerId) {
        this.mediaServerId = mediaServerId;
    }
    public void setSsrcPrefix(String ssrcPrefix) {
        this.ssrcPrefix = ssrcPrefix;
    }
    public List<String> getIsUsed() {
        return isUsed;
    }
    public void setIsUsed(List<String> isUsed) {
        this.isUsed = isUsed;
    }
    public List<String> getNotUsed() {
        return notUsed;
    }
    public void setNotUsed(List<String> notUsed) {
        this.notUsed = notUsed;
    }
    public boolean checkSsrc(String ssrcInResponse) {
        return !isUsed.contains(ssrcInResponse);
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/session/VideoStreamSessionManager.java
New file
@@ -0,0 +1,143 @@
package com.genersoft.iot.vmp.gb28181.session;
import java.util.ArrayList;
import java.util.List;
import com.genersoft.iot.vmp.common.VideoManagerConstants;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo;
import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction;
import com.genersoft.iot.vmp.utils.redis.RedisUtil;
import gov.nist.javax.sip.message.SIPResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
/**
 * @description:视频流session管理器,管理视频预览、预览回放的通信句柄
 * @author: swwheihei
 * @date:   2020年5月13日 下午4:03:02
 */
@Component
public class VideoStreamSessionManager {
    @Autowired
    private UserSetting userSetting;
    public enum SessionType {
        play,
        playback,
        download
    }
    /**
     * 添加一个点播/回放的事务信息
     * 后续可以通过流Id/callID
     * @param deviceId 设备ID
     * @param channelId 通道ID
     * @param callId 一次请求的CallID
     * @param stream 流名称
     * @param mediaServerId 所使用的流媒体ID
     * @param response 回复
     */
    public void put(String deviceId, String channelId, String callId, String stream, String ssrc, String mediaServerId, SIPResponse response, SessionType type){
        SsrcTransaction ssrcTransaction = new SsrcTransaction();
        ssrcTransaction.setDeviceId(deviceId);
        ssrcTransaction.setChannelId(channelId);
        ssrcTransaction.setStream(stream);
        ssrcTransaction.setSipTransactionInfo(new SipTransactionInfo(response));
        ssrcTransaction.setCallId(callId);
        ssrcTransaction.setSsrc(ssrc);
        ssrcTransaction.setMediaServerId(mediaServerId);
        ssrcTransaction.setType(type);
        RedisUtil.set(VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId()
                + "_" +  deviceId + "_" + channelId + "_" + callId + "_" + stream, ssrcTransaction);
        RedisUtil.set(VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId()
                + "_" +  deviceId + "_" + channelId + "_" + callId + "_" + stream, ssrcTransaction);
    }
    public SsrcTransaction getSsrcTransaction(String deviceId, String channelId, String callId, String stream){
        if (ObjectUtils.isEmpty(deviceId)) {
            deviceId ="*";
        }
        if (ObjectUtils.isEmpty(channelId)) {
            channelId ="*";
        }
        if (ObjectUtils.isEmpty(callId)) {
            callId ="*";
        }
        if (ObjectUtils.isEmpty(stream)) {
            stream ="*";
        }
        String key = VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_" + deviceId + "_" + channelId + "_" + callId+ "_" + stream;
        List<Object> scanResult = RedisUtil.scan(key);
        if (scanResult.size() == 0) {
            return null;
        }
        return (SsrcTransaction)RedisUtil.get((String) scanResult.get(0));
    }
    public List<SsrcTransaction> getSsrcTransactionForAll(String deviceId, String channelId, String callId, String stream){
        if (ObjectUtils.isEmpty(deviceId)) {
            deviceId ="*";
        }
        if (ObjectUtils.isEmpty(channelId)) {
            channelId ="*";
        }
        if (ObjectUtils.isEmpty(callId)) {
            callId ="*";
        }
        if (ObjectUtils.isEmpty(stream)) {
            stream ="*";
        }
        String key = VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_" + deviceId + "_" + channelId + "_" + callId+ "_" + stream;
        List<Object> scanResult = RedisUtil.scan(key);
        if (scanResult.size() == 0) {
            return null;
        }
        List<SsrcTransaction> result = new ArrayList<>();
        for (Object keyObj : scanResult) {
            result.add((SsrcTransaction)RedisUtil.get((String) keyObj));
        }
        return result;
    }
    public String getMediaServerId(String deviceId, String channelId, String stream){
        SsrcTransaction ssrcTransaction = getSsrcTransaction(deviceId, channelId, null, stream);
        if (ssrcTransaction == null) {
            return null;
        }
        return ssrcTransaction.getMediaServerId();
    }
    public String getSSRC(String deviceId, String channelId, String stream){
        SsrcTransaction ssrcTransaction = getSsrcTransaction(deviceId, channelId, null, stream);
        if (ssrcTransaction == null) {
            return null;
        }
        return ssrcTransaction.getSsrc();
    }
    public void remove(String deviceId, String channelId, String stream) {
        SsrcTransaction ssrcTransaction = getSsrcTransaction(deviceId, channelId, null, stream);
        if (ssrcTransaction == null) {
            return;
        }
        RedisUtil.del(VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_"
                +  deviceId + "_" + channelId + "_" + ssrcTransaction.getCallId() + "_" + ssrcTransaction.getStream());
    }
    public List<SsrcTransaction> getAllSsrc() {
        List<Object> ssrcTransactionKeys = RedisUtil.scan(String.format("%s_*_*_*_*", VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX+ userSetting.getServerId()));
        List<SsrcTransaction> result= new ArrayList<>();
        for (int i = 0; i < ssrcTransactionKeys.size(); i++) {
            String key = (String)ssrcTransactionKeys.get(i);
            SsrcTransaction ssrcTransaction = (SsrcTransaction)RedisUtil.get(key);
            result.add(ssrcTransaction);
        }
        return result;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/task/ISubscribeTask.java
New file
@@ -0,0 +1,10 @@
package com.genersoft.iot.vmp.gb28181.task;
import javax.sip.DialogState;
/**
 * @author lin
 */
public interface ISubscribeTask extends Runnable{
    void stop();
}
src/main/java/com/genersoft/iot/vmp/gb28181/task/SipDeviceRunner.java
New file
@@ -0,0 +1,51 @@
package com.genersoft.iot.vmp.gb28181.task;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.service.IDeviceService;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
 * 系统启动时控制设备
 * @author lin
 */
@Component
@Order(value=4)
public class SipDeviceRunner implements CommandLineRunner {
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Autowired
    private UserSetting userSetting;
    @Autowired
    private IDeviceService deviceService;
    @Override
    public void run(String... args) throws Exception {
        List<Device> deviceList = deviceService.getAllOnlineDevice();
        for (Device device : deviceList) {
            if (deviceService.expire(device)){
                deviceService.offline(device.getDeviceId());
            }else {
                deviceService.online(device);
            }
        }
        // 重置cseq计数
        redisCatchStorage.resetAllCSEQ();
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/task/impl/CatalogSubscribeTask.java
New file
@@ -0,0 +1,105 @@
package com.genersoft.iot.vmp.gb28181.task.impl;
import com.genersoft.iot.vmp.conf.DynamicTask;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
import gov.nist.javax.sip.message.SIPRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import javax.sip.*;
import javax.sip.header.ToHeader;
import java.text.ParseException;
import java.util.Timer;
import java.util.TimerTask;
/**
 * 目录订阅任务
 * @author lin
 */
public class CatalogSubscribeTask implements ISubscribeTask {
    private final Logger logger = LoggerFactory.getLogger(CatalogSubscribeTask.class);
    private Device device;
    private final ISIPCommander sipCommander;
    private SIPRequest request;
    private DynamicTask dynamicTask;
    private String taskKey = "catalog-subscribe-timeout";
    public CatalogSubscribeTask(Device device, ISIPCommander sipCommander, DynamicTask dynamicTask) {
        this.device = device;
        this.sipCommander = sipCommander;
        this.dynamicTask = dynamicTask;
    }
    @Override
    public void run() {
        if (dynamicTask.get(taskKey) != null) {
            dynamicTask.stop(taskKey);
        }
        SIPRequest sipRequest = null;
        try {
            sipRequest = sipCommander.catalogSubscribe(device, request, eventResult -> {
                ResponseEvent event = (ResponseEvent) eventResult.event;
                // 成功
                logger.info("[目录订阅]成功: {}", device.getDeviceId());
                ToHeader toHeader = (ToHeader)event.getResponse().getHeader(ToHeader.NAME);
                try {
                    this.request.getToHeader().setTag(toHeader.getTag());
                } catch (ParseException e) {
                    logger.info("[目录订阅]成功: 但为request设置ToTag失败");
                    this.request = null;
                }
            },eventResult -> {
                this.request = null;
                // 失败
                logger.warn("[目录订阅]失败,信令发送失败: {}-{} ", device.getDeviceId(), eventResult.msg);
                dynamicTask.startDelay(taskKey, CatalogSubscribeTask.this, 2000);
            });
        } catch (InvalidArgumentException | SipException | ParseException e) {
            logger.error("[命令发送失败] 目录订阅: {}", e.getMessage());
        }
        if (sipRequest != null) {
            this.request = sipRequest;
        }
    }
    @Override
    public void stop() {
        /**
         * dialog 的各个状态
         * EARLY-> Early state状态-初始请求发送以后,收到了一个临时响应消息
         * CONFIRMED-> Confirmed Dialog状态-已确认
         * COMPLETED-> Completed Dialog状态-已完成
         * TERMINATED-> Terminated Dialog状态-终止
         */
        logger.info("取消目录订阅时dialog状态为{}", DialogState.CONFIRMED);
        if (dynamicTask.get(taskKey) != null) {
            dynamicTask.stop(taskKey);
        }
        device.setSubscribeCycleForCatalog(0);
        try {
            sipCommander.catalogSubscribe(device, request, eventResult -> {
                ResponseEvent event = (ResponseEvent) eventResult.event;
                if (event.getResponse().getRawContent() != null) {
                    // 成功
                    logger.info("[取消目录订阅订阅]成功: {}", device.getDeviceId());
                }else {
                    // 成功
                    logger.info("[取消目录订阅订阅]成功: {}", device.getDeviceId());
                }
            },eventResult -> {
                // 失败
                logger.warn("[取消目录订阅订阅]失败,信令发送失败: {}-{} ", device.getDeviceId(), eventResult.msg);
            });
        } catch (InvalidArgumentException | SipException | ParseException e) {
            logger.error("[命令发送失败] 取消目录订阅订阅: {}", e.getMessage());
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/task/impl/MobilePositionSubscribeHandlerTask.java
New file
@@ -0,0 +1,44 @@
package com.genersoft.iot.vmp.gb28181.task.impl;
import com.genersoft.iot.vmp.conf.DynamicTask;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform;
import com.genersoft.iot.vmp.service.IPlatformService;
import com.genersoft.iot.vmp.service.bean.GPSMsgInfo;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import com.genersoft.iot.vmp.utils.SpringBeanFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import javax.sip.DialogState;
import java.util.List;
/**
 * 向已经订阅(移动位置)的上级发送MobilePosition消息
 * @author lin
 */
public class MobilePositionSubscribeHandlerTask implements ISubscribeTask {
    private IPlatformService platformService;
    private String platformId;
    public MobilePositionSubscribeHandlerTask(String platformId) {
        this.platformService = SpringBeanFactory.getBean("platformServiceImpl");
        this.platformId = platformId;
    }
    @Override
    public void run() {
        platformService.sendNotifyMobilePosition(this.platformId);
    }
    @Override
    public void stop() {
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/task/impl/MobilePositionSubscribeTask.java
New file
@@ -0,0 +1,103 @@
package com.genersoft.iot.vmp.gb28181.task.impl;
import com.genersoft.iot.vmp.conf.DynamicTask;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import javax.sip.*;
import javax.sip.header.ToHeader;
import java.text.ParseException;
import java.util.Timer;
import java.util.TimerTask;
/**
 * 移动位置订阅的定时更新
 * @author lin
 */
public class MobilePositionSubscribeTask implements ISubscribeTask {
    private final Logger logger = LoggerFactory.getLogger(MobilePositionSubscribeTask.class);
    private Device device;
    private ISIPCommander sipCommander;
    private SIPRequest request;
    private DynamicTask dynamicTask;
    private String taskKey = "mobile-position-subscribe-timeout";
    public MobilePositionSubscribeTask(Device device, ISIPCommander sipCommander, DynamicTask dynamicTask) {
        this.device = device;
        this.sipCommander = sipCommander;
        this.dynamicTask = dynamicTask;
    }
    @Override
    public void run() {
        if (dynamicTask.get(taskKey) != null) {
            dynamicTask.stop(taskKey);
        }
        SIPRequest sipRequest = null;
        try {
            sipRequest = sipCommander.mobilePositionSubscribe(device, request, eventResult -> {
                // 成功
                logger.info("[移动位置订阅]成功: {}", device.getDeviceId());
                ResponseEvent event = (ResponseEvent) eventResult.event;
                ToHeader toHeader = (ToHeader)event.getResponse().getHeader(ToHeader.NAME);
                try {
                    this.request.getToHeader().setTag(toHeader.getTag());
                } catch (ParseException e) {
                    logger.info("[移动位置订阅]成功: 为request设置ToTag失败");
                    this.request = null;
                }
            },eventResult -> {
                this.request = null;
                // 失败
                logger.warn("[移动位置订阅]失败,信令发送失败: {}-{} ", device.getDeviceId(), eventResult.msg);
                dynamicTask.startDelay(taskKey, MobilePositionSubscribeTask.this, 2000);
            });
        } catch (InvalidArgumentException | SipException | ParseException e) {
            logger.error("[命令发送失败] 移动位置订阅: {}", e.getMessage());
        }
        if (sipRequest != null) {
            this.request = sipRequest;
        }
    }
    @Override
    public void stop() {
        /**
         * dialog 的各个状态
         * EARLY-> Early state状态-初始请求发送以后,收到了一个临时响应消息
         * CONFIRMED-> Confirmed Dialog状态-已确认
         * COMPLETED-> Completed Dialog状态-已完成
         * TERMINATED-> Terminated Dialog状态-终止
         */
        if (dynamicTask.get(taskKey) != null) {
            dynamicTask.stop(taskKey);
        }
        device.setSubscribeCycleForMobilePosition(0);
        try {
            sipCommander.mobilePositionSubscribe(device, request, eventResult -> {
                ResponseEvent event = (ResponseEvent) eventResult.event;
                if (event.getResponse().getRawContent() != null) {
                    // 成功
                    logger.info("[取消移动位置订阅]成功: {}", device.getDeviceId());
                }else {
                    // 成功
                    logger.info("[取消移动位置订阅]成功: {}", device.getDeviceId());
                }
            },eventResult -> {
                // 失败
                logger.warn("[取消移动位置订阅]失败,信令发送失败: {}-{} ", device.getDeviceId(), eventResult.msg);
            });
        } catch (InvalidArgumentException | SipException | ParseException e) {
            logger.error("[命令发送失败] 取消移动位置订阅: {}", e.getMessage());
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/ISIPProcessorObserver.java
New file
@@ -0,0 +1,6 @@
package com.genersoft.iot.vmp.gb28181.transmit;
import javax.sip.SipListener;
public interface ISIPProcessorObserver extends SipListener {
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPProcessorObserver.java
New file
@@ -0,0 +1,202 @@
package com.genersoft.iot.vmp.gb28181.transmit;
import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor;
import com.genersoft.iot.vmp.gb28181.transmit.event.response.ISIPResponseProcessor;
import com.genersoft.iot.vmp.gb28181.transmit.event.timeout.ITimeoutProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import javax.sip.*;
import javax.sip.header.*;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
 * @description: SIP信令处理类观察者
 * @author: panlinlin
 * @date:   2021年11月5日 下午15:32
 */
@Component
public class SIPProcessorObserver implements ISIPProcessorObserver {
    private final static Logger logger = LoggerFactory.getLogger(SIPProcessorObserver.class);
    private static Map<String,  ISIPRequestProcessor> requestProcessorMap = new ConcurrentHashMap<>();
    private static Map<String, ISIPResponseProcessor> responseProcessorMap = new ConcurrentHashMap<>();
    private static ITimeoutProcessor timeoutProcessor;
    @Autowired
    private SipSubscribe sipSubscribe;
    @Autowired
    private EventPublisher eventPublisher;
    /**
     * 添加 request订阅
     * @param method 方法名
     * @param processor 处理程序
     */
    public void addRequestProcessor(String method, ISIPRequestProcessor processor) {
        requestProcessorMap.put(method, processor);
    }
    /**
     * 添加 response订阅
     * @param method 方法名
     * @param processor 处理程序
     */
    public void addResponseProcessor(String method, ISIPResponseProcessor processor) {
        responseProcessorMap.put(method, processor);
    }
    /**
     * 添加 超时事件订阅
     * @param processor 处理程序
     */
    public void addTimeoutProcessor(ITimeoutProcessor processor) {
        timeoutProcessor = processor;
    }
    /**
     * 分发RequestEvent事件
     * @param requestEvent RequestEvent事件
     */
    @Override
    @Async("taskExecutor")
    public void processRequest(RequestEvent requestEvent) {
        String method = requestEvent.getRequest().getMethod();
        ISIPRequestProcessor sipRequestProcessor = requestProcessorMap.get(method);
        if (sipRequestProcessor == null) {
            logger.warn("不支持方法{}的request", method);
            return;
        }
        requestProcessorMap.get(method).process(requestEvent);
    }
    /**
     * 分发ResponseEvent事件
     * @param responseEvent responseEvent事件
     */
    @Override
    @Async("taskExecutor")
    public void processResponse(ResponseEvent responseEvent) {
        Response response = responseEvent.getResponse();
        int status = response.getStatusCode();
        // Success
        if (((status >= Response.OK) && (status < Response.MULTIPLE_CHOICES)) || status == Response.UNAUTHORIZED) {
            CSeqHeader cseqHeader = (CSeqHeader) responseEvent.getResponse().getHeader(CSeqHeader.NAME);
            String method = cseqHeader.getMethod();
            ISIPResponseProcessor sipRequestProcessor = responseProcessorMap.get(method);
            if (sipRequestProcessor != null) {
                sipRequestProcessor.process(responseEvent);
            }
            if (status != Response.UNAUTHORIZED && responseEvent.getResponse() != null && sipSubscribe.getOkSubscribesSize() > 0 ) {
                CallIdHeader callIdHeader = (CallIdHeader)responseEvent.getResponse().getHeader(CallIdHeader.NAME);
                if (callIdHeader != null) {
                    SipSubscribe.Event subscribe = sipSubscribe.getOkSubscribe(callIdHeader.getCallId());
                    if (subscribe != null) {
                        SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(responseEvent);
                        sipSubscribe.removeOkSubscribe(callIdHeader.getCallId());
                        subscribe.response(eventResult);
                    }
                }
            }
        } else if ((status >= Response.TRYING) && (status < Response.OK)) {
            // 增加其它无需回复的响应,如101、180等
        } else {
            logger.warn("接收到失败的response响应!status:" + status + ",message:" + response.getReasonPhrase());
            if (responseEvent.getResponse() != null && sipSubscribe.getErrorSubscribesSize() > 0 ) {
                CallIdHeader callIdHeader = (CallIdHeader)responseEvent.getResponse().getHeader(CallIdHeader.NAME);
                if (callIdHeader != null) {
                    SipSubscribe.Event subscribe = sipSubscribe.getErrorSubscribe(callIdHeader.getCallId());
                    if (subscribe != null) {
                        SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(responseEvent);
                        subscribe.response(eventResult);
                        sipSubscribe.removeErrorSubscribe(callIdHeader.getCallId());
                    }
                }
            }
            if (responseEvent.getDialog() != null) {
                responseEvent.getDialog().delete();
            }
        }
    }
    /**
     * 向超时订阅发送消息
     * @param timeoutEvent timeoutEvent事件
     */
    @Override
    public void processTimeout(TimeoutEvent timeoutEvent) {
        logger.info("[消息发送超时]");
        ClientTransaction clientTransaction = timeoutEvent.getClientTransaction();
        if (clientTransaction != null) {
            logger.info("[发送错误订阅] clientTransaction != null");
            Request request = clientTransaction.getRequest();
            if (request != null) {
                logger.info("[发送错误订阅] request != null");
                CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME);
                if (callIdHeader != null) {
                    logger.info("[发送错误订阅]");
                    SipSubscribe.Event subscribe = sipSubscribe.getErrorSubscribe(callIdHeader.getCallId());
                    SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(timeoutEvent);
                    if (subscribe != null){
                        subscribe.response(eventResult);
                    }
                    sipSubscribe.removeOkSubscribe(callIdHeader.getCallId());
                    sipSubscribe.removeErrorSubscribe(callIdHeader.getCallId());
                }
            }
        }
        eventPublisher.requestTimeOut(timeoutEvent);
    }
    @Override
    public void processIOException(IOExceptionEvent exceptionEvent) {
        System.out.println("processIOException");
    }
    @Override
    public void processTransactionTerminated(TransactionTerminatedEvent transactionTerminatedEvent) {
//        if (transactionTerminatedEvent.isServerTransaction()) {
//            ServerTransaction serverTransaction = transactionTerminatedEvent.getServerTransaction();
//            serverTransaction.get
//        }
//        Transaction transaction = null;
//        System.out.println("processTransactionTerminated");
//        if (transactionTerminatedEvent.isServerTransaction()) {
//            transaction = transactionTerminatedEvent.getServerTransaction();
//        }else {
//            transaction = transactionTerminatedEvent.getClientTransaction();
//        }
//
//        System.out.println(transaction.getBranchId());
//        System.out.println(transaction.getState());
//        System.out.println(transaction.getRequest().getMethod());
//        CallIdHeader header = (CallIdHeader)transaction.getRequest().getHeader(CallIdHeader.NAME);
//        SipSubscribe.EventResult<TransactionTerminatedEvent> terminatedEventEventResult = new SipSubscribe.EventResult<>(transactionTerminatedEvent);
//        sipSubscribe.getErrorSubscribe(header.getCallId()).response(terminatedEventEventResult);
    }
    @Override
    public void processDialogTerminated(DialogTerminatedEvent dialogTerminatedEvent) {
        CallIdHeader callId = dialogTerminatedEvent.getDialog().getCallId();
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java
New file
@@ -0,0 +1,110 @@
package com.genersoft.iot.vmp.gb28181.transmit;
import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import com.genersoft.iot.vmp.utils.GitUtil;
import gov.nist.javax.sip.SipProviderImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import javax.sip.SipException;
import javax.sip.SipFactory;
import javax.sip.header.CallIdHeader;
import javax.sip.header.UserAgentHeader;
import javax.sip.header.ViaHeader;
import javax.sip.message.Message;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.text.ParseException;
/**
 * 发送SIP消息
 * @author lin
 */
@Component
public class SIPSender {
    private final Logger logger = LoggerFactory.getLogger(SIPSender.class);
    @Autowired
    @Qualifier(value = "tcpSipProvider")
    private SipProviderImpl tcpSipProvider;
    @Autowired
    @Qualifier(value = "udpSipProvider")
    private SipProviderImpl udpSipProvider;
    @Autowired
    private SipFactory sipFactory;
    @Autowired
    private GitUtil gitUtil;
    @Autowired
    private SipSubscribe sipSubscribe;
    public void transmitRequest(Message message) throws SipException, ParseException {
        transmitRequest(message, null, null);
    }
    public void transmitRequest(Message message, SipSubscribe.Event errorEvent) throws SipException, ParseException {
        transmitRequest(message, errorEvent, null);
    }
    public void transmitRequest(Message message, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, ParseException {
        ViaHeader viaHeader = (ViaHeader)message.getHeader(ViaHeader.NAME);
        String transport = "UDP";
        if (viaHeader == null) {
            logger.warn("[消息头缺失]: ViaHeader");
        }else {
            transport = viaHeader.getTransport();
        }
        if (message.getHeader(UserAgentHeader.NAME) == null) {
            try {
                message.addHeader(SipUtils.createUserAgentHeader(sipFactory, gitUtil));
            } catch (ParseException e) {
                logger.error("添加UserAgentHeader失败", e);
            }
        }
        CallIdHeader callIdHeader = (CallIdHeader) message.getHeader(CallIdHeader.NAME);
        // 添加错误订阅
        if (errorEvent != null) {
            sipSubscribe.addErrorSubscribe(callIdHeader.getCallId(), (eventResult -> {
                errorEvent.response(eventResult);
                sipSubscribe.removeErrorSubscribe(eventResult.callId);
                sipSubscribe.removeOkSubscribe(eventResult.callId);
            }));
        }
        // 添加订阅
        if (okEvent != null) {
            sipSubscribe.addOkSubscribe(callIdHeader.getCallId(), eventResult -> {
                okEvent.response(eventResult);
                sipSubscribe.removeOkSubscribe(eventResult.callId);
                sipSubscribe.removeErrorSubscribe(eventResult.callId);
            });
        }
        if ("TCP".equals(transport)) {
            if (message instanceof Request) {
                tcpSipProvider.sendRequest((Request)message);
            }else if (message instanceof Response) {
                tcpSipProvider.sendResponse((Response)message);
            }
        } else if ("UDP".equals(transport)) {
            if (message instanceof Request) {
                udpSipProvider.sendRequest((Request)message);
            }else if (message instanceof Response) {
                udpSipProvider.sendResponse((Response)message);
            }
        }
    }
    public CallIdHeader getNewCallIdHeader(String transport){
        return  transport.equalsIgnoreCase("TCP") ? tcpSipProvider.getNewCallId()
                : udpSipProvider.getNewCallId();
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java
New file
@@ -0,0 +1,125 @@
package com.genersoft.iot.vmp.gb28181.transmit.callback;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.async.DeferredResult;
/**
 * @description: 异步请求处理
 * @author: swwheihei
 * @date:   2020年5月8日 下午7:59:05
 */
@SuppressWarnings(value = {"rawtypes", "unchecked"})
@Component
public class DeferredResultHolder {
    public static final String CALLBACK_CMD_DEVICESTATUS = "CALLBACK_DEVICESTATUS";
    public static final String CALLBACK_CMD_DEVICEINFO = "CALLBACK_DEVICEINFO";
    public static final String CALLBACK_CMD_DEVICECONTROL = "CALLBACK_DEVICECONTROL";
    public static final String CALLBACK_CMD_DEVICECONFIG = "CALLBACK_DEVICECONFIG";
    public static final String CALLBACK_CMD_CONFIGDOWNLOAD = "CALLBACK_CONFIGDOWNLOAD";
    public static final String CALLBACK_CMD_CATALOG = "CALLBACK_CATALOG";
    public static final String CALLBACK_CMD_RECORDINFO = "CALLBACK_RECORDINFO";
    public static final String CALLBACK_CMD_PLAY = "CALLBACK_PLAY";
    public static final String CALLBACK_CMD_PLAYBACK = "CALLBACK_PLAYBACK";
    public static final String CALLBACK_CMD_DOWNLOAD = "CALLBACK_DOWNLOAD";
    public static final String CALLBACK_CMD_STOP = "CALLBACK_STOP";
    public static final String UPLOAD_FILE_CHANNEL = "UPLOAD_FILE_CHANNEL";
    public static final String CALLBACK_CMD_MOBILEPOSITION = "CALLBACK_MOBILEPOSITION";
    public static final String CALLBACK_CMD_PRESETQUERY = "CALLBACK_PRESETQUERY";
    public static final String CALLBACK_CMD_ALARM = "CALLBACK_ALARM";
    public static final String CALLBACK_CMD_BROADCAST = "CALLBACK_BROADCAST";
    private Map<String, Map<String, DeferredResult>> map = new ConcurrentHashMap<>();
    public void put(String key, String id, DeferredResult result) {
        Map<String, DeferredResult> deferredResultMap = map.get(key);
        if (deferredResultMap == null) {
            deferredResultMap = new ConcurrentHashMap<>();
            map.put(key, deferredResultMap);
        }
        deferredResultMap.put(id, result);
    }
    public DeferredResult get(String key, String id) {
        Map<String, DeferredResult> deferredResultMap = map.get(key);
        if (deferredResultMap == null) {
            return null;
        }
        return deferredResultMap.get(id);
    }
    public boolean exist(String key, String id){
        if (key == null) {
            return false;
        }
        Map<String, DeferredResult> deferredResultMap = map.get(key);
        if (id == null) {
            return deferredResultMap != null;
        }else {
            return deferredResultMap != null && deferredResultMap.get(id) != null;
        }
    }
    /**
     * 释放单个请求
     * @param msg
     */
    public void invokeResult(RequestMessage msg) {
        Map<String, DeferredResult> deferredResultMap = map.get(msg.getKey());
        if (deferredResultMap == null) {
            return;
        }
        DeferredResult result = deferredResultMap.get(msg.getId());
        if (result == null) {
            return;
        }
        result.setResult(msg.getData());
        deferredResultMap.remove(msg.getId());
        if (deferredResultMap.size() == 0) {
            map.remove(msg.getKey());
        }
    }
    /**
     * 释放所有的请求
     * @param msg
     */
    public void invokeAllResult(RequestMessage msg) {
        Map<String, DeferredResult> deferredResultMap = map.get(msg.getKey());
        if (deferredResultMap == null) {
            return;
        }
        Set<String> ids = deferredResultMap.keySet();
        for (String id : ids) {
            DeferredResult result = deferredResultMap.get(id);
            if (result == null) {
                return;
            }
            result.setResult(msg.getData());
        }
        map.remove(msg.getKey());
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/RequestMessage.java
New file
@@ -0,0 +1,39 @@
package com.genersoft.iot.vmp.gb28181.transmit.callback;
/**
 * @description: 请求信息定义
 * @author: swwheihei
 * @date:   2020年5月8日 下午1:09:18
 */
public class RequestMessage {
    private String id;
    private String key;
    private Object data;
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public void setKey(String key) {
        this.key = key;
    }
    public String getKey() {
        return key;
    }
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
New file
@@ -0,0 +1,366 @@
package com.genersoft.iot.vmp.gb28181.transmit.cmd;
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.service.bean.SSRCInfo;
import gov.nist.javax.sip.message.SIPRequest;
import javax.sip.Dialog;
import javax.sip.InvalidArgumentException;
import javax.sip.PeerUnavailableException;
import javax.sip.SipException;
import javax.sip.message.Message;
import javax.sip.message.Request;
import java.text.ParseException;
/**
 * @description:设备能力接口,用于定义设备的控制、查询能力
 * @author: swwheihei
 * @date:   2020年5月3日 下午9:16:34
 */
public interface ISIPCommander {
    /**
     * 云台方向放控制,使用配置文件中的默认镜头移动速度
     *
     * @param device  控制设备
     * @param channelId  预览通道
     * @param leftRight  镜头左移右移 0:停止 1:左移 2:右移
     * @param upDown     镜头上移下移 0:停止 1:上移 2:下移
     */
    void ptzdirectCmd(Device device,String channelId,int leftRight, int upDown) throws InvalidArgumentException, ParseException, SipException;
    /**
     * 云台方向放控制
     *
     * @param device  控制设备
     * @param channelId  预览通道
     * @param leftRight  镜头左移右移 0:停止 1:左移 2:右移
     * @param upDown     镜头上移下移 0:停止 1:上移 2:下移
     * @param moveSpeed  镜头移动速度
     */
    void ptzdirectCmd(Device device,String channelId,int leftRight, int upDown, int moveSpeed) throws InvalidArgumentException, ParseException, SipException;
    /**
     * 云台缩放控制,使用配置文件中的默认镜头缩放速度
     *
     * @param device  控制设备
     * @param channelId  预览通道
     * @param inOut      镜头放大缩小 0:停止 1:缩小 2:放大
     */
    void ptzZoomCmd(Device device,String channelId,int inOut) throws InvalidArgumentException, ParseException, SipException;
    /**
     * 云台缩放控制
     *
     * @param device  控制设备
     * @param channelId  预览通道
     * @param inOut      镜头放大缩小 0:停止 1:缩小 2:放大
     */
    void ptzZoomCmd(Device device,String channelId,int inOut, int moveSpeed) throws InvalidArgumentException, ParseException, SipException;
    /**
     * 云台控制,支持方向与缩放控制
     *
     * @param device  控制设备
     * @param channelId  预览通道
     * @param leftRight  镜头左移右移 0:停止 1:左移 2:右移
     * @param upDown     镜头上移下移 0:停止 1:上移 2:下移
     * @param inOut      镜头放大缩小 0:停止 1:缩小 2:放大
     * @param moveSpeed  镜头移动速度
     * @param zoomSpeed  镜头缩放速度
     */
    void ptzCmd(Device device,String channelId,int leftRight, int upDown, int inOut, int moveSpeed, int zoomSpeed) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 前端控制,包括PTZ指令、FI指令、预置位指令、巡航指令、扫描指令和辅助开关指令
     *
     * @param device          控制设备
     * @param channelId        预览通道
     * @param cmdCode        指令码
     * @param parameter1    数据1
     * @param parameter2    数据2
     * @param combineCode2    组合码2
     */
    void frontEndCmd(Device device, String channelId, int cmdCode, int parameter1, int parameter2, int combineCode2) throws SipException, InvalidArgumentException, ParseException;
    /**
     * 前端控制指令(用于转发上级指令)
     * @param device        控制设备
     * @param channelId        预览通道
     * @param cmdString        前端控制指令串
     */
    void fronEndCmd(Device device, String channelId, String cmdString, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 请求预览视频流
     * @param device  视频设备
     * @param channelId  预览通道
     */
    void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 请求回放视频流
     *
     * @param device  视频设备
     * @param channelId  预览通道
     * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
     * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
     */
    void playbackStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInf, Device device, String channelId, String startTime, String endTime,InviteStreamCallback inviteStreamCallback, InviteStreamCallback event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 请求历史媒体下载
     *
     * @param device  视频设备
     * @param channelId  预览通道
     * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
     * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
     * @param downloadSpeed 下载倍速参数
     */
    void downloadStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,
                           String startTime, String endTime, int downloadSpeed, InviteStreamCallback inviteStreamCallback, InviteStreamCallback hookEvent,
                           SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 视频流停止
     */
    void streamByeCmd(Device device, String channelId, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException;
    void streamByeCmd(Device device, String channelId, String stream, String callId) throws InvalidArgumentException, ParseException, SipException, SsrcTransactionNotFoundException;
    /**
     * 回放暂停
     */
    void playPauseCmd(Device device, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException;
    /**
     * 回放恢复
     */
    void playResumeCmd(Device device, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException;
    /**
     * 回放拖动播放
     */
    void playSeekCmd(Device device, StreamInfo streamInfo, long seekTime) throws InvalidArgumentException, ParseException, SipException;
    /**
     * 回放倍速播放
     */
    void playSpeedCmd(Device device, StreamInfo streamInfo, Double speed) throws InvalidArgumentException, ParseException, SipException;
    /**
     * 回放控制
     * @param device
     * @param streamInfo
     * @param content
     */
    void playbackControlCmd(Device device, StreamInfo streamInfo, String content,SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException;
    /**
     * 语音广播
     *
     * @param device  视频设备
     * @param channelId  预览通道
     */
    void audioBroadcastCmd(Device device,String channelId);
    /**
     * 语音广播
     *
     * @param device  视频设备
     */
    void audioBroadcastCmd(Device device, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
    void audioBroadcastCmd(Device device) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 音视频录像控制
     *
     * @param device          视频设备
     * @param channelId      预览通道
     * @param recordCmdStr    录像命令:Record / StopRecord
     */
    void recordCmd(Device device, String channelId, String recordCmdStr, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 远程启动控制命令
     *
     * @param device    视频设备
     */
    void teleBootCmd(Device device) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 报警布防/撤防命令
     *
     * @param device      视频设备
     */
    void guardCmd(Device device, String guardCmdStr, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 报警复位命令
     *
     * @param device        视频设备
     * @param alarmMethod    报警方式(可选)
     * @param alarmType        报警类型(可选)
     */
    void alarmCmd(Device device, String alarmMethod, String alarmType, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 强制关键帧命令,设备收到此命令应立刻发送一个IDR帧
     *
     * @param device  视频设备
     * @param channelId  预览通道
     */
    void iFrameCmd(Device device, String channelId) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 看守位控制命令
     *
     * @param device        视频设备
     * @param enabled        看守位使能:1 = 开启,0 = 关闭
     * @param resetTime        自动归位时间间隔,开启看守位时使用,单位:秒(s)
     * @param presetIndex    调用预置位编号,开启看守位时使用,取值范围0~255
     */
    void homePositionCmd(Device device, String channelId, String enabled, String resetTime, String presetIndex, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 设备配置命令
     *
     * @param device  视频设备
     */
    void deviceConfigCmd(Device device);
        /**
     * 设备配置命令:basicParam
     *
     * @param device              视频设备
     * @param channelId            通道编码(可选)
     * @param name                设备/通道名称(可选)
     * @param expiration        注册过期时间(可选)
     * @param heartBeatInterval    心跳间隔时间(可选)
     * @param heartBeatCount    心跳超时次数(可选)
     */
    void deviceBasicConfigCmd(Device device, String channelId, String name, String expiration, String heartBeatInterval, String heartBeatCount, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 查询设备状态
     *
     * @param device 视频设备
     */
    void deviceStatusQuery(Device device, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 查询设备信息
     *
     * @param device 视频设备
     * @return
     */
    void deviceInfoQuery(Device device) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 查询目录列表
     *
     * @param device 视频设备
     */
    void catalogQuery(Device device, int sn, SipSubscribe.Event errorEvent) throws SipException, InvalidArgumentException, ParseException;
    /**
     * 查询录像信息
     *
     * @param device 视频设备
     * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
     * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
     * @param sn
     */
    void recordInfoQuery(Device device, String channelId, String startTime, String endTime, int sn,  Integer Secrecy, String type, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 查询报警信息
     *
     * @param device        视频设备
     * @param startPriority    报警起始级别(可选)
     * @param endPriority    报警终止级别(可选)
     * @param alarmMethod    报警方式条件(可选)
     * @param alarmType        报警类型
     * @param startTime        报警发生起始时间(可选)
     * @param endTime        报警发生终止时间(可选)
     * @return                true = 命令发送成功
     */
    void alarmInfoQuery(Device device, String startPriority, String endPriority, String alarmMethod,
                            String alarmType, String startTime, String endTime, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 查询设备配置
     *
     * @param device         视频设备
     * @param channelId        通道编码(可选)
     * @param configType    配置类型:
     */
    void deviceConfigQuery(Device device, String channelId, String configType,  SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 查询设备预置位置
     *
     * @param device 视频设备
     */
    void presetQuery(Device device, String channelId, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 查询移动设备位置数据
     *
     * @param device 视频设备
     */
    void mobilePostitionQuery(Device device, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 订阅、取消订阅移动位置
     *
     * @param device    视频设备
     * @return            true = 命令发送成功
     */
    SIPRequest mobilePositionSubscribe(Device device, SIPRequest request, SipSubscribe.Event okEvent , SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 订阅、取消订阅报警信息
     * @param device        视频设备
     * @param expires        订阅过期时间(0 = 取消订阅)
     * @param startPriority    报警起始级别(可选)
     * @param endPriority    报警终止级别(可选)
     * @param alarmType        报警类型
     * @param startTime        报警发生起始时间(可选)
     * @param endTime        报警发生终止时间(可选)
     * @return                true = 命令发送成功
     */
    void alarmSubscribe(Device device, int expires, String startPriority, String endPriority, String alarmMethod, String alarmType, String startTime, String endTime) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 订阅、取消订阅目录信息
     * @param device        视频设备
     * @return                true = 命令发送成功
     */
    SIPRequest catalogSubscribe(Device device, SIPRequest request, SipSubscribe.Event okEvent ,SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 拉框控制命令
     *
     * @param device    控制设备
     * @param channelId 通道id
     * @param cmdString 前端控制指令串
     */
    void dragZoomCmd(Device device, String channelId, String cmdString) throws InvalidArgumentException, SipException, ParseException;
    /**
     * 向设备发送报警NOTIFY消息, 用于互联结构下,此时将设备当成一个平级平台看待
     * @param device 设备
     * @param deviceAlarm 报警信息信息
     * @return
     */
    void sendAlarmMessage(Device device, DeviceAlarm deviceAlarm) throws InvalidArgumentException, SipException, ParseException;
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommanderForPlatform.java
New file
@@ -0,0 +1,124 @@
package com.genersoft.iot.vmp.gb28181.transmit.cmd;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
import com.genersoft.iot.vmp.service.bean.GPSMsgInfo;
import javax.sip.InvalidArgumentException;
import javax.sip.SipException;
import javax.sip.header.WWWAuthenticateHeader;
import java.text.ParseException;
import java.util.List;
public interface ISIPCommanderForPlatform {
    /**
     * 向上级平台注册
     * @param parentPlatform
     * @return
     */
    void register(ParentPlatform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException;
    void register(ParentPlatform parentPlatform, String callId, WWWAuthenticateHeader www, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent, boolean registerAgain, boolean isRegister) throws SipException, InvalidArgumentException, ParseException;
    /**
     * 向上级平台注销
     * @param parentPlatform
     * @return
     */
    void unregister(ParentPlatform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException;
    /**
     * 向上级平发送心跳信息
     * @param parentPlatform
     * @return callId(作为接受回复的判定)
     */
    String keepalive(ParentPlatform parentPlatform,SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException;
    /**
     * 向上级回复通道信息
     * @param channel 通道信息
     * @param parentPlatform 平台信息
     * @param sn
     * @param fromTag
     * @param size
     * @return
     */
    void catalogQuery(DeviceChannel channel, ParentPlatform parentPlatform, String sn, String fromTag, int size) throws SipException, InvalidArgumentException, ParseException;
    void catalogQuery(List<DeviceChannel> channels, ParentPlatform parentPlatform, String sn, String fromTag) throws InvalidArgumentException, ParseException, SipException;
    /**
     * 向上级回复DeviceInfo查询信息
     * @param parentPlatform 平台信息
     * @param sn
     * @param fromTag
     * @return
     */
    void deviceInfoResponse(ParentPlatform parentPlatform, String sn, String fromTag) throws SipException, InvalidArgumentException, ParseException;
    /**
     * 向上级回复DeviceStatus查询信息
     * @param parentPlatform 平台信息
     * @param sn
     * @param fromTag
     * @return
     */
    void deviceStatusResponse(ParentPlatform parentPlatform, String sn, String fromTag) throws SipException, InvalidArgumentException, ParseException;
    /**
     * 向上级回复移动位置订阅消息
     * @param parentPlatform 平台信息
     * @param gpsMsgInfo GPS信息
     * @param subscribeInfo 订阅相关的信息
     * @return
     */
    void sendNotifyMobilePosition(ParentPlatform parentPlatform, GPSMsgInfo gpsMsgInfo, SubscribeInfo subscribeInfo) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException;
    /**
     * 向上级回复报警消息
     * @param parentPlatform 平台信息
     * @param deviceAlarm 报警信息信息
     * @return
     */
    void sendAlarmMessage(ParentPlatform parentPlatform, DeviceAlarm deviceAlarm) throws SipException, InvalidArgumentException, ParseException;
    /**
     * 回复catalog事件-增加/更新
     * @param parentPlatform
     * @param deviceChannels
     */
    void sendNotifyForCatalogAddOrUpdate(String type, ParentPlatform parentPlatform, List<DeviceChannel> deviceChannels, SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException;
    /**
     * 回复catalog事件-删除
     * @param parentPlatform
     * @param deviceChannels
     */
    void sendNotifyForCatalogOther(String type, ParentPlatform parentPlatform, List<DeviceChannel> deviceChannels, SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException;
    /**
     * 回复recordInfo
     * @param deviceChannel 通道信息
     * @param parentPlatform 平台信息
     * @param fromTag fromTag
     * @param recordInfo 录像信息
     */
    void recordInfo(DeviceChannel deviceChannel, ParentPlatform parentPlatform, String fromTag, RecordInfo recordInfo) throws SipException, InvalidArgumentException, ParseException;
    /**
     * 录像播放推送完成时发送MediaStatus消息
     * @param platform
     * @param sendRtpItem
     * @return
     */
    void sendMediaStatusNotify(ParentPlatform platform, SendRtpItem sendRtpItem) throws SipException, InvalidArgumentException, ParseException;
    /**
     * 向发起点播的上级回复bye
     * @param platform 平台信息
     * @param callId  callId
     */
    void streamByeCmd(ParentPlatform platform, String callId) throws SipException, InvalidArgumentException, ParseException;
    void streamByeCmd(ParentPlatform platform, SendRtpItem sendRtpItem) throws SipException, InvalidArgumentException, ParseException;
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java
New file
@@ -0,0 +1,310 @@
package com.genersoft.iot.vmp.gb28181.transmit.cmd;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.bean.SendRtpItem;
import com.genersoft.iot.vmp.gb28181.bean.SubscribeInfo;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.utils.GitUtil;
import gov.nist.javax.sip.message.MessageFactoryImpl;
import gov.nist.javax.sip.message.SIPRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import javax.sip.*;
import javax.sip.address.Address;
import javax.sip.address.SipURI;
import javax.sip.header.*;
import javax.sip.message.Request;
import javax.validation.constraints.NotNull;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
 * @description: 平台命令request创造器 TODO 冗余代码太多待优化
 * @author: panll
 * @date: 2020年5月6日 上午9:29:02
 */
@Component
public class SIPRequestHeaderPlarformProvider {
    @Autowired
    private SipConfig sipConfig;
    @Autowired
    private SipFactory sipFactory;
    @Autowired
    private GitUtil gitUtil;
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    public Request createRegisterRequest(@NotNull ParentPlatform platform, long CSeq, String fromTag, String viaTag, CallIdHeader callIdHeader, boolean isRegister) throws ParseException, InvalidArgumentException, PeerUnavailableException {
        Request request = null;
        String sipAddress = sipConfig.getIp() + ":" + sipConfig.getPort();
        //请求行
        SipURI requestLine = sipFactory.createAddressFactory().createSipURI(platform.getServerGBId(),
                platform.getServerIP() + ":" + platform.getServerPort());
        //via
        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
        ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(platform.getServerIP(), platform.getServerPort(), platform.getTransport(), viaTag);
        viaHeader.setRPort();
        viaHeaders.add(viaHeader);
        //from
        SipURI fromSipURI = sipFactory.createAddressFactory().createSipURI(platform.getDeviceGBId(), sipConfig.getDomain());
        Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipURI);
        FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, fromTag);
        //to
        SipURI toSipURI = sipFactory.createAddressFactory().createSipURI(platform.getDeviceGBId(), sipConfig.getDomain());
        Address toAddress = sipFactory.createAddressFactory().createAddress(toSipURI);
        ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress,null);
        //Forwards
        MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
        //ceq
        CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(CSeq, Request.REGISTER);
        request = sipFactory.createMessageFactory().createRequest(requestLine, Request.REGISTER, callIdHeader,
                cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards);
        Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory()
                .createSipURI(platform.getDeviceGBId(), sipAddress));
        request.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
        ExpiresHeader expires = sipFactory.createHeaderFactory().createExpiresHeader(isRegister ? platform.getExpires() : 0);
        request.addHeader(expires);
        request.addHeader(SipUtils.createUserAgentHeader(sipFactory, gitUtil));
        return request;
    }
    public Request createRegisterRequest(@NotNull ParentPlatform parentPlatform, String fromTag, String viaTag,
                                         String callId, WWWAuthenticateHeader www , CallIdHeader callIdHeader, boolean isRegister) throws ParseException, PeerUnavailableException, InvalidArgumentException {
        Request registerRequest = createRegisterRequest(parentPlatform, redisCatchStorage.getCSEQ(), fromTag, viaTag, callIdHeader, isRegister);
        SipURI requestURI = sipFactory.createAddressFactory().createSipURI(parentPlatform.getServerGBId(), parentPlatform.getServerIP() + ":" + parentPlatform.getServerPort());
        if (www == null) {
            AuthorizationHeader authorizationHeader = sipFactory.createHeaderFactory().createAuthorizationHeader("Digest");
            authorizationHeader.setUsername(parentPlatform.getDeviceGBId());
            authorizationHeader.setURI(requestURI);
            authorizationHeader.setAlgorithm("MD5");
            registerRequest.addHeader(authorizationHeader);
            return  registerRequest;
        }
        String realm = www.getRealm();
        String nonce = www.getNonce();
        String scheme = www.getScheme();
        // 参考 https://blog.csdn.net/y673533511/article/details/88388138
        // qop 保护质量 包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略
        String qop = www.getQop();
        callIdHeader.setCallId(callId);
        String cNonce = null;
        String nc = "00000001";
        if (qop != null) {
            if ("auth".equalsIgnoreCase(qop)) {
                // 客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。
                // 这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护
                cNonce = UUID.randomUUID().toString();
            }else if ("auth-int".equalsIgnoreCase(qop)){
                // TODO
            }
        }
        String HA1 = DigestUtils.md5DigestAsHex((parentPlatform.getDeviceGBId() + ":" + realm + ":" + parentPlatform.getPassword()).getBytes());
        String HA2=DigestUtils.md5DigestAsHex((Request.REGISTER + ":" + requestURI.toString()).getBytes());
        StringBuffer reStr = new StringBuffer(200);
        reStr.append(HA1);
        reStr.append(":");
        reStr.append(nonce);
        reStr.append(":");
        if (qop != null) {
            reStr.append(nc);
            reStr.append(":");
            reStr.append(cNonce);
            reStr.append(":");
            reStr.append(qop);
            reStr.append(":");
        }
        reStr.append(HA2);
        String RESPONSE = DigestUtils.md5DigestAsHex(reStr.toString().getBytes());
        AuthorizationHeader authorizationHeader = sipFactory.createHeaderFactory().createAuthorizationHeader(scheme);
        authorizationHeader.setUsername(parentPlatform.getDeviceGBId());
        authorizationHeader.setRealm(realm);
        authorizationHeader.setNonce(nonce);
        authorizationHeader.setURI(requestURI);
        authorizationHeader.setResponse(RESPONSE);
        authorizationHeader.setAlgorithm("MD5");
        if (qop != null) {
            authorizationHeader.setQop(qop);
            authorizationHeader.setCNonce(cNonce);
            authorizationHeader.setNonceCount(1);
        }
        registerRequest.addHeader(authorizationHeader);
        return registerRequest;
    }
    public Request createMessageRequest(ParentPlatform parentPlatform, String content, SendRtpItem sendRtpItem) throws PeerUnavailableException, ParseException, InvalidArgumentException {
        CallIdHeader callIdHeader = sipFactory.createHeaderFactory().createCallIdHeader(sendRtpItem.getCallId());
        return createMessageRequest(parentPlatform, content, sendRtpItem.getToTag(), SipUtils.getNewViaTag(), sendRtpItem.getFromTag(), callIdHeader);
    }
    public Request createMessageRequest(ParentPlatform parentPlatform, String content, String fromTag, String viaTag, CallIdHeader callIdHeader) throws PeerUnavailableException, ParseException, InvalidArgumentException {
        return createMessageRequest(parentPlatform, content, fromTag, viaTag, null, callIdHeader);
    }
    public Request createMessageRequest(ParentPlatform parentPlatform, String content, String fromTag, String viaTag, String toTag, CallIdHeader callIdHeader) throws PeerUnavailableException, ParseException, InvalidArgumentException {
        Request request = null;
        String serverAddress = parentPlatform.getServerIP()+ ":" + parentPlatform.getServerPort();
        // sipuri
        SipURI requestURI = sipFactory.createAddressFactory().createSipURI(parentPlatform.getServerGBId(), serverAddress);
        // via
        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
        ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), Integer.parseInt(parentPlatform.getDevicePort()),
                parentPlatform.getTransport(), viaTag);
        viaHeader.setRPort();
        viaHeaders.add(viaHeader);
        // from
        // SipURI fromSipURI = sipFactory.createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), parentPlatform.getDeviceIp() + ":" + parentPlatform.getDeviceIp());
        SipURI fromSipURI = sipFactory.createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), sipConfig.getDomain());
        Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipURI);
        FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, fromTag);
        // to
        SipURI toSipURI = sipFactory.createAddressFactory().createSipURI(parentPlatform.getServerGBId(), serverAddress);
        Address toAddress = sipFactory.createAddressFactory().createAddress(toSipURI);
        ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress, toTag);
        // Forwards
        MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
        // ceq
        CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.MESSAGE);
        MessageFactoryImpl messageFactory = (MessageFactoryImpl) sipFactory.createMessageFactory();
        // 设置编码, 防止中文乱码
        messageFactory.setDefaultContentEncodingCharset(parentPlatform.getCharacterSet());
        request = messageFactory.createRequest(requestURI, Request.MESSAGE, callIdHeader, cSeqHeader, fromHeader,
                toHeader, viaHeaders, maxForwards);
        request.addHeader(SipUtils.createUserAgentHeader(sipFactory, gitUtil));
        ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml");
        request.setContent(content, contentTypeHeader);
        return request;
    }
    public SIPRequest createNotifyRequest(ParentPlatform parentPlatform, String content, SubscribeInfo subscribeInfo) throws PeerUnavailableException, ParseException, InvalidArgumentException {
        SIPRequest request = null;
        // sipuri
        SipURI requestURI = sipFactory.createAddressFactory().createSipURI(parentPlatform.getServerGBId(), parentPlatform.getServerIP()+ ":" + parentPlatform.getServerPort());
        // via
        ArrayList<ViaHeader> viaHeaders = new ArrayList<>();
        ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), Integer.parseInt(parentPlatform.getDevicePort()),
                parentPlatform.getTransport(), SipUtils.getNewViaTag());
        viaHeader.setRPort();
        viaHeaders.add(viaHeader);
        // from
        SipURI fromSipURI = sipFactory.createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(),
                parentPlatform.getDeviceIp() + ":" + parentPlatform.getDevicePort());
        Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipURI);
        FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, subscribeInfo.getResponse().getToTag());
        // to
        SipURI toSipURI = sipFactory.createAddressFactory().createSipURI(parentPlatform.getServerGBId(), parentPlatform.getServerGBDomain());
        Address toAddress = sipFactory.createAddressFactory().createAddress(toSipURI);
        ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress, subscribeInfo.getRequest().getFromTag());
        // Forwards
        MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
        // ceq
        CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.NOTIFY);
        MessageFactoryImpl messageFactory = (MessageFactoryImpl) sipFactory.createMessageFactory();
        // 设置编码, 防止中文乱码
        messageFactory.setDefaultContentEncodingCharset("gb2312");
        CallIdHeader callIdHeader = sipFactory.createHeaderFactory().createCallIdHeader(subscribeInfo.getRequest().getCallIdHeader().getCallId());
        request = (SIPRequest) messageFactory.createRequest(requestURI, Request.NOTIFY, callIdHeader, cSeqHeader, fromHeader,
                toHeader, viaHeaders, maxForwards);
        request.addHeader(SipUtils.createUserAgentHeader(sipFactory, gitUtil));
        EventHeader event = sipFactory.createHeaderFactory().createEventHeader(subscribeInfo.getEventType());
        if (subscribeInfo.getEventId() != null) {
            event.setEventId(subscribeInfo.getEventId());
        }
        request.addHeader(event);
        SubscriptionStateHeader active = sipFactory.createHeaderFactory().createSubscriptionStateHeader("active");
        request.setHeader(active);
        String sipAddress = sipConfig.getIp() + ":" + sipConfig.getPort();
        Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory()
                .createSipURI(parentPlatform.getDeviceGBId(), sipAddress));
        request.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
        ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml");
        request.setContent(content, contentTypeHeader);
        return request;
    }
    public SIPRequest createByeRequest(ParentPlatform platform, SendRtpItem sendRtpItem) throws PeerUnavailableException, ParseException, InvalidArgumentException {
        if (sendRtpItem == null ) {
            return null;
        }
        SIPRequest request = null;
        // sipuri
        SipURI requestURI = sipFactory.createAddressFactory().createSipURI(platform.getServerGBId(), platform.getServerIP()+ ":" + platform.getServerPort());
        // via
        ArrayList<ViaHeader> viaHeaders = new ArrayList<>();
        ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(platform.getDeviceIp(), Integer.parseInt(platform.getDevicePort()),
                platform.getTransport(), SipUtils.getNewViaTag());
        viaHeader.setRPort();
        viaHeaders.add(viaHeader);
        // from
        SipURI fromSipURI = sipFactory.createAddressFactory().createSipURI(platform.getDeviceGBId(),
                platform.getDeviceIp() + ":" + platform.getDevicePort());
        Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipURI);
        FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, sendRtpItem.getToTag());
        // to
        SipURI toSipURI = sipFactory.createAddressFactory().createSipURI(platform.getServerGBId(), platform.getServerGBDomain());
        Address toAddress = sipFactory.createAddressFactory().createAddress(toSipURI);
        ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress, sendRtpItem.getFromTag());
        // Forwards
        MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
        // ceq
        CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.BYE);
        MessageFactoryImpl messageFactory = (MessageFactoryImpl) sipFactory.createMessageFactory();
        // 设置编码, 防止中文乱码
        messageFactory.setDefaultContentEncodingCharset("gb2312");
        CallIdHeader callIdHeader = sipFactory.createHeaderFactory().createCallIdHeader(sendRtpItem.getCallId());
        request = (SIPRequest) messageFactory.createRequest(requestURI, Request.BYE, callIdHeader, cSeqHeader, fromHeader,
                toHeader, viaHeaders, maxForwards);
        request.addHeader(SipUtils.createUserAgentHeader(sipFactory, gitUtil));
        String sipAddress = sipConfig.getIp() + ":" + sipConfig.getPort();
        Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory()
                .createSipURI(platform.getDeviceGBId(), sipAddress));
        request.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
        return request;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java
New file
@@ -0,0 +1,321 @@
package com.genersoft.iot.vmp.gb28181.transmit.cmd;
import java.text.ParseException;
import java.util.ArrayList;
import javax.sip.*;
import javax.sip.address.Address;
import javax.sip.address.SipURI;
import javax.sip.header.*;
import javax.sip.message.Request;
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.gb28181.bean.SipMsgInfo;
import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo;
import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction;
import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.utils.GitUtil;
import gov.nist.javax.sip.SipProviderImpl;
import gov.nist.javax.sip.SipStackImpl;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import gov.nist.javax.sip.stack.SIPDialog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.gb28181.bean.Device;
/**
 * @description:摄像头命令request创造器 TODO 冗余代码太多待优化
 * @author: swwheihei
 * @date: 2020年5月6日 上午9:29:02
 */
@Component
public class SIPRequestHeaderProvider {
    @Autowired
    private SipConfig sipConfig;
    @Autowired
    private SipFactory sipFactory;
    @Autowired
    private GitUtil gitUtil;
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Autowired
    private VideoStreamSessionManager streamSession;
    public Request createMessageRequest(Device device, String content, String viaTag, String fromTag, String toTag, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException {
        Request request = null;
        // sipuri
        SipURI requestURI = sipFactory.createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress());
        // via
        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
        ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(sipConfig.getIp(), sipConfig.getPort(), device.getTransport(), viaTag);
        viaHeader.setRPort();
        viaHeaders.add(viaHeader);
        // from
        SipURI fromSipURI = sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain());
        Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipURI);
        FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, fromTag);
        // to
        SipURI toSipURI = sipFactory.createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress());
        Address toAddress = sipFactory.createAddressFactory().createAddress(toSipURI);
        ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress, toTag);
        // Forwards
        MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
        // ceq
        CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.MESSAGE);
        request = sipFactory.createMessageFactory().createRequest(requestURI, Request.MESSAGE, callIdHeader, cSeqHeader, fromHeader,
                toHeader, viaHeaders, maxForwards);
        request.addHeader(SipUtils.createUserAgentHeader(sipFactory, gitUtil));
        ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml");
        request.setContent(content, contentTypeHeader);
        return request;
    }
    public Request createInviteRequest(Device device, String channelId, String content, String viaTag, String fromTag, String toTag, String ssrc, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException {
        Request request = null;
        //请求行
        SipURI requestLine = sipFactory.createAddressFactory().createSipURI(channelId, device.getHostAddress());
        //via
        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
        ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(sipConfig.getIp(), sipConfig.getPort(), device.getTransport(), viaTag);
        viaHeader.setRPort();
        viaHeaders.add(viaHeader);
        //from
        SipURI fromSipURI = sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain());
        Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipURI);
        FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack
        //to
        SipURI toSipURI = sipFactory.createAddressFactory().createSipURI(channelId, device.getHostAddress());
        Address toAddress = sipFactory.createAddressFactory().createAddress(toSipURI);
        ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress,null);
        //Forwards
        MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
        //ceq
        CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INVITE);
        request = sipFactory.createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards);
        request.addHeader(SipUtils.createUserAgentHeader(sipFactory, gitUtil));
        Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getIp()+":"+sipConfig.getPort()));
        // Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), device.getHost().getIp()+":"+device.getHost().getPort()));
        request.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
        // Subject
        SubjectHeader subjectHeader = sipFactory.createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", channelId, ssrc, sipConfig.getId(), 0));
        request.addHeader(subjectHeader);
        ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP");
        request.setContent(content, contentTypeHeader);
        return request;
    }
    public Request createPlaybackInviteRequest(Device device, String channelId, String content, String viaTag, String fromTag, String toTag, CallIdHeader callIdHeader, String ssrc) throws ParseException, InvalidArgumentException, PeerUnavailableException {
        Request request = null;
        //请求行
        SipURI requestLine = sipFactory.createAddressFactory().createSipURI(channelId, device.getHostAddress());
        // via
        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
        ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(sipConfig.getIp(), sipConfig.getPort(), device.getTransport(), viaTag);
        viaHeader.setRPort();
        viaHeaders.add(viaHeader);
        //from
        SipURI fromSipURI = sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain());
        Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipURI);
        FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack
        //to
        SipURI toSipURI = sipFactory.createAddressFactory().createSipURI(channelId, device.getHostAddress());
        Address toAddress = sipFactory.createAddressFactory().createAddress(toSipURI);
        ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress,null);
        //Forwards
        MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
        //ceq
        CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INVITE);
        request = sipFactory.createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards);
        Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getIp()+":"+sipConfig.getPort()));
        // Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), device.getHost().getIp()+":"+device.getHost().getPort()));
        request.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
        request.addHeader(SipUtils.createUserAgentHeader(sipFactory, gitUtil));
        // Subject
        SubjectHeader subjectHeader = sipFactory.createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", channelId, ssrc, sipConfig.getId(), 0));
        request.addHeader(subjectHeader);
        ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP");
        request.setContent(content, contentTypeHeader);
        return request;
    }
    public Request createByteRequest(Device device, String channelId, SipTransactionInfo transactionInfo) throws ParseException, InvalidArgumentException, PeerUnavailableException {
        Request request = null;
        //请求行
        SipURI requestLine = sipFactory.createAddressFactory().createSipURI(channelId, device.getHostAddress());
        // via
        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
        ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(sipConfig.getIp(), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag());
        viaHeaders.add(viaHeader);
        //from
        SipURI fromSipURI = sipFactory.createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain());
        Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipURI);
        FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getFromTag());
        //to
        SipURI toSipURI = sipFactory.createAddressFactory().createSipURI(channelId,device.getHostAddress());
        Address toAddress = sipFactory.createAddressFactory().createAddress(toSipURI);
        ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress,    transactionInfo.getToTag());
        //Forwards
        MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
        //ceq
        CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.BYE);
        CallIdHeader callIdHeader = sipFactory.createHeaderFactory().createCallIdHeader(transactionInfo.getCallId());
        request = sipFactory.createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards);
        request.addHeader(SipUtils.createUserAgentHeader(sipFactory, gitUtil));
        Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getIp()+":"+sipConfig.getPort()));
        request.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
        request.addHeader(SipUtils.createUserAgentHeader(sipFactory, gitUtil));
        return request;
    }
    public Request createSubscribeRequest(Device device, String content, SIPRequest requestOld, Integer expires, String event, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException {
        Request request = null;
        // sipuri
        SipURI requestURI = sipFactory.createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress());
        // via
        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
        ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(sipConfig.getIp(), sipConfig.getPort(),
                device.getTransport(), SipUtils.getNewViaTag());
        viaHeader.setRPort();
        viaHeaders.add(viaHeader);
        // from
        SipURI fromSipURI = sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain());
        Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipURI);
        FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, requestOld == null ? SipUtils.getNewFromTag() :requestOld.getFromTag());
        // to
        SipURI toSipURI = sipFactory.createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress());
        Address toAddress = sipFactory.createAddressFactory().createAddress(toSipURI);
        ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress, requestOld == null ? null :requestOld.getToTag());
        // Forwards
        MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
        // ceq
        CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.SUBSCRIBE);
        request = sipFactory.createMessageFactory().createRequest(requestURI, Request.SUBSCRIBE, callIdHeader, cSeqHeader, fromHeader,
                toHeader, viaHeaders, maxForwards);
        Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getIp()+":"+sipConfig.getPort()));
        request.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
        // Expires
        ExpiresHeader expireHeader = sipFactory.createHeaderFactory().createExpiresHeader(expires);
        request.addHeader(expireHeader);
        // Event
        EventHeader eventHeader = sipFactory.createHeaderFactory().createEventHeader(event);
        int random = (int) Math.floor(Math.random() * 10000);
        eventHeader.setEventId(random + "");
        request.addHeader(eventHeader);
        ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml");
        request.setContent(content, contentTypeHeader);
        request.addHeader(SipUtils.createUserAgentHeader(sipFactory, gitUtil));
        return request;
    }
    public SIPRequest createInfoRequest(Device device, String channelId, String content, SipTransactionInfo transactionInfo)
            throws SipException, ParseException, InvalidArgumentException {
        if (device == null || transactionInfo == null) {
            return null;
        }
        SIPRequest request = null;
        //请求行
        SipURI requestLine = sipFactory.createAddressFactory().createSipURI(channelId, device.getHostAddress());
        // via
        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
        ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(sipConfig.getIp(), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag());
        viaHeaders.add(viaHeader);
        //from
        SipURI fromSipURI = sipFactory.createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain());
        Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipURI);
        FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getFromTag());
        //to
        SipURI toSipURI = sipFactory.createAddressFactory().createSipURI(channelId,device.getHostAddress());
        Address toAddress = sipFactory.createAddressFactory().createAddress(toSipURI);
        ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress,    transactionInfo.getToTag());
        //Forwards
        MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
        //ceq
        CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INFO);
        CallIdHeader callIdHeader = sipFactory.createHeaderFactory().createCallIdHeader(transactionInfo.getCallId());
        request = (SIPRequest)sipFactory.createMessageFactory().createRequest(requestLine, Request.INFO, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards);
        request.addHeader(SipUtils.createUserAgentHeader(sipFactory, gitUtil));
        Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getIp()+":"+sipConfig.getPort()));
        request.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
        request.addHeader(SipUtils.createUserAgentHeader(sipFactory, gitUtil));
        if (content != null) {
            ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("Application",
                    "MANSRTSP");
            request.setContent(content, contentTypeHeader);
        }
        return request;
    }
    public Request createAckRequest(SipURI sipURI, SIPResponse sipResponse) throws ParseException, InvalidArgumentException, PeerUnavailableException {
        // via
        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
        ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(sipConfig.getIp(), sipConfig.getPort(), sipResponse.getTopmostViaHeader().getTransport(), SipUtils.getNewViaTag());
        viaHeaders.add(viaHeader);
        //Forwards
        MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
        //ceq
        CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(sipResponse.getCSeqHeader().getSeqNumber(), Request.ACK);
        Request request = sipFactory.createMessageFactory().createRequest(sipURI, Request.ACK, sipResponse.getCallIdHeader(), cSeqHeader, sipResponse.getFromHeader(), sipResponse.getToHeader(), viaHeaders, maxForwards);
        request.addHeader(SipUtils.createUserAgentHeader(sipFactory, gitUtil));
        Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getIp()+":"+sipConfig.getPort()));
        request.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
        request.addHeader(SipUtils.createUserAgentHeader(sipFactory, gitUtil));
        return request;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
New file
@@ -0,0 +1,1402 @@
package com.genersoft.iot.vmp.gb28181.transmit.cmd.impl;
import com.alibaba.fastjson.JSONObject;
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.conf.DynamicTask;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
import com.genersoft.iot.vmp.gb28181.transmit.SIPSender;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.SIPRequestHeaderProvider;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeFactory;
import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeForStreamChange;
import com.genersoft.iot.vmp.utils.DateUtil;
import com.genersoft.iot.vmp.gb28181.utils.NumericUtil;
import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.service.IMediaServerService;
import com.genersoft.iot.vmp.service.bean.SSRCInfo;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import com.genersoft.iot.vmp.utils.GitUtil;
import gov.nist.javax.sip.SIPConstants;
import gov.nist.javax.sip.SipProviderImpl;
import gov.nist.javax.sip.SipStackImpl;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import gov.nist.javax.sip.stack.SIPClientTransaction;
import gov.nist.javax.sip.stack.SIPClientTransactionImpl;
import gov.nist.javax.sip.stack.SIPDialog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import javax.sip.*;
import javax.sip.address.Address;
import javax.sip.address.SipURI;
import javax.sip.header.*;
import javax.sip.message.Message;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.lang.reflect.Field;
import java.text.ParseException;
import java.util.HashSet;
/**
 * @description:设备能力接口,用于定义设备的控制、查询能力
 * @author: swwheihei
 * @date: 2020年5月3日 下午9:22:48
 */
@Component
@DependsOn("sipLayer")
public class SIPCommander implements ISIPCommander {
    private final Logger logger = LoggerFactory.getLogger(SIPCommander.class);
    @Autowired
    private SipConfig sipConfig;
    @Autowired
    private SipFactory sipFactory;
    @Autowired
    private SIPSender sipSender;
    @Autowired
    private SIPRequestHeaderProvider headerProvider;
    @Autowired
    private VideoStreamSessionManager streamSession;
    @Autowired
    private UserSetting userSetting;
    @Autowired
    private ZlmHttpHookSubscribe subscribe;
    @Autowired
    private IMediaServerService mediaServerService;
    /**
     * 云台方向放控制,使用配置文件中的默认镜头移动速度
     *
     * @param device    控制设备
     * @param channelId 预览通道
     * @param leftRight 镜头左移右移 0:停止 1:左移 2:右移
     * @param upDown    镜头上移下移 0:停止 1:上移 2:下移
     */
    @Override
    public void ptzdirectCmd(Device device, String channelId, int leftRight, int upDown) throws InvalidArgumentException, ParseException, SipException {
        ptzCmd(device, channelId, leftRight, upDown, 0, sipConfig.getPtzSpeed(), 0);
    }
    /**
     * 云台方向放控制
     *
     * @param device    控制设备
     * @param channelId 预览通道
     * @param leftRight 镜头左移右移 0:停止 1:左移 2:右移
     * @param upDown    镜头上移下移 0:停止 1:上移 2:下移
     * @param moveSpeed 镜头移动速度
     */
    @Override
    public void ptzdirectCmd(Device device, String channelId, int leftRight, int upDown, int moveSpeed) throws InvalidArgumentException, ParseException, SipException {
        ptzCmd(device, channelId, leftRight, upDown, 0, moveSpeed, 0);
    }
    /**
     * 云台缩放控制,使用配置文件中的默认镜头缩放速度
     *
     * @param device    控制设备
     * @param channelId 预览通道
     * @param inOut     镜头放大缩小 0:停止 1:缩小 2:放大
     */
    @Override
    public void ptzZoomCmd(Device device, String channelId, int inOut) throws InvalidArgumentException, ParseException, SipException {
        ptzCmd(device, channelId, 0, 0, inOut, 0, sipConfig.getPtzSpeed());
    }
    /**
     * 云台缩放控制
     *
     * @param device    控制设备
     * @param channelId 预览通道
     * @param inOut     镜头放大缩小 0:停止 1:缩小 2:放大
     * @param zoomSpeed 镜头缩放速度
     */
    @Override
    public void ptzZoomCmd(Device device, String channelId, int inOut, int zoomSpeed) throws InvalidArgumentException, ParseException, SipException {
        ptzCmd(device, channelId, 0, 0, inOut, 0, zoomSpeed);
    }
    /**
     * 云台指令码计算
     *
     * @param cmdCode      指令码
     * @param parameter1   数据1
     * @param parameter2   数据2
     * @param combineCode2 组合码2
     */
    public static String frontEndCmdString(int cmdCode, int parameter1, int parameter2, int combineCode2) {
        StringBuilder builder = new StringBuilder("A50F01");
        String strTmp;
        strTmp = String.format("%02X", cmdCode);
        builder.append(strTmp, 0, 2);
        strTmp = String.format("%02X", parameter1);
        builder.append(strTmp, 0, 2);
        strTmp = String.format("%02X", parameter2);
        builder.append(strTmp, 0, 2);
        strTmp = String.format("%X", combineCode2);
        builder.append(strTmp, 0, 1).append("0");
        //计算校验码
        int checkCode = (0XA5 + 0X0F + 0X01 + cmdCode + parameter1 + parameter2 + (combineCode2 & 0XF0)) % 0X100;
        strTmp = String.format("%02X", checkCode);
        builder.append(strTmp, 0, 2);
        return builder.toString();
    }
    /**
     * 云台控制,支持方向与缩放控制
     *
     * @param device    控制设备
     * @param channelId 预览通道
     * @param leftRight 镜头左移右移 0:停止 1:左移 2:右移
     * @param upDown    镜头上移下移 0:停止 1:上移 2:下移
     * @param inOut     镜头放大缩小 0:停止 1:缩小 2:放大
     * @param moveSpeed 镜头移动速度
     * @param zoomSpeed 镜头缩放速度
     */
    @Override
    public void ptzCmd(Device device, String channelId, int leftRight, int upDown, int inOut, int moveSpeed,
                       int zoomSpeed) throws InvalidArgumentException, SipException, ParseException {
        String cmdStr = SipUtils.cmdString(leftRight, upDown, inOut, moveSpeed, zoomSpeed);
        StringBuffer ptzXml = new StringBuffer(200);
        String charset = device.getCharset();
        ptzXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        ptzXml.append("<Control>\r\n");
        ptzXml.append("<CmdType>DeviceControl</CmdType>\r\n");
        ptzXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        ptzXml.append("<DeviceID>" + channelId + "</DeviceID>\r\n");
        ptzXml.append("<PTZCmd>" + cmdStr + "</PTZCmd>\r\n");
        ptzXml.append("<Info>\r\n");
        ptzXml.append("<ControlPriority>5</ControlPriority>\r\n");
        ptzXml.append("</Info>\r\n");
        ptzXml.append("</Control>\r\n");
        Request request = headerProvider.createMessageRequest(device, ptzXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null, sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request);
    }
    /**
     * 前端控制,包括PTZ指令、FI指令、预置位指令、巡航指令、扫描指令和辅助开关指令
     *
     * @param device       控制设备
     * @param channelId    预览通道
     * @param cmdCode      指令码
     * @param parameter1   数据1
     * @param parameter2   数据2
     * @param combineCode2 组合码2
     */
    @Override
    public void frontEndCmd(Device device, String channelId, int cmdCode, int parameter1, int parameter2, int combineCode2) throws SipException, InvalidArgumentException, ParseException {
        String cmdStr = frontEndCmdString(cmdCode, parameter1, parameter2, combineCode2);
        StringBuffer ptzXml = new StringBuffer(200);
        String charset = device.getCharset();
        ptzXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        ptzXml.append("<Control>\r\n");
        ptzXml.append("<CmdType>DeviceControl</CmdType>\r\n");
        ptzXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        ptzXml.append("<DeviceID>" + channelId + "</DeviceID>\r\n");
        ptzXml.append("<PTZCmd>" + cmdStr + "</PTZCmd>\r\n");
        ptzXml.append("<Info>\r\n");
        ptzXml.append("<ControlPriority>5</ControlPriority>\r\n");
        ptzXml.append("</Info>\r\n");
        ptzXml.append("</Control>\r\n");
        SIPRequest request = (SIPRequest) headerProvider.createMessageRequest(device, ptzXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest(request);
    }
    /**
     * 前端控制指令(用于转发上级指令)
     *
     * @param device    控制设备
     * @param channelId 预览通道
     * @param cmdString 前端控制指令串
     */
    @Override
    public void fronEndCmd(Device device, String channelId, String cmdString, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer ptzXml = new StringBuffer(200);
        String charset = device.getCharset();
        ptzXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        ptzXml.append("<Control>\r\n");
        ptzXml.append("<CmdType>DeviceControl</CmdType>\r\n");
        ptzXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        ptzXml.append("<DeviceID>" + channelId + "</DeviceID>\r\n");
        ptzXml.append("<PTZCmd>" + cmdString + "</PTZCmd>\r\n");
        ptzXml.append("<Info>\r\n");
        ptzXml.append("<ControlPriority>5</ControlPriority>\r\n");
        ptzXml.append("</Info>\r\n");
        ptzXml.append("</Control>\r\n");
        Request request = headerProvider.createMessageRequest(device, ptzXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest(request, errorEvent, okEvent);
    }
    /**
     * 请求预览视频流
     *
     * @param device     视频设备
     * @param channelId  预览通道
     * @param event      hook订阅
     * @param errorEvent sip错误订阅
     */
    @Override
    public void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,
                              ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        String stream = ssrcInfo.getStream();
        if (device == null) {
            return;
        }
        logger.info("{} 分配的ZLM为: {} [{}:{}]", stream, mediaServerItem.getId(), mediaServerItem.getIp(), ssrcInfo.getPort());
        HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", stream, true, "rtsp", mediaServerItem.getId());
        subscribe.addSubscribe(hookSubscribe, (MediaServerItem mediaServerItemInUse, JSONObject json) -> {
            if (event != null) {
                event.response(mediaServerItemInUse, json);
                subscribe.removeSubscribe(hookSubscribe);
            }
        });
        //
        StringBuffer content = new StringBuffer(200);
        content.append("v=0\r\n");
        content.append("o=" + channelId + " 0 0 IN IP4 " + mediaServerItem.getSdpIp() + "\r\n");
        content.append("s=Play\r\n");
        content.append("c=IN IP4 " + mediaServerItem.getSdpIp() + "\r\n");
        content.append("t=0 0\r\n");
        if (userSetting.isSeniorSdp()) {
            if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) {
                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
            } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) {
                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
            } else if ("UDP".equalsIgnoreCase(device.getStreamMode())) {
                content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 126 125 99 34 98 97\r\n");
            }
            content.append("a=recvonly\r\n");
            content.append("a=rtpmap:96 PS/90000\r\n");
            content.append("a=fmtp:126 profile-level-id=42e01e\r\n");
            content.append("a=rtpmap:126 H264/90000\r\n");
            content.append("a=rtpmap:125 H264S/90000\r\n");
            content.append("a=fmtp:125 profile-level-id=42e01e\r\n");
            content.append("a=rtpmap:99 H265/90000\r\n");
            content.append("a=rtpmap:98 H264/90000\r\n");
            content.append("a=rtpmap:97 MPEG4/90000\r\n");
            if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp被动模式
                content.append("a=setup:passive\r\n");
                content.append("a=connection:new\r\n");
            } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp主动模式
                content.append("a=setup:active\r\n");
                content.append("a=connection:new\r\n");
            }
        } else {
            if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) {
                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n");
            } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) {
                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n");
            } else if ("UDP".equalsIgnoreCase(device.getStreamMode())) {
                content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 97 98 99\r\n");
            }
            content.append("a=recvonly\r\n");
            content.append("a=rtpmap:96 PS/90000\r\n");
            content.append("a=rtpmap:98 H264/90000\r\n");
            content.append("a=rtpmap:97 MPEG4/90000\r\n");
            content.append("a=rtpmap:99 H265/90000\r\n");
            if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp被动模式
                content.append("a=setup:passive\r\n");
                content.append("a=connection:new\r\n");
            } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp主动模式
                content.append("a=setup:active\r\n");
                content.append("a=connection:new\r\n");
            }
        }
        content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc
        // f字段:f= v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率
//            content.append("f=v/2/5/25/1/4000a/1/8/1" + "\r\n"); // 未发现支持此特性的设备
        Request request = headerProvider.createInviteRequest(device, channelId, content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null, ssrcInfo.getSsrc(),sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request, (e -> {
            streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
            mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
            errorEvent.response(e);
        }), e -> {
            // 这里为例避免一个通道的点播只有一个callID这个参数使用一个固定值
            ResponseEvent responseEvent = (ResponseEvent) e.event;
            SIPResponse response = (SIPResponse) responseEvent.getResponse();
            streamSession.put(device.getDeviceId(), channelId, "play", stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, VideoStreamSessionManager.SessionType.play);
            okEvent.response(e);
        });
    }
    /**
     * 请求回放视频流
     *
     * @param device    视频设备
     * @param channelId 预览通道
     * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
     * @param endTime   结束时间,格式要求:yyyy-MM-dd HH:mm:ss
     */
    @Override
    public void playbackStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,
                                  String startTime, String endTime, InviteStreamCallback inviteStreamCallback, InviteStreamCallback hookEvent,
                                  SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        logger.info("{} 分配的ZLM为: {} [{}:{}]", ssrcInfo.getStream(), mediaServerItem.getId(), mediaServerItem.getIp(), ssrcInfo.getPort());
        StringBuffer content = new StringBuffer(200);
        content.append("v=0\r\n");
        content.append("o=" + channelId + " 0 0 IN IP4 " + mediaServerItem.getSdpIp() + "\r\n");
        content.append("s=Playback\r\n");
        content.append("u=" + channelId + ":0\r\n");
        content.append("c=IN IP4 " + mediaServerItem.getSdpIp() + "\r\n");
        content.append("t=" + DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime) + " "
                + DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime) + "\r\n");
        String streamMode = device.getStreamMode();
        if (userSetting.isSeniorSdp()) {
            if ("TCP-PASSIVE".equalsIgnoreCase(streamMode)) {
                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
            } else if ("TCP-ACTIVE".equalsIgnoreCase(streamMode)) {
                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
            } else if ("UDP".equalsIgnoreCase(streamMode)) {
                content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 126 125 99 34 98 97\r\n");
            }
            content.append("a=recvonly\r\n");
            content.append("a=rtpmap:96 PS/90000\r\n");
            content.append("a=fmtp:126 profile-level-id=42e01e\r\n");
            content.append("a=rtpmap:126 H264/90000\r\n");
            content.append("a=rtpmap:125 H264S/90000\r\n");
            content.append("a=fmtp:125 profile-level-id=42e01e\r\n");
            content.append("a=rtpmap:99 H265/90000\r\n");
            content.append("a=rtpmap:98 H264/90000\r\n");
            content.append("a=rtpmap:97 MPEG4/90000\r\n");
            if ("TCP-PASSIVE".equalsIgnoreCase(streamMode)) { // tcp被动模式
                content.append("a=setup:passive\r\n");
                content.append("a=connection:new\r\n");
            } else if ("TCP-ACTIVE".equalsIgnoreCase(streamMode)) { // tcp主动模式
                content.append("a=setup:active\r\n");
                content.append("a=connection:new\r\n");
            }
        } else {
            if ("TCP-PASSIVE".equalsIgnoreCase(streamMode)) {
                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n");
            } else if ("TCP-ACTIVE".equalsIgnoreCase(streamMode)) {
                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n");
            } else if ("UDP".equalsIgnoreCase(streamMode)) {
                content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 97 98 99\r\n");
            }
            content.append("a=recvonly\r\n");
            content.append("a=rtpmap:96 PS/90000\r\n");
            content.append("a=rtpmap:97 MPEG4/90000\r\n");
            content.append("a=rtpmap:98 H264/90000\r\n");
            content.append("a=rtpmap:99 H265/90000\r\n");
            if ("TCP-PASSIVE".equalsIgnoreCase(streamMode)) { // tcp被动模式
                content.append("a=setup:passive\r\n");
                content.append("a=connection:new\r\n");
            } else if ("TCP-ACTIVE".equalsIgnoreCase(streamMode)) { // tcp主动模式
                content.append("a=setup:active\r\n");
                content.append("a=connection:new\r\n");
            }
        }
        content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc
        HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", ssrcInfo.getStream(), true, "rtsp", mediaServerItem.getId());
        // 添加订阅
        subscribe.addSubscribe(hookSubscribe, (MediaServerItem mediaServerItemInUse, JSONObject json) -> {
            if (hookEvent != null) {
                InviteStreamInfo inviteStreamInfo = new InviteStreamInfo(mediaServerItemInUse, json,sipSender.getNewCallIdHeader(device.getTransport()).getCallId(), "rtp", ssrcInfo.getStream());
                hookEvent.call(inviteStreamInfo);
            }
            subscribe.removeSubscribe(hookSubscribe);
        });
        Request request = headerProvider.createPlaybackInviteRequest(device, channelId, content.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()), ssrcInfo.getSsrc());
        sipSender.transmitRequest( request, errorEvent, event -> {
            ResponseEvent responseEvent = (ResponseEvent) event.event;
            SIPResponse response = (SIPResponse) responseEvent.getResponse();
            streamSession.put(device.getDeviceId(), channelId,sipSender.getNewCallIdHeader(device.getTransport()).getCallId(), ssrcInfo.getStream(), ssrcInfo.getSsrc(), mediaServerItem.getId(), response, VideoStreamSessionManager.SessionType.playback);
            okEvent.response(event);
        });
        if (inviteStreamCallback != null) {
            inviteStreamCallback.call(new InviteStreamInfo(mediaServerItem, null,sipSender.getNewCallIdHeader(device.getTransport()).getCallId(), "rtp", ssrcInfo.getStream()));
        }
    }
    /**
     * 请求历史媒体下载
     *
     * @param device        视频设备
     * @param channelId     预览通道
     * @param startTime     开始时间,格式要求:yyyy-MM-dd HH:mm:ss
     * @param endTime       结束时间,格式要求:yyyy-MM-dd HH:mm:ss
     * @param downloadSpeed 下载倍速参数
     */
    @Override
    public void downloadStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,
                                  String startTime, String endTime, int downloadSpeed, InviteStreamCallback inviteStreamCallback, InviteStreamCallback hookEvent,
                                  SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        logger.info("{} 分配的ZLM为: {} [{}:{}]", ssrcInfo.getStream(), mediaServerItem.getId(), mediaServerItem.getIp(), ssrcInfo.getPort());
        StringBuffer content = new StringBuffer(200);
        content.append("v=0\r\n");
        content.append("o=" + channelId + " 0 0 IN IP4 " + mediaServerItem.getSdpIp() + "\r\n");
        content.append("s=Download\r\n");
        content.append("u=" + channelId + ":0\r\n");
        content.append("c=IN IP4 " + mediaServerItem.getSdpIp() + "\r\n");
        content.append("t=" + DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime) + " "
                + DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime) + "\r\n");
        String streamMode = device.getStreamMode().toUpperCase();
        if (userSetting.isSeniorSdp()) {
            if ("TCP-PASSIVE".equals(streamMode)) {
                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
            } else if ("TCP-ACTIVE".equals(streamMode)) {
                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
            } else if ("UDP".equals(streamMode)) {
                content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 126 125 99 34 98 97\r\n");
            }
            content.append("a=recvonly\r\n");
            content.append("a=rtpmap:96 PS/90000\r\n");
            content.append("a=fmtp:126 profile-level-id=42e01e\r\n");
            content.append("a=rtpmap:126 H264/90000\r\n");
            content.append("a=rtpmap:125 H264S/90000\r\n");
            content.append("a=fmtp:125 profile-level-id=42e01e\r\n");
            content.append("a=rtpmap:99 MP4V-ES/90000\r\n");
            content.append("a=fmtp:99 profile-level-id=3\r\n");
            content.append("a=rtpmap:98 H264/90000\r\n");
            content.append("a=rtpmap:97 MPEG4/90000\r\n");
            if ("TCP-PASSIVE".equals(streamMode)) { // tcp被动模式
                content.append("a=setup:passive\r\n");
                content.append("a=connection:new\r\n");
            } else if ("TCP-ACTIVE".equals(streamMode)) { // tcp主动模式
                content.append("a=setup:active\r\n");
                content.append("a=connection:new\r\n");
            }
        } else {
            if ("TCP-PASSIVE".equals(streamMode)) {
                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n");
            } else if ("TCP-ACTIVE".equals(streamMode)) {
                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n");
            } else if ("UDP".equals(streamMode)) {
                content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 97 98 99\r\n");
            }
            content.append("a=recvonly\r\n");
            content.append("a=rtpmap:96 PS/90000\r\n");
            content.append("a=rtpmap:97 MPEG4/90000\r\n");
            content.append("a=rtpmap:98 H264/90000\r\n");
            content.append("a=rtpmap:99 H265/90000\r\n");
            if ("TCP-PASSIVE".equals(streamMode)) { // tcp被动模式
                content.append("a=setup:passive\r\n");
                content.append("a=connection:new\r\n");
            } else if ("TCP-ACTIVE".equals(streamMode)) { // tcp主动模式
                content.append("a=setup:active\r\n");
                content.append("a=connection:new\r\n");
            }
        }
        content.append("a=downloadspeed:" + downloadSpeed + "\r\n");
        content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc
        HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", ssrcInfo.getStream(), true, null, mediaServerItem.getId());
        // 添加订阅
        subscribe.addSubscribe(hookSubscribe, (MediaServerItem mediaServerItemInUse, JSONObject json) -> {
            hookEvent.call(new InviteStreamInfo(mediaServerItem, json,sipSender.getNewCallIdHeader(device.getTransport()).getCallId(), "rtp", ssrcInfo.getStream()));
            subscribe.removeSubscribe(hookSubscribe);
            hookSubscribe.getContent().put("regist", false);
            hookSubscribe.getContent().put("schema", "rtsp");
            // 添加流注销的订阅,注销了后向设备发送bye
            subscribe.addSubscribe(hookSubscribe,
                    (MediaServerItem mediaServerItemForEnd, JSONObject jsonForEnd) -> {
                        logger.info("[录像]下载结束, 发送BYE");
                        try {
                            streamByeCmd(device, channelId, ssrcInfo.getStream(),sipSender.getNewCallIdHeader(device.getTransport()).getCallId());
                        } catch (InvalidArgumentException | ParseException | SipException |
                                 SsrcTransactionNotFoundException e) {
                            logger.error("[录像]下载结束, 发送BYE失败 {}", e.getMessage());
                        }
                    });
        });
        Request request = headerProvider.createPlaybackInviteRequest(device, channelId, content.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()), ssrcInfo.getSsrc());
        if (inviteStreamCallback != null) {
            inviteStreamCallback.call(new InviteStreamInfo(mediaServerItem, null,sipSender.getNewCallIdHeader(device.getTransport()).getCallId(), "rtp", ssrcInfo.getStream()));
        }
        sipSender.transmitRequest( request, errorEvent, okEvent -> {
            ResponseEvent responseEvent = (ResponseEvent) okEvent.event;
            SIPResponse response = (SIPResponse) responseEvent.getResponse();
            streamSession.put(device.getDeviceId(), channelId,sipSender.getNewCallIdHeader(device.getTransport()).getCallId(), ssrcInfo.getStream(), ssrcInfo.getSsrc(), mediaServerItem.getId(), response, VideoStreamSessionManager.SessionType.download);
        });
    }
    /**
     * 视频流停止, 不使用回调
     */
    @Override
    public void streamByeCmd(Device device, String channelId, String stream, String callId) throws InvalidArgumentException, ParseException, SipException, SsrcTransactionNotFoundException {
        streamByeCmd(device, channelId, stream, callId, null);
    }
    /**
     * 视频流停止
     */
    @Override
    public void streamByeCmd(Device device, String channelId, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException {
        SsrcTransaction ssrcTransaction = streamSession.getSsrcTransaction(device.getDeviceId(), channelId, callId, stream);
        if (ssrcTransaction == null) {
            throw new SsrcTransactionNotFoundException(device.getDeviceId(), channelId, callId, stream);
        }
        mediaServerService.releaseSsrc(ssrcTransaction.getMediaServerId(), ssrcTransaction.getSsrc());
        mediaServerService.closeRTPServer(ssrcTransaction.getMediaServerId(), ssrcTransaction.getStream());
        streamSession.remove(ssrcTransaction.getDeviceId(), ssrcTransaction.getChannelId(), ssrcTransaction.getStream());
        Request byteRequest = headerProvider.createByteRequest(device, channelId, ssrcTransaction.getSipTransactionInfo());
        sipSender.transmitRequest( byteRequest, null, okEvent);
    }
    /**
     * 语音广播
     *
     * @param device    视频设备
     * @param channelId 预览通道
     */
    @Override
    public void audioBroadcastCmd(Device device, String channelId) {
    }
    /**
     * 语音广播
     *
     * @param device 视频设备
     */
    @Override
    public void audioBroadcastCmd(Device device) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer broadcastXml = new StringBuffer(200);
        String charset = device.getCharset();
        broadcastXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        broadcastXml.append("<Notify>\r\n");
        broadcastXml.append("<CmdType>Broadcast</CmdType>\r\n");
        broadcastXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        broadcastXml.append("<SourceID>" + sipConfig.getId() + "</SourceID>\r\n");
        broadcastXml.append("<TargetID>" + device.getDeviceId() + "</TargetID>\r\n");
        broadcastXml.append("</Notify>\r\n");
        Request request = headerProvider.createMessageRequest(device, broadcastXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request);
    }
    @Override
    public void audioBroadcastCmd(Device device, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer broadcastXml = new StringBuffer(200);
        String charset = device.getCharset();
        broadcastXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        broadcastXml.append("<Notify>\r\n");
        broadcastXml.append("<CmdType>Broadcast</CmdType>\r\n");
        broadcastXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        broadcastXml.append("<SourceID>" + sipConfig.getId() + "</SourceID>\r\n");
        broadcastXml.append("<TargetID>" + device.getDeviceId() + "</TargetID>\r\n");
        broadcastXml.append("</Notify>\r\n");
        Request request = headerProvider.createMessageRequest(device, broadcastXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request, errorEvent);
    }
    /**
     * 音视频录像控制
     *
     * @param device       视频设备
     * @param channelId    预览通道
     * @param recordCmdStr 录像命令:Record / StopRecord
     */
    @Override
    public void recordCmd(Device device, String channelId, String recordCmdStr, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer cmdXml = new StringBuffer(200);
        String charset = device.getCharset();
        cmdXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        cmdXml.append("<Control>\r\n");
        cmdXml.append("<CmdType>DeviceControl</CmdType>\r\n");
        cmdXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        if (ObjectUtils.isEmpty(channelId)) {
            cmdXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        } else {
            cmdXml.append("<DeviceID>" + channelId + "</DeviceID>\r\n");
        }
        cmdXml.append("<RecordCmd>" + recordCmdStr + "</RecordCmd>\r\n");
        cmdXml.append("</Control>\r\n");
        Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request, errorEvent);
    }
    /**
     * 远程启动控制命令
     *
     * @param device 视频设备
     */
    @Override
    public void teleBootCmd(Device device) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer cmdXml = new StringBuffer(200);
        String charset = device.getCharset();
        cmdXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        cmdXml.append("<Control>\r\n");
        cmdXml.append("<CmdType>DeviceControl</CmdType>\r\n");
        cmdXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        cmdXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        cmdXml.append("<TeleBoot>Boot</TeleBoot>\r\n");
        cmdXml.append("</Control>\r\n");
        Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request);
    }
    /**
     * 报警布防/撤防命令
     *
     * @param device      视频设备
     * @param guardCmdStr "SetGuard"/"ResetGuard"
     */
    @Override
    public void guardCmd(Device device, String guardCmdStr, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer cmdXml = new StringBuffer(200);
        String charset = device.getCharset();
        cmdXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        cmdXml.append("<Control>\r\n");
        cmdXml.append("<CmdType>DeviceControl</CmdType>\r\n");
        cmdXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        cmdXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        cmdXml.append("<GuardCmd>" + guardCmdStr + "</GuardCmd>\r\n");
        cmdXml.append("</Control>\r\n");
        Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request, errorEvent);
    }
    /**
     * 报警复位命令
     *
     * @param device 视频设备
     */
    @Override
    public void alarmCmd(Device device, String alarmMethod, String alarmType, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer cmdXml = new StringBuffer(200);
        String charset = device.getCharset();
        cmdXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        cmdXml.append("<Control>\r\n");
        cmdXml.append("<CmdType>DeviceControl</CmdType>\r\n");
        cmdXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        cmdXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        cmdXml.append("<AlarmCmd>ResetAlarm</AlarmCmd>\r\n");
        if (!ObjectUtils.isEmpty(alarmMethod) || !ObjectUtils.isEmpty(alarmType)) {
            cmdXml.append("<Info>\r\n");
        }
        if (!ObjectUtils.isEmpty(alarmMethod)) {
            cmdXml.append("<AlarmMethod>" + alarmMethod + "</AlarmMethod>\r\n");
        }
        if (!ObjectUtils.isEmpty(alarmType)) {
            cmdXml.append("<AlarmType>" + alarmType + "</AlarmType>\r\n");
        }
        if (!ObjectUtils.isEmpty(alarmMethod) || !ObjectUtils.isEmpty(alarmType)) {
            cmdXml.append("</Info>\r\n");
        }
        cmdXml.append("</Control>\r\n");
        Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request, errorEvent);
    }
    /**
     * 强制关键帧命令,设备收到此命令应立刻发送一个IDR帧
     *
     * @param device    视频设备
     * @param channelId 预览通道
     */
    @Override
    public void iFrameCmd(Device device, String channelId) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer cmdXml = new StringBuffer(200);
        String charset = device.getCharset();
        cmdXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        cmdXml.append("<Control>\r\n");
        cmdXml.append("<CmdType>DeviceControl</CmdType>\r\n");
        cmdXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        if (ObjectUtils.isEmpty(channelId)) {
            cmdXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        } else {
            cmdXml.append("<DeviceID>" + channelId + "</DeviceID>\r\n");
        }
        cmdXml.append("<IFameCmd>Send</IFameCmd>\r\n");
        cmdXml.append("</Control>\r\n");
        Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request);
    }
    /**
     * 看守位控制命令
     *
     * @param device      视频设备
     * @param enabled     看守位使能:1 = 开启,0 = 关闭
     * @param resetTime   自动归位时间间隔,开启看守位时使用,单位:秒(s)
     * @param presetIndex 调用预置位编号,开启看守位时使用,取值范围0~255
     */
    @Override
    public void homePositionCmd(Device device, String channelId, String enabled, String resetTime, String presetIndex, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer cmdXml = new StringBuffer(200);
        String charset = device.getCharset();
        cmdXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        cmdXml.append("<Control>\r\n");
        cmdXml.append("<CmdType>DeviceControl</CmdType>\r\n");
        cmdXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        if (ObjectUtils.isEmpty(channelId)) {
            cmdXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        } else {
            cmdXml.append("<DeviceID>" + channelId + "</DeviceID>\r\n");
        }
        cmdXml.append("<HomePosition>\r\n");
        if (NumericUtil.isInteger(enabled) && (!enabled.equals("0"))) {
            cmdXml.append("<Enabled>1</Enabled>\r\n");
            if (NumericUtil.isInteger(resetTime)) {
                cmdXml.append("<ResetTime>" + resetTime + "</ResetTime>\r\n");
            } else {
                cmdXml.append("<ResetTime>0</ResetTime>\r\n");
            }
            if (NumericUtil.isInteger(presetIndex)) {
                cmdXml.append("<PresetIndex>" + presetIndex + "</PresetIndex>\r\n");
            } else {
                cmdXml.append("<PresetIndex>0</PresetIndex>\r\n");
            }
        } else {
            cmdXml.append("<Enabled>0</Enabled>\r\n");
        }
        cmdXml.append("</HomePosition>\r\n");
        cmdXml.append("</Control>\r\n");
        Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request, errorEvent);
    }
    /**
     * 设备配置命令
     *
     * @param device 视频设备
     */
    @Override
    public void deviceConfigCmd(Device device) {
        // TODO Auto-generated method stub
    }
    /**
     * 设备配置命令:basicParam
     *
     * @param device            视频设备
     * @param channelId         通道编码(可选)
     * @param name              设备/通道名称(可选)
     * @param expiration        注册过期时间(可选)
     * @param heartBeatInterval 心跳间隔时间(可选)
     * @param heartBeatCount    心跳超时次数(可选)
     */
    @Override
    public void deviceBasicConfigCmd(Device device, String channelId, String name, String expiration,
                                     String heartBeatInterval, String heartBeatCount, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer cmdXml = new StringBuffer(200);
        String charset = device.getCharset();
        cmdXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        cmdXml.append("<Control>\r\n");
        cmdXml.append("<CmdType>DeviceConfig</CmdType>\r\n");
        cmdXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        if (ObjectUtils.isEmpty(channelId)) {
            cmdXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        } else {
            cmdXml.append("<DeviceID>" + channelId + "</DeviceID>\r\n");
        }
        cmdXml.append("<BasicParam>\r\n");
        if (!ObjectUtils.isEmpty(name)) {
            cmdXml.append("<Name>" + name + "</Name>\r\n");
        }
        if (NumericUtil.isInteger(expiration)) {
            if (Integer.valueOf(expiration) > 0) {
                cmdXml.append("<Expiration>" + expiration + "</Expiration>\r\n");
            }
        }
        if (NumericUtil.isInteger(heartBeatInterval)) {
            if (Integer.valueOf(heartBeatInterval) > 0) {
                cmdXml.append("<HeartBeatInterval>" + heartBeatInterval + "</HeartBeatInterval>\r\n");
            }
        }
        if (NumericUtil.isInteger(heartBeatCount)) {
            if (Integer.valueOf(heartBeatCount) > 0) {
                cmdXml.append("<HeartBeatCount>" + heartBeatCount + "</HeartBeatCount>\r\n");
            }
        }
        cmdXml.append("</BasicParam>\r\n");
        cmdXml.append("</Control>\r\n");
        Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request, errorEvent);
    }
    /**
     * 查询设备状态
     *
     * @param device 视频设备
     */
    @Override
    public void deviceStatusQuery(Device device, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        String charset = device.getCharset();
        StringBuffer catalogXml = new StringBuffer(200);
        catalogXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        catalogXml.append("<Query>\r\n");
        catalogXml.append("<CmdType>DeviceStatus</CmdType>\r\n");
        catalogXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        catalogXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        catalogXml.append("</Query>\r\n");
        Request request = headerProvider.createMessageRequest(device, catalogXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request, errorEvent);
    }
    /**
     * 查询设备信息
     *
     * @param device 视频设备
     */
    @Override
    public void deviceInfoQuery(Device device) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer catalogXml = new StringBuffer(200);
        String charset = device.getCharset();
        catalogXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        catalogXml.append("<Query>\r\n");
        catalogXml.append("<CmdType>DeviceInfo</CmdType>\r\n");
        catalogXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        catalogXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        catalogXml.append("</Query>\r\n");
        Request request = headerProvider.createMessageRequest(device, catalogXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request);
    }
    /**
     * 查询目录列表
     *
     * @param device 视频设备
     */
    @Override
    public void catalogQuery(Device device, int sn, SipSubscribe.Event errorEvent) throws SipException, InvalidArgumentException, ParseException {
        StringBuffer catalogXml = new StringBuffer(200);
        String charset = device.getCharset();
        catalogXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        catalogXml.append("<Query>\r\n");
        catalogXml.append("  <CmdType>Catalog</CmdType>\r\n");
        catalogXml.append("  <SN>" + sn + "</SN>\r\n");
        catalogXml.append("  <DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        catalogXml.append("</Query>\r\n");
        Request request = headerProvider.createMessageRequest(device, catalogXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request, errorEvent);
    }
    /**
     * 查询录像信息
     *
     * @param device    视频设备
     * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
     * @param endTime   结束时间,格式要求:yyyy-MM-dd HH:mm:ss
     */
    @Override
    public void recordInfoQuery(Device device, String channelId, String startTime, String endTime, int sn, Integer secrecy, String type, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        if (secrecy == null) {
            secrecy = 0;
        }
        if (type == null) {
            type = "all";
        }
        StringBuffer recordInfoXml = new StringBuffer(200);
        String charset = device.getCharset();
        recordInfoXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        recordInfoXml.append("<Query>\r\n");
        recordInfoXml.append("<CmdType>RecordInfo</CmdType>\r\n");
        recordInfoXml.append("<SN>" + sn + "</SN>\r\n");
        recordInfoXml.append("<DeviceID>" + channelId + "</DeviceID>\r\n");
        if (startTime != null) {
            recordInfoXml.append("<StartTime>" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(startTime) + "</StartTime>\r\n");
        }
        if (endTime != null) {
            recordInfoXml.append("<EndTime>" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(endTime) + "</EndTime>\r\n");
        }
        if (secrecy != null) {
            recordInfoXml.append("<Secrecy> " + secrecy + " </Secrecy>\r\n");
        }
        if (type != null) {
            // 大华NVR要求必须增加一个值为all的文本元素节点Type
            recordInfoXml.append("<Type>" + type + "</Type>\r\n");
        }
        recordInfoXml.append("</Query>\r\n");
        Request request = headerProvider.createMessageRequest(device, recordInfoXml.toString(),
                SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request, errorEvent, okEvent);
    }
    /**
     * 查询报警信息
     *
     * @param device        视频设备
     * @param startPriority 报警起始级别(可选)
     * @param endPriority   报警终止级别(可选)
     * @param alarmMethod   报警方式条件(可选)
     * @param alarmType     报警类型
     * @param startTime     报警发生起始时间(可选)
     * @param endTime       报警发生终止时间(可选)
     * @return true = 命令发送成功
     */
    @Override
    public void alarmInfoQuery(Device device, String startPriority, String endPriority, String alarmMethod, String alarmType,
                               String startTime, String endTime, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer cmdXml = new StringBuffer(200);
        String charset = device.getCharset();
        cmdXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        cmdXml.append("<Query>\r\n");
        cmdXml.append("<CmdType>Alarm</CmdType>\r\n");
        cmdXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        cmdXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        if (!ObjectUtils.isEmpty(startPriority)) {
            cmdXml.append("<StartAlarmPriority>" + startPriority + "</StartAlarmPriority>\r\n");
        }
        if (!ObjectUtils.isEmpty(endPriority)) {
            cmdXml.append("<EndAlarmPriority>" + endPriority + "</EndAlarmPriority>\r\n");
        }
        if (!ObjectUtils.isEmpty(alarmMethod)) {
            cmdXml.append("<AlarmMethod>" + alarmMethod + "</AlarmMethod>\r\n");
        }
        if (!ObjectUtils.isEmpty(alarmType)) {
            cmdXml.append("<AlarmType>" + alarmType + "</AlarmType>\r\n");
        }
        if (!ObjectUtils.isEmpty(startTime)) {
            cmdXml.append("<StartAlarmTime>" + startTime + "</StartAlarmTime>\r\n");
        }
        if (!ObjectUtils.isEmpty(endTime)) {
            cmdXml.append("<EndAlarmTime>" + endTime + "</EndAlarmTime>\r\n");
        }
        cmdXml.append("</Query>\r\n");
        Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request, errorEvent);
    }
    /**
     * 查询设备配置
     *
     * @param device     视频设备
     * @param channelId  通道编码(可选)
     * @param configType 配置类型:
     */
    @Override
    public void deviceConfigQuery(Device device, String channelId, String configType, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer cmdXml = new StringBuffer(200);
        String charset = device.getCharset();
        cmdXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        cmdXml.append("<Query>\r\n");
        cmdXml.append("<CmdType>ConfigDownload</CmdType>\r\n");
        cmdXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        if (ObjectUtils.isEmpty(channelId)) {
            cmdXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        } else {
            cmdXml.append("<DeviceID>" + channelId + "</DeviceID>\r\n");
        }
        cmdXml.append("<ConfigType>" + configType + "</ConfigType>\r\n");
        cmdXml.append("</Query>\r\n");
        Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request, errorEvent);
    }
    /**
     * 查询设备预置位置
     *
     * @param device 视频设备
     */
    @Override
    public void presetQuery(Device device, String channelId, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer cmdXml = new StringBuffer(200);
        String charset = device.getCharset();
        cmdXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        cmdXml.append("<Query>\r\n");
        cmdXml.append("<CmdType>PresetQuery</CmdType>\r\n");
        cmdXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        if (ObjectUtils.isEmpty(channelId)) {
            cmdXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        } else {
            cmdXml.append("<DeviceID>" + channelId + "</DeviceID>\r\n");
        }
        cmdXml.append("</Query>\r\n");
        Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request, errorEvent);
    }
    /**
     * 查询移动设备位置数据
     *
     * @param device 视频设备
     */
    @Override
    public void mobilePostitionQuery(Device device, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer mobilePostitionXml = new StringBuffer(200);
        String charset = device.getCharset();
        mobilePostitionXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        mobilePostitionXml.append("<Query>\r\n");
        mobilePostitionXml.append("<CmdType>MobilePosition</CmdType>\r\n");
        mobilePostitionXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        mobilePostitionXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        mobilePostitionXml.append("<Interval>60</Interval>\r\n");
        mobilePostitionXml.append("</Query>\r\n");
        Request request = headerProvider.createMessageRequest(device, mobilePostitionXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request, errorEvent);
    }
    /**
     * 订阅、取消订阅移动位置
     *
     * @param device 视频设备
     * @return true = 命令发送成功
     */
    @Override
    public SIPRequest mobilePositionSubscribe(Device device, SIPRequest requestOld, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer subscribePostitionXml = new StringBuffer(200);
        String charset = device.getCharset();
        subscribePostitionXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        subscribePostitionXml.append("<Query>\r\n");
        subscribePostitionXml.append("<CmdType>MobilePosition</CmdType>\r\n");
        subscribePostitionXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        subscribePostitionXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        if (device.getSubscribeCycleForMobilePosition() > 0) {
            subscribePostitionXml.append("<Interval>" + device.getMobilePositionSubmissionInterval() + "</Interval>\r\n");
        }
        subscribePostitionXml.append("</Query>\r\n");
        CallIdHeader callIdHeader;
        if (requestOld != null) {
            callIdHeader = sipFactory.createHeaderFactory().createCallIdHeader(requestOld.getCallIdHeader().getCallId());
        } else {
            callIdHeader = sipSender.getNewCallIdHeader(device.getTransport());
        }
        SIPRequest request = (SIPRequest) headerProvider.createSubscribeRequest(device, subscribePostitionXml.toString(), requestOld, device.getSubscribeCycleForMobilePosition(), "presence",callIdHeader); //Position;id=" + tm.substring(tm.length() - 4));
        sipSender.transmitRequest( request, errorEvent, okEvent);
        return request;
    }
    /**
     * 订阅、取消订阅报警信息
     *
     * @param device        视频设备
     * @param expires       订阅过期时间(0 = 取消订阅)
     * @param startPriority 报警起始级别(可选)
     * @param endPriority   报警终止级别(可选)
     * @param alarmMethod   报警方式条件(可选)
     * @param alarmType     报警类型
     * @param startTime     报警发生起始时间(可选)
     * @param endTime       报警发生终止时间(可选)
     * @return true = 命令发送成功
     */
    @Override
    public void alarmSubscribe(Device device, int expires, String startPriority, String endPriority, String alarmMethod, String alarmType, String startTime, String endTime) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer cmdXml = new StringBuffer(200);
        String charset = device.getCharset();
        cmdXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        cmdXml.append("<Query>\r\n");
        cmdXml.append("<CmdType>Alarm</CmdType>\r\n");
        cmdXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        cmdXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        if (!ObjectUtils.isEmpty(startPriority)) {
            cmdXml.append("<StartAlarmPriority>" + startPriority + "</StartAlarmPriority>\r\n");
        }
        if (!ObjectUtils.isEmpty(endPriority)) {
            cmdXml.append("<EndAlarmPriority>" + endPriority + "</EndAlarmPriority>\r\n");
        }
        if (!ObjectUtils.isEmpty(alarmMethod)) {
            cmdXml.append("<AlarmMethod>" + alarmMethod + "</AlarmMethod>\r\n");
        }
        if (!ObjectUtils.isEmpty(alarmType)) {
            cmdXml.append("<AlarmType>" + alarmType + "</AlarmType>\r\n");
        }
        if (!ObjectUtils.isEmpty(startTime)) {
            cmdXml.append("<StartAlarmTime>" + startTime + "</StartAlarmTime>\r\n");
        }
        if (!ObjectUtils.isEmpty(endTime)) {
            cmdXml.append("<EndAlarmTime>" + endTime + "</EndAlarmTime>\r\n");
        }
        cmdXml.append("</Query>\r\n");
        Request request = headerProvider.createSubscribeRequest(device, cmdXml.toString(), null, expires, "presence",sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest( request);
    }
    @Override
    public SIPRequest catalogSubscribe(Device device, SIPRequest requestOld, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer cmdXml = new StringBuffer(200);
        String charset = device.getCharset();
        cmdXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        cmdXml.append("<Query>\r\n");
        cmdXml.append("<CmdType>Catalog</CmdType>\r\n");
        cmdXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        cmdXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        cmdXml.append("</Query>\r\n");
        CallIdHeader callIdHeader;
        if (requestOld != null) {
            callIdHeader = sipFactory.createHeaderFactory().createCallIdHeader(requestOld.getCallIdHeader().getCallId());
        } else {
            callIdHeader = sipSender.getNewCallIdHeader(device.getTransport());
        }
        // 有效时间默认为60秒以上
        SIPRequest request = (SIPRequest) headerProvider.createSubscribeRequest(device, cmdXml.toString(), requestOld, device.getSubscribeCycleForCatalog(), "Catalog",
                callIdHeader);
        sipSender.transmitRequest( request, errorEvent, okEvent);
        return request;
    }
    @Override
    public void dragZoomCmd(Device device, String channelId, String cmdString) throws InvalidArgumentException, SipException, ParseException {
        StringBuffer dragXml = new StringBuffer(200);
        String charset = device.getCharset();
        dragXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        dragXml.append("<Control>\r\n");
        dragXml.append("<CmdType>DeviceControl</CmdType>\r\n");
        dragXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        if (ObjectUtils.isEmpty(channelId)) {
            dragXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        } else {
            dragXml.append("<DeviceID>" + channelId + "</DeviceID>\r\n");
        }
        dragXml.append(cmdString);
        dragXml.append("</Control>\r\n");
        Request request = headerProvider.createMessageRequest(device, dragXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        logger.debug("拉框信令: " + request.toString());
        sipSender.transmitRequest(request);
    }
    /**
     * 回放暂停
     */
    @Override
    public void playPauseCmd(Device device, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException {
        StringBuffer content = new StringBuffer(200);
        content.append("PAUSE RTSP/1.0\r\n");
        content.append("CSeq: " + getInfoCseq() + "\r\n");
        content.append("PauseTime: now\r\n");
        playbackControlCmd(device, streamInfo, content.toString(), null, null);
    }
    /**
     * 回放恢复
     */
    @Override
    public void playResumeCmd(Device device, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException {
        StringBuffer content = new StringBuffer(200);
        content.append("PLAY RTSP/1.0\r\n");
        content.append("CSeq: " + getInfoCseq() + "\r\n");
        content.append("Range: npt=now-\r\n");
        playbackControlCmd(device, streamInfo, content.toString(), null, null);
    }
    /**
     * 回放拖动播放
     */
    @Override
    public void playSeekCmd(Device device, StreamInfo streamInfo, long seekTime) throws InvalidArgumentException, ParseException, SipException {
        StringBuffer content = new StringBuffer(200);
        content.append("PLAY RTSP/1.0\r\n");
        content.append("CSeq: " + getInfoCseq() + "\r\n");
        content.append("Range: npt=" + Math.abs(seekTime) + "-\r\n");
        playbackControlCmd(device, streamInfo, content.toString(), null, null);
    }
    /**
     * 回放倍速播放
     */
    @Override
    public void playSpeedCmd(Device device, StreamInfo streamInfo, Double speed) throws InvalidArgumentException, ParseException, SipException {
        StringBuffer content = new StringBuffer(200);
        content.append("PLAY RTSP/1.0\r\n");
        content.append("CSeq: " + getInfoCseq() + "\r\n");
        content.append("Scale: " + String.format("%.6f", speed) + "\r\n");
        playbackControlCmd(device, streamInfo, content.toString(), null, null);
    }
    private int getInfoCseq() {
        return (int) ((Math.random() * 9 + 1) * Math.pow(10, 8));
    }
    @Override
    public void playbackControlCmd(Device device, StreamInfo streamInfo, String content, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException {
        SsrcTransaction ssrcTransaction = streamSession.getSsrcTransaction(device.getDeviceId(), streamInfo.getChannelId(), null, streamInfo.getStream());
        if (ssrcTransaction == null) {
            logger.info("[回放控制]未找到视频流信息,设备:{}, 流ID: {}", device.getDeviceId(), streamInfo.getStream());
            return;
        }
        SIPRequest request = headerProvider.createInfoRequest(device, streamInfo.getChannelId(), content.toString(), ssrcTransaction.getSipTransactionInfo());
        if (request == null) {
            logger.info("[回放控制]构建Request信息失败,设备:{}, 流ID: {}", device.getDeviceId(), streamInfo.getStream());
            return;
        }
        sipSender.transmitRequest( request, errorEvent, okEvent);
    }
    @Override
    public void sendAlarmMessage(Device device, DeviceAlarm deviceAlarm) throws InvalidArgumentException, SipException, ParseException {
        if (device == null) {
            return;
        }
        logger.info("[发送 报警通知] {}/{}->{},{}", device.getDeviceId(), deviceAlarm.getChannelId(),
                deviceAlarm.getLongitude(), deviceAlarm.getLatitude());
        String characterSet = device.getCharset();
        StringBuffer deviceStatusXml = new StringBuffer(600);
        deviceStatusXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
        deviceStatusXml.append("<Notify>\r\n");
        deviceStatusXml.append("<CmdType>Alarm</CmdType>\r\n");
        deviceStatusXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        deviceStatusXml.append("<DeviceID>" + deviceAlarm.getChannelId() + "</DeviceID>\r\n");
        deviceStatusXml.append("<AlarmPriority>" + deviceAlarm.getAlarmPriority() + "</AlarmPriority>\r\n");
        deviceStatusXml.append("<AlarmMethod>" + deviceAlarm.getAlarmMethod() + "</AlarmMethod>\r\n");
        deviceStatusXml.append("<AlarmTime>" + deviceAlarm.getAlarmTime() + "</AlarmTime>\r\n");
        deviceStatusXml.append("<AlarmDescription>" + deviceAlarm.getAlarmDescription() + "</AlarmDescription>\r\n");
        deviceStatusXml.append("<Longitude>" + deviceAlarm.getLongitude() + "</Longitude>\r\n");
        deviceStatusXml.append("<Latitude>" + deviceAlarm.getLatitude() + "</Latitude>\r\n");
        deviceStatusXml.append("<info>\r\n");
        deviceStatusXml.append("<AlarmType>" + deviceAlarm.getAlarmType() + "</AlarmType>\r\n");
        deviceStatusXml.append("</info>\r\n");
        deviceStatusXml.append("</Notify>\r\n");
        Request request = headerProvider.createMessageRequest(device, deviceStatusXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(device.getTransport()));
        sipSender.transmitRequest(request);
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderFroPlatform.java
New file
@@ -0,0 +1,648 @@
package com.genersoft.iot.vmp.gb28181.transmit.cmd.impl;
import com.alibaba.fastjson.JSONObject;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
import com.genersoft.iot.vmp.gb28181.transmit.SIPSender;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.SIPRequestHeaderPlarformProvider;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import com.genersoft.iot.vmp.storager.dao.dto.PlatformRegisterInfo;
import com.genersoft.iot.vmp.utils.DateUtil;
import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.service.IMediaServerService;
import com.genersoft.iot.vmp.service.bean.GPSMsgInfo;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import gov.nist.javax.sip.SipProviderImpl;
import gov.nist.javax.sip.message.MessageFactoryImpl;
import gov.nist.javax.sip.message.SIPRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import javax.sip.*;
import javax.sip.header.*;
import javax.sip.message.Request;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
@Component
@DependsOn("sipLayer")
public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
    private final Logger logger = LoggerFactory.getLogger(SIPCommanderFroPlatform.class);
    @Autowired
    private SIPRequestHeaderPlarformProvider headerProviderPlatformProvider;
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Autowired
    private IMediaServerService mediaServerService;
    @Autowired
    private SipSubscribe sipSubscribe;
    @Autowired
    private ZLMRTPServerFactory zlmrtpServerFactory;
    @Autowired
    private SipFactory sipFactory;
    @Autowired
    private SIPSender sipSender;
    @Override
    public void register(ParentPlatform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException {
        register(parentPlatform, null, null, errorEvent, okEvent, false, true);
    }
    @Override
    public void unregister(ParentPlatform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException {
        register(parentPlatform, null, null, errorEvent, okEvent, false, false);
    }
    @Override
    public void register(ParentPlatform parentPlatform, @Nullable String callId, @Nullable WWWAuthenticateHeader www,
                            SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent, boolean registerAgain, boolean isRegister) throws SipException, InvalidArgumentException, ParseException {
            Request request;
            if (!registerAgain ) {
                CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getTransport());
                request = headerProviderPlatformProvider.createRegisterRequest(parentPlatform,
                        redisCatchStorage.getCSEQ(), SipUtils.getNewFromTag(),
                        SipUtils.getNewViaTag(), callIdHeader, isRegister);
                // 将 callid 写入缓存, 等注册成功可以更新状态
                String callIdFromHeader = callIdHeader.getCallId();
                redisCatchStorage.updatePlatformRegisterInfo(callIdFromHeader, PlatformRegisterInfo.getInstance(parentPlatform.getServerGBId(), isRegister));
                sipSubscribe.addErrorSubscribe(callIdHeader.getCallId(), (event)->{
                    if (event != null) {
                        logger.info("向上级平台 [ {} ] 注册发生错误: {} ",
                                parentPlatform.getServerGBId(),
                                event.msg);
                    }
                    redisCatchStorage.delPlatformRegisterInfo(callIdFromHeader);
                    if (errorEvent != null ) {
                        errorEvent.response(event);
                    }
                });
            }else {
                CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getTransport());
                request = headerProviderPlatformProvider.createRegisterRequest(parentPlatform, SipUtils.getNewFromTag(), null, callId, www, callIdHeader, isRegister);
            }
            sipSender.transmitRequest( request, null, okEvent);
    }
    @Override
    public String keepalive(ParentPlatform parentPlatform,SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException {
            String characterSet = parentPlatform.getCharacterSet();
            StringBuffer keepaliveXml = new StringBuffer(200);
            keepaliveXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
            keepaliveXml.append("<Notify>\r\n");
            keepaliveXml.append("<CmdType>Keepalive</CmdType>\r\n");
            keepaliveXml.append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n");
            keepaliveXml.append("<DeviceID>" + parentPlatform.getDeviceGBId() + "</DeviceID>\r\n");
            keepaliveXml.append("<Status>OK</Status>\r\n");
            keepaliveXml.append("</Notify>\r\n");
        CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getTransport());
            Request request = headerProviderPlatformProvider.createMessageRequest(
                    parentPlatform,
                    keepaliveXml.toString(),
                    SipUtils.getNewFromTag(),
                    SipUtils.getNewViaTag(),
                    callIdHeader);
            sipSender.transmitRequest( request, errorEvent, okEvent);
        return callIdHeader.getCallId();
    }
    /**
     * 向上级回复通道信息
     * @param channel 通道信息
     * @param parentPlatform 平台信息
     * @return
     */
    @Override
    public void catalogQuery(DeviceChannel channel, ParentPlatform parentPlatform, String sn, String fromTag, int size) throws SipException, InvalidArgumentException, ParseException {
        if ( parentPlatform ==null) {
            return ;
        }
        List<DeviceChannel> channels = new ArrayList<>();
        if (channel != null) {
            channels.add(channel);
        }
        String catalogXml = getCatalogXml(channels, sn, parentPlatform, size);
        // callid
        CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getTransport());
        Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, catalogXml.toString(), fromTag, SipUtils.getNewViaTag(), callIdHeader);
        sipSender.transmitRequest( request);
    }
    @Override
    public void catalogQuery(List<DeviceChannel> channels, ParentPlatform parentPlatform, String sn, String fromTag) throws InvalidArgumentException, ParseException, SipException {
        if ( parentPlatform ==null) {
            return ;
        }
        sendCatalogResponse(channels, parentPlatform, sn, fromTag, 0);
    }
    private String getCatalogXml(List<DeviceChannel> channels, String sn, ParentPlatform parentPlatform, int size) {
        String characterSet = parentPlatform.getCharacterSet();
        StringBuffer catalogXml = new StringBuffer(600);
        catalogXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet +"\"?>\r\n");
        catalogXml.append("<Response>\r\n");
        catalogXml.append("<CmdType>Catalog</CmdType>\r\n");
        catalogXml.append("<SN>" +sn + "</SN>\r\n");
        catalogXml.append("<DeviceID>" + parentPlatform.getDeviceGBId() + "</DeviceID>\r\n");
        catalogXml.append("<SumNum>" + size + "</SumNum>\r\n");
        catalogXml.append("<DeviceList Num=\"" + channels.size() +"\">\r\n");
        if (channels.size() > 0) {
            for (DeviceChannel channel : channels) {
                if (parentPlatform.getServerGBId().equals(channel.getParentId())) {
                    channel.setParentId(parentPlatform.getDeviceGBId());
                }
                catalogXml.append("<Item>\r\n");
                // 行政区划分组只需要这两项就可以
                catalogXml.append("<DeviceID>" + channel.getChannelId() + "</DeviceID>\r\n");
                catalogXml.append("<Name>" + channel.getName() + "</Name>\r\n");
                if (channel.getParentId() != null) {
                    // 业务分组加上这一项即可,提高兼容性,
                    catalogXml.append("<ParentID>" + channel.getParentId() + "</ParentID>\r\n");
//                    catalogXml.append("<ParentID>" + parentPlatform.getDeviceGBId() + "/" + channel.getParentId() + "</ParentID>\r\n");
                }
                if (channel.getChannelId().length() == 20 && Integer.parseInt(channel.getChannelId().substring(10, 13)) == 216) {
                    // 虚拟组织增加BusinessGroupID字段
                    catalogXml.append("<BusinessGroupID>" + channel.getParentId() + "</BusinessGroupID>\r\n");
                }
                if (!channel.getChannelId().equals(parentPlatform.getDeviceGBId())) {
                    catalogXml.append("<Parental>" + channel.getParental() + "</Parental>\r\n");
                    if (channel.getParental() == 0) {
                        catalogXml.append("<Status>" + (channel.getStatus() == 0 ? "OFF" : "ON") + "</Status>\r\n");
                    }
                }
                if (channel.getParental() == 0) {
                    // 通道项
                    catalogXml.append("<manufacture>" + channel.getManufacture() + "</manufacture>\r\n");
                    catalogXml.append("<Secrecy>" + channel.getSecrecy() + "</Secrecy>\r\n");
                    catalogXml.append("<RegisterWay>" + channel.getRegisterWay() + "</RegisterWay>\r\n");
                    String civilCode = channel.getCivilCode() == null?parentPlatform.getAdministrativeDivision() : channel.getCivilCode();
                    if (channel.getChannelType() != 2) {  // 业务分组/虚拟组织/行政区划 不设置以下属性
                        catalogXml.append("<Model>" + channel.getModel() + "</Model>\r\n");
                        catalogXml.append("<Owner>" + parentPlatform.getDeviceGBId()+ "</Owner>\r\n");
                        catalogXml.append("<CivilCode>" + civilCode + "</CivilCode>\r\n");
                        if (channel.getAddress() == null) {
                            catalogXml.append("<Address></Address>\r\n");
                        }else {
                            catalogXml.append("<Address>" + channel.getAddress() + "</Address>\r\n");
                        }
                    }
                }
                catalogXml.append("</Item>\r\n");
            }
        }
        catalogXml.append("</DeviceList>\r\n");
        catalogXml.append("</Response>\r\n");
        return catalogXml.toString();
    }
    private void sendCatalogResponse(List<DeviceChannel> channels, ParentPlatform parentPlatform, String sn, String fromTag, int index) throws SipException, InvalidArgumentException, ParseException {
        if (index >= channels.size()) {
            return;
        }
        List<DeviceChannel> deviceChannels;
        if (index + parentPlatform.getCatalogGroup() < channels.size()) {
            deviceChannels = channels.subList(index, index + parentPlatform.getCatalogGroup());
        }else {
            deviceChannels = channels.subList(index, channels.size());
        }
        String catalogXml = getCatalogXml(deviceChannels, sn, parentPlatform, channels.size());
        // callid
        CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getTransport());
        Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, catalogXml, fromTag, SipUtils.getNewViaTag(), callIdHeader);
        sipSender.transmitRequest( request, null, eventResult -> {
            int indexNext = index + parentPlatform.getCatalogGroup();
            try {
                sendCatalogResponse(channels, parentPlatform, sn, fromTag, indexNext);
            } catch (SipException | InvalidArgumentException | ParseException e) {
                logger.error("[命令发送失败] 国标级联 目录查询回复: {}", e.getMessage());
            }
        });
    }
    /**
     * 向上级回复DeviceInfo查询信息
     * @param parentPlatform 平台信息
     * @param sn
     * @param fromTag
     * @return
     */
    @Override
    public void deviceInfoResponse(ParentPlatform parentPlatform, String sn, String fromTag) throws SipException, InvalidArgumentException, ParseException {
        if (parentPlatform == null) {
            return;
        }
        String characterSet = parentPlatform.getCharacterSet();
        StringBuffer deviceInfoXml = new StringBuffer(600);
        deviceInfoXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
        deviceInfoXml.append("<Response>\r\n");
        deviceInfoXml.append("<CmdType>DeviceInfo</CmdType>\r\n");
        deviceInfoXml.append("<SN>" +sn + "</SN>\r\n");
        deviceInfoXml.append("<DeviceID>" + parentPlatform.getDeviceGBId() + "</DeviceID>\r\n");
        deviceInfoXml.append("<DeviceName>" + parentPlatform.getName() + "</DeviceName>\r\n");
        deviceInfoXml.append("<manufacture>wvp</manufacture>\r\n");
        deviceInfoXml.append("<Model>wvp-28181-2.0</Model>\r\n");
        deviceInfoXml.append("<Firmware>2.0.202107</Firmware>\r\n");
        deviceInfoXml.append("<Result>OK</Result>\r\n");
        deviceInfoXml.append("</Response>\r\n");
        CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getTransport());
        Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, deviceInfoXml.toString(), fromTag, SipUtils.getNewViaTag(), callIdHeader);
        sipSender.transmitRequest( request);
    }
    /**
     * 向上级回复DeviceStatus查询信息
     * @param parentPlatform 平台信息
     * @param sn
     * @param fromTag
     * @return
     */
    @Override
    public void deviceStatusResponse(ParentPlatform parentPlatform, String sn, String fromTag) throws SipException, InvalidArgumentException, ParseException {
        if (parentPlatform == null) {
            return ;
        }
        String characterSet = parentPlatform.getCharacterSet();
        StringBuffer deviceStatusXml = new StringBuffer(600);
        deviceStatusXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
        deviceStatusXml.append("<Response>\r\n");
        deviceStatusXml.append("<CmdType>DeviceStatus</CmdType>\r\n");
        deviceStatusXml.append("<SN>" +sn + "</SN>\r\n");
        deviceStatusXml.append("<DeviceID>" + parentPlatform.getDeviceGBId() + "</DeviceID>\r\n");
        deviceStatusXml.append("<Result>OK</Result>\r\n");
        deviceStatusXml.append("<Online>ONLINE</Online>\r\n");
        deviceStatusXml.append("<Status>OK</Status>\r\n");
        deviceStatusXml.append("</Response>\r\n");
        CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getTransport());
        Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, deviceStatusXml.toString(), fromTag, SipUtils.getNewViaTag(), callIdHeader);
        sipSender.transmitRequest( request);
    }
    @Override
    public void sendNotifyMobilePosition(ParentPlatform parentPlatform, GPSMsgInfo gpsMsgInfo, SubscribeInfo subscribeInfo) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException {
        if (parentPlatform == null) {
            return;
        }
        if (logger.isDebugEnabled()) {
            logger.debug("[发送 移动位置订阅] {}/{}->{},{}", parentPlatform.getServerGBId(), gpsMsgInfo.getId(), gpsMsgInfo.getLng(), gpsMsgInfo.getLat());
        }
        String characterSet = parentPlatform.getCharacterSet();
        StringBuffer deviceStatusXml = new StringBuffer(600);
        deviceStatusXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
        deviceStatusXml.append("<Notify>\r\n");
        deviceStatusXml.append("<CmdType>MobilePosition</CmdType>\r\n");
        deviceStatusXml.append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n");
        deviceStatusXml.append("<DeviceID>" + gpsMsgInfo.getId() + "</DeviceID>\r\n");
        deviceStatusXml.append("<Time>" + gpsMsgInfo.getTime() + "</Time>\r\n");
        deviceStatusXml.append("<Longitude>" + gpsMsgInfo.getLng() + "</Longitude>\r\n");
        deviceStatusXml.append("<Latitude>" + gpsMsgInfo.getLat() + "</Latitude>\r\n");
        deviceStatusXml.append("<Speed>" + gpsMsgInfo.getSpeed() + "</Speed>\r\n");
        deviceStatusXml.append("<Direction>" + gpsMsgInfo.getDirection() + "</Direction>\r\n");
        deviceStatusXml.append("<Altitude>" + gpsMsgInfo.getAltitude() + "</Altitude>\r\n");
        deviceStatusXml.append("</Notify>\r\n");
       sendNotify(parentPlatform, deviceStatusXml.toString(), subscribeInfo, eventResult -> {
            logger.error("发送NOTIFY通知消息失败。错误:{} {}", eventResult.statusCode, eventResult.msg);
        }, null);
    }
    @Override
    public void sendAlarmMessage(ParentPlatform parentPlatform, DeviceAlarm deviceAlarm) throws SipException, InvalidArgumentException, ParseException {
        if (parentPlatform == null) {
            return;
        }
        logger.info("[发送报警通知] {}/{}->{},{}: {}", parentPlatform.getServerGBId(), deviceAlarm.getChannelId(),
                deviceAlarm.getLongitude(), deviceAlarm.getLatitude(), JSONObject.toJSON(deviceAlarm));
        String characterSet = parentPlatform.getCharacterSet();
        StringBuffer deviceStatusXml = new StringBuffer(600);
        deviceStatusXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
        deviceStatusXml.append("<Notify>\r\n");
        deviceStatusXml.append("<CmdType>Alarm</CmdType>\r\n");
        deviceStatusXml.append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n");
        deviceStatusXml.append("<DeviceID>" + deviceAlarm.getChannelId() + "</DeviceID>\r\n");
        deviceStatusXml.append("<AlarmPriority>" + deviceAlarm.getAlarmPriority() + "</AlarmPriority>\r\n");
        deviceStatusXml.append("<AlarmMethod>" + deviceAlarm.getAlarmMethod() + "</AlarmMethod>\r\n");
        deviceStatusXml.append("<AlarmTime>" + deviceAlarm.getAlarmTime() + "</AlarmTime>\r\n");
        deviceStatusXml.append("<AlarmDescription>" + deviceAlarm.getAlarmDescription() + "</AlarmDescription>\r\n");
        deviceStatusXml.append("<Longitude>" + deviceAlarm.getLongitude() + "</Longitude>\r\n");
        deviceStatusXml.append("<Latitude>" + deviceAlarm.getLatitude() + "</Latitude>\r\n");
        deviceStatusXml.append("<info>\r\n");
        deviceStatusXml.append("<AlarmType>" + deviceAlarm.getAlarmType() + "</AlarmType>\r\n");
        deviceStatusXml.append("</info>\r\n");
        deviceStatusXml.append("</Notify>\r\n");
        CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getTransport());
        Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, deviceStatusXml.toString(), SipUtils.getNewFromTag(), SipUtils.getNewViaTag(), callIdHeader);
        sipSender.transmitRequest( request);
    }
    @Override
    public void sendNotifyForCatalogAddOrUpdate(String type, ParentPlatform parentPlatform, List<DeviceChannel> deviceChannels, SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException {
        if (parentPlatform == null || deviceChannels == null || deviceChannels.size() == 0 || subscribeInfo == null) {
            return;
        }
        if (index == null) {
            index = 0;
        }
        if (index >= deviceChannels.size()) {
            return;
        }
        List<DeviceChannel> channels;
        if (index + parentPlatform.getCatalogGroup() < deviceChannels.size()) {
            channels = deviceChannels.subList(index, index + parentPlatform.getCatalogGroup());
        }else {
            channels = deviceChannels.subList(index, deviceChannels.size());
        }
        Integer finalIndex = index;
        String catalogXmlContent = getCatalogXmlContentForCatalogAddOrUpdate(parentPlatform, channels,
                deviceChannels.size(), type, subscribeInfo);
        sendNotify(parentPlatform, catalogXmlContent, subscribeInfo, eventResult -> {
            logger.error("发送NOTIFY通知消息失败。错误:{} {}", eventResult.statusCode, eventResult.msg);
        }, (eventResult -> {
            try {
                sendNotifyForCatalogAddOrUpdate(type, parentPlatform, deviceChannels, subscribeInfo,
                        finalIndex + parentPlatform.getCatalogGroup());
            } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException |
                     IllegalAccessException e) {
                logger.error("[命令发送失败] 国标级联 NOTIFY通知: {}", e.getMessage());
            }
        }));
    }
    private void sendNotify(ParentPlatform parentPlatform, String catalogXmlContent,
                                   SubscribeInfo subscribeInfo, SipSubscribe.Event errorEvent,  SipSubscribe.Event okEvent )
            throws SipException, ParseException, InvalidArgumentException {
        MessageFactoryImpl messageFactory = (MessageFactoryImpl) sipFactory.createMessageFactory();
        String characterSet = parentPlatform.getCharacterSet();
         // 设置编码, 防止中文乱码
        messageFactory.setDefaultContentEncodingCharset(characterSet);
        SIPRequest notifyRequest = headerProviderPlatformProvider.createNotifyRequest(parentPlatform, catalogXmlContent, subscribeInfo);
        sipSender.transmitRequest( notifyRequest);
    }
    private  String getCatalogXmlContentForCatalogAddOrUpdate(ParentPlatform parentPlatform, List<DeviceChannel> channels, int sumNum, String type, SubscribeInfo subscribeInfo) {
        StringBuffer catalogXml = new StringBuffer(600);
        String characterSet = parentPlatform.getCharacterSet();
        catalogXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
        catalogXml.append("<Notify>\r\n");
        catalogXml.append("<CmdType>Catalog</CmdType>\r\n");
        catalogXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        catalogXml.append("<DeviceID>" + parentPlatform.getDeviceGBId() + "</DeviceID>\r\n");
        catalogXml.append("<SumNum>1</SumNum>\r\n");
        catalogXml.append("<DeviceList Num=\"" + channels.size() + "\">\r\n");
        if (channels.size() > 0) {
            for (DeviceChannel channel : channels) {
                if (parentPlatform.getServerGBId().equals(channel.getParentId())) {
                    channel.setParentId(parentPlatform.getDeviceGBId());
                }
                catalogXml.append("<Item>\r\n");
                // 行政区划分组只需要这两项就可以
                catalogXml.append("<DeviceID>" + channel.getChannelId() + "</DeviceID>\r\n");
                catalogXml.append("<Name>" + channel.getName() + "</Name>\r\n");
                if (channel.getParentId() != null) {
                    // 业务分组加上这一项即可,提高兼容性,
                    catalogXml.append("<ParentID>" + channel.getParentId() + "</ParentID>\r\n");
                }
                if (channel.getChannelId().length() == 20 && Integer.parseInt(channel.getChannelId().substring(10, 13)) == 216) {
                    // 虚拟组织增加BusinessGroupID字段
                    catalogXml.append("<BusinessGroupID>" + channel.getParentId() + "</BusinessGroupID>\r\n");
                }
                catalogXml.append("<Parental>" + channel.getParental() + "</Parental>\r\n");
                if (channel.getParental() == 0) {
                    // 通道项
                    catalogXml.append("<manufacture>" + channel.getManufacture() + "</manufacture>\r\n");
                    catalogXml.append("<Secrecy>" + channel.getSecrecy() + "</Secrecy>\r\n");
                    catalogXml.append("<RegisterWay>" + channel.getRegisterWay() + "</RegisterWay>\r\n");
                    catalogXml.append("<Status>" + (channel.getStatus() == 0 ? "OFF" : "ON") + "</Status>\r\n");
                    if (channel.getChannelType() != 2) {  // 业务分组/虚拟组织/行政区划 不设置以下属性
                        catalogXml.append("<Model>" + channel.getModel() + "</Model>\r\n");
                        catalogXml.append("<Owner> " + channel.getOwner()+ "</Owner>\r\n");
                        catalogXml.append("<CivilCode>" + channel.getCivilCode() + "</CivilCode>\r\n");
                        catalogXml.append("<Address>" + channel.getAddress() + "</Address>\r\n");
                    }
                    if (!"presence".equals(subscribeInfo.getEventType())) {
                        catalogXml.append("<Event>" + type + "</Event>\r\n");
                    }
                }
                catalogXml.append("</Item>\r\n");
            }
        }
        catalogXml.append("</DeviceList>\r\n");
        catalogXml.append("</Notify>\r\n");
        return catalogXml.toString();
    }
    @Override
    public void sendNotifyForCatalogOther(String type, ParentPlatform parentPlatform, List<DeviceChannel> deviceChannels,
                                             SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException {
        if (parentPlatform == null
                || deviceChannels == null
                || deviceChannels.size() == 0
                || subscribeInfo == null) {
            logger.warn("[缺少必要参数]");
            return;
        }
        if (index == null) {
            index = 0;
        }
        if (index >= deviceChannels.size()) {
            return;
        }
        List<DeviceChannel> channels;
        if (index + parentPlatform.getCatalogGroup() < deviceChannels.size()) {
            channels = deviceChannels.subList(index, index + parentPlatform.getCatalogGroup());
        }else {
            channels = deviceChannels.subList(index, deviceChannels.size());
        }
        Integer finalIndex = index;
        String catalogXmlContent = getCatalogXmlContentForCatalogOther(parentPlatform, channels, type);
        sendNotify(parentPlatform, catalogXmlContent, subscribeInfo, eventResult -> {
            logger.error("发送NOTIFY通知消息失败。错误:{} {}", eventResult.statusCode, eventResult.msg);
        }, eventResult -> {
            try {
                sendNotifyForCatalogOther(type, parentPlatform, deviceChannels, subscribeInfo,
                        finalIndex + parentPlatform.getCatalogGroup());
            } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException |
                     IllegalAccessException e) {
                logger.error("[命令发送失败] 国标级联 NOTIFY通知: {}", e.getMessage());
            }
        });
    }
    private String getCatalogXmlContentForCatalogOther(ParentPlatform parentPlatform, List<DeviceChannel> channels, String type) {
        String characterSet = parentPlatform.getCharacterSet();
        StringBuffer catalogXml = new StringBuffer(600);
        catalogXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
        catalogXml.append("<Notify>\r\n");
        catalogXml.append("<CmdType>Catalog</CmdType>\r\n");
        catalogXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        catalogXml.append("<DeviceID>" + parentPlatform.getDeviceGBId() + "</DeviceID>\r\n");
        catalogXml.append("<SumNum>1</SumNum>\r\n");
        catalogXml.append("<DeviceList Num=\" " + channels.size() + " \">\r\n");
        if (channels.size() > 0) {
            for (DeviceChannel channel : channels) {
                if (parentPlatform.getServerGBId().equals(channel.getParentId())) {
                    channel.setParentId(parentPlatform.getDeviceGBId());
                }
                catalogXml.append("<Item>\r\n");
                catalogXml.append("<DeviceID>" + channel.getChannelId() + "</DeviceID>\r\n");
                catalogXml.append("<Event>" + type + "</Event>\r\n");
                catalogXml.append("</Item>\r\n");
            }
        }
        catalogXml.append("</DeviceList>\r\n");
        catalogXml.append("</Notify>\r\n");
        return catalogXml.toString();
    }
    @Override
    public void recordInfo(DeviceChannel deviceChannel, ParentPlatform parentPlatform, String fromTag, RecordInfo recordInfo) throws SipException, InvalidArgumentException, ParseException {
        if ( parentPlatform ==null) {
            return ;
        }
        String characterSet = parentPlatform.getCharacterSet();
        StringBuffer recordXml = new StringBuffer(600);
        recordXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
        recordXml.append("<Response>\r\n");
        recordXml.append("<CmdType>RecordInfo</CmdType>\r\n");
        recordXml.append("<SN>" +recordInfo.getSn() + "</SN>\r\n");
        recordXml.append("<DeviceID>" + recordInfo.getDeviceId() + "</DeviceID>\r\n");
        recordXml.append("<SumNum>" + recordInfo.getSumNum() + "</SumNum>\r\n");
        if (recordInfo.getRecordList() == null ) {
            recordXml.append("<RecordList Num=\"0\">\r\n");
        }else {
            recordXml.append("<RecordList Num=\"" + recordInfo.getRecordList().size()+"\">\r\n");
            if (recordInfo.getRecordList().size() > 0) {
                for (RecordItem recordItem : recordInfo.getRecordList()) {
                    recordXml.append("<Item>\r\n");
                    if (deviceChannel != null) {
                        recordXml.append("<DeviceID>" + recordItem.getDeviceId() + "</DeviceID>\r\n");
                        recordXml.append("<Name>" + recordItem.getName() + "</Name>\r\n");
                        recordXml.append("<StartTime>" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(recordItem.getStartTime()) + "</StartTime>\r\n");
                        recordXml.append("<EndTime>" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(recordItem.getEndTime()) + "</EndTime>\r\n");
                        recordXml.append("<Secrecy>" + recordItem.getSecrecy() + "</Secrecy>\r\n");
                        recordXml.append("<Type>" + recordItem.getType() + "</Type>\r\n");
                        if (!ObjectUtils.isEmpty(recordItem.getFileSize())) {
                            recordXml.append("<FileSize>" + recordItem.getFileSize() + "</FileSize>\r\n");
                        }
                        if (!ObjectUtils.isEmpty(recordItem.getFilePath())) {
                            recordXml.append("<FilePath>" + recordItem.getFilePath() + "</FilePath>\r\n");
                        }
                    }
                    recordXml.append("</Item>\r\n");
                }
            }
        }
        recordXml.append("</RecordList>\r\n");
        recordXml.append("</Response>\r\n");
        // callid
        CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getTransport());
        Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, recordXml.toString(), fromTag, SipUtils.getNewViaTag(), callIdHeader);
        sipSender.transmitRequest( request);
    }
    @Override
    public void sendMediaStatusNotify(ParentPlatform platform, SendRtpItem sendRtpItem) throws SipException, InvalidArgumentException, ParseException {
        if (sendRtpItem == null || platform == null) {
            return;
        }
        String characterSet = platform.getCharacterSet();
        StringBuffer mediaStatusXml = new StringBuffer(200);
        mediaStatusXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
        mediaStatusXml.append("<Notify>\r\n");
        mediaStatusXml.append("<CmdType>MediaStatus</CmdType>\r\n");
        mediaStatusXml.append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n");
        mediaStatusXml.append("<DeviceID>" + sendRtpItem.getChannelId() + "</DeviceID>\r\n");
        mediaStatusXml.append("<NotifyType>121</NotifyType>\r\n");
        mediaStatusXml.append("</Notify>\r\n");
        SIPRequest messageRequest = (SIPRequest)headerProviderPlatformProvider.createMessageRequest(platform, mediaStatusXml.toString(),
                sendRtpItem);
        sipSender.transmitRequest(messageRequest);
    }
    @Override
    public void streamByeCmd(ParentPlatform platform, String callId) throws SipException, InvalidArgumentException, ParseException {
        if (platform == null) {
            return;
        }
        SendRtpItem sendRtpItem = redisCatchStorage.querySendRTPServer(platform.getServerGBId(), null, null, callId);
        if (sendRtpItem != null) {
            streamByeCmd(platform, sendRtpItem);
        }
    }
    @Override
    public void streamByeCmd(ParentPlatform platform, SendRtpItem sendRtpItem) throws SipException, InvalidArgumentException, ParseException {
        if (sendRtpItem == null ) {
            logger.info("[向上级发送BYE], sendRtpItem 为NULL");
            return;
        }
        if (platform == null) {
            logger.info("[向上级发送BYE], platform 为NULL");
            return;
        }
        logger.info("[向上级发送BYE], {}/{}", platform.getServerGBId(), sendRtpItem.getChannelId());
        String mediaServerId = sendRtpItem.getMediaServerId();
        MediaServerItem mediaServerItem = mediaServerService.getOne(mediaServerId);
        if (mediaServerItem != null) {
            mediaServerService.releaseSsrc(mediaServerItem.getId(), sendRtpItem.getSsrc());
            zlmrtpServerFactory.closeRTPServer(mediaServerItem, sendRtpItem.getStreamId());
        }
        SIPRequest byeRequest = headerProviderPlatformProvider.createByeRequest(platform, sendRtpItem);
        if (byeRequest == null) {
            logger.warn("[向上级发送bye]:无法创建 byeRequest");
        }
        sipSender.transmitRequest(byeRequest);
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/ISIPRequestProcessor.java
New file
@@ -0,0 +1,14 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request;
import javax.sip.RequestEvent;
/**
 * @description: 对SIP事件进行处理,包括request, response, timeout, ioException, transactionTerminated,dialogTerminated
 * @author: panlinlin
 * @date:   2021年11月5日 15:47
 */
public interface ISIPRequestProcessor {
    void process(RequestEvent event);
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/SIPRequestProcessorParent.java
New file
@@ -0,0 +1,265 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.SIPSender;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import gov.nist.javax.sip.SipProviderImpl;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import org.apache.commons.lang3.ArrayUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import javax.sip.*;
import javax.sip.address.Address;
import javax.sip.address.AddressFactory;
import javax.sip.address.SipURI;
import javax.sip.header.*;
import javax.sip.message.MessageFactory;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.io.ByteArrayInputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
 * @description:处理接收IPCamera发来的SIP协议请求消息
 * @author: songww
 * @date:   2020年5月3日 下午4:42:22
 */
public abstract class SIPRequestProcessorParent {
    private final static Logger logger = LoggerFactory.getLogger(SIPRequestProcessorParent.class);
    @Autowired
    private SIPSender sipSender;
    public AddressFactory getAddressFactory() {
        try {
            return SipFactory.getInstance().createAddressFactory();
        } catch (PeerUnavailableException e) {
            e.printStackTrace();
        }
        return null;
    }
    public HeaderFactory getHeaderFactory() {
        try {
            return SipFactory.getInstance().createHeaderFactory();
        } catch (PeerUnavailableException e) {
            e.printStackTrace();
        }
        return null;
    }
    public MessageFactory getMessageFactory() {
        try {
            return SipFactory.getInstance().createMessageFactory();
        } catch (PeerUnavailableException e) {
            e.printStackTrace();
        }
        return null;
    }
    class ResponseAckExtraParam{
        String content;
        ContentTypeHeader contentTypeHeader;
        SipURI sipURI;
        int expires = -1;
    }
    /***
     * 回复状态码
     * 100 trying
     * 200 OK
     * 400
     * 404
     */
    public SIPResponse responseAck(SIPRequest sipRequest, int statusCode) throws SipException, InvalidArgumentException, ParseException {
        return responseAck(sipRequest, statusCode, null);
    }
    public SIPResponse responseAck(SIPRequest sipRequest, int statusCode, String msg) throws SipException, InvalidArgumentException, ParseException {
        return responseAck(sipRequest, statusCode, msg, null);
    }
//    public SIPResponse responseAck(ServerTransaction serverTransaction, int statusCode, String msg, ResponseAckExtraParam responseAckExtraParam) throws SipException, InvalidArgumentException, ParseException {
//        if (serverTransaction == null) {
//            logger.warn("[回复消息] ServerTransaction 为null");
//            return null;
//        }
//        ToHeader toHeader = (ToHeader) serverTransaction.getRequest().getHeader(ToHeader.NAME);
//        if (toHeader.getTag() == null) {
//            toHeader.setTag(SipUtils.getNewTag());
//        }
//        SIPResponse response = (SIPResponse)getMessageFactory().createResponse(statusCode, serverTransaction.getRequest());
//        if (msg != null) {
//            response.setReasonPhrase(msg);
//        }
//        if (responseAckExtraParam != null) {
//            if (responseAckExtraParam.sipURI != null && serverTransaction.getRequest().getMethod().equals(Request.INVITE)) {
//                logger.debug("responseSdpAck SipURI: {}:{}", responseAckExtraParam.sipURI.getHost(), responseAckExtraParam.sipURI.getPort());
//                Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(
//                        SipFactory.getInstance().createAddressFactory().createSipURI(responseAckExtraParam.sipURI.getUser(),  responseAckExtraParam.sipURI.getHost()+":"+responseAckExtraParam.sipURI.getPort()
//                        ));
//                response.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress));
//            }
//            if (responseAckExtraParam.contentTypeHeader != null) {
//                response.setContent(responseAckExtraParam.content, responseAckExtraParam.contentTypeHeader);
//            }
//
//            if (serverTransaction.getRequest().getMethod().equals(Request.SUBSCRIBE)) {
//                if (responseAckExtraParam.expires == -1) {
//                    logger.error("[参数不全] 2xx的SUBSCRIBE回复,必须设置Expires header");
//                }else {
//                    ExpiresHeader expiresHeader = SipFactory.getInstance().createHeaderFactory().createExpiresHeader(responseAckExtraParam.expires);
//                    response.addHeader(expiresHeader);
//                }
//            }
//        }else {
//            if (serverTransaction.getRequest().getMethod().equals(Request.SUBSCRIBE)) {
//                logger.error("[参数不全] 2xx的SUBSCRIBE回复,必须设置Expires header");
//            }
//        }
//        serverTransaction.sendResponse(response);
//        if (statusCode >= 200 && !"NOTIFY".equalsIgnoreCase(serverTransaction.getRequest().getMethod())) {
//            if (serverTransaction.getDialog() != null) {
//                serverTransaction.getDialog().delete();
//            }
//        }
//        return response;
//    }
    public SIPResponse responseAck(SIPRequest sipRequest, int statusCode, String msg, ResponseAckExtraParam responseAckExtraParam) throws SipException, InvalidArgumentException, ParseException {
        if (sipRequest.getToHeader().getTag() == null) {
            sipRequest.getToHeader().setTag(SipUtils.getNewTag());
        }
        SIPResponse response = (SIPResponse)getMessageFactory().createResponse(statusCode, sipRequest);
        response.setStatusCode(statusCode);
        if (msg != null) {
            response.setReasonPhrase(msg);
        }
        if (responseAckExtraParam != null) {
            if (responseAckExtraParam.sipURI != null && sipRequest.getMethod().equals(Request.INVITE)) {
                logger.debug("responseSdpAck SipURI: {}:{}", responseAckExtraParam.sipURI.getHost(), responseAckExtraParam.sipURI.getPort());
                Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(
                        SipFactory.getInstance().createAddressFactory().createSipURI(responseAckExtraParam.sipURI.getUser(),  responseAckExtraParam.sipURI.getHost()+":"+responseAckExtraParam.sipURI.getPort()
                        ));
                response.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress));
            }
            if (responseAckExtraParam.contentTypeHeader != null) {
                response.setContent(responseAckExtraParam.content, responseAckExtraParam.contentTypeHeader);
            }
            if (sipRequest.getMethod().equals(Request.SUBSCRIBE)) {
                if (responseAckExtraParam.expires == -1) {
                    logger.error("[参数不全] 2xx的SUBSCRIBE回复,必须设置Expires header");
                }else {
                    ExpiresHeader expiresHeader = SipFactory.getInstance().createHeaderFactory().createExpiresHeader(responseAckExtraParam.expires);
                    response.addHeader(expiresHeader);
                }
            }
        }else {
            if (sipRequest.getMethod().equals(Request.SUBSCRIBE)) {
                logger.error("[参数不全] 2xx的SUBSCRIBE回复,必须设置Expires header");
            }
        }
        // 发送response
        sipSender.transmitRequest(response);
        return response;
    }
    /**
     * 回复带sdp的200
     */
    public SIPResponse responseSdpAck(SIPRequest request, String sdp, ParentPlatform platform) throws SipException, InvalidArgumentException, ParseException {
        ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP");
        // 兼容国标中的使用编码@域名作为RequestURI的情况
        SipURI sipURI = (SipURI)request.getRequestURI();
        if (sipURI.getPort() == -1) {
            sipURI = SipFactory.getInstance().createAddressFactory().createSipURI(platform.getServerGBId(),  platform.getServerIP()+":"+platform.getServerPort());
        }
        ResponseAckExtraParam responseAckExtraParam = new ResponseAckExtraParam();
        responseAckExtraParam.contentTypeHeader = contentTypeHeader;
        responseAckExtraParam.content = sdp;
        responseAckExtraParam.sipURI = sipURI;
        return responseAck(request, Response.OK, null, responseAckExtraParam);
    }
    /**
     * 回复带xml的200
     */
    public SIPResponse responseXmlAck(SIPRequest request, String xml, ParentPlatform platform, Integer expires) throws SipException, InvalidArgumentException, ParseException {
        ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml");
        SipURI sipURI = (SipURI)request.getRequestURI();
        if (sipURI.getPort() == -1) {
            sipURI = SipFactory.getInstance().createAddressFactory().createSipURI(platform.getServerGBId(),  platform.getServerIP()+":"+platform.getServerPort());
        }
        ResponseAckExtraParam responseAckExtraParam = new ResponseAckExtraParam();
        responseAckExtraParam.contentTypeHeader = contentTypeHeader;
        responseAckExtraParam.content = xml;
        responseAckExtraParam.sipURI = sipURI;
        responseAckExtraParam.expires = expires;
        return responseAck(request, Response.OK, null, responseAckExtraParam);
    }
    public Element getRootElement(RequestEvent evt) throws DocumentException {
        return getRootElement(evt, "gb2312");
    }
    public Element getRootElement(RequestEvent evt, String charset) throws DocumentException {
        if (charset == null) {
            charset = "gb2312";
        }
        Request request = evt.getRequest();
        SAXReader reader = new SAXReader();
        reader.setEncoding(charset);
        // 对海康出现的未转义字符做处理。
        String[] destStrArray = new String[]{"&lt;","&gt;","&amp;","&apos;","&quot;"};
        char despChar = '&'; // 或许可扩展兼容其他字符
        byte destBye = (byte) despChar;
        List<Byte> result = new ArrayList<>();
        byte[] rawContent = request.getRawContent();
        if (rawContent == null) {
            return null;
        }
        for (int i = 0; i < rawContent.length; i++) {
            if (rawContent[i] == destBye) {
                boolean resul = false;
                for (String destStr : destStrArray) {
                    if (i + destStr.length() <= rawContent.length) {
                        byte[] bytes = Arrays.copyOfRange(rawContent, i, i + destStr.length());
                        resul = resul || (Arrays.equals(bytes,destStr.getBytes()));
                    }
                }
                if (resul) {
                    result.add(rawContent[i]);
                }
            }else {
                result.add(rawContent[i]);
            }
        }
        Byte[] bytes = new Byte[0];
        byte[] bytesResult = ArrayUtils.toPrimitive(result.toArray(bytes));
        Document xml = reader.read(new ByteArrayInputStream(bytesResult));
        return xml.getRootElement();
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/AckRequestProcessor.java
New file
@@ -0,0 +1,157 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl;
import com.alibaba.fastjson.JSONObject;
import com.genersoft.iot.vmp.conf.DynamicTask;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.bean.SendRtpItem;
import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.service.IMediaServerService;
import com.genersoft.iot.vmp.service.bean.RequestPushStreamMsg;
import com.genersoft.iot.vmp.service.redisMsg.RedisGbPlayMsgListener;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.address.SipURI;
import javax.sip.header.CallIdHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.HeaderAddress;
import javax.sip.header.ToHeader;
import java.text.ParseException;
import java.util.*;
/**
 * SIP命令类型: ACK请求
 */
@Component
public class AckRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor {
    private Logger logger = LoggerFactory.getLogger(AckRequestProcessor.class);
    private final String method = "ACK";
    @Autowired
    private SIPProcessorObserver sipProcessorObserver;
    @Override
    public void afterPropertiesSet() throws Exception {
        // 添加消息处理的订阅
        sipProcessorObserver.addRequestProcessor(method, this);
    }
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private ZLMRTPServerFactory zlmrtpServerFactory;
    @Autowired
    private IMediaServerService mediaServerService;
    @Autowired
    private ZlmHttpHookSubscribe subscribe;
    @Autowired
    private DynamicTask dynamicTask;
    @Autowired
    private ISIPCommander cmder;
    @Autowired
    private ISIPCommanderForPlatform commanderForPlatform;
    @Autowired
    private RedisGbPlayMsgListener redisGbPlayMsgListener;
    /**
     * 处理  ACK请求
     *
     * @param evt
     */
    @Override
    public void process(RequestEvent evt) {
        CallIdHeader callIdHeader = (CallIdHeader)evt.getRequest().getHeader(CallIdHeader.NAME);
        String platformGbId = ((SipURI) ((HeaderAddress) evt.getRequest().getHeader(FromHeader.NAME)).getAddress().getURI()).getUser();
        logger.info("[收到ACK]: platformGbId->{}", platformGbId);
        ParentPlatform parentPlatform = storager.queryParentPlatByServerGBId(platformGbId);
        // 取消设置的超时任务
        dynamicTask.stop(callIdHeader.getCallId());
        String channelId = ((SipURI) ((HeaderAddress) evt.getRequest().getHeader(ToHeader.NAME)).getAddress().getURI()).getUser();
        SendRtpItem sendRtpItem =  redisCatchStorage.querySendRTPServer(platformGbId, channelId, null, callIdHeader.getCallId());
        if (sendRtpItem == null) {
            logger.warn("[收到ACK]:未找到通道({})的推流信息", channelId);
            return;
        }
        String is_Udp = sendRtpItem.isTcp() ? "0" : "1";
        MediaServerItem mediaInfo = mediaServerService.getOne(sendRtpItem.getMediaServerId());
        logger.info("收到ACK,rtp/{}开始向上级推流, 目标={}:{},SSRC={}", sendRtpItem.getStreamId(), sendRtpItem.getIp(), sendRtpItem.getPort(), sendRtpItem.getSsrc());
        Map<String, Object> param = new HashMap<>(12);
        param.put("vhost","__defaultVhost__");
        param.put("app",sendRtpItem.getApp());
        param.put("stream",sendRtpItem.getStreamId());
        param.put("ssrc", sendRtpItem.getSsrc());
        param.put("dst_url",sendRtpItem.getIp());
        param.put("dst_port", sendRtpItem.getPort());
        param.put("is_udp", is_Udp);
        param.put("src_port", sendRtpItem.getLocalPort());
        param.put("pt", sendRtpItem.getPt());
        param.put("use_ps", sendRtpItem.isUsePs() ? "1" : "0");
        param.put("only_audio", sendRtpItem.isOnlyAudio() ? "1" : "0");
        if (!sendRtpItem.isTcp() && parentPlatform.isRtcp()) {
            // 开启rtcp保活
            param.put("udp_rtcp_timeout", "1");
        }
        if (mediaInfo == null) {
            RequestPushStreamMsg requestPushStreamMsg = RequestPushStreamMsg.getInstance(
                    sendRtpItem.getMediaServerId(), sendRtpItem.getApp(), sendRtpItem.getStreamId(),
                    sendRtpItem.getIp(), sendRtpItem.getPort(), sendRtpItem.getSsrc(), sendRtpItem.isTcp(),
                    sendRtpItem.getLocalPort(), sendRtpItem.getPt(), sendRtpItem.isUsePs(), sendRtpItem.isOnlyAudio());
            redisGbPlayMsgListener.sendMsgForStartSendRtpStream(sendRtpItem.getServerId(), requestPushStreamMsg, jsonObject->{
                startSendRtpStreamHand(evt, sendRtpItem, parentPlatform, jsonObject, param, callIdHeader);
            });
        }else {
            JSONObject jsonObject = zlmrtpServerFactory.startSendRtpStream(mediaInfo, param);
            startSendRtpStreamHand(evt, sendRtpItem, parentPlatform, jsonObject, param, callIdHeader);
        }
    }
    private void startSendRtpStreamHand(RequestEvent evt, SendRtpItem sendRtpItem, ParentPlatform parentPlatform,
                                        JSONObject jsonObject, Map<String, Object> param, CallIdHeader callIdHeader) {
        if (jsonObject == null) {
            logger.error("RTP推流失败: 请检查ZLM服务");
        } else if (jsonObject.getInteger("code") == 0) {
            logger.info("调用ZLM推流接口, 结果: {}",  jsonObject);
            logger.info("RTP推流成功[ {}/{} ],{}->{}:{}, " ,param.get("app"), param.get("stream"), jsonObject.getString("local_port"), param.get("dst_url"), param.get("dst_port"));
        } else {
            logger.error("RTP推流失败: {}, 参数:{}",jsonObject.getString("msg"),JSONObject.toJSON(param));
            if (sendRtpItem.isOnlyAudio()) {
                // TODO 可能是语音对讲
            }else {
                // 向上级平台
                try {
                    commanderForPlatform.streamByeCmd(parentPlatform, callIdHeader.getCallId());
                } catch (SipException | InvalidArgumentException | ParseException e) {
                    logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage());
                }
            }
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java
New file
@@ -0,0 +1,162 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl;
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.InviteStreamType;
import com.genersoft.iot.vmp.gb28181.bean.SendRtpItem;
import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction;
import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.service.IDeviceService;
import com.genersoft.iot.vmp.service.IMediaServerService;
import com.genersoft.iot.vmp.service.bean.MessageForPushChannel;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import gov.nist.javax.sip.message.SIPRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sip.*;
import javax.sip.address.SipURI;
import javax.sip.header.CallIdHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.HeaderAddress;
import javax.sip.header.ToHeader;
import javax.sip.message.Response;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
/**
 * SIP命令类型: BYE请求
 */
@Component
public class ByeRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor {
    private final Logger logger = LoggerFactory.getLogger(ByeRequestProcessor.class);
    private final String method = "BYE";
    @Autowired
    private ISIPCommander cmder;
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Autowired
    private IDeviceService deviceService;
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private ZLMRTPServerFactory zlmrtpServerFactory;
    @Autowired
    private IMediaServerService mediaServerService;
    @Autowired
    private SIPProcessorObserver sipProcessorObserver;
    @Autowired
    private VideoStreamSessionManager streamSession;
    @Override
    public void afterPropertiesSet() throws Exception {
        // 添加消息处理的订阅
        sipProcessorObserver.addRequestProcessor(method, this);
    }
    /**
     * 处理BYE请求
     * @param evt
     */
    @Override
    public void process(RequestEvent evt) {
        try {
            responseAck((SIPRequest) evt.getRequest(), Response.OK);
        } catch (SipException | InvalidArgumentException | ParseException e) {
            logger.error("[回复BYE信息失败],{}", e.getMessage());
        }
        CallIdHeader callIdHeader = (CallIdHeader)evt.getRequest().getHeader(CallIdHeader.NAME);
            String platformGbId = ((SipURI) ((HeaderAddress) evt.getRequest().getHeader(FromHeader.NAME)).getAddress().getURI()).getUser();
            String channelId = ((SipURI) ((HeaderAddress) evt.getRequest().getHeader(ToHeader.NAME)).getAddress().getURI()).getUser();
            SendRtpItem sendRtpItem =  redisCatchStorage.querySendRTPServer(platformGbId, channelId, null, callIdHeader.getCallId());
            logger.info("[收到bye] {}/{}", platformGbId, channelId);
            if (sendRtpItem != null){
                String streamId = sendRtpItem.getStreamId();
                Map<String, Object> param = new HashMap<>();
                param.put("vhost","__defaultVhost__");
                param.put("app",sendRtpItem.getApp());
                param.put("stream",streamId);
                param.put("ssrc",sendRtpItem.getSsrc());
                logger.info("[收到bye] 停止向上级推流:{}", streamId);
                MediaServerItem mediaInfo = mediaServerService.getOne(sendRtpItem.getMediaServerId());
                redisCatchStorage.deleteSendRTPServer(platformGbId, channelId, callIdHeader.getCallId(), null);
                zlmrtpServerFactory.stopSendRtpStream(mediaInfo, param);
                int totalReaderCount = zlmrtpServerFactory.totalReaderCount(mediaInfo, sendRtpItem.getApp(), streamId);
                if (totalReaderCount <= 0) {
                    logger.info("[收到bye] {} 无其它观看者,通知设备停止推流", streamId);
                    if (sendRtpItem.getPlayType().equals(InviteStreamType.PLAY)) {
                        Device device = deviceService.getDevice(sendRtpItem.getDeviceId());
                        if (device == null) {
                            logger.info("[收到bye] {} 通知设备停止推流时未找到设备信息", streamId);
                        }
                        try {
                            logger.warn("[停止点播] {}/{}", sendRtpItem.getDeviceId(), channelId);
                            cmder.streamByeCmd(device, channelId, streamId, null);
                        } catch (InvalidArgumentException | ParseException | SipException |
                                 SsrcTransactionNotFoundException e) {
                            logger.error("[收到bye] {} 无其它观看者,通知设备停止推流, 发送BYE失败 {}",streamId, e.getMessage());
                        }
                    }
                    if (sendRtpItem.getPlayType().equals(InviteStreamType.PUSH)) {
                        MessageForPushChannel messageForPushChannel = MessageForPushChannel.getInstance(0,
                                sendRtpItem.getApp(), sendRtpItem.getStreamId(), sendRtpItem.getChannelId(),
                                sendRtpItem.getPlatformId(), null, null, sendRtpItem.getMediaServerId());
                        redisCatchStorage.sendStreamPushRequestedMsg(messageForPushChannel);
                    }
                }
            }
            // 可能是设备主动停止
            Device device = storager.queryVideoDeviceByChannelId(platformGbId);
            if (device != null) {
                storager.stopPlay(device.getDeviceId(), channelId);
                StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(device.getDeviceId(), channelId);
                if (streamInfo != null) {
                    redisCatchStorage.stopPlay(streamInfo);
                    mediaServerService.closeRTPServer(streamInfo.getMediaServerId(), streamInfo.getStream());
                }
                SsrcTransaction ssrcTransactionForPlay = streamSession.getSsrcTransaction(device.getDeviceId(), channelId, "play", null);
                if (ssrcTransactionForPlay != null){
                    if (ssrcTransactionForPlay.getCallId().equals(callIdHeader.getCallId())){
                        // 释放ssrc
                        MediaServerItem mediaServerItem = mediaServerService.getOne(ssrcTransactionForPlay.getMediaServerId());
                        if (mediaServerItem != null) {
                            mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcTransactionForPlay.getSsrc());
                        }
                        streamSession.remove(device.getDeviceId(), channelId, ssrcTransactionForPlay.getStream());
                    }
                }
                SsrcTransaction ssrcTransactionForPlayBack = streamSession.getSsrcTransaction(device.getDeviceId(), channelId, callIdHeader.getCallId(), null);
                if (ssrcTransactionForPlayBack != null) {
                    // 释放ssrc
                    MediaServerItem mediaServerItem = mediaServerService.getOne(ssrcTransactionForPlayBack.getMediaServerId());
                    if (mediaServerItem != null) {
                        mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcTransactionForPlayBack.getSsrc());
                    }
                    streamSession.remove(device.getDeviceId(), channelId, ssrcTransactionForPlayBack.getStream());
                }
            }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/CancelRequestProcessor.java
New file
@@ -0,0 +1,40 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl;
import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sip.RequestEvent;
/**
 * SIP命令类型: CANCEL请求
 */
@Component
public class CancelRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor {
    private final String method = "CANCEL";
    @Autowired
    private SIPProcessorObserver sipProcessorObserver;
    @Override
    public void afterPropertiesSet() throws Exception {
        // 添加消息处理的订阅
        sipProcessorObserver.addRequestProcessor(method, this);
    }
    /**
     * 处理CANCEL请求
     *
     * @param evt 事件
     */
    @Override
    public void process(RequestEvent evt) {
        // TODO 优先级99 Cancel Request消息实现,此消息一般为级联消息,上级给下级发送请求取消指令
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
New file
@@ -0,0 +1,932 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl;
import com.alibaba.fastjson.JSONObject;
import com.genersoft.iot.vmp.conf.DynamicTask;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver;
import com.genersoft.iot.vmp.gb28181.transmit.SIPSender;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
import com.genersoft.iot.vmp.media.zlm.ZLMMediaListManager;
import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory;
import com.genersoft.iot.vmp.media.zlm.dto.*;
import com.genersoft.iot.vmp.service.IMediaServerService;
import com.genersoft.iot.vmp.service.IPlayService;
import com.genersoft.iot.vmp.service.IStreamProxyService;
import com.genersoft.iot.vmp.service.IStreamPushService;
import com.genersoft.iot.vmp.service.bean.MessageForPushChannel;
import com.genersoft.iot.vmp.service.bean.SSRCInfo;
import com.genersoft.iot.vmp.service.redisMsg.RedisGbPlayMsgListener;
import com.genersoft.iot.vmp.service.redisMsg.RedisPushStreamResponseListener;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import com.genersoft.iot.vmp.utils.DateUtil;
import gov.nist.javax.sdp.TimeDescriptionImpl;
import gov.nist.javax.sdp.fields.TimeField;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sdp.*;
import javax.sip.*;
import javax.sip.header.CallIdHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.text.ParseException;
import java.time.Instant;
import java.util.Vector;
/**
 * SIP命令类型: INVITE请求
 */
@SuppressWarnings("rawtypes")
@Component
public class InviteRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor {
    private final static Logger logger = LoggerFactory.getLogger(InviteRequestProcessor.class);
    private final String method = "INVITE";
    @Autowired
    private SIPCommanderFroPlatform cmderFroPlatform;
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private IStreamPushService streamPushService;
    @Autowired
    private IStreamProxyService streamProxyService;
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Autowired
    private DynamicTask dynamicTask;
    @Autowired
    private RedisPushStreamResponseListener redisPushStreamResponseListener;
    @Autowired
    private IPlayService playService;
    @Autowired
    private SIPSender sipSender;
    @Autowired
    private ZLMRTPServerFactory zlmrtpServerFactory;
    @Autowired
    private IMediaServerService mediaServerService;
    @Autowired
    private ZlmHttpHookSubscribe zlmHttpHookSubscribe;
    @Autowired
    private SIPProcessorObserver sipProcessorObserver;
    @Autowired
    private VideoStreamSessionManager sessionManager;
    @Autowired
    private UserSetting userSetting;
    @Autowired
    private ZLMMediaListManager mediaListManager;
    @Autowired
    private RedisGbPlayMsgListener redisGbPlayMsgListener;
    @Override
    public void afterPropertiesSet() throws Exception {
        // 添加消息处理的订阅
        sipProcessorObserver.addRequestProcessor(method, this);
    }
    /**
     * 处理invite请求
     *
     * @param evt 请求消息
     */
    @Override
    public void process(RequestEvent evt) {
        //  Invite Request消息实现,此消息一般为级联消息,上级给下级发送请求视频指令
        try {
            SIPRequest request = (SIPRequest)evt.getRequest();
            String channelId = SipUtils.getChannelIdFromRequest(request);
            String requesterId = SipUtils.getUserIdFromFromHeader(request);
            CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME);
            if (requesterId == null || channelId == null) {
                logger.info("无法从FromHeader的Address中获取到平台id,返回400");
                // 参数不全, 发400,请求错误
                try {
                    responseAck(request, Response.BAD_REQUEST);
                } catch (SipException | InvalidArgumentException | ParseException e) {
                    logger.error("[命令发送失败] invite BAD_REQUEST: {}", e.getMessage());
                }
                return;
            }
            // 查询请求是否来自上级平台\设备
            ParentPlatform platform = storager.queryParentPlatByServerGBId(requesterId);
            if (platform == null) {
                inviteFromDeviceHandle(request, requesterId);
            } else {
                // 查询平台下是否有该通道
                DeviceChannel channel = storager.queryChannelInParentPlatform(requesterId, channelId);
                GbStream gbStream = storager.queryStreamInParentPlatform(requesterId, channelId);
                PlatformCatalog catalog = storager.getCatalog(channelId);
                MediaServerItem mediaServerItem = null;
                StreamPushItem streamPushItem = null;
                StreamProxyItem proxyByAppAndStream =null;
                // 不是通道可能是直播流
                if (channel != null && gbStream == null) {
//                    if (channel.getStatus() == 0) {
//                        logger.info("通道离线,返回400");
//                        responseAck(request, Response.BAD_REQUEST, "channel [" + channel.getChannelId() + "] offline");
//                        return;
//                    }
                    // 通道存在,发100,TRYING
                    try {
                        responseAck(request, Response.TRYING);
                    } catch (SipException | InvalidArgumentException | ParseException e) {
                        logger.error("[命令发送失败] invite TRYING: {}", e.getMessage());
                    }
                } else if (channel == null && gbStream != null) {
                    String mediaServerId = gbStream.getMediaServerId();
                    mediaServerItem = mediaServerService.getOne(mediaServerId);
                    if (mediaServerItem == null) {
                        if ("proxy".equals(gbStream.getStreamType())) {
                            logger.info("[ app={}, stream={} ]找不到zlm {},返回410", gbStream.getApp(), gbStream.getStream(), mediaServerId);
                            try {
                                responseAck(request, Response.GONE);
                            } catch (SipException | InvalidArgumentException | ParseException e) {
                                logger.error("[命令发送失败] invite GONE: {}", e.getMessage());
                            }
                            return;
                        } else {
                            streamPushItem = streamPushService.getPush(gbStream.getApp(), gbStream.getStream());
                            if (streamPushItem == null || streamPushItem.getServerId().equals(userSetting.getServerId())) {
                                logger.info("[ app={}, stream={} ]找不到zlm {},返回410", gbStream.getApp(), gbStream.getStream(), mediaServerId);
                                try {
                                    responseAck(request, Response.GONE);
                                } catch (SipException | InvalidArgumentException | ParseException e) {
                                    logger.error("[命令发送失败] invite GONE: {}", e.getMessage());
                                }
                                return;
                            }
                        }
                    } else {
                        if ("push".equals(gbStream.getStreamType())) {
                            streamPushItem = streamPushService.getPush(gbStream.getApp(), gbStream.getStream());
                            if (streamPushItem == null) {
                                logger.info("[ app={}, stream={} ]找不到zlm {},返回410", gbStream.getApp(), gbStream.getStream(), mediaServerId);
                                try {
                                    responseAck(request, Response.GONE);
                                } catch (SipException | InvalidArgumentException | ParseException e) {
                                    logger.error("[命令发送失败] invite GONE: {}", e.getMessage());
                                }
                                return;
                            }
                        }else if("proxy".equals(gbStream.getStreamType())){
                            proxyByAppAndStream = streamProxyService.getStreamProxyByAppAndStream(gbStream.getApp(), gbStream.getStream());
                            if (proxyByAppAndStream == null) {
                                logger.info("[ app={}, stream={} ]找不到zlm {},返回410", gbStream.getApp(), gbStream.getStream(), mediaServerId);
                                try {
                                    responseAck(request, Response.GONE);
                                } catch (SipException | InvalidArgumentException | ParseException e) {
                                    logger.error("[命令发送失败] invite GONE: {}", e.getMessage());
                                }
                                return;
                            }
                        }
                    }
                    try {
                        responseAck(request, Response.CALL_IS_BEING_FORWARDED);
                    } catch (SipException | InvalidArgumentException | ParseException e) {
                        logger.error("[命令发送失败] invite CALL_IS_BEING_FORWARDED: {}", e.getMessage());
                    }
                } else if (catalog != null) {
                    try {
                        // 目录不支持点播
                        responseAck(request, Response.BAD_REQUEST, "catalog channel can not play");
                    } catch (SipException | InvalidArgumentException | ParseException e) {
                        logger.error("[命令发送失败] invite 目录不支持点播: {}", e.getMessage());
                    }
                    return;
                } else {
                    logger.info("通道不存在,返回404");
                    try {
                        // 通道不存在,发404,资源不存在
                        responseAck(request, Response.NOT_FOUND);
                    } catch (SipException | InvalidArgumentException | ParseException e) {
                        logger.error("[命令发送失败] invite 通道不存在: {}", e.getMessage());
                    }
                    return;
                }
                // 解析sdp消息, 使用jainsip 自带的sdp解析方式
                String contentString = new String(request.getRawContent());
                // jainSip不支持y=字段, 移除以解析。
                int ssrcIndex = contentString.indexOf("y=");
                // 检查是否有y字段
                String ssrcDefault = "0000000000";
                String ssrc;
                SessionDescription sdp;
                if (ssrcIndex >= 0) {
                    //ssrc规定长度为10个字节,不取余下长度以避免后续还有“f=”字段
                    ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
                    String substring = contentString.substring(0, contentString.indexOf("y="));
                    sdp = SdpFactory.getInstance().createSessionDescription(substring);
                } else {
                    ssrc = ssrcDefault;
                    sdp = SdpFactory.getInstance().createSessionDescription(contentString);
                }
                String sessionName = sdp.getSessionName().getValue();
                Long startTime = null;
                Long stopTime = null;
                Instant start = null;
                Instant end = null;
                if (sdp.getTimeDescriptions(false) != null && sdp.getTimeDescriptions(false).size() > 0) {
                    TimeDescriptionImpl timeDescription = (TimeDescriptionImpl) (sdp.getTimeDescriptions(false).get(0));
                    TimeField startTimeFiled = (TimeField) timeDescription.getTime();
                    startTime = startTimeFiled.getStartTime();
                    stopTime = startTimeFiled.getStopTime();
                    start = Instant.ofEpochSecond(startTime);
                    end = Instant.ofEpochSecond(stopTime);
                }
                //  获取支持的格式
                Vector mediaDescriptions = sdp.getMediaDescriptions(true);
                // 查看是否支持PS 负载96
                //String ip = null;
                int port = -1;
                boolean mediaTransmissionTCP = false;
                Boolean tcpActive = null;
                for (Object description : mediaDescriptions) {
                    MediaDescription mediaDescription = (MediaDescription) description;
                    Media media = mediaDescription.getMedia();
                    Vector mediaFormats = media.getMediaFormats(false);
                    if (mediaFormats.contains("96")) {
                        port = media.getMediaPort();
                        //String mediaType = media.getMediaType();
                        String protocol = media.getProtocol();
                        // 区分TCP发流还是udp, 当前默认udp
                        if ("TCP/RTP/AVP".equalsIgnoreCase(protocol)) {
                            String setup = mediaDescription.getAttribute("setup");
                            if (setup != null) {
                                mediaTransmissionTCP = true;
                                if ("active".equalsIgnoreCase(setup)) {
                                    tcpActive = true;
                                } else if ("passive".equalsIgnoreCase(setup)) {
                                    tcpActive = false;
                                }
                            }
                        }
                        break;
                    }
                }
                if (port == -1) {
                    logger.info("不支持的媒体格式,返回415");
                    // 回复不支持的格式
                    try {
                        // 不支持的格式,发415
                        responseAck(request, Response.UNSUPPORTED_MEDIA_TYPE);
                    } catch (SipException | InvalidArgumentException | ParseException e) {
                        logger.error("[命令发送失败] invite 不支持的格式: {}", e.getMessage());
                    }
                    return;
                }
                String username = sdp.getOrigin().getUsername();
                String addressStr = sdp.getOrigin().getAddress();
                logger.info("[上级点播]用户:{}, 通道:{}, 地址:{}:{}, ssrc:{}", username, channelId, addressStr, port, ssrc);
                Device device = null;
                // 通过 channel 和 gbStream 是否为null 值判断来源是直播流合适国标
                if (channel != null) {
                    device = storager.queryVideoDeviceByPlatformIdAndChannelId(requesterId, channelId);
                    if (device == null) {
                        logger.warn("点播平台{}的通道{}时未找到设备信息", requesterId, channel);
                        try {
                            responseAck(request, Response.SERVER_INTERNAL_ERROR);
                        } catch (SipException | InvalidArgumentException | ParseException e) {
                            logger.error("[命令发送失败] invite 未找到设备信息: {}", e.getMessage());
                        }
                        return;
                    }
                    mediaServerItem = playService.getNewMediaServerItem(device);
                    if (mediaServerItem == null) {
                        logger.warn("未找到可用的zlm");
                        try {
                            responseAck(request, Response.BUSY_HERE);
                        } catch (SipException | InvalidArgumentException | ParseException e) {
                            logger.error("[命令发送失败] invite BUSY_HERE: {}", e.getMessage());
                        }
                        return;
                    }
                    SendRtpItem sendRtpItem = zlmrtpServerFactory.createSendRtpItem(mediaServerItem, addressStr, port, ssrc, requesterId,
                            device.getDeviceId(), channelId,
                            mediaTransmissionTCP);
                    if (tcpActive != null) {
                        sendRtpItem.setTcpActive(tcpActive);
                    }
                    if (sendRtpItem == null) {
                        logger.warn("服务器端口资源不足");
                        try {
                            responseAck(request, Response.BUSY_HERE);
                        } catch (SipException | InvalidArgumentException | ParseException e) {
                            logger.error("[命令发送失败] invite 服务器端口资源不足: {}", e.getMessage());
                        }
                        return;
                    }
                    sendRtpItem.setCallId(callIdHeader.getCallId());
                    sendRtpItem.setPlayType("Play".equalsIgnoreCase(sessionName) ? InviteStreamType.PLAY : InviteStreamType.PLAYBACK);
                    Long finalStartTime = startTime;
                    Long finalStopTime = stopTime;
                    ZlmHttpHookSubscribe.Event hookEvent = (mediaServerItemInUSe, responseJSON) -> {
                        String app = responseJSON.getString("app");
                        String stream = responseJSON.getString("stream");
                        logger.info("[上级点播]下级已经开始推流。 回复200OK(SDP), {}/{}", app, stream);
                        //     * 0 等待设备推流上来
                        //     * 1 下级已经推流,等待上级平台回复ack
                        //     * 2 推流中
                        sendRtpItem.setStatus(1);
                        redisCatchStorage.updateSendRTPSever(sendRtpItem);
                        StringBuffer content = new StringBuffer(200);
                        content.append("v=0\r\n");
                        content.append("o=" + channelId + " 0 0 IN IP4 " + mediaServerItemInUSe.getSdpIp() + "\r\n");
                        content.append("s=" + sessionName + "\r\n");
                        content.append("c=IN IP4 " + mediaServerItemInUSe.getSdpIp() + "\r\n");
                        if ("Playback".equalsIgnoreCase(sessionName)) {
                            content.append("t=" + finalStartTime + " " + finalStopTime + "\r\n");
                        } else {
                            content.append("t=0 0\r\n");
                        }
                        content.append("m=video " + sendRtpItem.getLocalPort() + " RTP/AVP 96\r\n");
                        content.append("a=sendonly\r\n");
                        content.append("a=rtpmap:96 PS/90000\r\n");
                        content.append("y=" + sendRtpItem.getSsrc() + "\r\n");
                        content.append("f=\r\n");
                        try {
                            // 超时未收到Ack应该回复bye,当前等待时间为10秒
                            dynamicTask.startDelay(callIdHeader.getCallId(), () -> {
                                logger.info("Ack 等待超时");
                                mediaServerService.releaseSsrc(mediaServerItemInUSe.getId(), sendRtpItem.getSsrc());
                                // 回复bye
                                try {
                                    cmderFroPlatform.streamByeCmd(platform, callIdHeader.getCallId());
                                } catch (SipException | InvalidArgumentException | ParseException e) {
                                    logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage());
                                }
                            }, 60 * 1000);
                            responseSdpAck(request, content.toString(), platform);
                        } catch (SipException e) {
                            e.printStackTrace();
                        } catch (InvalidArgumentException e) {
                            e.printStackTrace();
                        } catch (ParseException e) {
                            e.printStackTrace();
                        }
                    };
                    SipSubscribe.Event errorEvent = ((event) -> {
                        // 未知错误。直接转发设备点播的错误
                        try {
                            Response response = getMessageFactory().createResponse(event.statusCode, evt.getRequest());
                            sipSender.transmitRequest(response);
                        } catch (ParseException | SipException  e) {
                            e.printStackTrace();
                        }
                    });
                    sendRtpItem.setApp("rtp");
                    if ("Playback".equalsIgnoreCase(sessionName)) {
                        sendRtpItem.setPlayType(InviteStreamType.PLAYBACK);
                        SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, null, device.isSsrcCheck(), true);
                        sendRtpItem.setStreamId(ssrcInfo.getStream());
                        // 写入redis, 超时时回复
                        redisCatchStorage.updateSendRTPSever(sendRtpItem);
                        playService.playBack(mediaServerItem, ssrcInfo, device.getDeviceId(), channelId, DateUtil.formatter.format(start),
                                DateUtil.formatter.format(end), null, result -> {
                                    if (result.getCode() != 0) {
                                        logger.warn("录像回放失败");
                                        if (result.getEvent() != null) {
                                            errorEvent.response(result.getEvent());
                                        }
                                        redisCatchStorage.deleteSendRTPServer(platform.getServerGBId(), channelId, callIdHeader.getCallId(), null);
                                        try {
                                            responseAck(request, Response.REQUEST_TIMEOUT);
                                        } catch (SipException | InvalidArgumentException | ParseException e) {
                                            logger.error("[命令发送失败] 国标级联 录像回放 发送REQUEST_TIMEOUT: {}", e.getMessage());
                                        }
                                    } else {
                                        if (result.getMediaServerItem() != null) {
                                            hookEvent.response(result.getMediaServerItem(), result.getResponse());
                                        }
                                    }
                                });
                    } else {
                        sendRtpItem.setPlayType(InviteStreamType.PLAY);
                        SsrcTransaction playTransaction = sessionManager.getSsrcTransaction(device.getDeviceId(), channelId, "play", null);
                        if (playTransaction != null) {
                            Boolean streamReady = zlmrtpServerFactory.isStreamReady(mediaServerItem, "rtp", playTransaction.getStream());
                            if (!streamReady) {
                                boolean hasRtpServer = mediaServerService.checkRtpServer(mediaServerItem, "rtp", playTransaction.getStream());
                                if (hasRtpServer) {
                                    logger.info("[上级点播]已经开启rtpServer但是尚未收到流,开启监听流的到来");
                                    HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", playTransaction.getStream(), true, "rtsp", mediaServerItem.getId());
                                    zlmHttpHookSubscribe.addSubscribe(hookSubscribe, hookEvent);
                                }else {
                                    playTransaction = null;
                                }
                            }
                        }
                        if (playTransaction == null) {
                            String streamId = null;
                            if (mediaServerItem.isRtpEnable()) {
                                streamId = String.format("%s_%s", device.getDeviceId(), channelId);
                            }
                            SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, null, device.isSsrcCheck(), false);
                            logger.info(JSONObject.toJSONString(ssrcInfo));
                            sendRtpItem.setStreamId(ssrcInfo.getStream());
                            sendRtpItem.setSsrc(ssrc.equals(ssrcDefault) ? ssrcInfo.getSsrc() : ssrc);
                            // 写入redis, 超时时回复
                            redisCatchStorage.updateSendRTPSever(sendRtpItem);
                            playService.play(mediaServerItem, ssrcInfo, device, channelId, hookEvent, errorEvent, (code, msg) -> {
                                logger.info("[上级点播]超时, 用户:{}, 通道:{}", username, channelId);
                                redisCatchStorage.deleteSendRTPServer(platform.getServerGBId(), channelId, callIdHeader.getCallId(), null);
                            }, null);
                        } else {
                            sendRtpItem.setStreamId(playTransaction.getStream());
                            // 写入redis, 超时时回复
                            redisCatchStorage.updateSendRTPSever(sendRtpItem);
                            JSONObject jsonObject = new JSONObject();
                            jsonObject.put("app", sendRtpItem.getApp());
                            jsonObject.put("stream", sendRtpItem.getStreamId());
                            hookEvent.response(mediaServerItem, jsonObject);
                        }
                    }
                } else if (gbStream != null) {
                    if("push".equals(gbStream.getStreamType())) {
                        if (streamPushItem != null && streamPushItem.isPushIng()) {
                            // 推流状态
                            pushStream(evt, request, gbStream, streamPushItem, platform, callIdHeader, mediaServerItem, port, tcpActive,
                                    mediaTransmissionTCP, channelId, addressStr, ssrc, requesterId);
                        } else {
                            // 未推流 拉起
                            notifyStreamOnline(evt, request,gbStream, streamPushItem, platform, callIdHeader, mediaServerItem, port, tcpActive,
                                    mediaTransmissionTCP, channelId, addressStr, ssrc, requesterId);
                        }
                    }else if ("proxy".equals(gbStream.getStreamType())){
                        if (null != proxyByAppAndStream) {
                            if(proxyByAppAndStream.isStatus()){
                                pushProxyStream(evt, request, gbStream,  platform, callIdHeader, mediaServerItem, port, tcpActive,
                                        mediaTransmissionTCP, channelId, addressStr, ssrc, requesterId);
                            }else{
                                //开启代理拉流
                                notifyStreamOnline(evt, request,gbStream, null, platform, callIdHeader, mediaServerItem, port, tcpActive,
                                        mediaTransmissionTCP, channelId, addressStr, ssrc, requesterId);
                            }
                        }
                    }
                }
            }
        } catch (SdpParseException e) {
            logger.error("sdp解析错误", e);
        } catch (SdpException e) {
            e.printStackTrace();
        }
    }
    /**
     * 安排推流
     */
    private void pushProxyStream(RequestEvent evt, SIPRequest request, GbStream gbStream, ParentPlatform platform,
                            CallIdHeader callIdHeader, MediaServerItem mediaServerItem,
                            int port, Boolean tcpActive, boolean mediaTransmissionTCP,
                            String channelId, String addressStr, String ssrc, String requesterId) {
            Boolean streamReady = zlmrtpServerFactory.isStreamReady(mediaServerItem, gbStream.getApp(), gbStream.getStream());
            if (streamReady) {
                // 自平台内容
                SendRtpItem sendRtpItem = zlmrtpServerFactory.createSendRtpItem(mediaServerItem, addressStr, port, ssrc, requesterId,
                        gbStream.getApp(), gbStream.getStream(), channelId,
                        mediaTransmissionTCP);
                if (sendRtpItem == null) {
                    logger.warn("服务器端口资源不足");
                    try {
                        responseAck(request, Response.BUSY_HERE);
                    } catch (SipException | InvalidArgumentException | ParseException e) {
                        logger.error("[命令发送失败] invite 服务器端口资源不足: {}", e.getMessage());
                    }
                    return;
                }
                if (tcpActive != null) {
                    sendRtpItem.setTcpActive(tcpActive);
                }
                sendRtpItem.setPlayType(InviteStreamType.PUSH);
                // 写入redis, 超时时回复
                sendRtpItem.setStatus(1);
                sendRtpItem.setCallId(callIdHeader.getCallId());
                sendRtpItem.setFromTag(request.getFromTag());
                SIPResponse response = sendStreamAck(mediaServerItem, request, sendRtpItem, platform, evt);
                if (response != null) {
                    sendRtpItem.setToTag(response.getToTag());
                }
                redisCatchStorage.updateSendRTPSever(sendRtpItem);
        }
    }
    private void pushStream(RequestEvent evt, SIPRequest request, GbStream gbStream, StreamPushItem streamPushItem, ParentPlatform platform,
                            CallIdHeader callIdHeader, MediaServerItem mediaServerItem,
                            int port, Boolean tcpActive, boolean mediaTransmissionTCP,
                            String channelId, String addressStr, String ssrc, String requesterId) {
        // 推流
        if (streamPushItem.isSelf()) {
            Boolean streamReady = zlmrtpServerFactory.isStreamReady(mediaServerItem, gbStream.getApp(), gbStream.getStream());
            if (streamReady) {
                // 自平台内容
                SendRtpItem sendRtpItem = zlmrtpServerFactory.createSendRtpItem(mediaServerItem, addressStr, port, ssrc, requesterId,
                        gbStream.getApp(), gbStream.getStream(), channelId,
                        mediaTransmissionTCP);
                if (sendRtpItem == null) {
                    logger.warn("服务器端口资源不足");
                    try {
                        responseAck(request, Response.BUSY_HERE);
                    } catch (SipException | InvalidArgumentException | ParseException e) {
                        logger.error("[命令发送失败] invite 服务器端口资源不足: {}", e.getMessage());
                    }
                    return;
                }
                if (tcpActive != null) {
                    sendRtpItem.setTcpActive(tcpActive);
                }
                sendRtpItem.setPlayType(InviteStreamType.PUSH);
                // 写入redis, 超时时回复
                sendRtpItem.setStatus(1);
                sendRtpItem.setCallId(callIdHeader.getCallId());
                sendRtpItem.setFromTag(request.getFromTag());
                SIPResponse response = sendStreamAck(mediaServerItem, request, sendRtpItem, platform, evt);
                if (response != null) {
                    sendRtpItem.setToTag(response.getToTag());
                }
                redisCatchStorage.updateSendRTPSever(sendRtpItem);
            } else {
                // 不在线 拉起
                notifyStreamOnline(evt, request,gbStream, streamPushItem, platform, callIdHeader, mediaServerItem, port, tcpActive,
                        mediaTransmissionTCP, channelId, addressStr, ssrc, requesterId);
            }
        } else {
            // 其他平台内容
            otherWvpPushStream(evt, request, gbStream, streamPushItem, platform, callIdHeader, mediaServerItem, port, tcpActive,
                    mediaTransmissionTCP, channelId, addressStr, ssrc, requesterId);
        }
    }
    /**
     * 通知流上线
     */
    private void notifyStreamOnline(RequestEvent evt, SIPRequest request, GbStream gbStream, StreamPushItem streamPushItem, ParentPlatform platform,
                                    CallIdHeader callIdHeader, MediaServerItem mediaServerItem,
                                    int port, Boolean tcpActive, boolean mediaTransmissionTCP,
                                    String channelId, String addressStr, String ssrc, String requesterId) {
        if ("proxy".equals(gbStream.getStreamType())) {
            // TODO 控制启用以使设备上线
            logger.info("[ app={}, stream={} ]通道未推流,启用流后开始推流", gbStream.getApp(), gbStream.getStream());
            // 监听流上线
            HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed(gbStream.getApp(), gbStream.getStream(), true, "rtsp", mediaServerItem.getId());
            zlmHttpHookSubscribe.addSubscribe(hookSubscribe, (mediaServerItemInUSe, responseJSON) -> {
                String app = responseJSON.getString("app");
                String stream = responseJSON.getString("stream");
                logger.info("[上级点播]拉流代理已经就绪, {}/{}", app, stream);
                dynamicTask.stop(callIdHeader.getCallId());
                pushProxyStream(evt, request, gbStream,  platform, callIdHeader, mediaServerItem, port, tcpActive,
                        mediaTransmissionTCP, channelId, addressStr, ssrc, requesterId);
            });
            dynamicTask.startDelay(callIdHeader.getCallId(), () -> {
                logger.info("[ app={}, stream={} ] 等待拉流代理流超时", gbStream.getApp(), gbStream.getStream());
                zlmHttpHookSubscribe.removeSubscribe(hookSubscribe);
            }, userSetting.getPlatformPlayTimeout());
            boolean start = streamProxyService.start(gbStream.getApp(), gbStream.getStream());
            if (!start) {
                try {
                    responseAck(request, Response.BUSY_HERE, "channel [" + gbStream.getGbId() + "] offline");
                } catch (SipException | InvalidArgumentException | ParseException e) {
                    logger.error("[命令发送失败] invite 通道未推流: {}", e.getMessage());
                }
                zlmHttpHookSubscribe.removeSubscribe(hookSubscribe);
                dynamicTask.stop(callIdHeader.getCallId());
            }
        } else if ("push".equals(gbStream.getStreamType())) {
            if (!platform.isStartOfflinePush()) {
                // 平台设置中关闭了拉起离线的推流则直接回复
                try {
                    responseAck(request, Response.TEMPORARILY_UNAVAILABLE, "channel stream not pushing");
                } catch (SipException | InvalidArgumentException | ParseException e) {
                    logger.error("[命令发送失败] invite 通道未推流: {}", e.getMessage());
                }
                return;
            }
            // 发送redis消息以使设备上线
            logger.info("[ app={}, stream={} ]通道未推流,发送redis信息控制设备开始推流", gbStream.getApp(), gbStream.getStream());
            MessageForPushChannel messageForPushChannel = MessageForPushChannel.getInstance(1,
                    gbStream.getApp(), gbStream.getStream(), gbStream.getGbId(), gbStream.getPlatformId(),
                    platform.getName(), null, gbStream.getMediaServerId());
            redisCatchStorage.sendStreamPushRequestedMsg(messageForPushChannel);
            // 设置超时
            dynamicTask.startDelay(callIdHeader.getCallId(), () -> {
                logger.info("[ app={}, stream={} ] 等待设备开始推流超时", gbStream.getApp(), gbStream.getStream());
                try {
                    mediaListManager.removedChannelOnlineEventLister(gbStream.getApp(), gbStream.getStream());
                    responseAck(request, Response.REQUEST_TIMEOUT); // 超时
                } catch (SipException e) {
                    e.printStackTrace();
                } catch (InvalidArgumentException e) {
                    e.printStackTrace();
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }, userSetting.getPlatformPlayTimeout());
            // 添加监听
            int finalPort = port;
            Boolean finalTcpActive = tcpActive;
            // 添加在本机上线的通知
            mediaListManager.addChannelOnlineEventLister(gbStream.getApp(), gbStream.getStream(), (app, stream, serverId) -> {
                dynamicTask.stop(callIdHeader.getCallId());
                if (serverId.equals(userSetting.getServerId())) {
                    SendRtpItem sendRtpItem = zlmrtpServerFactory.createSendRtpItem(mediaServerItem, addressStr, finalPort, ssrc, requesterId,
                            app, stream, channelId, mediaTransmissionTCP);
                    if (sendRtpItem == null) {
                        logger.warn("上级点时创建sendRTPItem失败,可能是服务器端口资源不足");
                        try {
                            responseAck(request, Response.BUSY_HERE);
                        } catch (SipException e) {
                            e.printStackTrace();
                        } catch (InvalidArgumentException e) {
                            e.printStackTrace();
                        } catch (ParseException e) {
                            e.printStackTrace();
                        }
                        return;
                    }
                    if (finalTcpActive != null) {
                        sendRtpItem.setTcpActive(finalTcpActive);
                    }
                    sendRtpItem.setPlayType(InviteStreamType.PUSH);
                    // 写入redis, 超时时回复
                    sendRtpItem.setStatus(1);
                    sendRtpItem.setCallId(callIdHeader.getCallId());
                    sendRtpItem.setFromTag(request.getFromTag());
                    SIPResponse response = sendStreamAck(mediaServerItem, request, sendRtpItem, platform, evt);
                    if (response != null) {
                        sendRtpItem.setToTag(response.getToTag());
                    }
                    redisCatchStorage.updateSendRTPSever(sendRtpItem);
                } else {
                    // 其他平台内容
                    otherWvpPushStream(evt, request, gbStream, streamPushItem, platform, callIdHeader, mediaServerItem, port, tcpActive,
                            mediaTransmissionTCP, channelId, addressStr, ssrc, requesterId);
                }
            });
            // 添加回复的拒绝或者错误的通知
            redisPushStreamResponseListener.addEvent(gbStream.getApp(), gbStream.getStream(), response -> {
                if (response.getCode() != 0) {
                    dynamicTask.stop(callIdHeader.getCallId());
                    mediaListManager.removedChannelOnlineEventLister(gbStream.getApp(), gbStream.getStream());
                    try {
                        responseAck(request, Response.TEMPORARILY_UNAVAILABLE, response.getMsg());
                    } catch (SipException | InvalidArgumentException | ParseException e) {
                        logger.error("[命令发送失败] 国标级联 点播回复: {}", e.getMessage());
                    }
                }
            });
        }
    }
    /**
     * 来自其他wvp的推流
     */
    private void otherWvpPushStream(RequestEvent evt, SIPRequest request, GbStream gbStream, StreamPushItem streamPushItem, ParentPlatform platform,
                                    CallIdHeader callIdHeader, MediaServerItem mediaServerItem,
                                    int port, Boolean tcpActive, boolean mediaTransmissionTCP,
                                    String channelId, String addressStr, String ssrc, String requesterId) {
        logger.info("[级联点播]直播流来自其他平台,发送redis消息");
        // 发送redis消息
        redisGbPlayMsgListener.sendMsg(streamPushItem.getServerId(), streamPushItem.getMediaServerId(),
                streamPushItem.getApp(), streamPushItem.getStream(), addressStr, port, ssrc, requesterId,
                channelId, mediaTransmissionTCP, null, responseSendItemMsg -> {
                    SendRtpItem sendRtpItem = responseSendItemMsg.getSendRtpItem();
                    if (sendRtpItem == null || responseSendItemMsg.getMediaServerItem() == null) {
                        logger.warn("服务器端口资源不足");
                        try {
                            responseAck(request, Response.BUSY_HERE);
                        } catch (SipException e) {
                            e.printStackTrace();
                        } catch (InvalidArgumentException e) {
                            e.printStackTrace();
                        } catch (ParseException e) {
                            e.printStackTrace();
                        }
                        return;
                    }
                    // 收到sendItem
                    if (tcpActive != null) {
                        sendRtpItem.setTcpActive(tcpActive);
                    }
                    sendRtpItem.setPlayType(InviteStreamType.PUSH);
                    // 写入redis, 超时时回复
                    sendRtpItem.setStatus(1);
                    sendRtpItem.setCallId(callIdHeader.getCallId());
                    sendRtpItem.setFromTag(request.getFromTag());
                    SIPResponse response = sendStreamAck(responseSendItemMsg.getMediaServerItem(), request,sendRtpItem, platform, evt);
                    if (response != null) {
                        sendRtpItem.setToTag(response.getToTag());
                    }
                    redisCatchStorage.updateSendRTPSever(sendRtpItem);
                }, (wvpResult) -> {
                    // 错误
                    if (wvpResult.getCode() == RedisGbPlayMsgListener.ERROR_CODE_OFFLINE) {
                        // 离线
                        // 查询是否在本机上线了
                        StreamPushItem currentStreamPushItem = streamPushService.getPush(streamPushItem.getApp(), streamPushItem.getStream());
                        if (currentStreamPushItem.isPushIng()) {
                            // 在线状态
                            pushStream(evt, request, gbStream, streamPushItem, platform, callIdHeader, mediaServerItem, port, tcpActive,
                                    mediaTransmissionTCP, channelId, addressStr, ssrc, requesterId);
                        } else {
                            // 不在线 拉起
                            notifyStreamOnline(evt, request, gbStream, streamPushItem, platform, callIdHeader, mediaServerItem, port, tcpActive,
                                    mediaTransmissionTCP, channelId, addressStr, ssrc, requesterId);
                        }
                    }
                    try {
                        responseAck(request, Response.BUSY_HERE);
                    } catch (InvalidArgumentException | ParseException | SipException e) {
                        logger.error("[命令发送失败] 国标级联 点播回复 BUSY_HERE: {}", e.getMessage());
                    }
                });
    }
    public SIPResponse sendStreamAck(MediaServerItem mediaServerItem, SIPRequest request, SendRtpItem sendRtpItem, ParentPlatform platform, RequestEvent evt) {
        StringBuffer content = new StringBuffer(200);
        content.append("v=0\r\n");
        content.append("o=" + sendRtpItem.getChannelId() + " 0 0 IN IP4 " + mediaServerItem.getSdpIp() + "\r\n");
        content.append("s=Play\r\n");
        content.append("c=IN IP4 " + mediaServerItem.getSdpIp() + "\r\n");
        content.append("t=0 0\r\n");
        content.append("m=video " + sendRtpItem.getLocalPort() + " RTP/AVP 96\r\n");
        content.append("a=sendonly\r\n");
        content.append("a=rtpmap:96 PS/90000\r\n");
        if (sendRtpItem.isTcp()) {
            content.append("a=connection:new\r\n");
            if (!sendRtpItem.isTcpActive()) {
                content.append("a=setup:active\r\n");
            } else {
                content.append("a=setup:passive\r\n");
            }
        }
        content.append("y=" + sendRtpItem.getSsrc() + "\r\n");
        content.append("f=\r\n");
        try {
            return responseSdpAck(request, content.toString(), platform);
        } catch (SipException e) {
            e.printStackTrace();
        } catch (InvalidArgumentException e) {
            e.printStackTrace();
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }
    public void inviteFromDeviceHandle(SIPRequest request, String requesterId) {
        // 非上级平台请求,查询是否设备请求(通常为接收语音广播的设备)
        Device device = redisCatchStorage.getDevice(requesterId);
        if (device != null) {
            logger.info("收到设备" + requesterId + "的语音广播Invite请求");
            try {
                responseAck(request, Response.TRYING);
            } catch (SipException | InvalidArgumentException | ParseException e) {
                logger.error("[命令发送失败] invite BAD_REQUEST: {}", e.getMessage());
            }
            String contentString = new String(request.getRawContent());
            // jainSip不支持y=字段, 移除移除以解析。
            String substring = contentString;
            String ssrc = "0000000404";
            int ssrcIndex = contentString.indexOf("y=");
            if (ssrcIndex > 0) {
                substring = contentString.substring(0, ssrcIndex);
                ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
            }
            ssrcIndex = substring.indexOf("f=");
            if (ssrcIndex > 0) {
                substring = contentString.substring(0, ssrcIndex);
            }
            SessionDescription sdp = null;
            try {
                sdp = SdpFactory.getInstance().createSessionDescription(substring);
                //  获取支持的格式
                Vector mediaDescriptions = sdp.getMediaDescriptions(true);
                // 查看是否支持PS 负载96
                int port = -1;
                //boolean recvonly = false;
                boolean mediaTransmissionTCP = false;
                Boolean tcpActive = null;
                for (int i = 0; i < mediaDescriptions.size(); i++) {
                    MediaDescription mediaDescription = (MediaDescription) mediaDescriptions.get(i);
                    Media media = mediaDescription.getMedia();
                    Vector mediaFormats = media.getMediaFormats(false);
                    if (mediaFormats.contains("8")) {
                        port = media.getMediaPort();
                        String protocol = media.getProtocol();
                        // 区分TCP发流还是udp, 当前默认udp
                        if ("TCP/RTP/AVP".equals(protocol)) {
                            String setup = mediaDescription.getAttribute("setup");
                            if (setup != null) {
                                mediaTransmissionTCP = true;
                                if ("active".equals(setup)) {
                                    tcpActive = true;
                                } else if ("passive".equals(setup)) {
                                    tcpActive = false;
                                }
                            }
                        }
                        break;
                    }
                }
                if (port == -1) {
                    logger.info("不支持的媒体格式,返回415");
                    // 回复不支持的格式
                    try {
                        responseAck(request, Response.UNSUPPORTED_MEDIA_TYPE); // 不支持的格式,发415
                    } catch (SipException | InvalidArgumentException | ParseException e) {
                        logger.error("[命令发送失败] invite 不支持的媒体格式,返回415, {}", e.getMessage());
                    }
                    return;
                }
                String username = sdp.getOrigin().getUsername();
                String addressStr = sdp.getOrigin().getAddress();
                logger.info("设备{}请求语音流,地址:{}:{},ssrc:{}", username, addressStr, port, ssrc);
            } catch (SdpException e) {
                logger.error("[SDP解析异常]", e);
            }
        } else {
            logger.warn("来自无效设备/平台的请求");
            try {
                responseAck(request, Response.BAD_REQUEST);; // 不支持的格式,发415
            } catch (SipException | InvalidArgumentException | ParseException e) {
                logger.error("[命令发送失败] invite 来自无效设备/平台的请求, {}", e.getMessage());
            }
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java
New file
@@ -0,0 +1,460 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl;
import com.alibaba.fastjson.JSONObject;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent;
import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver;
import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.utils.Coordtransform;
import com.genersoft.iot.vmp.gb28181.utils.NumericUtil;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import com.genersoft.iot.vmp.gb28181.utils.XmlUtil;
import com.genersoft.iot.vmp.service.IDeviceChannelService;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import com.genersoft.iot.vmp.utils.DateUtil;
import com.genersoft.iot.vmp.utils.redis.RedisUtil;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.header.FromHeader;
import javax.sip.message.Response;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
 * SIP命令类型: NOTIFY请求,这是作为上级发送订阅请求后,设备才会响应的
 */
@Component
public class NotifyRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor {
    private final static Logger logger = LoggerFactory.getLogger(NotifyRequestProcessor.class);
    @Autowired
    private UserSetting userSetting;
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private EventPublisher eventPublisher;
    @Autowired
    private SipConfig sipConfig;
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Autowired
    private EventPublisher publisher;
    private final String method = "NOTIFY";
    @Autowired
    private SIPProcessorObserver sipProcessorObserver;
    @Autowired
    private IDeviceChannelService deviceChannelService;
    private boolean taskQueueHandlerRun = false;
    private ConcurrentLinkedQueue<HandlerCatchData> taskQueue = new ConcurrentLinkedQueue<>();
    @Qualifier("taskExecutor")
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;
    @Override
    public void afterPropertiesSet() throws Exception {
        // 添加消息处理的订阅
        sipProcessorObserver.addRequestProcessor(method, this);
    }
    @Override
    public void process(RequestEvent evt) {
        try {
            responseAck((SIPRequest) evt.getRequest(), Response.OK, null, null);
        }catch (SipException | InvalidArgumentException | ParseException e) {
            e.printStackTrace();
        }
        taskQueue.offer(new HandlerCatchData(evt, null, null));
        if (!taskQueueHandlerRun) {
            taskQueueHandlerRun = true;
            taskExecutor.execute(()-> {
                while (!taskQueue.isEmpty()) {
                    try {
                        HandlerCatchData take = taskQueue.poll();
                        Element rootElement = getRootElement(take.getEvt());
                        if (rootElement == null) {
                            logger.error("处理NOTIFY消息时未获取到消息体,{}", take.getEvt().getRequest());
                            continue;
                        }
                        String cmd = XmlUtil.getText(rootElement, "CmdType");
                        if (CmdType.CATALOG.equals(cmd)) {
                            logger.info("接收到Catalog通知");
                            processNotifyCatalogList(take.getEvt());
                        } else if (CmdType.ALARM.equals(cmd)) {
                            logger.info("接收到Alarm通知");
                            processNotifyAlarm(take.getEvt());
                        } else if (CmdType.MOBILE_POSITION.equals(cmd)) {
                            logger.info("接收到MobilePosition通知");
                            processNotifyMobilePosition(take.getEvt());
                        } else {
                            logger.info("接收到消息:" + cmd);
                        }
                    } catch (DocumentException e) {
                        logger.error("处理NOTIFY消息时错误", e);
                    }
                }
                taskQueueHandlerRun = false;
            });
        }
    }
    /**
     * 处理MobilePosition移动位置Notify
     *
     * @param evt
     */
    private void processNotifyMobilePosition(RequestEvent evt) {
        try {
            FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME);
            String deviceId = SipUtils.getUserIdFromFromHeader(fromHeader);
            // 回复 200 OK
            Element rootElement = getRootElement(evt);
            if (rootElement == null) {
                logger.error("处理MobilePosition移动位置Notify时未获取到消息体,{}", evt.getRequest());
                return;
            }
            MobilePosition mobilePosition = new MobilePosition();
            mobilePosition.setCreateTime(DateUtil.getNow());
            Element deviceIdElement = rootElement.element("DeviceID");
            String channelId = deviceIdElement.getTextTrim().toString();
            Device device = redisCatchStorage.getDevice(deviceId);
            if (device != null) {
                if (!ObjectUtils.isEmpty(device.getName())) {
                    mobilePosition.setDeviceName(device.getName());
                }
            }
            mobilePosition.setDeviceId(XmlUtil.getText(rootElement, "DeviceID"));
            mobilePosition.setChannelId(channelId);
            String time = XmlUtil.getText(rootElement, "Time");
            mobilePosition.setTime(time);
            mobilePosition.setLongitude(Double.parseDouble(XmlUtil.getText(rootElement, "Longitude")));
            mobilePosition.setLatitude(Double.parseDouble(XmlUtil.getText(rootElement, "Latitude")));
            if (NumericUtil.isDouble(XmlUtil.getText(rootElement, "Speed"))) {
                mobilePosition.setSpeed(Double.parseDouble(XmlUtil.getText(rootElement, "Speed")));
            } else {
                mobilePosition.setSpeed(0.0);
            }
            if (NumericUtil.isDouble(XmlUtil.getText(rootElement, "Direction"))) {
                mobilePosition.setDirection(Double.parseDouble(XmlUtil.getText(rootElement, "Direction")));
            } else {
                mobilePosition.setDirection(0.0);
            }
            if (NumericUtil.isDouble(XmlUtil.getText(rootElement, "Altitude"))) {
                mobilePosition.setAltitude(Double.parseDouble(XmlUtil.getText(rootElement, "Altitude")));
            } else {
                mobilePosition.setAltitude(0.0);
            }
            logger.info("[收到移动位置订阅通知]:{}/{}->{}.{}", mobilePosition.getDeviceId(), mobilePosition.getChannelId(),
                    mobilePosition.getLongitude(), mobilePosition.getLatitude());
            mobilePosition.setReportSource("Mobile Position");
            // 更新device channel 的经纬度
            DeviceChannel deviceChannel = new DeviceChannel();
            deviceChannel.setDeviceId(device.getDeviceId());
            deviceChannel.setChannelId(channelId);
            deviceChannel.setLongitude(mobilePosition.getLongitude());
            deviceChannel.setLatitude(mobilePosition.getLatitude());
            deviceChannel.setGpsTime(mobilePosition.getTime());
            deviceChannel = deviceChannelService.updateGps(deviceChannel, device);
            mobilePosition.setLongitudeWgs84(deviceChannel.getLongitudeWgs84());
            mobilePosition.setLatitudeWgs84(deviceChannel.getLatitudeWgs84());
            mobilePosition.setLongitudeGcj02(deviceChannel.getLongitudeGcj02());
            mobilePosition.setLatitudeGcj02(deviceChannel.getLatitudeGcj02());
            if (userSetting.getSavePositionHistory()) {
                storager.insertMobilePosition(mobilePosition);
            }
            //组装坐标
            String position = mobilePosition.getLongitude() + " " + mobilePosition.getLatitude();
            deviceChannel.setCoordinate("'POINT("+position+")'");
            storager.updateChannelPosition(deviceChannel);
            // 发送redis消息。 通知位置信息的变化
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("time", time);
            jsonObject.put("serial", deviceId);
            jsonObject.put("code", channelId);
            jsonObject.put("longitude", mobilePosition.getLongitude());
            jsonObject.put("latitude", mobilePosition.getLatitude());
            jsonObject.put("altitude", mobilePosition.getAltitude());
            jsonObject.put("direction", mobilePosition.getDirection());
            jsonObject.put("speed", mobilePosition.getSpeed());
            redisCatchStorage.sendMobilePositionMsg(jsonObject);
        } catch (DocumentException  e) {
            e.printStackTrace();
        }
    }
    /***
     * 处理alarm设备报警Notify
     *
     * @param evt
     */
    private void processNotifyAlarm(RequestEvent evt) {
        if (!sipConfig.isAlarm()) {
            return;
        }
        try {
            FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME);
            String deviceId = SipUtils.getUserIdFromFromHeader(fromHeader);
            Element rootElement = getRootElement(evt);
            if (rootElement == null) {
                logger.error("处理alarm设备报警Notify时未获取到消息体{}", evt.getRequest());
                return;
            }
            Element deviceIdElement = rootElement.element("DeviceID");
            String channelId = deviceIdElement.getText().toString();
            Device device = redisCatchStorage.getDevice(deviceId);
            if (device == null) {
                logger.warn("[ NotifyAlarm ] 未找到设备:{}", deviceId);
                return;
            }
            rootElement = getRootElement(evt, device.getCharset());
            if (rootElement == null) {
                logger.warn("[ NotifyAlarm ] content cannot be null, {}", evt.getRequest());
                return;
            }
            DeviceAlarm deviceAlarm = new DeviceAlarm();
            deviceAlarm.setDeviceId(deviceId);
            deviceAlarm.setAlarmPriority(XmlUtil.getText(rootElement, "AlarmPriority"));
            deviceAlarm.setAlarmMethod(XmlUtil.getText(rootElement, "AlarmMethod"));
            String alarmTime = XmlUtil.getText(rootElement, "AlarmTime");
            if (alarmTime == null) {
                logger.warn("[ NotifyAlarm ] AlarmTime cannot be null");
                return;
            }
            deviceAlarm.setAlarmTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(alarmTime));
            if (XmlUtil.getText(rootElement, "AlarmDescription") == null) {
                deviceAlarm.setAlarmDescription("");
            } else {
                deviceAlarm.setAlarmDescription(XmlUtil.getText(rootElement, "AlarmDescription"));
            }
            if (NumericUtil.isDouble(XmlUtil.getText(rootElement, "Longitude"))) {
                deviceAlarm.setLongitude(Double.parseDouble(XmlUtil.getText(rootElement, "Longitude")));
            } else {
                deviceAlarm.setLongitude(0.00);
            }
            if (NumericUtil.isDouble(XmlUtil.getText(rootElement, "Latitude"))) {
                deviceAlarm.setLatitude(Double.parseDouble(XmlUtil.getText(rootElement, "Latitude")));
            } else {
                deviceAlarm.setLatitude(0.00);
            }
            logger.info("[收到Notify-Alarm]:{}/{}", device.getDeviceId(), deviceAlarm.getChannelId());
            if ("4".equals(deviceAlarm.getAlarmMethod())) {
                MobilePosition mobilePosition = new MobilePosition();
                mobilePosition.setCreateTime(DateUtil.getNow());
                mobilePosition.setDeviceId(deviceAlarm.getDeviceId());
                mobilePosition.setTime(deviceAlarm.getAlarmTime());
                mobilePosition.setLongitude(deviceAlarm.getLongitude());
                mobilePosition.setLatitude(deviceAlarm.getLatitude());
                mobilePosition.setReportSource("GPS Alarm");
                // 更新device channel 的经纬度
                DeviceChannel deviceChannel = new DeviceChannel();
                deviceChannel.setDeviceId(device.getDeviceId());
                deviceChannel.setChannelId(channelId);
                deviceChannel.setLongitude(mobilePosition.getLongitude());
                deviceChannel.setLatitude(mobilePosition.getLatitude());
                deviceChannel.setGpsTime(mobilePosition.getTime());
                deviceChannel = deviceChannelService.updateGps(deviceChannel, device);
                mobilePosition.setLongitudeWgs84(deviceChannel.getLongitudeWgs84());
                mobilePosition.setLatitudeWgs84(deviceChannel.getLatitudeWgs84());
                mobilePosition.setLongitudeGcj02(deviceChannel.getLongitudeGcj02());
                mobilePosition.setLatitudeGcj02(deviceChannel.getLatitudeGcj02());
                if (userSetting.getSavePositionHistory()) {
                    storager.insertMobilePosition(mobilePosition);
                }
                storager.updateChannelPosition(deviceChannel);
                // 发送redis消息。 通知位置信息的变化
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("time", mobilePosition.getTime());
                jsonObject.put("serial", deviceChannel.getDeviceId());
                jsonObject.put("code", deviceChannel.getChannelId());
                jsonObject.put("longitude", mobilePosition.getLongitude());
                jsonObject.put("latitude", mobilePosition.getLatitude());
                jsonObject.put("altitude", mobilePosition.getAltitude());
                jsonObject.put("direction", mobilePosition.getDirection());
                jsonObject.put("speed", mobilePosition.getSpeed());
                redisCatchStorage.sendMobilePositionMsg(jsonObject);
            }
            // TODO: 需要实现存储报警信息、报警分类
            // 回复200 OK
            if (redisCatchStorage.deviceIsOnline(deviceId)) {
                publisher.deviceAlarmEventPublish(deviceAlarm);
            }
        } catch (DocumentException e) {
            e.printStackTrace();
        }
    }
    /***
     * 处理catalog设备目录列表Notify
     *
     * @param evt
     */
    private void processNotifyCatalogList(RequestEvent evt) {
        try {
            FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME);
            String deviceId = SipUtils.getUserIdFromFromHeader(fromHeader);
            Device device = redisCatchStorage.getDevice(deviceId);
            if (device == null || device.getOnline() == 0) {
                logger.warn("[收到目录订阅]:{}, 但是设备已经离线", (device != null ? device.getDeviceId():"" ));
                return;
            }
            Element rootElement = getRootElement(evt, device.getCharset());
            if (rootElement == null) {
                logger.warn("[ 收到目录订阅 ] content cannot be null, {}", evt.getRequest());
                return;
            }
            Element deviceListElement = rootElement.element("DeviceList");
            if (deviceListElement == null) {
                return;
            }
            Iterator<Element> deviceListIterator = deviceListElement.elementIterator();
            if (deviceListIterator != null) {
                // 遍历DeviceList
                while (deviceListIterator.hasNext()) {
                    Element itemDevice = deviceListIterator.next();
                    Element channelDeviceElement = itemDevice.element("DeviceID");
                    if (channelDeviceElement == null) {
                        continue;
                    }
                    Element eventElement = itemDevice.element("Event");
                    String event;
                    if (eventElement == null) {
                        logger.warn("[收到目录订阅]:{}, 但是Event为空, 设为默认值 ADD", (device != null ? device.getDeviceId():"" ));
                        event = CatalogEvent.ADD;
                    }else {
                        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
                        logger.info(time + "---事件描述:"+eventElement.getText());
                        event = eventElement.getText().toUpperCase();
                    }
                    DeviceChannel channel = XmlUtil.channelContentHander(itemDevice, device, event);
                    channel.setDeviceId(device.getDeviceId());
                    logger.info("[收到目录订阅]:{}/{}", device.getDeviceId(), channel.getChannelId());
                    switch (event) {
                        case CatalogEvent.ON:
                            // 上线
                            logger.info("[收到通道上线通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId());
                            storager.deviceChannelOnline(deviceId, channel.getChannelId());
                            break;
                        case CatalogEvent.OFF :
                            // 离线
                            logger.info("[收到通道离线通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId());
                            storager.deviceChannelOffline(deviceId, channel.getChannelId());
                            break;
                        case CatalogEvent.VLOST:
                            // 视频丢失
                            logger.info("[收到通道视频丢失通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId());
                            storager.deviceChannelOffline(deviceId, channel.getChannelId());
                            break;
                        case CatalogEvent.DEFECT:
                            // 故障
                            break;
                        case CatalogEvent.ADD:
                            // 增加
                            logger.info("[收到增加通道通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId());
                            deviceChannelService.updateChannel(deviceId, channel);
                            break;
                        case CatalogEvent.DEL:
                            // 删除
                            logger.info("[收到删除通道通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId());
                            storager.delChannel(deviceId, channel.getChannelId());
                            break;
                        case CatalogEvent.UPDATE:
                            // 更新
                            logger.info("[收到更新通道通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId());
                            deviceChannelService.updateChannel(deviceId, channel);
                            break;
                        default:
                            logger.warn("[ NotifyCatalog ] event not found : {}", event );
                    }
                    // 转发变化信息
                    eventPublisher.catalogEventPublish(null, channel, event);
                }
            }
        } catch (DocumentException e) {
            e.printStackTrace();
        }
    }
    public void setCmder(SIPCommander cmder) {
    }
    public void setStorager(IVideoManagerStorage storager) {
        this.storager = storager;
    }
    public void setPublisher(EventPublisher publisher) {
        this.publisher = publisher;
    }
    public void setRedis(RedisUtil redis) {
    }
    public void setDeferredResultHolder(DeferredResultHolder deferredResultHolder) {
    }
    public IRedisCatchStorage getRedisCatchStorage() {
        return redisCatchStorage;
    }
    public void setRedisCatchStorage(IRedisCatchStorage redisCatchStorage) {
        this.redisCatchStorage = redisCatchStorage;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java
New file
@@ -0,0 +1,176 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.WvpSipDate;
import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver;
import com.genersoft.iot.vmp.gb28181.transmit.SIPSender;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.auth.DigestServerAuthenticationHelper;
import com.genersoft.iot.vmp.service.IDeviceService;
import com.genersoft.iot.vmp.utils.DateUtil;
import gov.nist.javax.sip.RequestEventExt;
import gov.nist.javax.sip.address.AddressImpl;
import gov.nist.javax.sip.address.SipUri;
import gov.nist.javax.sip.header.Expires;
import gov.nist.javax.sip.header.SIPDateHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.header.*;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Locale;
/**
 * SIP命令类型: REGISTER请求
 */
@Component
public class RegisterRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor {
    private final Logger logger = LoggerFactory.getLogger(RegisterRequestProcessor.class);
    public final String method = "REGISTER";
    @Autowired
    private SipConfig sipConfig;
    @Autowired
    private SIPProcessorObserver sipProcessorObserver;
    @Autowired
    private IDeviceService deviceService;
    @Autowired
    private SIPSender sipSender;
    @Override
    public void afterPropertiesSet() throws Exception {
        // 添加消息处理的订阅
        sipProcessorObserver.addRequestProcessor(method, this);
    }
    /**
     * 收到注册请求 处理
     *
     * @param evt
     */
    @Override
    public void process(RequestEvent evt) {
        try {
            RequestEventExt evtExt = (RequestEventExt) evt;
            String requestAddress = evtExt.getRemoteIpAddress() + ":" + evtExt.getRemotePort();
            logger.info("[注册请求] 开始处理: {}", requestAddress);
            Request request = evt.getRequest();
            ExpiresHeader expiresHeader = (ExpiresHeader) request.getHeader(Expires.NAME);
            Response response = null;
            boolean passwordCorrect = false;
            // 注册标志
            boolean registerFlag = false;
            FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME);
            AddressImpl address = (AddressImpl) fromHeader.getAddress();
            SipUri uri = (SipUri) address.getURI();
            String deviceId = uri.getUser();
            Device device = deviceService.getDevice(deviceId);
            String password = (device != null && !ObjectUtils.isEmpty(device.getPassword()))? device.getPassword() : sipConfig.getPassword();
            AuthorizationHeader authHead = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME);
            if (authHead == null && !ObjectUtils.isEmpty(password)) {
                logger.info("[注册请求] 未携带授权头 回复401: {}", requestAddress);
                response = getMessageFactory().createResponse(Response.UNAUTHORIZED, request);
                new DigestServerAuthenticationHelper().generateChallenge(getHeaderFactory(), response, sipConfig.getDomain());
                sipSender.transmitRequest(response);
                return;
            }
            // 校验密码是否正确
            passwordCorrect = ObjectUtils.isEmpty(password) ||
                    new DigestServerAuthenticationHelper().doAuthenticatePlainTextPassword(request, password);
            if (!passwordCorrect) {
                // 注册失败
                response = getMessageFactory().createResponse(Response.FORBIDDEN, request);
                response.setReasonPhrase("wrong password");
                logger.info("[注册请求] 密码/SIP服务器ID错误, 回复403: {}", requestAddress);
                sipSender.transmitRequest(response);
                return;
            }
            // 携带授权头并且密码正确
            response = getMessageFactory().createResponse(Response.OK, request);
            // 添加date头
            SIPDateHeader dateHeader = new SIPDateHeader();
            // 使用自己修改的
            WvpSipDate wvpSipDate = new WvpSipDate(Calendar.getInstance(Locale.ENGLISH).getTimeInMillis());
            dateHeader.setDate(wvpSipDate);
            response.addHeader(dateHeader);
            if (expiresHeader == null) {
                response = getMessageFactory().createResponse(Response.BAD_REQUEST, request);
                sipSender.transmitRequest(response);
                return;
            }
            // 添加Contact头
            response.addHeader(request.getHeader(ContactHeader.NAME));
            // 添加Expires头
            response.addHeader(request.getExpires());
            // 获取到通信地址等信息
            ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);
            String received = viaHeader.getReceived();
            int rPort = viaHeader.getRPort();
            // 解析本地地址替代
            if (ObjectUtils.isEmpty(received) || rPort == -1) {
                received = viaHeader.getHost();
                rPort = viaHeader.getPort();
            }
            if (device == null) {
                device = new Device();
                device.setStreamMode("UDP");
                device.setCharset("GB2312");
                device.setGeoCoordSys("WGS84");
                device.setTreeType("CivilCode");
                device.setDeviceId(deviceId);
                device.setOnline(0);
            }
            device.setIp(received);
            device.setPort(rPort);
            device.setHostAddress(received.concat(":").concat(String.valueOf(rPort)));
            if (expiresHeader.getExpires() == 0) {
                // 注销成功
                registerFlag = false;
            } else {
                // 注册成功
                device.setExpires(expiresHeader.getExpires());
                registerFlag = true;
                // 判断TCP还是UDP
                ViaHeader reqViaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);
                String transport = reqViaHeader.getTransport();
                device.setTransport("TCP".equalsIgnoreCase(transport) ? "TCP" : "UDP");
            }
            sipSender.transmitRequest(response);
            // 注册成功
            // 保存到redis
            if (registerFlag) {
                logger.info("[注册成功] deviceId: {}->{}",  deviceId, requestAddress);
                device.setRegisterTime(DateUtil.getNow());
                deviceService.online(device);
            } else {
                logger.info("[注销成功] deviceId: {}->{}" ,deviceId, requestAddress);
                deviceService.offline(deviceId);
            }
        } catch (SipException | NoSuchAlgorithmException | ParseException e) {
            e.printStackTrace();
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/SubscribeRequestProcessor.java
New file
@@ -0,0 +1,201 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl;
import com.genersoft.iot.vmp.common.VideoManagerConstants;
import com.genersoft.iot.vmp.conf.DynamicTask;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.gb28181.bean.CmdType;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.bean.SubscribeHolder;
import com.genersoft.iot.vmp.gb28181.bean.SubscribeInfo;
import com.genersoft.iot.vmp.gb28181.task.impl.MobilePositionSubscribeHandlerTask;
import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver;
import com.genersoft.iot.vmp.gb28181.transmit.SIPSender;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import com.genersoft.iot.vmp.gb28181.utils.XmlUtil;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import gov.nist.javax.sip.SipProviderImpl;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sip.*;
import javax.sip.header.ExpiresHeader;
import javax.sip.message.Response;
import java.text.ParseException;
/**
 * SIP命令类型: SUBSCRIBE请求
 * @author lin
 */
@Component
public class SubscribeRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor {
    private final Logger logger = LoggerFactory.getLogger(SubscribeRequestProcessor.class);
    private final String method = "SUBSCRIBE";
    @Autowired
    private SIPProcessorObserver sipProcessorObserver;
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private SubscribeHolder subscribeHolder;
    @Autowired
    private SIPSender sipSender;
    @Override
    public void afterPropertiesSet() throws Exception {
        // 添加消息处理的订阅
        sipProcessorObserver.addRequestProcessor(method, this);
    }
    /**
     * 处理SUBSCRIBE请求
     *
     * @param evt 事件
     */
    @Override
    public void process(RequestEvent evt) {
        SIPRequest request = (SIPRequest) evt.getRequest();
        try {
            Element rootElement = getRootElement(evt);
            if (rootElement == null) {
                logger.error("处理SUBSCRIBE请求  未获取到消息体{}", evt.getRequest());
                return;
            }
            String cmd = XmlUtil.getText(rootElement, "CmdType");
            if (CmdType.MOBILE_POSITION.equals(cmd)) {
                processNotifyMobilePosition(request, rootElement);
//            } else if (CmdType.ALARM.equals(cmd)) {
//                logger.info("接收到Alarm订阅");
//                processNotifyAlarm(serverTransaction, rootElement);
            } else if (CmdType.CATALOG.equals(cmd)) {
                processNotifyCatalogList(request, rootElement);
            } else {
                logger.info("接收到消息:" + cmd);
                Response response = getMessageFactory().createResponse(200, request);
                if (response != null) {
                    ExpiresHeader expireHeader = getHeaderFactory().createExpiresHeader(30);
                    response.setExpires(expireHeader);
                }
                logger.info("response : " + response);
                sipSender.transmitRequest(response);
            }
        } catch (ParseException | SipException | InvalidArgumentException | DocumentException e) {
            e.printStackTrace();
        }
    }
    /**
     * 处理移动位置订阅消息
     */
    private void processNotifyMobilePosition(SIPRequest request, Element rootElement) throws SipException {
        if (request == null) {
            return;
        }
        String platformId = SipUtils.getUserIdFromFromHeader(request);
        String deviceId = XmlUtil.getText(rootElement, "DeviceID");
        ParentPlatform platform = storager.queryParentPlatByServerGBId(platformId);
        SubscribeInfo subscribeInfo = new SubscribeInfo(request, platformId);
        if (platform == null) {
            return;
        }
        String sn = XmlUtil.getText(rootElement, "SN");
        logger.info("[回复上级的移动位置订阅请求]: {}", platformId);
        StringBuilder resultXml = new StringBuilder(200);
        resultXml.append("<?xml version=\"1.0\" ?>\r\n")
                .append("<Response>\r\n")
                .append("<CmdType>MobilePosition</CmdType>\r\n")
                .append("<SN>").append(sn).append("</SN>\r\n")
                .append("<DeviceID>").append(deviceId).append("</DeviceID>\r\n")
                .append("<Result>OK</Result>\r\n")
                .append("</Response>\r\n");
        if (subscribeInfo.getExpires() > 0) {
            // GPS上报时间间隔
            String interval = XmlUtil.getText(rootElement, "Interval");
            if (interval == null) {
                subscribeInfo.setGpsInterval(5);
            }else {
                subscribeInfo.setGpsInterval(Integer.parseInt(interval));
            }
            subscribeInfo.setSn(sn);
        }
        try {
            ParentPlatform parentPlatform = storager.queryParentPlatByServerGBId(platformId);
            SIPResponse response = responseXmlAck(request, resultXml.toString(), parentPlatform, subscribeInfo.getExpires());
            if (subscribeInfo.getExpires() == 0) {
                subscribeHolder.removeMobilePositionSubscribe(platformId);
            }else {
                subscribeInfo.setResponse(response);
                subscribeHolder.putMobilePositionSubscribe(platformId, subscribeInfo);
            }
        } catch (SipException | InvalidArgumentException | ParseException e) {
            e.printStackTrace();
        }
    }
    private void processNotifyAlarm(RequestEvent evt, Element rootElement) {
    }
    private void processNotifyCatalogList(SIPRequest request, Element rootElement) throws SipException {
        if (request == null) {
            return;
        }
        String platformId = SipUtils.getUserIdFromFromHeader(request);
        String deviceId = XmlUtil.getText(rootElement, "DeviceID");
        ParentPlatform platform = storager.queryParentPlatByServerGBId(platformId);
        if (platform == null){
            return;
        }
        SubscribeInfo subscribeInfo = new SubscribeInfo(request, platformId);
        String sn = XmlUtil.getText(rootElement, "SN");
        logger.info("[回复上级的目录订阅请求]: {}/{}", platformId, deviceId);
        StringBuilder resultXml = new StringBuilder(200);
        resultXml.append("<?xml version=\"1.0\" ?>\r\n")
                .append("<Response>\r\n")
                .append("<CmdType>Catalog</CmdType>\r\n")
                .append("<SN>").append(sn).append("</SN>\r\n")
                .append("<DeviceID>").append(deviceId).append("</DeviceID>\r\n")
                .append("<Result>OK</Result>\r\n")
                .append("</Response>\r\n");
        if (subscribeInfo.getExpires() > 0) {
            subscribeHolder.putCatalogSubscribe(platformId, subscribeInfo);
        }else if (subscribeInfo.getExpires() == 0) {
            subscribeHolder.removeCatalogSubscribe(platformId);
        }
        try {
            ParentPlatform parentPlatform = storager.queryParentPlatByServerGBId(platformId);
            SIPResponse response = responseXmlAck(request, resultXml.toString(), parentPlatform, subscribeInfo.getExpires());
            if (subscribeInfo.getExpires() == 0) {
                subscribeHolder.removeCatalogSubscribe(platformId);
            }else {
                subscribeInfo.setResponse(response);
                subscribeHolder.putCatalogSubscribe(platformId, subscribeInfo);
            }
        } catch (SipException | InvalidArgumentException | ParseException e) {
            e.printStackTrace();
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/info/InfoRequestProcessor.java
New file
@@ -0,0 +1,139 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.info;
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import gov.nist.javax.sip.message.SIPRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.header.*;
import javax.sip.message.Response;
import java.text.ParseException;
@Component
public class InfoRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor {
    private final static Logger logger = LoggerFactory.getLogger(InfoRequestProcessor.class);
    private final String method = "INFO";
    @Autowired
    private SIPProcessorObserver sipProcessorObserver;
    @Autowired
    private IVideoManagerStorage storage;
    @Autowired
    private SipSubscribe sipSubscribe;
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private SIPCommander cmder;
    @Autowired
    private VideoStreamSessionManager sessionManager;
    @Override
    public void afterPropertiesSet() throws Exception {
        // 添加消息处理的订阅
        sipProcessorObserver.addRequestProcessor(method, this);
    }
    @Override
    public void process(RequestEvent evt) {
        logger.debug("接收到消息:" + evt.getRequest());
        SIPRequest request = (SIPRequest) evt.getRequest();
        String deviceId = SipUtils.getUserIdFromFromHeader(request);
        CallIdHeader callIdHeader = request.getCallIdHeader();
        // 先从会话内查找
        SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransaction(null, null, callIdHeader.getCallId(), null);
        // 兼容海康 媒体通知 消息from字段不是设备ID的问题
        if (ssrcTransaction != null) {
            deviceId = ssrcTransaction.getDeviceId();
        }
        // 查询设备是否存在
        Device device = redisCatchStorage.getDevice(deviceId);
        // 查询上级平台是否存在
        ParentPlatform parentPlatform = storage.queryParentPlatByServerGBId(deviceId);
        try {
            if (device != null && parentPlatform != null) {
                logger.warn("[重复]平台与设备编号重复:{}", deviceId);
                String hostAddress = request.getRemoteAddress().getHostAddress();
                int remotePort = request.getRemotePort();
                if (device.getHostAddress().equals(hostAddress + ":" + remotePort)) {
                    parentPlatform = null;
                }else {
                    device = null;
                }
            }
            if (device == null && parentPlatform == null) {
                // 不存在则回复404
                responseAck(request, Response.NOT_FOUND, "device "+ deviceId +" not found");
                logger.warn("[设备未找到 ]: {}", deviceId);
                if (sipSubscribe.getErrorSubscribe(callIdHeader.getCallId()) != null){
                    DeviceNotFoundEvent deviceNotFoundEvent = new DeviceNotFoundEvent(evt.getDialog());
                    deviceNotFoundEvent.setCallId(callIdHeader.getCallId());
                    SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(deviceNotFoundEvent);
                    sipSubscribe.getErrorSubscribe(callIdHeader.getCallId()).response(eventResult);
                };
            }else {
                ContentTypeHeader header = (ContentTypeHeader)evt.getRequest().getHeader(ContentTypeHeader.NAME);
                String contentType = header.getContentType();
                String contentSubType = header.getContentSubType();
                if ("Application".equalsIgnoreCase(contentType) && "MANSRTSP".equalsIgnoreCase(contentSubType)) {
                    SendRtpItem sendRtpItem = redisCatchStorage.querySendRTPServer(null, null, null, callIdHeader.getCallId());
                    String streamId = sendRtpItem.getStreamId();
                    StreamInfo streamInfo = redisCatchStorage.queryPlayback(null, null, streamId, null);
                    if (null == streamInfo) {
                        responseAck(request, Response.NOT_FOUND, "stream " + streamId + " not found");
                        return;
                    }
                    Device device1 = storager.queryVideoDevice(streamInfo.getDeviceID());
                    cmder.playbackControlCmd(device1,streamInfo,new String(evt.getRequest().getRawContent()),eventResult -> {
                        // 失败的回复
                        try {
                            responseAck(request, eventResult.statusCode, eventResult.msg);
                        } catch (SipException | InvalidArgumentException | ParseException e) {
                            logger.error("[命令发送失败] 国标级联 录像控制: {}", e.getMessage());
                        }
                    }, eventResult -> {
                        // 成功的回复
                        try {
                            responseAck(request, eventResult.statusCode);
                        } catch (SipException | InvalidArgumentException | ParseException e) {
                            logger.error("[命令发送失败] 国标级联 录像控制: {}", e.getMessage());
                        }
                    });
                }
            }
        } catch (SipException e) {
            logger.warn("SIP 回复错误", e);
        } catch (InvalidArgumentException e) {
            logger.warn("参数无效", e);
        } catch (ParseException e) {
            logger.warn("SIP回复时解析异常", e);
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/IMessageHandler.java
New file
@@ -0,0 +1,23 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import org.dom4j.Element;
import javax.sip.RequestEvent;
public interface IMessageHandler {
    /**
     * 处理来自设备的信息
     * @param evt
     * @param device
     */
    void handForDevice(RequestEvent evt, Device device, Element element);
    /**
     * 处理来自平台的信息
     * @param evt
     * @param parentPlatform
     */
    void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element element);
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageHandlerAbstract.java
New file
@@ -0,0 +1,40 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import org.dom4j.Element;
import org.springframework.beans.factory.annotation.Autowired;
import javax.sip.RequestEvent;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText;
public abstract class MessageHandlerAbstract extends SIPRequestProcessorParent implements IMessageHandler{
    public Map<String, IMessageHandler> messageHandlerMap = new ConcurrentHashMap<>();
    public void addHandler(String cmdType, IMessageHandler messageHandler) {
        messageHandlerMap.put(cmdType, messageHandler);
    }
    @Override
    public void handForDevice(RequestEvent evt, Device device, Element element) {
        String cmd = getText(element, "CmdType");
        IMessageHandler messageHandler = messageHandlerMap.get(cmd);
        if (messageHandler != null) {
            messageHandler.handForDevice(evt, device, element);
        }
    }
    @Override
    public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element element) {
        String cmd = getText(element, "CmdType");
        IMessageHandler messageHandler = messageHandlerMap.get(cmd);
        if (messageHandler != null) {
            messageHandler.handForPlatform(evt, parentPlatform, element);
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageRequestProcessor.java
New file
@@ -0,0 +1,142 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.DeviceNotFoundEvent;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction;
import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.header.CallIdHeader;
import javax.sip.message.Response;
import java.text.ParseException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class MessageRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor {
    private final static Logger logger = LoggerFactory.getLogger(MessageRequestProcessor.class);
    private final String method = "MESSAGE";
    private static Map<String, IMessageHandler> messageHandlerMap = new ConcurrentHashMap<>();
    @Autowired
    private SIPProcessorObserver sipProcessorObserver;
    @Autowired
    private IVideoManagerStorage storage;
    @Autowired
    private SipSubscribe sipSubscribe;
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Autowired
    private VideoStreamSessionManager sessionManager;
    @Override
    public void afterPropertiesSet() throws Exception {
        // 添加消息处理的订阅
        sipProcessorObserver.addRequestProcessor(method, this);
    }
    public void addHandler(String name, IMessageHandler handler) {
        messageHandlerMap.put(name, handler);
    }
    @Override
    public void process(RequestEvent evt) {
        SIPRequest sipRequest = (SIPRequest)evt.getRequest();
        logger.debug("接收到消息:" + evt.getRequest());
        String deviceId = SipUtils.getUserIdFromFromHeader(evt.getRequest());
        CallIdHeader callIdHeader = sipRequest.getCallIdHeader();
        // 先从会话内查找
        SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransaction(null, null, callIdHeader.getCallId(), null);
        // 兼容海康 媒体通知 消息from字段不是设备ID的问题
        if (ssrcTransaction != null) {
            deviceId = ssrcTransaction.getDeviceId();
        }
        SIPRequest request = (SIPRequest) evt.getRequest();
        // 查询设备是否存在
        Device device = redisCatchStorage.getDevice(deviceId);
        // 查询上级平台是否存在
        ParentPlatform parentPlatform = storage.queryParentPlatByServerGBId(deviceId);
        try {
            if (device != null && parentPlatform != null) {
                String hostAddress = request.getRemoteAddress().getHostAddress();
                int remotePort = request.getRemotePort();
                if (device.getHostAddress().equals(hostAddress + ":" + remotePort)) {
                    parentPlatform = null;
                }else {
                    device = null;
                }
            }
            if (device == null && parentPlatform == null) {
                // 不存在则回复404
                responseAck(request, Response.NOT_FOUND, "device "+ deviceId +" not found");
                logger.warn("[设备未找到 ]: {}", deviceId);
                if (sipSubscribe.getErrorSubscribe(callIdHeader.getCallId()) != null){
                    DeviceNotFoundEvent deviceNotFoundEvent = new DeviceNotFoundEvent(evt.getDialog());
                    deviceNotFoundEvent.setCallId(callIdHeader.getCallId());
                    SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(deviceNotFoundEvent);
                    sipSubscribe.getErrorSubscribe(callIdHeader.getCallId()).response(eventResult);
                };
            }else {
                Element rootElement = null;
                try {
                    rootElement = getRootElement(evt);
                    if (rootElement == null) {
                        logger.error("处理MESSAGE请求  未获取到消息体{}", evt.getRequest());
                        responseAck(request, Response.BAD_REQUEST, "content is null");
                        return;
                    }
                } catch (DocumentException e) {
                    logger.warn("解析XML消息内容异常", e);
                    // 不存在则回复404
                    responseAck(request, Response.BAD_REQUEST, e.getMessage());
                }
                String name = rootElement.getName();
                IMessageHandler messageHandler = messageHandlerMap.get(name);
                if (messageHandler != null) {
                    if (device != null) {
                        messageHandler.handForDevice(evt, device, rootElement);
                    }else { // 由于上面已经判断都为null则直接返回,所以这里device和parentPlatform必有一个不为null
                        messageHandler.handForPlatform(evt, parentPlatform, rootElement);
                    }
                }else {
                    // 不支持的message
                    // 不存在则回复415
                    responseAck(request, Response.UNSUPPORTED_MEDIA_TYPE, "Unsupported message type, must Control/Notify/Query/Response");
                }
            }
        } catch (SipException e) {
            logger.warn("SIP 回复错误", e);
        } catch (InvalidArgumentException e) {
            logger.warn("参数无效", e);
        } catch (ParseException e) {
            logger.warn("SIP回复时解析异常", e);
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/ControlMessageHandler.java
New file
@@ -0,0 +1,27 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.control;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageHandlerAbstract;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageRequestProcessor;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
 * 命令类型: 控制命令
 * 命令类型: 设备控制: 远程启动, 录像控制(TODO), 报警布防/撤防命令(TODO), 报警复位命令(TODO),
 *                   强制关键帧命令(TODO), 拉框放大/缩小控制命令(TODO), 看守位控制(TODO), 报警复位(TODO)
 * 命令类型: 设备配置: SVAC编码配置(TODO), 音频参数(TODO), SVAC解码配置(TODO)
 */
@Component
public class ControlMessageHandler extends MessageHandlerAbstract implements InitializingBean  {
    private final String messageType = "Control";
    @Autowired
    private MessageRequestProcessor messageRequestProcessor;
    @Override
    public void afterPropertiesSet() throws Exception {
        messageRequestProcessor.addHandler(messageType, this);
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/cmd/DeviceControlQueryMessageHandler.java
New file
@@ -0,0 +1,142 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.control.cmd;
import com.genersoft.iot.vmp.VManageBootstrap;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.control.ControlMessageHandler;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import com.genersoft.iot.vmp.utils.SpringBeanFactory;
import gov.nist.javax.sip.SipStackImpl;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.sip.*;
import javax.sip.address.SipURI;
import javax.sip.header.HeaderAddress;
import javax.sip.header.ToHeader;
import javax.sip.message.Response;
import java.text.ParseException;
import java.util.Iterator;
import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText;
@Component
public class DeviceControlQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler {
    private Logger logger = LoggerFactory.getLogger(DeviceControlQueryMessageHandler.class);
    private final String cmdType = "DeviceControl";
    @Autowired
    private ControlMessageHandler controlMessageHandler;
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private SIPCommander cmder;
    @Autowired
    private SIPCommanderFroPlatform cmderFroPlatform;
    @Qualifier("taskExecutor")
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;
    @Override
    public void afterPropertiesSet() throws Exception {
        controlMessageHandler.addHandler(cmdType, this);
    }
    @Override
    public void handForDevice(RequestEvent evt, Device device, Element element) {
    }
    @Override
    public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element rootElement) {
        SIPRequest request = (SIPRequest) evt.getRequest();
        // 此处是上级发出的DeviceControl指令
        String targetGBId = ((SipURI) request.getToHeader().getAddress().getURI()).getUser();
        String channelId = getText(rootElement, "DeviceID");
        // 远程启动功能
        if (!ObjectUtils.isEmpty(getText(rootElement, "TeleBoot"))) {
            if (parentPlatform.getServerGBId().equals(targetGBId)) {
                // 远程启动本平台:需要在重新启动程序后先对SipStack解绑
                logger.info("执行远程启动本平台命令");
                try {
                    cmderFroPlatform.unregister(parentPlatform, null, null);
                } catch (InvalidArgumentException | ParseException | SipException e) {
                    logger.error("[命令发送失败] 国标级联 注销: {}", e.getMessage());
                }
                taskExecutor.execute(()->{
                    try {
                        Thread.sleep(3000);
                        SipProvider up = (SipProvider) SpringBeanFactory.getBean("udpSipProvider");
                        SipStackImpl stack = (SipStackImpl)up.getSipStack();
                        stack.stop();
                        Iterator listener = stack.getListeningPoints();
                        while (listener.hasNext()) {
                            stack.deleteListeningPoint((ListeningPoint) listener.next());
                        }
                        Iterator providers = stack.getSipProviders();
                        while (providers.hasNext()) {
                            stack.deleteSipProvider((SipProvider) providers.next());
                        }
                        VManageBootstrap.restart();
                    } catch (InterruptedException | ObjectInUseException e) {
                        logger.error("[任务执行失败] 服务重启: {}", e.getMessage());
                    }
                });
            } else {
                // 远程启动指定设备
            }
        }
        // 云台/前端控制命令
        if (!ObjectUtils.isEmpty(getText(rootElement,"PTZCmd")) && !parentPlatform.getServerGBId().equals(targetGBId)) {
            String cmdString = getText(rootElement,"PTZCmd");
            Device deviceForPlatform = storager.queryVideoDeviceByPlatformIdAndChannelId(parentPlatform.getServerGBId(), channelId);
            if (deviceForPlatform == null) {
                try {
                    responseAck(request, Response.NOT_FOUND);
                } catch (SipException | InvalidArgumentException | ParseException e) {
                    logger.error("[命令发送失败] 错误信息: {}", e.getMessage());
                }
                return;
            }
            try {
                cmder.fronEndCmd(deviceForPlatform, channelId, cmdString, eventResult -> {
                    // 失败的回复
                    try {
                        responseAck(request, eventResult.statusCode, eventResult.msg);
                    } catch (SipException | InvalidArgumentException | ParseException e) {
                        logger.error("[命令发送失败] 云台/前端回复: {}", e.getMessage());
                    }
                }, eventResult -> {
                    // 成功的回复
                    try {
                        responseAck(request, eventResult.statusCode);
                    } catch (SipException | InvalidArgumentException | ParseException e) {
                        logger.error("[命令发送失败] 云台/前端回复: {}", e.getMessage());
                    }
                });
            } catch (InvalidArgumentException | SipException | ParseException e) {
                logger.error("[命令发送失败] 云台/前端: {}", e.getMessage());
            }
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/NotifyMessageHandler.java
New file
@@ -0,0 +1,26 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageHandlerAbstract;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageRequestProcessor;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
 * 命令类型: 通知命令, 参看 A.2.5 通知命令
 * 命令类型: 状态信息(心跳)报送, 报警通知, 媒体通知, 移动设备位置数据,语音广播通知(TODO), 设备预置位(TODO)
 * @author lin
 */
@Component
public class NotifyMessageHandler extends MessageHandlerAbstract implements InitializingBean  {
    private final String messageType = "Notify";
    @Autowired
    private MessageRequestProcessor messageRequestProcessor;
    @Override
    public void afterPropertiesSet() throws Exception {
        messageRequestProcessor.addHandler(messageType, this);
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/AlarmNotifyMessageHandler.java
New file
@@ -0,0 +1,275 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd;
import com.alibaba.fastjson.JSONObject;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler;
import com.genersoft.iot.vmp.gb28181.utils.Coordtransform;
import com.genersoft.iot.vmp.gb28181.utils.NumericUtil;
import com.genersoft.iot.vmp.gb28181.utils.XmlUtil;
import com.genersoft.iot.vmp.service.IDeviceAlarmService;
import com.genersoft.iot.vmp.service.IDeviceChannelService;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import com.genersoft.iot.vmp.utils.DateUtil;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.message.Response;
import java.text.ParseException;
import java.util.concurrent.ConcurrentLinkedQueue;
import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.*;
/**
 * 报警事件的处理,参考:9.4
 */
@Component
public class AlarmNotifyMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler {
    private final Logger logger = LoggerFactory.getLogger(AlarmNotifyMessageHandler.class);
    private final String cmdType = "Alarm";
    @Autowired
    private NotifyMessageHandler notifyMessageHandler;
    @Autowired
    private EventPublisher publisher;
    @Autowired
    private UserSetting userSetting;
    @Autowired
    private SipConfig sipConfig;
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Autowired
    private IDeviceAlarmService deviceAlarmService;
    @Autowired
    private IDeviceChannelService deviceChannelService;
    private boolean taskQueueHandlerRun = false;
    private ConcurrentLinkedQueue<SipMsgInfo> taskQueue = new ConcurrentLinkedQueue<>();
    @Qualifier("taskExecutor")
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;
    @Override
    public void afterPropertiesSet() throws Exception {
        notifyMessageHandler.addHandler(cmdType, this);
    }
    @Override
    public void handForDevice(RequestEvent evt, Device device, Element rootElement) {
        logger.info("[收到报警通知]设备:{}", device.getDeviceId());
        taskQueue.offer(new SipMsgInfo(evt, device, rootElement));
        if (!taskQueueHandlerRun) {
            taskQueueHandlerRun = true;
            taskExecutor.execute(() -> {
                logger.info("[处理报警通知]待处理数量:{}", taskQueue.size() );
                while (!taskQueue.isEmpty()) {
                    SipMsgInfo sipMsgInfo = taskQueue.poll();
                    // 回复200 OK
                    try {
                        responseAck((SIPRequest) sipMsgInfo.getEvt().getRequest(), Response.OK);
                    } catch (SipException | InvalidArgumentException | ParseException e) {
                        logger.error("[处理报警通知], 回复200OK失败", e);
                    }
                    Element deviceIdElement = sipMsgInfo.getRootElement().element("DeviceID");
                    String channelId = deviceIdElement.getText().toString();
                    DeviceAlarm deviceAlarm = new DeviceAlarm();
                    deviceAlarm.setCreateTime(DateUtil.getNow());
                    deviceAlarm.setDeviceId(sipMsgInfo.getDevice().getDeviceId());
                    deviceAlarm.setChannelId(channelId);
                    deviceAlarm.setAlarmPriority(getText(sipMsgInfo.getRootElement(), "AlarmPriority"));
                    deviceAlarm.setAlarmMethod(getText(sipMsgInfo.getRootElement(), "AlarmMethod"));
                    String alarmTime = XmlUtil.getText(sipMsgInfo.getRootElement(), "AlarmTime");
                    if (alarmTime == null) {
                        continue;
                    }
                    deviceAlarm.setAlarmTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(alarmTime));
                    String alarmDescription = getText(sipMsgInfo.getRootElement(), "AlarmDescription");
                    if (alarmDescription == null) {
                        deviceAlarm.setAlarmDescription("");
                    } else {
                        deviceAlarm.setAlarmDescription(alarmDescription);
                    }
                    String longitude = getText(sipMsgInfo.getRootElement(), "Longitude");
                    if (longitude != null && NumericUtil.isDouble(longitude)) {
                        deviceAlarm.setLongitude(Double.parseDouble(longitude));
                    } else {
                        deviceAlarm.setLongitude(0.00);
                    }
                    String latitude = getText(sipMsgInfo.getRootElement(), "Latitude");
                    if (latitude != null && NumericUtil.isDouble(latitude)) {
                        deviceAlarm.setLatitude(Double.parseDouble(latitude));
                    } else {
                        deviceAlarm.setLatitude(0.00);
                    }
                    if (!ObjectUtils.isEmpty(deviceAlarm.getAlarmMethod())) {
                        if ( deviceAlarm.getAlarmMethod().contains(DeviceAlarmMethod.GPS.getVal() + "")) {
                            MobilePosition mobilePosition = new MobilePosition();
                            mobilePosition.setCreateTime(DateUtil.getNow());
                            mobilePosition.setDeviceId(deviceAlarm.getDeviceId());
                            mobilePosition.setTime(deviceAlarm.getAlarmTime());
                            mobilePosition.setLongitude(deviceAlarm.getLongitude());
                            mobilePosition.setLatitude(deviceAlarm.getLatitude());
                            mobilePosition.setReportSource("GPS Alarm");
                            // 更新device channel 的经纬度
                            DeviceChannel deviceChannel = new DeviceChannel();
                            deviceChannel.setDeviceId(sipMsgInfo.getDevice().getDeviceId());
                            deviceChannel.setChannelId(channelId);
                            deviceChannel.setLongitude(mobilePosition.getLongitude());
                            deviceChannel.setLatitude(mobilePosition.getLatitude());
                            deviceChannel.setGpsTime(mobilePosition.getTime());
                            deviceChannel = deviceChannelService.updateGps(deviceChannel, sipMsgInfo.getDevice());
                            mobilePosition.setLongitudeWgs84(deviceChannel.getLongitudeWgs84());
                            mobilePosition.setLatitudeWgs84(deviceChannel.getLatitudeWgs84());
                            mobilePosition.setLongitudeGcj02(deviceChannel.getLongitudeGcj02());
                            mobilePosition.setLatitudeGcj02(deviceChannel.getLatitudeGcj02());
                            if (userSetting.getSavePositionHistory()) {
                                storager.insertMobilePosition(mobilePosition);
                            }
                            storager.updateChannelPosition(deviceChannel);
                            // 发送redis消息。 通知位置信息的变化
                            JSONObject jsonObject = new JSONObject();
                            jsonObject.put("time", mobilePosition.getTime());
                            jsonObject.put("serial", deviceChannel.getDeviceId());
                            jsonObject.put("code", deviceChannel.getChannelId());
                            jsonObject.put("longitude", mobilePosition.getLongitude());
                            jsonObject.put("latitude", mobilePosition.getLatitude());
                            jsonObject.put("altitude", mobilePosition.getAltitude());
                            jsonObject.put("direction", mobilePosition.getDirection());
                            jsonObject.put("speed", mobilePosition.getSpeed());
                            redisCatchStorage.sendMobilePositionMsg(jsonObject);
                        }
                    }
                    if (!ObjectUtils.isEmpty(deviceAlarm.getDeviceId())) {
                        if (deviceAlarm.getAlarmMethod().contains(DeviceAlarmMethod.Video.getVal() + "")) {
                            deviceAlarm.setAlarmType(getText(sipMsgInfo.getRootElement().element("Info"), "AlarmType"));
                        }
                    }
                    logger.info("[收到报警通知]内容:{}", JSONObject.toJSON(deviceAlarm));
                    if ("7".equals(deviceAlarm.getAlarmMethod()) ) {
                        // 发送给平台的报警信息。 发送redis通知
                        AlarmChannelMessage alarmChannelMessage = new AlarmChannelMessage();
                        alarmChannelMessage.setAlarmSn(Integer.parseInt(deviceAlarm.getAlarmMethod()));
                        alarmChannelMessage.setAlarmDescription(deviceAlarm.getAlarmDescription());
                        alarmChannelMessage.setGbId(channelId);
                        redisCatchStorage.sendAlarmMsg(alarmChannelMessage);
                        continue;
                    }
                    logger.debug("存储报警信息、报警分类");
                    // 存储报警信息、报警分类
                    if (sipConfig.isAlarm()) {
                        deviceAlarmService.add(deviceAlarm);
                    }
                    if (redisCatchStorage.deviceIsOnline(sipMsgInfo.getDevice().getDeviceId())) {
                        publisher.deviceAlarmEventPublish(deviceAlarm);
                    }
                }
                taskQueueHandlerRun = false;
            });
        }
    }
    @Override
    public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element rootElement) {
        logger.info("收到来自平台[{}]的报警通知", parentPlatform.getServerGBId());
        // 回复200 OK
        try {
            responseAck((SIPRequest) evt.getRequest(), Response.OK);
        } catch (SipException | InvalidArgumentException | ParseException e) {
            logger.error("[命令发送失败] 国标级联 报警通知回复: {}", e.getMessage());
        }
        Element deviceIdElement = rootElement.element("DeviceID");
        String channelId = deviceIdElement.getText().toString();
        DeviceAlarm deviceAlarm = new DeviceAlarm();
        deviceAlarm.setCreateTime(DateUtil.getNow());
        deviceAlarm.setDeviceId(parentPlatform.getServerGBId());
        deviceAlarm.setChannelId(channelId);
        deviceAlarm.setAlarmPriority(getText(rootElement, "AlarmPriority"));
        deviceAlarm.setAlarmMethod(getText(rootElement, "AlarmMethod"));
        String alarmTime = XmlUtil.getText(rootElement, "AlarmTime");
        if (alarmTime == null) {
            return;
        }
        deviceAlarm.setAlarmTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(alarmTime));
        String alarmDescription = getText(rootElement, "AlarmDescription");
        if (alarmDescription == null) {
            deviceAlarm.setAlarmDescription("");
        } else {
            deviceAlarm.setAlarmDescription(alarmDescription);
        }
        String longitude = getText(rootElement, "Longitude");
        if (longitude != null && NumericUtil.isDouble(longitude)) {
            deviceAlarm.setLongitude(Double.parseDouble(longitude));
        } else {
            deviceAlarm.setLongitude(0.00);
        }
        String latitude = getText(rootElement, "Latitude");
        if (latitude != null && NumericUtil.isDouble(latitude)) {
            deviceAlarm.setLatitude(Double.parseDouble(latitude));
        } else {
            deviceAlarm.setLatitude(0.00);
        }
        if (!ObjectUtils.isEmpty(deviceAlarm.getAlarmMethod())) {
            if (deviceAlarm.getAlarmMethod().contains(DeviceAlarmMethod.Video.getVal() + "")) {
                deviceAlarm.setAlarmType(getText(rootElement.element("Info"), "AlarmType"));
            }
        }
        if (channelId.equals(parentPlatform.getDeviceGBId())) {
            // 发送给平台的报警信息。 发送redis通知
            AlarmChannelMessage alarmChannelMessage = new AlarmChannelMessage();
            alarmChannelMessage.setAlarmSn(Integer.parseInt(deviceAlarm.getAlarmMethod()));
            alarmChannelMessage.setAlarmDescription(deviceAlarm.getAlarmDescription());
            alarmChannelMessage.setGbId(channelId);
            redisCatchStorage.sendAlarmMsg(alarmChannelMessage);
            return;
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java
New file
@@ -0,0 +1,93 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler;
import com.genersoft.iot.vmp.service.IDeviceService;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import com.genersoft.iot.vmp.utils.DateUtil;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.header.ViaHeader;
import javax.sip.message.Response;
import java.text.ParseException;
/**
 * 状态信息(心跳)报送
 */
@Component
public class KeepaliveNotifyMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler {
    private Logger logger = LoggerFactory.getLogger(KeepaliveNotifyMessageHandler.class);
    private final static String cmdType = "Keepalive";
    @Autowired
    private NotifyMessageHandler notifyMessageHandler;
    @Autowired
    private IDeviceService deviceService;
    @Override
    public void afterPropertiesSet() throws Exception {
        notifyMessageHandler.addHandler(cmdType, this);
    }
    @Override
    public void handForDevice(RequestEvent evt, Device device, Element element) {
        if (device == null) {
            // 未注册的设备不做处理
            return;
        }
        // 回复200 OK
        try {
            responseAck((SIPRequest) evt.getRequest(), Response.OK);
        } catch (SipException | InvalidArgumentException | ParseException e) {
            logger.error("[命令发送失败] 国标级联 心跳回复: {}", e.getMessage());
        }
        // 判断RPort是否改变,改变则说明路由nat信息变化,修改设备信息
        // 获取到通信地址等信息
        ViaHeader viaHeader = (ViaHeader) evt.getRequest().getHeader(ViaHeader.NAME);
        String received = viaHeader.getReceived();
        int rPort = viaHeader.getRPort();
        // 解析本地地址替代
        if (ObjectUtils.isEmpty(received) || rPort == -1) {
            received = viaHeader.getHost();
            rPort = viaHeader.getPort();
        }
        if (device.getPort() != rPort) {
            device.setPort(rPort);
            device.setHostAddress(received.concat(":").concat(String.valueOf(rPort)));
        }
        device.setKeepaliveTime(DateUtil.getNow());
        if (device.getOnline() == 1) {
            deviceService.updateDevice(device);
        }else {
            // 对于已经离线的设备判断他的注册是否已经过期
            if (!deviceService.expire(device)){
                deviceService.online(device);
            }
        }
    }
    @Override
    public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element element) {
        // 不会收到上级平台的心跳信息
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MediaStatusNotifyMessageHandler.java
New file
@@ -0,0 +1,119 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd;
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.bean.SendRtpItem;
import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction;
import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.header.CallIdHeader;
import javax.sip.message.Response;
import java.text.ParseException;
import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText;
/**
 * 媒体通知
 */
@Component
public class MediaStatusNotifyMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler {
    private Logger logger = LoggerFactory.getLogger(MediaStatusNotifyMessageHandler.class);
    private final String cmdType = "MediaStatus";
    @Autowired
    private NotifyMessageHandler notifyMessageHandler;
    @Autowired
    private SIPCommander cmder;
    @Autowired
    private SIPCommanderFroPlatform sipCommanderFroPlatform;
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Autowired
    private IVideoManagerStorage storage;
    @Autowired
    private VideoStreamSessionManager sessionManager;
    @Override
    public void afterPropertiesSet() throws Exception {
        notifyMessageHandler.addHandler(cmdType, this);
    }
    @Override
    public void handForDevice(RequestEvent evt, Device device, Element rootElement) {
        // 回复200 OK
        try {
             responseAck((SIPRequest) evt.getRequest(), Response.OK);
        } catch (SipException | InvalidArgumentException | ParseException e) {
            logger.error("[命令发送失败] 国标级联 录像流推送完毕,回复200OK: {}", e.getMessage());
        }
        CallIdHeader callIdHeader = (CallIdHeader)evt.getRequest().getHeader(CallIdHeader.NAME);
        String NotifyType =getText(rootElement, "NotifyType");
        if ("121".equals(NotifyType)){
            logger.info("[录像流]推送完毕,收到关流通知");
            // 查询是设备
            StreamInfo streamInfo = redisCatchStorage.queryDownload(null, null, null, callIdHeader.getCallId());
            if (streamInfo != null) {
                // 设置进度100%
                streamInfo.setProgress(1);
                redisCatchStorage.startDownload(streamInfo, callIdHeader.getCallId());
            }
            // 先从会话内查找
            SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransaction(null, null, callIdHeader.getCallId(), null);
            if (ssrcTransaction != null) { // 兼容海康 媒体通知 消息from字段不是设备ID的问题
                try {
                    cmder.streamByeCmd(device, ssrcTransaction.getChannelId(), null, callIdHeader.getCallId());
                } catch (InvalidArgumentException | ParseException | SsrcTransactionNotFoundException | SipException e) {
                    logger.error("[录像流]推送完毕,收到关流通知, 发送BYE失败 {}", e.getMessage());
                }
                // 如果级联播放,需要给上级发送此通知 TODO 多个上级同时观看一个下级 可能存在停错的问题,需要将点播CallId进行上下级绑定
                SendRtpItem sendRtpItem =  redisCatchStorage.querySendRTPServer(null, ssrcTransaction.getChannelId(), null, null);
                if (sendRtpItem != null) {
                    ParentPlatform parentPlatform = storage.queryParentPlatByServerGBId(sendRtpItem.getPlatformId());
                    if (parentPlatform == null) {
                        logger.warn("[级联消息发送]:发送MediaStatus发现上级平台{}不存在", sendRtpItem.getPlatformId());
                        return;
                    }
                    try {
                        sipCommanderFroPlatform.sendMediaStatusNotify(parentPlatform, sendRtpItem);
                    } catch (SipException | InvalidArgumentException | ParseException e) {
                        logger.error("[命令发送失败] 国标级联 录像播放完毕: {}", e.getMessage());
                    }
                }
            }
        }
    }
    @Override
    public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element element) {
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MobilePositionNotifyMessageHandler.java
New file
@@ -0,0 +1,175 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd;
import com.alibaba.fastjson.JSONObject;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler;
import com.genersoft.iot.vmp.gb28181.utils.NumericUtil;
import com.genersoft.iot.vmp.service.IDeviceChannelService;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import com.genersoft.iot.vmp.utils.DateUtil;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.message.Response;
import java.text.ParseException;
import java.util.concurrent.ConcurrentLinkedQueue;
import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText;
/**
 * 移动设备位置数据通知,设备主动发起,不需要上级订阅
 */
@Component
public class MobilePositionNotifyMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler {
    private Logger logger = LoggerFactory.getLogger(MobilePositionNotifyMessageHandler.class);
    private final String cmdType = "MobilePosition";
    @Autowired
    private NotifyMessageHandler notifyMessageHandler;
    @Autowired
    private UserSetting userSetting;
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Autowired
    private IDeviceChannelService deviceChannelService;
    private boolean taskQueueHandlerRun = false;
    private ConcurrentLinkedQueue<SipMsgInfo> taskQueue = new ConcurrentLinkedQueue<>();
    @Qualifier("taskExecutor")
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;
    @Override
    public void afterPropertiesSet() throws Exception {
        notifyMessageHandler.addHandler(cmdType, this);
    }
    @Override
    public void handForDevice(RequestEvent evt, Device device, Element rootElement) {
        taskQueue.offer(new SipMsgInfo(evt, device, rootElement));
        if (!taskQueueHandlerRun) {
            taskQueueHandlerRun = true;
            taskExecutor.execute(() -> {
                while (!taskQueue.isEmpty()) {
                    SipMsgInfo sipMsgInfo = taskQueue.poll();
                    try {
                        Element rootElementAfterCharset = getRootElement(sipMsgInfo.getEvt(), sipMsgInfo.getDevice().getCharset());
                        if (rootElementAfterCharset == null) {
                            try {
                                logger.warn("[ 移动设备位置数据通知 ] content cannot be null, {}", sipMsgInfo.getEvt().getRequest());
                                responseAck((SIPRequest) sipMsgInfo.getEvt().getRequest(), Response.BAD_REQUEST);
                            } catch (SipException | InvalidArgumentException | ParseException e) {
                                logger.error("[命令发送失败] 移动设备位置数据通知 内容为空: {}", e.getMessage());
                            }
                            continue;
                        }
                        MobilePosition mobilePosition = new MobilePosition();
                        mobilePosition.setCreateTime(DateUtil.getNow());
                        if (!ObjectUtils.isEmpty(sipMsgInfo.getDevice().getName())) {
                            mobilePosition.setDeviceName(sipMsgInfo.getDevice().getName());
                        }
                        mobilePosition.setDeviceId(sipMsgInfo.getDevice().getDeviceId());
                        mobilePosition.setChannelId(getText(rootElementAfterCharset, "DeviceID"));
                        mobilePosition.setTime(getText(rootElementAfterCharset, "Time"));
                        mobilePosition.setLongitude(Double.parseDouble(getText(rootElementAfterCharset, "Longitude")));
                        mobilePosition.setLatitude(Double.parseDouble(getText(rootElementAfterCharset, "Latitude")));
                        if (NumericUtil.isDouble(getText(rootElementAfterCharset, "Speed"))) {
                            mobilePosition.setSpeed(Double.parseDouble(getText(rootElementAfterCharset, "Speed")));
                        } else {
                            mobilePosition.setSpeed(0.0);
                        }
                        if (NumericUtil.isDouble(getText(rootElementAfterCharset, "Direction"))) {
                            mobilePosition.setDirection(Double.parseDouble(getText(rootElementAfterCharset, "Direction")));
                        } else {
                            mobilePosition.setDirection(0.0);
                        }
                        if (NumericUtil.isDouble(getText(rootElementAfterCharset, "Altitude"))) {
                            mobilePosition.setAltitude(Double.parseDouble(getText(rootElementAfterCharset, "Altitude")));
                        } else {
                            mobilePosition.setAltitude(0.0);
                        }
                        mobilePosition.setReportSource("Mobile Position");
                        // 更新device channel 的经纬度
                        DeviceChannel deviceChannel = new DeviceChannel();
                        deviceChannel.setDeviceId(sipMsgInfo.getDevice().getDeviceId());
                        deviceChannel.setChannelId(mobilePosition.getChannelId());
                        deviceChannel.setLongitude(mobilePosition.getLongitude());
                        deviceChannel.setLatitude(mobilePosition.getLatitude());
                        deviceChannel.setGpsTime(mobilePosition.getTime());
                        deviceChannel = deviceChannelService.updateGps(deviceChannel, sipMsgInfo.getDevice());
                        mobilePosition.setLongitudeWgs84(deviceChannel.getLongitudeWgs84());
                        mobilePosition.setLatitudeWgs84(deviceChannel.getLatitudeWgs84());
                        mobilePosition.setLongitudeGcj02(deviceChannel.getLongitudeGcj02());
                        mobilePosition.setLatitudeGcj02(deviceChannel.getLatitudeGcj02());
                        if (userSetting.getSavePositionHistory()) {
                            storager.insertMobilePosition(mobilePosition);
                        }
                        storager.updateChannelPosition(deviceChannel);
                        //回复 200 OK
                        try {
                            responseAck((SIPRequest) sipMsgInfo.getEvt().getRequest(), Response.OK);
                        } catch (SipException | InvalidArgumentException | ParseException e) {
                            logger.error("[命令发送失败] 移动设备位置数据回复200: {}", e.getMessage());
                        }
                        // 发送redis消息。 通知位置信息的变化
                        JSONObject jsonObject = new JSONObject();
                        jsonObject.put("time", mobilePosition.getTime());
                        jsonObject.put("serial", deviceChannel.getDeviceId());
                        jsonObject.put("code", deviceChannel.getChannelId());
                        jsonObject.put("longitude", mobilePosition.getLongitude());
                        jsonObject.put("latitude", mobilePosition.getLatitude());
                        jsonObject.put("altitude", mobilePosition.getAltitude());
                        jsonObject.put("direction", mobilePosition.getDirection());
                        jsonObject.put("speed", mobilePosition.getSpeed());
                        redisCatchStorage.sendMobilePositionMsg(jsonObject);
                    } catch (DocumentException e) {
                        e.printStackTrace();
                    }
                }
                taskQueueHandlerRun = false;
            });
        }
    }
    @Override
    public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element element) {
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/QueryMessageHandler.java
New file
@@ -0,0 +1,25 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageHandlerAbstract;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageRequestProcessor;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
 * 命令类型: 查询指令
 * 命令类型: 设备状态, 设备目录信息, 设备信息, 文件目录检索(TODO), 报警(TODO), 设备配置(TODO), 设备预置位(TODO), 移动设备位置数据(TODO)
 */
@Component
public class QueryMessageHandler extends MessageHandlerAbstract implements InitializingBean  {
    private final String messageType = "Query";
    @Autowired
    private MessageRequestProcessor messageRequestProcessor;
    @Override
    public void afterPropertiesSet() throws Exception {
        messageRequestProcessor.addHandler(messageType, this);
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/AlarmQueryMessageHandler.java
New file
@@ -0,0 +1,68 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.QueryMessageHandler;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.message.Response;
import java.text.ParseException;
@Component
public class AlarmQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler {
    private Logger logger = LoggerFactory.getLogger(AlarmQueryMessageHandler.class);
    private final String cmdType = "Alarm";
    @Autowired
    private QueryMessageHandler queryMessageHandler;
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private SIPCommanderFroPlatform cmderFroPlatform;
    @Autowired
    private SipConfig config;
    @Autowired
    private EventPublisher publisher;
    @Override
    public void afterPropertiesSet() throws Exception {
        queryMessageHandler.addHandler(cmdType, this);
    }
    @Override
    public void handForDevice(RequestEvent evt, Device device, Element element) {
    }
    @Override
    public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element rootElement) {
        logger.info("不支持alarm查询");
        try {
             responseAck((SIPRequest) evt.getRequest(), Response.NOT_FOUND, "not support alarm query");
        } catch (SipException | InvalidArgumentException | ParseException e) {
            logger.error("[命令发送失败] 国标级联 alarm查询回复200OK: {}", e.getMessage());
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/CatalogQueryMessageHandler.java
New file
@@ -0,0 +1,130 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.QueryMessageHandler;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.header.FromHeader;
import javax.sip.message.Response;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
@Component
public class CatalogQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler {
    private Logger logger = LoggerFactory.getLogger(CatalogQueryMessageHandler.class);
    private final String cmdType = "Catalog";
    @Autowired
    private QueryMessageHandler queryMessageHandler;
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private SIPCommanderFroPlatform cmderFroPlatform;
    @Autowired
    private SipConfig config;
    @Autowired
    private EventPublisher publisher;
    @Autowired
    private IVideoManagerStorage storage;
    @Override
    public void afterPropertiesSet() throws Exception {
        queryMessageHandler.addHandler(cmdType, this);
    }
    @Override
    public void handForDevice(RequestEvent evt, Device device, Element element) {
    }
    @Override
    public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element rootElement) {
        String key = DeferredResultHolder.CALLBACK_CMD_CATALOG + parentPlatform.getServerGBId();
        FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME);
        try {
            // 回复200 OK
             responseAck((SIPRequest) evt.getRequest(), Response.OK);
        } catch (SipException | InvalidArgumentException | ParseException e) {
            logger.error("[命令发送失败] 国标级联 目录查询回复200OK: {}", e.getMessage());
        }
        Element snElement = rootElement.element("SN");
        String sn = snElement.getText();
        // 准备回复通道信息
        List<DeviceChannel> deviceChannelInPlatforms = storager.queryChannelWithCatalog(parentPlatform.getServerGBId());
        // 查询关联的直播通道
        List<DeviceChannel> gbStreams = storager.queryGbStreamListInPlatform(parentPlatform.getServerGBId());
        // 回复目录信息
        List<DeviceChannel> catalogs =  storager.queryCatalogInPlatform(parentPlatform.getServerGBId());
        List<DeviceChannel> allChannels = new ArrayList<>();
        // 回复平台
//            DeviceChannel deviceChannel = getChannelForPlatform(parentPlatform);
//            allChannels.add(deviceChannel);
        // 回复目录
        if (catalogs.size() > 0) {
            allChannels.addAll(catalogs);
        }
        // 回复级联的通道
        if (deviceChannelInPlatforms.size() > 0) {
            allChannels.addAll(deviceChannelInPlatforms);
        }
        // 回复直播的通道
        if (gbStreams.size() > 0) {
            allChannels.addAll(gbStreams);
        }
        try {
            if (allChannels.size() > 0) {
                cmderFroPlatform.catalogQuery(allChannels, parentPlatform, sn, fromHeader.getTag());
            }else {
                // 回复无通道
                cmderFroPlatform.catalogQuery(null, parentPlatform, sn, fromHeader.getTag(), 0);
            }
        } catch (SipException | InvalidArgumentException | ParseException e) {
            logger.error("[命令发送失败] 国标级联 目录查询回复: {}", e.getMessage());
        }
    }
    private DeviceChannel getChannelForPlatform(ParentPlatform platform) {
        DeviceChannel deviceChannel = new DeviceChannel();
        deviceChannel.setChannelId(platform.getDeviceGBId());
        deviceChannel.setName(platform.getName());
        deviceChannel.setManufacture("wvp-pro");
        deviceChannel.setOwner("wvp-pro");
        deviceChannel.setCivilCode(platform.getAdministrativeDivision());
        deviceChannel.setAddress("wvp-pro");
        deviceChannel.setRegisterWay(0);
        deviceChannel.setSecrecy("0");
        return deviceChannel;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceInfoQueryMessageHandler.java
New file
@@ -0,0 +1,63 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.QueryMessageHandler;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.header.FromHeader;
import javax.sip.message.Response;
import java.text.ParseException;
@Component
public class DeviceInfoQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler {
    private Logger logger = LoggerFactory.getLogger(DeviceInfoQueryMessageHandler.class);
    private final String cmdType = "DeviceInfo";
    @Autowired
    private QueryMessageHandler queryMessageHandler;
    @Autowired
    private SIPCommanderFroPlatform cmderFroPlatform;
    @Override
    public void afterPropertiesSet() throws Exception {
        queryMessageHandler.addHandler(cmdType, this);
    }
    @Override
    public void handForDevice(RequestEvent evt, Device device, Element rootElement) {
    }
    @Override
    public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element rootElement) {
        logger.info("[DeviceInfo查询]消息");
        FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME);
        try {
            // 回复200 OK
            responseAck((SIPRequest) evt.getRequest(), Response.OK);
        } catch (SipException | InvalidArgumentException | ParseException e) {
            logger.error("[命令发送失败] DeviceInfo查询回复: {}", e.getMessage());
        }
        String sn = rootElement.element("SN").getText();
        try {
            cmderFroPlatform.deviceInfoResponse(parentPlatform, sn, fromHeader.getTag());
        } catch (SipException | InvalidArgumentException | ParseException e) {
            logger.error("[命令发送失败] 国标级联 DeviceInfo查询回复: {}", e.getMessage());
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceStatusQueryMessageHandler.java
New file
@@ -0,0 +1,76 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.QueryMessageHandler;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.header.FromHeader;
import javax.sip.message.Response;
import java.text.ParseException;
@Component
public class DeviceStatusQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler {
    private Logger logger = LoggerFactory.getLogger(DeviceStatusQueryMessageHandler.class);
    private final String cmdType = "DeviceStatus";
    @Autowired
    private QueryMessageHandler queryMessageHandler;
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private SIPCommanderFroPlatform cmderFroPlatform;
    @Autowired
    private SipConfig config;
    @Autowired
    private EventPublisher publisher;
    @Override
    public void afterPropertiesSet() throws Exception {
        queryMessageHandler.addHandler(cmdType, this);
    }
    @Override
    public void handForDevice(RequestEvent evt, Device device, Element element) {
    }
    @Override
    public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element rootElement) {
        logger.info("接收到DeviceStatus查询消息");
        FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME);
        // 回复200 OK
        try {
             responseAck((SIPRequest) evt.getRequest(), Response.OK);
        } catch (SipException | InvalidArgumentException | ParseException e) {
            logger.error("[命令发送失败] 国标级联 DeviceStatus查询回复200OK: {}", e.getMessage());
        }
        String sn = rootElement.element("SN").getText();
        try {
            cmderFroPlatform.deviceStatusResponse(parentPlatform, sn, fromHeader.getTag());
        } catch (SipException | InvalidArgumentException | ParseException e) {
            logger.error("[命令发送失败] 国标级联 DeviceStatus查询回复: {}", e.getMessage());
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java
New file
@@ -0,0 +1,146 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
import com.genersoft.iot.vmp.gb28181.event.record.RecordEndEventListener;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.QueryMessageHandler;
import com.genersoft.iot.vmp.utils.DateUtil;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import com.genersoft.iot.vmp.storager.dao.dto.ChannelSourceInfo;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.message.Response;
import java.text.ParseException;
import java.util.List;
@Component
public class RecordInfoQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler {
    private Logger logger = LoggerFactory.getLogger(RecordInfoQueryMessageHandler.class);
    private final String cmdType = "RecordInfo";
    @Autowired
    private QueryMessageHandler queryMessageHandler;
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private SIPCommanderFroPlatform cmderFroPlatform;
    @Autowired
    private SIPCommander commander;
    @Autowired
    private RecordEndEventListener recordEndEventListener;
    @Autowired
    private SipConfig config;
    @Autowired
    private EventPublisher publisher;
    @Override
    public void afterPropertiesSet() throws Exception {
        queryMessageHandler.addHandler(cmdType, this);
    }
    @Override
    public void handForDevice(RequestEvent evt, Device device, Element element) {
    }
    @Override
    public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element rootElement) {
        SIPRequest request = (SIPRequest) evt.getRequest();
        Element snElement = rootElement.element("SN");
        int sn = Integer.parseInt(snElement.getText());
        Element deviceIDElement = rootElement.element("DeviceID");
        String channelId = deviceIDElement.getText();
        Element startTimeElement = rootElement.element("StartTime");
        String startTime = null;
        if (startTimeElement != null) {
            startTime = startTimeElement.getText();
        }
        Element endTimeElement = rootElement.element("EndTime");
        String endTime = null;
        if (endTimeElement != null) {
            endTime = endTimeElement.getText();
        }
        Element secrecyElement = rootElement.element("Secrecy");
        int secrecy = 0;
        if (secrecyElement != null) {
            secrecy = Integer.parseInt(secrecyElement.getText().trim());
        }
        String type = "all";
        Element typeElement = rootElement.element("Type");
        if (typeElement != null) {
            type =  typeElement.getText();
        }
        // 确认是直播还是国标, 国标直接请求下级,直播请求录像管理服务
        List<ChannelSourceInfo> channelSources = storager.getChannelSource(parentPlatform.getServerGBId(), channelId);
        if (channelSources.get(0).getCount() > 0) { // 国标
            // 向国标设备请求录像数据
            Device device = storager.queryVideoDeviceByPlatformIdAndChannelId(parentPlatform.getServerGBId(), channelId);
            DeviceChannel deviceChannel = storager.queryChannelInParentPlatform(parentPlatform.getServerGBId(), channelId);
            // 接收录像数据
            recordEndEventListener.addEndEventHandler(deviceChannel.getDeviceId(), channelId, (recordInfo)->{
                try {
                    cmderFroPlatform.recordInfo(deviceChannel, parentPlatform, request.getFromTag(), recordInfo);
                } catch (SipException | InvalidArgumentException | ParseException e) {
                    logger.error("[命令发送失败] 国标级联 回复录像数据: {}", e.getMessage());
                }
            });
            try {
                commander.recordInfoQuery(device, channelId, DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(startTime),
                        DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(endTime), sn, secrecy, type, (eventResult -> {
                            // 回复200 OK
                            try {
                                responseAck(request, Response.OK);
                            } catch (SipException | InvalidArgumentException | ParseException e) {
                                logger.error("[命令发送失败] 录像查询回复: {}", e.getMessage());
                            }
                        }),(eventResult -> {
                            // 查询失败
                            try {
                                responseAck(request, eventResult.statusCode, eventResult.msg);
                            } catch (SipException | InvalidArgumentException | ParseException e) {
                                logger.error("[命令发送失败] 录像查询回复: {}", e.getMessage());
                            }
                        }));
            } catch (InvalidArgumentException | ParseException | SipException e) {
                logger.error("[命令发送失败] 录像查询: {}", e.getMessage());
            }
        }else if (channelSources.get(1).getCount() > 0) { // 直播流
            // TODO
            try {
                responseAck(request, Response.NOT_IMPLEMENTED); // 回复未实现
            } catch (SipException | InvalidArgumentException | ParseException e) {
                logger.error("[命令发送失败] 录像查询: {}", e.getMessage());
            }
        }else { // 错误的请求
            try {
                responseAck(request, Response.BAD_REQUEST);
            } catch (SipException | InvalidArgumentException | ParseException e) {
                logger.error("[命令发送失败] 录像查询: {}", e.getMessage());
            }
        }
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/ResponseMessageHandler.java
New file
@@ -0,0 +1,25 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageHandlerAbstract;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageRequestProcessor;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
 * 命令类型: 请求动作的应答
 * 命令类型: 设备控制, 报警通知, 设备目录信息查询, 目录信息查询, 目录收到, 设备信息查询, 设备状态信息查询 ......
 */
@Component
public class ResponseMessageHandler extends MessageHandlerAbstract implements InitializingBean  {
    private final String messageType = "Response";
    @Autowired
    private MessageRequestProcessor messageRequestProcessor;
    @Override
    public void afterPropertiesSet() throws Exception {
        messageRequestProcessor.addHandler(messageType, this);
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/AlarmResponseMessageHandler.java
New file
@@ -0,0 +1,58 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd;
import com.alibaba.fastjson.JSONObject;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler;
import com.genersoft.iot.vmp.gb28181.utils.XmlUtil;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sip.RequestEvent;
@Component
public class AlarmResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler {
    private Logger logger = LoggerFactory.getLogger(AlarmResponseMessageHandler.class);
    private final String cmdType = "Alarm";
    @Autowired
    private ResponseMessageHandler responseMessageHandler;
    @Autowired
    private DeferredResultHolder deferredResultHolder;
    @Override
    public void afterPropertiesSet() throws Exception {
        responseMessageHandler.addHandler(cmdType, this);
    }
    @Override
    public void handForDevice(RequestEvent evt, Device device, Element rootElement) {
        Element deviceIdElement = rootElement.element("DeviceID");
        String channelId = deviceIdElement.getText().toString();
        String key = DeferredResultHolder.CALLBACK_CMD_ALARM + device.getDeviceId() + channelId;
        JSONObject json = new JSONObject();
        XmlUtil.node2Json(rootElement, json);
        if (logger.isDebugEnabled()) {
            logger.debug(json.toJSONString());
        }
        RequestMessage msg = new RequestMessage();
        msg.setKey(key);
        msg.setData(json);
        deferredResultHolder.invokeAllResult(msg);
    }
    @Override
    public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element element) {
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/BroadcastResponseMessageHandler.java
New file
@@ -0,0 +1,73 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd;
import com.alibaba.fastjson.JSONObject;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler;
import com.genersoft.iot.vmp.gb28181.utils.XmlUtil;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.message.Response;
import java.text.ParseException;
import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText;
@Component
public class BroadcastResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler {
    private Logger logger = LoggerFactory.getLogger(BroadcastResponseMessageHandler.class);
    private final String cmdType = "Broadcast";
    @Autowired
    private ResponseMessageHandler responseMessageHandler;
    @Autowired
    private DeferredResultHolder deferredResultHolder;
    @Override
    public void afterPropertiesSet() throws Exception {
        responseMessageHandler.addHandler(cmdType, this);
    }
    @Override
    public void handForDevice(RequestEvent evt, Device device, Element rootElement) {
        try {
            String channelId = getText(rootElement, "DeviceID");
            String key = DeferredResultHolder.CALLBACK_CMD_BROADCAST + device.getDeviceId() + channelId;
            // 回复200 OK
            responseAck((SIPRequest) evt.getRequest(), Response.OK);
            // 此处是对本平台发出Broadcast指令的应答
            JSONObject json = new JSONObject();
            XmlUtil.node2Json(rootElement, json);
            if (logger.isDebugEnabled()) {
                logger.debug(json.toJSONString());
            }
            RequestMessage msg = new RequestMessage();
            msg.setKey(key);
            msg.setData(json);
            deferredResultHolder.invokeAllResult(msg);
        } catch (ParseException | SipException | InvalidArgumentException e) {
            logger.error("[命令发送失败] 国标级联 语音喊话: {}", e.getMessage());
        }
    }
    @Override
    public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element element) {
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/CatalogResponseMessageHandler.java
New file
@@ -0,0 +1,177 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
import com.genersoft.iot.vmp.gb28181.session.CatalogDataCatch;
import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler;
import com.genersoft.iot.vmp.gb28181.utils.Coordtransform;
import com.genersoft.iot.vmp.gb28181.utils.NumericUtil;
import com.genersoft.iot.vmp.gb28181.utils.XmlUtil;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.message.Response;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
 * 目录查询的回复
 */
@Component
public class CatalogResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler {
    private Logger logger = LoggerFactory.getLogger(CatalogResponseMessageHandler.class);
    private final String cmdType = "Catalog";
    private boolean taskQueueHandlerRun = false;
    @Autowired
    private ResponseMessageHandler responseMessageHandler;
    private ConcurrentLinkedQueue<HandlerCatchData> taskQueue = new ConcurrentLinkedQueue<>();
    @Autowired
    private IVideoManagerStorage storager;
    @Autowired
    private CatalogDataCatch catalogDataCatch;
    @Qualifier("taskExecutor")
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;
    @Override
    public void afterPropertiesSet() throws Exception {
        responseMessageHandler.addHandler(cmdType, this);
    }
    @Override
    public void handForDevice(RequestEvent evt, Device device, Element element) {
        taskQueue.offer(new HandlerCatchData(evt, device, element));
        // 回复200 OK
        try {
            responseAck((SIPRequest) evt.getRequest(), Response.OK);
        } catch (SipException | InvalidArgumentException | ParseException e) {
            logger.error("[命令发送失败] 目录查询回复: {}", e.getMessage());
        }
        if (!taskQueueHandlerRun) {
            taskQueueHandlerRun = true;
            taskExecutor.execute(() -> {
                while (!taskQueue.isEmpty()) {
                    HandlerCatchData take = taskQueue.poll();
                    Element rootElement = null;
                    try {
                        rootElement = getRootElement(take.getEvt(), take.getDevice().getCharset());
                    } catch (DocumentException e) {
                        logger.error("[xml解析] 失败: ", e);
                        continue;
                    }
                    if (rootElement == null) {
                        logger.warn("[ 收到通道 ] content cannot be null, {}", evt.getRequest());
                        continue;
                    }
                    Element deviceListElement = rootElement.element("DeviceList");
                    Element sumNumElement = rootElement.element("SumNum");
                    Element snElement = rootElement.element("SN");
                    int sumNum = Integer.parseInt(sumNumElement.getText());
                    if (sumNum == 0) {
                        logger.info("[收到通道]设备:{}的: 0个", take.getDevice().getDeviceId());
                        // 数据已经完整接收
                        storager.cleanChannelsForDevice(take.getDevice().getDeviceId());
                        catalogDataCatch.setChannelSyncEnd(take.getDevice().getDeviceId(), null);
                    } else {
                        Iterator<Element> deviceListIterator = deviceListElement.elementIterator();
                        if (deviceListIterator != null) {
                            List<DeviceChannel> channelList = new ArrayList<>();
                            // 遍历DeviceList
                            while (deviceListIterator.hasNext()) {
                                Element itemDevice = deviceListIterator.next();
                                Element channelDeviceElement = itemDevice.element("DeviceID");
                                if (channelDeviceElement == null) {
                                    continue;
                                }
                                DeviceChannel deviceChannel = XmlUtil.channelContentHander(itemDevice, device, null);
                                deviceChannel.setDeviceId(take.getDevice().getDeviceId());
                                //组装坐标
                                String position = deviceChannel.getLongitude() + " " + deviceChannel.getLatitude();
                                deviceChannel.setCoordinate("'POINT("+position+")'");
                                channelList.add(deviceChannel);
                            }
                            int sn = Integer.parseInt(snElement.getText());
                            catalogDataCatch.put(take.getDevice().getDeviceId(), sn, sumNum, take.getDevice(), channelList);
                            logger.info("[收到通道]设备: {} -> {}个,{}/{}", take.getDevice().getDeviceId(), channelList.size(), catalogDataCatch.get(take.getDevice().getDeviceId()) == null ? 0 : catalogDataCatch.get(take.getDevice().getDeviceId()).size(), sumNum);
                            if (catalogDataCatch.get(take.getDevice().getDeviceId()).size() == sumNum) {
                                // 数据已经完整接收, 此时可能存在某个设备离线变上线的情况,但是考虑到性能,此处不做处理,
                                // 目前支持设备通道上线通知时和设备上线时向上级通知
                                boolean resetChannelsResult = storager.resetChannels(take.getDevice().getDeviceId(), catalogDataCatch.get(take.getDevice().getDeviceId()));
                                if (!resetChannelsResult) {
                                    String errorMsg = "接收成功,写入失败,共" + sumNum + "条,已接收" + catalogDataCatch.get(take.getDevice().getDeviceId()).size() + "条";
                                    catalogDataCatch.setChannelSyncEnd(take.getDevice().getDeviceId(), errorMsg);
                                } else {
                                    catalogDataCatch.setChannelSyncEnd(take.getDevice().getDeviceId(), null);
                                }
                            }
                        }
                    }
                }
                taskQueueHandlerRun = false;
            });
        }
    }
    @Override
    public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element rootElement) {
    }
    public SyncStatus getChannelSyncProgress(String deviceId) {
        if (catalogDataCatch.get(deviceId) == null) {
            return null;
        } else {
            return catalogDataCatch.getSyncStatus(deviceId);
        }
    }
    public boolean isSyncRunning(String deviceId) {
        if (catalogDataCatch.get(deviceId) == null) {
            return false;
        } else {
            return catalogDataCatch.isSyncRunning(deviceId);
        }
    }
    public void setChannelSyncReady(Device device, int sn) {
        catalogDataCatch.addReady(device, sn);
    }
    public void setChannelSyncEnd(String deviceId, String errorMsg) {
        catalogDataCatch.setChannelSyncEnd(deviceId, errorMsg);
    }
}
Diff truncated after the above file
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/ConfigDownloadResponseMessageHandler.java src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceConfigResponseMessageHandler.java src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceControlResponseMessageHandler.java src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceInfoResponseMessageHandler.java src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceStatusResponseMessageHandler.java src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/MobilePositionResponseMessageHandler.java src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/PresetQueryResponseMessageHandler.java src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/ISIPResponseProcessor.java src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/SIPResponseProcessorAbstract.java src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/ByeResponseProcessor.java src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/CancelResponseProcessor.java src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/RegisterResponseProcessor.java src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/timeout/ITimeoutProcessor.java src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/timeout/impl/TimeoutProcessorImpl.java src/main/java/com/genersoft/iot/vmp/gb28181/utils/Coordtransform.java src/main/java/com/genersoft/iot/vmp/gb28181/utils/NumericUtil.java src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java src/main/java/com/genersoft/iot/vmp/gb28181/utils/XmlUtil.java src/main/java/com/genersoft/iot/vmp/gb28181/vo/DeviceChannelVO.java src/main/java/com/genersoft/iot/vmp/media/zlm/AssistRESTfulUtils.java src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaListManager.java src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRunner.java src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMServerConfig.java src/main/java/com/genersoft/iot/vmp/media/zlm/ZlmHttpHookSubscribe.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ChannelOnlineEvent.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookParam.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeFactory.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeForServerStarted.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeForStreamChange.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookType.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/IHookSubscribe.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaItem.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaServerItem.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/OnPlayHookParam.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/OnPublishHookParam.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/OriginType.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamAuthorityInfo.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamProxyItem.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamPushItem.java src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMRunInfo.java src/main/java/com/genersoft/iot/vmp/media/zlm/event/ZLMEventAbstract.java src/main/java/com/genersoft/iot/vmp/media/zlm/event/ZLMOfflineEvent.java src/main/java/com/genersoft/iot/vmp/media/zlm/event/ZLMOnlineEvent.java src/main/java/com/genersoft/iot/vmp/media/zlm/event/ZLMStatusEventListener.java src/main/java/com/genersoft/iot/vmp/netty/business/entity/TalkBackEquipment.java src/main/java/com/genersoft/iot/vmp/netty/business/entity/TalkBackEquipmentRecord.java src/main/java/com/genersoft/iot/vmp/netty/business/mapper/TalkBackEquipmentMapper.java src/main/java/com/genersoft/iot/vmp/netty/business/mapper/TalkBackEquipmentMapper.xml src/main/java/com/genersoft/iot/vmp/netty/business/service/TalkBackEquipmentService.java src/main/java/com/genersoft/iot/vmp/netty/business/service/impl/TalkBackEquipmentServiceImpl.java src/main/java/com/genersoft/iot/vmp/netty/config/MyDecoder.java src/main/java/com/genersoft/iot/vmp/netty/config/SysConfig.java src/main/java/com/genersoft/iot/vmp/netty/event/StartupEvent.java src/main/java/com/genersoft/iot/vmp/netty/handle/UdpServerHandler.java src/main/java/com/genersoft/iot/vmp/netty/server/UdpServer.java src/main/java/com/genersoft/iot/vmp/netty/util/Hex.java src/main/java/com/genersoft/iot/vmp/netty/util/HexStringTool.java src/main/java/com/genersoft/iot/vmp/netty/util/Utils.java src/main/java/com/genersoft/iot/vmp/policeBodyCamera/entity/DeviceChannelPoliceCamera.java src/main/java/com/genersoft/iot/vmp/policeBodyCamera/mapper/DeviceChannelPoliceCameraMapper.java src/main/java/com/genersoft/iot/vmp/policeBodyCamera/service/DeviceChannelPoliceCameraService.java src/main/java/com/genersoft/iot/vmp/policeBodyCamera/service/impl/DeviceChannelPoliceCameraServiceImpl.java src/main/java/com/genersoft/iot/vmp/policeBodyCamera/vo/DeviceChannelPoliceCameraVO.java src/main/java/com/genersoft/iot/vmp/service/IDeviceAlarmService.java src/main/java/com/genersoft/iot/vmp/service/IDeviceChannelService.java src/main/java/com/genersoft/iot/vmp/service/IDeviceService.java src/main/java/com/genersoft/iot/vmp/service/IGbStreamService.java src/main/java/com/genersoft/iot/vmp/service/ILogService.java src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java src/main/java/com/genersoft/iot/vmp/service/IMediaService.java src/main/java/com/genersoft/iot/vmp/service/IPlatformChannelService.java src/main/java/com/genersoft/iot/vmp/service/IPlatformService.java src/main/java/com/genersoft/iot/vmp/service/IPlayService.java src/main/java/com/genersoft/iot/vmp/service/IRecordInfoServer.java src/main/java/com/genersoft/iot/vmp/service/IRoleService.java src/main/java/com/genersoft/iot/vmp/service/IStreamProxyService.java src/main/java/com/genersoft/iot/vmp/service/IStreamPushService.java src/main/java/com/genersoft/iot/vmp/service/IUserService.java src/main/java/com/genersoft/iot/vmp/service/bean/GPSMsgInfo.java src/main/java/com/genersoft/iot/vmp/service/bean/InviteTimeOutCallback.java src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannel.java src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannelResponse.java src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackCallback.java src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackResult.java src/main/java/com/genersoft/iot/vmp/service/bean/PushStreamStatusChangeFromRedisDto.java src/main/java/com/genersoft/iot/vmp/service/bean/RequestPushStreamMsg.java src/main/java/com/genersoft/iot/vmp/service/bean/RequestSendItemMsg.java src/main/java/com/genersoft/iot/vmp/service/bean/ResponseSendItemMsg.java src/main/java/com/genersoft/iot/vmp/service/bean/SSRCInfo.java src/main/java/com/genersoft/iot/vmp/service/bean/StreamPushItemFromRedis.java src/main/java/com/genersoft/iot/vmp/service/bean/ThirdPartyGB.java src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsg.java src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsgCmd.java src/main/java/com/genersoft/iot/vmp/service/impl/DeviceAlarmServiceImpl.java src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java src/main/java/com/genersoft/iot/vmp/service/impl/GbStreamServiceImpl.java src/main/java/com/genersoft/iot/vmp/service/impl/LogServiceImpl.java src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java src/main/java/com/genersoft/iot/vmp/service/impl/PlatformChannelServiceImpl.java src/main/java/com/genersoft/iot/vmp/service/impl/PlatformServiceImpl.java src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java src/main/java/com/genersoft/iot/vmp/service/impl/RecordInfoServerImpl.java src/main/java/com/genersoft/iot/vmp/service/impl/RoleServerImpl.java src/main/java/com/genersoft/iot/vmp/service/impl/StreamProxyServiceImpl.java src/main/java/com/genersoft/iot/vmp/service/impl/StreamPushServiceImpl.java src/main/java/com/genersoft/iot/vmp/service/impl/StreamPushUploadFileHandler.java src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisAlarmMsgListener.java src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGbPlayMsgListener.java src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGpsMsgListener.java src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamResponseListener.java src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamStatusListMsgListener.java src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamStatusMsgListener.java src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisStreamMsgListener.java src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java src/main/java/com/genersoft/iot/vmp/storager/IVideoManagerStorage.java src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceAlarmMapper.java src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMobilePositionMapper.java src/main/java/com/genersoft/iot/vmp/storager/dao/GbStreamMapper.java src/main/java/com/genersoft/iot/vmp/storager/dao/LogMapper.java src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java src/main/java/com/genersoft/iot/vmp/storager/dao/ParentPlatformMapper.java src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformCatalogMapper.java src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformChannelMapper.java src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformGbStreamMapper.java src/main/java/com/genersoft/iot/vmp/storager/dao/RecordInfoDao.java src/main/java/com/genersoft/iot/vmp/storager/dao/RoleMapper.java src/main/java/com/genersoft/iot/vmp/storager/dao/StreamProxyMapper.java src/main/java/com/genersoft/iot/vmp/storager/dao/StreamPushMapper.java src/main/java/com/genersoft/iot/vmp/storager/dao/UserMapper.java src/main/java/com/genersoft/iot/vmp/storager/dao/dto/ChannelSourceInfo.java src/main/java/com/genersoft/iot/vmp/storager/dao/dto/LogDto.java src/main/java/com/genersoft/iot/vmp/storager/dao/dto/PlatformRegisterInfo.java src/main/java/com/genersoft/iot/vmp/storager/dao/dto/RecordInfo.java src/main/java/com/genersoft/iot/vmp/storager/dao/dto/Role.java src/main/java/com/genersoft/iot/vmp/storager/dao/dto/User.java src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java src/main/java/com/genersoft/iot/vmp/utils/CollectionUtil.java src/main/java/com/genersoft/iot/vmp/utils/ConfigConst.java src/main/java/com/genersoft/iot/vmp/utils/Coordtransform.java src/main/java/com/genersoft/iot/vmp/utils/DateUtil.java src/main/java/com/genersoft/iot/vmp/utils/GitUtil.java src/main/java/com/genersoft/iot/vmp/utils/GpsUtil.java src/main/java/com/genersoft/iot/vmp/utils/ObjectUtils.java src/main/java/com/genersoft/iot/vmp/utils/SpringBeanFactory.java src/main/java/com/genersoft/iot/vmp/utils/SystemInfoUtils.java src/main/java/com/genersoft/iot/vmp/utils/node/BaseNode.java src/main/java/com/genersoft/iot/vmp/utils/node/ForestNode.java src/main/java/com/genersoft/iot/vmp/utils/node/ForestNodeManager.java src/main/java/com/genersoft/iot/vmp/utils/node/ForestNodeMerger.java src/main/java/com/genersoft/iot/vmp/utils/node/INode.java src/main/java/com/genersoft/iot/vmp/utils/node/TreeNode.java src/main/java/com/genersoft/iot/vmp/utils/redis/FastJsonRedisSerializer.java src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil.java src/main/java/com/genersoft/iot/vmp/vmanager/bean/BaseTree.java src/main/java/com/genersoft/iot/vmp/vmanager/bean/BatchGBStreamParam.java src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeviceChannelTree.java src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeviceChannelTreeNode.java src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java src/main/java/com/genersoft/iot/vmp/vmanager/bean/PlayTypeEnum.java src/main/java/com/genersoft/iot/vmp/vmanager/bean/StreamPushExcelDto.java src/main/java/com/genersoft/iot/vmp/vmanager/bean/WVPResult.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/MobilePosition/MobilePositionController.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/SseController/SseController.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/alarm/AlarmController.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceConfig.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceControl.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceQuery.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/gbStream/GbStreamController.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/gbStream/bean/GbStreamParam.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/media/MediaController.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/platform/PlatformController.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/platform/bean/ChannelReduce.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/platform/bean/UpdateChannelParam.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/bean/PlayResult.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/playback/PlaybackController.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/ptz/PtzController.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/record/GBRecordController.java src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/vo/PageVO.java src/main/java/com/genersoft/iot/vmp/vmanager/log/LogController.java src/main/java/com/genersoft/iot/vmp/vmanager/record/RecordController.java src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java src/main/java/com/genersoft/iot/vmp/vmanager/streamProxy/StreamProxyController.java src/main/java/com/genersoft/iot/vmp/vmanager/streamPush/StreamPushController.java src/main/java/com/genersoft/iot/vmp/vmanager/user/RoleController.java src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiControlController.java src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiController.java src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiDeviceController.java src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java src/main/java/com/genersoft/iot/vmp/web/gb28181/AuthController.java src/main/resources/all-application.yml src/main/resources/application.yml src/main/resources/banner.txt src/main/resources/logback-spring-local.xml src/main/resources/wvpssl.jks web_src/.babelrc web_src/.editorconfig web_src/.gitignore web_src/.postcssrc.js web_src/README.md web_src/build/build.js web_src/build/check-versions.js web_src/build/logo.png web_src/build/utils.js web_src/build/vue-loader.conf.js web_src/build/webpack.base.conf.js web_src/build/webpack.dev.conf.js web_src/build/webpack.prod.conf.js web_src/config/dev.env.js web_src/config/index.js web_src/config/prod.env.js web_src/index.html web_src/package-lock.json web_src/package.json web_src/src/App.vue web_src/src/assets/icons.png web_src/src/assets/loading.png web_src/src/assets/login-bg.jpg web_src/src/assets/login-cloud.png web_src/src/assets/logo.png web_src/src/assets/play.png web_src/src/assets/zlm-log.png web_src/src/components/CloudRecord.vue web_src/src/components/CloudRecordDetail.vue web_src/src/components/DeviceList.vue web_src/src/components/GeoConvertTools.js web_src/src/components/Login.vue web_src/src/components/MediaServerManger.vue web_src/src/components/ParentPlatformList.vue web_src/src/components/PushVideoList.vue web_src/src/components/StreamProxyList.vue web_src/src/components/UserManager.vue web_src/src/components/channelList.vue web_src/src/components/common/DeviceTree.vue web_src/src/components/common/DeviceTreeForZtree.vue web_src/src/components/common/MapComponent.vue web_src/src/components/common/h265web.vue web_src/src/components/common/jessibuca.vue web_src/src/components/control.vue web_src/src/components/dialog/MediaServerEdit.vue web_src/src/components/dialog/StreamProxyEdit.vue web_src/src/components/dialog/SyncChannelProgress.vue web_src/src/components/dialog/addUser.vue web_src/src/components/dialog/catalogEdit.vue web_src/src/components/dialog/changePassword.vue web_src/src/components/dialog/changePasswordForAdmin.vue web_src/src/components/dialog/changePushKey.vue web_src/src/components/dialog/channelMapInfobox.vue web_src/src/components/dialog/chooseChannel.vue web_src/src/components/dialog/chooseChannelForCatalog.vue web_src/src/components/dialog/chooseChannelForGb.vue web_src/src/components/dialog/chooseChannelForStream.vue web_src/src/components/dialog/deviceEdit.vue web_src/src/components/dialog/devicePlayer.vue web_src/src/components/dialog/easyPlayer.vue web_src/src/components/dialog/getCatalog.vue web_src/src/components/dialog/importChannel.vue web_src/src/components/dialog/importChannelShowErrorData.vue web_src/src/components/dialog/onvifEdit.vue web_src/src/components/dialog/platformEdit.vue web_src/src/components/dialog/pushStreamEdit.vue web_src/src/components/dialog/queryTrace.vue web_src/src/components/dialog/recordDownload.vue web_src/src/components/dialog/rtcPlayer.vue web_src/src/components/live.vue web_src/src/components/map.vue web_src/src/components/service/DeviceService.js web_src/src/components/service/MediaServer.js web_src/src/components/setting/Media.vue web_src/src/components/setting/Sip.vue web_src/src/components/setting/Web.vue web_src/src/layout/UiHeader.vue web_src/src/layout/index.vue web_src/src/main.js web_src/src/router/index.js web_src/static/.gitkeep web_src/static/EasyPlayer.swf web_src/static/css/iconfont.css web_src/static/css/iconfont.woff2 web_src/static/css/login.css web_src/static/favicon.ico web_src/static/fonts/poppins/Poppins-Bold.ttf web_src/static/fonts/poppins/Poppins-Medium.ttf web_src/static/fonts/poppins/Poppins-Regular.ttf web_src/static/fonts/poppins/Poppins-SemiBold.ttf web_src/static/images/arrow.png web_src/static/images/gis/camera-offline.png web_src/static/images/gis/camera.png web_src/static/images/gis/camera1-offline.png web_src/static/images/gis/camera1.png web_src/static/images/gis/camera2-offline.png web_src/static/images/gis/camera2.png web_src/static/images/gis/camera3-offline.png web_src/static/images/gis/camera3.png web_src/static/images/zlm-logo.png web_src/static/js/EasyWasmPlayer.js web_src/static/js/ZLMRTCClient.js web_src/static/js/ZLMRTCClient.js.map web_src/static/js/jessibuca/decoder.js web_src/static/js/jessibuca/decoder.wasm web_src/static/js/jessibuca/jessibuca.d.ts web_src/static/js/jessibuca/jessibuca.js web_src/static/js/mapConfig.js web_src/static/libDecoder.wasm web_src/static/logo.png