| | |
| | | import com.ard.sample.component.websocket.enums.MessageType; |
| | | import com.ard.sample.component.websocket.model.Payload; |
| | | import com.ard.sample.component.websocket.model.WebSocketMessage; |
| | | import com.ard.sample.manage.service.IControlService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Component; |
| | |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.Executors; |
| | | import java.util.concurrent.ScheduledExecutorService; |
| | | import java.util.concurrent.ScheduledFuture; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | /** |
| | |
| | | @Autowired |
| | | private LiveStreamHandler _liveStreamService; |
| | | |
| | | @Autowired |
| | | private IControlService _controlService; |
| | | |
| | | // ====================== 心跳相关常量(可配置化建议放到配置文件) ====================== |
| | | /** 心跳间隔(秒):客户端需每隔此时间发送一次PING消息 */ |
| | | private static final long HEARTBEAT_INTERVAL = 30; |
| | |
| | | private static final long HEARTBEAT_TIMEOUT = 90; |
| | | /** 心跳检测任务间隔(秒):定时扫描超时连接 */ |
| | | private static final long HEARTBEAT_CHECK_INTERVAL = 10; |
| | | /** |
| | | * 停流延迟(秒):页面刷新或网络抖动会先断开旧连接再建立新连接, |
| | | * 断开时立即 live_stop 会让新页面需要重新起流,视频出现空档甚至丢失。 |
| | | * 延迟期间重新订阅则取消停止。 |
| | | */ |
| | | private static final long LIVE_STOP_DELAY_SECONDS = 30; |
| | | |
| | | // ====================== 核心存储(线程安全) ====================== |
| | | /** 用户会话映射:userId -> WebSocketSession */ |
| | |
| | | private final Map<WebSocketSession, Object> sessionLocks = new ConcurrentHashMap<>(); |
| | | /** 用户最后心跳时间:userId -> 最后心跳时间戳(毫秒) */ |
| | | private final Map<String, Long> userLastHeartbeatTime = new ConcurrentHashMap<>(); |
| | | /** 待执行的延迟停流任务:设备SN -> 定时任务 */ |
| | | private final Map<String, ScheduledFuture<?>> pendingLiveStops = new ConcurrentHashMap<>(); |
| | | |
| | | // ====================== 线程池 ====================== |
| | | /** 心跳检测线程池(单线程守护线程) */ |
| | | private final ScheduledExecutorService heartbeatCheckExecutor = Executors.newSingleThreadScheduledExecutor(runnable -> { |
| | | Thread thread = new Thread(runnable, "websocket-heartbeat-check-thread"); |
| | | thread.setDaemon(true); // 守护线程,不阻塞JVM退出 |
| | | return thread; |
| | | }); |
| | | |
| | | /** 延迟停流线程池(单线程守护线程),停流可能走 HTTP,独立线程避免阻塞心跳检测 */ |
| | | private final ScheduledExecutorService liveStopExecutor = Executors.newSingleThreadScheduledExecutor(runnable -> { |
| | | Thread thread = new Thread(runnable, "websocket-live-stop-thread"); |
| | | thread.setDaemon(true); |
| | | return thread; |
| | | }); |
| | | |
| | |
| | | if (isSubscribe) { |
| | | // 订阅操作:不存在则创建,存在则添加用户 |
| | | subscriptions.computeIfAbsent(subscriptionKey, k -> ConcurrentHashMap.newKeySet()).add(userId); |
| | | // 页面刷新会先断连再重连,新订阅到达时取消上一次断连触发的延迟停流 |
| | | if (MessageType.LIVESTREAM.getType().equals(type)) { |
| | | cancelPendingLiveStop(sn); |
| | | } |
| | | // log.info("用户[{}]订阅设备[{}]的[{}]类型消息", userId, sn, type); |
| | | |
| | | // // 直播类型订阅:触发开启直播 |
| | |
| | | subscriberIds.remove(userId); |
| | | log.info("用户[{}]取消订阅设备[{}]的[{}]类型消息", userId, sn, type); |
| | | |
| | | // 订阅组为空时移除,并停止直播(直播类型) |
| | | // 订阅组为空时移除,并延迟停止直播(直播类型)。 |
| | | // 不立即停流:页面刷新会先断连再重连,立即停流会导致视频长时间黑屏。 |
| | | if (subscriberIds.isEmpty()) { |
| | | subscriptions.remove(subscriptionKey); |
| | | if (MessageType.LIVESTREAM.getType().equals(type)) { |
| | | _liveStreamService.StopLiveStreaming(sn); |
| | | log.info("设备[{}]直播流已停止(无订阅用户)", sn); |
| | | scheduleStopLiveStream(sn); |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | // 线程安全发送消息 |
| | | try { |
| | | if(MessageType.LIVESTREAM.getType().equals(type)) { |
| | | // 指定了接收用户时只推给该用户;uId 为空(如后台定时任务起流)时推给全部订阅者 |
| | | if (MessageType.LIVESTREAM.getType().equals(type) && uId != null && !uId.isEmpty()) { |
| | | WebSocketSession liveSession = userSessions.get(uId); |
| | | sendMessage(liveSession, content); |
| | | log.info("单独向用户[{}]推送设备[{}]的[{}]类型消息:{}", uId, sn, type, content); |
| | | if (liveSession != null && liveSession.isOpen()) { |
| | | sendMessage(liveSession, content); |
| | | log.info("单独向用户[{}]推送设备[{}]的[{}]类型消息:{}", uId, sn, type, content); |
| | | } |
| | | return; |
| | | } |
| | | sendMessage(session, content); |
| | |
| | | if (subscriberIds.remove(userId)) { |
| | | log.debug("用户[{}]已移除订阅:{}", userId, entry.getKey()); |
| | | |
| | | // 订阅组为空时移除,并停止直播(直播类型) |
| | | // 订阅组为空时移除,并延迟停止直播(直播类型)。 |
| | | // 页面刷新/短暂断网会重建连接,延迟停流可避免视频空档;期间重新订阅会取消停止。 |
| | | if (subscriberIds.isEmpty()) { |
| | | String subscriptionKey = entry.getKey(); |
| | | if (subscriptionKey.contains(MessageType.LIVESTREAM.getType())) { |
| | | String sn = subscriptionKey.split(":")[0]; |
| | | _liveStreamService.StopLiveStreaming(sn); |
| | | log.info("设备[{}]无订阅用户,已停止直播流", sn); |
| | | scheduleStopLiveStream(sn); |
| | | } |
| | | iterator.remove(); // 安全移除空订阅组 |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 延迟停止直播流。 |
| | | * 页面刷新/网络抖动会先断开旧连接再建立新连接,断开时立即停流会让新页面重新起流, |
| | | * 视频出现空档甚至丢失;这里延迟 LIVE_STOP_DELAY_SECONDS 秒再停,期间有新订阅则取消。 |
| | | */ |
| | | private void scheduleStopLiveStream(String sn) { |
| | | String subscriptionKey = getSubscriptionKey(sn, MessageType.LIVESTREAM.getType()); |
| | | pendingLiveStops.compute(sn, (key, oldFuture) -> { |
| | | if (oldFuture != null && !oldFuture.isDone()) { |
| | | oldFuture.cancel(false); |
| | | } |
| | | return liveStopExecutor.schedule(() -> { |
| | | pendingLiveStops.remove(sn); |
| | | Set<String> subscriberIds = subscriptions.get(subscriptionKey); |
| | | // 延迟期间又有用户订阅(如页面刷新重连),不停止直播 |
| | | if (subscriberIds != null && !subscriberIds.isEmpty()) { |
| | | log.info("设备[{}]延迟停流期间重新订阅,继续直播", sn); |
| | | return; |
| | | } |
| | | _liveStreamService.StopLiveStreaming(sn); |
| | | log.info("设备[{}]无订阅用户(已延迟{}秒),停止直播流", sn, LIVE_STOP_DELAY_SECONDS); |
| | | }, LIVE_STOP_DELAY_SECONDS, TimeUnit.SECONDS); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 取消待执行的延迟停流任务(设备重新被订阅时调用)。 |
| | | */ |
| | | private void cancelPendingLiveStop(String sn) { |
| | | ScheduledFuture<?> future = pendingLiveStops.remove(sn); |
| | | if (future != null && !future.isDone()) { |
| | | future.cancel(false); |
| | | } |
| | | } |
| | | |
| | |
| | | log.error("WebSocket心跳检测线程池关闭被中断", e); |
| | | } |
| | | } |
| | | // 关闭延迟停流线程池 |
| | | if (liveStopExecutor != null && !liveStopExecutor.isShutdown()) { |
| | | liveStopExecutor.shutdownNow(); |
| | | } |
| | | log.info("WebSocket处理器资源已销毁"); |
| | | } |
| | | } |