修正帧数统计错误,流量统计错误的问题
确认显示帧数策略有效
This commit is contained in:
@@ -280,4 +280,19 @@ public class CamerasController : ControllerBase
|
|||||||
UseGrayscale = dto.UseGrayscale
|
UseGrayscale = dto.UseGrayscale
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [新增] 查询某台设备的当前流控策略
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{id}/subscriptions")]
|
||||||
|
public IActionResult GetSubscriptions(long id)
|
||||||
|
{
|
||||||
|
var device = _manager.GetDevice(id);
|
||||||
|
if (device == null) return NotFound();
|
||||||
|
|
||||||
|
// 调用刚才在 FrameController 写的方法
|
||||||
|
var subs = device.Controller.GetCurrentRequirements();
|
||||||
|
|
||||||
|
return Ok(subs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -197,6 +197,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
|
|||||||
Status = cam.Status.ToString(),
|
Status = cam.Status.ToString(),
|
||||||
IsOnline = cam.IsOnline,
|
IsOnline = cam.IsOnline,
|
||||||
Fps = cam.RealFps,
|
Fps = cam.RealFps,
|
||||||
|
Bitrate = cam.RealBitrate, // [新增] 映射基类属性
|
||||||
TotalFrames = cam.TotalFrames,
|
TotalFrames = cam.TotalFrames,
|
||||||
HealthScore = healthScore,
|
HealthScore = healthScore,
|
||||||
LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null,
|
LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null,
|
||||||
|
|||||||
@@ -95,4 +95,19 @@ public class FrameController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [新增] 获取当前所有活跃的订阅需求快照
|
||||||
|
/// </summary>
|
||||||
|
public List<dynamic> GetCurrentRequirements()
|
||||||
|
{
|
||||||
|
// 将 ConcurrentDictionary 转换为列表返回
|
||||||
|
return _requirements.Values.Select(r => new
|
||||||
|
{
|
||||||
|
r.AppId,
|
||||||
|
r.TargetFps,
|
||||||
|
// 还可以计算一个预计带宽占用,或者上次取帧时间
|
||||||
|
LastActive = r.LastCaptureTick
|
||||||
|
}).ToList<dynamic>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,9 @@ public class CameraTelemetryInfo
|
|||||||
/// <summary> 累计接收帧数(相机启动后接收的总帧数,用于统计数据完整性) </summary>
|
/// <summary> 累计接收帧数(相机启动后接收的总帧数,用于统计数据完整性) </summary>
|
||||||
public long TotalFrames { get; set; }
|
public long TotalFrames { get; set; }
|
||||||
|
|
||||||
|
/// <summary> 实时码率 (Mbps) </summary>
|
||||||
|
public double Bitrate { get; set; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region --- 健康度与统计 (Health & Statistics) ---
|
#region --- 健康度与统计 (Health & Statistics) ---
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
|
|||||||
|
|
||||||
/// <summary> 实时码率 (Mbps) </summary>
|
/// <summary> 实时码率 (Mbps) </summary>
|
||||||
protected double _currentBitrate = 0;
|
protected double _currentBitrate = 0;
|
||||||
|
public double RealBitrate => _currentBitrate;
|
||||||
|
|
||||||
/// <summary> 码率计算临时字节计数器 </summary>
|
/// <summary> 码率计算临时字节计数器 </summary>
|
||||||
private long _tempByteCounter = 0;
|
private long _tempByteCounter = 0;
|
||||||
@@ -404,43 +405,66 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 标记帧接收事件(心跳保活 + FPS/码率统计)
|
/// 标记数据接收(心跳保活 + 双路统计)
|
||||||
|
/// <para>调用规则:</para>
|
||||||
|
/// <para>1. 网络层收到流数据时:调用 MarkFrameReceived(dwBufSize),只统计流量。</para>
|
||||||
|
/// <para>2. 解码层流控通过后:调用 MarkFrameReceived(0),只统计有效帧率。</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dataSize">当前帧字节大小</param>
|
/// <param name="dataSize">数据包大小(字节),0 表示这是一帧解码后的图像</param>
|
||||||
protected void MarkFrameReceived(uint dataSize = 0)
|
protected void MarkFrameReceived(uint dataSize = 0)
|
||||||
{
|
{
|
||||||
var now = Environment.TickCount64;
|
long now = Environment.TickCount64;
|
||||||
|
|
||||||
// 1. 更新心跳时间戳(原子操作)
|
// 1. [心跳保活] 无论网络包还是解码帧,都视为设备“活着”
|
||||||
|
// 使用 Interlocked 保证多线程读写安全
|
||||||
Interlocked.Exchange(ref _lastFrameTick, now);
|
Interlocked.Exchange(ref _lastFrameTick, now);
|
||||||
|
|
||||||
// 2. 累加总帧数(原子操作)
|
// 2. [分流累加] 根据来源不同,累加不同的计数器
|
||||||
Interlocked.Increment(ref _totalFramesReceived);
|
if (dataSize > 0)
|
||||||
|
|
||||||
// 3. 累加临时计数器(用于 FPS/码率计算)
|
|
||||||
_tempFrameCounter++;
|
|
||||||
_tempByteCounter += dataSize;
|
|
||||||
|
|
||||||
// 4. 每秒结算一次统计指标
|
|
||||||
var timeDiff = now - _lastFpsCalcTick;
|
|
||||||
if (timeDiff >= 1000 && _lastFpsCalcTick > 0)
|
|
||||||
{
|
{
|
||||||
var duration = timeDiff / 1000.0;
|
// --- 来源:网络层回调 (SafeOnRealDataReceived) ---
|
||||||
|
// 只累加字节数,用于计算带宽 (Mbps)
|
||||||
// 计算实时 FPS (保留 1 位小数)
|
// 绝对不能在这里累加帧数,否则会被网络包的数量误导(导致 FPS 虚高)
|
||||||
RealFps = Math.Round(_tempFrameCounter / duration, 1);
|
Interlocked.Add(ref _tempByteCounter, dataSize);
|
||||||
|
|
||||||
// 计算实时码率 (Mbps) = (字节数 * 8) / 1024 / 1024 / 秒
|
|
||||||
_currentBitrate = Math.Round((_tempByteCounter * 8.0) / 1024 / 1024 / duration, 2);
|
|
||||||
|
|
||||||
// 重置临时计数器
|
|
||||||
_lastFpsCalcTick = now;
|
|
||||||
_tempFrameCounter = 0;
|
|
||||||
_tempByteCounter = 0;
|
|
||||||
}
|
}
|
||||||
else if (_lastFpsCalcTick == 0)
|
else
|
||||||
{
|
{
|
||||||
// 初始化 FPS 计算起始时间
|
// --- 来源:解码层回调 (SafeOnDecodingCallBack) ---
|
||||||
|
// 只累加帧数,用于计算有效 FPS
|
||||||
|
// 只有经过 MakeDecision() 筛选保留下来的帧才走到这里,所以是真实的 "Output FPS"
|
||||||
|
Interlocked.Increment(ref _tempFrameCounter);
|
||||||
|
|
||||||
|
// 累加生命周期总帧数
|
||||||
|
Interlocked.Increment(ref _totalFramesReceived);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. [定期结算] 每 1000ms (1秒) 结算一次统计指标
|
||||||
|
long timeDiff = now - _lastFpsCalcTick;
|
||||||
|
if (timeDiff >= 1000)
|
||||||
|
{
|
||||||
|
// 忽略第一次冷启动的数据(避免除以 0 或时间跨度过大)
|
||||||
|
if (_lastFpsCalcTick > 0)
|
||||||
|
{
|
||||||
|
double duration = timeDiff / 1000.0;
|
||||||
|
|
||||||
|
// --- A. 结算有效帧率 (FPS) ---
|
||||||
|
// 原子读取并重置计数器,防止漏算
|
||||||
|
int frames = Interlocked.Exchange(ref _tempFrameCounter, 0);
|
||||||
|
RealFps = Math.Round(frames / duration, 1);
|
||||||
|
|
||||||
|
// --- B. 结算网络带宽 (Mbps) ---
|
||||||
|
// 公式: (字节数 * 8位) / 1024 / 1024 / 秒数
|
||||||
|
long bytes = Interlocked.Exchange(ref _tempByteCounter, 0);
|
||||||
|
_currentBitrate = Math.Round((bytes * 8.0) / 1024 / 1024 / duration, 2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 初始化重置
|
||||||
|
_tempFrameCounter = 0;
|
||||||
|
_tempByteCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新结算时间锚点
|
||||||
_lastFpsCalcTick = now;
|
_lastFpsCalcTick = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ public class HikVideoSource : BaseVideoSource
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// [优化] 维持心跳,防止被哨兵误杀
|
// 【关键位置】:在此处调用,统计网络层收到的每一字节数据
|
||||||
|
// 因为 dwBufSize > 0,MarkFrameReceived 内部只会累加码流,不会增加 FPS 计数
|
||||||
MarkFrameReceived(dwBufSize);
|
MarkFrameReceived(dwBufSize);
|
||||||
|
|
||||||
if (_realPlayHandle == -1) return;
|
if (_realPlayHandle == -1) return;
|
||||||
@@ -301,6 +302,9 @@ public class HikVideoSource : BaseVideoSource
|
|||||||
// 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU
|
// 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU
|
||||||
if (!decision.IsCaptured) return;
|
if (!decision.IsCaptured) return;
|
||||||
|
|
||||||
|
// [优化] 维持心跳,防止被哨兵误杀
|
||||||
|
MarkFrameReceived(0);
|
||||||
|
|
||||||
int width = pFrameInfo.nWidth;
|
int width = pFrameInfo.nWidth;
|
||||||
int height = pFrameInfo.nHeight;
|
int height = pFrameInfo.nHeight;
|
||||||
|
|
||||||
|
|||||||
@@ -122,56 +122,16 @@ namespace SHH.CameraSdk
|
|||||||
|
|
||||||
if (manager.GetDevice(101) is HikVideoSource hikCamera)
|
if (manager.GetDevice(101) is HikVideoSource hikCamera)
|
||||||
{
|
{
|
||||||
// 2. 注册需求 (告诉控制器我要什么)
|
|
||||||
// ----------------------------------------------------
|
|
||||||
hikCamera.Controller.Register("WPF_Display_Main", 8); // UI 要 8 帧
|
|
||||||
hikCamera.Controller.Register("AI_Behavior_Engine", 2); // AI 要 2 帧
|
|
||||||
|
|
||||||
// 1. 注册差异化需求 (给每个消费者唯一的 AppId)
|
// 1. 注册差异化需求 (给每个消费者唯一的 AppId)
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// 模拟:A 进程(如远程预览)带宽有限,只要 3fps
|
// 1. 注册需求时,手动加上 _Display 后缀
|
||||||
hikCamera.Controller.Register("Process_A_Remote", 20);
|
hikCamera.Controller.Register("Process_A_Remote_Display", 20);
|
||||||
|
|
||||||
// 模拟:B 进程(如本地大屏)性能强劲,要 8fps
|
// 2. 订阅时,也改用带后缀的名称
|
||||||
hikCamera.Controller.Register("Process_B_Local", 8);
|
GlobalStreamDispatcher.Subscribe("Process_A_Remote_Display", 101, frame =>
|
||||||
|
|
||||||
// 模拟:AI 引擎
|
|
||||||
hikCamera.Controller.Register("AI_Engine_Core", 2);
|
|
||||||
|
|
||||||
// [已移除] 这里的 using var remoteRenderer = ... 已被移除
|
|
||||||
// 改为使用传入的 renderer 参数,确保其生命周期受控于 Main
|
|
||||||
|
|
||||||
// 2. 精准订阅 (Subscribe 替代了 +=)
|
|
||||||
// ----------------------------------------------------
|
|
||||||
|
|
||||||
// [消费者 A] - 绝对只会收到 3fps
|
|
||||||
GlobalStreamDispatcher.Subscribe("Process_A_Remote", 101, frame =>
|
|
||||||
{
|
{
|
||||||
// 关键:增加引用计数,防止在投递过程中被 Pipeline 回收
|
|
||||||
frame.AddRef();
|
frame.AddRef();
|
||||||
|
|
||||||
// 投递到渲染线程 (FrameConsumer)
|
|
||||||
renderer.Enqueue(frame);
|
renderer.Enqueue(frame);
|
||||||
|
|
||||||
//Console.WriteLine("Frame Enqueued");
|
|
||||||
});
|
|
||||||
|
|
||||||
// [消费者 B] - 绝对只会收到 8fps
|
|
||||||
GlobalStreamDispatcher.Subscribe("Process_B_Local", (deviceId, frame) =>
|
|
||||||
{
|
|
||||||
//if (deviceId == 101)
|
|
||||||
//{
|
|
||||||
// Console.WriteLine($"[Process B] 本地渲染一帧 (8fps节奏)");
|
|
||||||
//}
|
|
||||||
});
|
|
||||||
|
|
||||||
// [消费者 AI]
|
|
||||||
GlobalStreamDispatcher.Subscribe("AI_Engine_Core", (deviceId, frame) =>
|
|
||||||
{
|
|
||||||
//if (deviceId == 101)
|
|
||||||
//{
|
|
||||||
// Console.WriteLine($" >>> [AI] 分析一帧...");
|
|
||||||
//}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user