增加了通过网络主动上报图像的支持

增加了指令维护通道的支持
This commit is contained in:
2026-01-07 10:59:03 +08:00
parent a697aab3e0
commit 3d47c8f009
47 changed files with 1613 additions and 1734 deletions

View File

@@ -1,67 +1,54 @@
using NetMQ;
using NetMQ.Sockets;
using System.Diagnostics; // 用于 Debug 输出
using Newtonsoft.Json;
using SHH.Contracts; // ★★★ 必须引用契约库 ★★★
using System.Diagnostics;
namespace SHH.CameraDashboard.Services;
namespace SHH.CameraDashboard;
public class StreamReceiverService : IDisposable
{
// 单例模式
public static StreamReceiverService Instance { get; } = new StreamReceiverService();
public event Action<string, byte[]>? OnFrameReceived;
// ★★★ 核心变更:使用强类型契约载体 ★★★
public event Action<VideoPayload>? OnPayloadReceived;
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 帧直接丢弃,防止内存爆炸
// 设置高水位,防止 UI 卡顿时内存溢出
_subSocket.Options.ReceiveHighWatermark = 1000;
_subSocket.Bind($"tcp://*:{ListenPort}");
_subSocket.Subscribe(""); // 订阅所有内容(这是 Dealer-Router/Pub-Sub 的基础)
string bindAddr = $"tcp://*:{ListenPort}";
_subSocket.Bind(bindAddr);
_subSocket.Subscribe("");
Console.WriteLine($"[Dashboard] 视频流接收服务启动: {bindAddr}");
Console.WriteLine($"[Dashboard] 视频流接收服务启动: tcp://*:{ListenPort}");
}
catch (Exception ex)
{
Console.WriteLine($"[Dashboard] 致命错误 - 端口绑定失败: {ex.Message}");
// 清理资源
// 明确抛出异常,让 App.xaml.cs 知道启动失败了
_subSocket?.Dispose();
_subSocket = null;
// 【修复4】抛出异常让上层知道启动失败了
throw new Exception($"端口 {port} 绑定失败,可能被占用。", ex);
}
// 3. 启动任务
_receiveTask = Task.Run(ReceiveLoop, _cts.Token);
}
@@ -73,61 +60,55 @@ public class StreamReceiverService : IDisposable
{
try
{
// 【修复2】线程安全检查
if (_subSocket == null) break;
// 接收 Topic
if (!_subSocket.TryReceiveFrameString(TimeSpan.FromMilliseconds(500), out string cameraId))
// =========================================================
// 核心解析逻辑:适配 Service 端的 4 帧复合协议
// =========================================================
NetMQMessage msg = new NetMQMessage();
// 1. 非阻塞接收多帧消息
if (!_subSocket.TryReceiveMultipartMessage(TimeSpan.FromMilliseconds(500), ref msg))
continue;
// 接收 Payload
if (!_subSocket.TryReceiveFrameBytes(TimeSpan.FromMilliseconds(100), out byte[] jpgBytes))
continue;
// 2. 协议完整性检查
if (msg.FrameCount < 4) continue;
// 触发事件
OnFrameReceived?.Invoke(cameraId, jpgBytes);
}
catch (ObjectDisposedException)
{
// 【修复2】这是正常的退出流程Socket被Dispose了优雅退出循环
break;
// 3. 协议头校验 (Frame 0)
if (msg[0].ConvertToString() != "SHH_V1") continue;
// 4. 反序列化元数据 (Frame 1)
string json = msg[1].ConvertToString();
var payload = JsonConvert.DeserializeObject<VideoPayload>(json);
if (payload == null) continue;
// 5. 填充二进制图像数据 (Frame 2 & 3)
// 注意NetMQ 的 msg 数据是非托管内存,转为 byte[] 实现了拷贝,安全供 UI 使用
if (payload.HasOriginalImage)
payload.OriginalImageBytes = msg[2].ToByteArray();
if (payload.HasTargetImage)
payload.TargetImageBytes = msg[3].ToByteArray();
// 6. 触发事件
OnPayloadReceived?.Invoke(payload);
}
catch (Exception ex)
{
// 记录日志,但不崩溃
Debug.WriteLine($"[ReceiverLoop Error] {ex.Message}");
Debug.WriteLine($"[Receiver 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;
_cts?.Cancel();
try { _subSocket?.Dispose(); } catch { }
_subSocket = null;
_receiveTask = null;
}
public void Dispose()
{
Stop();
}
public void Dispose() => Stop();
}