فهرست منبع

refactor(d2-02): MjpegStreamClient 审查整改——主动Stop统一报"已关闭"(M-4)+_http谁起谁清挪finally;删死字段_readTask(M-3);MemoryStream用using释放(M-5);加_disposed防复用+一次性契约注释(I-1)

huangjie 1 روز پیش
والد
کامیت
f4af652ae9
1فایلهای تغییر یافته به همراه24 افزوده شده و 10 حذف شده
  1. 24 10
      ivf_tl_operate_2.0/ivf_tl_Operate/Debug/MjpegStreamClient.cs

+ 24 - 10
ivf_tl_operate_2.0/ivf_tl_Operate/Debug/MjpegStreamClient.cs

@@ -10,13 +10,14 @@ namespace ivf_tl_Operate.Debug
     /// <summary>
     /// operate 端 MJPEG 预览客户端:流式读 control /debug/preview/stream → 切帧 → 解码 BitmapImage → 事件回调。
     /// 断开/异常 → Stopped 事件(View 据此提示操作人员手动重开,不自动重连,spec §7)。
+    /// 实例一次性:Stop/Dispose 后不可复用,需重新 new。View 每次开预览应 new 新实例。
     /// </summary>
     public sealed class MjpegStreamClient : IDisposable
     {
         private readonly string _baseUrl;   // 如 http://127.0.0.1:38080
         private HttpClient _http;
         private CancellationTokenSource _cts;
-        private Task _readTask;
+        private bool _disposed;
 
         /// <summary>收到一帧(已 Freeze,可跨线程贴 UI)。</summary>
         public event Action<BitmapImage> FrameReceived;
@@ -30,12 +31,13 @@ namespace ivf_tl_Operate.Debug
         /// <summary>开始预览。sessionId = 第一阶段 acquire 返回的会话 id。</summary>
         public void Start(string sessionId)
         {
+            if (_disposed) return;
             if (IsRunning) return;
             IsRunning = true;
             _cts = new CancellationTokenSource();
             _http = new HttpClient { Timeout = Timeout.InfiniteTimeSpan };  // 长连接,不超时掐断
             string url = $"{_baseUrl}/debug/preview/stream?sessionId={sessionId}";
-            _readTask = Task.Run(() => ReadLoop(url, _cts.Token));
+            _ = Task.Run(() => ReadLoop(url, _cts.Token));
         }
 
         private async Task ReadLoop(string url, CancellationToken token)
@@ -70,10 +72,16 @@ namespace ivf_tl_Operate.Debug
                 }
             }
             catch (OperationCanceledException) { reason = "预览已关闭"; }    // 主动 Stop
-            catch (Exception ex) { reason = $"预览中断,请重新打开预览({ex.Message})"; }
+            catch (Exception ex)
+            {
+                reason = token.IsCancellationRequested
+                    ? "预览已关闭"                                   // 主动 Stop 期间抛的任何异常都归为"已关闭"
+                    : $"预览中断,请重新打开预览({ex.Message})";
+            }
             finally
             {
                 IsRunning = false;
+                try { _http?.Dispose(); } catch { }   // 谁起的谁清:HttpClient 在 ReadLoop 结束时释放
                 Stopped?.Invoke(reason);
             }
         }
@@ -83,24 +91,30 @@ namespace ivf_tl_Operate.Debug
             try
             {
                 var img = new BitmapImage();
-                img.BeginInit();
-                img.CacheOption = BitmapCacheOption.OnLoad;   // 解完即脱离流
-                img.StreamSource = new MemoryStream(jpeg);
-                img.EndInit();
+                using (var ms = new MemoryStream(jpeg))
+                {
+                    img.BeginInit();
+                    img.CacheOption = BitmapCacheOption.OnLoad;   // 解完即脱离流
+                    img.StreamSource = ms;
+                    img.EndInit();
+                }
                 img.Freeze();   // 跨线程贴 UI 必须 Freeze
                 return img;
             }
             catch { return null; }   // 坏帧丢弃,不打断流
         }
 
-        /// <summary>主动停止预览(关预览按钮/返回)。</summary>
+        /// <summary>主动停止预览(关预览按钮/返回)。token 取消是唯一停止信号,_http 由 ReadLoop finally 清理。</summary>
         public void Stop()
         {
             try { _cts?.Cancel(); } catch { }
-            try { _http?.Dispose(); } catch { }
             IsRunning = false;
         }
 
-        public void Dispose() => Stop();
+        public void Dispose()
+        {
+            _disposed = true;
+            Stop();
+        }
     }
 }