nishiwei
5 days ago 3f43e40ed1a1e9f66b3ad2862da9dbc4b710c712
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
package com.ard.sample.manage.service.impl;
 
import com.ard.common.core.redis.RedisConst;
import com.ard.common.core.redis.RedisOpsUtils;
import com.ard.sample.common.model.ResponseResult;
import com.ard.sample.component.websocket.enums.MessageType;
import com.ard.sample.component.websocket.handler.DockWebSocketHandler;
import com.ard.sample.component.websocket.handler.LiveStreamHandler;
import com.ard.sample.manage.model.dto.DeviceDTO;
import com.ard.sample.manage.model.dto.LiveDTO;
import com.ard.sample.manage.model.dto.LiveTypeDTO;
import com.ard.sample.manage.service.IDeviceService;
import com.ard.sample.manage.service.ILiveStreamService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
 
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
 
@Slf4j
@Component
public class DockOnlineServiceImpl {
 
    @Autowired
    private IDeviceService _deviceService;
 
    @Autowired
    private DockWebSocketHandler _dockWebSocketHandler;
 
    @Autowired
    protected ObjectMapper _objectMapper;
 
    @Autowired
    private LiveStreamHandler _liveStreamHandler;
 
    @Autowired
    private ILiveStreamService _liveStreamService;
 
    /**
     * 定时任务,每2秒取一次机场设备的在线状态,如已离线更新状态
     * */
    @Scheduled(cron = "0/2 * * * * ?")
    public void cronOnlineTask() {
        List<DeviceDTO> devicesList = _deviceService.getDevicesTopoForWeb();
        for (DeviceDTO device : devicesList) {
            //String key = "online:" + device.getDeviceSn();
            //System.out.println("在线状态:"+RedisOpsUtils.checkExist(key));
            // 更新机场设备状态:"3"-"设备已离线",
            if (!_deviceService.checkDeviceOnline(device.getDeviceSn())) {
                _deviceService.updateWorkType(device.getDeviceSn(), 3L);
                RedisOpsUtils.set(RedisConst.OSD_STATUS_PREFIX + device.getDeviceSn(), 3);
                RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + device.getDeviceSn());
                RedisOpsUtils.del(RedisConst.COVER_STATE_PREFIX + device.getDeviceSn());
                clearLiveRetry(device.getDeviceSn());
            } else {
                tryStartLiveStreaming(device.getDeviceSn());
            }
 
            // 机场1:未绑定飞行器时 childDeviceSn 为空,跳过,避免无效的在线判断和起流
            String childDeviceSn = device.getChildDeviceSn();
            if (childDeviceSn == null || childDeviceSn.isEmpty()) {
                continue;
            }
            if (!_deviceService.checkDeviceOnline(childDeviceSn)) {
                _deviceService.updateWorkType(childDeviceSn, 6L);
                RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + childDeviceSn);
                RedisOpsUtils.del(RedisConst.COVER_STATE_PREFIX + childDeviceSn);
                clearLiveRetry(childDeviceSn);
            } else if (isDronePoweredOff(childDeviceSn)) {
                // 飞行器舱内关机(6)或开机中(7)时相机尚未就绪,起流必然失败(如 513006),直接跳过
                clearLiveRetry(childDeviceSn);
            } else {
                tryStartLiveStreaming(childDeviceSn);
            }
        }
    }
 
    /**
     * 按退避策略尝试开启直播。
     * 原实现每 2 秒对未就绪设备执行一次“停-开”,开机阶段会刷出大量 513006/613011 日志;
     * 这里失败后按 2s、4s、8s、16s、30s 递增间隔重试。
     */
    private void tryStartLiveStreaming(String sn) {
        if (!canRetryLive(sn)) {
            return;
        }
        markLiveRetry(sn, StartLiveStreaming(sn));
    }
 
    /**
     * 飞行器是否处于关机或开机中状态。
     * 6-舱内关机(机场 OSD 写入),7-开机中(下发 drone_open 时写入)。
     */
    private boolean isDronePoweredOff(String childDeviceSn) {
        Object status = RedisOpsUtils.get(RedisConst.OSD_STATUS_PREFIX + childDeviceSn);
        if (status == null) {
            return false;
        }
        String value = String.valueOf(status);
        return "6".equals(value) || "7".equals(value);
    }
 
    /**
     * 连续失败次数与下次允许重试的时间戳。
     */
    private static class LiveRetryState {
        private int failures;
        private long nextRetryTime;
    }
 
    private static final long LIVE_RETRY_BASE_DELAY_MS = 2000L;
    private static final long LIVE_RETRY_MAX_DELAY_MS = 30000L;
 
    private final Map<String, LiveRetryState> liveRetryStates = new ConcurrentHashMap<>();
 
    private boolean canRetryLive(String sn) {
        LiveRetryState state = liveRetryStates.get(sn);
        return state == null || System.currentTimeMillis() >= state.nextRetryTime;
    }
 
    private void markLiveRetry(String sn, boolean success) {
        if (success) {
            liveRetryStates.remove(sn);
            return;
        }
        LiveRetryState state = liveRetryStates.computeIfAbsent(sn, k -> new LiveRetryState());
        state.failures++;
        long delay = Math.min(LIVE_RETRY_BASE_DELAY_MS << Math.min(state.failures - 1, 4), LIVE_RETRY_MAX_DELAY_MS);
        state.nextRetryTime = System.currentTimeMillis() + delay;
        log.info("SN: {}-直播启动失败(第 {} 次),{} 秒后重试", sn, state.failures, delay / 1000);
    }
 
    private void clearLiveRetry(String sn) {
        liveRetryStates.remove(sn);
    }
 
    /**
     * 定时任务,每1秒读一次redis的机场设备的状态,并发送到websocket客户端
     * */
    //@Scheduled(cron = "0/1 * * * * ?")
    @Scheduled(initialDelay = 2, fixedRate = 3, timeUnit = TimeUnit.SECONDS)
    public void sendDeviceStatus(){
        // 测试 机场oss读取文件
//        URL url = _ossService.getObjectUrl("kmz","/2025/11/25/新建航点航线(1)_20251125161322A001.kmz");
//        log.info("机场任务航线文件url地址:{}", url.toString());
        // 发送设备状态到websocket客户端
        List<DeviceDTO> devicesList = _deviceService.getDevicesTopoForWeb();
        for (DeviceDTO device : devicesList) {
            String dockStatus = "";
            String droneStatus = "";
            // 判断redis中是否存在设备状态数据
            if(RedisOpsUtils.checkExist(RedisConst.OSD_STATUS_PREFIX + device.getDeviceSn())){
                dockStatus = String.valueOf(RedisOpsUtils.get(RedisConst.OSD_STATUS_PREFIX + device.getDeviceSn()));
            }
            if(RedisOpsUtils.checkExist(RedisConst.OSD_STATUS_PREFIX + device.getChildDeviceSn())){
                droneStatus = String.valueOf(RedisOpsUtils.get(RedisConst.OSD_STATUS_PREFIX + device.getChildDeviceSn()));
            }
            String json = getJSONString(MessageType.STATE.getType(), device.getDeviceSn(), dockStatus, droneStatus);
 
            // 发送给websocket客户端
            _dockWebSocketHandler.sendMessageToSubscribers(device.getDeviceSn(), MessageType.STATE.getType(), json, "");
//            log.info("发送设备状态{}到websocket客户端:{}", MessageType.STATE.getType(), json);
        }
    }
 
    public String getJSONString(String messageType, String dockSn, String dockStatus, String droneStatus){
        Map<String, Object> json = new HashMap<>();
        json.put("code", 200);
        json.put("gateway", dockSn);
        json.put("type", messageType);
        json.put("message", "success");
 
        Map<String, String> data = new HashMap<>();
        data.put("dockstate", dockStatus);
        data.put("dronestate", droneStatus);
 
        json.put("data", data);
 
        // 将Map转换为JSON字符串
        String jsonString = "";
        try{
            jsonString = _objectMapper.writeValueAsString(json);
        }
        catch (Exception e){
            e.printStackTrace();
        }
 
        return jsonString;
    }
 
    /**
     * 开启直播(后台定时任务调用)。
     *
     * @return true-已存在可用地址或起流成功;false-起流失败
     */
    public boolean StartLiveStreaming(String sn) {
        //调用开始直播
        ResponseResult rlive = null;
        String resultLive = "";
        try {
            // 获取直播参数
            LiveTypeDTO param = _liveStreamHandler.getLiveParam(sn);
            // 判断param是否为空,不为空则返回正确的直播参数,为null则未获取直播参数,不为空则继续操作
            if (param == null) {
                log.info("SN: {}-未获取到直播参数param为null,无法开启直播!", sn);
                return false;
            }
            // 如果存在直播则读取直播地址返回
            if (RedisOpsUtils.checkExist(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + sn)) {
                resultLive = String.valueOf(RedisOpsUtils.get(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + sn));
//                log.info("SN: {}-redis中存在相机的播放地址信息,直接返回地址!", sn);
                return true;
            } else {
//                log.info("SN: {}-redis中未找到相机的播放地址信息!停止直播后再开启直播", sn);
                _liveStreamHandler.StopLiveStreaming(sn);
                // 如果不存在直播,则开始直播,并返回直播地址
                rlive = _liveStreamService.liveStart(param);
                if (rlive.getCode() == 0) {
                    // 直播成功后,在redis中存一个标记,将videoid保留,留给停止直播使用
                    log.info("SN: {}-开启直播成功!", sn);
                    rlive.setGateway(sn);
                    rlive.setType(MessageType.LIVESTREAM.getType());
                    resultLive = _objectMapper.writeValueAsString(rlive);
                    RedisOpsUtils.set(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + sn, resultLive);
                    LiveDTO live = (LiveDTO) rlive.getData();
                    Boolean isWeb = _deviceService.updateWebrtcUrl(sn,live.getWebrtcUrl());
//                    if (isWeb) {
//                        log.info("SN: {}-更新webrtc地址成功!", sn);
//                    }
                    // 定时任务是后台自动起流,前端并没有发订阅,必须主动把地址推给已订阅该设备的页面;
                    // 否则飞行器起飞后相机就绪、后台起流成功,页面却收不到地址,只能刷新后才出画面
                    _dockWebSocketHandler.sendMessageToSubscribers(sn, MessageType.LIVESTREAM.getType(), resultLive, "");
                    return true;
                }
                log.info("SN: {}-开启直播失败!返回值:{}", sn, rlive);
            }
        } catch (Exception e) {
            log.info("订时任务推流-SN: {}-开启直播失败!原因:{}", sn, e.getMessage());
            e.printStackTrace();
        }
        return false;
    }
}