279 lines
11 KiB
C#
279 lines
11 KiB
C#
using IntegrationGateway.Core.Abstractions;
|
||
using IntegrationGateway.Core.Infrastructure;
|
||
using IntegrationGateway.Core.Models;
|
||
using System.Text.Json;
|
||
using System.Net.Http.Json;
|
||
|
||
namespace IntegrationGateway.Adapters.Owl;
|
||
|
||
/// <summary>
|
||
/// Owl 视频监控子系统适配器。
|
||
///
|
||
/// 实现的能力接口:
|
||
/// - IHasFlatDevices:设备列表(NVR)和通道列表
|
||
/// - IHasStreams:实时取流、录像回放、云台控制、截图
|
||
/// - IHasRecordings:录像文件查询
|
||
/// - IAcceptsMetadataPush:设备元数据回写(如改名)
|
||
///
|
||
/// 限流:5 QPS(Owl 推荐值)
|
||
/// PTZ 限制:仅支持 continuous 方向移动 + stop,不支持预设位
|
||
/// </summary>
|
||
public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAcceptsMetadataPush
|
||
{
|
||
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
|
||
};
|
||
|
||
/// <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;
|
||
_http = http;
|
||
_auth = new OwlAuthHelper(http, baseUrl, username, password);
|
||
}
|
||
|
||
/// <summary>初始化适配器:获取 Owl JWT Token</summary>
|
||
public async Task InitializeAsync() => await _auth.GetTokenAsync();
|
||
|
||
/// <summary>健康检查:尝试访问 Owl /health 端点</summary>
|
||
public async Task<bool> HealthCheckAsync()
|
||
{
|
||
try
|
||
{
|
||
var client = await _auth.GetAuthenticatedClientAsync();
|
||
var resp = await client.GetAsync("/health");
|
||
return resp.IsSuccessStatusCode;
|
||
}
|
||
catch { return false; }
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// IHasFlatDevices 实现
|
||
// ═══════════════════════════════════════════
|
||
|
||
/// <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}";
|
||
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>
|
||
{
|
||
Items = owl.Items.Select(MapDevice).ToList(),
|
||
Total = owl.Total
|
||
};
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// IHasStreams 实现
|
||
// ═══════════════════════════════════════════
|
||
|
||
/// <summary>获取通道实时视频流地址</summary>
|
||
public async Task<StreamUrls> GetLiveUrlAsync(string channelId)
|
||
{
|
||
await _limiter.WaitAsync();
|
||
var client = await _auth.GetAuthenticatedClientAsync();
|
||
var resp = await client.PostAsync($"/channels/{channelId}/play", null);
|
||
resp.EnsureSuccessStatusCode();
|
||
var json = await resp.Content.ReadAsStringAsync();
|
||
var play = JsonSerializer.Deserialize<OwlPlayResponse>(json)!;
|
||
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 startMs = new DateTimeOffset(start).ToUnixTimeMilliseconds();
|
||
var endMs = new DateTimeOffset(end).ToUnixTimeMilliseconds();
|
||
var token = await _auth.GetTokenAsync();
|
||
return new StreamUrls
|
||
{
|
||
Hls = $"{client.BaseAddress}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 });
|
||
}
|
||
|
||
/// <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" });
|
||
}
|
||
|
||
/// <summary>获取通道实时截图</summary>
|
||
public async Task<StreamUrls> GetSnapshotAsync(string channelId)
|
||
{
|
||
await _limiter.WaitAsync();
|
||
var client = await _auth.GetAuthenticatedClientAsync();
|
||
var resp = await client.PostAsync($"/channels/{channelId}/snapshot",
|
||
new StringContent("{}", System.Text.Encoding.UTF8, "application/json"));
|
||
var json = await resp.Content.ReadAsStringAsync();
|
||
var snap = JsonSerializer.Deserialize<OwlSnapshotResponse>(json)!;
|
||
return new StreamUrls { Hls = snap.Link };
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// IHasRecordings 实现
|
||
// ═══════════════════════════════════════════
|
||
|
||
/// <summary>分页查询录像文件记录</summary>
|
||
public async Task<PagedResult<StandardRecording>> GetRecordingsAsync(
|
||
string channelId, DateTime start, DateTime end, int page, int size)
|
||
{
|
||
await _limiter.WaitAsync();
|
||
var client = await _auth.GetAuthenticatedClientAsync();
|
||
var startMs = new DateTimeOffset(start).ToUnixTimeMilliseconds();
|
||
var endMs = new DateTimeOffset(end).ToUnixTimeMilliseconds();
|
||
var json = await client.GetStringAsync(
|
||
$"/recordings?cid={channelId}&start_ms={startMs}&end_ms={endMs}&page={page}&size={size}");
|
||
var owl = JsonSerializer.Deserialize<OwlPagedResult<OwlRecording>>(json)!;
|
||
return new PagedResult<StandardRecording>
|
||
{
|
||
Items = owl.Items.Select(r => new StandardRecording
|
||
{
|
||
Id = r.Id, ChannelId = r.Cid,
|
||
StartedAt = r.StartedAt, EndedAt = r.EndedAt,
|
||
Duration = r.Duration, FilePath = r.Path, Size = r.Size
|
||
}).ToList(),
|
||
Total = owl.Total
|
||
};
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// IAcceptsMetadataPush 实现
|
||
// ═══════════════════════════════════════════
|
||
|
||
/// <summary>回写设备元数据(如改名)到 Owl</summary>
|
||
public async Task<MetadataPushResult> PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes)
|
||
{
|
||
var client = await _auth.GetAuthenticatedClientAsync();
|
||
var body = new Dictionary<string, object>();
|
||
if (changes.Name != null) body["name"] = changes.Name;
|
||
await client.PutAsJsonAsync($"/devices/{sourceDeviceId}", body);
|
||
return new MetadataPushResult { Success = true };
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// 内部映射方法
|
||
// ═══════════════════════════════════════════
|
||
|
||
/// <summary>Owl 设备 → StandardDevice 映射</summary>
|
||
private static StandardDevice MapDevice(OwlDevice d) => 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 port) ? port : null,
|
||
Extra = new Dictionary<string, object?>
|
||
{
|
||
["owlDeviceId"] = d.Id,
|
||
["protocol"] = d.Protocol ?? "GB28181",
|
||
["transport"] = d.Transport
|
||
}
|
||
};
|
||
|
||
/// <summary>Owl 播放响应 → StreamUrls 映射(取第一个可用流)</summary>
|
||
private static StreamUrls MapStreamUrls(OwlPlayResponse play)
|
||
{
|
||
var item = play.Items?.FirstOrDefault();
|
||
return new StreamUrls
|
||
{
|
||
WsFlv = item?.WsFlv, HttpFlv = item?.HttpFlv, Hls = item?.Hls,
|
||
WebRtc = item?.WebRtc, Rtmp = item?.Rtmp, Rtsp = item?.Rtsp
|
||
};
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// 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; }
|
||
}
|