diff --git a/gateway/IntegrationGateway.slnx b/gateway/IntegrationGateway.slnx index 160e8ab..9fea9e6 100644 --- a/gateway/IntegrationGateway.slnx +++ b/gateway/IntegrationGateway.slnx @@ -1,4 +1,7 @@ + + + diff --git a/gateway/src/IntegrationGateway.Adapters.Owl/IntegrationGateway.Adapters.Owl.csproj b/gateway/src/IntegrationGateway.Adapters.Owl/IntegrationGateway.Adapters.Owl.csproj new file mode 100644 index 0000000..e0c9f9c --- /dev/null +++ b/gateway/src/IntegrationGateway.Adapters.Owl/IntegrationGateway.Adapters.Owl.csproj @@ -0,0 +1,13 @@ + + + + + + + + net8.0 + enable + enable + + + diff --git a/gateway/src/IntegrationGateway.Adapters.Owl/OwlAdapter.cs b/gateway/src/IntegrationGateway.Adapters.Owl/OwlAdapter.cs new file mode 100644 index 0000000..5b5e04a --- /dev/null +++ b/gateway/src/IntegrationGateway.Adapters.Owl/OwlAdapter.cs @@ -0,0 +1,153 @@ +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; + +public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, 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 + }; + } + + // ─── 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.ReadAsStringAsync(); + 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 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 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 }; + } + + // ─── IHasRecordings ─── + 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, 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; + await client.PutAsJsonAsync($"/devices/{sourceDeviceId}", body); + return new MetadataPushResult { Success = true }; + } + + // ─── 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 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 class OwlPlayResponse { public List? Items { get; set; } } +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; } } +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; } } diff --git a/gateway/src/IntegrationGateway.Adapters.Owl/OwlAuthHelper.cs b/gateway/src/IntegrationGateway.Adapters.Owl/OwlAuthHelper.cs new file mode 100644 index 0000000..7b7fc91 --- /dev/null +++ b/gateway/src/IntegrationGateway.Adapters.Owl/OwlAuthHelper.cs @@ -0,0 +1,58 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Net.Http.Json; + +namespace IntegrationGateway.Adapters.Owl; + +public class OwlAuthHelper +{ + private readonly HttpClient _http; + private readonly string _baseUrl; + private readonly string _username; + private readonly string _password; + private string? _token; + private DateTime _tokenExpiry = DateTime.MinValue; + + public OwlAuthHelper(HttpClient http, string baseUrl, string username, string password) + { + _http = http; _baseUrl = baseUrl.TrimEnd('/'); + _username = username; _password = password; + } + + public async Task GetTokenAsync() + { + if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token; + + var keyResp = await _http.GetStringAsync($"{_baseUrl}/login/key"); + var keyData = JsonSerializer.Deserialize(keyResp); + var publicKey = Encoding.UTF8.GetString(Convert.FromBase64String(keyData!.Key!)); + + using var rsa = RSA.Create(); + rsa.ImportFromPem(publicKey); + var plain = JsonSerializer.Serialize(new { username = _username, password = _password }); + var encrypted = rsa.Encrypt(Encoding.UTF8.GetBytes(plain), RSAEncryptionPadding.Pkcs1); + var payload = JsonSerializer.Serialize(new { data = Convert.ToBase64String(encrypted) }); + + var resp = await _http.PostAsync($"{_baseUrl}/login", + new StringContent(payload, Encoding.UTF8, "application/json")); + resp.EnsureSuccessStatusCode(); + var loginResult = await resp.Content.ReadFromJsonAsync(); + _token = loginResult!.Token; + _tokenExpiry = DateTime.UtcNow.AddDays(2.5); + return _token; + } + + public void Invalidate() => _token = null; + + public async Task GetAuthenticatedClientAsync() + { + var token = await GetTokenAsync(); + var client = new HttpClient { BaseAddress = new Uri(_baseUrl) }; + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}"); + return client; + } + + public class LoginKeyResponse { public string? Key { get; set; } } + public class LoginResponse { public string Token { get; set; } = ""; public string? User { get; set; } } +} diff --git a/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj b/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj index e928e5c..e6369c2 100644 --- a/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj +++ b/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj @@ -2,6 +2,7 @@ +