降低CPU使用率,处置好因降低CPU使用率带来的颜色偏差
This commit is contained in:
@@ -5,9 +5,16 @@
|
||||
/// </summary>
|
||||
public class SdkGlobal
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否保存摄像头配置
|
||||
/// </summary>
|
||||
/// <summary>是否保存摄像头配置</summary>
|
||||
public static bool SaveCameraConfigEnable { get; set; } = false;
|
||||
|
||||
/// <summary>是否使用 TurboJpegWrapper 降低图片编码开销</summary>
|
||||
public static bool UseTurboJpegWrapper { get; set;} = true;
|
||||
|
||||
/// <summary>禁用 TurboJpegWrapper</summary>
|
||||
public static void DisableTurboJpegAcceleration()
|
||||
{
|
||||
UseTurboJpegWrapper = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Ayay.SerilogLogs;
|
||||
using Lennox.LibYuvSharp;
|
||||
using OpenCvSharp;
|
||||
using Serilog;
|
||||
using System.Runtime.ExceptionServices;
|
||||
@@ -92,7 +93,7 @@ public class DahuaVideoSource : BaseVideoSource
|
||||
{
|
||||
string err = NETClient.GetLastError();
|
||||
NETClient.Logout(_loginId);
|
||||
throw new Exception($"大华预览失败: {err}");
|
||||
throw new Exception($"大华预览失败, {err}");
|
||||
}
|
||||
|
||||
_sdkLog.Information($"[SDK] Dahua 取流成功 => RealPlayID:{_realPlayId}");
|
||||
@@ -184,6 +185,8 @@ public class DahuaVideoSource : BaseVideoSource
|
||||
// =================================================================================
|
||||
try
|
||||
{
|
||||
_sdkLog.Information($"[Perf] Dahua 尝试开启硬解码. ID:{_config.Id} Port:{_playPort}");
|
||||
|
||||
// nDecodeEngine: 1 = 开启硬解码 (Nvidia/Intel)
|
||||
// 注意:大华 SDK 若不支持会自动降级,try-catch 仅为了防止 P/Invoke 签名缺失崩溃
|
||||
// Optimized: 使用新版接口开启硬件解码,优先尝试 CUDA 以保证 Ayay 的多路并发性能
|
||||
@@ -195,7 +198,6 @@ public class DahuaVideoSource : BaseVideoSource
|
||||
// 如果显卡不支持 CUDA,降级为普通硬解或软解
|
||||
PLAY_SetEngine(_playPort, DecodeType.DECODE_HW, RenderType.RENDER_D3D9);
|
||||
}
|
||||
_sdkLog.Information($"[Perf] Dahua 尝试开启硬解码. ID:{_config.Id} Port:{_playPort}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -223,7 +225,7 @@ public class DahuaVideoSource : BaseVideoSource
|
||||
/// </summary>
|
||||
[HandleProcessCorruptedStateExceptions] // 捕获非托管状态损坏异常 (AccessViolation)
|
||||
[SecurityCritical]
|
||||
private void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref DahuaPlaySDK.FRAME_INFO pFrameInfo, IntPtr nUser, int nReserved2)
|
||||
private unsafe void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref DahuaPlaySDK.FRAME_INFO pFrameInfo, IntPtr nUser, int nReserved2)
|
||||
{
|
||||
// 1. 基础指针检查
|
||||
if (pBuf == IntPtr.Zero || nSize <= 0) return;
|
||||
@@ -307,15 +309,37 @@ public class DahuaVideoSource : BaseVideoSource
|
||||
smartFrame = _framePool?.Get();
|
||||
if (smartFrame == null) return; // 池满丢帧
|
||||
|
||||
// =========================================================================================
|
||||
// ⚡ [核心操作:零拷贝转换]
|
||||
// 大华 PlaySDK 默认输出 I420 (YUV420P)。
|
||||
// 使用 Mat.FromPixelData 封装指针,避免内存拷贝。
|
||||
// =========================================================================================
|
||||
using (var yuvMat = Mat.FromPixelData(currentHeight + currentHeight / 2, currentWidth, MatType.CV_8UC1, pBuf))
|
||||
{
|
||||
Cv2.CvtColor(yuvMat, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_I420);
|
||||
}
|
||||
int width = pFrameInfo.nWidth;
|
||||
int height = pFrameInfo.nHeight;
|
||||
|
||||
// 计算 YUV 分量地址
|
||||
byte* pY = (byte*)pBuf;
|
||||
byte* pU = pY + (width * height);
|
||||
byte* pV = pU + (width * height / 4);
|
||||
|
||||
// 目标 BGR 地址
|
||||
byte* pDst = (byte*)smartFrame.InternalMat.Data;
|
||||
|
||||
// 调用 LibYuvSharp
|
||||
// 注意:LibYuvSharp 内部通常处理的是 BGR 顺序,
|
||||
// 如果发现图像发蓝,请将 pU 和 pV 的位置对调
|
||||
LibYuv.I420ToRGB24(
|
||||
pY, width,
|
||||
pU, width / 2,
|
||||
pV, width / 2,
|
||||
pDst, width * 3,
|
||||
width, height
|
||||
);
|
||||
|
||||
//// =========================================================================================
|
||||
//// ⚡ [核心操作:零拷贝转换]
|
||||
//// 大华 PlaySDK 默认输出 I420 (YUV420P)。
|
||||
//// 使用 Mat.FromPixelData 封装指针,避免内存拷贝。
|
||||
//// =========================================================================================
|
||||
//using (var yuvMat = Mat.FromPixelData(currentHeight + currentHeight / 2, currentWidth, MatType.CV_8UC1, pBuf))
|
||||
//{
|
||||
// Cv2.CvtColor(yuvMat, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_I420);
|
||||
//}
|
||||
|
||||
// =========================================================================================
|
||||
// 🛡️ [第三道防线:空结果防御]
|
||||
|
||||
@@ -341,11 +341,18 @@ public static class HikPlayMethods
|
||||
[DllImport(DllName)]
|
||||
public static extern bool PlayM4_ResetSourceBuffer(int nPort);
|
||||
|
||||
// =========================================================================
|
||||
// 🚀 [修正] 适配 V6.1.9+ 新版 SDK 的硬解码 API
|
||||
// =========================================================================
|
||||
/// <summary>
|
||||
/// [新增] 开启硬件解码
|
||||
/// 设置解码引擎 (扩展版)
|
||||
/// 对应 C++: BOOL PlayM4_SetDecodeEngineEx(LONG nPort, DWORD dwEngine);
|
||||
/// </summary>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool PlayM4_SetHardWareDecode(int nPort, int nMode);
|
||||
/// <param name="nPort">通道号</param>
|
||||
/// <param name="dwEngine">0:软解, 1:显卡硬解(D3D9), 2:显卡硬解(D3D11), 3:Intel核显</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("PlayCtrl.dll")]
|
||||
public static extern bool PlayM4_SetDecodeEngineEx(int nPort, uint dwEngine);
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Ayay.SerilogLogs;
|
||||
using Lennox.LibYuvSharp;
|
||||
using OpenCvSharp;
|
||||
using Serilog;
|
||||
using SHH.CameraSdk.HikFeatures;
|
||||
@@ -487,20 +488,27 @@ public class HikVideoSource : BaseVideoSource,
|
||||
return;
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// 🚀 [新增代码] 尝试开启 GPU 硬解码 (1=开启, 0=关闭)
|
||||
// 位置:必须在 OpenStream 成功之后,SetDecCallBack 之前
|
||||
// =================================================================================
|
||||
try
|
||||
{
|
||||
HikPlayMethods.PlayM4_SetHardWareDecode(_playPort, 1);
|
||||
_sdkLog.Information($"[Perf] Hik 尝试开启硬解码. ID:{_config.Id} Port:{_playPort}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 即使失败也不影响流程,仅记录警告
|
||||
_sdkLog.Warning($"[Perf] Hik 开启硬解码失败: {ex.Message}");
|
||||
}
|
||||
//// =================================================================================
|
||||
//// 🚀 [新增代码] 性能优化:适配新版 SDK 开启硬解码
|
||||
//// =================================================================================
|
||||
//try
|
||||
//{
|
||||
// // 尝试调用 Ex 版本的接口 (参数 2 表示 D3D11 硬解)
|
||||
// if (HikPlayMethods.PlayM4_SetDecodeEngineEx(_playPort, 1))
|
||||
// {
|
||||
// _sdkLog.Information($"[Perf] Hik 强制硬解码(SetDecodeEngineEx)已开启. ID:{_config.Id}");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // 如果返回 false,打印一下错误码
|
||||
// uint err = HikPlayMethods.PlayM4_GetLastError(_playPort);
|
||||
// _sdkLog.Warning($"[Perf] Hik 硬解码开启失败 Err={err}.");
|
||||
// }
|
||||
//}
|
||||
//catch (EntryPointNotFoundException)
|
||||
//{
|
||||
// _sdkLog.Warning($"[Perf] PlayM4_SetDecodeEngineEx 也没找到,这太奇怪了。");
|
||||
//}
|
||||
|
||||
HikPlayMethods.PlayM4_SetDecCallBackEx(_playPort, _decCallBack, IntPtr.Zero, 0);
|
||||
|
||||
@@ -536,7 +544,7 @@ public class HikVideoSource : BaseVideoSource,
|
||||
/// <param name="nReserved2"></param>
|
||||
[HandleProcessCorruptedStateExceptions]
|
||||
[SecurityCritical]
|
||||
private void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref HikPlayMethods.FRAME_INFO pFrameInfo, int nReserved1, int nReserved2)
|
||||
private unsafe void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref HikPlayMethods.FRAME_INFO pFrameInfo, int nReserved1, int nReserved2)
|
||||
{
|
||||
//Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} 帧抵达.");
|
||||
|
||||
@@ -633,11 +641,35 @@ public class HikVideoSource : BaseVideoSource,
|
||||
smartFrame = _framePool.Get();
|
||||
if (smartFrame == null) return; // 池满丢帧
|
||||
|
||||
// Optimized: [原因] 使用局部作用域封装 YUV 转换,确保原生指针尽快脱离
|
||||
using (var rawYuvWrapper = Mat.FromPixelData(currentHeight + currentHeight / 2, currentWidth, MatType.CV_8UC1, pBuf))
|
||||
{
|
||||
Cv2.CvtColor(rawYuvWrapper, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_YV12);
|
||||
}
|
||||
|
||||
int width = pFrameInfo.nWidth;
|
||||
int height = pFrameInfo.nHeight;
|
||||
|
||||
// 计算 YUV 分量地址
|
||||
byte* pY = (byte*)pBuf;
|
||||
byte* pU = pY + (width * height);
|
||||
byte* pV = pU + (width * height / 4);
|
||||
|
||||
// 目标 BGR 地址
|
||||
byte* pDst = (byte*)smartFrame.InternalMat.Data;
|
||||
|
||||
// 调用 LibYuvSharp
|
||||
// 注意:LibYuvSharp 内部通常处理的是 BGR 顺序,
|
||||
// 如果发现图像发蓝,请将 pU 和 pV 的位置对调
|
||||
LibYuv.I420ToRGB24(
|
||||
pY, width,
|
||||
pU, width / 2,
|
||||
pV, width / 2,
|
||||
pDst, width * 3,
|
||||
width, height
|
||||
);
|
||||
|
||||
|
||||
//// Optimized: [原因] 使用局部作用域封装 YUV 转换,确保原生指针尽快脱离
|
||||
//using (var rawYuvWrapper = Mat.FromPixelData(currentHeight + currentHeight / 2, currentWidth, MatType.CV_8UC1, pBuf))
|
||||
//{
|
||||
// Cv2.CvtColor(rawYuvWrapper, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_YV12);
|
||||
//}
|
||||
|
||||
// =========================================================
|
||||
// 【新增防御】: 检查转换结果是否有效
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<BaseOutputPath>D:\Codes\Ayay\SHH.CameraService\bin</BaseOutputPath>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -15,6 +16,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Lennox.LibYuvSharp" Version="1.1.2" />
|
||||
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
|
||||
|
||||
@@ -215,7 +215,7 @@ public class DeviceStatusHandler : BackgroundService
|
||||
catch (RpcException ex)
|
||||
{
|
||||
// 这里是关键:打印 RpcException 的详细状态
|
||||
_gRpcLog.Error("[gRpc] StatusCode: {Code}, Detail: {Detail}", ex.StatusCode, ex.Status.Detail);
|
||||
_gRpcLog.Error("[gRpc] StatusCode: {Code}, Detail: {Detail}, Uri:{Uri}", ex.StatusCode, ex.Status.Detail, endpoint.Uri);
|
||||
|
||||
// 如果是 Unimplemented,通常意味着路径不对
|
||||
if (ex.StatusCode == StatusCode.Unimplemented)
|
||||
|
||||
@@ -5,6 +5,7 @@ using Serilog;
|
||||
using SHH.CameraSdk; // 引用 SDK 核心
|
||||
using SHH.Contracts;
|
||||
using System.Diagnostics;
|
||||
using TurboJpegWrapper;
|
||||
|
||||
namespace SHH.CameraService;
|
||||
|
||||
@@ -81,18 +82,22 @@ public class ImageMonitorController : BackgroundService
|
||||
// 理由:在这里同步编码是最安全的,因为出了这个函数 frame 内存就会失效。
|
||||
// 且只编一次,后续分发给 10 个目标也只用这一份数据。
|
||||
|
||||
byte[] jpgBytes = null;
|
||||
byte[]? jpgBytes = null;
|
||||
// 如果有更小的图片, 原始图片不压缩, 除非有特殊需求
|
||||
if (frame.TargetMat == null)
|
||||
{
|
||||
jpgBytes = EncodeImage(frame.InternalMat);
|
||||
jpgBytes = SdkGlobal.UseTurboJpegWrapper
|
||||
? TurboEncodeImage(frame.InternalMat)
|
||||
: EncodeImage(frame.InternalMat);
|
||||
}
|
||||
|
||||
// 双流支持:如果存在处理后的 AI 图,也一并编码
|
||||
byte[] targetBytes = null;
|
||||
byte[]? targetBytes = null;
|
||||
if (frame.TargetMat != null && !frame.TargetMat.Empty())
|
||||
{
|
||||
targetBytes = EncodeImage(frame.TargetMat);
|
||||
targetBytes = SdkGlobal.UseTurboJpegWrapper
|
||||
? TurboEncodeImage(frame.TargetMat)
|
||||
: EncodeImage(frame.TargetMat);
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
@@ -145,10 +150,92 @@ public class ImageMonitorController : BackgroundService
|
||||
/// </summary>
|
||||
/// <param name="mat">待编码的 OpenCV Mat 矩阵</param>
|
||||
/// <returns>JPG 字节数组</returns>
|
||||
private byte[] EncodeImage(Mat mat)
|
||||
private byte[]? EncodeImage(Mat mat)
|
||||
{
|
||||
if (mat == null || mat.Empty())
|
||||
return null;
|
||||
|
||||
// ImEncode 将 Mat 编码为一维字节数组 (托管内存)
|
||||
Cv2.ImEncode(".jpg", mat, out byte[] buf, _encodeParams);
|
||||
return buf;
|
||||
}
|
||||
|
||||
// 建议将转换器定义为类成员,避免重复创建(内部持有句柄)
|
||||
private static readonly ThreadLocal<TJCompressor> _encoderPool = new(() => new TJCompressor());
|
||||
|
||||
/// <summary>
|
||||
/// TurboJPEG 快速编码
|
||||
/// </summary>
|
||||
/// <param name="mat"></param>
|
||||
/// <returns></returns>
|
||||
private byte[]? TurboEncodeImage(Mat mat)
|
||||
{
|
||||
// 1. 空引用与销毁状态防御
|
||||
if (mat == null || mat.Empty() || mat.IsDisposed)
|
||||
return Array.Empty<byte>();
|
||||
|
||||
try
|
||||
{
|
||||
// 2. 线程安全防护 (如果不用 ThreadLocal,至少保留 lock)
|
||||
var encoder = _encoderPool.Value;
|
||||
if (encoder == null)
|
||||
{
|
||||
_sysLog.Error("[Perf] ThreadLocal 编码器实例初始化失败,降级使用 OpenCV.");
|
||||
return EncodeImage(mat); // 自动降级,保证业务不中断
|
||||
}
|
||||
|
||||
// 3. 内存连续性确保
|
||||
// 保持原逻辑:不连续则 Clone,这是最稳妥的零拷贝退守方案,已通过您的严格测试
|
||||
if (!mat.IsContinuous())
|
||||
{
|
||||
using var continuousMat = mat.Clone();
|
||||
return encoder.Compress(continuousMat.Data, (int)continuousMat.Step(),
|
||||
continuousMat.Width, continuousMat.Height,
|
||||
// 2026-01-31 解决黄色变蓝色问题
|
||||
// 原因:经实测当前 Mat 内存排布为 RGB,原 BGR 参数导致红蓝通道反转
|
||||
TJPixelFormats.TJPF_RGB,
|
||||
TJSubsamplingOptions.TJSAMP_420, 95, TJFlags.NONE);
|
||||
}
|
||||
|
||||
// 执行并行编码
|
||||
// 注意:TJPF_BGR 确保了 OpenCV 默认内存排布,防止色偏
|
||||
return encoder.Compress(mat.Data, (int)mat.Step(), mat.Width, mat.Height,
|
||||
// 2026-01-31 解决黄色变蓝色问题
|
||||
// 修正像素格式为 RGB,匹配底层数据流,确保工业视频颜色还原准确
|
||||
TJPixelFormats.TJPF_RGB,
|
||||
TJSubsamplingOptions.TJSAMP_420, 95, TJFlags.NONE);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// 自动降级,保证业务不中断
|
||||
SdkGlobal.DisableTurboJpegAcceleration();
|
||||
return EncodeImage(mat);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 4. 记录异常但不让采集线程崩掉
|
||||
_sysLog.Error(ex, "[Perf] TurboJpeg 编码失败,请检查依赖或内存状态");
|
||||
|
||||
// 自动降级,保证业务不中断
|
||||
SdkGlobal.DisableTurboJpegAcceleration();
|
||||
return EncodeImage(mat);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
GlobalStreamDispatcher.OnGlobalFrame -= ProcessFrame;
|
||||
|
||||
if (_encoderPool.IsValueCreated)
|
||||
{
|
||||
// 严谨做法:由于 ThreadLocal 无法直接遍历销毁所有线程的实例,
|
||||
// 建议通过清理当前线程并由 GC 处理剩余部分,或在更高级的对象池中管理 Dispose。
|
||||
_encoderPool.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ namespace SHH.CameraService;
|
||||
|
||||
public class Program
|
||||
{
|
||||
private static bool _isExiting = false;
|
||||
|
||||
/// <summary>
|
||||
/// 主程序
|
||||
/// </summary>
|
||||
@@ -32,6 +34,12 @@ public class Program
|
||||
string argString = string.Join(" ", args);
|
||||
sysLog.Debug($"[Core] 🚀 启动参数({(isDebugArgs ? "调试环境" : "生产环境")}): {argString}");
|
||||
|
||||
AppDomain.CurrentDomain.ProcessExit += (s, e) => HandleExit("ProcessExit");
|
||||
Console.CancelKeyPress += (s, e) => {
|
||||
e.Cancel = true; // 阻止立即强制杀死进程
|
||||
HandleExit("CancelKeyPress");
|
||||
};
|
||||
|
||||
// =============================================================
|
||||
// 2. 硬件预热、端口扫描、gRpc链接
|
||||
// =============================================================
|
||||
@@ -123,6 +131,28 @@ public class Program
|
||||
var sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||||
sysLog.Information($"[Core] 🚀 核心业务逻辑已激活, 设备管理器已就绪.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 退出, 刷新日志
|
||||
/// </summary>
|
||||
/// <param name="source"></param>
|
||||
private static void HandleExit(string source)
|
||||
{
|
||||
if (_isExiting) return;
|
||||
_isExiting = true;
|
||||
|
||||
Log.ForContext("SourceContext", LogModules.Core)
|
||||
.Warning("// Modified: 处理手动关闭请求。来源: {Source}", source);
|
||||
|
||||
// TODO: 执行 SHH.CameraService 的清理逻辑 (释放海康/大华 SDK 句柄)
|
||||
Log.ForContext("SourceContext", LogModules.Core)
|
||||
.Warning("SHH.CameraService 已安全关闭,日志已刷新。");
|
||||
|
||||
// 必须显式调用,否则在 ProcessExit 触发时异步日志可能丢失
|
||||
Log.CloseAndFlush();
|
||||
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="TurboJpegWrapper" Version="1.5.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -86,3 +86,53 @@ message GenericResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
|
||||
// AI 分析专用服务
|
||||
service AiAnalysisProvider {
|
||||
// 1. 注册 (AIServer -> AiVideo) - 使用 AI 专有的消息名
|
||||
rpc RegisterAiInstance (AiRegisterRequest) returns (AiGenericResponse);
|
||||
|
||||
// 2. 图像流交互 (AiVideo -> AIServer)
|
||||
rpc GetRawVideoStream (AiCommandStreamRequest) returns (stream AiVideoFrameRequest);
|
||||
|
||||
// 3. 图像流交互 (AIServer -> AiVideo)
|
||||
rpc UploadAnalysisResult (stream AiVideoFrameRequest) returns (AiGenericResponse);
|
||||
|
||||
// 4. 指令通道
|
||||
rpc OpenAiCommandChannel (AiCommandStreamRequest) returns (stream AiCommandPayloadProto);
|
||||
rpc SendAiCommand (AiCommandPayloadProto) returns (AiGenericResponse);
|
||||
}
|
||||
|
||||
// --- 以下是 AI 专属的消息体定义,不再引用 gateway_service.proto ---
|
||||
|
||||
message AiGenericResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
message AiRegisterRequest {
|
||||
int32 process_id = 1;
|
||||
string instance_id = 2;
|
||||
string version = 3;
|
||||
string description = 4;
|
||||
}
|
||||
|
||||
message AiCommandStreamRequest {
|
||||
string instance_id = 1;
|
||||
}
|
||||
|
||||
message AiVideoFrameRequest {
|
||||
string camera_id = 1;
|
||||
int64 capture_timestamp = 2;
|
||||
map<string, string> diagnostics = 3;
|
||||
bytes original_image_bytes = 4;
|
||||
bytes target_image_bytes = 5;
|
||||
bool has_target_image = 6;
|
||||
}
|
||||
|
||||
message AiCommandPayloadProto {
|
||||
string cmd_code = 1;
|
||||
string json_params = 2;
|
||||
string request_id = 3;
|
||||
}
|
||||
@@ -23,16 +23,22 @@ namespace SHH.MjpegPlayer
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static MjpegConfig LoadConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
// [修复] 路径处理脆弱性:使用 BaseDirectory 拼接,避免相对路径替换的风险
|
||||
// 生产环境:强制使用绝对路径确保能找到配置文件
|
||||
if (!Debugger.IsAttached)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(JsonConfigUris.MjpegConfig))
|
||||
JsonConfigUris.MjpegConfig = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileName(JsonConfigUris.MjpegConfig));
|
||||
}
|
||||
|
||||
// 加载配置文件
|
||||
var cfg = JsonConfig.Load<MjpegConfig>(JsonConfigUris.MjpegConfig);
|
||||
MjpegConfig? cfg = null;
|
||||
if (!string.IsNullOrEmpty(JsonConfigUris.MjpegConfig))
|
||||
{
|
||||
cfg = JsonConfig.Load<MjpegConfig>(JsonConfigUris.MjpegConfig);
|
||||
if (cfg == null)
|
||||
{
|
||||
cfg = new MjpegConfig();
|
||||
@@ -40,8 +46,20 @@ namespace SHH.MjpegPlayer
|
||||
_sysLog.Warning("未找到配置文件,已生成默认配置: {Path}", JsonConfigUris.MjpegConfig);
|
||||
}
|
||||
MjpegStatics.Cfg = cfg;
|
||||
}
|
||||
|
||||
if (cfg == null)
|
||||
cfg = new MjpegConfig();
|
||||
|
||||
return cfg;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_sysLog.Error("加载配置文件失败.");
|
||||
Console.ReadLine();
|
||||
return new MjpegConfig();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ public class MjpegConfig
|
||||
= "0.0.0.0";
|
||||
|
||||
/// <summary>Mjpeg 服务端口开始</summary>
|
||||
public int SvrMjpegPortBegin
|
||||
public int SvrMjpegPortBegin { get; }
|
||||
= 25031;
|
||||
|
||||
/// <summary>Mjpeg 服务端口结束</summary>
|
||||
public int SvrMjpegPortEnd
|
||||
public int SvrMjpegPortEnd { get; }
|
||||
= 25300;
|
||||
|
||||
/// <summary>帧间隔, 单位毫秒 (值为 125, 每秒 8 帧)</summary>
|
||||
@@ -22,7 +22,7 @@ public class MjpegConfig
|
||||
= 125;
|
||||
|
||||
/// <summary>Mjpeg Wcf 接收图片接口</summary>
|
||||
public int WcfPushImagePort
|
||||
public int WcfPushImagePort { get; }
|
||||
= 25030;
|
||||
|
||||
/// <summary>接收图片的服务器名称</summary>
|
||||
|
||||
@@ -11,12 +11,15 @@ namespace SHH.MjpegPlayer
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
InitTemporaryLog();
|
||||
|
||||
_sysLog.Information("MjpegPlayer 正在初始化...");
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// 1. 注册 gRpc 服务
|
||||
builder.Services.AddGrpc(options => {
|
||||
builder.Services.AddGrpc(options =>
|
||||
{
|
||||
options.MaxReceiveMessageSize = 10 * 1024 * 1024; // 针对工业视频流,建议放宽至 10MB
|
||||
});
|
||||
|
||||
@@ -39,6 +42,19 @@ namespace SHH.MjpegPlayer
|
||||
app.Run("http://0.0.0.0:9002");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [新增] 临时日志初始化
|
||||
/// </summary>
|
||||
private static void InitTemporaryLog()
|
||||
{
|
||||
// 在未读取到 MjpegConfig 前,先使用默认参数启动日志
|
||||
LogBootstrapper.Init(new LogOptions
|
||||
{
|
||||
AppId = "MjpegPlayer",
|
||||
LogRootPath = @"D:\Logs",
|
||||
ConsoleLevel = Serilog.Events.LogEventLevel.Information
|
||||
});
|
||||
}
|
||||
|
||||
#region StartServer
|
||||
|
||||
@@ -73,6 +89,9 @@ namespace SHH.MjpegPlayer
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Logs.LogCritical<Program>(ex.Message, ex.StackTrace);
|
||||
Console.WriteLine(ex.ToString());
|
||||
Console.ReadLine();
|
||||
|
||||
// 退出应用
|
||||
Bootstrapper.ExitApp("应用程序崩溃.");
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<PackageIcon>notifyIcon.ico</PackageIcon>
|
||||
<ApplicationIcon>notifyIcon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -13,6 +16,10 @@
|
||||
<None Remove="Bat\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="notifyIcon.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CoreWCF.Http" Version="1.8.0" />
|
||||
<PackageReference Include="CoreWCF.Primitives" Version="1.8.0" />
|
||||
@@ -25,4 +32,11 @@
|
||||
<ProjectReference Include="..\SHH.Contracts.Grpc\SHH.Contracts.Grpc.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="notifyIcon.ico">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Net;
|
||||
using Ayay.SerilogLogs;
|
||||
using Serilog;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace SHH.MjpegPlayer
|
||||
@@ -8,6 +10,8 @@ namespace SHH.MjpegPlayer
|
||||
/// </summary>
|
||||
public class MjpegServer
|
||||
{
|
||||
private static readonly ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||||
|
||||
// [修复] 静态列表管理监听器,支持优雅停止
|
||||
private static readonly List<TcpListener> _listeners = new List<TcpListener>();
|
||||
private static readonly object _lock = new object();
|
||||
@@ -39,6 +43,7 @@ namespace SHH.MjpegPlayer
|
||||
|
||||
server.Start();
|
||||
// Logs.LogInformation...
|
||||
_sysLog.Information($"启动服务成功,端口:{port}");
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
BIN
SHH.MjpegPlayer/notifyIcon.ico
Normal file
BIN
SHH.MjpegPlayer/notifyIcon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
Reference in New Issue
Block a user