133 lines
3.8 KiB
C#
133 lines
3.8 KiB
C#
|
|
using NetMQ;
|
|||
|
|
using NetMQ.Sockets;
|
|||
|
|
using System.Diagnostics; // 用于 Debug 输出
|
|||
|
|
|
|||
|
|
namespace SHH.CameraDashboard.Services;
|
|||
|
|
|
|||
|
|
public class StreamReceiverService : IDisposable
|
|||
|
|
{
|
|||
|
|
// 单例模式
|
|||
|
|
public static StreamReceiverService Instance { get; } = new StreamReceiverService();
|
|||
|
|
|
|||
|
|
public event Action<string, byte[]>? OnFrameReceived;
|
|||
|
|
|
|||
|
|
private SubscriberSocket? _subSocket;
|
|||
|
|
private Task? _receiveTask;
|
|||
|
|
|
|||
|
|
// 【修复1】不要在这里初始化,改为在 Start 中初始化
|
|||
|
|
private CancellationTokenSource? _cts;
|
|||
|
|
|
|||
|
|
public int ListenPort { get; private set; }
|
|||
|
|
|
|||
|
|
// 增加运行状态标记
|
|||
|
|
public bool IsRunning => _receiveTask != null && !_receiveTask.IsCompleted;
|
|||
|
|
|
|||
|
|
private StreamReceiverService() { }
|
|||
|
|
|
|||
|
|
public void Start(int port = 6000)
|
|||
|
|
{
|
|||
|
|
// 1. 防止重复启动
|
|||
|
|
if (IsRunning) return;
|
|||
|
|
|
|||
|
|
ListenPort = port;
|
|||
|
|
|
|||
|
|
// 【修复1】每次启动时创建新的 TokenSource
|
|||
|
|
_cts = new CancellationTokenSource();
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 2. 初始化 Socket
|
|||
|
|
_subSocket = new SubscriberSocket();
|
|||
|
|
|
|||
|
|
// 【优化】设置高水位限制 (HWM)
|
|||
|
|
// 如果 UI 处理不过来,积压超过 1000 帧直接丢弃,防止内存爆炸
|
|||
|
|
_subSocket.Options.ReceiveHighWatermark = 1000;
|
|||
|
|
|
|||
|
|
string bindAddr = $"tcp://*:{ListenPort}";
|
|||
|
|
_subSocket.Bind(bindAddr);
|
|||
|
|
_subSocket.Subscribe("");
|
|||
|
|
|
|||
|
|
Console.WriteLine($"[Dashboard] 视频流接收服务启动: {bindAddr}");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
Console.WriteLine($"[Dashboard] 致命错误 - 端口绑定失败: {ex.Message}");
|
|||
|
|
|
|||
|
|
// 清理资源
|
|||
|
|
_subSocket?.Dispose();
|
|||
|
|
_subSocket = null;
|
|||
|
|
|
|||
|
|
// 【修复4】抛出异常让上层知道启动失败了
|
|||
|
|
throw new Exception($"端口 {port} 绑定失败,可能被占用。", ex);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 启动任务
|
|||
|
|
_receiveTask = Task.Run(ReceiveLoop, _cts.Token);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void ReceiveLoop()
|
|||
|
|
{
|
|||
|
|
var token = _cts?.Token ?? CancellationToken.None;
|
|||
|
|
|
|||
|
|
while (!token.IsCancellationRequested)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 【修复2】线程安全检查
|
|||
|
|
if (_subSocket == null) break;
|
|||
|
|
|
|||
|
|
// 接收 Topic
|
|||
|
|
if (!_subSocket.TryReceiveFrameString(TimeSpan.FromMilliseconds(500), out string cameraId))
|
|||
|
|
continue;
|
|||
|
|
|
|||
|
|
// 接收 Payload
|
|||
|
|
if (!_subSocket.TryReceiveFrameBytes(TimeSpan.FromMilliseconds(100), out byte[] jpgBytes))
|
|||
|
|
continue;
|
|||
|
|
|
|||
|
|
// 触发事件
|
|||
|
|
OnFrameReceived?.Invoke(cameraId, jpgBytes);
|
|||
|
|
}
|
|||
|
|
catch (ObjectDisposedException)
|
|||
|
|
{
|
|||
|
|
// 【修复2】这是正常的退出流程(Socket被Dispose了),优雅退出循环
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
// 记录日志,但不崩溃
|
|||
|
|
Debug.WriteLine($"[ReceiverLoop Error] {ex.Message}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
Console.WriteLine("[Dashboard] 接收循环已停止");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 停止服务(支持停止后重新 Start)
|
|||
|
|
/// </summary>
|
|||
|
|
public void Stop()
|
|||
|
|
{
|
|||
|
|
// 1. 发出取消信号
|
|||
|
|
if (_cts != null && !_cts.IsCancellationRequested)
|
|||
|
|
{
|
|||
|
|
_cts.Cancel();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 销毁 Socket (这会触发 ReceiveLoop 中的 ObjectDisposedException 从而退出循环)
|
|||
|
|
if (_subSocket != null)
|
|||
|
|
{
|
|||
|
|
try { _subSocket.Dispose(); } catch { }
|
|||
|
|
_subSocket = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 清理 Token
|
|||
|
|
_cts?.Dispose();
|
|||
|
|
_cts = null;
|
|||
|
|
|
|||
|
|
_receiveTask = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Dispose()
|
|||
|
|
{
|
|||
|
|
Stop();
|
|||
|
|
}
|
|||
|
|
}
|