diff --git a/SHH.CameraSdk/Abstractions/IHikContext.cs b/SHH.CameraSdk/Abstractions/IHikContext.cs
new file mode 100644
index 0000000..9989a1d
--- /dev/null
+++ b/SHH.CameraSdk/Abstractions/IHikContext.cs
@@ -0,0 +1,14 @@
+namespace SHH.CameraSdk;
+
+///
+/// 海康驱动上下文
+/// 作用:允许功能组件(如校时、云台)访问主驱动的核心数据,而无需公开给外部
+///
+public interface IHikContext
+{
+ /// 获取 SDK 登录句柄 (lUserId)
+ int GetUserId();
+
+ /// 获取设备 IP (用于日志)
+ string GetDeviceIp();
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Abstractions/ISyncFeature.cs b/SHH.CameraSdk/Abstractions/ISyncFeature.cs
new file mode 100644
index 0000000..74c50d1
--- /dev/null
+++ b/SHH.CameraSdk/Abstractions/ISyncFeature.cs
@@ -0,0 +1,40 @@
+using SHH.CameraSdk.HikFeatures;
+
+namespace SHH.CameraSdk;
+
+///
+/// 能力接口:时间同步
+/// 只有实现了此接口的设备,才支持 WebAPI 的时间查询与设置
+///
+public interface ITimeSyncFeature
+{
+ /// 获取设备当前时间
+ Task GetTimeAsync();
+
+ /// 设置设备时间
+ Task SetTimeAsync(DateTime time);
+}
+
+///
+/// 能力接口:设备重启
+///
+public interface IRebootFeature
+{
+ ///
+ /// 发送重启指令
+ ///
+ /// 任务完成表示指令发送成功
+ Task RebootAsync();
+}
+
+///
+/// 能力接口:云台控制
+///
+public interface IPtzFeature
+{
+ // 原有的手动控制 (按下/松开)
+ Task PtzControlAsync(PtzAction action, bool stop, int speed = 4);
+
+ // [新增] 点动控制 (自动复位)
+ Task PtzStepAsync(PtzAction action, int durationMs, int speed = 4);
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Controllers/CamerasController.cs b/SHH.CameraSdk/Controllers/CamerasController.cs
index 33c446b..22c0c98 100644
--- a/SHH.CameraSdk/Controllers/CamerasController.cs
+++ b/SHH.CameraSdk/Controllers/CamerasController.cs
@@ -399,4 +399,126 @@ public class CamerasController : ControllerBase
// 3. 返回 JSON 给前端
return Ok(options);
}
+
+ ///
+ /// 获取设备时间 (支持海康/大华等具备此能力的设备)
+ ///
+ [HttpGet("{id}/time")]
+ public async Task GetDeviceTime(long id)
+ {
+ var device = _manager.GetDevice(id);
+ if (device == null) return NotFound(new { error = "Device not found" });
+
+ if (!device.IsPhysicalOnline) return BadRequest(new { error = "Device is offline" });
+
+ // 【核心】模式匹配:判断这个设备是否实现了“时间同步接口”
+ if (device is ITimeSyncFeature timeFeature)
+ {
+ try
+ {
+ var time = await timeFeature.GetTimeAsync();
+ return Ok(new { deviceId = id, currentTime = time });
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(500, new { error = ex.Message });
+ }
+ }
+
+ // 如果是 RTSP/USB 等不支持的设备
+ return BadRequest(new { error = "This device does not support time synchronization." });
+ }
+
+ ///
+ /// 设置设备时间
+ ///
+ [HttpPost("{id}/time")]
+ public async Task SetDeviceTime(long id, [FromBody] DateTime time)
+ {
+ var device = _manager.GetDevice(id);
+ if (device == null) return NotFound();
+
+ if (device is ITimeSyncFeature timeFeature)
+ {
+ try
+ {
+ await timeFeature.SetTimeAsync(time);
+ return Ok(new { success = true, message = $"Time synced to {time}" });
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(500, new { error = ex.Message });
+ }
+ }
+
+ return BadRequest(new { error = "This device does not support time synchronization." });
+ }
+
+ ///
+ /// 远程重启设备
+ ///
+ [HttpPost("{id}/reboot")]
+ public async Task RebootDevice(long id)
+ {
+ var device = _manager.GetDevice(id);
+ if (device == null) return NotFound(new { error = "Device not found" });
+
+ // 依然是两道防线:先检查在线,再检查能力
+ if (!device.IsOnline) return BadRequest(new { error = "Device is offline" });
+
+ if (device is IRebootFeature rebootFeature)
+ {
+ try
+ {
+ await rebootFeature.RebootAsync();
+
+ // 记录审计日志 (建议加上)
+ device.AddAuditLog("用户执行了远程重启");
+
+ return Ok(new { success = true, message = "重启指令已发送,设备将在几分钟后重新上线。" });
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(500, new { error = ex.Message });
+ }
+ }
+
+ return BadRequest(new { error = "This device does not support remote reboot." });
+ }
+
+ [HttpPost("{id}/ptz")]
+ public async Task PtzControl(long id, [FromBody] PtzControlDto dto)
+ {
+ var device = _manager.GetDevice(id);
+ if (device == null) return NotFound();
+ if (!device.IsOnline) return BadRequest("Device offline");
+
+ if (device is IPtzFeature ptz)
+ {
+ try
+ {
+ // 逻辑分流
+ if (dto.Duration > 0)
+ {
+ // 场景:点动模式 (一次调用,自动停止)
+ // 建议限制一下最大时长,防止前端传个 10000秒 导致云台转疯了
+ int safeDuration = Math.Clamp(dto.Duration, 50, 2000);
+ await ptz.PtzStepAsync(dto.Action, safeDuration, dto.Speed);
+ }
+ else
+ {
+ // 场景:手动模式 (按下/松开)
+ await ptz.PtzControlAsync(dto.Action, dto.Stop, dto.Speed);
+ }
+
+ return Ok();
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(500, new { error = ex.Message });
+ }//
+ }
+
+ return BadRequest("Device does not support PTZ.");
+ }
}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Controllers/Dto/PtzControlDto.cs b/SHH.CameraSdk/Controllers/Dto/PtzControlDto.cs
new file mode 100644
index 0000000..9e7f175
--- /dev/null
+++ b/SHH.CameraSdk/Controllers/Dto/PtzControlDto.cs
@@ -0,0 +1,22 @@
+using SHH.CameraSdk.HikFeatures;
+
+namespace SHH.CameraSdk;
+
+public class PtzControlDto
+{
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public PtzAction Action { get; set; }
+
+ ///
+ /// 仅用于手动模式:true=停止, false=开始
+ ///
+ public bool Stop { get; set; }
+
+ public int Speed { get; set; } = 4;
+
+ ///
+ /// [新增] 点动耗时 (毫秒)
+ /// 如果此值 > 0,则忽略 Stop 参数,执行 "开始 -> 等待 -> 停止" 的原子操作
+ ///
+ public int Duration { get; set; } = 0;
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Drivers/HikVision/Features/FeaturesEnums.cs b/SHH.CameraSdk/Drivers/HikVision/Features/FeaturesEnums.cs
new file mode 100644
index 0000000..6bf8e09
--- /dev/null
+++ b/SHH.CameraSdk/Drivers/HikVision/Features/FeaturesEnums.cs
@@ -0,0 +1,19 @@
+namespace SHH.CameraSdk.HikFeatures;
+
+///
+/// 云台动作枚举
+///
+public enum PtzAction
+{
+ Up,
+ Down,
+ Left,
+ Right,
+ ZoomIn, // 放大
+ ZoomOut, // 缩小
+ FocusNear, // 聚焦近
+ FocusFar, // 聚焦远
+ IrisOpen, // 光圈大
+ IrisClose, // 光圈小
+ Wiper, // 雨刷
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Drivers/HikVision/Features/HikPtzProvider.cs b/SHH.CameraSdk/Drivers/HikVision/Features/HikPtzProvider.cs
new file mode 100644
index 0000000..580f8ec
--- /dev/null
+++ b/SHH.CameraSdk/Drivers/HikVision/Features/HikPtzProvider.cs
@@ -0,0 +1,71 @@
+namespace SHH.CameraSdk.HikFeatures;
+
+public class HikPtzProvider : IPtzFeature
+{
+ private readonly IHikContext _context;
+
+ public HikPtzProvider(IHikContext context)
+ {
+ _context = context;
+ }
+
+ public async Task PtzControlAsync(PtzAction action, bool stop, int speed)
+ {
+ int userId = _context.GetUserId();
+ if (userId < 0) throw new InvalidOperationException("设备离线");
+
+ // 1. 映射指令
+ uint hikCommand = action switch
+ {
+ PtzAction.Up => HikNativeMethods.TILT_UP,
+ PtzAction.Down => HikNativeMethods.TILT_DOWN,
+ PtzAction.Left => HikNativeMethods.PAN_LEFT,
+ PtzAction.Right => HikNativeMethods.PAN_RIGHT,
+ PtzAction.ZoomIn => HikNativeMethods.ZOOM_IN,
+ PtzAction.ZoomOut => HikNativeMethods.ZOOM_OUT,
+ PtzAction.FocusNear => HikNativeMethods.FOCUS_NEAR,
+ PtzAction.FocusFar => HikNativeMethods.FOCUS_FAR,
+ PtzAction.IrisOpen => HikNativeMethods.IRIS_OPEN,
+ PtzAction.IrisClose => HikNativeMethods.IRIS_CLOSE,
+ PtzAction.Wiper => HikNativeMethods.WIPER_PWRON,
+ _ => 0
+ };
+
+ if (hikCommand == 0) return;
+
+ // 2. 转换停止标志 (海康: 0=开始, 1=停止)
+ uint dwStop = stop ? 1u : 0u;
+
+ // 3. 限制速度范围 (1-7)
+ uint dwSpeed = (uint)Math.Clamp(speed, 1, 7);
+
+ // 4. 调用 SDK
+ await Task.Run(() =>
+ {
+ // Channel 默认为 1
+ bool result = HikNativeMethods.NET_DVR_PTZControlWithSpeed_Other(userId, 1, hikCommand, dwStop, dwSpeed);
+
+ if (!result)
+ {
+ // 这里通常不抛异常,因为云台繁忙或到头是常态,记录日志即可
+ // Console.WriteLine($"PTZ Error: {HikNativeMethods.NET_DVR_GetLastError()}");
+ }
+ });
+ }
+
+ public async Task PtzStepAsync(PtzAction action, int durationMs, int speed)
+ {
+ // 1. 开始转动 (调用已有的逻辑,stop=false)
+ await PtzControlAsync(action, false, speed);
+
+ // 2. 等待指定时间 (非阻塞等待)
+ // 注意:这里使用 Task.Delay,精度对云台控制来说足够了
+ if (durationMs > 0)
+ {
+ await Task.Delay(durationMs);
+ }
+
+ // 3. 停止转动 (调用已有的逻辑,stop=true)
+ await PtzControlAsync(action, true, speed);
+ }
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Drivers/HikVision/Features/HikRebootProvider.cs b/SHH.CameraSdk/Drivers/HikVision/Features/HikRebootProvider.cs
new file mode 100644
index 0000000..b601b2a
--- /dev/null
+++ b/SHH.CameraSdk/Drivers/HikVision/Features/HikRebootProvider.cs
@@ -0,0 +1,36 @@
+namespace SHH.CameraSdk.HikFeatures;
+
+public class HikRebootProvider : IRebootFeature
+{
+ private readonly IHikContext _context;
+
+ public HikRebootProvider(IHikContext context)
+ {
+ _context = context;
+ }
+
+ public async Task RebootAsync()
+ {
+ // 1. 检查登录状态
+ int userId = _context.GetUserId();
+ if (userId < 0) throw new InvalidOperationException("设备未登录或离线,无法发送重启指令");
+
+ // 2. 执行 SDK 调用
+ await Task.Run(() =>
+ {
+ bool result = HikNativeMethods.NET_DVR_RebootDVR(userId);
+
+ if (!result)
+ {
+ uint err = HikNativeMethods.NET_DVR_GetLastError();
+ throw new Exception($"重启指令发送失败,错误码: {err}");
+ }
+ });
+
+ // 3. 注意:
+ // 重启指令发送成功后,设备会断开网络。
+ // 宿主类(HikVideoSource)的保活机制(KeepAlive)会检测到断线,
+ // 并自动开始尝试重连,直到设备重启完成上线。
+ // 所以这里我们不需要手动断开连接,交给底层自愈机制即可。
+ }
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Drivers/HikVision/Features/HikTimeSyncProvider.cs b/SHH.CameraSdk/Drivers/HikVision/Features/HikTimeSyncProvider.cs
new file mode 100644
index 0000000..3f5ce3e
--- /dev/null
+++ b/SHH.CameraSdk/Drivers/HikVision/Features/HikTimeSyncProvider.cs
@@ -0,0 +1,98 @@
+namespace SHH.CameraSdk.HikFeatures;
+
+///
+/// 海康时间同步组件
+///
+public class HikTimeSyncProvider : ITimeSyncFeature
+{
+ private readonly IHikContext _context;
+
+ public HikTimeSyncProvider(IHikContext context)
+ {
+ _context = context;
+ }
+
+ public async Task GetTimeAsync()
+ {
+ // 1. 获取句柄
+ int userId = _context.GetUserId();
+ if (userId < 0) throw new InvalidOperationException("设备未登录");
+
+ return await Task.Run(() =>
+ {
+ uint returned = 0;
+ uint size = (uint)Marshal.SizeOf(typeof(HikNativeMethods.NET_DVR_TIME));
+ IntPtr ptr = Marshal.AllocHGlobal((int)size);
+
+ try
+ {
+ // 调用 SDK: NET_DVR_GET_TIME (命令号 118)
+ bool result = HikNativeMethods.NET_DVR_GetDVRConfig(
+ userId,
+ HikNativeMethods.NET_DVR_GET_TIMECFG,
+ 0,
+ ptr,
+ size,
+ ref returned);
+
+ if (!result)
+ {
+ var err = HikNativeMethods.NET_DVR_GetLastError();
+ throw new Exception($"获取时间失败, 错误码: {err}");
+ }
+
+ var t = Marshal.PtrToStructure(ptr);
+ return new DateTime((int)t.dwYear, (int)t.dwMonth, (int)t.dwDay, (int)t.dwHour, (int)t.dwMinute, (int)t.dwSecond);
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(ptr);
+ }
+ });
+ }
+
+ public async Task SetTimeAsync(DateTime time)
+ {
+ int userId = _context.GetUserId();
+ if (userId < 0) throw new InvalidOperationException("设备未登录");
+
+ await Task.Run(() =>
+ {
+ var t = new HikNativeMethods.NET_DVR_TIME
+ {
+ dwYear = (uint)time.Year,
+ dwMonth = (uint)time.Month,
+ dwDay = (uint)time.Day,
+ dwHour = (uint)time.Hour,
+ dwMinute = (uint)time.Minute,
+ dwSecond = (uint)time.Second
+ };
+
+ uint size = (uint)Marshal.SizeOf(t);
+ IntPtr ptr = Marshal.AllocHGlobal((int)size);
+
+ try
+ {
+ Marshal.StructureToPtr(t, ptr, false);
+
+ // 调用 SDK: NET_DVR_SET_TIME (命令号 119)
+ bool result = HikNativeMethods.NET_DVR_SetDVRConfig(
+ userId,
+ HikNativeMethods.NET_DVR_SET_TIMECFG,
+ 0,
+ ptr,
+ size);
+
+ if (!result)
+ {
+ var err = HikNativeMethods.NET_DVR_GetLastError();
+ throw new Exception($"设置时间失败, 错误码: {err}");
+ }
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(ptr);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs b/SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs
index 37cbd5d..4299773 100644
--- a/SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs
+++ b/SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs
@@ -252,6 +252,9 @@ public static partial class HikNativeMethods
/// 命令常量:获取时间配置
public const uint NET_DVR_GET_TIMECFG = 118;
+ /// 命令常量:设置时间
+ public const int NET_DVR_SET_TIMECFG = 119;
+
#endregion
#region --- PTZ 控制相关 (PTZ Control) ---
@@ -492,4 +495,16 @@ public static partial class HikNativeMethods
ref uint lpBytesReturned);
#endregion
+
+
+ [DllImport(DllName)]
+ public static extern bool NET_DVR_SetDVRConfig(int lUserID, uint dwCommand, int lChannel, System.IntPtr lpInBuffer, uint dwInBufferSize);
+
+ ///
+ /// 设备重启
+ ///
+ ///
+ ///
+ [DllImport(DllName)]
+ public static extern bool NET_DVR_RebootDVR(int lUserID);
}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs
index 4a80e1f..9b30d2e 100644
--- a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs
+++ b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs
@@ -1,4 +1,6 @@
using OpenCvSharp;
+using SHH.CameraSdk.HikFeatures;
+using System;
namespace SHH.CameraSdk;
@@ -9,7 +11,8 @@ namespace SHH.CameraSdk;
/// 2. [Feat A] 热更新支持:实现 OnApplyOptions,支持码流/句柄不亦断线热切换。
/// 3. [Feat B] 审计集成:全面接入 AddAuditLog,对接 Web 运维仪表盘。
///
-public class HikVideoSource : BaseVideoSource
+public class HikVideoSource : BaseVideoSource,
+ IHikContext, ITimeSyncFeature, IRebootFeature, IPtzFeature
{
#region --- 静态资源 (Global Resources) ---
@@ -22,6 +25,33 @@ public class HikVideoSource : BaseVideoSource
#endregion
+ // 声明组件
+ private readonly HikTimeSyncProvider _timeProvider;
+ private readonly HikRebootProvider _rebootProvider;
+ private readonly HikPtzProvider _ptzProvider;
+
+ // ==========================================
+ // 实现 IHikContext (核心数据暴露)
+ // ==========================================
+ public int GetUserId() => _userId; // 暴露父类或私有的 _userId
+ public string GetDeviceIp() => Config.IpAddress;
+
+ // ==========================================
+ // 实现 ITimeSyncFeature (路由转发)
+ // ==========================================
+ // 核心逻辑:全部委托给 _timeProvider 处理,自己不写一行逻辑
+ public Task GetTimeAsync() => _timeProvider.GetTimeAsync();
+
+ public Task SetTimeAsync(DateTime time) => _timeProvider.SetTimeAsync(time);
+
+ public Task RebootAsync() => _rebootProvider.RebootAsync();
+
+ public Task PtzControlAsync(PtzAction action, bool stop, int speed = 4)
+ => _ptzProvider.PtzControlAsync(action, stop, speed);
+
+ public Task PtzStepAsync(PtzAction action, int durationMs, int speed = 4)
+ => _ptzProvider.PtzStepAsync(action, durationMs, speed);
+
#region --- 实例成员 (Instance Members) ---
private int _userId = -1; // SDK 登录句柄
@@ -53,7 +83,10 @@ public class HikVideoSource : BaseVideoSource
public HikVideoSource(VideoSourceConfig config) : base(config)
{
- // 构造函数保持简洁
+ // 初始化组件,将 "this" 作为上下文传进去
+ _timeProvider = new HikTimeSyncProvider(this);
+ _rebootProvider = new HikRebootProvider(this);
+ _ptzProvider = new HikPtzProvider(this);
}
#endregion
diff --git a/SHH.CameraSdk/Htmls/CameraControl.html b/SHH.CameraSdk/Htmls/CameraControl.html
index 346daa9..611787a 100644
--- a/SHH.CameraSdk/Htmls/CameraControl.html
+++ b/SHH.CameraSdk/Htmls/CameraControl.html
@@ -7,80 +7,197 @@
-