package com.ard.gb28181.service.impl; import com.ard.common.core.constant.SecurityConstants; import com.ard.common.core.domain.RtpServerParam; import com.ard.common.core.exception.ServiceException; import com.ard.gb28181.api.bean.ErrorCallback; import com.ard.gb28181.api.bean.Preset; import com.ard.gb28181.api.bean.SipTransactionInfo; import com.ard.gb28181.api.domain.Device; import com.ard.gb28181.api.domain.DeviceChannel; import com.ard.gb28181.api.domain.GbCode; import com.ard.gb28181.api.domain.SsrcTransaction; import com.ard.gb28181.api.utils.DateUtil; import com.ard.gb28181.common.ErrorCode; import com.ard.gb28181.config.UserSetting; import com.ard.gb28181.domain.GbChannel; import com.ard.gb28181.domain.GbDevice; import com.ard.gb28181.service.IDeviceService; import com.ard.gb28181.service.IGbChannelService; import com.ard.gb28181.service.IGbDeviceService; import com.ard.gb28181.service.IRedisCatchStorage; import com.ard.gb28181.service.ISIPCommander; import com.ard.gb28181.session.SipInviteSessionManager; import com.ard.gb28181.task.deviceStatus.DeviceStatusTask; import com.ard.gb28181.task.deviceStatus.DeviceStatusTaskRunner; import com.ard.gb28181.task.deviceSubscribe.deviceSubscribe.SubscribeTaskRunner; import com.ard.gb28181.task.deviceSubscribe.deviceSubscribe.impl.SubscribeTaskForCatalog; import com.ard.gb28181.task.deviceSubscribe.deviceSubscribe.impl.SubscribeTaskForMobilPosition; import com.ard.zlm.api.RemoteZlmService; import jakarta.validation.constraints.NotNull; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.sip.InvalidArgumentException; import javax.sip.SipException; import java.text.ParseException; import java.util.List; import java.util.Objects; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; /** * 设备业务 * * @author lin */ @Slf4j @Service @Order(value = 16) public class DeviceServiceImpl implements IDeviceService { @Autowired private IRedisCatchStorage redisCatchStorage; @Autowired private ISIPCommander commander; @Autowired private UserSetting userSetting; @Autowired private DeviceStatusTaskRunner deviceStatusTaskRunner; @Autowired private SubscribeTaskRunner subscribeTaskRunner; @Autowired private SipInviteSessionManager sessionManager; @Autowired private RemoteZlmService remoteZlmService; @Autowired private IGbDeviceService gbDeviceService; @Autowired private IGbChannelService gbChannelService; /** * 查询设备信息 * * @param deviceId 设备编号 * @return 设备信息 */ @Override public Device getDeviceByDeviceId(String deviceId) { return redisCatchStorage.getDevice(deviceId); } /** * 设备上线 * * @param device 设备信息 */ @Override public void online(Device device, SipTransactionInfo sipTransactionInfo) { log.info("[设备上线] deviceId:{}->{}:{}", device.getDeviceId(), device.getIp(), device.getPort()); Device deviceInRedis = redisCatchStorage.getDevice(device.getDeviceId()); String now = DateUtil.getNow(); device.setUpdateTime(now); device.setKeepaliveTime(now); if (device.getHeartBeatCount() == null) { // 读取设备配置, 获取心跳间隔和心跳超时次数, 在次之前暂时设置为默认值 device.setHeartBeatCount(3); device.setHeartBeatInterval(60); device.setPositionCapability(0); } if (sipTransactionInfo != null) { device.setSipTransactionInfo(sipTransactionInfo); } else { if (deviceInRedis != null) { device.setSipTransactionInfo(deviceInRedis.getSipTransactionInfo()); } } // 第一次上线 或则设备之前是离线状态--进行通道同步和设备信息查询 if (deviceInRedis == null) { device.setOnLine(true); device.setCreateTime(now); device.setUpdateTime(now); log.info("[设备上线,首次注册]: {},查询设备信息以及通道信息", device.getDeviceId()); if (device.getStreamMode() == null) { device.setStreamMode("TCP-PASSIVE"); } redisCatchStorage.updateDevice(device); try { commander.deviceInfoQuery(device, null); commander.deviceConfigQuery(device, null, "BasicParam", null); } catch (InvalidArgumentException | SipException | ParseException e) { log.error("[命令发送失败] 查询设备信息: {}", e.getMessage()); } log.info("[设备上线]: {},查询通道信息", device.getDeviceId()); sync(device); } else { device.setServerId(userSetting.getServerId()); if (!deviceInRedis.isOnLine()) { device.setOnLine(true); device.setCreateTime(now); redisCatchStorage.updateDevice(device); if (userSetting.getSyncChannelOnDeviceOnline()) { log.info("[设备上线,离线状态下重新注册]: {},查询设备信息以及通道信息", device.getDeviceId()); try { commander.deviceInfoQuery(device, null); } catch (InvalidArgumentException | SipException | ParseException e) { log.error("[命令发送失败] 查询设备信息: {}", e.getMessage()); } log.info("[设备上线]: {},查询通道信息", device.getDeviceId()); sync(device); } else { if (isDevice(device.getDeviceId())) { log.info("[设备上线]: {},查询通道信息", device.getDeviceId()); sync(device); } } } else { redisCatchStorage.updateDevice(device); } } long expiresTime = Math.min(device.getExpires(), device.getHeartBeatInterval() * device.getHeartBeatCount()) * 1000L; if (deviceStatusTaskRunner.containsKey(device.getDeviceId())) { if (sipTransactionInfo == null) { deviceStatusTaskRunner.updateDelay(device.getDeviceId(), expiresTime + System.currentTimeMillis()); } else { deviceStatusTaskRunner.removeTask(device.getDeviceId()); DeviceStatusTask task = DeviceStatusTask.getInstance(device.getDeviceId(), sipTransactionInfo, expiresTime + System.currentTimeMillis(), this::deviceStatusExpire); deviceStatusTaskRunner.addTask(task); } } else { DeviceStatusTask task = DeviceStatusTask.getInstance(device.getDeviceId(), sipTransactionInfo, expiresTime + System.currentTimeMillis(), this::deviceStatusExpire); deviceStatusTaskRunner.addTask(task); } // 设备注册成功时,自动在 ard_gb_device 表中创建设备记录(若不存在) try { GbDevice existingDevice = gbDeviceService.selectGbDeviceByGbDeviceId(device.getDeviceId()); if (existingDevice == null) { GbDevice gbDevice = new GbDevice(); gbDevice.setGbDeviceId(device.getDeviceId()); gbDevice.setDeviceName(StringUtils.hasText(device.getName()) ? device.getName() : device.getDeviceId()); gbDevice.setDeviceCode(device.getDeviceId()); gbDevice.setIp(device.getIp()); gbDevice.setPort(device.getPort()); gbDevice.setManufacturer(device.getManufacturer()); gbDevice.setModel(device.getModel()); gbDevice.setFirmware(device.getFirmware()); gbDevice.setOnLine(true); gbDeviceService.insertGbDevice(gbDevice); log.info("[GbDevice自动创建] deviceId: {}, id: {}", device.getDeviceId(), gbDevice.getId()); // 同时创建默认通道(设备自身) GbChannel gbChannel = new GbChannel(); gbChannel.setGbDeviceId(device.getDeviceId()); gbChannel.setGbChannelId(device.getDeviceId()); gbChannel.setChannelName(StringUtils.hasText(device.getName()) ? device.getName() : device.getDeviceId()); gbChannel.setDeviceCode(device.getDeviceId()); gbChannel.setStreamMode(device.getStreamMode() != null ? device.getStreamMode() : "TCP-PASSIVE"); gbChannel.setEnableMp4("0"); gbChannelService.insertGbChannel(gbChannel); log.info("[GbChannel默认创建] deviceId: {}, channelId: {}, id: {}", device.getDeviceId(), device.getDeviceId(), gbChannel.getId()); } else { // 设备已存在,更新最新信息(名称、IP、端口、厂商、型号、固件) boolean needUpdate = false; String newName = StringUtils.hasText(device.getName()) ? device.getName() : device.getDeviceId(); if (!newName.equals(existingDevice.getDeviceName())) { existingDevice.setDeviceName(newName); needUpdate = true; } if (device.getIp() != null && !device.getIp().equals(existingDevice.getIp())) { existingDevice.setIp(device.getIp()); needUpdate = true; } if (device.getPort() > 0 && (existingDevice.getPort() == null || device.getPort() != existingDevice.getPort())) { existingDevice.setPort(device.getPort()); needUpdate = true; } if (device.getManufacturer() != null && !device.getManufacturer().equals(existingDevice.getManufacturer())) { existingDevice.setManufacturer(device.getManufacturer()); needUpdate = true; } if (device.getModel() != null && !device.getModel().equals(existingDevice.getModel())) { existingDevice.setModel(device.getModel()); needUpdate = true; } if (device.getFirmware() != null && !device.getFirmware().equals(existingDevice.getFirmware())) { existingDevice.setFirmware(device.getFirmware()); needUpdate = true; } if (existingDevice.getOnLine() == null || !existingDevice.getOnLine()) { existingDevice.setOnLine(true); needUpdate = true; } if (needUpdate) { gbDeviceService.updateGbDevice(existingDevice); log.info("[GbDevice更新] deviceId: {}, name={}, ip={}, port={}, manufacturer={}, model={}, firmware={}", device.getDeviceId(), existingDevice.getDeviceName(), existingDevice.getIp(), existingDevice.getPort(), existingDevice.getManufacturer(), existingDevice.getModel(), existingDevice.getFirmware()); } } } catch (Exception e) { log.error("[GbDevice自动创建] 设备上线,自动创建失败:{}", device.getDeviceId(), e); } } @Override public void offline(String deviceId, String reason) { Device device = getDeviceByDeviceId(deviceId); if (device == null) { log.warn("[设备不存在] device:{}", deviceId); return; } // 主动查询设备状态, 没有 HostAddress 无法发送请求,可能是手动添加的设备 if (device.getHostAddress() != null) { Boolean deviceStatus = getDeviceStatus(device); if (deviceStatus != null && deviceStatus) { log.info("[设备离线] 主动探测发现设备在线,暂不处理 device:{}", deviceId); online(device, null); return; } } log.info("[设备离线] {}, device:{}, 心跳间隔: {},心跳超时次数: {}, 上次心跳时间:{}, 上次注册时间: {}", reason, deviceId, device.getHeartBeatInterval(), device.getHeartBeatCount(), device.getKeepaliveTime(), device.getRegisterTime()); device.setOnLine(false); cleanOfflineDevice(device); redisCatchStorage.updateDevice(device); if (isDevice(deviceId)) { channelOfflineByDevice(device); } // 同步更新 MySQL 在线状态 try { GbDevice gbDevice = gbDeviceService.selectGbDeviceByGbDeviceId(deviceId); if (gbDevice != null) { gbDevice.setOnLine(false); gbDeviceService.updateGbDevice(gbDevice); log.info("[设备离线] 更新MySQL在线状态: {} -> offline", deviceId); } } catch (Exception e) { log.error("[设备离线] 更新MySQL在线状态失败: {}", deviceId, e); } // 主动注销时:清理 Redis 中的通道数据,删除 MySQL 中该设备的通道记录 if ("主动注销".equals(reason)) { try { redisCatchStorage.cleanChannelsForDevice(deviceId); log.info("[设备离线] 主动注销,已清理Redis通道: {}", deviceId); } catch (Exception e) { log.error("[设备离线] 清理Redis通道失败: {}", deviceId, e); } try { List channels = gbChannelService.selectGbChannelByGbDeviceId(deviceId); int deletedCount = 0; int skippedCount = 0; for (GbChannel ch : channels) { // 跳过正在推流的通道,避免误删 if ("1".equals(ch.getStreamStatus())) { ch.setStreamStatus("0"); gbChannelService.updateGbChannelStream(ch); skippedCount++; continue; } gbChannelService.deleteGbChannelById(ch.getId()); deletedCount++; } log.info("[设备离线] 主动注销,已清理MySQL通道: {}条删除, {}条跳过", deletedCount, skippedCount); } catch (Exception e) { log.error("[设备离线] 清理MySQL通道失败: {}", deviceId, e); } } } /** * 更新设备心跳信息 * * @param device */ @Override public void updateDeviceHeartInfo(Device device) { Device deviceInDb = getDeviceByDeviceId(device.getDeviceId()); if (deviceInDb == null) { return; } if (!Objects.equals(deviceInDb.getHeartBeatCount(), device.getHeartBeatCount()) || !Objects.equals(deviceInDb.getHeartBeatInterval(), device.getHeartBeatInterval())) { deviceInDb.setHeartBeatCount(device.getHeartBeatCount()); deviceInDb.setHeartBeatInterval(device.getHeartBeatInterval()); deviceInDb.setPositionCapability(device.getPositionCapability()); updateDevice(deviceInDb); long expiresTime = Math.min(device.getExpires(), device.getHeartBeatInterval() * device.getHeartBeatCount()) * 1000L; if (deviceStatusTaskRunner.containsKey(device.getDeviceId())) { deviceStatusTaskRunner.updateDelay(device.getDeviceId(), expiresTime + System.currentTimeMillis()); } } } /** * 根据设备id和通道获取设备通道 * * @param gbDeviceId * @param gbChannelId * @return */ @Override public DeviceChannel getDeviceChannelByChannelId(String gbDeviceId, String gbChannelId) { List deviceChannels = redisCatchStorage.queryAllChannelsForRefresh(gbDeviceId); if (deviceChannels.isEmpty()) { throw new RuntimeException("设备【" + gbDeviceId + "】没有通道"); } DeviceChannel deviceChannel = null; for (DeviceChannel channel : deviceChannels) { if (gbChannelId.equals(channel.getDeviceId())) { deviceChannel = channel; } } if (deviceChannel == null) { throw new RuntimeException("设备【" + gbDeviceId + "】找不到【" + gbChannelId + "】通道"); } return deviceChannel; } private void deviceStatusExpire(String deviceId, SipTransactionInfo transactionInfo) { log.info("[设备状态] 到期, 编号: {}", deviceId); offline(deviceId, "保活到期"); } public Boolean getDeviceStatus(@NotNull Device device) { SynchronousQueue queue = new SynchronousQueue<>(); try { commander.deviceStatusQuery(device, ((code, msg, data) -> { queue.offer(msg); })); String data = queue.poll(10, TimeUnit.SECONDS); if (data != null && "ONLINE".equalsIgnoreCase(data.trim())) { return Boolean.TRUE; } else { return Boolean.FALSE; } } catch (InvalidArgumentException | SipException | ParseException | InterruptedException e) { log.error("[命令发送失败] 设备状态查询: {}", e.getMessage()); } return null; } /** * 批量修改设备 * * @param deviceList */ @Override public void updateDeviceList(List deviceList) { for (Device device : deviceList) { redisCatchStorage.updateDevice(device); } } /** * 修改设备 * * @param device */ @Override public void updateDevice(Device device) { redisCatchStorage.updateDevice(device); } public void sync(Device device) { int sn = (int) ((Math.random() * 9 + 1) * 100000); try { commander.catalogQuery(device, sn, ((code, msg, data) -> { // log.info("[获取通道]失败, deviceId: {}", device.getDeviceId()); })); } catch (SipException | InvalidArgumentException | ParseException e) { log.error("[获取通道]失败,信令发送失败, deviceId: {}", device.getDeviceId()); } } private boolean isDevice(String deviceId) { GbCode decode = GbCode.decode(deviceId); if (decode == null) { return true; } int code = Integer.parseInt(decode.getTypeCode()); return code <= 199; } private void cleanOfflineDevice(Device device) { if (subscribeTaskRunner.containsKey(SubscribeTaskForCatalog.getKey(device))) { subscribeTaskRunner.removeSubscribe(SubscribeTaskForCatalog.getKey(device)); } if (subscribeTaskRunner.containsKey(SubscribeTaskForMobilPosition.getKey(device))) { subscribeTaskRunner.removeSubscribe(SubscribeTaskForMobilPosition.getKey(device)); } // 清理设备相关的视频流会话 List ssrcTransactions = sessionManager.getSsrcTransactionByDeviceId(device.getDeviceId()); if (ssrcTransactions != null && !ssrcTransactions.isEmpty()) { log.info("[设备离线] 清理设备相关的视频流会话, deviceId: {}, 会话数量: {}", device.getDeviceId(), ssrcTransactions.size()); for (SsrcTransaction ssrcTransaction : ssrcTransactions) { try { log.info("[BYE 清理资源] deviceId: {}, channelId: {}, app: {}, stream: {}, ssrc: {}", ssrcTransaction.getDeviceId(), ssrcTransaction.getChannelId(), ssrcTransaction.getApp(), ssrcTransaction.getStream(), ssrcTransaction.getSsrc()); sessionManager.removeByCallId(ssrcTransaction.getCallId()); remoteZlmService.releaseSsrc(ssrcTransaction.getMediaServerId(), ssrcTransaction.getSsrc(), SecurityConstants.INNER); RtpServerParam rtpServerParam = new RtpServerParam(); rtpServerParam.setMediaServerId(ssrcTransaction.getMediaServerId()); rtpServerParam.setApp(ssrcTransaction.getApp()); rtpServerParam.setStream(ssrcTransaction.getStream()); rtpServerParam.setSsrc(ssrcTransaction.getSsrc()); rtpServerParam.setGbDeviceId(ssrcTransaction.getDeviceId()); rtpServerParam.setGbChannelId(ssrcTransaction.getChannelId()); remoteZlmService.closeRTPServer(ssrcTransaction.getMediaServerId(), rtpServerParam, SecurityConstants.INNER); } catch (Exception e) { log.error("[设备离线] 清理视频流会话异常, deviceId: {}, ssrc: {}", device.getDeviceId(), ssrcTransaction.getSsrc(), e); } } } } private void channelOfflineByDevice(Device device) { // 进行通道离线 List channelList = redisCatchStorage.queryAllChannelsForRefresh(device.getDeviceId()); if (channelList.isEmpty()) { return; } for (DeviceChannel deviceChannel : channelList) { deviceChannel.setStatus("OFF"); } redisCatchStorage.batchUpdate(device.getDeviceId(), channelList); } /** * 获取所有国标设备(从数据库查询,Redis 补充在线状态等运行时信息) * * @return 设备列表 */ @Override public List getAllDevices() { List result = new java.util.ArrayList<>(); List gbDevices = gbDeviceService.selectGbDeviceList(new GbDevice()); for (GbDevice gbDevice : gbDevices) { // 从 Redis 获取运行时信息(在线状态、IP、端口等) Device redisDevice = redisCatchStorage.getDevice(gbDevice.getGbDeviceId()); if (redisDevice != null) { // 用数据库的名称/厂商/型号覆盖 Redis 数据 if (StringUtils.hasText(gbDevice.getDeviceName())) { redisDevice.setName(gbDevice.getDeviceName()); } if (StringUtils.hasText(gbDevice.getManufacturer())) { redisDevice.setManufacturer(gbDevice.getManufacturer()); } if (StringUtils.hasText(gbDevice.getModel())) { redisDevice.setModel(gbDevice.getModel()); } if (StringUtils.hasText(gbDevice.getFirmware())) { redisDevice.setFirmware(gbDevice.getFirmware()); } result.add(redisDevice); } else { // Redis 中没有(设备离线已久),用数据库数据构造基本 Device 对象 Device device = new Device(); device.setDeviceId(gbDevice.getGbDeviceId()); device.setName(StringUtils.hasText(gbDevice.getDeviceName()) ? gbDevice.getDeviceName() : gbDevice.getGbDeviceId()); device.setOnLine(gbDevice.getOnLine() != null && gbDevice.getOnLine()); device.setIp(gbDevice.getIp()); device.setPort(gbDevice.getPort() != null ? gbDevice.getPort() : 0); device.setManufacturer(gbDevice.getManufacturer()); device.setPort(gbDevice.getPort() != null ? gbDevice.getPort() : 0); device.setManufacturer(gbDevice.getManufacturer()); device.setModel(gbDevice.getModel()); device.setFirmware(gbDevice.getFirmware()); result.add(device); } } return result; } /** * 获取所有国标设备(直接从 Redis 查询,用于迁移/同步等场景) * * @return 设备列表 */ @Override public List getAllRedisDevices() { return redisCatchStorage.getAllDevices(); } /** * 根据设备id获取所有通道 * * @param gbDeviceId 设备编号 * @return 通道列表 */ @Override public List getChannelsByDeviceId(String gbDeviceId) { return redisCatchStorage.queryAllChannelsForRefresh(gbDeviceId); } /** * 通用前端控制命令(参考国标文档A.3.1指令格式) * * @param device 设备 * @param channelId 通道国标编号 * @param cmdCode 指令码(对应国标文档指令格式中的字节4) * @param parameter1 数据一(对应国标文档指令格式中的字节5, 范围0-255) * @param parameter2 数据二(对应国标文档指令格式中的字节6, 范围0-255) * @param combindCode2 组合码二(对应国标文档指令格式中的字节7, 范围0-15) */ @Override public void frontEndCommand(Device device, String channelId, Integer cmdCode, Integer parameter1, Integer parameter2, Integer combindCode2) { try { commander.frontEndCmd(device, channelId, cmdCode, parameter1, parameter2, combindCode2); } catch (SipException | InvalidArgumentException | ParseException e) { log.error("[命令发送失败] 前端控制: {}", e.getMessage()); throw new ServiceException("命令发送失败: " + e.getMessage()); } } /** * 查询预置位 * * @param device 设备国标编号 * @param channelId 通道国标编号 * @param callback */ @Override public void queryPreset(Device device, String channelId, ErrorCallback> callback) { try { commander.presetQuery(device, channelId, callback); } catch (InvalidArgumentException | SipException | ParseException e) { log.error("[命令发送失败] 预制位查询: {}", e.getMessage()); callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); throw new ServiceException("命令发送失败: " + e.getMessage()); } } }