package com.xtrh.node.controller; import com.xtrh.node.service.WorkerService; import com.xtrh.node.config.WorkerCapabilityCondition; import org.springframework.context.annotation.Conditional; import org.springframework.http.CacheControl; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import java.nio.charset.StandardCharsets; /** * 节点 HTTP 入口只负责查询与流输出,业务许可由服务层校验。 * 播放地址包含任务号和版本,迁移时由总控向 PXHJ 回传新地址。 * 主节点兼任执行端时在同一端口提供媒体入口,UDP 网关不承担视频任务。 */ @RestController @Conditional(WorkerCapabilityCondition.class) public class WorkerController { private final WorkerService worker; public WorkerController(WorkerService worker) { this.worker = worker; } // 状态快照由服务生成,控制层不读取或修改内部任务集合。 @GetMapping("/worker/status") public Object status() { return worker.status(); } /** * 输出浏览器可直接读取的 MJPEG 流,每个分段是一张完整 JPEG。 * 先检查授权再返回成功响应,避免无效地址建立空的长连接。 * 不缓存实时画面,防止浏览器复用结束或迁移前的内容。 */ @GetMapping("/media/{taskId}/{version}/stream") public ResponseEntity stream( @PathVariable String taskId, @PathVariable long version, @RequestParam String key) { worker.frame(taskId, version, key); StreamingResponseBody body = output -> { try { while (true) { // 每帧重新取得当前状态,准备转运行后不再引用旧任务对象。 byte[] frame = worker.frame(taskId, version, key); if (frame != null) { String header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: " + frame.length + "\r\n\r\n"; output.write(header.getBytes(StandardCharsets.US_ASCII)); output.write(frame); output.write("\r\n".getBytes(StandardCharsets.US_ASCII)); output.flush(); } Thread.sleep(250); } } catch (InterruptedException interrupted) { // 容器关闭线程时保留中断标记,流循环随之结束。 Thread.currentThread().interrupt(); } catch (IllegalArgumentException inactiveTask) { // 迁移、结束或许可到期后停止旧流,不能继续发送缓存画面。 } }; return ResponseEntity.ok().cacheControl(CacheControl.noStore()) .header("Content-Type", "multipart/x-mixed-replace; boundary=frame").body(body); } }