using IntegrationGateway.Core.Abstractions; using IntegrationGateway.Core.Infrastructure; using IntegrationGateway.Core.Models; using System.Text.Json; namespace IntegrationGateway.Adapters.Owl; public class OwlAdapter : IHasFlatDevices, IHasStreams, IAcceptsMetadataPush { private readonly HttpClient _http; private readonly OwlAuthHelper _auth; private readonly RateLimiter _limiter = new(5); public string AdapterCode { get; } public string DisplayName => $"Owl ({AdapterCode})"; public AdapterCapabilities Capabilities => new() { HasFlatDevices = true, HasStreams = true, HasPtz = true, HasRecordings = true, AcceptsMetadataPush = true }; public OwlAdapter(string adapterCode, HttpClient http, string baseUrl, string username, string password) { AdapterCode = adapterCode; _http = http; _auth = new OwlAuthHelper(http, baseUrl, username, password); } public async Task InitializeAsync() { await _auth.GetTokenAsync(); } public async Task HealthCheckAsync() { try { var client = await _auth.GetAuthenticatedClientAsync(); var resp = await client.GetAsync("/health"); return resp.IsSuccessStatusCode; } catch { return false; } } // ─── IHasFlatDevices ─── 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}"; if (!string.IsNullOrEmpty(keyword)) url += $"&key={Uri.EscapeDataString(keyword)}"; var json = await client.GetStringAsync(url); var owl = JsonSerializer.Deserialize>(json)!; return new PagedResult { Items = owl.Items.Select(MapDevice).ToList(), Total = owl.Total }; } public async Task GetDeviceAsync(string sourceDeviceId) { await _limiter.WaitAsync(); var client = await _auth.GetAuthenticatedClientAsync(); var json = await client.GetStringAsync($"/devices/{sourceDeviceId}"); var owl = JsonSerializer.Deserialize(json); return owl is null ? null : MapDevice(owl); } public async Task> GetAllDevicesAsync() { var result = new List(); int page = 1; while (true) { var paged = await GetDevicesAsync(page, 50); result.AddRange(paged.Items); if (result.Count >= paged.Total) break; page++; } // Also get channels for each NVR var channels = await GetAllChannelsAsync(); result.AddRange(channels); return result; } public async Task> GetChannelsAsync(int page, int size, string? parentDeviceId = null) { await _limiter.WaitAsync(); var client = await _auth.GetAuthenticatedClientAsync(); var url = $"/channels?page={page}&size={size}"; if (!string.IsNullOrEmpty(parentDeviceId)) { var resp = await client.GetStringAsync($"/devices/{parentDeviceId}/channels"); var channels = JsonSerializer.Deserialize>(resp)!; return new PagedResult { Items = channels.Items.Select(c => MapChannel(c)).ToList(), Total = channels.Total }; } var json = await client.GetStringAsync(url); var owl = JsonSerializer.Deserialize>(json)!; return new PagedResult { Items = owl.Items.Select(c => MapChannel(c)).ToList(), Total = owl.Total }; } public async Task> GetAllChannelsAsync() { await _limiter.WaitAsync(); var client = await _auth.GetAuthenticatedClientAsync(); var json = await client.GetStringAsync("/channels?size=1000"); var owl = JsonSerializer.Deserialize>(json)!; return owl.Items.Select(c => MapChannel(c)).ToList(); } // ─── IHasStreams ─── public async Task 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.GetStringAsync(); var play = JsonSerializer.Deserialize(json)!; return MapStreamUrls(play); } public async Task 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}" }; } public async Task StopPlayAsync(string channelId) { await _limiter.WaitAsync(); var client = await _auth.GetAuthenticatedClientAsync(); await client.PostAsync($"/channels/{channelId}/stop", null); } public async Task 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(json)!; return new StreamUrls { Hls = snap.Link }; } 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 }); } public async Task PtzStopAsync(string channelId) { await _limiter.WaitAsync(); var client = await _auth.GetAuthenticatedClientAsync(); await client.PostAsJsonAsync($"/channels/{channelId}/ptz/control", new { action = "stop" }); } public async Task> 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>(json)!; return new PagedResult { Items = owl.Items.Select(r => new StandardRecording { Id = r.Id.ToString(), ChannelId = r.Cid ?? "", StartedAt = r.StartedAt, EndedAt = r.EndedAt, Duration = r.Duration, FilePath = r.Path, Size = r.Size }).ToList(), Total = owl.Total }; } // ─── IAcceptsMetadataPush ─── public async Task PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes) { var client = await _auth.GetAuthenticatedClientAsync(); var body = new Dictionary(); if (changes.Name != null) body["name"] = changes.Name; var resp = await client.PutAsJsonAsync($"/devices/{sourceDeviceId}", body); return new MetadataPushResult { Success = resp.IsSuccessStatusCode }; } // ─── Mapping ─── 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 { ["owlDeviceId"] = d.Id, ["protocol"] = d.Protocol ?? "GB28181", ["transport"] = d.Transport } }; private static StandardDevice MapChannel(OwlChannel c) => new() { SourceId = c.Id ?? "", Name = c.Name ?? c.Id ?? "", Category = "摄像机", Group = "视频设备", IsOnline = c.IsOnline == "1", IsParent = false, ParentSourceId = c.DeviceId, Extra = new Dictionary { ["owlChannelId"] = c.Id, ["streamApp"] = c.App, ["streamName"] = c.Stream, ["hasPtz"] = c.HasPtz == "1" } }; 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 Models ─── public class OwlPagedResult { public List Items { get; set; } = new(); public int Total { get; set; } } 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; } public int Channels { get; set; } } public class OwlChannel { public string? Id { get; set; } public string? DeviceId { get; set; } public string? Name { get; set; } public string? IsOnline { get; set; } public string? App { get; set; } public string? Stream { get; set; } public string? HasPtz { get; set; } public string? HasRecording { get; set; } public int RecordMode { get; set; } } public class OwlPlayResponse { public string? App { get; set; } public string? Stream { get; set; } public List? Items { get; set; } } public class OwlPlayItem { public string? Label { get; set; } 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; } } public class OwlSnapshotResponse { public string? Link { get; set; } } 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; } }