Proxy playlist/segments
This commit is contained in:
@ -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>();
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
120
NostrStreamer/Controllers/PlaylistController.cs
Normal file
120
NostrStreamer/Controllers/PlaylistController.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
@ -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;
|
||||||
|
Reference in New Issue
Block a user