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