diff --git a/NostrStreamer/Config.cs b/NostrStreamer/Config.cs index d651813..a70df00 100644 --- a/NostrStreamer/Config.cs +++ b/NostrStreamer/Config.cs @@ -40,6 +40,10 @@ public class Config public S3BlobConfig S3Store { get; init; } = null!; public DateTime TosDate { get; init; } + + public string GeoIpDatabase { get; init; } = null!; + + public List Edges { get; init; } = new(); } public class LndConfig @@ -62,3 +66,11 @@ public sealed class S3BlobConfig public bool DisablePayloadSigning { get; init; } public Uri PublicHost { get; init; } = null!; } + +public sealed class EdgeLocation +{ + public string Name { get; init; } = null!; + public Uri Url { get; init; } = null!; + public double Latitude { get; init; } + public double Longitude { get; init; } +} \ No newline at end of file diff --git a/NostrStreamer/Controllers/PlaylistController.cs b/NostrStreamer/Controllers/PlaylistController.cs index 7e2d64e..1d52ab0 100644 --- a/NostrStreamer/Controllers/PlaylistController.cs +++ b/NostrStreamer/Controllers/PlaylistController.cs @@ -16,9 +16,10 @@ public class PlaylistController : Controller private readonly SrsApi _srsApi; private readonly ViewCounter _viewCounter; private readonly StreamManagerFactory _streamManagerFactory; + private readonly EdgeSteering _edgeSteering; public PlaylistController(Config config, ILogger logger, - HttpClient client, SrsApi srsApi, ViewCounter viewCounter, StreamManagerFactory streamManagerFactory) + HttpClient client, SrsApi srsApi, ViewCounter viewCounter, StreamManagerFactory streamManagerFactory, EdgeSteering edgeSteering) { _config = config; _logger = logger; @@ -26,6 +27,7 @@ public class PlaylistController : Controller _srsApi = srsApi; _viewCounter = viewCounter; _streamManagerFactory = streamManagerFactory; + _edgeSteering = edgeSteering; } [ResponseCache(Duration = 1, Location = ResponseCacheLocation.Any)] @@ -106,6 +108,7 @@ public class PlaylistController : Controller { try { + var edge = _edgeSteering.GetEdge(HttpContext); var streamManager = await _streamManagerFactory.ForStream(id); var userStream = streamManager.GetStream(); @@ -138,8 +141,17 @@ public class PlaylistController : Controller await sw.WriteLineAsync( $"#EXT-X-STREAM-INF:{string.Join(",", allArgs)}"); - var u = $"../{variant.SourceName}/{userStream.Id}.m3u8{(!string.IsNullOrEmpty(hlsCtx) ? $"?hls_ctx={hlsCtx}" : "")}"; - await sw.WriteLineAsync(u); + var path = $"{variant.SourceName}/{userStream.Id}.m3u8{(!string.IsNullOrEmpty(hlsCtx) ? $"?hls_ctx={hlsCtx}" : "")}"; + if (edge != default) + { + var u = new Uri(edge.Url, path); + await sw.WriteLineAsync(u.ToString()); + } + else + { + var u = $"../{path}"; + await sw.WriteLineAsync(u); + } } } catch (Exception ex) diff --git a/NostrStreamer/Extensions.cs b/NostrStreamer/Extensions.cs index 1b66d4e..843c6f0 100644 --- a/NostrStreamer/Extensions.cs +++ b/NostrStreamer/Extensions.cs @@ -1,6 +1,7 @@ using Amazon; using Amazon.Runtime; using Amazon.S3; +using MaxMind.GeoIP2; using Newtonsoft.Json; using Nostr.Client.Json; using Nostr.Client.Keys; @@ -50,6 +51,46 @@ public static class Extensions return !string.IsNullOrEmpty(user.Tags) ? user.Tags.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) : Array.Empty(); } + + public static (double lat, double lon)? GetLocation(this HttpContext ctx, IGeoIP2DatabaseReader db) + { + var ip = ctx.GetRealIp(); + var loc = db.TryCity(ip, out var city) ? city?.Location : default; + if ((loc?.Latitude.HasValue ?? false) && loc.Longitude.HasValue) + { + return (loc.Latitude.Value, loc.Longitude.Value); + } + + return default; + } + + public static string GetRealIp(this HttpContext ctx) + { + var cci = ctx.Request.Headers.TryGetValue("CF-Connecting-IP", out var xx) ? xx.ToString() : null; + if (!string.IsNullOrEmpty(cci)) + { + return cci; + } + + var xff = ctx.Request.Headers.TryGetValue("X-Forwarded-For", out var x) ? x.ToString() : null; + if (!string.IsNullOrEmpty(xff)) + { + return xff.Split(",", StringSplitOptions.RemoveEmptyEntries).First(); + } + + return ctx.Connection.RemoteIpAddress!.ToString(); + } + + public static double GetDistance(double longitude, double latitude, double otherLongitude, double otherLatitude) + { + var d1 = latitude * (Math.PI / 180.0); + var num1 = longitude * (Math.PI / 180.0); + var d2 = otherLatitude * (Math.PI / 180.0); + var num2 = otherLongitude * (Math.PI / 180.0) - num1; + var d3 = Math.Pow(Math.Sin((d2 - d1) / 2.0), 2.0) + Math.Cos(d1) * Math.Cos(d2) * Math.Pow(Math.Sin(num2 / 2.0), 2.0); + + return 6376500.0 * (2.0 * Math.Atan2(Math.Sqrt(d3), Math.Sqrt(1.0 - d3))); + } } public class Variant diff --git a/NostrStreamer/NostrStreamer.csproj b/NostrStreamer/NostrStreamer.csproj index 25eb93e..ffbd207 100644 --- a/NostrStreamer/NostrStreamer.csproj +++ b/NostrStreamer/NostrStreamer.csproj @@ -38,6 +38,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/NostrStreamer/Program.cs b/NostrStreamer/Program.cs index f43ac33..6c04707 100644 --- a/NostrStreamer/Program.cs +++ b/NostrStreamer/Program.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using MaxMind.GeoIP2; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Infrastructure; using Microsoft.EntityFrameworkCore; @@ -29,6 +30,10 @@ internal static class Program services.AddControllers().AddNewtonsoftJson(); services.AddSingleton(config); + // GeoIP + services.AddSingleton(_ => new DatabaseReader(config.GeoIpDatabase)); + services.AddTransient(); + // nostr auth services.AddTransient(); services.AddAuthentication(o => diff --git a/NostrStreamer/Services/EdgeSteering.cs b/NostrStreamer/Services/EdgeSteering.cs new file mode 100644 index 0000000..e53b653 --- /dev/null +++ b/NostrStreamer/Services/EdgeSteering.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using MaxMind.GeoIP2; + +namespace NostrStreamer.Services; + +public class EdgeSteering +{ + private readonly Config _config; + private readonly IGeoIP2DatabaseReader _db; + private readonly ILogger _logger; + + public EdgeSteering(Config config, IGeoIP2DatabaseReader db, ILogger logger) + { + _config = config; + _db = db; + _logger = logger; + } + + public EdgeLocation? GetEdge(HttpContext ctx) + { + var sw = Stopwatch.StartNew(); + var loc = ctx.GetLocation(_db); + if (loc != default) + { + var ret = _config.Edges.MinBy(a => Extensions.GetDistance(a.Longitude, a.Latitude, loc.Value.lon, loc.Value.lat)); + sw.Stop(); + _logger.LogTrace("Found edge in {n:#,##0.#}ms", sw.Elapsed.TotalMilliseconds); + return ret; + } + + sw.Stop(); + _logger.LogTrace("Found no edge in {n:#,##0.#}ms", sw.Elapsed.TotalMilliseconds); + return default; + } +} diff --git a/NostrStreamer/appsettings.json b/NostrStreamer/appsettings.json index 7d6e822..5f22fe2 100644 --- a/NostrStreamer/appsettings.json +++ b/NostrStreamer/appsettings.json @@ -32,6 +32,21 @@ "AccessKey": "TQcxug1ZAXfnZ5bvc9n5", "SecretKey": "p7EK4qew6DBkBPqrpRPuJgTOc6ChUlfIcEdAwE7K", "PublicHost": "http://localhost:9010" - } + }, + "GeoIpDatabase": "/Users/kieran/Downloads/GeoLite2-City_20230801/GeoLite2-City.mmdb", + "Edges": [ + { + "Name": "US0", + "Url": "https://us0.edge.zap.stream/", + "Longitude": -73.8024, + "Latitude": 45.4616 + }, + { + "Name": "Origin", + "Url": "https://data.zap.stream/", + "Longitude": 2.12664, + "Latitude": 50.98515 + } + ] } }