package com.ard.zlm.service.impl; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.ard.zlm.api.config.ZLMServerConfig; import com.ard.zlm.api.domain.*; import com.ard.zlm.common.CommonCallback; import com.ard.zlm.config.UserSetting; import com.ard.zlm.domain.RecordInfo; import com.ard.zlm.domain.dto.FlagData; import com.ard.zlm.domain.dto.StreamProxyResult; import com.ard.zlm.domain.dto.ZLMResult; import com.ard.zlm.service.IMediaNodeServerService; import com.ard.zlm.utils.ZLMRESTfulUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.ObjectUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * zlm媒体节点服务接口 * * @FileName ZLMMediaNodeServerServiceImpl * @Description * @Author fengcheng * @date 2026-03-31 **/ @Slf4j @Service("zlm") public class ZLMMediaNodeServerServiceImpl implements IMediaNodeServerService { @Autowired private ZLMRESTfulUtils zlmresTfulUtils; @Autowired private UserSetting userSetting; @Override public List getMediaList(ZlmMediaServer mediaServer, String app, String stream) { List streamInfoList = new ArrayList<>(); ZLMResult zlmResult = zlmresTfulUtils.getMediaList(mediaServer, app, stream); if (zlmResult != null) { if (zlmResult.getCode() == 0) { if (zlmResult.getData() == null) { return streamInfoList; } for (int i = 0; i < zlmResult.getData().size(); i++) { JSONObject mediaJSON = zlmResult.getData().getJSONObject(i); MediaInfo mediaInfo = MediaInfo.getInstance(mediaJSON, mediaServer, userSetting.getServerId()); StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, mediaInfo.getApp(), mediaInfo.getStream(), mediaInfo, null, true); if (streamInfo != null) { streamInfoList.add(streamInfo); } } } } return streamInfoList; } @Override public StreamInfo getStreamInfoByAppAndStream(ZlmMediaServer mediaServer, String app, String stream, MediaInfo mediaInfo, String addr, boolean isPlay) { StreamInfo streamInfoResult = new StreamInfo(); streamInfoResult.setStream(stream); streamInfoResult.setApp(app); if (addr == null) { addr = mediaServer.getStreamIp(); } streamInfoResult.setIp(addr); if (mediaInfo != null) { streamInfoResult.setServerId(mediaInfo.getServerId()); } else { streamInfoResult.setServerId(userSetting.getServerId()); } streamInfoResult.setMediaServer(mediaServer); Map param = new HashMap<>(); if (mediaInfo != null && !ObjectUtils.isEmpty(mediaInfo.getOriginTypeStr())) { if (!ObjectUtils.isEmpty(mediaInfo.getOriginTypeStr())) { param.put("originTypeStr", mediaInfo.getOriginTypeStr()); } if (!ObjectUtils.isEmpty(mediaInfo.getVideoCodec())) { param.put("videoCodec", mediaInfo.getVideoCodec()); } if (!ObjectUtils.isEmpty(mediaInfo.getAudioCodec())) { param.put("audioCodec", mediaInfo.getAudioCodec()); } } StringBuilder callIdParamBuilder = new StringBuilder(); if (!param.isEmpty()) { callIdParamBuilder.append("?"); for (Map.Entry entry : param.entrySet()) { callIdParamBuilder.append(entry.getKey()).append("=").append(entry.getValue()); callIdParamBuilder.append("&"); } callIdParamBuilder.deleteCharAt(callIdParamBuilder.length() - 1); } String callIdParam = callIdParamBuilder.toString(); streamInfoResult.setRtmp(addr, mediaServer.getRtmpPort(), mediaServer.getRtmpSslPort(), app, stream, callIdParam); streamInfoResult.setRtsp(addr, mediaServer.getRtspPort(), mediaServer.getRtspSslPort(), app, stream, callIdParam); String flvFile = String.format("%s/%s.live.flv%s", app, stream, callIdParam); streamInfoResult.setFlv(addr, mediaServer.getHttpPort(), mediaServer.getHttpSslPort(), flvFile); streamInfoResult.setWsFlv(addr, mediaServer.getHttpPort(), mediaServer.getHttpSslPort(), flvFile); String mp4File = String.format("%s/%s.live.mp4%s", app, stream, callIdParam); streamInfoResult.setFmp4(addr, mediaServer.getHttpPort(), mediaServer.getHttpSslPort(), mp4File); streamInfoResult.setWsMp4(addr, mediaServer.getHttpPort(), mediaServer.getHttpSslPort(), mp4File); streamInfoResult.setHls(addr, mediaServer.getHttpPort(), mediaServer.getHttpSslPort(), app, stream, callIdParam); streamInfoResult.setWsHls(addr, mediaServer.getHttpPort(), mediaServer.getHttpSslPort(), app, stream, callIdParam); streamInfoResult.setTs(addr, mediaServer.getHttpPort(), mediaServer.getHttpSslPort(), app, stream, callIdParam); streamInfoResult.setWsTs(addr, mediaServer.getHttpPort(), mediaServer.getHttpSslPort(), app, stream, callIdParam); streamInfoResult.setRtc(addr, mediaServer.getHttpPort(), mediaServer.getHttpSslPort(), app, stream, callIdParam, isPlay); streamInfoResult.setMediaInfo(mediaInfo); if (!"broadcast".equalsIgnoreCase(app) && !ObjectUtils.isEmpty(mediaServer.getTranscodeSuffix()) && !"null".equalsIgnoreCase(mediaServer.getTranscodeSuffix())) { String newStream = stream + "_" + mediaServer.getTranscodeSuffix(); mediaServer.setTranscodeSuffix(null); StreamInfo transcodeStreamInfo = getStreamInfoByAppAndStream(mediaServer, app, newStream, null, addr, isPlay); streamInfoResult.setTranscodeStream(transcodeStreamInfo); } return streamInfoResult; } @Override public String startProxy(ZlmMediaServer mediaServer, StreamPullPlay streamPullPlay) { ZLMResult zlmResult = zlmresTfulUtils.addStreamProxy( mediaServer, streamPullPlay.getApp(), streamPullPlay.getStream(), streamPullPlay.getUrl(), streamPullPlay.isEnable_audio(), streamPullPlay.isEnable_mp4(), streamPullPlay.getRtp_type(), streamPullPlay.getTimeOut()); if (zlmResult.getCode() != 0) { throw new RuntimeException(zlmResult.getMsg()); } else { StreamProxyResult data = zlmResult.getData(); if (data == null) { throw new RuntimeException("代理结果异常: " + zlmResult); } else { return data.getKey(); } } } @Override public void stopProxy(ZlmMediaServer mediaServer, String streamKey) { ZLMResult zlmResult = zlmresTfulUtils.delStreamProxy(mediaServer, streamKey); if (zlmResult == null) { throw new RuntimeException("请求失败"); } else if (zlmResult.getCode() != 0) { throw new RuntimeException(zlmResult.getMsg()); } } @Override public void getSnap(ZlmMediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName) { String streamUrl; if (mediaServer.getRtspPort() != 0) { streamUrl = String.format("rtsp://127.0.0.1:%s/%s/%s", mediaServer.getRtspPort(), app, stream); } else { streamUrl = String.format("http://127.0.0.1:%s/%s/%s.live.mp4", mediaServer.getHttpPort(), app, stream); } zlmresTfulUtils.getSnap(mediaServer, streamUrl, timeoutSec, expireSec, path, fileName); } @Override public void getSnap(ZlmMediaServer mediaServer, String streamUrl, int timeoutSec, int expireSec, String path, String fileName) { zlmresTfulUtils.getSnap(mediaServer, streamUrl, timeoutSec, expireSec, path, fileName); } @Override public void closeRtpServer(ZlmMediaServer mediaServer, String streamId, CommonCallback callback) { if (mediaServer == null) { if (callback != null) { callback.run(false); } return; } Map param = new HashMap<>(); param.put("stream_id", streamId); zlmresTfulUtils.closeRtpServer(mediaServer, param, zlmResult -> { if (zlmResult.getCode() == 0) { if (callback != null) { callback.run(zlmResult.getHit() >= 1); } return; } else { log.error("关闭RTP Server 失败: " + zlmResult.getMsg()); } if (callback != null) { callback.run(false); } }); } @Override public MediaInfo getMediaInfo(ZlmMediaServer mediaServer, String app, String stream) { ZLMResult zlmResult = zlmresTfulUtils.getMediaInfo(mediaServer, app, "rtsp", stream); if (zlmResult.getCode() != 0 || zlmResult.getData() == null || zlmResult.getData().getString("app") == null) { return null; } return MediaInfo.getInstance(zlmResult.getData(), mediaServer, userSetting.getServerId()); } @Override public void closeStreams(ZlmMediaServer mediaServer, String app, String stream) { zlmresTfulUtils.closeStreams(mediaServer, app, stream); } @Override public ZlmMediaServer checkMediaServer(String ip, int port, String secret) { // 1. 初始化基础信息 ZlmMediaServer mediaServer = new ZlmMediaServer(); mediaServer.setServerId(userSetting.getServerId()); mediaServer.setIp(ip); // 用于API调用的IP mediaServer.setHttpPort(port); // 传入的HTTP端口 mediaServer.setSecret(secret); mediaServer.setType("zlm"); // 2. 调用 ZLM API 获取配置 ZLMResult> mediaServerConfigResult = zlmresTfulUtils.getMediaServerConfig(mediaServer); if (mediaServerConfigResult == null || mediaServerConfigResult.getCode() != 0) { throw new RuntimeException("连接ZLMediaKit失败或返回异常"); } List configList = mediaServerConfigResult.getData(); if (configList == null || configList.isEmpty()) { throw new RuntimeException("读取ZLMediaKit配置失败:返回数据为空"); } // 3. 解析配置对象 ZLMServerConfig zlmServerConfig = JSON.parseObject(JSON.toJSONString(configList.get(0)), ZLMServerConfig.class); if (zlmServerConfig == null) { throw new RuntimeException("解析ZLMediaKit配置对象失败"); } // 4. 将 ZLM 返回的真实配置映射到 mediaServer 对象 mediaServer.setId(zlmServerConfig.getGeneralMediaServerId()); // 优先使用 ZLM 返回的真实端口,而不是传入的 port mediaServer.setHttpPort(zlmServerConfig.getHttpPort()); mediaServer.setHttpSslPort(zlmServerConfig.getHttpSSLport()); mediaServer.setRtmpPort(zlmServerConfig.getRtmpPort()); mediaServer.setRtmpSslPort(zlmServerConfig.getRtmpSslPort()); mediaServer.setRtspPort(zlmServerConfig.getRtspPort()); mediaServer.setRtspSslPort(zlmServerConfig.getRtspSSlport()); mediaServer.setRtpProxyPort(zlmServerConfig.getRtpProxyPort()); // 5. 修正 IP 赋值逻辑 // streamIp 应该是前端播放器能访问到的 IP(如果是公网部署,这里可能需要配置为公网IP) mediaServer.setStreamIp(ip); // hookIp 应该设置为你的业务后端(或WVP)能被 ZLM 访问到的 IP // 建议从配置文件读取,而不是硬编码 127.0.0.1 mediaServer.setHookIp("127.0.0.1"); // sdpIp 用于 GB28181 的 SDP 交互,通常与 streamIp 保持一致 mediaServer.setSdpIp(ip); return mediaServer; } @Override public boolean deleteRecordDirectory(ZlmMediaServer mediaServer, String app, String stream, String date, String fileName) { log.info("[zlm-deleteRecordDirectory] 删除磁盘文件, server: {} {}:{}->{}/{}", mediaServer.getId(), app, stream, date, fileName); ZLMResult zlmResult = zlmresTfulUtils.deleteRecordDirectory(mediaServer, app, stream, date, fileName); if (zlmResult.getCode() == 0) { return true; } else { log.info("[zlm-deleteRecordDirectory] 删除磁盘文件错误, server: {} {}:{}->{}/{}, 结果: {}", mediaServer.getId(), app, stream, date, fileName, zlmResult); throw new RuntimeException("删除磁盘文件失败"); } } @Override public DownloadFileInfo getDownloadFilePath(ZlmMediaServer mediaServerItem, RecordInfo recordInfo) { // 将filePath作为独立参数传入,避免%符号解析问题 String pathTemplate = "%s://%s:%s/index/api/downloadFile?file_path=%s"; DownloadFileInfo info = new DownloadFileInfo(); // filePath作为第4个参数 info.setHttpPath(String.format(pathTemplate, "http", mediaServerItem.getStreamIp(), mediaServerItem.getHttpPort(), recordInfo.getFilePath())); // 同样作为第4个参数 if (mediaServerItem.getHttpSslPort() > 0) { info.setHttpsPath(String.format(pathTemplate, "https", mediaServerItem.getStreamIp(), mediaServerItem.getHttpSslPort(), recordInfo.getFilePath())); } return info; } @Override public void seekRecordStamp(ZlmMediaServer mediaServer, String app, String stream, Double stamp, String schema) { ZLMResult zlmResult = zlmresTfulUtils.seekRecordStamp(mediaServer, app, stream, stamp, schema); if (zlmResult == null) { throw new RuntimeException("请求失败"); } if (zlmResult.getCode() != 0) { throw new RuntimeException(zlmResult.getMsg()); } } @Override public void setRecordSpeed(ZlmMediaServer mediaServer, String app, String stream, Integer speed, String schema) { ZLMResult zlmResult = zlmresTfulUtils.setRecordSpeed(mediaServer, app, stream, speed, schema); if (zlmResult == null) { throw new RuntimeException("请求失败"); } if (zlmResult.getCode() != 0) { throw new RuntimeException(zlmResult.getMsg()); } } @Override public void startRecord(ZlmMediaServer mediaServer, String app, String stream) { ZLMResult zlmResult = zlmresTfulUtils.startRecord(mediaServer, app, stream); if (zlmResult == null) { throw new RuntimeException("请求失败"); } if (zlmResult.getCode() != 0) { throw new RuntimeException(zlmResult.getMsg()); } } @Override public void stopRecord(ZlmMediaServer mediaServer, String app, String stream) { ZLMResult zlmResult = zlmresTfulUtils.stopRecord(mediaServer, app, stream); if (zlmResult == null) { throw new RuntimeException("请求失败"); } if (zlmResult.getCode() != 0) { throw new RuntimeException(zlmResult.getMsg()); } } @Override public ZLMResult getThreadsLoad(ZlmMediaServer mediaServer) { return zlmresTfulUtils.getThreadsLoad(mediaServer); } @Override public ZLMResult getWorkThreadsLoad(ZlmMediaServer mediaServer) { return zlmresTfulUtils.getWorkThreadsLoad(mediaServer); } /** * 重启流媒体 * * @param mediaServer 流媒体 * @return */ @Override public void restartServer(ZlmMediaServer mediaServer) { zlmresTfulUtils.restartServer(mediaServer); } /** * 连接rtp服务 * * @param mediaServer * @param address * @param port * @param stream * @return */ @Override public Boolean connectRtpServer(ZlmMediaServer mediaServer, String address, int port, String stream) { ZLMResult zlmResult = zlmresTfulUtils.connectRtpServer(mediaServer, address, port, stream); log.info("[TCP主动连接对方] 结果: {}", zlmResult); return zlmResult.getCode() == 0; } }