diff --git a/gateway/IntegrationGateway.sln b/gateway/IntegrationGateway.sln index e575fba..6d51811 100644 --- a/gateway/IntegrationGateway.sln +++ b/gateway/IntegrationGateway.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationGateway.Host", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationGateway.Core", "src\IntegrationGateway.Core\IntegrationGateway.Core.csproj", "{2055B7A5-418F-456C-8642-A99A68282561}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationGateway.Adapters.Owl", "src\IntegrationGateway.Adapters.Owl\IntegrationGateway.Adapters.Owl.csproj", "{DA8C823D-D3D2-4D73-973E-05E4ED33B50C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +45,18 @@ Global {2055B7A5-418F-456C-8642-A99A68282561}.Release|x64.Build.0 = Release|Any CPU {2055B7A5-418F-456C-8642-A99A68282561}.Release|x86.ActiveCfg = Release|Any CPU {2055B7A5-418F-456C-8642-A99A68282561}.Release|x86.Build.0 = Release|Any CPU + {DA8C823D-D3D2-4D73-973E-05E4ED33B50C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA8C823D-D3D2-4D73-973E-05E4ED33B50C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA8C823D-D3D2-4D73-973E-05E4ED33B50C}.Debug|x64.ActiveCfg = Debug|Any CPU + {DA8C823D-D3D2-4D73-973E-05E4ED33B50C}.Debug|x64.Build.0 = Debug|Any CPU + {DA8C823D-D3D2-4D73-973E-05E4ED33B50C}.Debug|x86.ActiveCfg = Debug|Any CPU + {DA8C823D-D3D2-4D73-973E-05E4ED33B50C}.Debug|x86.Build.0 = Debug|Any CPU + {DA8C823D-D3D2-4D73-973E-05E4ED33B50C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA8C823D-D3D2-4D73-973E-05E4ED33B50C}.Release|Any CPU.Build.0 = Release|Any CPU + {DA8C823D-D3D2-4D73-973E-05E4ED33B50C}.Release|x64.ActiveCfg = Release|Any CPU + {DA8C823D-D3D2-4D73-973E-05E4ED33B50C}.Release|x64.Build.0 = Release|Any CPU + {DA8C823D-D3D2-4D73-973E-05E4ED33B50C}.Release|x86.ActiveCfg = Release|Any CPU + {DA8C823D-D3D2-4D73-973E-05E4ED33B50C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -50,5 +64,6 @@ Global GlobalSection(NestedProjects) = preSolution {387BD4FC-725B-4948-B413-F50BC6BD605D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {2055B7A5-418F-456C-8642-A99A68282561} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {DA8C823D-D3D2-4D73-973E-05E4ED33B50C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} EndGlobalSection EndGlobal 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..fbdb0af --- /dev/null +++ b/gateway/src/IntegrationGateway.Adapters.Owl/OwlAdapter.cs @@ -0,0 +1,202 @@ +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, 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; } + } + + 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++; + } + 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(); + if (!string.IsNullOrEmpty(parentDeviceId)) + { + var resp = await client.GetStringAsync($"/devices/{parentDeviceId}/channels"); + var channels = JsonSerializer.Deserialize>(resp)!; + return new PagedResult { Items = channels.Items.Select(MapChannel).ToList(), Total = channels.Total }; + } + var json = await client.GetStringAsync($"/channels?page={page}&size={size}"); + var owl = JsonSerializer.Deserialize>(json)!; + return new PagedResult { Items = owl.Items.Select(MapChannel).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(MapChannel).ToList(); + } + + 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 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 + }; + } + + 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 }; + } + + 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 }; + } +} + +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 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 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; } } diff --git a/gateway/src/IntegrationGateway.Adapters.Owl/OwlAuthHelper.cs b/gateway/src/IntegrationGateway.Adapters.Owl/OwlAuthHelper.cs new file mode 100644 index 0000000..b759fcf --- /dev/null +++ b/gateway/src/IntegrationGateway.Adapters.Owl/OwlAuthHelper.cs @@ -0,0 +1,57 @@ +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 441a01e..283b1a9 100644 --- a/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj +++ b/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj @@ -13,6 +13,7 @@ + diff --git a/gateway/src/IntegrationGateway.Host/Program.cs b/gateway/src/IntegrationGateway.Host/Program.cs index 35560f0..6f72503 100644 --- a/gateway/src/IntegrationGateway.Host/Program.cs +++ b/gateway/src/IntegrationGateway.Host/Program.cs @@ -1,4 +1,5 @@ using IntegrationGateway.Core.Infrastructure; +using IntegrationGateway.Adapters.Owl; using IntegrationGateway.Host; var builder = WebApplication.CreateBuilder(args); @@ -23,6 +24,11 @@ app.Lifetime.ApplicationStarted.Register(() => { var gw = app.Services.GetRequiredService(); var registry = app.Services.GetRequiredService(); +var owlCfg = builder.Configuration.GetSection("Owl"); +var owlHttp = app.Services.GetRequiredService().CreateClient("VolPro"); +var owlAdapter = new OwlAdapter("Owl:main", owlHttp, owlCfg["BaseUrl"] ?? "http://localhost:15123", owlCfg["Username"] ?? "admin", owlCfg["Password"] ?? "admin"); +registry.Register(owlAdapter); + try { var result = await gw.RegisterAsync(); diff --git a/gateway/src/IntegrationGateway.Host/appsettings.json b/gateway/src/IntegrationGateway.Host/appsettings.json index 376406d..ca1e83a 100644 --- a/gateway/src/IntegrationGateway.Host/appsettings.json +++ b/gateway/src/IntegrationGateway.Host/appsettings.json @@ -9,5 +9,10 @@ "Urls": "http://*:5100", "VolProBaseUrl": "http://localhost:9100", "NodeCode": "gw-31ku", - "NodeToken": "xxxxxxxxxx" + "NodeToken": "xxxxxxxxxx", + "Owl": { + "BaseUrl": "http://owl_host:15123", + "Username": "admin", + "Password": "your_owl_password" + } } \ No newline at end of file