新增 Mjpegplayer 用来播放 Web 流

This commit is contained in:
2026-01-21 19:03:59 +08:00
parent f79cb6e74d
commit c438edfa0d
71 changed files with 4538 additions and 452 deletions

View File

@@ -0,0 +1,120 @@
using Ayay.SerilogLogs;
using Newtonsoft.Json;
using Serilog;
using System.Text;
namespace SHH.MjpegPlayer;
/// <summary>
/// 扩展 HttpClient 的 PostJson 方法,用于发送 JSON 格式的数据
/// </summary>
public static class NetHttpExtension
{
// Optimized: 统一日志对象
private static readonly ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
// Optimized: 使用静态单例 HttpClient 防止套接字耗尽。注意:生产环境建议配合 SocketsHttpHandler
private static readonly HttpClient _httpClient = new HttpClient();
#region (Sync-over-Async, 使)
/// <summary>
/// 发送 JSON 格式的 POST 请求 (同步)
/// </summary>
public static string PostJson(this object jsonData, string url, int timeout = 2000)
{
try
{
// Optimized: 显式调用异步版本并等待,注意在某些上下文可能死锁
return PostJsonAsync(jsonData, url, timeout).GetAwaiter().GetResult();
}
catch (Exception ex)
{
_sysLog.Error(ex, "Post 同步请求异常: {Url}", url);
return string.Empty;
}
}
/// <summary>
/// 发送 JSON 格式的 POST 请求并反序列化 (同步)
/// </summary>
public static T? PostJson<T>(this object jsonData, string url, int timeout = 2000)
{
try
{
var msg = PostJson(jsonData, url, timeout);
return string.IsNullOrWhiteSpace(msg) ? default : JsonConvert.DeserializeObject<T>(msg);
}
catch (Exception ex)
{
_sysLog.Error(ex, "Post 同步请求并解析 JSON 异常: {Url}", url);
return default;
}
}
#endregion
#region (使)
/// <summary>
/// 发送 JSON 格式的 POST 请求 (异步)
/// </summary>
/// <param name="jsonData">要发送的对象</param>
/// <param name="url">目标地址</param>
/// <param name="timeout">超时(ms)</param>
public static async Task<string> PostJsonAsync(this object jsonData, string url, int timeout = 2000)
{
string jsonString = string.Empty;
try
{
// Optimized: 序列化处理
jsonString = jsonData is string s ? s : JsonConvert.SerializeObject(jsonData);
using var content = new StringContent(jsonString, Encoding.UTF8, "application/json");
// Optimized: 设置请求级别的超时处理HttpClient.Timeout 是全局的,此处利用 CancellationTokenSource
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeout));
var response = await _httpClient.PostAsync(url, content, cts.Token);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadAsStringAsync();
}
_sysLog.Warning("Post 请求状态异常: {Url}, StatusCode: {Code}", url, response.StatusCode);
return string.Empty;
}
catch (OperationCanceledException)
{
_sysLog.Warning("Post 请求超时: {Url}, Timeout: {Timeout}ms", url, timeout);
return string.Empty;
}
catch (Exception ex)
{
// Modified: 使用结构化日志记录错误
_sysLog.Error(ex, "Post 异步请求发生故障: {Url}", url);
return string.Empty;
}
}
/// <summary>
/// 发送 JSON 格式的 POST 请求并反序列化 (异步)
/// </summary>
public static async Task<T?> PostJsonAsync<T>(this object jsonData, string url, int timeout = 2000)
{
try
{
var result = await PostJsonAsync(jsonData, url, timeout);
if (string.IsNullOrWhiteSpace(result)) return default;
return JsonConvert.DeserializeObject<T>(result);
}
catch (Exception ex)
{
_sysLog.Error(ex, "Post 异步请求解析 JSON 失败: {Url}", url);
return default;
}
}
#endregion
}

View File

@@ -0,0 +1,190 @@
using Ayay.SerilogLogs;
using Serilog;
using System.Diagnostics;
using System.Net;
using System.Net.NetworkInformation;
using System.Text.RegularExpressions;
namespace SHH.MjpegPlayer
{
/// <summary>
/// 网口占用检测
/// </summary>
public static class NetPortExtension
{
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
#region IsServerPort
/// <summary>
/// 是否端口
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static bool IsServerPort(this int value)
{
if (value > 0 && value < 65535)
return true;
return false;
}
#endregion
#region IsPortOccupied
/// <summary>
/// 端口占用检测
/// </summary>
/// <param name="port"></param>
/// <returns></returns>
public static bool IsPortOccupied(this int port)
{
var ipProperties = IPGlobalProperties.GetIPGlobalProperties();
IPEndPoint[] activeListeners = ipProperties.GetActiveTcpListeners();
foreach (var endPoint in activeListeners)
{
if (endPoint.Port == port)
return true; // 端口被占用
}
return false; // 端口可用
}
#endregion
#region GetProcessIdByPort
/// <summary>
/// 查询端口占用进程 Pid
/// </summary>
/// <param name="port"></param>
/// <returns></returns>
public static int GetProcessIdByPort(this int port)
{
try
{
using (Process proc = new Process())
{
proc.StartInfo.FileName = "cmd.exe";
proc.StartInfo.Arguments = $"/c netstat -ano | findstr :{port}";
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardOutput = true;
proc.StartInfo.CreateNoWindow = true;
proc.Start();
string output = proc.StandardOutput.ReadToEnd();
proc.WaitForExit();
// 解析输出示例TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 1234
Match match = Regex.Match(output, @":\d+\s+.*?LISTENING\s+(\d+)");
if (match.Success && int.TryParse(match.Groups[1].Value, out int pid))
return pid;
}
return 0;
}
catch (Exception ex)
{
_sysLog.Warning("查询端口占用进程出错", ex.Message, ex.StackTrace);
return 0;
}
}
#endregion
#region GetProcessIdByPort
/// <summary>
/// 查询端口占用进程 Pid
/// </summary>
/// <param name="port"></param>
/// <returns></returns>
public static string GetProcessNameIdByPort(this int port)
{
try
{
using (Process proc = new Process())
{
proc.StartInfo.FileName = "cmd.exe";
proc.StartInfo.Arguments = $"/c netstat -ano | findstr :{port}";
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardOutput = true;
proc.StartInfo.CreateNoWindow = true;
proc.Start();
string output = proc.StandardOutput.ReadToEnd();
proc.WaitForExit();
// 解析输出示例TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 1234
Match match = Regex.Match(output, @":\d+\s+.*?LISTENING\s+(\d+)");
if (match.Success && int.TryParse(match.Groups[1].Value, out int pid))
{
using (Process process = Process.GetProcessById(pid))
return process.ProcessName;
}
}
return string.Empty;
}
catch (Exception ex)
{
_sysLog.Warning($"查询端口占用进程出错, 错误信息:{ex.Message} {ex.StackTrace}");
return string.Empty;
}
}
#endregion
#region PortOccupiedProc
/// <summary>
/// 端口占用检测并杀掉进程
/// </summary>
/// <param name="port"></param>
/// <returns>返回占用端口清理结果</returns>
public static bool PortOccupiedProc(this int port)
{
if (port.IsPortOccupied())
{
_sysLog.Warning("服务器端口被占用, Port: {port}");
// 等待 5 秒
for (var i = 0; i < 10; i++)
Thread.Sleep(500);
// 查找占用端口的进程
var pid = port.GetProcessIdByPort();
if (pid != 0)
{
// 获取进程名
string procName = pid.GetProcessName();
// 找到占用端口的进程
_sysLog.Warning($"找到占用端口进程 Pid: {pid} 进程名:{procName}, 5 秒后即将尝试杀掉占用端口的进程.");
// 等待 5 秒
for (var i = 0; i < 10; i++)
Thread.Sleep(500);
// 杀掉指定进程
if (!pid.KillProcessByPid(procName))
{
// 退出应用
return false;
}
// 等待 2 秒
Thread.Sleep(2000);
return true;
}
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,197 @@
using Ayay.SerilogLogs;
using Serilog;
using System.Diagnostics;
namespace SHH.MjpegPlayer
{
/// <summary>
/// 进程扩展
/// </summary>
public static class ProcessExtension
{
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
#region GetProcessName
/// <summary>
/// 获取进程名称
/// </summary>
/// <param name="pid"></param>
/// <returns></returns>
public static string GetProcessName(this int pid)
{
try
{
var process = Process.GetProcessById(pid);
return process.ProcessName;
}
catch (Exception ex)
{
_sysLog.Error(ex, "查询进程名出错, Pid: {Pid}", pid);
return string.Empty;
}
}
#endregion
#region KillProcessByPid
/// <summary>
/// 杀掉进程
/// </summary>
/// <param name="pid"></param>
/// <param name="procName"></param>
/// <returns></returns>
public static bool KillProcessByPid(this int pid, string procName = "")
{
try
{
var process = Process.GetProcessById(pid);
if (process != null)
{
procName = process.ProcessName;
process.Kill();
_sysLog.Warning("拒绝停止高权限系统进程: {Pid} - {Name}", pid, process.ProcessName);
return true;
}
else
{
// 找不到 ID 对应的进程,应该是进异常不会进这里
_sysLog.Information("成功杀掉进程 - Pid: {Pid}", pid);
return false;
}
}
catch (ArgumentException)
{
_sysLog.Warning("杀掉进程失败Pid: {Pid} 不存在", pid);
return false;
}
catch (Exception ex)
{
_sysLog.Error(ex, "杀掉进程异常, Pid: {Pid}", pid);
return false;
}
}
#endregion
#region KillProcessByName
/// <summary>
/// 杀掉进程
/// </summary>
/// <param name="pid"></param>
/// <param name="procName"></param>
/// <returns></returns>
public static int KillProcessByName(this string procName)
{
if (string.IsNullOrWhiteSpace(procName)) return 0;
int killCount = 0;
try
{
var processes = Process.GetProcessesByName(procName);
foreach (var proc in processes)
{
using (proc) // Optimized: 确保 Process 资源被释放
{
try
{
if (proc.IsHighPrivilegeProcess()) continue;
int currentId = proc.Id;
proc.Kill();
killCount++;
_sysLog.Information("成功通过名称杀掉进程 - Pid: {Pid}, Name: {Name}", currentId, procName);
}
catch (Exception ex)
{
_sysLog.Error(ex, "通过名称杀掉单个进程失败: {Name}", procName);
}
}
}
return killCount;
}
catch (Exception ex)
{
_sysLog.Error(ex, "通过名称杀掉进程列表异常: {Name}", procName);
return 0;
}
}
#endregion
#region StartProcess
/// <summary>
/// 开启进程
/// </summary>
/// <param name="procPath"></param>
public static bool StartProcess(this string procPath)
{
try
{
if (!File.Exists(procPath))
{
_sysLog.Error("启动进程失败,路径不存在: {Path}", procPath);
return false;
}
// Optimized: 显式记录启动行为
var process = Process.Start(procPath);
if (process != null)
{
_sysLog.Information("进程启动成功: {Path}, Pid: {Pid}", procPath, process.Id);
return true;
}
return false;
}
catch (Exception ex)
{
_sysLog.Error(ex, "启动进程异常: {Path}", procPath);
return false;
}
}
#endregion
#region IsHighPrivilegeProcess
/// <summary>
/// 检测是否高权限等级
/// </summary>
/// <param name="proc"></param>
/// <returns></returns>
public static bool IsHighPrivilegeProcess(this Process proc)
{
// 典型的高权限进程列表(可根据实际需求扩展)
string[] highPrivilegeProcesses = new[] {
"System", "smss.exe", "csrss.exe", "wininit.exe", "services.exe",
"lsass.exe", "winlogon.exe", "spoolsv.exe", "svchost.exe",
"csrss", "msedge"
};
// 检查进程名称是否在高权限列表中
foreach (string name in highPrivilegeProcesses)
{
if (proc.ProcessName.Equals(name, StringComparison.OrdinalIgnoreCase))
return true;
}
// 检查进程是否属于系统会话Session 0
try
{
return proc.SessionId == 0;
}
catch
{
// 如果无法获取 SessionId保守返回 true
return true;
}
}
#endregion
}
}

View File

@@ -0,0 +1,61 @@
namespace SHH.MjpegPlayer
{
/// <summary>图片通道</summary>
public class ImageChannel
{
/// <summary>进程 ID</summary>
public Int32 ProcId { get; set; }
/// <summary>设备 ID</summary>
public Int64 DeviceId { get; set; }
/// <summary>设备 IP</summary>
public string IpAddress { get; set; } = string.Empty;
/// <summary>名称</summary>
public string Name { get; set; } = string.Empty;
/// <summary>类型</summary>
public string Type { get; set; } = string.Empty;
/// <summary>图像宽度</summary>
public int ImageWidth { get; set; }
/// <summary>图像高度</summary>
public int ImageHeight { get; set; }
/// <summary>更新时间</summary>
public DateTime UpdateTime { get; set; }
/// <summary>是否正在播放</summary>
public bool IsPlaying { get; set; }
/// <summary>是否需要推流到 Rtmp 服务器</summary>
public bool UseRtmp { get; set; } = true;
#region RtmpUri
private string _rtmpUri = string.Empty;
/// <summary>Rtmp 推流地址</summary>
public string RtmpUri
{
get => _rtmpUri;
set
{
if (_rtmpUri == value)
return;
_rtmpUri = value;
}
}
#endregion
#region TestUri
/// <summary>测试地址</summary>
public string TestUri => $"?id={DeviceId}&typeCode={Type}";
#endregion
}
}

View File

@@ -0,0 +1,72 @@
using Core.WcfProtocol;
using System.Collections.Concurrent;
namespace SHH.MjpegPlayer
{
/// <summary>图片通道集合</summary>
public class ImageChannels
{
#region Channels
/// <summary>
/// 通道信息 (线程安全版本)
/// </summary>
// [修复] 使用 ConcurrentDictionary 替代 Dictionary防止多线程读写如推流和接收图片同时进行时崩溃
public ConcurrentDictionary<string, ImageChannel> Channels { get; set; }
= new ConcurrentDictionary<string, ImageChannel>();
#endregion
#region Do
/// <summary>
/// 处置图片
/// </summary>
/// <param name="req"></param>
/// <param name="key"></param>
public ImageChannel? Do(UploadImageRequest req, string key)
{
// [修复] 使用 GetOrAdd 原子操作,无需 lock彻底解决并发冲突
// 如果 key 不存在,则创建新通道;如果存在,则返回现有通道
var chn = Channels.GetOrAdd(key, k => new ImageChannel
{
DeviceId = req.Id,
Name = req.Name,
Type = req.Type,
});
// 更新指定信息 (直接属性赋值是原子性的,无需锁)
chn.IpAddress = req.IpAddress;
chn.ProcId = req.ProcId;
chn.ImageWidth = req.ImageWidth;
chn.ImageHeight = req.ImageHeight;
chn.UpdateTime = req.Time;
return chn;
}
#endregion
#region Get
/// <summary>
/// 获取通道信息
/// </summary>
/// <param name="deviceId"></param>
/// <param name="aiTypeCode"></param>
/// <returns></returns>
public ImageChannel? Get(string deviceId, string aiTypeCode)
{
string key = $"{deviceId}#{aiTypeCode}";
// [修复] ConcurrentDictionary 读取原本就是线程安全的
if (Channels.TryGetValue(key, out var val))
{
return val;
}
return null;
}
#endregion
}
}

View File

@@ -0,0 +1,94 @@
using Newtonsoft.Json;
namespace SHH.MjpegPlayer;
/// <summary>
/// Json 配置文件
/// </summary>
public class JsonConfig
{
#region Load
/// <summary>
/// 加载配置
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="path"></param>
/// <returns></returns>
public static T? Load<T>(string path)
{
try
{
var newPath = $"{Environment.CurrentDirectory}\\{path}";
path = newPath.Replace("Res\\Plugins\\", "");
var sr = new StreamReader(path);
var data = sr.ReadToEnd();
sr.Close();
sr = null;
data = data.Replace(@"""$schema"": ""https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json"",", "");
var obj = JsonConvert.DeserializeObject<T>(data);
//Logs.LogInformation<JsonConfig>(EIdFiles.LoadSucceed,
// $"配置{EIdFiles.LoadSucceed.GetDescription()}, Path:{path} 类型:{typeof(T).FullName}.");
return obj;
}
catch (Exception ex)
{
//Logs.LogWarning<JsonConfig>(EIdFiles.LoadFailed,
// $"配置{EIdFiles.LoadSucceed.GetDescription()}, Path:{path} 类型:{typeof(T).FullName}.", ex.Message, ex.StackTrace);
return default(T);
}
}
#endregion
#region Save
/// <summary>
/// 保存配置
/// </summary>
/// <param name="obj"></param>
/// <param name="path"></param>
/// <param name="caption"></param>
/// <returns></returns>
public static bool Save(object obj, string path, string caption)
{
try
{
var newPath = Path.GetFullPath(path);
if (File.Exists(newPath))
File.Delete(newPath);
var loc = newPath.LastIndexOf("\\");
if (loc > 0)
{
var newDir = newPath.Substring(0, loc);
Directory.CreateDirectory(newDir);
}
var msg = JsonConvert.SerializeObject(obj, Formatting.Indented);
msg = msg.Insert(1, "\"$schema\": \"https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json\",\r\n");
var sw = new StreamWriter(newPath);
sw.Write(msg);
sw.Flush();
sw.Close();
sw = null;
//Logs.LogInformation<JsonConfig>(EIdFiles.SaveSucceed,
// $"配置{EIdFiles.SaveSucceed.GetDescription()}, Path:{path}\r\n\t\t\tCaption:{caption} 类型:{obj.GetType().FullName}.");
return true;
}
catch (Exception ex)
{
//Logs.LogInformation<JsonConfig>(EIdFiles.SaveFailed,
// $"配置{EIdFiles.SaveFailed.GetDescription()}, Path:{path}\r\n\t\t\tCaption:{caption} 类型:{obj.GetType().FullName}.", ex.Message, ex.StackTrace);
return false;
}
}
#endregion
}

View File

@@ -0,0 +1,17 @@
namespace SHH.MjpegPlayer;
/// <summary>RTMP 配置响应类</summary>
public class CfgRtmpReply
{
/// <summary>响应消息</summary>
public string msg { get; set; } = string.Empty;
/// <summary>响应状态码</summary>
public int code { get; set; }
/// <summary>RTMP 推流地址列表</summary>
public RtmpVo[]? rtmpVoList { get; set; }
/// <summary>是否成功(状态码为 200 时返回 true</summary>
public bool IsSuccess => code == 200;
}

View File

@@ -0,0 +1,35 @@
using System.ComponentModel;
namespace SHH.MjpegPlayer
{
public enum EIdSys
{
/// <summary>根据PID杀掉进程成功</summary>
[Description("根据PID杀掉进程成功")]
KillProcByIdSucceed = 1000101,
/// <summary>按秒统计汇总</summary>
[Description("按秒统计汇总")]
TotalBySecond = 100701,
/// <summary>按分钟统计汇总</summary>
[Description("按分钟统计汇总")]
TotalByMinute = 100702,
/// <summary>按小时统计汇总</summary>
[Description("按小时统计汇总")]
TotalByHour = 100703,
/// <summary>查询进程名出错</summary>
[Description("查询进程名出错")]
SearchProcNameError = 1000901,
/// <summary>根据PID杀掉进程出错</summary>
[Description("根据PID杀掉进程出错")]
KillProcByIdError = 1000902,
/// <summary>启动进程出错</summary>
[Description("启动进程出错")]
StartProcessError = 1000903,
}
}

View File

@@ -0,0 +1,18 @@
namespace SHH.MjpegPlayer
{
public class JsonConfigUris
{
public static string DispatcherConfig;
public static string RtspRtcConfig;
public static string RtspRtcPortsConfig;
public static string MjpegConfig;
public static string CloudServerConfig;
public static string CloudServerSessionsConfig;
public static string CloudAgentConfig;
public static string CloudAITerminalConfig;
public static string VirtualCameraConfig;
public static string AIMainConfig;
public static string AIDbConfig;
public static string ToolLogConfig;
}
}

View File

@@ -0,0 +1,43 @@
namespace SHH.MjpegPlayer;
/// <summary>
/// Mjpeg 配置
/// </summary>
public class MjpegConfig
{
/// <summary>Mjpeg 服务 IP 地址</summary>
public string SvrMjpegIp
= "0.0.0.0";
/// <summary>Mjpeg 服务端口开始</summary>
public int SvrMjpegPortBegin
= 25031;
/// <summary>Mjpeg 服务端口结束</summary>
public int SvrMjpegPortEnd
= 25300;
/// <summary>帧间隔, 单位毫秒 (值为 125, 每秒 8 帧)</summary>
public int FrameInterval { get; set; }
= 125;
/// <summary>Mjpeg Wcf 接收图片接口</summary>
public int WcfPushImagePort
= 25030;
/// <summary>接收图片的服务器名称</summary>
public string SvrNamePushImage { get; set; }
= "ImageService.svc";
/// <summary>最大接收数据大小</summary>
public int SvrPushImageMaxRecMsgSize { get; set; }
= 2000 * 1024 * 1024;
/// <summary>Rtmp 服务地址</summary>
public string RtmpServerDjhUri { get; set; }
= "http://172.16.41.108:8889/intellect/nvr/getRtmp";
/// <summary>是否使用 Rtmp 服务</summary>
public bool UseRtmpServer { get; set; }
= false;
}

View File

@@ -0,0 +1,17 @@
namespace SHH.MjpegPlayer;
/// <summary>RTMP 推流对象类</summary>
public class RtmpVo
{
/// <summary>算法代码</summary>
public string algCode { get; set; } = string.Empty;
/// <summary>设备ID</summary>
public string deviceId { get; set; } = string.Empty;
/// <summary>设备IP地址</summary>
public string deviceIp { get; set; } = string.Empty;
/// <summary>RTMP 推流地址</summary>
public string rtmp { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,100 @@
namespace SHH.MjpegPlayer
{
/// <summary>
/// 会话信息
/// </summary>
public class SessionInfo
{
#region Key
/// <summary>流标识</summary>
public string? Key => $"{DeviceId}#{TypeCode}";
#endregion
#region DeviceId
/// <summary>设备类型</summary>
public string? DeviceId { get; set; }
#endregion
#region TypeCode
/// <summary>类型编码</summary>
public string? TypeCode { get; set; }
#endregion
#region ClientIp
/// <summary>客户端 IP</summary>
public string? ClientIp { get; set; }
#endregion
#region ClientPort
/// <summary>客户端端口</summary>
public int ClientPort { get; set; }
#endregion
#region Message
/// <summary>消息</summary>
public string? Message { get; set; }
#endregion
#region AcceptTime
/// <summary>接入时间</summary>
public DateTime AcceptTime { get; set; }
#endregion
#region Counter
/// <summary>计数器</summary>
public SumByTime? Counter { get; init; }
#endregion
// =======================================================
// [新增] 专门给诊断大屏用的属性,前端可直接读取数值
// =======================================================
/// <summary>接收帧率 (源头健康度)</summary>
public int RecvFps
{
get
{
if (Counter == null || Counter.TotalSecond == null) return 0;
// 从字典中安全获取 "接收帧数"
if (Counter.TotalSecond.TryGetValue("接收帧数", out uint val))
{
return (int)val;
}
return 0;
}
}
/// <summary>播放/发送帧率 (客户端健康度)</summary>
public int PlayFps
{
get
{
if (Counter == null || Counter.TotalSecond == null) return 0;
// 从字典中安全获取 "播放帧数"
if (Counter.TotalSecond.TryGetValue("播放帧数", out uint val))
{
return (int)val;
}
return 0;
}
}
}
}

View File

@@ -0,0 +1,190 @@
using System.Text;
namespace SHH.MjpegPlayer
{
/// <summary>
/// 按时间统计
/// </summary>
public class SumByTime
{
#region Defines
/// <summary>最近刷新在哪一秒</summary>
private int LastRefreshSecond = DateTime.Now.Second;
/// <summary>最近刷新在哪一分钟</summary>
private int LastRefreshMinute = DateTime.Now.Minute;
/// <summary>最近刷新在哪一小时</summary>
private int LastRefreshHour = DateTime.Now.Minute;
/// <summary>秒统计</summary>
private Dictionary<string, uint> _second
= new Dictionary<string, uint>();
/// <summary>分钟统计</summary>
private Dictionary<string, uint> _minute
= new Dictionary<string, uint>();
/// <summary>小时统计</summary>
private Dictionary<string, uint> _hour
= new Dictionary<string, uint>();
/// <summary>累计统计</summary>
public Dictionary<string, ulong> All { get; init; }
= new Dictionary<string, ulong>();
#endregion
#region TotalSecond
/// <summary>秒统计</summary>
public Dictionary<string, uint> TotalSecond { get; init; }
= new Dictionary<string, uint>();
#endregion
#region TotalMinute
/// <summary>分统计</summary>
public Dictionary<string, uint> TotalMinute { get; init; }
= new Dictionary<string, uint>();
#endregion
#region TotalHour
/// <summary>小时统计</summary>
public Dictionary<string, uint> TotalHour { get; init; }
= new Dictionary<string, uint>();
#endregion
#region Refresh
/// <summary>
/// 刷新方法调用次数
/// </summary>
/// <param name="logger"></param>
/// <param name="methodName"></param>
/// <param name="count"></param>
public void Refresh(string methodName, uint count = 1)
{
try
{
#region
// 加入集合
lock (_second)
{
if (!_second.ContainsKey(methodName))
_second.Add(methodName, 0);
}
// 加入集合
lock (_minute)
{
if (!_minute.ContainsKey(methodName))
_minute.Add(methodName, 0);
}
lock (_hour)
{
if (!_hour.ContainsKey(methodName))
_hour.Add(methodName, 0);
}
// 加入集合
lock (All)
{
if (!All.ContainsKey(methodName))
All.Add(methodName, 0);
}
#endregion
#region
// 秒刷新
if (!LastRefreshSecond.Equals(DateTime.Now.Second))
{
LastRefreshSecond = DateTime.Now.Second;
var sb = new StringBuilder();
foreach (var de in _second)
{
// 更新输出用统计信息
if (!TotalSecond.ContainsKey(de.Key))
TotalSecond.Add(de.Key, de.Value);
else
TotalSecond[de.Key] = de.Value;
sb.Append($"\r\n\t{de.Key} => 执行 {de.Value} 次");
_second[de.Key] = 0;
}
var logMsg = $"统计 => SumBySecond 统计时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm")}{sb.ToString()}";
//Logs.LogInformation<SumByTime>(EIdSys.TotalBySecond, logMsg);
}
// 分钟刷新
if (!LastRefreshMinute.Equals(DateTime.Now.Minute))
{
LastRefreshMinute = DateTime.Now.Minute;
var sb = new StringBuilder();
foreach (var de in _minute)
{
// 更新输出用统计信息
if (!TotalMinute.ContainsKey(de.Key))
TotalMinute.Add(de.Key, de.Value);
else
TotalMinute[de.Key] = de.Value;
sb.Append($"\r\n\t{de.Key} => 执行 {de.Value} 次, 平均每秒 {Math.Round((double)de.Value / 60, 2)} 次");
_minute[de.Key] = 0;
}
var logMsg = $"统计 => SumByMinute 统计时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm")}{sb.ToString()}";
//Logs.LogInformation<SumByTime>(EIdSys.TotalByMinute, logMsg);
}
// 小时刷新
if (!LastRefreshHour.Equals(DateTime.Now.Hour))
{
LastRefreshHour = DateTime.Now.Hour;
var sb = new StringBuilder();
foreach (var de in _hour)
{
// 更新输出用统计信息
if (!TotalHour.ContainsKey(de.Key))
TotalHour.Add(de.Key, de.Value);
else
TotalHour[de.Key] = de.Value;
sb.Append($"\r\n\t{de.Key} => 执行 {de.Value} 次, 平均每秒 {Math.Round((double)de.Value / 60, 2)} 次");
_hour[de.Key] = 0;
}
var logMsg = $"统计 => SumByHour 统计时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm")}{sb.ToString()}";
//Logs.LogInformation<SumByTime>(EIdSys.TotalByHour, logMsg);
}
#endregion
#region
_second[methodName] += count;
_minute[methodName] += count;
_hour[methodName] += count;
All[methodName] += count;
#endregion
}
catch (Exception ex)
{
//Logs.LogWarning<SumByTime>(ex.Message);
}
}
#endregion
}
}

View File

@@ -0,0 +1,68 @@
using Prism.Events;
namespace SHH.MjpegPlayer;
/// <summary>Prism 消息框架</summary>
public class PrismMsg<T>
{
#region Defines
public IEventAggregator _ea;
private static PrismMsg<T>? _instance = null;
#endregion
#region Constructor
/// <summary>构造函数</summary>
private PrismMsg()
{
_ea = new EventAggregator();
}
#endregion
#region Instance
/// <summary>获取实例信息</summary>
public static PrismMsg<T> Instance
{
get
{
if (_instance == null)
_instance = new PrismMsg<T>();
return _instance;
}
}
#endregion
#region Publish
/// <summary>发送消息</summary>
public static void Publish(T msg)
{
if (Instance == null)
return;
dynamic? data = msg;
Instance._ea.GetEvent<PubSubEvent<T>>().Publish(data);
}
#endregion
#region Subscribe
/// <summary>订阅消息</summary>
public static void Subscribe(Action<T> method)
{
if (Instance == null || Instance._ea == null)
return;
Instance._ea.GetEvent<PubSubEvent<T>>().Subscribe(method);
}
#endregion
}

View File

@@ -0,0 +1,101 @@
using Ayay.SerilogLogs;
using Serilog;
using System.Diagnostics;
using System.Timers;
namespace SHH.MjpegPlayer;
/// <summary>
/// 内存监控
/// </summary>
public static class MemoryWatchdog
{
private static readonly ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
private static System.Timers.Timer? _timer;
private static long _thresholdBytes;
private static ILogger _logger => Log.Logger;
/// <summary>
/// 启动内存监控
/// </summary>
/// <param name="intervalSeconds">检查间隔(秒)默认60秒</param>
/// <param name="limitMB">内存阈值(MB)超过此值自动退出默认800MB</param>
public static void Start(int intervalSeconds = 60, int limitMB = 800)
{
// 1. 参数安全检查
if (intervalSeconds < 1) intervalSeconds = 1; // 至少 1 秒
if (limitMB < 100) limitMB = 100; // 至少100MB防止误杀
// 2. 转换单位
// MB -> Bytes
_thresholdBytes = (long)limitMB * 1024 * 1024;
// 秒 -> 毫秒
double intervalMs = intervalSeconds * 1000.0;
// 3. 初始化定时器
Stop(); // 防止重复启动
_timer = new System.Timers.Timer(intervalMs);
_timer.Elapsed += CheckMemoryUsage;
_timer.AutoReset = true; // 循环执行
_timer.Start();
// 可选:记录启动日志
if (_logger != null)
{
_sysLog.Warning($"[系统] 内存看门狗已启动。每 {intervalSeconds} 秒检查一次,阈值: {limitMB} MB.");
}
}
private static void CheckMemoryUsage(object sender, ElapsedEventArgs e)
{
try
{
Process currentProc = Process.GetCurrentProcess();
// 【重要】刷新快照
currentProc.Refresh();
long currentUsage = currentProc.WorkingSet64;
if (currentUsage > _thresholdBytes)
{
double currentMB = currentUsage / 1024.0 / 1024.0;
double limitMB = _thresholdBytes / 1024.0 / 1024.0;
if (_logger != null)
{
_sysLog.Warning($"[严重] 内存占用 ({currentMB:F2} MB) 超过阈值 ({limitMB} MB),程序即将自杀重启或退出.");
}
// 等待日志输出
for (var i = 0; i < 10; i++)
{
System.Threading.Thread.Sleep(100);
}
// 强制退出
Environment.Exit(0);
}
}
catch (Exception ex)
{
if (_logger != null)
{
_sysLog.Warning($"[严重] 内存检查出错.");
}
}
}
public static void Stop()
{
if (_timer != null)
{
_timer.Stop();
_timer.Dispose();
_timer = null;
}
}
}