| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 |
- 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();
- }
- }
|