Proxy playlist/segments

This commit is contained in:
2023-06-30 20:31:55 +01:00
parent 6c41cfaeb1
commit 1e53a78d77
7 changed files with 161 additions and 17 deletions

View File

@ -2,10 +2,30 @@ namespace NostrStreamer;
public class Config public class Config
{ {
public Uri SrsPublicHost { get; init; } = null!; /// <summary>
public string App { get; init; } = null!; /// Ingest URL
/// </summary>
public Uri RtmpHost { get; init; } = null!;
/// <summary>
/// SRS app name
/// </summary>
public string App { get; init; } = "live";
public Uri SrsApi { get; init; } = null!; /// <summary>
/// SRS api server host
/// </summary>
public Uri SrsApiHost { get; init; } = null!;
/// <summary>
/// SRS Http server host
/// </summary>
public Uri SrsHttpHost { get; init; } = null!;
/// <summary>
/// Public host where playlists are located
/// </summary>
public Uri DataHost { get; init; } = null!;
public string PrivateKey { get; init; } = null!; public string PrivateKey { get; init; } = null!;
public string[] Relays { get; init; } = Array.Empty<string>(); public string[] Relays { get; init; } = Array.Empty<string>();

View File

@ -8,19 +8,19 @@ using NostrStreamer.Database;
namespace NostrStreamer.Controllers; namespace NostrStreamer.Controllers;
[Authorize] [Authorize]
[Route("/api/account")] [Route("/api/nostr")]
public class AccountController : Controller public class NostrController : Controller
{ {
private readonly StreamerContext _db; private readonly StreamerContext _db;
private readonly Config _config; private readonly Config _config;
public AccountController(StreamerContext db, Config config) public NostrController(StreamerContext db, Config config)
{ {
_db = db; _db = db;
_config = config; _config = config;
} }
[HttpGet] [HttpGet("account")]
public async Task<ActionResult> GetAccount() public async Task<ActionResult> GetAccount()
{ {
var user = await GetUser(); var user = await GetUser();
@ -41,7 +41,7 @@ public class AccountController : Controller
return Json(new Account return Json(new Account
{ {
Url = $"rtmp://{_config.SrsPublicHost.Host}/${_config.App}", Url = new Uri(_config.RtmpHost, _config.App).ToString(),
Key = user.StreamKey Key = user.StreamKey
}); });
} }

View File

@ -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<PlaylistController> _logger;
private readonly IMemoryCache _cache;
private readonly Config _config;
private readonly IServiceScopeFactory _scopeFactory;
private readonly HttpClient _client;
public PlaylistController(Config config, IMemoryCache cache, ILogger<PlaylistController> 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<string?> GetStreamKey(string pubkey)
{
var cacheKey = $"stream-key:{pubkey}";
var cached = _cache.Get<string>(cacheKey);
if (cached != default)
{
return cached;
}
using var scope = _scopeFactory.CreateScope();
await using var db = scope.ServiceProvider.GetRequiredService<StreamerContext>();
var user = await db.Users.SingleOrDefaultAsync(a => a.PubKey == pubkey);
_cache.Set(cacheKey, user?.StreamKey);
return user?.StreamKey;
}
}

View File

@ -15,6 +15,9 @@ internal static class Program
var config = builder.Configuration.GetSection("Config").Get<Config>(); var config = builder.Configuration.GetSection("Config").Get<Config>();
ConfigureDb(services, builder.Configuration); ConfigureDb(services, builder.Configuration);
services.AddCors();
services.AddMemoryCache();
services.AddHttpClient();
services.AddControllers(); services.AddControllers();
services.AddSingleton(config); services.AddSingleton(config);
@ -34,7 +37,8 @@ internal static class Program
var db = scope.ServiceProvider.GetRequiredService<StreamerContext>(); var db = scope.ServiceProvider.GetRequiredService<StreamerContext>();
await db.Database.MigrateAsync(); await db.Database.MigrateAsync();
} }
app.UseCors(o => o.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
app.MapControllers(); app.MapControllers();
await app.RunAsync(); await app.RunAsync();

View File

@ -97,12 +97,8 @@ public class StreamManager
private string GetStreamUrl(User u) private string GetStreamUrl(User u)
{ {
var ub = new UriBuilder(_config.SrsPublicHost) var ub = new Uri(_config.DataHost, $"{u.PubKey}.m3u8");
{ return ub.ToString();
Path = $"/{_config.App}/${u.StreamKey}.m3u8"
};
return ub.Uri.ToString();
} }
private async Task<User?> GetUserFromStreamKey(string streamKey) private async Task<User?> GetUserFromStreamKey(string streamKey)

View File

@ -11,8 +11,10 @@
"Database": "User ID=postgres;Password=postgres;Database=streaming;Pooling=true;Host=127.0.0.1:5431" "Database": "User ID=postgres;Password=postgres;Database=streaming;Pooling=true;Host=127.0.0.1:5431"
}, },
"Config": { "Config": {
"SrsPublicHost": "http://localhost:8080", "RtmpHost": "rtmp://localhost:1935",
"SrsApi": "http://localhost:1985", "SrsHttpHost": "http://localhost:8080",
"SrsApiHost": "http://localhost:1985",
"DataHost": "http://localhost:5295/api/playlist/",
"App": "test", "App": "test",
"Relays": ["ws://localhost:8081"], "Relays": ["ws://localhost:8081"],
"PrivateKey": "nsec1yqtv8s8y9krh6l8pwp09lk2jkulr9e0klu95tlk7dgus9cklr4ssdv3d88" "PrivateKey": "nsec1yqtv8s8y9krh6l8pwp09lk2jkulr9e0klu95tlk7dgus9cklr4ssdv3d88"

View File

@ -14,6 +14,8 @@ vhost __defaultVhost__ {
hls { hls {
enabled on; enabled on;
hls_dispose 30; hls_dispose 30;
hls_fragment 5;
hls_window 15;
} }
http_hooks { http_hooks {
enabled on; enabled on;