Create variants
This commit is contained in:
@ -41,6 +41,11 @@ public class Config
|
|||||||
/// Cost/min
|
/// Cost/min
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Cost { get; init; } = 10;
|
public int Cost { get; init; } = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of video variants
|
||||||
|
/// </summary>
|
||||||
|
public List<Variant> Variants { get; init; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LndConfig
|
public class LndConfig
|
||||||
@ -51,3 +56,11 @@ public class LndConfig
|
|||||||
|
|
||||||
public string MacaroonPath { get; init; } = null!;
|
public string MacaroonPath { get; init; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class Variant
|
||||||
|
{
|
||||||
|
public string Name { get; init; } = null!;
|
||||||
|
public int Width { get; init; }
|
||||||
|
public int Height { get; init; }
|
||||||
|
public int Bandwidth { get; init; }
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using NostrStreamer.Database;
|
using NostrStreamer.Database;
|
||||||
|
using NostrStreamer.Services;
|
||||||
|
|
||||||
namespace NostrStreamer.Controllers;
|
namespace NostrStreamer.Controllers;
|
||||||
|
|
||||||
@ -14,19 +15,21 @@ public class PlaylistController : Controller
|
|||||||
private readonly Config _config;
|
private readonly Config _config;
|
||||||
private readonly IServiceScopeFactory _scopeFactory;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
private readonly HttpClient _client;
|
private readonly HttpClient _client;
|
||||||
|
private readonly SrsApi _srsApi;
|
||||||
|
|
||||||
public PlaylistController(Config config, IMemoryCache cache, ILogger<PlaylistController> logger, IServiceScopeFactory scopeFactory,
|
public PlaylistController(Config config, IMemoryCache cache, ILogger<PlaylistController> logger, IServiceScopeFactory scopeFactory,
|
||||||
HttpClient client)
|
HttpClient client, SrsApi srsApi)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_scopeFactory = scopeFactory;
|
_scopeFactory = scopeFactory;
|
||||||
_client = client;
|
_client = client;
|
||||||
|
_srsApi = srsApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{pubkey}.m3u8")]
|
[HttpGet("{variant}/{pubkey}.m3u8")]
|
||||||
public async Task RewritePlaylist([FromRoute] string pubkey)
|
public async Task RewritePlaylist([FromRoute] string pubkey, [FromRoute] string variant)
|
||||||
{
|
{
|
||||||
var key = await GetStreamKey(pubkey);
|
var key = await GetStreamKey(pubkey);
|
||||||
if (string.IsNullOrEmpty(key))
|
if (string.IsNullOrEmpty(key))
|
||||||
@ -35,7 +38,7 @@ public class PlaylistController : Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = $"/{_config.App}/{key}.m3u8";
|
var path = $"/{_config.App}/{variant}/{key}.m3u8";
|
||||||
var ub = new UriBuilder(_config.SrsHttpHost)
|
var ub = new UriBuilder(_config.SrsHttpHost)
|
||||||
{
|
{
|
||||||
Path = path,
|
Path = path,
|
||||||
@ -63,13 +66,6 @@ public class PlaylistController : Controller
|
|||||||
var seg = Regex.Match(trackPath!, @"-(\d+)\.ts$");
|
var seg = Regex.Match(trackPath!, @"-(\d+)\.ts$");
|
||||||
await sw.WriteLineAsync($"{pubkey}/{seg.Groups[1].Value}.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
|
else
|
||||||
{
|
{
|
||||||
await sw.WriteLineAsync(line);
|
await sw.WriteLineAsync(line);
|
||||||
@ -79,8 +75,8 @@ public class PlaylistController : Controller
|
|||||||
Response.Body.Close();
|
Response.Body.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{pubkey}/{segment}")]
|
[HttpGet("{pubkey}.m3u8")]
|
||||||
public async Task GetSegment([FromRoute] string pubkey, [FromRoute] string segment)
|
public async Task CreateMultiBitrate([FromRoute] string pubkey)
|
||||||
{
|
{
|
||||||
var key = await GetStreamKey(pubkey);
|
var key = await GetStreamKey(pubkey);
|
||||||
if (string.IsNullOrEmpty(key))
|
if (string.IsNullOrEmpty(key))
|
||||||
@ -89,10 +85,76 @@ public class PlaylistController : Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = $"/{_config.App}/{key}-{segment}";
|
Response.ContentType = "application/x-mpegurl";
|
||||||
|
await using var sw = new StreamWriter(Response.Body);
|
||||||
|
|
||||||
|
|
||||||
|
var streams = await _srsApi.ListStreams();
|
||||||
|
await sw.WriteLineAsync("#EXTM3U");
|
||||||
|
|
||||||
|
var hlsCtx = await GetHlsCtx(key);
|
||||||
|
foreach (var variant in _config.Variants.OrderBy(a => a.Bandwidth))
|
||||||
|
{
|
||||||
|
var stream = streams.FirstOrDefault(a =>
|
||||||
|
a.Name == key && a.App == $"{_config.App}/{variant.Name}");
|
||||||
|
|
||||||
|
var resArg = stream?.Video != default ? $"RESOLUTION={stream.Video?.Width}x{stream.Video?.Height}" :
|
||||||
|
$"RESOLUTION={variant.Width}x{variant.Height}";
|
||||||
|
|
||||||
|
var bandwidthArg = $"BANDWIDTH={variant.Bandwidth * 1000}";
|
||||||
|
|
||||||
|
var averageBandwidthArg = stream?.Kbps?.Recv30s.HasValue ?? false ? $"AVERAGE-BANDWIDTH={stream.Kbps.Recv30s * 1000}" : "";
|
||||||
|
var allArgs = new[] {bandwidthArg, averageBandwidthArg, resArg}.Where(a => !string.IsNullOrEmpty(a));
|
||||||
|
await sw.WriteLineAsync(
|
||||||
|
$"#EXT-X-STREAM-INF:{string.Join(",", allArgs)},CODECS=\"avc1.640028,mp4a.40.2\"");
|
||||||
|
|
||||||
|
var u = new Uri(_config.DataHost, $"{variant.Name}/{pubkey}.m3u8{(!string.IsNullOrEmpty(hlsCtx) ? $"?hls_ctx={hlsCtx}" : "")}");
|
||||||
|
await sw.WriteLineAsync(u.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{variant}/{pubkey}/{segment}")]
|
||||||
|
public async Task GetSegment([FromRoute] string pubkey, [FromRoute] string segment, [FromRoute] string variant)
|
||||||
|
{
|
||||||
|
var key = await GetStreamKey(pubkey);
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
Response.StatusCode = 404;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = $"/{_config.App}/{variant}/{key}-{segment}";
|
||||||
await ProxyRequest(path);
|
await ProxyRequest(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<string?> GetHlsCtx(string key)
|
||||||
|
{
|
||||||
|
var path = $"/{_config.App}/{key}.m3u8";
|
||||||
|
var ub = new Uri(_config.SrsHttpHost, path);
|
||||||
|
using var rsp = await _client.GetAsync(ub);
|
||||||
|
if (!rsp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var sr = new StreamReader(await rsp.Content.ReadAsStreamAsync());
|
||||||
|
while (await sr.ReadLineAsync() is { } line)
|
||||||
|
{
|
||||||
|
if (line.StartsWith("#EXT-X-STREAM-INF"))
|
||||||
|
{
|
||||||
|
var trackLine = await sr.ReadLineAsync();
|
||||||
|
var rx = new Regex("\\?hls_ctx=(\\w+)$");
|
||||||
|
var match = rx.Match(trackLine!);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
return match.Groups[1].Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ProxyRequest(string path)
|
private async Task ProxyRequest(string path)
|
||||||
{
|
{
|
||||||
using var rsp = await _client.GetAsync(new Uri(_config.SrsHttpHost, path));
|
using var rsp = await _client.GetAsync(new Uri(_config.SrsHttpHost, path));
|
||||||
|
@ -25,7 +25,7 @@ public class SrsController : Controller
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(req.Stream) || string.IsNullOrEmpty(req.App) || string.IsNullOrEmpty(req.Stream) ||
|
if (string.IsNullOrEmpty(req.Stream) || string.IsNullOrEmpty(req.App) || string.IsNullOrEmpty(req.Stream) ||
|
||||||
!req.App.Equals(_config.App, StringComparison.InvariantCultureIgnoreCase))
|
!req.App.StartsWith(_config.App, StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
@ -45,7 +45,7 @@ public class SrsController : Controller
|
|||||||
}
|
}
|
||||||
if (req.Action == "on_hls" && req.Duration.HasValue && !string.IsNullOrEmpty(req.ClientId))
|
if (req.Action == "on_hls" && req.Duration.HasValue && !string.IsNullOrEmpty(req.ClientId))
|
||||||
{
|
{
|
||||||
await _streamManager.ConsumeQuota(req.Stream, req.Duration.Value, req.ClientId);
|
await _streamManager.ConsumeQuota(req.Stream, req.Duration.Value, req.ClientId, req.App);
|
||||||
return new();
|
return new();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ public class SrsApi
|
|||||||
|
|
||||||
public async Task<List<Client>> ListClients()
|
public async Task<List<Client>> ListClients()
|
||||||
{
|
{
|
||||||
var rsp = await _client.GetFromJsonAsync<ListClientsResponse>("/api/v1/clients");
|
var rsp = await _client.GetFromJsonAsync<ListClientsResponse>("/api/v1/clients/?count=10000");
|
||||||
return rsp!.Clients;
|
return rsp!.Clients;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,13 +105,13 @@ public class Stream
|
|||||||
public long? RecvBytes { get; set; }
|
public long? RecvBytes { get; set; }
|
||||||
|
|
||||||
[JsonProperty("kbps")]
|
[JsonProperty("kbps")]
|
||||||
public Kbps Kbps { get; set; }
|
public Kbps? Kbps { get; set; }
|
||||||
|
|
||||||
[JsonProperty("publish")]
|
[JsonProperty("publish")]
|
||||||
public Publish Publish { get; set; }
|
public Publish Publish { get; set; }
|
||||||
|
|
||||||
[JsonProperty("video")]
|
[JsonProperty("video")]
|
||||||
public Video Video { get; set; }
|
public Video? Video { get; set; }
|
||||||
|
|
||||||
[JsonProperty("audio")]
|
[JsonProperty("audio")]
|
||||||
public Audio Audio { get; set; }
|
public Audio Audio { get; set; }
|
||||||
|
@ -56,8 +56,9 @@ public class StreamManager
|
|||||||
await PublishEvent(user, ev);
|
await PublishEvent(user, ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ConsumeQuota(string streamKey, double duration, string clientId)
|
public async Task ConsumeQuota(string streamKey, double duration, string clientId, string app)
|
||||||
{
|
{
|
||||||
|
if (!app.EndsWith("/source")) return;
|
||||||
var user = await GetUserFromStreamKey(streamKey);
|
var user = await GetUserFromStreamKey(streamKey);
|
||||||
if (user == default) throw new Exception("No stream key found");
|
if (user == default) throw new Exception("No stream key found");
|
||||||
|
|
||||||
|
@ -22,6 +22,32 @@
|
|||||||
"Endpoint": "https://localhost:10002",
|
"Endpoint": "https://localhost:10002",
|
||||||
"CertPath": "/Users/kieran/.polar/networks/1/volumes/lnd/bob/tls.cert",
|
"CertPath": "/Users/kieran/.polar/networks/1/volumes/lnd/bob/tls.cert",
|
||||||
"MacaroonPath": "/Users/kieran/.polar/networks/1/volumes/lnd/bob/data/chain/bitcoin/regtest/admin.macaroon"
|
"MacaroonPath": "/Users/kieran/.polar/networks/1/volumes/lnd/bob/data/chain/bitcoin/regtest/admin.macaroon"
|
||||||
}
|
},
|
||||||
|
"Variants": [
|
||||||
|
{
|
||||||
|
"Name": "source",
|
||||||
|
"Width": 1920,
|
||||||
|
"Height": 1080,
|
||||||
|
"Bandwidth": 8000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "720p",
|
||||||
|
"Width": 1280,
|
||||||
|
"Height": 720,
|
||||||
|
"Bandwidth": 3000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "480p",
|
||||||
|
"Width": 854,
|
||||||
|
"Height": 480,
|
||||||
|
"Bandwidth": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "240p",
|
||||||
|
"Width": 426,
|
||||||
|
"Height": 240,
|
||||||
|
"Bandwidth": 500
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,18 +15,12 @@ rtc_server {
|
|||||||
listen 8000;
|
listen 8000;
|
||||||
candidate *;
|
candidate *;
|
||||||
}
|
}
|
||||||
vhost __defaultVhost__ {
|
vhost transcode {
|
||||||
hls {
|
hls {
|
||||||
enabled on;
|
enabled on;
|
||||||
hls_dispose 30;
|
hls_dispose 30;
|
||||||
hls_fragment 5;
|
hls_fragment 2;
|
||||||
hls_window 15;
|
hls_window 10;
|
||||||
}
|
|
||||||
http_hooks {
|
|
||||||
enabled on;
|
|
||||||
on_publish http://10.100.2.226:5295/api/srs;
|
|
||||||
on_unpublish http://10.100.2.226:5295/api/srs;
|
|
||||||
on_hls http://10.100.2.226:5295/api/srs;
|
|
||||||
}
|
}
|
||||||
rtc {
|
rtc {
|
||||||
enabled on;
|
enabled on;
|
||||||
@ -37,4 +31,84 @@ vhost __defaultVhost__ {
|
|||||||
enabled on;
|
enabled on;
|
||||||
mount [vhost]/[app]/[stream].ts;
|
mount [vhost]/[app]/[stream].ts;
|
||||||
}
|
}
|
||||||
|
http_hooks {
|
||||||
|
enabled on;
|
||||||
|
on_publish http://10.100.2.226:5295/api/srs;
|
||||||
|
on_unpublish http://10.100.2.226:5295/api/srs;
|
||||||
|
on_hls http://10.100.2.226:5295/api/srs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vhost __defaultVhost__ {
|
||||||
|
transcode {
|
||||||
|
enabled on;
|
||||||
|
ffmpeg ./objs/ffmpeg/bin/ffmpeg;
|
||||||
|
|
||||||
|
engine source {
|
||||||
|
enabled on;
|
||||||
|
vcodec copy;
|
||||||
|
acodec copy;
|
||||||
|
output rtmp://127.0.0.1:[port]/[app]/[engine]/[stream]?vhost=transcode;
|
||||||
|
}
|
||||||
|
engine 720p {
|
||||||
|
enabled on;
|
||||||
|
vcodec libx264;
|
||||||
|
vbitrate 3000;
|
||||||
|
vfps 30;
|
||||||
|
vprofile baseline;
|
||||||
|
vpreset veryfast;
|
||||||
|
vfilter {
|
||||||
|
vf 'scale=-2:720';
|
||||||
|
}
|
||||||
|
vparams {
|
||||||
|
g 60;
|
||||||
|
tune 'zerolatency';
|
||||||
|
}
|
||||||
|
acodec libfdk_aac;
|
||||||
|
abitrate 160;
|
||||||
|
asample_rate 44100;
|
||||||
|
achannels 2;
|
||||||
|
output rtmp://127.0.0.1:[port]/[app]/[engine]/[stream]?vhost=transcode;
|
||||||
|
}
|
||||||
|
engine 480p {
|
||||||
|
enabled on;
|
||||||
|
vcodec libx264;
|
||||||
|
vbitrate 1000;
|
||||||
|
vfps 30;
|
||||||
|
vprofile baseline;
|
||||||
|
vpreset veryfast;
|
||||||
|
vfilter {
|
||||||
|
vf 'scale=-2:480';
|
||||||
|
}
|
||||||
|
vparams {
|
||||||
|
g 60;
|
||||||
|
tune 'zerolatency';
|
||||||
|
}
|
||||||
|
acodec libfdk_aac;
|
||||||
|
abitrate 96;
|
||||||
|
asample_rate 44100;
|
||||||
|
achannels 2;
|
||||||
|
output rtmp://127.0.0.1:[port]/[app]/[engine]/[stream]?vhost=transcode;
|
||||||
|
}
|
||||||
|
engine 240p {
|
||||||
|
enabled on;
|
||||||
|
vcodec libx264;
|
||||||
|
vbitrate 500;
|
||||||
|
vfps 30;
|
||||||
|
vprofile baseline;
|
||||||
|
vpreset veryfast;
|
||||||
|
vfilter {
|
||||||
|
vf 'scale=-2:240';
|
||||||
|
}
|
||||||
|
vparams {
|
||||||
|
g 60;
|
||||||
|
tune 'zerolatency';
|
||||||
|
}
|
||||||
|
acodec libfdk_aac;
|
||||||
|
abitrate 72;
|
||||||
|
asample_rate 44100;
|
||||||
|
achannels 2;
|
||||||
|
output rtmp://127.0.0.1:[port]/[app]/[engine]/[stream]?vhost=transcode;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user