liusuyi
2026-06-01 a2e7e8ff9cfaa69b001d483710bddbda50d55c91
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
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<StreamInfo> getMediaList(ZlmMediaServer mediaServer, String app, String stream) {
        List<StreamInfo> streamInfoList = new ArrayList<>();
        ZLMResult<JSONArray> 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<String, String> 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<String, String> 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<StreamProxyResult> 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<FlagData> 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<Boolean> callback) {
        if (mediaServer == null) {
            if (callback != null) {
                callback.run(false);
            }
            return;
        }
        Map<String, Object> 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<JSONObject> 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<List<JSONObject>> mediaServerConfigResult = zlmresTfulUtils.getMediaServerConfig(mediaServer);
        if (mediaServerConfigResult == null || mediaServerConfigResult.getCode() != 0) {
            throw new RuntimeException("连接ZLMediaKit失败或返回异常");
        }
 
        List<JSONObject> 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;
    }
}