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