diff --git a/gateway/src/IntegrationGateway.Adapters.Owl/OwlAdapter.cs b/gateway/src/IntegrationGateway.Adapters.Owl/OwlAdapter.cs
index b8e3c26..68e747e 100644
--- a/gateway/src/IntegrationGateway.Adapters.Owl/OwlAdapter.cs
+++ b/gateway/src/IntegrationGateway.Adapters.Owl/OwlAdapter.cs
@@ -10,37 +10,29 @@ namespace IntegrationGateway.Adapters.Owl;
/// Owl 视频监控子系统适配器。
///
/// 实现的能力接口:
-/// - IHasFlatDevices:设备列表(NVR)和通道列表
+/// - IHasFlatDevices:GET /devices/channels → 设备+通道联合映射
/// - IHasStreams:实时取流、录像回放、云台控制、截图
/// - IHasRecordings:录像文件查询
/// - IAcceptsMetadataPush:设备元数据回写(如改名)
+/// - IHasAlarms:AI 事件映射
+/// - IAcceptsControl:AI 检测启停、远程控制
///
-/// 限流:5 QPS(Owl 推荐值)
-/// PTZ 限制:仅支持 continuous 方向移动 + stop,不支持预设位
+/// 限流:5 QPS
///
-public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAcceptsMetadataPush
+public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAcceptsMetadataPush, IHasAlarms, IAcceptsControl
{
private readonly HttpClient _http;
private readonly OwlAuthHelper _auth;
- /// 令牌桶限流器(5 QPS)
private readonly RateLimiter _limiter = new(5);
- /// 适配器编码,格式 "Owl:实例名"
public string AdapterCode { get; }
- /// 人类可读的适配器名称
public string DisplayName => $"Owl ({AdapterCode})";
- /// 适配器能力声明
public AdapterCapabilities Capabilities => new()
{
- HasFlatDevices = true, HasStreams = true, HasPtz = true, HasRecordings = true, AcceptsMetadataPush = true
+ HasFlatDevices = true, HasStreams = true, HasPtz = true,
+ HasRecordings = true, AcceptsMetadataPush = true, HasAlarms = true
};
- /// 创建 OwlAdapter 实例
- /// 适配器编码
- /// HttpClient 实例
- /// Owl 服务地址
- /// 登录用户名
- /// 登录密码
public OwlAdapter(string adapterCode, HttpClient http, string baseUrl, string username, string password)
{
AdapterCode = adapterCode;
@@ -48,10 +40,12 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
_auth = new OwlAuthHelper(http, baseUrl, username, password);
}
- /// 初始化适配器:获取 Owl JWT Token
public async Task InitializeAsync() => await _auth.GetTokenAsync();
- /// 健康检查:尝试访问 Owl /health 端点
+ // ═══════════════════════════════════════════
+ // IGatewayAdapter — 健康检查
+ // ═══════════════════════════════════════════
+
public async Task HealthCheckAsync()
{
try
@@ -64,30 +58,76 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
}
// ═══════════════════════════════════════════
- // IHasFlatDevices 实现
+ // IHasFlatDevices — GET /devices/channels 设备+通道联合映射
// ═══════════════════════════════════════════
- /// 分页获取 NVR 设备列表
public async Task> GetDevicesAsync(int page, int size, string? keyword = null)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
- var url = $"/devices?page={page}&size={size}";
+ var url = $"/devices/channels?page={page}&size=1000";
if (!string.IsNullOrEmpty(keyword)) url += $"&key={Uri.EscapeDataString(keyword)}";
var json = await client.GetStringAsync(url);
- var owl = JsonSerializer.Deserialize>(json)!;
- return new PagedResult
+ var result = JsonSerializer.Deserialize>(json)!;
+
+ var devices = new List();
+ var deviceItems = result.Items.Where(x => x.Type == "DEVICE").ToList();
+ var channelItems = result.Items.Where(x => x.Type == "CHANNEL").ToList();
+
+ foreach (var d in deviceItems)
{
- Items = owl.Items.Select(MapDevice).ToList(),
- Total = owl.Total
- };
+ var childChannels = channelItems.Where(c => c.Did == d.Id).ToList();
+ devices.Add(MapDevice(d, childChannels));
+ foreach (var ch in childChannels)
+ devices.Add(MapChannel(ch, d.Id));
+ }
+ return new PagedResult { Items = devices, Total = devices.Count };
}
+ private static StandardDevice MapDevice(OwlDeviceChannel d, List channels) => new()
+ {
+ SourceId = d.Id ?? "",
+ Name = d.Name ?? d.Id ?? "",
+ Category = "硬盘录像机",
+ Group = "视频设备",
+ IsOnline = d.IsOnline == "1",
+ IsParent = true,
+ IpAddress = d.Address,
+ Port = int.TryParse(d.Port, out var p) ? p : null,
+ Extra = new Dictionary
+ {
+ ["manufacturer"] = d.Manufacturer,
+ ["model"] = d.Model,
+ ["firmware"] = d.Firmware,
+ ["longitude"] = d.Longitude,
+ ["latitude"] = d.Latitude,
+ ["protocol"] = d.Protocol ?? "GB28181",
+ ["transport"] = d.Transport,
+ ["channelCount"] = d.ChannelCount ?? channels.Count
+ }
+ };
+
+ private static StandardDevice MapChannel(OwlDeviceChannel ch, string? parentDeviceId) => new()
+ {
+ SourceId = ch.Id ?? "",
+ Name = ch.Name ?? $"通道{ch.Id}",
+ Category = "摄像机",
+ Group = "视频设备",
+ IsOnline = ch.IsOnline?.ToLower() == "true" || ch.IsOnline == "1",
+ IsParent = false,
+ ParentSourceId = parentDeviceId,
+ Extra = new Dictionary
+ {
+ ["hasPtz"] = (ch.Ptztype ?? 0) > 0 ? "1" : "0",
+ ["app"] = ch.App,
+ ["streamId"] = ch.StreamId
+ }
+ };
+
// ═══════════════════════════════════════════
- // IHasStreams 实现
+ // IHasStreams
// ═══════════════════════════════════════════
- /// 获取通道实时视频流地址
public async Task GetLiveUrlAsync(string channelId)
{
await _limiter.WaitAsync();
@@ -99,39 +139,43 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
return MapStreamUrls(play);
}
- /// 获取历史录像回放地址(HLS VOD 格式)
public async Task GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
+ var token = await _auth.GetTokenAsync();
var startMs = new DateTimeOffset(start).ToUnixTimeMilliseconds();
var endMs = new DateTimeOffset(end).ToUnixTimeMilliseconds();
- var token = await _auth.GetTokenAsync();
+ var baseUrl = (client.BaseAddress?.ToString() ?? "").TrimEnd('/');
return new StreamUrls
{
- Hls = $"{client.BaseAddress}recordings/channels/{channelId}/index.m3u8?start_ms={startMs}&end_ms={endMs}&token={token}"
+ Hls = $"{baseUrl}/recordings/channels/{channelId}/index.m3u8?start_ms={startMs}&end_ms={endMs}&token={token}"
};
}
- /// 云台方向控制(continuous 模式,仅方向移动)
public async Task PtzControlAsync(string channelId, string direction, float speed)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
- await client.PostAsJsonAsync($"/channels/{channelId}/ptz/control",
- new { action = "continuous", direction, speed });
+ if (direction.StartsWith("preset_"))
+ {
+ var idx = int.Parse(direction.Replace("preset_", ""));
+ await client.PostAsJsonAsync($"/channels/{channelId}/ptz/control", new { action = "preset", preset = idx });
+ }
+ else
+ {
+ await client.PostAsJsonAsync($"/channels/{channelId}/ptz/control",
+ new { action = "continuous", direction, speed });
+ }
}
- /// 云台停止
public async Task PtzStopAsync(string channelId)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
- await client.PostAsJsonAsync($"/channels/{channelId}/ptz/control",
- new { action = "stop" });
+ await client.PostAsJsonAsync($"/channels/{channelId}/ptz/control", new { action = "stop" });
}
- /// 获取通道实时截图
public async Task GetSnapshotAsync(string channelId)
{
await _limiter.WaitAsync();
@@ -144,10 +188,9 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
}
// ═══════════════════════════════════════════
- // IHasRecordings 实现
+ // IHasRecordings
// ═══════════════════════════════════════════
- /// 分页查询录像文件记录
public async Task> GetRecordingsAsync(
string channelId, DateTime start, DateTime end, int page, int size)
{
@@ -171,10 +214,9 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
}
// ═══════════════════════════════════════════
- // IAcceptsMetadataPush 实现
+ // IAcceptsMetadataPush
// ═══════════════════════════════════════════
- /// 回写设备元数据(如改名)到 Owl
public async Task PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes)
{
var client = await _auth.GetAuthenticatedClientAsync();
@@ -185,29 +227,75 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
}
// ═══════════════════════════════════════════
- // 内部映射方法
+ // IHasAlarms — AI 事件
// ═══════════════════════════════════════════
- /// Owl 设备 → StandardDevice 映射
- private static StandardDevice MapDevice(OwlDevice d) => new()
+ public async Task> GetAlarmsAsync(
+ int page, int size, DateTime from, DateTime to, string? level = null, string? state = null)
{
- SourceId = d.Id ?? "",
- Name = d.Name ?? d.Id ?? "",
- Category = "硬盘录像机",
- Group = "视频设备",
- IsOnline = d.IsOnline == "1",
- IsParent = true,
- IpAddress = d.Address,
- Port = int.TryParse(d.Port, out var port) ? port : null,
- Extra = new Dictionary
+ await _limiter.WaitAsync();
+ var client = await _auth.GetAuthenticatedClientAsync();
+ var fromMs = new DateTimeOffset(from).ToUnixTimeMilliseconds();
+ var toMs = new DateTimeOffset(to).ToUnixTimeMilliseconds();
+ var json = await client.GetStringAsync(
+ $"/events?page={page}&size={size}&start_ms={fromMs}&end_ms={toMs}");
+ var result = JsonSerializer.Deserialize>(json)!;
+ return new PagedResult
{
- ["owlDeviceId"] = d.Id,
- ["protocol"] = d.Protocol ?? "GB28181",
- ["transport"] = d.Transport
- }
+ Items = result.Items.Select(MapEventToAlarm).ToList(),
+ Total = result.Total
+ };
+ }
+
+ public Task ConfirmAlarmAsync(string alarmId) => Task.CompletedTask;
+ public Task EndAlarmAsync(string alarmId) => Task.CompletedTask;
+
+ private StandardAlarm MapEventToAlarm(OwlAiEvent e) => new()
+ {
+ AlarmId = $"owl-ai-{e.Id}",
+ AdapterCode = AdapterCode,
+ Level = e.Label switch { "person" => "重要", "car" => "重要", _ => "普通" },
+ Title = $"AI检测: {e.Label} (置信度 {e.Score:P0})",
+ Content = e.Zones ?? "",
+ OccurTime = e.StartedAt.HasValue
+ ? DateTimeOffset.FromUnixTimeMilliseconds(e.StartedAt.Value).DateTime
+ : DateTime.MinValue,
+ Status = (e.EndedAt ?? 0) > 0 ? "已结束" : "未确认"
};
- /// Owl 播放响应 → StreamUrls 映射(取第一个可用流)
+ // ═══════════════════════════════════════════
+ // IAcceptsControl — AI 启停 + 区域管理
+ // ═══════════════════════════════════════════
+
+ public async Task SendControlAsync(string sourceDeviceId, string command, Dictionary parameters)
+ {
+ await _limiter.WaitAsync();
+ var client = await _auth.GetAuthenticatedClientAsync();
+ try
+ {
+ switch (command)
+ {
+ case "ai-enable":
+ await client.PostAsync($"/channels/{sourceDeviceId}/ai/enable", null);
+ break;
+ case "ai-disable":
+ await client.PostAsync($"/channels/{sourceDeviceId}/ai/disable", null);
+ break;
+ case "zone-add":
+ await client.PostAsJsonAsync($"/channels/{sourceDeviceId}/zones", parameters!);
+ break;
+ default:
+ return new ControlResult { Success = false, Message = $"不支持的指令: {command}" };
+ }
+ return new ControlResult { Success = true };
+ }
+ catch (Exception ex) { return new ControlResult { Success = false, Message = ex.Message }; }
+ }
+
+ // ═══════════════════════════════════════════
+ // 内部工具
+ // ═══════════════════════════════════════════
+
private static StreamUrls MapStreamUrls(OwlPlayResponse play)
{
var item = play.Items?.FirstOrDefault();
@@ -218,61 +306,3 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
};
}
}
-
-// ═══════════════════════════════════════════
-// Owl JSON 反序列化模型(内部使用)
-// ═══════════════════════════════════════════
-
-/// Owl API 分页响应
-public class OwlPagedResult
-{
- public List Items { get; set; } = new();
- public int Total { get; set; }
-}
-
-/// Owl 设备(NVR)
-public class OwlDevice
-{
- public string? Id { get; set; }
- public string? Name { get; set; }
- public string? IsOnline { get; set; }
- public string? Protocol { get; set; }
- public string? Address { get; set; }
- public string? Port { get; set; }
- public string? Transport { get; set; }
-}
-
-/// Owl 播放响应
-public class OwlPlayResponse
-{
- public List? Items { get; set; }
-}
-
-/// Owl 播放流条目
-public class OwlPlayItem
-{
- public string? WsFlv { get; set; }
- public string? HttpFlv { get; set; }
- public string? Hls { get; set; }
- public string? WebRtc { get; set; }
- public string? Rtmp { get; set; }
- public string? Rtsp { get; set; }
-}
-
-/// Owl 截图响应
-public class OwlSnapshotResponse
-{
- public string? Link { get; set; }
-}
-
-/// Owl 录像记录
-public class OwlRecording
-{
- public int Id { get; set; }
- public string? Cid { get; set; }
- public DateTime StartedAt { get; set; }
- public DateTime EndedAt { get; set; }
- public double Duration { get; set; }
- public string? Path { get; set; }
- public long Size { get; set; }
-}
diff --git a/gateway/src/IntegrationGateway.Adapters.Owl/OwlModels.cs b/gateway/src/IntegrationGateway.Adapters.Owl/OwlModels.cs
new file mode 100644
index 0000000..c4291c9
--- /dev/null
+++ b/gateway/src/IntegrationGateway.Adapters.Owl/OwlModels.cs
@@ -0,0 +1,107 @@
+///
+/// Owl/GoWVP API 响应模型。
+/// 从 OwlAdapter.cs 分离,便于维护和扩展。
+///
+namespace IntegrationGateway.Adapters.Owl;
+
+// ═══════════════════════════════════════════
+// 通用
+// ═══════════════════════════════════════════
+
+/// Owl API 分页响应
+public class OwlPagedResult
+{
+ public List Items { get; set; } = new();
+ public int Total { get; set; }
+}
+
+// ═══════════════════════════════════════════
+// 设备+通道联合模型 (GET /devices/channels)
+// ═══════════════════════════════════════════
+
+/// Owl 设备或通道(联合接口返回)
+public class OwlDeviceChannel
+{
+ public string? Id { get; set; }
+ public string? Type { get; set; } // "DEVICE" | "CHANNEL"
+ public string? Did { get; set; } // 通道所属设备 ID
+ public string? Name { get; set; }
+ public string? IsOnline { get; set; } // DEVICE: "1"/"0", CHANNEL: true/false
+ public string? Manufacturer { get; set; }
+ public string? Model { get; set; }
+ public string? Firmware { get; set; }
+ public string? Longitude { get; set; }
+ public string? Latitude { get; set; }
+ public int? ChannelCount { get; set; }
+ public int? Ptztype { get; set; } // 0=无云台, 1=方向, 2=预置位
+ public string? Protocol { get; set; }
+ public string? Address { get; set; }
+ public string? Port { get; set; }
+ public string? Transport { get; set; }
+ public string? App { get; set; } // 流应用名
+ public string? StreamId { get; set; } // 流ID
+ public string? Status { get; set; }
+ public string? RegisterWay { get; set; }
+}
+
+// ═══════════════════════════════════════════
+// 播放/流
+// ═══════════════════════════════════════════
+
+/// Owl 播放响应
+public class OwlPlayResponse
+{
+ public List? Items { get; set; }
+}
+
+/// Owl 播放流条目
+public class OwlPlayItem
+{
+ public string? WsFlv { get; set; }
+ public string? HttpFlv { get; set; }
+ public string? Hls { get; set; }
+ public string? WebRtc { get; set; }
+ public string? Rtmp { get; set; }
+ public string? Rtsp { get; set; }
+}
+
+/// Owl 截图响应
+public class OwlSnapshotResponse
+{
+ public string? Link { get; set; }
+}
+
+// ═══════════════════════════════════════════
+// 录像
+// ═══════════════════════════════════════════
+
+/// Owl 录像记录
+public class OwlRecording
+{
+ public int Id { get; set; }
+ public string? Cid { get; set; }
+ public DateTime StartedAt { get; set; }
+ public DateTime EndedAt { get; set; }
+ public double Duration { get; set; }
+ public string? Path { get; set; }
+ public long Size { get; set; }
+}
+
+// ═══════════════════════════════════════════
+// AI 事件
+// ═══════════════════════════════════════════
+
+/// Owl AI 检测事件
+public class OwlAiEvent
+{
+ public long? Id { get; set; }
+ public string? Did { get; set; }
+ public string? Cid { get; set; }
+ public long? StartedAt { get; set; } // 毫秒时间戳
+ public long? EndedAt { get; set; }
+ public string? Label { get; set; } // person / car / ...
+ public float? Score { get; set; }
+ public string? Zones { get; set; }
+ public string? ImagePath { get; set; }
+ public string? Model { get; set; }
+}