1 files added
14 files modified
| | |
| | | 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.*; |
| | |
| | | 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 设备国标编号 |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | @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()); |
| | | } |
| New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | |
| | | @Autowired |
| | | private RemoteZlmService remoteZlmService; |
| | | |
| | | @Autowired |
| | | private IGbDeviceService gbDeviceService; |
| | | |
| | | @Autowired |
| | | private IGbChannelService gbChannelService; |
| | | |
| | | /** |
| | | * 根据设备id获取设备 |
| | |
| | | } |
| | | 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 + " 条"); |
| | | } |
| | | } |
| | |
| | | |
| | | 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; |
| | |
| | | |
| | | @Autowired |
| | | private IRedisCatchStorage redisCatchStorage; |
| | | |
| | | @Autowired |
| | | private IGbChannelService gbChannelService; |
| | | |
| | | /** |
| | | * 根据设备id清楚设备通道 |
| | |
| | | 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); |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | 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; |
| | |
| | | |
| | | @Autowired |
| | | private RemoteQsDeviceService remoteQsDeviceService; |
| | | |
| | | @Autowired |
| | | private IGbDeviceService gbDeviceService; |
| | | |
| | | @Autowired |
| | | private IGbChannelService gbChannelService; |
| | | |
| | | /** |
| | | * 查询设备信息 |
| | |
| | | } 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 |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | package com.ard.work.device.camera.domain; |
| | | |
| | | import com.alibaba.fastjson2.annotation.JSONField; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Builder; |
| | | import lombok.Data; |
| | |
| | | **/ |
| | | @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; |
| | | } |
| | |
| | | |
| | | 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缓存管理器 |
| | |
| | | * @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]; |
| | | } |
| | | } |
| | |
| | | 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 |
| | |
| | | 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()); |
| | |
| | | dto.setT(ptz.getT()); |
| | | dto.setZ(ptz.getZ()); |
| | | } |
| | | |
| | | return dto; |
| | | } |
| | | |
| | |
| | | && !EXCLUDE_FACTORY.contains(camera.getFactory()) |
| | | && SUPPORT_TYPES.contains(camera.getType()); |
| | | } |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | |
| | | 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); |
| | | } |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | |
| | | 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; |
| | | |
| | |
| | | @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; |
| | |
| | | } |
| | | 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+$"; |
| | |
| | | return matcher.matches(); |
| | | }).map(Map.Entry::getValue).forEach(session -> PTZWebSocketUtils.sendMessage(session, message)); |
| | | } |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | * @param id 设备id |
| | | * @return |
| | | */ |
| | | @Operation(summary = "gb28181播放") |
| | | @GetMapping("/startGb28181Play/{id}") |
| | | public DeferredResult<R<StreamContent>> startGb28181Play( |
| | | HttpServletRequest request, |
| | |
| | | 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); |
| | | } |
| | | |
| | | } |
| | |
| | | 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.*; |
| | |
| | | */ |
| | | 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); |
| | | } |
| | |
| | | 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; |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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播放 |
| | |
| | | } |
| | | |
| | | /** |
| | | * 开启国标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:// |