liusuyi
2026-05-30 f4f4fc53260eb67483dce406a963628273786a61
优化
1 files added
14 files modified
1146 ■■■■■ changed files
ard-api/ard-api-gb28181/src/main/java/com/ard/gb28181/api/RemoteGb28181Service.java 32 ●●●●● patch | view | raw | blame | history
ard-api/ard-api-gb28181/src/main/java/com/ard/gb28181/api/factory/RemoteGb28181FallbackFactory.java 27 ●●●●● patch | view | raw | blame | history
ard-modules/ard-modules-agent/src/main/java/com/ard/agent/controller/ChatClientController.java 45 ●●●●● patch | view | raw | blame | history
ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/api/Gb28181ApiController.java 190 ●●●●● patch | view | raw | blame | history
ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/service/impl/DeviceChannelServiceImpl.java 55 ●●●●● patch | view | raw | blame | history
ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/service/impl/DeviceServiceImpl.java 39 ●●●●● patch | view | raw | blame | history
ard-modules/ard-modules-work/src/main/java/com/ard/work/config/AsyncConfiguration.java 17 ●●●●● patch | view | raw | blame | history
ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/domain/PtzParamDTO.java 17 ●●●●● patch | view | raw | blame | history
ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzCacheManager.java 51 ●●●●● patch | view | raw | blame | history
ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzDataCollector.java 65 ●●●● patch | view | raw | blame | history
ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzTask.java 71 ●●●● patch | view | raw | blame | history
ard-modules/ard-modules-work/src/main/java/com/ard/work/websocket/utils/PTZWebSocketUtils.java 93 ●●●●● patch | view | raw | blame | history
ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/controller/ZlmController.java 147 ●●●●● patch | view | raw | blame | history
ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/service/IMediaServerService.java 10 ●●●●● patch | view | raw | blame | history
ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/service/impl/MediaServerServiceImpl.java 287 ●●●●● patch | view | raw | blame | history
ard-api/ard-api-gb28181/src/main/java/com/ard/gb28181/api/RemoteGb28181Service.java
@@ -7,6 +7,8 @@
import com.ard.common.core.domain.RtpServerParam;
import com.ard.gb28181.api.domain.Device;
import com.ard.gb28181.api.domain.DeviceChannel;
import com.ard.gb28181.api.domain.GbChannelDTO;
import com.ard.gb28181.api.domain.GbDeviceDTO;
import com.ard.gb28181.api.factory.RemoteGb28181FallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@@ -76,6 +78,36 @@
    R<List<Device>> getAllDevices(@RequestHeader(SecurityConstants.FROM_SOURCE) String inner);
    /**
     * 根据国标设备ID获取GbDevice
     */
    @GetMapping("/api/gb28181/getGbDevice/{gbDeviceId}")
    R<GbDeviceDTO> getGbDevice(@PathVariable String gbDeviceId, @RequestHeader(SecurityConstants.FROM_SOURCE) String inner);
    /**
     * 获取所有GbDevice列表
     */
    @GetMapping("/api/gb28181/getAllGbDevices")
    R<List<GbDeviceDTO>> getAllGbDevices(@RequestHeader(SecurityConstants.FROM_SOURCE) String inner);
    /**
     * 根据国标设备ID和通道ID获取GbChannel
     */
    @GetMapping("/api/gb28181/getGbChannel/{gbDeviceId}/{gbChannelId}")
    R<GbChannelDTO> getGbChannel(@PathVariable String gbDeviceId, @PathVariable String gbChannelId, @RequestHeader(SecurityConstants.FROM_SOURCE) String inner);
    /**
     * 根据国标设备ID获取所有通道
     */
    @GetMapping("/api/gb28181/getGbChannelsByDeviceId/{gbDeviceId}")
    R<List<GbChannelDTO>> getGbChannelsByDeviceId(@PathVariable String gbDeviceId, @RequestHeader(SecurityConstants.FROM_SOURCE) String inner);
    /**
     * 更新GbChannel流状态
     */
    @PostMapping("/api/gb28181/updateGbChannelStream")
    R<Boolean> updateGbChannelStream(@RequestBody GbChannelDTO gbChannel, @RequestHeader(SecurityConstants.FROM_SOURCE) String inner);
    /**
     * 通用前端控制命令(参考国标文档A.3.1指令格式)
     *
     * @param deviceId     设备国标编号
ard-api/ard-api-gb28181/src/main/java/com/ard/gb28181/api/factory/RemoteGb28181FallbackFactory.java
@@ -5,6 +5,8 @@
import com.ard.gb28181.api.RemoteGb28181Service;
import com.ard.gb28181.api.domain.Device;
import com.ard.gb28181.api.domain.DeviceChannel;
import com.ard.gb28181.api.domain.GbChannelDTO;
import com.ard.gb28181.api.domain.GbDeviceDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
@@ -55,6 +57,31 @@
            }
            @Override
            public R<GbDeviceDTO> getGbDevice(String gbDeviceId, String inner) {
                return R.fail("gb28181 获取GbDevice失败:" + throwable.getMessage());
            }
            @Override
            public R<List<GbDeviceDTO>> getAllGbDevices(String inner) {
                return R.fail("gb28181 获取所有GbDevice失败:" + throwable.getMessage());
            }
            @Override
            public R<GbChannelDTO> getGbChannel(String gbDeviceId, String gbChannelId, String inner) {
                return R.fail("gb28181 获取GbChannel失败:" + throwable.getMessage());
            }
            @Override
            public R<List<GbChannelDTO>> getGbChannelsByDeviceId(String gbDeviceId, String inner) {
                return R.fail("gb28181 获取设备通道列表失败:" + throwable.getMessage());
            }
            @Override
            public R<Boolean> updateGbChannelStream(GbChannelDTO gbChannel, String inner) {
                return R.fail("gb28181 更新GbChannel流状态失败:" + throwable.getMessage());
            }
            @Override
            public R<Void> frontEndCommand(String deviceId, String channelId, Integer cmdCode, Integer parameter1, Integer parameter2, Integer combindCode2, String inner) {
                return R.fail("gb28181 通用前端控制命令失败:" + throwable.getMessage());
            }
ard-modules/ard-modules-agent/src/main/java/com/ard/agent/controller/ChatClientController.java
New file
@@ -0,0 +1,45 @@
package com.ard.agent.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@Tag(name = "ChatClient使用案例")
@RestController
@RequestMapping("/chatclient")
public class ChatClientController {
    @Resource(name = "ollamaChatClient")
    private ChatClient chatClient;
    @Operation(summary = "同步聊天 - ChatClient基础用法")
    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
    @Operation(summary = "流式聊天 - ChatClient流式输出")
    @GetMapping(value = "/chat/stream", produces = "text/html;charset=UTF-8")
    public Flux<String> streamChat(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                .stream()
                .content();
    }
    @Operation(summary = "带系统提示词的聊天")
    @GetMapping("/chat/withSystem")
    public String chatWithSystem(@RequestParam String message) {
        return chatClient.prompt()
                .system(s -> s.text("你是一个智能助手,请用简洁的中文回答问题,回答不超过100字。"))
                .user(message)
                .call()
                .content();
    }
}
ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/api/Gb28181ApiController.java
@@ -7,8 +7,14 @@
import com.ard.common.core.exception.ServiceException;
import com.ard.gb28181.api.domain.Device;
import com.ard.gb28181.api.domain.DeviceChannel;
import com.ard.gb28181.api.domain.GbChannelDTO;
import com.ard.gb28181.api.domain.GbDeviceDTO;
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.ISIPCommander;
import com.ard.gb28181.session.SipInviteSessionManager;
import com.ard.zlm.api.RemoteZlmService;
@@ -22,6 +28,7 @@
import org.springframework.web.context.request.async.DeferredResult;
import javax.sip.ResponseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@@ -48,6 +55,12 @@
    @Autowired
    private RemoteZlmService remoteZlmService;
    @Autowired
    private IGbDeviceService gbDeviceService;
    @Autowired
    private IGbChannelService gbChannelService;
    /**
     * 根据设备id获取设备
@@ -686,4 +699,181 @@
        }
        frontEndCommand(deviceId, channelId, cmdCode, switchId, 0, 0);
    }
    // ==================== GbDevice / GbChannel 管理接口 ====================
    /**
     * 根据国标设备ID获取GbDevice
     */
    @GetMapping("/getGbDevice/{gbDeviceId}")
    public R<GbDeviceDTO> getGbDevice(@PathVariable String gbDeviceId) {
        GbDevice gbDevice = gbDeviceService.selectGbDeviceByGbDeviceId(gbDeviceId);
        if (gbDevice == null) {
            return R.fail("GbDevice不存在 gbDeviceId:" + gbDeviceId);
        }
        return R.ok(toGbDeviceDTO(gbDevice));
    }
    /**
     * 获取所有GbDevice列表
     */
    @GetMapping("/getAllGbDevices")
    public R<List<GbDeviceDTO>> getAllGbDevices() {
        List<GbDevice> list = gbDeviceService.selectGbDeviceList(new GbDevice());
        List<GbDeviceDTO> dtoList = new ArrayList<>();
        for (GbDevice d : list) {
            dtoList.add(toGbDeviceDTO(d));
        }
        return R.ok(dtoList);
    }
    /**
     * 根据国标设备ID和通道ID获取GbChannel
     */
    @GetMapping("/getGbChannel/{gbDeviceId}/{gbChannelId}")
    public R<GbChannelDTO> getGbChannel(@PathVariable String gbDeviceId, @PathVariable String gbChannelId) {
        GbChannel gbChannel = gbChannelService.selectByGbDeviceIdAndGbChannelId(gbDeviceId, gbChannelId);
        if (gbChannel == null) {
            return R.fail("GbChannel不存在 gbDeviceId:" + gbDeviceId + " gbChannelId:" + gbChannelId);
        }
        return R.ok(toGbChannelDTO(gbChannel));
    }
    /**
     * 根据国标设备ID获取所有通道
     */
    @GetMapping("/getGbChannelsByDeviceId/{gbDeviceId}")
    public R<List<GbChannelDTO>> getGbChannelsByDeviceId(@PathVariable String gbDeviceId) {
        List<GbChannel> list = gbChannelService.selectGbChannelByGbDeviceId(gbDeviceId);
        List<GbChannelDTO> dtoList = new ArrayList<>();
        for (GbChannel c : list) {
            dtoList.add(toGbChannelDTO(c));
        }
        return R.ok(dtoList);
    }
    /**
     * 更新GbChannel流状态
     */
    @PostMapping("/updateGbChannelStream")
    R<Boolean> updateGbChannelStream(@RequestBody GbChannelDTO dto) {
        GbChannel gbChannel = new GbChannel();
        gbChannel.setId(dto.getId());
        gbChannel.setStreamStatus(dto.getStreamStatus());
        gbChannel.setStreamKey(dto.getStreamKey());
        gbChannel.setMediaServerId(dto.getMediaServerId());
        gbChannel.setSnap(dto.getSnap());
        gbChannelService.updateGbChannelStream(gbChannel);
        return R.ok(true);
    }
    private GbDeviceDTO toGbDeviceDTO(GbDevice d) {
        GbDeviceDTO dto = new GbDeviceDTO();
        dto.setId(d.getId());
        dto.setGbDeviceId(d.getGbDeviceId());
        dto.setDeviceName(d.getDeviceName());
        dto.setDeviceCode(d.getDeviceCode());
        dto.setStreamMode(d.getStreamMode());
        dto.setEnableMp4(d.getEnableMp4());
        dto.setStreamStatus(d.getStreamStatus());
        dto.setMediaServerId(d.getMediaServerId());
        dto.setStreamKey(d.getStreamKey());
        dto.setSnap(d.getSnap());
        return dto;
    }
    private GbChannelDTO toGbChannelDTO(GbChannel c) {
        GbChannelDTO dto = new GbChannelDTO();
        dto.setId(c.getId());
        dto.setGbDeviceId(c.getGbDeviceId());
        dto.setGbChannelId(c.getGbChannelId());
        dto.setChannelName(c.getChannelName());
        dto.setDeviceCode(c.getDeviceCode());
        dto.setStreamMode(c.getStreamMode());
        dto.setEnableMp4(c.getEnableMp4());
        dto.setStreamStatus(c.getStreamStatus());
        dto.setMediaServerId(c.getMediaServerId());
        dto.setStreamKey(c.getStreamKey());
        dto.setSnap(c.getSnap());
        return dto;
    }
    /**
     * 一键迁移:将 Redis 中已注册的国标设备同步到 MySQL(供首次执行 SQL 后使用)
     */
    @Operation(summary = "迁移Redis国标设备到MySQL")
    @PostMapping("/migrateGbDevices")
    public R<String> migrateGbDevices() {
        List<Device> allDevices = deviceService.getAllDevices();
        int deviceCount = 0;
        int channelCount = 0;
        for (Device device : allDevices) {
            try {
                // 创建设备记录
                GbDevice existingDevice = gbDeviceService.selectGbDeviceByGbDeviceId(device.getDeviceId());
                if (existingDevice == null) {
                    GbDevice gbDevice = new GbDevice();
                    gbDevice.setGbDeviceId(device.getDeviceId());
                    gbDevice.setDeviceName(device.getName() != null ? device.getName() : device.getDeviceId());
                    gbDevice.setDeviceCode(device.getDeviceId());
                    gbDevice.setStreamMode(device.getStreamMode() != null ? device.getStreamMode() : "TCP-PASSIVE");
                    gbDevice.setEnableMp4("0");
                    gbDeviceService.insertGbDevice(gbDevice);
                    deviceCount++;
                    log.info("[迁移] 创建设备: {}", device.getDeviceId());
                }
                // 创建默认通道(设备自身)
                GbChannel existingDefaultChannel = gbChannelService.selectByGbDeviceIdAndGbChannelId(
                        device.getDeviceId(), device.getDeviceId());
                if (existingDefaultChannel == null) {
                    GbChannel gbChannel = new GbChannel();
                    gbChannel.setGbDeviceId(device.getDeviceId());
                    gbChannel.setGbChannelId(device.getDeviceId());
                    gbChannel.setChannelName(device.getName() != null ? device.getName() : device.getDeviceId());
                    gbChannel.setDeviceCode(device.getDeviceId());
                    gbChannel.setStreamMode(device.getStreamMode() != null ? device.getStreamMode() : "TCP-PASSIVE");
                    gbChannel.setEnableMp4("0");
                    gbChannelService.insertGbChannel(gbChannel);
                    channelCount++;
                }
                // 创建所有通道
                List<DeviceChannel> channels = deviceService.getChannelsByDeviceId(device.getDeviceId());
                if (channels != null) {
                    for (DeviceChannel channel : channels) {
                        String channelId = channel.getDeviceId();
                        if (channelId == null || channelId.length() <= 8) {
                            continue;
                        }
                        if (channelId.length() == 20) {
                            try {
                                com.ard.gb28181.api.domain.GbCode gbCode = com.ard.gb28181.api.domain.GbCode.decode(channelId);
                                if (gbCode != null && ("215".equals(gbCode.getTypeCode()) || "216".equals(gbCode.getTypeCode()))) {
                                    continue;
                                }
                            } catch (Exception ignored) {}
                        }
                        GbChannel existing = gbChannelService.selectByGbDeviceIdAndGbChannelId(device.getDeviceId(), channelId);
                        if (existing == null) {
                            GbChannel gbChannel = new GbChannel();
                            gbChannel.setGbDeviceId(device.getDeviceId());
                            gbChannel.setGbChannelId(channelId);
                            gbChannel.setChannelName(channel.getName() != null ? channel.getName() : channelId);
                            gbChannel.setDeviceCode(channelId);
                            gbChannel.setStreamMode(device.getStreamMode() != null ? device.getStreamMode() : "TCP-PASSIVE");
                            gbChannel.setEnableMp4("0");
                            gbChannelService.insertGbChannel(gbChannel);
                            channelCount++;
                        }
                    }
                }
            } catch (Exception e) {
                log.error("[迁移] 处理设备失败: {}", device.getDeviceId(), e);
            }
        }
        return R.ok("迁移完成: 设备 " + deviceCount + " 条, 通道 " + channelCount + " 条");
    }
}
ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/service/impl/DeviceChannelServiceImpl.java
@@ -2,7 +2,10 @@
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.domain.GbChannel;
import com.ard.gb28181.service.IDeviceChannelService;
import com.ard.gb28181.service.IGbChannelService;
import com.ard.gb28181.service.IRedisCatchStorage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -21,6 +24,9 @@
    @Autowired
    private IRedisCatchStorage redisCatchStorage;
    @Autowired
    private IGbChannelService gbChannelService;
    /**
     * 根据设备id清楚设备通道
@@ -47,6 +53,55 @@
        List<DeviceChannel> deviceChannelsInRedis = redisCatchStorage.queryAllChannelsForRefresh(device.getDeviceId());
        redisCatchStorage.batchAdd(device.getDeviceId(), mergeWithNewChannels(deviceChannelsInRedis, channels, device.isOnLine()));
        // 自动为每个通道创建 GbChannel 记录(若不存在)
        autoCreateGbChannelForChannels(device, channels);
    }
    /**
     * 自动为设备下的每个通道创建 GbChannel 记录
     */
    private void autoCreateGbChannelForChannels(Device device, List<DeviceChannel> channels) {
        try {
            String parentDeviceId = device.getDeviceId();
            for (DeviceChannel channel : channels) {
                String channelId = channel.getDeviceId();
                if (channelId == null) {
                    continue;
                }
                // 跳过行政区划(设备ID长度 <= 8)
                if (channelId.length() <= 8) {
                    continue;
                }
                // 跳过业务分组(215)和虚拟组织(216)
                if (channelId.length() == 20) {
                    try {
                        GbCode gbCode = GbCode.decode(channelId);
                        if (gbCode != null && ("215".equals(gbCode.getTypeCode()) || "216".equals(gbCode.getTypeCode()))) {
                            continue;
                        }
                    } catch (Exception ignored) {
                    }
                }
                // 检查是否已存在
                GbChannel existing = gbChannelService.selectByGbDeviceIdAndGbChannelId(parentDeviceId, channelId);
                if (existing == null) {
                    GbChannel gbChannel = new GbChannel();
                    gbChannel.setGbDeviceId(parentDeviceId);
                    gbChannel.setGbChannelId(channelId);
                    gbChannel.setChannelName(channel.getName() != null ? channel.getName() : channelId);
                    gbChannel.setDeviceCode(channelId);
                    gbChannel.setStreamMode(device.getStreamMode() != null ? device.getStreamMode() : "TCP-PASSIVE");
                    gbChannel.setEnableMp4("0");
                    gbChannelService.insertGbChannel(gbChannel);
                    log.info("[GbChannel自动创建] parentDeviceId={}, channelId={}, id={}",
                            parentDeviceId, channelId, gbChannel.getId());
                }
            }
        } catch (Exception e) {
            log.error("[GbChannel自动创建] 失败 parentDeviceId={}", device.getDeviceId(), e);
        }
    }
    /**
ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/service/impl/DeviceServiceImpl.java
@@ -13,7 +13,11 @@
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;
@@ -71,6 +75,12 @@
    @Autowired
    private RemoteQsDeviceService remoteQsDeviceService;
    @Autowired
    private IGbDeviceService gbDeviceService;
    @Autowired
    private IGbChannelService gbChannelService;
    /**
     * 查询设备信息
@@ -176,6 +186,35 @@
        } catch (Exception e) {
            log.error("[同步设备状态] 设备上线,同步到 QS 模块失败:{}", device.getDeviceId(), e);
        }
        // 设备注册成功时,自动在 ard_gb_device 表中创建设备记录(若不存在)
        try {
            GbDevice existingDevice = gbDeviceService.selectGbDeviceByGbDeviceId(device.getDeviceId());
            if (existingDevice == null) {
                GbDevice gbDevice = new GbDevice();
                gbDevice.setGbDeviceId(device.getDeviceId());
                gbDevice.setDeviceName(device.getName() != null ? device.getName() : device.getDeviceId());
                gbDevice.setDeviceCode(device.getDeviceId());
                gbDevice.setStreamMode(device.getStreamMode() != null ? device.getStreamMode() : "TCP-PASSIVE");
                gbDevice.setEnableMp4("0");
                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(device.getName() != null ? 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());
            }
        } catch (Exception e) {
            log.error("[GbDevice自动创建] 设备上线,自动创建失败:{}", device.getDeviceId(), e);
        }
    }
    @Override
ard-modules/ard-modules-work/src/main/java/com/ard/work/config/AsyncConfiguration.java
@@ -64,4 +64,21 @@
        executor.initialize();
        return executor;
    }
    /**
     * WebSocket推送线程池(避免慢客户端阻塞调度线程)
     * DiscardOldestPolicy: 推送任务允许丢弃旧消息以保证实时性
     */
    @Bean("wsPushExecutor")
    public ThreadPoolTaskExecutor wsPushExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(16);
        executor.setQueueCapacity(2000);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("ws-push-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
        executor.initialize();
        return executor;
    }
}
ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/domain/PtzParamDTO.java
@@ -1,5 +1,6 @@
package com.ard.work.device.camera.domain;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -13,22 +14,26 @@
 **/
@Data
@Builder
@NoArgsConstructor // 1. 显式添加无参构造函数
@NoArgsConstructor
@AllArgsConstructor
public class PtzParamDTO {
    String cameraId;
    private Integer chanNo;
    @Builder.Default
    private  Float p=0.0f;
    private Float p = 0.0f;
    @Builder.Default
    private Float t=0.0f;
    private Float t = 0.0f;
    @Builder.Default
    private Float z=0.0f;
    private Float z = 0.0f;
    @Builder.Default
    private  Float fHorFieldAngle=1.8f;
    private Float fHorFieldAngle = 1.8f;
    @Builder.Default
    private  Float fVerFieldAngle=1f;
    private Float fVerFieldAngle = 1f;
    private Double longitude;
    private Double latitude;
    private Double altitude;
    /** 数据更新时间戳,仅服务端使用,不参与前端序列化 */
    @JSONField(serialize = false)
    private long updateTime;
}
ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzCacheManager.java
@@ -2,10 +2,11 @@
import com.ard.work.device.camera.domain.PtzParamDTO;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
 * ptz缓存管理器
@@ -13,36 +14,50 @@
 * @author 刘苏义
 * @date 2026-03-20
 */
@Component
public class PtzCacheManager {
    // Key: cameraId_chanNo (例如 "CAM001_1"), Value: 最新的 PTZ 数据对象
    // 使用 ConcurrentHashMap 保证多线程安全
    /** Key: cameraId_chanNo, Value: 最新PTZ数据(自带 updateTime) */
    private final ConcurrentHashMap<String, PtzParamDTO> ptzDataMap = new ConcurrentHashMap<>();
    // 记录最后更新时间,用于判断数据是否过期
    private final ConcurrentHashMap<String, Long> updateTimeMap = new ConcurrentHashMap<>();
    public void updatePtzData(String key, PtzParamDTO data) {
        if (data == null) return;
        data.setUpdateTime(System.currentTimeMillis());
        ptzDataMap.put(key, data);
        updateTimeMap.put(key, System.currentTimeMillis());
    }
    public PtzParamDTO getPtzData(String key) {
        return ptzDataMap.get(key);
    }
    // 获取所有有效数据快照
    /** 获取所有未过期数据快照 */
    public List<PtzParamDTO> getAllValidData(long timeoutMs) {
        long now = System.currentTimeMillis();
        return ptzDataMap.entrySet().stream()
            .filter(entry -> (now - updateTimeMap.get(entry.getKey())) < timeoutMs)
            .map(Map.Entry::getValue)
            .collect(Collectors.toList());
        Collection<PtzParamDTO> values = ptzDataMap.values();
        List<PtzParamDTO> result = new ArrayList<>(values.size());
        for (PtzParamDTO dto : values) {
            if (now - dto.getUpdateTime() < timeoutMs) {
                result.add(dto);
            }
        }
        return result;
    }
    public void removeData(String key) {
        ptzDataMap.remove(key);
        updateTimeMap.remove(key);
    }
}
    /** 清理过期数据,返回清理条数 */
    public int evictStale(long timeoutMs) {
        long now = System.currentTimeMillis();
        int[] count = {0};
        ptzDataMap.entrySet().removeIf(e -> {
            if (now - e.getValue().getUpdateTime() > timeoutMs) {
                count[0]++;
                return true;
            }
            return false;
        });
        return count[0];
    }
}
ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzDataCollector.java
@@ -12,10 +12,14 @@
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@Slf4j
@Component
@@ -25,37 +29,69 @@
    private final CameraSDKService cameraSDKService;
    private final PtzCacheManager ptzCacheManager;
    @Resource(name = "timeoutExecutor")
    private ThreadPoolTaskExecutor sdkExecutor;
    private static final Set<String> EXCLUDE_FACTORY = new HashSet<>(Arrays.asList("7"));
    private static final String CAMERA_ENABLE = "1";
    private static final Set<String> SUPPORT_TYPES = new HashSet<>(Arrays.asList("1", "4"));
    /** 单通道 SDK 调用超时 */
    private static final long SDK_CALL_TIMEOUT_MS = 2000L;
    /** 整相机所有通道总采集超时 */
    private static final long CAMERA_TOTAL_TIMEOUT_MS = 3000L;
    @Async("cameraPTZExecutor")
    public void collectCamera(ArdCamera camera, Set<String> runningCamera) {
        String camId = camera.getId();
        try {
            if (!isValidCamera(camera)) return;
            List<ArdChannel> channels = camera.getChannelList();
            if (channels == null || channels.isEmpty()) return;
            for (ArdChannel channel : channels) {
                String key = camId + "_" + channel.getChanNo();
                PtzParamDTO dto= fetchPtz(camera, channel);
                if (dto != null) {
                    ptzCacheManager.updatePtzData(key, dto);
                }
            CompletableFuture<?>[] futures = channels.stream()
                    .map(ch -> CompletableFuture.runAsync(
                            () -> collectChannel(camera, ch), sdkExecutor))
                    .toArray(CompletableFuture[]::new);
            try {
                CompletableFuture.allOf(futures).get(CAMERA_TOTAL_TIMEOUT_MS, TimeUnit.MILLISECONDS);
            } catch (TimeoutException e) {
                log.warn("采集相机 {} 整体超时,部分通道数据可能未更新", camId);
            } catch (Exception e) {
                log.error("采集相机 {} 异常", camId, e);
            }
        } finally {
            // ✅ 必须释放(防死锁)
            runningCamera.remove(camId);
        }
    }
    /**
     * 原始SDK调用(不改)
     */
    private PtzParamDTO fetchPtz(ArdCamera camera, ArdChannel channel) {
    private void collectChannel(ArdCamera camera, ArdChannel channel) {
        PtzParamDTO dto = fetchPtzWithTimeout(camera, channel);
        if (dto != null) {
            ptzCacheManager.updatePtzData(camera.getId() + "_" + channel.getChanNo(), dto);
        }
    }
    /**
     * 带超时的 SDK 调用,防止 SDK 卡死拖垮线程池
     */
    private PtzParamDTO fetchPtzWithTimeout(ArdCamera camera, ArdChannel channel) {
        CompletableFuture<PtzParamDTO> future = CompletableFuture.supplyAsync(
                () -> fetchPtz(camera, channel), sdkExecutor);
        try {
            return future.get(SDK_CALL_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            log.warn("相机 {} 通道 {} SDK 调用超时", camera.getId(), channel.getChanNo());
            future.cancel(true);
            return null;
        } catch (Exception e) {
            log.error("相机 {} 通道 {} SDK 调用异常", camera.getId(), channel.getChanNo(), e);
            return null;
        }
    }
    private PtzParamDTO fetchPtz(ArdCamera camera, ArdChannel channel) {
        CameraCmd cmd = new CameraCmd();
        cmd.setCameraId(camera.getId());
        cmd.setChanNo(channel.getChanNo());
@@ -78,7 +114,6 @@
            dto.setT(ptz.getT());
            dto.setZ(ptz.getZ());
        }
        return dto;
    }
@@ -87,4 +122,4 @@
                && !EXCLUDE_FACTORY.contains(camera.getFactory())
                && SUPPORT_TYPES.contains(camera.getType());
    }
}
}
ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzTask.java
@@ -9,6 +9,9 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -28,33 +31,38 @@
    private final Set<String> runningCamera = ConcurrentHashMap.newKeySet();
    /** 缓存数据有效期: 超过此时间不再推送 */
    private static final long DATA_VALID_MS = 5_000L;
    /** 缓存数据淘汰阈值: 超过此时间从内存清掉 */
    private static final long DATA_EVICT_MS = 60_000L;
    /** 相机列表本地缓存 TTL */
    private static final long CAMERA_LIST_TTL_MS = 30_000L;
    private volatile List<ArdCamera> cachedCameraList = Collections.emptyList();
    private volatile long cameraListExpireAt = 0L;
    /**
     * 采集任务(不会堆积)
     * 采集任务
     */
    @Scheduled(initialDelay = 5000, fixedDelay = 1000)
    public void startCollectTask() {
        List<ArdCamera> cameraList = getCameraList();
        if (cameraList.isEmpty()) return;
        List<Object> cameraList = redisService.getCacheMapValues(CacheConstants.CAMERA_LIST);
        if (cameraList == null || cameraList.isEmpty()) return;
        for (Object obj : cameraList) {
            ArdCamera camera = (ArdCamera) obj;
        for (ArdCamera camera : cameraList) {
            String camId = camera.getId();
            // ✅ 防重复执行
            if (!runningCamera.add(camId)) continue;
            ptzDataCollector.collectCamera(camera, runningCamera);
        }
    }
    /**
     * 推送
     * 推送任务
     */
    @Scheduled(fixedDelay = 1000)
    public void pushPTZ() {
        try {
            List<PtzParamDTO> list = ptzCacheManager.getAllValidData(5000);
            List<PtzParamDTO> list = ptzCacheManager.getAllValidData(DATA_VALID_MS);
            if (!list.isEmpty()) {
                PTZWebSocketUtils.sendMessageAll(list);
            }
@@ -62,4 +70,43 @@
            log.error("推送PTZ异常", e);
        }
    }
}
    /**
     * 清理过期缓存,防止已禁用/删除的相机数据残留
     */
    @Scheduled(fixedDelay = 60_000)
    public void evictStaleCache() {
        int n = ptzCacheManager.evictStale(DATA_EVICT_MS);
        if (n > 0) {
            log.debug("清理过期PTZ缓存 {} 条", n);
        }
    }
    /**
     * 本地缓存相机列表,降低 Redis QPS
     */
    private List<ArdCamera> getCameraList() {
        long now = System.currentTimeMillis();
        if (now < cameraListExpireAt) {
            return cachedCameraList;
        }
        synchronized (this) {
            if (now < cameraListExpireAt) {
                return cachedCameraList;
            }
            List<Object> raw = redisService.getCacheMapValues(CacheConstants.CAMERA_LIST);
            List<ArdCamera> list;
            if (raw == null || raw.isEmpty()) {
                list = Collections.emptyList();
            } else {
                list = new ArrayList<>(raw.size());
                for (Object o : raw) {
                    list.add((ArdCamera) o);
                }
            }
            cachedCameraList = list;
            cameraListExpireAt = now + CAMERA_LIST_TTL_MS;
            return list;
        }
    }
}
ard-modules/ard-modules-work/src/main/java/com/ard/work/websocket/utils/PTZWebSocketUtils.java
@@ -12,6 +12,7 @@
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -25,13 +26,15 @@
@Slf4j
public final class PTZWebSocketUtils {
    // 存储 websocket session
    public static final ConcurrentMap<String, WebSocketSession> ONLINE_USER_SESSIONS = new ConcurrentHashMap<>();
    /**
     * @param session 用户 session
     * @param message 发送内容
     */
    /** 推送线程池,由 PTZWebSocketInitializer 在启动时注入;未注入时降级为同步执行 */
    private static volatile Executor pushExecutor;
    public static void setPushExecutor(Executor executor) {
        pushExecutor = executor;
    }
    public static void sendMessage(WebSocketSession session, String message) {
        if (session == null) {
            return;
@@ -42,84 +45,58 @@
        }
        synchronized (session) {
            try {
                //log.debug("发送消息:" + message);
                session.sendMessage(new TextMessage(String.join(", ", message)));
                session.sendMessage(new TextMessage(message));
            } catch (IOException e) {
                log.error("sendMessage IOException ", e);
            }
        }
    }
    /**
     * @param session 用户 session
     * @param message 发送内容
     */
    public static void sendMessage(WebSocketSession session, Map message) {
        if (session == null) {
            return;
        }
       final InetSocketAddress remoteAddress = session.getRemoteAddress();
        if (remoteAddress == null) {
            return;
        }
        synchronized (session) {
            try {
                session.sendMessage(new TextMessage(JSONObject.toJSONString(message)));
            } catch (IOException e) {
                log.error("sendMessage IOException ", e);
            }
        }
        sendMessage(session, JSONObject.toJSONString(message));
    }
    public static void sendMessage(WebSocketSession session, List message) {
        if (session == null) {
            return;
        }
        final InetSocketAddress remoteAddress = session.getRemoteAddress();
        if (remoteAddress == null) {
            return;
        }
        synchronized (session) {
            try {
                session.sendMessage(new TextMessage(JSONObject.toJSONString(message)));
            } catch (IOException e) {
                log.error("sendMessage IOException ", e);
            }
        }
        sendMessage(session, JSON.toJSONString(message));
    }
    /**
     * 推送消息到其他客户端
     *
     * @param message
     * 推送字符串消息到所有客户端
     */
    public static void sendMessageAll(String message) {
        ONLINE_USER_SESSIONS.forEach((sessionId, session) -> sendMessage(session, message));
        broadcast(message);
    }
    /**
     * 推送消息到其他客户端
     *
     * @param message
     * 推送Map消息到所有客户端
     */
    public static void sendMessageAll(Map message) {
        JSONObject jsonObject = new JSONObject(message);
        ONLINE_USER_SESSIONS.forEach((sessionId, session) -> sendMessage(session, jsonObject.toString()));
        broadcast(JSONObject.toJSONString(message));
    }
    /**
     * 推送消息到其他客户端
     *
     * @param message
     * 推送List消息到所有客户端
     */
    public static void sendMessageAll(List message) {
        ONLINE_USER_SESSIONS.forEach((sessionId, session) -> sendMessage(session, JSON.toJSONString(message)));
        broadcast(JSON.toJSONString(message));
    }
    /**
     * @Author 刘苏义
     * @Description  发送消息给userId_前缀的人
     * @Date   2024/7/16 10:24
     * @Param
     * @return
     * 一次序列化,并行分发到所有 session
     */
    private static void broadcast(String payload) {
        if (ONLINE_USER_SESSIONS.isEmpty()) return;
        Executor executor = pushExecutor;
        if (executor == null) {
            ONLINE_USER_SESSIONS.values().forEach(session -> sendMessage(session, payload));
            return;
        }
        ONLINE_USER_SESSIONS.values().forEach(session ->
                executor.execute(() -> sendMessage(session, payload)));
    }
    /**
     * 发送消息给 userId_ 前缀的人
     */
    public static void sendMessagePrefix(String targetId, String message) {
        String regex = "^" + Pattern.quote(targetId) + "_\\d+$";
@@ -129,4 +106,4 @@
            return matcher.matches();
        }).map(Map.Entry::getValue).forEach(session -> PTZWebSocketUtils.sendMessage(session, message));
    }
}
}
ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/controller/ZlmController.java
@@ -10,6 +10,7 @@
import com.ard.gb28181.api.RemoteGb28181Service;
import com.ard.gb28181.api.domain.Device;
import com.ard.gb28181.api.domain.DeviceChannel;
import com.ard.gb28181.api.domain.GbChannelDTO;
import com.ard.qs.api.RemoteQsDeviceService;
import com.ard.qs.api.domain.QsDevice;
import com.ard.work.api.RemoteCameraService;
@@ -492,6 +493,7 @@
     * @param id      设备id
     * @return
     */
    @Operation(summary = "gb28181播放")
    @GetMapping("/startGb28181Play/{id}")
    public DeferredResult<R<StreamContent>> startGb28181Play(
            HttpServletRequest request,
@@ -633,4 +635,149 @@
        return AjaxResult.success(json);
    }
    /**
     * gb28181 播放(基于GbDevice,不依赖QS,按国标编码播放)
     *
     * @param request
     * @param gbDeviceId  国标设备编码
     * @param gbChannelId 国标通道编码
     * @return
     */
    @Operation(summary = "gb28181播放(按国标编码)")
    @GetMapping("/startGb28181PlayByGbDeviceId/{gbDeviceId}/{gbChannelId}")
    public DeferredResult<R<StreamContent>> startGb28181PlayByGbDeviceId(
            HttpServletRequest request,
            @PathVariable String gbDeviceId,
            @PathVariable String gbChannelId
    ) {
        log.info("[gb28181 开始点播] gbDeviceId:{}, gbChannelId:{}", gbDeviceId, gbChannelId);
        Assert.hasText(gbDeviceId, "国标设备编码不可为空");
        Assert.hasText(gbChannelId, "国标通道编码不可为空");
        // 1. 查询 GbChannel 配置
        R<GbChannelDTO> channelR = remoteGb28181Service.getGbChannel(gbDeviceId, gbChannelId, SecurityConstants.INNER);
        if (channelR.getCode() != Constants.SUCCESS) {
            throw new RuntimeException("获取GbChannel配置失败 gbDeviceId:" + gbDeviceId + " gbChannelId:" + gbChannelId
                    + "。设备注册后会自动创建,请确认设备已上线并完成目录同步。");
        }
        Assert.notNull(channelR.getData(), "GbChannel不存在 gbDeviceId:" + gbDeviceId + " gbChannelId:" + gbChannelId);
        GbChannelDTO gbChannelDTO = channelR.getData();
        // 2. 查询国标设备
        R<Device> deviceR = remoteGb28181Service.getDeviceByDeviceId(gbDeviceId, SecurityConstants.INNER);
        if (deviceR.getCode() != Constants.SUCCESS) {
            throw new RuntimeException("gb28181 获取设备信息失败 deviceId:" + gbDeviceId);
        }
        Assert.notNull(deviceR.getData(), "gb28181 国标设备不存在 deviceId:" + gbDeviceId);
        if (!deviceR.getData().isOnLine()) {
            throw new RuntimeException("gb28181 国标设备不在线 deviceId:" + gbDeviceId);
        }
        // 3. 查询国标通道
        R<DeviceChannel> deviceChannelR = remoteGb28181Service.getDeviceChannelByChannelId(gbDeviceId, gbChannelId, SecurityConstants.INNER);
        if (deviceChannelR.getCode() != Constants.SUCCESS) {
            throw new RuntimeException("gb28181 获取设备通道失败 gbDeviceId:" + gbDeviceId + " gbChannelId:" + gbChannelId);
        }
        Assert.notNull(deviceChannelR.getData(), "gb28181 国标设备通道不存在 gbDeviceId:" + gbDeviceId + " gbChannelId:" + gbChannelId);
        if (!"ON".equals(deviceChannelR.getData().getStatus())) {
            throw new RuntimeException("gb28181 国标设备通道不在线 gbDeviceId:" + gbDeviceId + " gbChannelId:" + gbChannelId);
        }
        DeferredResult<R<StreamContent>> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue());
        result.onTimeout(() -> {
            log.info("[点播等待超时] gbDeviceId:{}, gbChannelId:{}", gbDeviceId, gbChannelId);
            R<StreamContent> fail = R.fail();
            fail.setMsg("点播超时");
            result.setResult(fail);
            inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, gbChannelDTO.getId());
            mediaServerService.stopGb28181PlayByGbChannel(InviteSessionType.PLAY, gbChannelDTO, deviceR.getData(), gbChannelDTO.getDeviceCode());
        });
        ErrorCallback<StreamInfo> callback = (code, msg, streamInfo) -> {
            if (code == InviteErrorCode.SUCCESS.getCode()) {
                R<StreamContent> r = R.ok();
                if (streamInfo != null) {
                    if (userSetting.getUseSourceIpAsStreamIp()) {
                        streamInfo = streamInfo.clone();
                        String host;
                        try {
                            URL url = new URL(request.getRequestURL().toString());
                            host = url.getHost();
                        } catch (MalformedURLException e) {
                            host = request.getLocalAddr();
                        }
                        streamInfo.changeStreamIp(host);
                    }
                    if (!ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix())
                            && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) {
                        streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix());
                    }
                    r.setData(new StreamContent(streamInfo));
                } else {
                    r.setCode(code);
                    r.setMsg(msg);
                }
                result.setResult(r);
            } else {
                result.setResult(R.fail(code, msg));
            }
        };
        mediaServerService.startGb28181PlayByGbChannel(gbChannelDTO, deviceR.getData(), callback);
        return result;
    }
    /**
     * gb28181 停止点播(基于GbDevice,不依赖QS,按国标编码停止)
     *
     * @param gbDeviceId  国标设备编码
     * @param gbChannelId 国标通道编码
     * @return
     */
    @Operation(summary = "gb28181停止点播(按国标编码)")
    @GetMapping("/stopGb28181PlayByGbDeviceId/{gbDeviceId}/{gbChannelId}")
    public AjaxResult stopGb28181PlayByGbDeviceId(
            @PathVariable String gbDeviceId,
            @PathVariable String gbChannelId
    ) {
        log.info("[gb28181 停止点播] gbDeviceId:{}, gbChannelId:{}", gbDeviceId, gbChannelId);
        Assert.hasText(gbDeviceId, "国标设备编码不可为空");
        Assert.hasText(gbChannelId, "国标通道编码不可为空");
        // 1. 查询 GbChannel 配置
        R<GbChannelDTO> channelR = remoteGb28181Service.getGbChannel(gbDeviceId, gbChannelId, SecurityConstants.INNER);
        if (channelR.getCode() != Constants.SUCCESS || channelR.getData() == null) {
            throw new RuntimeException("GbChannel不存在 gbDeviceId:" + gbDeviceId + " gbChannelId:" + gbChannelId);
        }
        GbChannelDTO gbChannelDTO = channelR.getData();
        // 2. 查询国标设备
        R<Device> deviceR = remoteGb28181Service.getDeviceByDeviceId(gbDeviceId, SecurityConstants.INNER);
        if (deviceR.getCode() != Constants.SUCCESS) {
            throw new RuntimeException("gb28181 获取设备信息失败 deviceId:" + gbDeviceId);
        }
        Assert.notNull(deviceR.getData(), "gb28181 国标设备不存在 deviceId:" + gbDeviceId);
        // 3. 查询国标通道
        R<DeviceChannel> deviceChannelR = remoteGb28181Service.getDeviceChannelByChannelId(gbDeviceId, gbChannelId, SecurityConstants.INNER);
        if (deviceChannelR.getCode() != Constants.SUCCESS) {
            throw new RuntimeException("gb28181 获取设备通道失败 gbDeviceId:" + gbDeviceId + " gbChannelId:" + gbChannelId);
        }
        Assert.notNull(deviceChannelR.getData(), "gb28181 国标设备通道不存在 gbDeviceId:" + gbDeviceId + " gbChannelId:" + gbChannelId);
        mediaServerService.stopGb28181PlayByGbChannel(InviteSessionType.PLAY, gbChannelDTO,
                deviceR.getData(), gbChannelDTO.getDeviceCode());
        JSONObject json = new JSONObject();
        json.put("gbDeviceId", gbDeviceId);
        json.put("gbChannelId", gbChannelId);
        return AjaxResult.success(json);
    }
}
ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/service/IMediaServerService.java
@@ -1,6 +1,7 @@
package com.ard.zlm.service;
import com.ard.gb28181.api.domain.Device;
import com.ard.gb28181.api.domain.GbChannelDTO;
import com.ard.qs.api.domain.QsDevice;
import com.ard.work.api.domian.ArdChannel;
import com.ard.zlm.api.domain.*;
@@ -362,4 +363,13 @@
     */
    void stopGb28181Play(InviteSessionType type, QsDevice qsDevice, Device device, String stream);
    /**
     * gb28181 播放(基于GbChannel,不依赖QS)
     */
    void startGb28181PlayByGbChannel(GbChannelDTO gbChannelDTO, Device gbDevice, ErrorCallback<StreamInfo> callback);
    /**
     * gb28181 停止点播(基于GbChannel,不依赖QS)
     */
    void stopGb28181PlayByGbChannel(InviteSessionType type, GbChannelDTO gbChannelDTO, Device device, String stream);
}
ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/service/impl/MediaServerServiceImpl.java
@@ -11,6 +11,7 @@
import com.ard.common.core.utils.file.FileMultipartFile;
import com.ard.gb28181.api.RemoteGb28181Service;
import com.ard.gb28181.api.domain.Device;
import com.ard.gb28181.api.domain.GbChannelDTO;
import com.ard.qs.api.RemoteQsDeviceService;
import com.ard.qs.api.domain.QsDevice;
import com.ard.system.api.RemoteFileService;
@@ -1811,6 +1812,101 @@
        }
    }
    /**
     * gb28181 播放(基于GbDevice,不依赖QS)
     */
    @Override
    public void startGb28181PlayByGbChannel(GbChannelDTO gbChannelDTO, Device gbDevice, ErrorCallback<StreamInfo> callback) {
        ZlmMediaServer mediaServer = getMediaServerForMinimumLoad(null);
        if (mediaServer == null) {
            callback.run(InviteErrorCode.FAIL.getCode(), "无可用的节点", null);
            return;
        }
        String streamMode = gbChannelDTO.getStreamMode() != null
                ? gbChannelDTO.getStreamMode() : gbDevice.getStreamMode();
        int tcpMode = streamMode.equals("TCP-ACTIVE") ? 2
                : (streamMode.equals("TCP-PASSIVE") ? 1 : 0);
        RTPServerParam rtpServerParam = new RTPServerParam();
        rtpServerParam.setApp("gb28181");
        rtpServerParam.setMediaServer(mediaServer);
        rtpServerParam.setType(LiveStreamType.GB28181.getCode());
        rtpServerParam.setStreamId(gbChannelDTO.getDeviceCode());
        rtpServerParam.setTcpMode(tcpMode);
        rtpServerParam.setId(gbChannelDTO.getId());
        startGb28181PlayFunByGbChannel(mediaServer, gbChannelDTO, gbDevice, rtpServerParam, null, callback);
    }
    /**
     * gb28181 停止点播(基于GbDevice,不依赖QS)
     */
    @Override
    public void stopGb28181PlayByGbChannel(InviteSessionType type, GbChannelDTO gbChannelDTO, Device device, String stream) {
        InviteInfo inviteInfo = inviteStreamService.getInviteInfo(type, gbChannelDTO.getId(), stream);
        if (inviteInfo == null) {
            if (type == InviteSessionType.PLAY) {
                GbChannelDTO update = new GbChannelDTO();
                update.setId(gbChannelDTO.getId());
                update.setStreamKey("");
                update.setMediaServerId("");
                update.setStreamStatus("0");
                R<Boolean> r = remoteGb28181Service.updateGbChannelStream(update, SecurityConstants.INNER);
                if (r.getCode() != Constants.SUCCESS) {
                    throw new RuntimeException("更新GbDevice失败");
                }
            }
            return;
        }
        inviteStreamService.removeInviteInfo(inviteInfo);
        if (InviteSessionStatus.ok == inviteInfo.getStatus()) {
            try {
                log.info("[停止点播/回放/下载] {}/{}", gbChannelDTO.getGbDeviceId(), gbChannelDTO.getGbChannelId());
                RtpServerParam rtpServer = new RtpServerParam();
                rtpServer.setApp("gb28181");
                rtpServer.setStream(gbChannelDTO.getDeviceCode());
                rtpServer.setGbDeviceId(gbChannelDTO.getGbDeviceId());
                rtpServer.setGbChannelId(gbChannelDTO.getGbChannelId());
                R<Void> r = remoteGb28181Service.streamByeCmd(rtpServer, SecurityConstants.INNER);
                if (r.getCode() != Constants.SUCCESS) {
                    log.error("[命令发送失败] 停止点播/回放/下载, deviceId:{}", gbChannelDTO.getGbDeviceId());
                    throw new RuntimeException("[命令发送失败] 停止点播/回放/下载, deviceId:" + gbChannelDTO.getGbDeviceId());
                }
            } catch (Exception e) {
                log.error("[命令发送失败] 停止点播/回放/下载, 发送BYE: {}", e.getMessage());
                throw new RuntimeException("命令发送失败: " + e.getMessage());
            }
        }
        if (inviteInfo.getType() == InviteSessionType.PLAY) {
            GbChannelDTO update = new GbChannelDTO();
            update.setId(gbChannelDTO.getId());
            update.setStreamKey("");
            update.setMediaServerId("");
            update.setStreamStatus("0");
            R<Boolean> r = remoteGb28181Service.updateGbChannelStream(update, SecurityConstants.INNER);
            if (r.getCode() != Constants.SUCCESS) {
                throw new RuntimeException("更新GbDevice失败");
            }
        }
        ZlmMediaServer mediaServer = null;
        if (inviteInfo.getStreamInfo() != null) {
            mediaServer = inviteInfo.getStreamInfo().getMediaServer();
        } else {
            mediaServer = getOne(inviteInfo.getMediaServerId());
        }
        if (mediaServer != null && inviteInfo.getSsrcInfo() != null) {
            closeRTPServer(mediaServer, inviteInfo.getSsrcInfo().getStream());
            ssrcFactory.releaseSsrc(inviteInfo.getMediaServerId(), inviteInfo.getSsrcInfo().getSsrc());
        }
    }
    /**
     * 开启国标28181播放
@@ -2029,6 +2125,197 @@
    }
    /**
     * 开启国标28181播放(基于GbDevice,不依赖QS)
     */
    private SSRCInfo startGb28181PlayFunByGbChannel(ZlmMediaServer mediaServer, GbChannelDTO gbChannelDTO,
                                                    Device gbDevice, RTPServerParam rtpServerParam,
                                                    String ssrc, ErrorCallback<StreamInfo> callback) {
        // 获取点播的状态信息
        InviteInfo inviteInfoInCatch = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY,
                gbChannelDTO.getId());
        if (inviteInfoInCatch != null) {
            if (inviteInfoInCatch.getStreamInfo() == null) {
                ssrcFactory.releaseSsrc(mediaServer.getId(), null);
                inviteStreamService.once(InviteSessionType.PLAY, gbChannelDTO.getId(), null, callback);
                log.info("[点播开始] 已经请求中,等待结果, deviceId: {}, channel: {}", gbChannelDTO.getId(), gbChannelDTO.getId());
                return inviteInfoInCatch.getSsrcInfo();
            } else {
                StreamInfo streamInfo = inviteInfoInCatch.getStreamInfo();
                String streamId = streamInfo.getStream();
                if (streamId == null) {
                    callback.run(InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(), "点播失败, redis缓存streamId等于null", null);
                    inviteStreamService.call(InviteSessionType.PLAY, gbChannelDTO.getId(), null,
                            InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(), "点播失败, redis缓存streamId等于null", null);
                    return inviteInfoInCatch.getSsrcInfo();
                }
                ZlmMediaServer mediaInfo = streamInfo.getMediaServer();
                Boolean ready = isStreamReady(mediaInfo, rtpServerParam.getApp(), streamId);
                if (ready != null && ready) {
                    if (callback != null) {
                        callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
                    }
                    inviteStreamService.call(InviteSessionType.PLAY, gbChannelDTO.getId(), null,
                            InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
                    log.info("[点播已存在] 直接返回, 设备编号: {}", gbChannelDTO.getId());
                    return inviteInfoInCatch.getSsrcInfo();
                } else {
                    inviteStreamService.once(InviteSessionType.PLAY, gbChannelDTO.getId(), null, callback);
                    RTPServerParam stopRtp = new RTPServerParam();
                    stopRtp.setId(gbChannelDTO.getId());
                    stopRtp.setType(rtpServerParam.getType());
                    stopRtp.setStreamId(rtpServerParam.getStreamId());
                    stopRtpPlay(stopRtp);
                    inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, gbChannelDTO.getId());
                }
            }
        }
        rtpServerParam.setMediaServer(mediaServer);
        if (rtpServerParam.getPresetSsrc() != null) {
            ssrc = rtpServerParam.getPresetSsrc();
        } else {
            if (rtpServerParam.isPlayback()) {
                ssrc = ssrcFactory.getPlayBackSsrc(mediaServer.getId());
            } else {
                ssrc = ssrcFactory.getPlaySsrc(mediaServer.getId());
            }
        }
        rtpServerParam.setSsrc(ssrc);
        SSRCInfo ssrcInfo = receiveRtpServerService.openRTPServer(rtpServerParam, (code, msg, result) -> {
            if (code == InviteErrorCode.SUCCESS.getCode() && result != null && result.getHookData() != null) {
                log.info("[创建RTP服务器] 成功, code: {}, msg: {}, result: {}", code, msg, result);
                StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, rtpServerParam.getApp(),
                        rtpServerParam.getStreamId(), result.getHookData().getMediaInfo());
                if (streamInfo == null) {
                    if (callback != null) {
                        callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
                                InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
                    }
                    inviteStreamService.call(InviteSessionType.PLAY, gbChannelDTO.getId(), null,
                            InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
                            InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
                    if (result != null && result.getSsrcInfo() != null) {
                        closeRTPServer(mediaServer, result.getSsrcInfo().getStream());
                        ssrcFactory.releaseSsrc(mediaServer.getId(), result.getSsrcInfo().getSsrc());
                    }
                    return;
                }
                if (callback != null) {
                    callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
                    inviteStreamService.call(InviteSessionType.PLAY, gbChannelDTO.getId(), null,
                            InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
                    InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(
                            InviteSessionType.PLAY, gbChannelDTO.getId());
                    if (inviteInfo != null) {
                        inviteInfo.setStatus(InviteSessionStatus.ok);
                        inviteInfo.setStreamInfo(streamInfo);
                        inviteStreamService.updateInviteInfo(inviteInfo);
                    }
                    String filePath = snapOnPlay(streamInfo.getMediaServer(), streamInfo.getApp(),
                            streamInfo.getStream());
                    // 更新GbDevice流状态
                    GbChannelDTO update = new GbChannelDTO();
                    update.setId(rtpServerParam.getId());
                    update.setStreamKey(rtpServerParam.getStreamId());
                    update.setMediaServerId(mediaServer.getId());
                    update.setStreamStatus("1");
                    update.setSnap(filePath);
                    R<Boolean> r = remoteGb28181Service.updateGbChannelStream(update, SecurityConstants.INNER);
                    if (r.getCode() != Constants.SUCCESS) {
                        throw new RuntimeException("更新GbDevice失败");
                    }
                }
            } else {
                log.error("[创建RTP服务器] 失败, code: {}, msg: {}, result: {}", code, msg, result);
                if (callback != null) {
                    callback.run(code, msg, null);
                }
                inviteStreamService.call(InviteSessionType.PLAY, gbChannelDTO.getId(), null, code, msg, null);
                inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, gbChannelDTO.getId());
                if (result != null && result.getSsrcInfo() != null) {
                    closeRTPServer(mediaServer, result.getSsrcInfo().getStream());
                    ssrcFactory.releaseSsrc(mediaServer.getId(), result.getSsrcInfo().getSsrc());
                }
            }
        });
        if (ssrcInfo == null || ssrcInfo.getPort() <= 0) {
            log.info("[点播端口/SSRC]获取失败,设备编号:{}, 通道编号:{}, ssrcInfo: {}", gbChannelDTO.getId(), gbChannelDTO.getId(), ssrcInfo);
            if (rtpServerParam.getPresetSsrc() == null) {
                ssrcFactory.releaseSsrc(mediaServer.getId(), ssrc);
            }
            callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "获取端口或者ssrc失败", null);
            inviteStreamService.call(InviteSessionType.PLAY, gbChannelDTO.getId(), null,
                    InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(),
                    InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(), null);
            return null;
        }
        int port = ssrcInfo.getPort();
        String ip = mediaServer.getIp();
        RtpServerParam rtpServer = new RtpServerParam();
        rtpServer.setPort(port);
        rtpServer.setIp(ip);
        rtpServer.setId(rtpServerParam.getId());
        rtpServer.setSsrc(rtpServerParam.getSsrc());
        rtpServer.setGbDeviceId(gbDevice.getDeviceId());
        rtpServer.setGbChannelId(gbChannelDTO.getGbChannelId());
        rtpServer.setStreamMode(gbDevice.getStreamMode());
        rtpServer.setMediaServerId(mediaServer.getId());
        rtpServer.setApp(rtpServerParam.getApp());
        rtpServer.setStream(rtpServerParam.getStreamId());
        log.info("[国标28181点播开始(基于GbDevice)] ===============================");
        log.info("[国标28181] GbDeviceId: {}, 设备国标ID: {}, 通道国标ID: {}", gbChannelDTO.getId(),
                gbDevice.getDeviceId(), gbChannelDTO.getGbChannelId());
        log.info("[国标28181] 流模式: {}, ZLM tcpMode: {}, ssrcCheck: {}", gbDevice.getStreamMode(),
                rtpServerParam.getTcpMode(), rtpServerParam.isSsrcCheck());
        log.info("[国标28181] ZLM媒体服务器IP: {}, 收流端口: {}, 流ID: {}, SSRC: {}", ip, port, ssrcInfo.getStream(),
                ssrcInfo.getSsrc());
        log.info("[国标28181] =======================================");
        InviteInfo inviteInfo = InviteInfo.getInviteInfo(gbChannelDTO.getId().toString(), gbChannelDTO.getId(),
                ssrcInfo.getStream(), ssrcInfo, mediaServer.getId(), mediaServer.getSdpIp(), ssrcInfo.getPort(),
                gbDevice.getStreamMode(), InviteSessionType.PLAY, InviteSessionStatus.ready,
                userSetting.getRecordSip());
        if ("1".equals(gbChannelDTO.getEnableMp4())) {
            inviteInfo.setRecord(true);
        }
        inviteStreamService.updateInviteInfo(inviteInfo);
        R<Void> r = remoteGb28181Service.playStreamCmd(rtpServer, SecurityConstants.INNER);
        if (r.getCode() != Constants.SUCCESS) {
            log.info("[点播失败]{}:{} deviceId: {}, channelId:{}", r.getCode(), r.getMsg(),
                    gbChannelDTO.getGbDeviceId(), gbChannelDTO.getGbChannelId());
            inviteInfo = inviteStreamService.getInviteInfo(InviteSessionType.PLAY, gbChannelDTO.getId(),
                    rtpServerParam.getStreamId());
            if (inviteInfo != null) {
                inviteStreamService.removeInviteInfo(inviteInfo);
                if (inviteInfo.getSsrcInfo() != null) {
                    ssrcFactory.releaseSsrc(mediaServer.getId(), inviteInfo.getSsrcInfo().getSsrc());
                }
            }
            closeRTPServer(mediaServer, ssrcInfo.getStream());
            ssrcFactory.releaseSsrc(mediaServer.getId(), ssrcInfo.getSsrc());
            if (callback != null) {
                callback.run(r.getCode(), r.getMsg(), null);
            }
            inviteStreamService.call(InviteSessionType.PLAY, gbChannelDTO.getId(), null,
                    r.getCode(), r.getMsg(), null);
            inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, gbChannelDTO.getId());
            return ssrcInfo;
        }
        return ssrcInfo;
    }
    /**
     * 将 WebSocket 协议地址转换为 HTTP 协议地址
     * ws:// -> http://
     * wss:// -> https://