91 lines
2.9 KiB
C#
91 lines
2.9 KiB
C#
|
|
using SHH.CameraSdk;
|
|||
|
|
using System.Collections.Concurrent;
|
|||
|
|
|
|||
|
|
namespace SHH.CameraService;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 网络推流管理器
|
|||
|
|
/// 职责:管理 ZeroMQ 推流任务的生命周期
|
|||
|
|
/// 类似于 DisplayWindowManager,它负责订阅数据并将其桥接到传输层
|
|||
|
|
/// </summary>
|
|||
|
|
public class NetworkStreamManager
|
|||
|
|
{
|
|||
|
|
private readonly VideoDataChannel _channel;
|
|||
|
|
// 记录当前活跃的推流任务,防止重复订阅
|
|||
|
|
private readonly ConcurrentDictionary<string, bool> _activeStreams = new();
|
|||
|
|
|
|||
|
|
public NetworkStreamManager(VideoDataChannel channel)
|
|||
|
|
{
|
|||
|
|
_channel = channel;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 启动推流任务
|
|||
|
|
/// </summary>
|
|||
|
|
public void StartStream(string appId, long deviceId)
|
|||
|
|
{
|
|||
|
|
// 1. 防止重复启动
|
|||
|
|
if (_activeStreams.ContainsKey(appId)) return;
|
|||
|
|
|
|||
|
|
// 2. 向全局分发器订阅精准数据
|
|||
|
|
// 这里实现了业务逻辑的闭环:只有被 Manager 管理的任务才会消耗 CPU 去转码
|
|||
|
|
GlobalStreamDispatcher.Subscribe(appId, deviceId, (frame) =>
|
|||
|
|
{
|
|||
|
|
// --- 这里的代码运行在分发线程中 ---
|
|||
|
|
|
|||
|
|
// A. 转码 (耗时操作封装在这里,不污染 Controller)
|
|||
|
|
byte[] jpgBytes = EncodeFrameToJpg(frame);
|
|||
|
|
|
|||
|
|
if (jpgBytes != null && jpgBytes.Length > 0)
|
|||
|
|
{
|
|||
|
|
var payload = new VideoPayload
|
|||
|
|
{
|
|||
|
|
CameraId = appId, // 使用 AppId 作为 Topic (给 Dashboard 订阅用)
|
|||
|
|
OriginalImageBytes = jpgBytes,
|
|||
|
|
CaptureTime = DateTime.Now,
|
|||
|
|
OriginalWidth = frame.TargetWidth,
|
|||
|
|
OriginalHeight = frame.TargetHeight
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// B. 写入传输通道
|
|||
|
|
_ = _channel.WriteAsync(payload);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
_activeStreams.TryAdd(appId, true);
|
|||
|
|
Console.WriteLine($"[Network] 推流任务已启动: {appId} -> Device {deviceId}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 停止推流任务
|
|||
|
|
/// </summary>
|
|||
|
|
public void StopStream(string appId)
|
|||
|
|
{
|
|||
|
|
if (_activeStreams.TryRemove(appId, out _))
|
|||
|
|
{
|
|||
|
|
// 1. 从全局分发器注销
|
|||
|
|
GlobalStreamDispatcher.Unsubscribe(appId);
|
|||
|
|
Console.WriteLine($"[Network] 推流任务已停止: {appId}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- 辅助方法 ---
|
|||
|
|
private byte[] EncodeFrameToJpg(SmartFrame frame)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 优先使用处理后的 TargetMat,如果没有则用原始的 InternalMat
|
|||
|
|
var mat = frame.TargetMat ?? frame.InternalMat;
|
|||
|
|
if (mat != null && !mat.Empty())
|
|||
|
|
{
|
|||
|
|
// 80 质量平衡体积与画质
|
|||
|
|
return mat.ImEncode(".jpg", new int[] { 1, 80 });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
Console.WriteLine($"[Network] 转码失败: {ex.Message}");
|
|||
|
|
}
|
|||
|
|
return Array.Empty<byte>();
|
|||
|
|
}
|
|||
|
|
}
|