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 0000000..01495ab Binary files /dev/null and b/SHH.MjpegPlayer/notifyIcon.ico differ