From f4f4fc53260eb67483dce406a963628273786a61 Mon Sep 17 00:00:00 2001
From: liusuyi <1951119284@qq.com>
Date: Sat, 30 May 2026 17:22:17 +0800
Subject: [PATCH] 优化

---
 ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/service/IMediaServerService.java                   |   10 
 ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/service/impl/MediaServerServiceImpl.java           |  287 ++++++++++++++
 ard-modules/ard-modules-work/src/main/java/com/ard/work/websocket/utils/PTZWebSocketUtils.java           |   93 +--
 ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzTask.java               |   71 ++
 ard-modules/ard-modules-work/src/main/java/com/ard/work/config/AsyncConfiguration.java                   |   17 
 ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/domain/PtzParamDTO.java            |   17 
 ard-modules/ard-modules-agent/src/main/java/com/ard/agent/controller/ChatClientController.java           |   45 ++
 ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/controller/ZlmController.java                      |  147 +++++++
 ard-api/ard-api-gb28181/src/main/java/com/ard/gb28181/api/factory/RemoteGb28181FallbackFactory.java      |   27 +
 ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzDataCollector.java      |   65 ++
 ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/api/Gb28181ApiController.java              |  190 +++++++++
 ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzCacheManager.java       |   51 +
 ard-api/ard-api-gb28181/src/main/java/com/ard/gb28181/api/RemoteGb28181Service.java                      |   32 +
 ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/service/impl/DeviceChannelServiceImpl.java |   55 ++
 ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/service/impl/DeviceServiceImpl.java        |   39 +
 15 files changed, 1,037 insertions(+), 109 deletions(-)

diff --git a/ard-api/ard-api-gb28181/src/main/java/com/ard/gb28181/api/RemoteGb28181Service.java b/ard-api/ard-api-gb28181/src/main/java/com/ard/gb28181/api/RemoteGb28181Service.java
index da3574a..03b561f 100644
--- a/ard-api/ard-api-gb28181/src/main/java/com/ard/gb28181/api/RemoteGb28181Service.java
+++ b/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     设备国标编号
diff --git a/ard-api/ard-api-gb28181/src/main/java/com/ard/gb28181/api/factory/RemoteGb28181FallbackFactory.java b/ard-api/ard-api-gb28181/src/main/java/com/ard/gb28181/api/factory/RemoteGb28181FallbackFactory.java
index bd159d3..1000237 100644
--- a/ard-api/ard-api-gb28181/src/main/java/com/ard/gb28181/api/factory/RemoteGb28181FallbackFactory.java
+++ b/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());
             }
diff --git a/ard-modules/ard-modules-agent/src/main/java/com/ard/agent/controller/ChatClientController.java b/ard-modules/ard-modules-agent/src/main/java/com/ard/agent/controller/ChatClientController.java
new file mode 100644
index 0000000..f340113
--- /dev/null
+++ b/ard-modules/ard-modules-agent/src/main/java/com/ard/agent/controller/ChatClientController.java
@@ -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();
+    }
+}
\ No newline at end of file
diff --git a/ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/api/Gb28181ApiController.java b/ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/api/Gb28181ApiController.java
index bbaa09c..6e8a4ce 100644
--- a/ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/api/Gb28181ApiController.java
+++ b/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 + " 条");
+    }
 }
diff --git a/ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/service/impl/DeviceChannelServiceImpl.java b/ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/service/impl/DeviceChannelServiceImpl.java
index 9078f9d..1344e69 100644
--- a/ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/service/impl/DeviceChannelServiceImpl.java
+++ b/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);
+        }
     }
 
     /**
diff --git a/ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/service/impl/DeviceServiceImpl.java b/ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/service/impl/DeviceServiceImpl.java
index f60ea6e..6d6390d 100644
--- a/ard-modules/ard-modules-gb28181/src/main/java/com/ard/gb28181/service/impl/DeviceServiceImpl.java
+++ b/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
diff --git a/ard-modules/ard-modules-work/src/main/java/com/ard/work/config/AsyncConfiguration.java b/ard-modules/ard-modules-work/src/main/java/com/ard/work/config/AsyncConfiguration.java
index 896ac64..cdc6717 100644
--- a/ard-modules/ard-modules-work/src/main/java/com/ard/work/config/AsyncConfiguration.java
+++ b/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;
+    }
 }
\ No newline at end of file
diff --git a/ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/domain/PtzParamDTO.java b/ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/domain/PtzParamDTO.java
index 89e3e43..2ba946a 100644
--- a/ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/domain/PtzParamDTO.java
+++ b/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;
 }
diff --git a/ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzCacheManager.java b/ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzCacheManager.java
index 9ba82b3..02c6af6 100644
--- a/ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzCacheManager.java
+++ b/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);
     }
-}
\ No newline at end of file
+
+    /** 清理过期数据,返回清理条数 */
+    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];
+    }
+}
diff --git a/ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzDataCollector.java b/ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzDataCollector.java
index 432300e..7ed9362 100644
--- a/ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzDataCollector.java
+++ b/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());
     }
-}
\ No newline at end of file
+}
diff --git a/ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzTask.java b/ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzTask.java
index 546430a..273e488 100644
--- a/ard-modules/ard-modules-work/src/main/java/com/ard/work/device/camera/service/PtzTask.java
+++ b/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);
         }
     }
-}
\ No newline at end of file
+
+    /**
+     * 清理过期缓存,防止已禁用/删除的相机数据残留
+     */
+    @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;
+        }
+    }
+}
diff --git a/ard-modules/ard-modules-work/src/main/java/com/ard/work/websocket/utils/PTZWebSocketUtils.java b/ard-modules/ard-modules-work/src/main/java/com/ard/work/websocket/utils/PTZWebSocketUtils.java
index 905f768..3ae10a5 100644
--- a/ard-modules/ard-modules-work/src/main/java/com/ard/work/websocket/utils/PTZWebSocketUtils.java
+++ b/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));
     }
-}
\ No newline at end of file
+}
diff --git a/ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/controller/ZlmController.java b/ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/controller/ZlmController.java
index fb017db..940b32e 100644
--- a/ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/controller/ZlmController.java
+++ b/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);
+    }
+
 }
diff --git a/ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/service/IMediaServerService.java b/ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/service/IMediaServerService.java
index 05949f3..7d49b49 100644
--- a/ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/service/IMediaServerService.java
+++ b/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);
 }
diff --git a/ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/service/impl/MediaServerServiceImpl.java b/ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/service/impl/MediaServerServiceImpl.java
index e95947c..f43e858 100644
--- a/ard-modules/ard-modules-zlm/src/main/java/com/ard/zlm/service/impl/MediaServerServiceImpl.java
+++ b/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://

--
Gitblit v1.9.3