liusuyi
2026-06-02 f652169cc5d6501511eece5df57b7b396fd7a7ab
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
package com.ard.zlm.service.impl;
 
import com.alibaba.nacos.api.model.v2.ErrorCode;
import com.ard.common.core.utils.DateUtils;
import com.ard.zlm.api.domain.DownloadFileInfo;
import com.ard.zlm.api.domain.StreamInfo;
import com.ard.zlm.api.domain.ZlmCloudRecord;
import com.ard.zlm.api.domain.ZlmMediaServer;
import com.ard.zlm.common.InviteErrorCode;
import com.ard.zlm.config.UserSetting;
import com.ard.zlm.domain.CloudRecordUrl;
import com.ard.zlm.domain.RecordInfo;
import com.ard.zlm.domain.dto.ZLMResult;
import com.ard.zlm.hook.Hook;
import com.ard.zlm.hook.HookSubscribe;
import com.ard.zlm.hook.HookType;
import com.ard.zlm.mapper.ZlmCloudRecordMapper;
import com.ard.zlm.service.*;
import com.ard.zlm.utils.DateUtil;
import com.ard.zlm.utils.ZLMRESTfulUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
 
import java.io.File;
import java.util.*;
 
/**
 * 云端录像Service业务层处理
 *
 * @author fengcheng
 * @date 2026-04-10
 */
@Slf4j
@Service
public class ZlmCloudRecordServiceImpl implements IZlmCloudRecordService {
    @Autowired
    private ZlmCloudRecordMapper zlmCloudRecordMapper;
 
    @Autowired
    private UserSetting userSetting;
 
    @Autowired
    @Lazy
    private IMediaServerService mediaServerService;
 
    @Autowired
    private Map<String, IMediaNodeServerService> nodeServerServiceMap;
 
    @Autowired
    private HookSubscribe subscribe;
 
    @Autowired
    private ZLMRESTfulUtils zlmresTfulUtils;
 
    @Autowired
    private IRedisRpcPlayService redisRpcPlayService;
 
    /**
     * 查询云端录像
     *
     * @param id 云端录像主键
     * @return 云端录像
     */
    @Override
    public ZlmCloudRecord selectZlmCloudRecordById(Long id) {
        return zlmCloudRecordMapper.selectZlmCloudRecordById(id);
    }
 
    /**
     * 查询云端录像列表
     *
     * @param zlmCloudRecord 云端录像
     * @return 云端录像
     */
    @Override
    public List<ZlmCloudRecord> selectZlmCloudRecordList(ZlmCloudRecord zlmCloudRecord) {
        if (zlmCloudRecord.getMediaServerId() != null) {
            ZlmMediaServer mediaServer = mediaServerService.getOne(zlmCloudRecord.getMediaServerId());
            if (mediaServer == null) {
                throw new RuntimeException("未找到流媒体: " + zlmCloudRecord.getMediaServerId());
            }
        }
        if (zlmCloudRecord.getQueryStartTime() != null) {
            if (!DateUtil.verification(zlmCloudRecord.getQueryStartTime(), DateUtil.formatter)) {
                throw new RuntimeException("开始时间格式错误,正确格式为: " + DateUtil.formatter);
            }
            zlmCloudRecord.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(zlmCloudRecord.getQueryStartTime()));
        }
        if (zlmCloudRecord.getQueryEndTime() != null) {
            if (!DateUtil.verification(zlmCloudRecord.getQueryEndTime(), DateUtil.formatter)) {
                throw new RuntimeException("结束时间格式错误,正确格式为: " + DateUtil.formatter);
            }
            zlmCloudRecord.setEndTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(zlmCloudRecord.getQueryEndTime()));
        }
 
        return zlmCloudRecordMapper.selectZlmCloudRecordList(zlmCloudRecord);
    }
 
    /**
     * 新增云端录像
     *
     * @param zlmCloudRecord 云端录像
     * @return 结果
     */
    @Override
    public int insertZlmCloudRecord(ZlmCloudRecord zlmCloudRecord) {
        zlmCloudRecord.setCreateTime(DateUtils.getNowDate());
        return zlmCloudRecordMapper.insertZlmCloudRecord(zlmCloudRecord);
    }
 
    /**
     * 修改云端录像
     *
     * @param zlmCloudRecord 云端录像
     * @return 结果
     */
    @Override
    public int updateZlmCloudRecord(ZlmCloudRecord zlmCloudRecord) {
        zlmCloudRecord.setUpdateTime(DateUtils.getNowDate());
        return zlmCloudRecordMapper.updateZlmCloudRecord(zlmCloudRecord);
    }
 
    /**
     * 批量删除云端录像
     *
     * @param ids 需要删除的云端录像主键
     * @return 结果
     */
    @Override
    public void deleteZlmCloudRecordByIds(Long[] ids) {
        log.info("[删除录像文件] ids: {}", Arrays.stream(ids).toArray());
 
        List<ZlmCloudRecord> cloudRecordItemList = zlmCloudRecordMapper.queryZlmCloudRecordByIds(ids);
        if (cloudRecordItemList.isEmpty()) {
            return;
        }
        List<ZlmCloudRecord> cloudRecordItemIdListForDelete = new ArrayList<>();
        StringBuilder stringBuilder = new StringBuilder();
        for (ZlmCloudRecord cloudRecordItem : cloudRecordItemList) {
            String date = new File(cloudRecordItem.getFilePath()).getParentFile().getName();
            ZlmMediaServer mediaServer = mediaServerService.getOne(cloudRecordItem.getMediaServerId());
            if (mediaServer == null) {
                throw new RuntimeException("未找到流媒体: " + cloudRecordItem.getMediaServerId());
            }
            try {
                boolean deleteResult = mediaServerService.deleteRecordDirectory(mediaServer, cloudRecordItem.getApp(),
                        cloudRecordItem.getStream(), date, cloudRecordItem.getFileName());
                if (deleteResult) {
                    log.warn("[录像文件] 删除磁盘文件成功: {}", cloudRecordItem.getFilePath());
                    cloudRecordItemIdListForDelete.add(cloudRecordItem);
                }
            } catch (RuntimeException e) {
                if (stringBuilder.length() > 0) {
                    stringBuilder.append(", ");
                }
                stringBuilder.append(cloudRecordItem.getFileName());
            }
 
        }
        if (!cloudRecordItemIdListForDelete.isEmpty()) {
            zlmCloudRecordMapper.deleteZlmCloudRecordByIds(ids);
        }
        if (stringBuilder.length() > 0) {
            stringBuilder.append(" 删除失败");
            throw new RuntimeException(stringBuilder.toString());
        }
    }
 
    /**
     * 播放云端录像
     *
     * @param id
     * @param callback
     */
    @Override
    public void loadRecord(Long id, ErrorCallback<StreamInfo> callback) {
        ZlmCloudRecord zlmCloudRecord = zlmCloudRecordMapper.selectZlmCloudRecordById(id);
        if (zlmCloudRecord == null) {
            callback.run(InviteErrorCode.FAIL.getCode(), "录像不存在", null);
            return;
        }
 
        ZlmMediaServer mediaServer = mediaServerService.getOne(zlmCloudRecord.getMediaServerId());
 
        if (mediaServer == null) {
            callback.run(InviteErrorCode.FAIL.getCode(), "无可用的节点", null);
            return;
        }
 
        loadMP4File(mediaServer, "record_file", zlmCloudRecord, ((code, msg, streamInfo) -> {
            callback.run(code, msg, streamInfo);
        }));
    }
 
    @Override
    public void closeStreams(Long id) {
        ZlmCloudRecord zlmCloudRecord = zlmCloudRecordMapper.selectZlmCloudRecordById(id);
        if (zlmCloudRecord == null) {
            throw new RuntimeException("录像不存在");
        }
 
        ZlmMediaServer mediaServer = mediaServerService.getOne(zlmCloudRecord.getMediaServerId());
 
        if (mediaServer == null) {
            throw new RuntimeException("无可用的节点");
        }
 
        IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType());
        if (mediaNodeServerService == null) {
            log.info("[closeStreams] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType());
            return;
        }
        mediaNodeServerService.closeStreams(mediaServer, "record_file", zlmCloudRecord.getStream());
    }
 
    /**
     * 定时查询待删除的录像文件
     */
    @Override
    public void task() {
        log.info("[录像文件定时清理] 开始清理过期录像文件");
 
        // 获取配置了assist的流媒体节点
        List<ZlmMediaServer> mediaServerItemList = mediaServerService.getAllOnlineMediaServe();
        if (mediaServerItemList.isEmpty()) {
            return;
        }
 
        long result = 0;
        for (ZlmMediaServer mediaServerItem : mediaServerItemList) {
            Calendar lastCalendar = Calendar.getInstance();
            if (mediaServerItem.getRecordDay() > 0) {
                lastCalendar.setTime(new Date());
                // 获取保存的最后截至日[期,因为每个节点都有一个日期,也就是支持每个节点设置不同的保存日期,
                lastCalendar.add(Calendar.DAY_OF_MONTH, -mediaServerItem.getRecordDay());
                Long lastDate = lastCalendar.getTimeInMillis();
 
                // 获取到截至日期之前的录像文件列表,文件列表满足未被收藏和保持的。这两个字段目前共能一致,
                // 为我自己业务系统相关的代码,大家使用的时候直接使用收藏(collect)这一个类型即可
                List<ZlmCloudRecord> cloudRecordItemList = zlmCloudRecordMapper.queryCloudRecordListForDelete(lastDate, mediaServerItem.getId());
                if (cloudRecordItemList.isEmpty()) {
                    continue;
                }
                // TODO 后续可以删除空了的过期日期文件夹
                for (ZlmCloudRecord cloudRecordItem : cloudRecordItemList) {
                    String date = new File(cloudRecordItem.getFilePath()).getParentFile().getName();
                    try {
                        boolean deleteResult = mediaServerService.deleteRecordDirectory(mediaServerItem, cloudRecordItem.getApp(),
                                cloudRecordItem.getStream(), date, cloudRecordItem.getFileName());
                        if (deleteResult) {
                            log.warn("[录像文件定时清理] 删除磁盘文件成功: {}", cloudRecordItem.getFilePath());
                        }
                    } catch (Exception ignored) {
 
                    }
 
                }
                List<Long> idList = cloudRecordItemList.stream().map(ZlmCloudRecord::getId).toList();
                result += zlmCloudRecordMapper.deleteZlmCloudRecordByIds(idList.toArray(Long[]::new));
            }
        }
        log.info("[录像文件定时清理] 共清理{}个过期录像文件", result);
    }
 
    /**
     * 根据id获取url
     *
     * @param ids
     * @return
     */
    @Override
    public List<CloudRecordUrl> getUrlListByIds(List<Long> ids) {
        List<ZlmCloudRecord> cloudRecordItems = zlmCloudRecordMapper.queryZlmCloudRecordByIds(ids.toArray(Long[]::new));
        if (cloudRecordItems.isEmpty()) {
            return List.of();
        }
        return getCloudRecordUrl(cloudRecordItems);
    }
 
    /**
     * 设置录像播放速度
     *
     * @param mediaServerId 使用的节点Id
     * @param app           应用名
     * @param stream        流id
     * @param speed         播放速度
     * @param schema        播放协议
     */
    @Override
    public void setRecordSpeed(String mediaServerId, String app, String stream, Integer speed, String schema) {
        ZlmMediaServer mediaServer = mediaServerService.getOne(mediaServerId);
        if (mediaServer == null) {
            throw new RuntimeException("媒体节点不存在: " + mediaServerId);
        }
        mediaServerService.setRecordSpeed(mediaServer, app, stream, speed, schema);
    }
 
    /**
     * 定位录像播放到制定位置
     *
     * @param mediaServerId 使用的节点Id
     * @param app           应用名
     * @param stream        流ID
     * @param stamp         要定位的时间位置,从录像开始的时间算起
     * @param schema        播放协议
     */
    @Override
    public void seekRecord(String mediaServerId, String app, String stream, Double stamp, String schema) {
        ZlmMediaServer mediaServer = mediaServerService.getOne(mediaServerId);
        if (mediaServer == null) {
            throw new RuntimeException("媒体节点不存在: " + mediaServerId);
        }
        mediaServerService.seekRecordStamp(mediaServer, app, stream, stamp, schema);
    }
 
    private List<CloudRecordUrl> getCloudRecordUrl(List<ZlmCloudRecord> cloudRecordItems) {
        if (cloudRecordItems.isEmpty()) {
            return List.of();
        }
        List<CloudRecordUrl> resultList = new ArrayList<>();
        for (ZlmCloudRecord cloudRecordItem : cloudRecordItems) {
            CloudRecordUrl cloudRecordUrl = new CloudRecordUrl();
            cloudRecordUrl.setId(cloudRecordItem.getId());
            cloudRecordUrl.setFileName(cloudRecordItem.getStartTime() + ".mp4");
            cloudRecordUrl.setFilePath(cloudRecordItem.getFilePath());
            if (!userSetting.getServerId().equals(cloudRecordItem.getServerId())) {
                cloudRecordUrl.setDownloadUrl(redisRpcPlayService.getRecordPlayUrl(cloudRecordItem.getServerId(), cloudRecordItem.getId()).getHttpPath());
            } else {
                ZlmMediaServer mediaServer = mediaServerService.getOne(cloudRecordItem.getMediaServerId());
                if (mediaServer == null) {
                    throw new RuntimeException("媒体节点不存在: " + cloudRecordItem.getMediaServerId());
                }
                mediaServer.setStreamIp(mediaServer.getIp());
                DownloadFileInfo downloadFilePath = mediaServerService.getDownloadFilePath(mediaServer, RecordInfo.getInstance(cloudRecordItem));
                cloudRecordUrl.setDownloadUrl(downloadFilePath.getHttpPath());
            }
            resultList.add(cloudRecordUrl);
        }
 
        return resultList;
    }
 
    private void loadMP4File(ZlmMediaServer mediaServer, String app, ZlmCloudRecord zlmCloudRecord, ErrorCallback<StreamInfo> callback) {
        IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType());
        if (mediaNodeServerService == null) {
            log.info("[loadMP4File] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType());
            throw new RuntimeException("未找到mediaServer对应的实现类");
        }
 
        StreamInfo streamData = mediaServerService.getStreamInfoByAppAndStreamWithCheck(app, zlmCloudRecord.getStream(), mediaServer.getId(), null, false);
        if (streamData != null) {
            callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamData);
            return;
        }
 
        Hook hook = Hook.getInstance(HookType.on_media_arrival, app, zlmCloudRecord.getStream(), mediaServer.getServerId());
        subscribe.addSubscribe(hook, (hookData) -> {
            StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServer, app, zlmCloudRecord.getStream(), hookData.getMediaInfo());
            if (callback != null) {
                callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo);
            }
        });
 
        ZLMResult<?> zlmResult = zlmresTfulUtils.loadMP4File(mediaServer, app, zlmCloudRecord.getStream(), zlmCloudRecord.getFilePath());
 
        if (zlmResult == null) {
            throw new RuntimeException("请求失败");
        }
        if (zlmResult.getCode() != 0) {
            throw new RuntimeException(zlmResult.getMsg());
        }
    }
}