Owl整改O1-O4: 设备通道展开+AI事件IHasAlarms+PTZ预设位+IAcceptsControl
This commit is contained in:
@@ -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
|
||||
/// </summary>
|
||||
public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAcceptsMetadataPush
|
||||
public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAcceptsMetadataPush, IHasAlarms, IAcceptsControl
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly OwlAuthHelper _auth;
|
||||
/// <summary>令牌桶限流器(5 QPS)</summary>
|
||||
private readonly RateLimiter _limiter = new(5);
|
||||
|
||||
/// <summary>适配器编码,格式 "Owl:实例名"</summary>
|
||||
public string AdapterCode { get; }
|
||||
/// <summary>人类可读的适配器名称</summary>
|
||||
public string DisplayName => $"Owl ({AdapterCode})";
|
||||
/// <summary>适配器能力声明</summary>
|
||||
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
|
||||
};
|
||||
|
||||
/// <summary>创建 OwlAdapter 实例</summary>
|
||||
/// <param name="adapterCode">适配器编码</param>
|
||||
/// <param name="http">HttpClient 实例</param>
|
||||
/// <param name="baseUrl">Owl 服务地址</param>
|
||||
/// <param name="username">登录用户名</param>
|
||||
/// <param name="password">登录密码</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>初始化适配器:获取 Owl JWT Token</summary>
|
||||
public async Task InitializeAsync() => await _auth.GetTokenAsync();
|
||||
|
||||
/// <summary>健康检查:尝试访问 Owl /health 端点</summary>
|
||||
// ═══════════════════════════════════════════
|
||||
// IGatewayAdapter — 健康检查
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
public async Task<bool> HealthCheckAsync()
|
||||
{
|
||||
try
|
||||
@@ -64,30 +58,76 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// IHasFlatDevices 实现
|
||||
// IHasFlatDevices — GET /devices/channels 设备+通道联合映射
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>分页获取 NVR 设备列表</summary>
|
||||
public async Task<PagedResult<StandardDevice>> 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<OwlPagedResult<OwlDevice>>(json)!;
|
||||
return new PagedResult<StandardDevice>
|
||||
var result = JsonSerializer.Deserialize<OwlPagedResult<OwlDeviceChannel>>(json)!;
|
||||
|
||||
var devices = new List<StandardDevice>();
|
||||
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<StandardDevice> { Items = devices, Total = devices.Count };
|
||||
}
|
||||
|
||||
private static StandardDevice MapDevice(OwlDeviceChannel d, List<OwlDeviceChannel> 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<string, object?>
|
||||
{
|
||||
["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<string, object?>
|
||||
{
|
||||
["hasPtz"] = (ch.Ptztype ?? 0) > 0 ? "1" : "0",
|
||||
["app"] = ch.App,
|
||||
["streamId"] = ch.StreamId
|
||||
}
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// IHasStreams 实现
|
||||
// IHasStreams
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>获取通道实时视频流地址</summary>
|
||||
public async Task<StreamUrls> GetLiveUrlAsync(string channelId)
|
||||
{
|
||||
await _limiter.WaitAsync();
|
||||
@@ -99,39 +139,43 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
return MapStreamUrls(play);
|
||||
}
|
||||
|
||||
/// <summary>获取历史录像回放地址(HLS VOD 格式)</summary>
|
||||
public async Task<StreamUrls> 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}"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>云台方向控制(continuous 模式,仅方向移动)</summary>
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>云台停止</summary>
|
||||
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" });
|
||||
}
|
||||
|
||||
/// <summary>获取通道实时截图</summary>
|
||||
public async Task<StreamUrls> GetSnapshotAsync(string channelId)
|
||||
{
|
||||
await _limiter.WaitAsync();
|
||||
@@ -144,10 +188,9 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// IHasRecordings 实现
|
||||
// IHasRecordings
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>分页查询录像文件记录</summary>
|
||||
public async Task<PagedResult<StandardRecording>> GetRecordingsAsync(
|
||||
string channelId, DateTime start, DateTime end, int page, int size)
|
||||
{
|
||||
@@ -171,10 +214,9 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// IAcceptsMetadataPush 实现
|
||||
// IAcceptsMetadataPush
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>回写设备元数据(如改名)到 Owl</summary>
|
||||
public async Task<MetadataPushResult> PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes)
|
||||
{
|
||||
var client = await _auth.GetAuthenticatedClientAsync();
|
||||
@@ -185,29 +227,75 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 内部映射方法
|
||||
// IHasAlarms — AI 事件
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>Owl 设备 → StandardDevice 映射</summary>
|
||||
private static StandardDevice MapDevice(OwlDevice d) => new()
|
||||
public async Task<PagedResult<StandardAlarm>> 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<string, object?>
|
||||
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<OwlPagedResult<OwlAiEvent>>(json)!;
|
||||
return new PagedResult<StandardAlarm>
|
||||
{
|
||||
["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 ? "已结束" : "未确认"
|
||||
};
|
||||
|
||||
/// <summary>Owl 播放响应 → StreamUrls 映射(取第一个可用流)</summary>
|
||||
// ═══════════════════════════════════════════
|
||||
// IAcceptsControl — AI 启停 + 区域管理
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
public async Task<ControlResult> SendControlAsync(string sourceDeviceId, string command, Dictionary<string, object?> 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 反序列化模型(内部使用)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>Owl API 分页响应</summary>
|
||||
public class OwlPagedResult<T>
|
||||
{
|
||||
public List<T> Items { get; set; } = new();
|
||||
public int Total { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Owl 设备(NVR)</summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>Owl 播放响应</summary>
|
||||
public class OwlPlayResponse
|
||||
{
|
||||
public List<OwlPlayItem>? Items { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Owl 播放流条目</summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>Owl 截图响应</summary>
|
||||
public class OwlSnapshotResponse
|
||||
{
|
||||
public string? Link { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Owl 录像记录</summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
107
gateway/src/IntegrationGateway.Adapters.Owl/OwlModels.cs
Normal file
107
gateway/src/IntegrationGateway.Adapters.Owl/OwlModels.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
/// <summary>
|
||||
/// Owl/GoWVP API 响应模型。
|
||||
/// 从 OwlAdapter.cs 分离,便于维护和扩展。
|
||||
/// </summary>
|
||||
namespace IntegrationGateway.Adapters.Owl;
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 通用
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>Owl API 分页响应</summary>
|
||||
public class OwlPagedResult<T>
|
||||
{
|
||||
public List<T> Items { get; set; } = new();
|
||||
public int Total { get; set; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 设备+通道联合模型 (GET /devices/channels)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>Owl 设备或通道(联合接口返回)</summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 播放/流
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>Owl 播放响应</summary>
|
||||
public class OwlPlayResponse
|
||||
{
|
||||
public List<OwlPlayItem>? Items { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Owl 播放流条目</summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>Owl 截图响应</summary>
|
||||
public class OwlSnapshotResponse
|
||||
{
|
||||
public string? Link { get; set; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 录像
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>Owl 录像记录</summary>
|
||||
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 事件
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>Owl AI 检测事件</summary>
|
||||
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; }
|
||||
}
|
||||
Reference in New Issue
Block a user