This commit is contained in:
2025-10-29 11:42:58 +08:00
parent 7f6f84cd0e
commit a178c3550e
190 changed files with 81361 additions and 92 deletions

View File

@@ -9,6 +9,10 @@ using FATrace.OEMApp.Services;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Linq;
using TaskStatus = FATrace.Model.TaskStatus;
using System.Windows.Forms;
using System.Drawing;
namespace FATrace.OEMApp
{
@@ -37,6 +41,13 @@ namespace FATrace.OEMApp
InitializeComponent();
}
/// <summary>
/// 主窗体加载:
/// - 初始化海康客户端与基础配置(日志目录与保存路径)
/// - 初始化播放器与历史记录网格绑定
/// - 启动后台任务服务(顺序下载队列 + Jellyfin 批量监听)
/// - 初始化并启动 gridRULogDataGridView定时刷新实时展示下载/监听任务状态
/// </summary>
private void MainApp_Load(object sender, EventArgs e)
{
@@ -50,12 +61,30 @@ namespace FATrace.OEMApp
//HkCameraClient.NVR_UserName = ConfigHelper.GetValue("NVRUserName");
HkCameraClient.NVRVideoSavePath = ConfigHelper.GetValue("NVRVideoSavePath");
//NVR登录
NVRLogin();
InitMediaPlayer();
InitHistoryGridBinding();
// 启动后台任务服务:下载队列(单线程顺序)与 Jellyfin 批量监控(并行匹配)
try
{
DownloadTaskWorker.Instance.Start(HkCameraClient);
JellyfinMonitorQueueService.Instance.Start();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[TaskServices] 启动失败: {ex.Message}");
}
// 初始化 gridRULog 并启动 UI 定时刷新
InitRuLogGrid();
// 立即刷新一次,避免首次 1s 空白
RefreshRuLogGrid();
StartTaskUiTimer();
//materialListView1.DataBindings
try
{
@@ -80,6 +109,23 @@ namespace FATrace.OEMApp
}
}
private void NVRLogin()
{
HkCameraClient.NVR_IP = ConfigHelper.GetValue("NVRIP");
HkCameraClient.NVR_Port = ushort.Parse(ConfigHelper.GetValue("NVRPort"));
HkCameraClient.NVR_UserName = ConfigHelper.GetValue("NVRUserName");
HkCameraClient.NVR_Pw = ConfigHelper.GetValue("NVRPw");
var result = HkCameraClient.Sdk_NET_DVR_Login_V30(HkCameraClient.NVR_IP, HkCameraClient.NVR_Port, HkCameraClient.NVR_UserName, HkCameraClient.NVR_Pw);
if (result)
{
MessageBox.Show("登录成功");
return;
}
MessageBox.Show($"登录失败:{HkCameraClient.LastMsgErr}");
return;
}
/// <summary>
/// 当前登录用户
/// </summary>
@@ -133,26 +179,21 @@ namespace FATrace.OEMApp
}
/// <summary>
/// 下载按钮:仅将请求入队为 DownloadTask由后台 DownloadTaskWorker 顺序处理。
/// 注意:不会在此处直接调用 SDK 下载,也不会订阅 SDK 事件,避免并发与重复。
/// </summary>
private void btnLoadVideo_Click(object sender, EventArgs e)
{
CurrentVideoPath = NVRCom.GetVideoName(HkCameraClient.NVRVideoSavePath, "CODE");
var Result = HkCameraClient.Sdk_NET_DVR_GetFileByTime_V40(DateTime.Now.AddMinutes(-5), DateTime.Now, CurrentVideoPath);
if (Result.Result)
{
HkCameraClient.NVRLoadVideoProcessEventHandler -= HkCameraClient_NVRLoadVideoProcessEventHandler;
HkCameraClient.NVRLoadVideoProcessEventHandler += HkCameraClient_NVRLoadVideoProcessEventHandler;
HkCameraClient.NVRLoadVideoCompleteEventHandler -= HkCameraClient_NVRLoadVideoCompleteEventHandler;
HkCameraClient.NVRLoadVideoCompleteEventHandler += HkCameraClient_NVRLoadVideoCompleteEventHandler;
// 进度轮询已在库方法内部自动启动,这里无需再次调用
HkCameraClient.StartDownloadProgressMonitor();
}
else
{
MessageBox.Show($"[Sdk_NET_DVR_GetFileByTime_V40] 执行失败:{Result.Msg}");
}
//btnStopDownload.Enabled = true;
// 仅入队下载任务,由后台 DownloadTaskWorker 顺序处理
var taskId = DownloadTaskWorker.Instance.Enqueue(
code: CurInBagCode,
rawName: CurInBagRawName,
user: CurUserName,
start: DateTime.Now.AddMinutes(-5),
end: DateTime.Now
);
MessageBox.Show($"已入队下载任务Id={taskId}");
}
private void HkCameraClient_NVRLoadVideoProcessEventHandler(object? sender, short value)
@@ -432,29 +473,20 @@ namespace FATrace.OEMApp
/// <summary>
/// 操作结束
/// 测试/结束操作按钮:同样仅入队一条下载任务,方便快速验证下载->监听全流程。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnTestAction_Click(object sender, EventArgs e)
{
CurrentVideoPath = NVRCom.GetVideoName(HkCameraClient.NVRVideoSavePath, "CODE");
var Result = HkCameraClient.Sdk_NET_DVR_GetFileByTime_V40(DateTime.Now.AddMinutes(-5), DateTime.Now, CurrentVideoPath);
if (Result.Result)
{
HkCameraClient.NVRLoadVideoProcessEventHandler -= HkCameraClient_NVRLoadVideoProcessEventHandler;
HkCameraClient.NVRLoadVideoProcessEventHandler += HkCameraClient_NVRLoadVideoProcessEventHandler;
HkCameraClient.NVRLoadVideoCompleteEventHandler -= HkCameraClient_NVRLoadVideoCompleteEventHandler;
HkCameraClient.NVRLoadVideoCompleteEventHandler += HkCameraClient_NVRLoadVideoCompleteEventHandler;
// 进度轮询已在库方法内部自动启动,这里无需再次调用
}
else
{
MessageBox.Show($"[Sdk_NET_DVR_GetFileByTime_V40] 执行失败:{Result.Msg}");
}
var taskId = DownloadTaskWorker.Instance.Enqueue(
code: CurInBagCode,
rawName: CurInBagRawName,
user: CurUserName,
start: DateTime.Now.AddMinutes(-5),
end: DateTime.Now
);
MessageBox.Show($"[Test] 已入队下载任务Id={taskId}");
}
/// <summary>
@@ -526,6 +558,268 @@ namespace FATrace.OEMApp
});
}
#region gridRULog / DataGridView
private System.Windows.Forms.Timer _taskUiTimer;
private BindingSource ruLogBindingSource;
private BindingList<RuLogRow> ruLogBindingList;
/// <summary>
/// DataGridView 行模型
/// </summary>
private sealed class RuLogRow
{
public string TimeText { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty; // 下载/监听
public long TaskId { get; set; }
public string StatusText { get; set; } = string.Empty;
public string? ProgressText { get; set; }
public string Remark { get; set; } = string.Empty; // 错误优先,否则文件路径/ItemId
}
/// <summary>
/// 初始化 gridRULog列定义/数据绑定/低闪烁
/// </summary>
private void InitRuLogGrid()
{
ruLogBindingSource = new BindingSource();
ruLogBindingList = new BindingList<RuLogRow>();
ruLogBindingSource.DataSource = ruLogBindingList;
gridRULog.AutoGenerateColumns = false;
gridRULog.Columns.Clear();
// 列时间、类型、任务Id、状态、进度、备注(填充)
gridRULog.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "colTime",
HeaderText = "时间",
DataPropertyName = nameof(RuLogRow.TimeText),
Width = 100,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None
});
gridRULog.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "colType",
HeaderText = "类型",
DataPropertyName = nameof(RuLogRow.Type),
Width = 100,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None
});
gridRULog.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "colTaskId",
HeaderText = "任务Id",
DataPropertyName = nameof(RuLogRow.TaskId),
Width = 120,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None
});
gridRULog.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "colStatus",
HeaderText = "状态",
DataPropertyName = nameof(RuLogRow.StatusText),
Width = 120,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None
});
gridRULog.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "colProgress",
HeaderText = "进度",
DataPropertyName = nameof(RuLogRow.ProgressText),
Width = 80,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None
});
gridRULog.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "colRemark",
HeaderText = "备注",
DataPropertyName = nameof(RuLogRow.Remark),
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill
});
// 双缓存减少闪烁
SetDataGridViewDoubleBuffered(gridRULog);
gridRULog.DataSource = ruLogBindingSource;
gridRULog.ColumnHeadersVisible = true;
gridRULog.EnableHeadersVisualStyles = false;
gridRULog.AllowUserToResizeRows = false;
gridRULog.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.DisplayedCells;
gridRULog.BackgroundColor = Color.White;
gridRULog.BorderStyle = BorderStyle.FixedSingle;
gridRULog.ColumnHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single;
gridRULog.ColumnHeadersDefaultCellStyle.BackColor = Color.Gainsboro;
gridRULog.ColumnHeadersDefaultCellStyle.ForeColor = Color.Black;
gridRULog.RowHeadersVisible = false;
// 使用 Designer 中的 Location/Size 与 Anchor不额外 Dock
gridRULog.BringToFront();
// 跟随父容器尺寸变化进行定位与大小调整
materialCard2.Resize -= MaterialCard2_Resize;
materialCard2.Resize += MaterialCard2_Resize;
LayoutRuLogGrid();
}
/// <summary>
/// 根据左侧控件宽度动态计算 gridRULog 的 Left/Size确保位置与尺寸正确
/// </summary>
private void LayoutRuLogGrid()
{
if (gridRULog == null || materialCard2 == null) return;
try
{
int leftReserved = 540; // 默认左侧操作区宽度
try
{
var candidates = new Control[] { txtRUInBagCode, txtRURawName, btnTestAction, btnRawStopLoadVideo, DownloadProgressBarMain };
foreach (var c in candidates)
{
if (c?.Parent == materialCard2)
{
leftReserved = Math.Max(leftReserved, c.Right);
}
}
leftReserved += 20; // 预留间距
}
catch { }
var client = materialCard2.ClientSize;
var left = leftReserved;
var top = 9;
var width = Math.Max(200, client.Width - left - 14);
var height = Math.Max(120, client.Height - top - 9);
gridRULog.Location = new Point(left, top);
gridRULog.Size = new Size(width, height);
gridRULog.BringToFront();
}
catch { }
}
private void MaterialCard2_Resize(object? sender, EventArgs e)
{
LayoutRuLogGrid();
}
/// <summary>
/// 启动 UI 定时器,定时刷新下载与 Jellyfin 监听任务状态
/// </summary>
private void StartTaskUiTimer()
{
_taskUiTimer = new System.Windows.Forms.Timer();
_taskUiTimer.Interval = 1000; // 1s 刷新
_taskUiTimer.Tick += TaskUiTimer_Tick;
_taskUiTimer.Start();
}
private void TaskUiTimer_Tick(object? sender, EventArgs e)
{
RefreshRuLogGrid();
}
/// <summary>
/// 刷新 gridRULog下载 + 监听)
/// </summary>
private void RefreshRuLogGrid()
{
try
{
var db = FSqlContext.FDb;
var downloads = db.Select<DownloadTask>()
.OrderByDescending(a => a.UpdateTime)
.Limit(50)
.ToList();
var monitors = db.Select<JellyfinMonitorTask>()
.OrderByDescending(a => a.UpdateTime)
.Limit(50)
.ToList();
var rows = new List<RuLogRow>(downloads.Count + monitors.Count);
// 运行中优先
foreach (var t in downloads.Where(x => x.Status == TaskStatus.Running))
{
rows.Add(new RuLogRow
{
TimeText = t.UpdateTime.ToString("HH:mm:ss"),
Type = "下载",
TaskId = t.Id,
StatusText = t.Status.ToString(),
ProgressText = t.Progress.ToString(),
Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.VideoFilePath ?? string.Empty) : t.Error!
});
}
foreach (var t in monitors.Where(x => x.Status == TaskStatus.Running))
{
rows.Add(new RuLogRow
{
TimeText = t.UpdateTime.ToString("HH:mm:ss"),
Type = "监听",
TaskId = t.Id,
StatusText = t.Status.ToString(),
ProgressText = string.Empty,
Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.FoundItemId ?? t.LocalFileNameOrPath ?? string.Empty) : t.Error!
});
}
// 其余
foreach (var t in downloads.Where(x => x.Status != TaskStatus.Running))
{
rows.Add(new RuLogRow
{
TimeText = t.UpdateTime.ToString("HH:mm:ss"),
Type = "下载",
TaskId = t.Id,
StatusText = t.Status.ToString(),
ProgressText = t.Progress.ToString(),
Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.VideoFilePath ?? string.Empty) : t.Error!
});
}
foreach (var t in monitors.Where(x => x.Status != TaskStatus.Running))
{
rows.Add(new RuLogRow
{
TimeText = t.UpdateTime.ToString("HH:mm:ss"),
Type = "监听",
TaskId = t.Id,
StatusText = t.Status.ToString(),
ProgressText = string.Empty,
Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.FoundItemId ?? t.LocalFileNameOrPath ?? string.Empty) : t.Error!
});
}
// 批量更新绑定列表,尽量减少闪烁
ruLogBindingList.RaiseListChangedEvents = false;
ruLogBindingList.Clear();
foreach (var r in rows)
{
ruLogBindingList.Add(r);
}
ruLogBindingList.RaiseListChangedEvents = true;
ruLogBindingList.ResetBindings();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[gridRULog] 刷新失败: {ex.Message}");
}
}
private static void SetDataGridViewDoubleBuffered(DataGridView dgv)
{
try
{
typeof(DataGridView).InvokeMember(
"DoubleBuffered",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.SetProperty,
null,
dgv,
new object[] { true }
);
}
catch { }
}
#endregion
private void btnRawStopLoadVideo_Click(object sender, EventArgs e)
{
HkCameraClient.Sdk_NET_DVR_StopGetFile();