Day3 OwlAdapter

This commit is contained in:
2026-05-17 01:24:31 +08:00
parent 0f0d0c6a9b
commit ead127acab
7 changed files with 300 additions and 1 deletions

View File

@@ -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

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\IntegrationGateway.Core\IntegrationGateway.Core.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -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<bool> HealthCheckAsync()
{
try { var client = await _auth.GetAuthenticatedClientAsync(); var resp = await client.GetAsync("/health"); return resp.IsSuccessStatusCode; }
catch { return false; }
}
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 };
}
public async Task<StandardDevice?> GetDeviceAsync(string sourceDeviceId)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var json = await client.GetStringAsync($"/devices/{sourceDeviceId}");
var owl = JsonSerializer.Deserialize<OwlDevice>(json);
return owl is null ? null : MapDevice(owl);
}
public async Task<List<StandardDevice>> GetAllDevicesAsync()
{
var result = new List<StandardDevice>();
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<PagedResult<StandardDevice>> 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<OwlPagedResult<OwlChannel>>(resp)!;
return new PagedResult<StandardDevice> { Items = channels.Items.Select(MapChannel).ToList(), Total = channels.Total };
}
var json = await client.GetStringAsync($"/channels?page={page}&size={size}");
var owl = JsonSerializer.Deserialize<OwlPagedResult<OwlChannel>>(json)!;
return new PagedResult<StandardDevice> { Items = owl.Items.Select(MapChannel).ToList(), Total = owl.Total };
}
public async Task<List<StandardDevice>> GetAllChannelsAsync()
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var json = await client.GetStringAsync("/channels?size=1000");
var owl = JsonSerializer.Deserialize<OwlPagedResult<OwlChannel>>(json)!;
return owl.Items.Select(MapChannel).ToList();
}
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);
}
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}" };
}
public async Task StopPlayAsync(string channelId)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
await client.PostAsync($"/channels/{channelId}/stop", null);
}
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 };
}
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<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.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<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 };
}
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 }
};
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<string, object?> { ["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<T> { public List<T> 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<OwlPlayItem>? 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; } }

View File

@@ -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<string> GetTokenAsync()
{
if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token;
var keyResp = await _http.GetStringAsync($"{_baseUrl}/login/key");
var keyData = JsonSerializer.Deserialize<LoginKeyResponse>(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<LoginResponse>();
_token = loginResult!.Token;
_tokenExpiry = DateTime.UtcNow.AddDays(2.5);
return _token;
}
public void Invalidate() => _token = null;
public async Task<HttpClient> 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; } }
}

View File

@@ -13,6 +13,7 @@
<ItemGroup>
<ProjectReference Include="..\IntegrationGateway.Core\IntegrationGateway.Core.csproj" />
<ProjectReference Include="..\IntegrationGateway.Adapters.Owl\IntegrationGateway.Adapters.Owl.csproj" />
</ItemGroup>
</Project>

View File

@@ -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<GatewayClient>();
var registry = app.Services.GetRequiredService<AdapterRegistry>();
var owlCfg = builder.Configuration.GetSection("Owl");
var owlHttp = app.Services.GetRequiredService<IHttpClientFactory>().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();

View File

@@ -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"
}
}