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 { /// /// operate 端 MJPEG 预览客户端:流式读 control /debug/preview/stream → 切帧 → 解码 BitmapImage → 事件回调。 /// 断开/异常 → Stopped 事件(View 据此提示操作人员手动重开,不自动重连,spec §7)。 /// 实例一次性:Stop/Dispose 后不可复用,需重新 new。View 每次开预览应 new 新实例。 /// public sealed class MjpegStreamClient : IDisposable { private readonly string _baseUrl; // 如 http://127.0.0.1:38080 private HttpClient _http; private CancellationTokenSource _cts; private bool _disposed; /// 收到一帧(已 Freeze,可跨线程贴 UI)。 public event Action FrameReceived; /// 预览停止(reason:断开原因,供 View 提示)。 public event Action Stopped; public bool IsRunning { get; private set; } public MjpegStreamClient(string baseUrl) { _baseUrl = baseUrl.TrimEnd('/'); } /// 开始预览。sessionId = 第一阶段 acquire 返回的会话 id。 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}"; _ = 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 = token.IsCancellationRequested ? "预览已关闭" // 主动 Stop 期间抛的任何异常都归为"已关闭" : $"预览中断,请重新打开预览({ex.Message})"; } finally { IsRunning = false; try { _http?.Dispose(); } catch { } // 谁起的谁清:HttpClient 在 ReadLoop 结束时释放 Stopped?.Invoke(reason); } } private static BitmapImage Decode(byte[] jpeg) { try { var img = new BitmapImage(); 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; } // 坏帧丢弃,不打断流 } /// 主动停止预览(关预览按钮/返回)。token 取消是唯一停止信号,_http 由 ReadLoop finally 清理。 public void Stop() { try { _cts?.Cancel(); } catch { } IsRunning = false; } public void Dispose() { _disposed = true; Stop(); } } }