diff --git a/NostrStreamer/Config.cs b/NostrStreamer/Config.cs
index d0b89eb..02227a4 100644
--- a/NostrStreamer/Config.cs
+++ b/NostrStreamer/Config.cs
@@ -2,10 +2,30 @@ namespace NostrStreamer;
public class Config
{
- public Uri SrsPublicHost { get; init; } = null!;
- public string App { get; init; } = null!;
+ ///
+ /// Ingest URL
+ ///
+ public Uri RtmpHost { get; init; } = null!;
+
+ ///
+ /// SRS app name
+ ///
+ public string App { get; init; } = "live";
- public Uri SrsApi { get; init; } = null!;
+ ///
+ /// SRS api server host
+ ///
+ public Uri SrsApiHost { get; init; } = null!;
+
+ ///
+ /// SRS Http server host
+ ///
+ public Uri SrsHttpHost { get; init; } = null!;
+
+ ///
+ /// Public host where playlists are located
+ ///
+ public Uri DataHost { get; init; } = null!;
public string PrivateKey { get; init; } = null!;
public string[] Relays { get; init; } = Array.Empty();
diff --git a/NostrStreamer/Controllers/AccountController.cs b/NostrStreamer/Controllers/NostrController.cs
similarity index 84%
rename from NostrStreamer/Controllers/AccountController.cs
rename to NostrStreamer/Controllers/NostrController.cs
index 64ecbf1..a0e6d95 100644
--- a/NostrStreamer/Controllers/AccountController.cs
+++ b/NostrStreamer/Controllers/NostrController.cs
@@ -8,19 +8,19 @@ using NostrStreamer.Database;
namespace NostrStreamer.Controllers;
[Authorize]
-[Route("/api/account")]
-public class AccountController : Controller
+[Route("/api/nostr")]
+public class NostrController : Controller
{
private readonly StreamerContext _db;
private readonly Config _config;
- public AccountController(StreamerContext db, Config config)
+ public NostrController(StreamerContext db, Config config)
{
_db = db;
_config = config;
}
- [HttpGet]
+ [HttpGet("account")]
public async Task GetAccount()
{
var user = await GetUser();
@@ -41,7 +41,7 @@ public class AccountController : Controller
return Json(new Account
{
- Url = $"rtmp://{_config.SrsPublicHost.Host}/${_config.App}",
+ Url = new Uri(_config.RtmpHost, _config.App).ToString(),
Key = user.StreamKey
});
}
diff --git a/NostrStreamer/Controllers/PlaylistController.cs b/NostrStreamer/Controllers/PlaylistController.cs
new file mode 100644
index 0000000..4bb452d
--- /dev/null
+++ b/NostrStreamer/Controllers/PlaylistController.cs
@@ -0,0 +1,120 @@
+using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Memory;
+using NostrStreamer.Database;
+
+namespace NostrStreamer.Controllers;
+
+[Route("/api/playlist")]
+public class PlaylistController : Controller
+{
+ private readonly ILogger _logger;
+ private readonly IMemoryCache _cache;
+ private readonly Config _config;
+ private readonly IServiceScopeFactory _scopeFactory;
+ private readonly HttpClient _client;
+
+ public PlaylistController(Config config, IMemoryCache cache, ILogger logger, IServiceScopeFactory scopeFactory,
+ HttpClient client)
+ {
+ _config = config;
+ _cache = cache;
+ _logger = logger;
+ _scopeFactory = scopeFactory;
+ _client = client;
+ }
+
+ [HttpGet("{pubkey}.m3u8")]
+ public async Task RewritePlaylist([FromRoute] string pubkey)
+ {
+ var key = await GetStreamKey(pubkey);
+ if (string.IsNullOrEmpty(key))
+ {
+ Response.StatusCode = 404;
+ return;
+ }
+
+ var path = $"/{_config.App}/{key}.m3u8";
+ var ub = new UriBuilder(_config.SrsHttpHost)
+ {
+ Path = path,
+ Query = string.Join("&", Request.Query.Select(a => $"{a.Key}={a.Value}"))
+ };
+
+ Response.ContentType = "application/x-mpegurl";
+ await using var sw = new StreamWriter(Response.Body);
+
+ using var rsp = await _client.GetAsync(ub.Uri);
+ if (!rsp.IsSuccessStatusCode)
+ {
+ Response.StatusCode = (int)rsp.StatusCode;
+ return;
+ }
+
+ await Response.StartAsync();
+ using var sr = new StreamReader(await rsp.Content.ReadAsStreamAsync());
+ while (await sr.ReadLineAsync() is { } line)
+ {
+ if (line.StartsWith("#EXTINF"))
+ {
+ await sw.WriteLineAsync(line);
+ var trackPath = await sr.ReadLineAsync();
+ var seg = Regex.Match(trackPath!, @"-(\d+)\.ts$");
+ await sw.WriteLineAsync($"{pubkey}/{seg.Groups[1].Value}.ts");
+ }
+ else if (line.StartsWith("#EXT-X-STREAM-INF"))
+ {
+ await sw.WriteLineAsync(line);
+ var trackPath = await sr.ReadLineAsync();
+ var trackUri = new Uri(_config.SrsHttpHost, trackPath!);
+ await sw.WriteLineAsync($"{pubkey}.m3u8{trackUri.Query}");
+ }
+ else
+ {
+ await sw.WriteLineAsync(line);
+ }
+ }
+
+ Response.Body.Close();
+ }
+
+ [HttpGet("{pubkey}/{segment}")]
+ public async Task GetSegment([FromRoute] string pubkey, [FromRoute] string segment)
+ {
+ var key = await GetStreamKey(pubkey);
+ if (string.IsNullOrEmpty(key))
+ {
+ Response.StatusCode = 404;
+ return;
+ }
+
+ var path = $"/{_config.App}/{key}-{segment}";
+ await ProxyRequest(path);
+ }
+
+ private async Task ProxyRequest(string path)
+ {
+ using var rsp = await _client.GetAsync(new Uri(_config.SrsHttpHost, path));
+ Response.Headers.ContentType = rsp.Content.Headers.ContentType?.ToString();
+
+ await rsp.Content.CopyToAsync(Response.Body);
+ }
+
+ private async Task GetStreamKey(string pubkey)
+ {
+ var cacheKey = $"stream-key:{pubkey}";
+ var cached = _cache.Get(cacheKey);
+ if (cached != default)
+ {
+ return cached;
+ }
+
+ using var scope = _scopeFactory.CreateScope();
+ await using var db = scope.ServiceProvider.GetRequiredService();
+ var user = await db.Users.SingleOrDefaultAsync(a => a.PubKey == pubkey);
+
+ _cache.Set(cacheKey, user?.StreamKey);
+ return user?.StreamKey;
+ }
+}
diff --git a/NostrStreamer/Program.cs b/NostrStreamer/Program.cs
index 374dd18..44e1311 100644
--- a/NostrStreamer/Program.cs
+++ b/NostrStreamer/Program.cs
@@ -15,6 +15,9 @@ internal static class Program
var config = builder.Configuration.GetSection("Config").Get();
ConfigureDb(services, builder.Configuration);
+ services.AddCors();
+ services.AddMemoryCache();
+ services.AddHttpClient();
services.AddControllers();
services.AddSingleton(config);
@@ -34,7 +37,8 @@ internal static class Program
var db = scope.ServiceProvider.GetRequiredService();
await db.Database.MigrateAsync();
}
-
+
+ app.UseCors(o => o.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
app.MapControllers();
await app.RunAsync();
diff --git a/NostrStreamer/Services/StreamManager.cs b/NostrStreamer/Services/StreamManager.cs
index 4853046..093693c 100644
--- a/NostrStreamer/Services/StreamManager.cs
+++ b/NostrStreamer/Services/StreamManager.cs
@@ -97,12 +97,8 @@ public class StreamManager
private string GetStreamUrl(User u)
{
- var ub = new UriBuilder(_config.SrsPublicHost)
- {
- Path = $"/{_config.App}/${u.StreamKey}.m3u8"
- };
-
- return ub.Uri.ToString();
+ var ub = new Uri(_config.DataHost, $"{u.PubKey}.m3u8");
+ return ub.ToString();
}
private async Task GetUserFromStreamKey(string streamKey)
diff --git a/NostrStreamer/appsettings.json b/NostrStreamer/appsettings.json
index f633289..d8c7420 100644
--- a/NostrStreamer/appsettings.json
+++ b/NostrStreamer/appsettings.json
@@ -11,8 +11,10 @@
"Database": "User ID=postgres;Password=postgres;Database=streaming;Pooling=true;Host=127.0.0.1:5431"
},
"Config": {
- "SrsPublicHost": "http://localhost:8080",
- "SrsApi": "http://localhost:1985",
+ "RtmpHost": "rtmp://localhost:1935",
+ "SrsHttpHost": "http://localhost:8080",
+ "SrsApiHost": "http://localhost:1985",
+ "DataHost": "http://localhost:5295/api/playlist/",
"App": "test",
"Relays": ["ws://localhost:8081"],
"PrivateKey": "nsec1yqtv8s8y9krh6l8pwp09lk2jkulr9e0klu95tlk7dgus9cklr4ssdv3d88"
diff --git a/docker/srs.conf b/docker/srs.conf
index 1343e7a..713748e 100644
--- a/docker/srs.conf
+++ b/docker/srs.conf
@@ -14,6 +14,8 @@ vhost __defaultVhost__ {
hls {
enabled on;
hls_dispose 30;
+ hls_fragment 5;
+ hls_window 15;
}
http_hooks {
enabled on;