Bladeren bron

feat(d2-02): operate MjpegStreamClient 流式读+解码 BitmapImage(Freeze)+FrameReceived/Stopped 事件;断开明确提示不自动重连

huangjie 1 dag geleden
bovenliggende
commit
7929f97010
1 gewijzigde bestanden met toevoegingen van 106 en 0 verwijderingen
  1. 106 0
      ivf_tl_operate_2.0/ivf_tl_Operate/Debug/MjpegStreamClient.cs

+ 106 - 0
ivf_tl_operate_2.0/ivf_tl_Operate/Debug/MjpegStreamClient.cs

@@ -0,0 +1,106 @@
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Media.Imaging;
+
+namespace ivf_tl_Operate.Debug
+{
+    /// <summary>
+    /// operate 端 MJPEG 预览客户端:流式读 control /debug/preview/stream → 切帧 → 解码 BitmapImage → 事件回调。
+    /// 断开/异常 → Stopped 事件(View 据此提示操作人员手动重开,不自动重连,spec §7)。
+    /// </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;
+
+        /// <summary>收到一帧(已 Freeze,可跨线程贴 UI)。</summary>
+        public event Action<BitmapImage> FrameReceived;
+        /// <summary>预览停止(reason:断开原因,供 View 提示)。</summary>
+        public event Action<string> Stopped;
+
+        public bool IsRunning { get; private set; }
+
+        public MjpegStreamClient(string baseUrl) { _baseUrl = baseUrl.TrimEnd('/'); }
+
+        /// <summary>开始预览。sessionId = 第一阶段 acquire 返回的会话 id。</summary>
+        public void Start(string sessionId)
+        {
+            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));
+        }
+
+        private async Task ReadLoop(string url, CancellationToken token)
+        {
+            string reason = "预览已结束";
+            try
+            {
+                using (var resp = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token))
+                {
+                    if (!resp.IsSuccessStatusCode)
+                    {
+                        reason = resp.StatusCode == System.Net.HttpStatusCode.NotFound
+                            ? "调试会话已超时,请重新进入调试"
+                            : $"预览打开失败({(int)resp.StatusCode})";
+                        return;
+                    }
+                    var parser = new MjpegFrameParser();
+                    using (var stream = await resp.Content.ReadAsStreamAsync())
+                    {
+                        var buf = new byte[64 * 1024];
+                        while (!token.IsCancellationRequested)
+                        {
+                            int n = await stream.ReadAsync(buf, 0, buf.Length, token);
+                            if (n <= 0) { reason = "预览连接已断开,请重新打开预览"; break; }
+                            foreach (var jpeg in parser.Feed(buf, n))
+                            {
+                                var img = Decode(jpeg);
+                                if (img != null) FrameReceived?.Invoke(img);
+                            }
+                        }
+                    }
+                }
+            }
+            catch (OperationCanceledException) { reason = "预览已关闭"; }    // 主动 Stop
+            catch (Exception ex) { reason = $"预览中断,请重新打开预览({ex.Message})"; }
+            finally
+            {
+                IsRunning = false;
+                Stopped?.Invoke(reason);
+            }
+        }
+
+        private static BitmapImage Decode(byte[] jpeg)
+        {
+            try
+            {
+                var img = new BitmapImage();
+                img.BeginInit();
+                img.CacheOption = BitmapCacheOption.OnLoad;   // 解完即脱离流
+                img.StreamSource = new MemoryStream(jpeg);
+                img.EndInit();
+                img.Freeze();   // 跨线程贴 UI 必须 Freeze
+                return img;
+            }
+            catch { return null; }   // 坏帧丢弃,不打断流
+        }
+
+        /// <summary>主动停止预览(关预览按钮/返回)。</summary>
+        public void Stop()
+        {
+            try { _cts?.Cancel(); } catch { }
+            try { _http?.Dispose(); } catch { }
+            IsRunning = false;
+        }
+
+        public void Dispose() => Stop();
+    }
+}