From 4afbf06439382bb7116a20268314f28f791cf009 Mon Sep 17 00:00:00 2001 From: wilson Date: Sat, 31 Jan 2026 10:43:41 +0800 Subject: [PATCH] =?UTF-8?q?=E9=99=8D=E4=BD=8ECPU=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E7=8E=87=EF=BC=8C=E5=A4=84=E7=BD=AE=E5=A5=BD=E5=9B=A0=E9=99=8D?= =?UTF-8?q?=E4=BD=8ECPU=E4=BD=BF=E7=94=A8=E7=8E=87=E5=B8=A6=E6=9D=A5?= =?UTF-8?q?=E7=9A=84=E9=A2=9C=E8=89=B2=E5=81=8F=E5=B7=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SHH.CameraSdk/Core/SdkGlobal.cs | 13 ++- .../{Temp => Core}/UserActionFilter.cs | 0 .../Drivers/DaHua/DahuaVideoSource.cs | 48 ++++++--- .../Drivers/HikVision/HikPlayMethods.cs | 13 ++- .../Drivers/HikVision/HikVideoSource.cs | 72 +++++++++---- SHH.CameraSdk/SHH.CameraSdk.csproj | 2 + .../GrpcImpls/Handlers/DeviceStatusHandler.cs | 2 +- .../ImageFactory/ImageMonitorController.cs | 97 +++++++++++++++++- SHH.CameraService/Program.cs | 30 ++++++ SHH.CameraService/SHH.CameraService.csproj | 1 + .../Protos/gateway_service.proto | 50 +++++++++ SHH.MjpegPlayer/Bootstrapper.cs | 46 ++++++--- SHH.MjpegPlayer/Core/Models/MjpegConfig.cs | 6 +- SHH.MjpegPlayer/Program.cs | 21 +++- SHH.MjpegPlayer/SHH.MjpegPlayer.csproj | 14 +++ SHH.MjpegPlayer/Server/MjpegServer.cs | 9 +- SHH.MjpegPlayer/notifyIcon.ico | Bin 0 -> 136606 bytes 17 files changed, 360 insertions(+), 64 deletions(-) rename SHH.CameraSdk/{Temp => Core}/UserActionFilter.cs (100%) create mode 100644 SHH.MjpegPlayer/notifyIcon.ico diff --git a/SHH.CameraSdk/Core/SdkGlobal.cs b/SHH.CameraSdk/Core/SdkGlobal.cs index 5ddc02c..79296ec 100644 --- a/SHH.CameraSdk/Core/SdkGlobal.cs +++ b/SHH.CameraSdk/Core/SdkGlobal.cs @@ -5,9 +5,16 @@ /// public class SdkGlobal { - /// - /// 是否保存摄像头配置 - /// + /// 是否保存摄像头配置 public static bool SaveCameraConfigEnable { get; set; } = false; + + /// 是否使用 TurboJpegWrapper 降低图片编码开销 + public static bool UseTurboJpegWrapper { get; set;} = true; + + /// 禁用 TurboJpegWrapper + public static void DisableTurboJpegAcceleration() + { + UseTurboJpegWrapper = false; + } } } \ No newline at end of file diff --git a/SHH.CameraSdk/Temp/UserActionFilter.cs b/SHH.CameraSdk/Core/UserActionFilter.cs similarity index 100% rename from SHH.CameraSdk/Temp/UserActionFilter.cs rename to SHH.CameraSdk/Core/UserActionFilter.cs diff --git a/SHH.CameraSdk/Drivers/DaHua/DahuaVideoSource.cs b/SHH.CameraSdk/Drivers/DaHua/DahuaVideoSource.cs index 0d1b4d3..43a2b4d 100644 --- a/SHH.CameraSdk/Drivers/DaHua/DahuaVideoSource.cs +++ b/SHH.CameraSdk/Drivers/DaHua/DahuaVideoSource.cs @@ -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 /// [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); + //} // ========================================================================================= // 🛡️ [第三道防线:空结果防御] diff --git a/SHH.CameraSdk/Drivers/HikVision/HikPlayMethods.cs b/SHH.CameraSdk/Drivers/HikVision/HikPlayMethods.cs index e29966f..0814ad2 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikPlayMethods.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikPlayMethods.cs @@ -341,11 +341,18 @@ public static class HikPlayMethods [DllImport(DllName)] public static extern bool PlayM4_ResetSourceBuffer(int nPort); + // ========================================================================= + // 🚀 [修正] 适配 V6.1.9+ 新版 SDK 的硬解码 API + // ========================================================================= /// - /// [新增] 开启硬件解码 + /// 设置解码引擎 (扩展版) + /// 对应 C++: BOOL PlayM4_SetDecodeEngineEx(LONG nPort, DWORD dwEngine); /// - [DllImport(DllName)] - public static extern bool PlayM4_SetHardWareDecode(int nPort, int nMode); + /// 通道号 + /// 0:软解, 1:显卡硬解(D3D9), 2:显卡硬解(D3D11), 3:Intel核显 + /// + [DllImport("PlayCtrl.dll")] + public static extern bool PlayM4_SetDecodeEngineEx(int nPort, uint dwEngine); #endregion } \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs index 72387f1..cff1bd8 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs @@ -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, /// [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); + //} // ========================================================= // 【新增防御】: 检查转换结果是否有效 diff --git a/SHH.CameraSdk/SHH.CameraSdk.csproj b/SHH.CameraSdk/SHH.CameraSdk.csproj index c6cf5d1..3809a3f 100644 --- a/SHH.CameraSdk/SHH.CameraSdk.csproj +++ b/SHH.CameraSdk/SHH.CameraSdk.csproj @@ -7,6 +7,7 @@ enable AnyCPU D:\Codes\Ayay\SHH.CameraService\bin + true @@ -15,6 +16,7 @@ + diff --git a/SHH.CameraService/GrpcImpls/Handlers/DeviceStatusHandler.cs b/SHH.CameraService/GrpcImpls/Handlers/DeviceStatusHandler.cs index dac13a6..104c0fa 100644 --- a/SHH.CameraService/GrpcImpls/Handlers/DeviceStatusHandler.cs +++ b/SHH.CameraService/GrpcImpls/Handlers/DeviceStatusHandler.cs @@ -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) diff --git a/SHH.CameraService/GrpcImpls/ImageFactory/ImageMonitorController.cs b/SHH.CameraService/GrpcImpls/ImageFactory/ImageMonitorController.cs index 65024f2..82e9341 100644 --- a/SHH.CameraService/GrpcImpls/ImageFactory/ImageMonitorController.cs +++ b/SHH.CameraService/GrpcImpls/ImageFactory/ImageMonitorController.cs @@ -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 /// /// 待编码的 OpenCV Mat 矩阵 /// JPG 字节数组 - 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 _encoderPool = new(() => new TJCompressor()); + + /// + /// TurboJPEG 快速编码 + /// + /// + /// + private byte[]? TurboEncodeImage(Mat mat) + { + // 1. 空引用与销毁状态防御 + if (mat == null || mat.Empty() || mat.IsDisposed) + return Array.Empty(); + + 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); + } + } + + /// + /// 释放资源 + /// + public override void Dispose() + { + GlobalStreamDispatcher.OnGlobalFrame -= ProcessFrame; + + if (_encoderPool.IsValueCreated) + { + // 严谨做法:由于 ThreadLocal 无法直接遍历销毁所有线程的实例, + // 建议通过清理当前线程并由 GC 处理剩余部分,或在更高级的对象池中管理 Dispose。 + _encoderPool.Dispose(); + } + + base.Dispose(); + } } \ No newline at end of file diff --git a/SHH.CameraService/Program.cs b/SHH.CameraService/Program.cs index 154202a..cc7d679 100644 --- a/SHH.CameraService/Program.cs +++ b/SHH.CameraService/Program.cs @@ -10,6 +10,8 @@ namespace SHH.CameraService; public class Program { + private static bool _isExiting = false; + /// /// 主程序 /// @@ -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] 🚀 核心业务逻辑已激活, 设备管理器已就绪."); } + + /// + /// 退出, 刷新日志 + /// + /// + 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); + } } /* diff --git a/SHH.CameraService/SHH.CameraService.csproj b/SHH.CameraService/SHH.CameraService.csproj index d4add69..6820d69 100644 --- a/SHH.CameraService/SHH.CameraService.csproj +++ b/SHH.CameraService/SHH.CameraService.csproj @@ -25,6 +25,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/SHH.Contracts.Grpc/Protos/gateway_service.proto b/SHH.Contracts.Grpc/Protos/gateway_service.proto index f15524b..4422080 100644 --- a/SHH.Contracts.Grpc/Protos/gateway_service.proto +++ b/SHH.Contracts.Grpc/Protos/gateway_service.proto @@ -85,4 +85,54 @@ message CommandStreamRequest { 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 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; } \ No newline at end of file diff --git a/SHH.MjpegPlayer/Bootstrapper.cs b/SHH.MjpegPlayer/Bootstrapper.cs index b79e12a..8101725 100644 --- a/SHH.MjpegPlayer/Bootstrapper.cs +++ b/SHH.MjpegPlayer/Bootstrapper.cs @@ -24,23 +24,41 @@ namespace SHH.MjpegPlayer /// public static MjpegConfig LoadConfig() { - // [修复] 路径处理脆弱性:使用 BaseDirectory 拼接,避免相对路径替换的风险 - // 生产环境:强制使用绝对路径确保能找到配置文件 - if (!Debugger.IsAttached) + try { - JsonConfigUris.MjpegConfig = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileName(JsonConfigUris.MjpegConfig)); - } + // [修复] 路径处理脆弱性:使用 BaseDirectory 拼接,避免相对路径替换的风险 + // 生产环境:强制使用绝对路径确保能找到配置文件 + if (!Debugger.IsAttached) + { + if (!string.IsNullOrEmpty(JsonConfigUris.MjpegConfig)) + JsonConfigUris.MjpegConfig = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileName(JsonConfigUris.MjpegConfig)); + } - // 加载配置文件 - var cfg = JsonConfig.Load(JsonConfigUris.MjpegConfig); - if (cfg == null) - { - cfg = new MjpegConfig(); - JsonConfig.Save(cfg, JsonConfigUris.MjpegConfig, "MjpegServer配置项"); - _sysLog.Warning("未找到配置文件,已生成默认配置: {Path}", JsonConfigUris.MjpegConfig); + // 加载配置文件 + MjpegConfig? cfg = null; + if (!string.IsNullOrEmpty(JsonConfigUris.MjpegConfig)) + { + cfg = JsonConfig.Load(JsonConfigUris.MjpegConfig); + if (cfg == null) + { + cfg = new MjpegConfig(); + JsonConfig.Save(cfg, JsonConfigUris.MjpegConfig, "MjpegServer配置项"); + _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(); } - MjpegStatics.Cfg = cfg; - return cfg; } #endregion diff --git a/SHH.MjpegPlayer/Core/Models/MjpegConfig.cs b/SHH.MjpegPlayer/Core/Models/MjpegConfig.cs index 8ff8a89..de3fcce 100644 --- a/SHH.MjpegPlayer/Core/Models/MjpegConfig.cs +++ b/SHH.MjpegPlayer/Core/Models/MjpegConfig.cs @@ -10,11 +10,11 @@ public class MjpegConfig = "0.0.0.0"; /// Mjpeg 服务端口开始 - public int SvrMjpegPortBegin + public int SvrMjpegPortBegin { get; } = 25031; /// Mjpeg 服务端口结束 - public int SvrMjpegPortEnd + public int SvrMjpegPortEnd { get; } = 25300; /// 帧间隔, 单位毫秒 (值为 125, 每秒 8 帧) @@ -22,7 +22,7 @@ public class MjpegConfig = 125; /// Mjpeg Wcf 接收图片接口 - public int WcfPushImagePort + public int WcfPushImagePort { get; } = 25030; /// 接收图片的服务器名称 diff --git a/SHH.MjpegPlayer/Program.cs b/SHH.MjpegPlayer/Program.cs index a348c98..07b329f 100644 --- a/SHH.MjpegPlayer/Program.cs +++ b/SHH.MjpegPlayer/Program.cs @@ -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"); } + /// + /// [新增] 临时日志初始化 + /// + 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(ex.Message, ex.StackTrace); + Console.WriteLine(ex.ToString()); + Console.ReadLine(); + // 退出应用 Bootstrapper.ExitApp("应用程序崩溃."); } diff --git a/SHH.MjpegPlayer/SHH.MjpegPlayer.csproj b/SHH.MjpegPlayer/SHH.MjpegPlayer.csproj index 6a5c817..8ffcf35 100644 --- a/SHH.MjpegPlayer/SHH.MjpegPlayer.csproj +++ b/SHH.MjpegPlayer/SHH.MjpegPlayer.csproj @@ -5,6 +5,9 @@ net8.0 enable enable + x64 + notifyIcon.ico + notifyIcon.ico @@ -13,6 +16,10 @@ + + + + @@ -25,4 +32,11 @@ + + + True + \ + + + diff --git a/SHH.MjpegPlayer/Server/MjpegServer.cs b/SHH.MjpegPlayer/Server/MjpegServer.cs index b59ad3f..67ba4d1 100644 --- a/SHH.MjpegPlayer/Server/MjpegServer.cs +++ b/SHH.MjpegPlayer/Server/MjpegServer.cs @@ -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 /// public class MjpegServer { + private static readonly ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); + // [修复] 静态列表管理监听器,支持优雅停止 private static readonly List _listeners = new List(); private static readonly object _lock = new object(); @@ -36,9 +40,10 @@ namespace SHH.MjpegPlayer var server = new TcpListener(ipAddress, port); lock (_lock) _listeners.Add(server); - + server.Start(); // Logs.LogInformation... + _sysLog.Information($"启动服务成功,端口:{port}"); try { diff --git a/SHH.MjpegPlayer/notifyIcon.ico b/SHH.MjpegPlayer/notifyIcon.ico new file mode 100644 index 0000000000000000000000000000000000000000..01495ab4316e623b5bca8537f38dffe96f6b628d GIT binary patch literal 136606 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 literal 0 HcmV?d00001