瀏覽代碼

feat(d2-02): ControlHttpServer /debug/preview/stream 推流分支——专用后台线程抓帧→JPEG→multipart,不阻塞 HttpListener,退出标记 StreamBroken

huangjie 1 天之前
父節點
當前提交
4927fc5518
共有 1 個文件被更改,包括 76 次插入0 次删除
  1. 76 0
      ivf_tl_operate_2.0/control/ivf_tl_ControlHost/ControlHttpServer.cs

+ 76 - 0
ivf_tl_operate_2.0/control/ivf_tl_ControlHost/ControlHttpServer.cs

@@ -139,6 +139,19 @@ namespace IvfTl.ControlHost
                         body = JsonConvert.SerializeObject(r);
                     }
                     break;
+                case "/debug/preview/stream":
+                    if (method != "GET") { code = 405; body = Err("method not allowed"); break; }
+                    {
+                        string sid = ctx.Request.QueryString["sessionId"];
+                        if (_debug == null || sid == null || !_debug.TryGet(sid, out var session))
+                        {
+                            code = 404; body = Err("session not found");
+                            break;  // 走统一收尾返回 404
+                        }
+                        // 校验通过:分流起后台推流线程,不走统一收尾(那会 Close 流终止推流)。
+                        StartPreviewStream(ctx, session);
+                        return;
+                    }
                 default:
                     code = 404; body = Err("not found");
                     break;
@@ -189,6 +202,69 @@ namespace IvfTl.ControlHost
             catch (Exception ex) { _log("解析 body 异常:" + ex.Message); return null; }
         }
 
+        /// <summary>
+        /// 推流分支:起专用后台线程,抓帧→JPEG→multipart 持续写。
+        /// HttpListener 工作线程立即返回(本方法起线程后即返回),不被推流阻塞。
+        /// 任何退出路径都标记 session.StreamBroken,会话靠心跳 TTL 看门狗最终回收(spec §7)。
+        /// </summary>
+        private void StartPreviewStream(HttpListenerContext ctx, IvfTl.ControlHost.Debug.DebugSession session)
+        {
+            var resp = ctx.Response;
+            resp.StatusCode = 200;
+            resp.ContentType = IvfTl.ControlHost.Debug.MjpegStreamWriter.ContentType;
+            resp.SendChunked = true;  // 流式,长度未知
+            resp.Headers.Add("Cache-Control", "no-cache");
+
+            var t = new Thread(() =>
+            {
+                int errCount = 0;
+                var cam = session.Lease?.Camera;
+                try
+                {
+                    if (cam == null) { _log($"[debug] 推流舱{session.HouseSn} 无相机句柄,放弃"); return; }
+                    cam.SetOpMode(1);  // 实时模式(0=单帧/1=实时,见 ICamera 注释)
+                    var outStream = resp.OutputStream;
+                    while (true)
+                    {
+                        // 会话已被回收(release/超时)→ 停推流
+                        if (!_debug.TryGet(session.SessionId, out _)) { _log($"[debug] 推流舱{session.HouseSn} 会话已失效,停"); break; }
+                        try
+                        {
+                            byte[] bgr = cam.GrabStable();   // 走全进程相机锁,与采集/对焦串行
+                            if (bgr == null) { Thread.Sleep(100); continue; }
+                            byte[] jpeg = IvfTl.ControlHost.Debug.MjpegStreamWriter.EncodeJpeg(bgr, cam.Width, cam.Height);
+                            if (jpeg == null) { Thread.Sleep(100); continue; }
+                            byte[] frame = IvfTl.ControlHost.Debug.MjpegStreamWriter.FrameBytes(jpeg);
+                            outStream.Write(frame, 0, frame.Length);
+                            outStream.Flush();
+                            errCount = 0;
+                            Thread.Sleep(66);  // ~15fps(spec §4.2)
+                        }
+                        catch (IOException) { _log($"[debug] 推流舱{session.HouseSn} 客户端断开"); break; }     // operate 关预览/崩溃:正常退出
+                        catch (HttpListenerException) { _log($"[debug] 推流舱{session.HouseSn} 连接断开"); break; }
+                        catch (Exception ex)
+                        {
+                            errCount++;
+                            _log($"[debug] 推流舱{session.HouseSn} 抓帧/编码异常({errCount}/5): {ex.Message}");
+                            if (errCount >= 5) { _log($"[debug] 推流舱{session.HouseSn} 连续错误过多,停"); break; }
+                            Thread.Sleep(500);
+                        }
+                    }
+                }
+                catch (Exception ex) { _log($"[debug] 推流舱{session.HouseSn} 线程异常: {ex.Message}"); }
+                finally
+                {
+                    session.StreamBroken = true;   // 可回收快信号;会话最终由心跳 TTL 看门狗收(不在此 Dispose,避免与命令分发/超时回收争 lease)
+                    try { resp.OutputStream.Close(); } catch { }
+                    try { resp.Close(); } catch { }
+                    _log($"[debug] 推流舱{session.HouseSn} 线程结束");
+                }
+            });
+            t.IsBackground = true;
+            t.Name = $"MjpegStream-h{session.HouseSn}";
+            t.Start();
+        }
+
         public void Stop()
         {
             try { _cts?.Cancel(); _listener?.Stop(); _listener?.Close(); }