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