From ff430675f3833488161c3b0866ace83627ba2185 Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 27 Jul 2023 20:56:48 +0100 Subject: [PATCH] Generate thumbnails --- .gitignore | 1 + .../Controllers/PlaylistController.cs | 29 +++++++---- NostrStreamer/NostrStreamer.csproj | 1 + NostrStreamer/Program.cs | 2 + NostrStreamer/Services/StreamEventBuilder.cs | 4 +- NostrStreamer/Services/ThumbnailGenerator.cs | 47 +++++++++++++++++ NostrStreamer/Services/ThumbnailService.cs | 50 +++++++++++++++++++ docker/srs-origin.conf | 16 ------ 8 files changed, 123 insertions(+), 27 deletions(-) create mode 100644 NostrStreamer/Services/ThumbnailGenerator.cs create mode 100644 NostrStreamer/Services/ThumbnailService.cs diff --git a/.gitignore b/.gitignore index 0e945e0..ee8d6a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ bin/ obj/ .idea/ +thumbs/ \ No newline at end of file diff --git a/NostrStreamer/Controllers/PlaylistController.cs b/NostrStreamer/Controllers/PlaylistController.cs index 739e771..90496e0 100644 --- a/NostrStreamer/Controllers/PlaylistController.cs +++ b/NostrStreamer/Controllers/PlaylistController.cs @@ -15,9 +15,10 @@ public class PlaylistController : Controller private readonly SrsApi _srsApi; private readonly ViewCounter _viewCounter; private readonly StreamManagerFactory _streamManagerFactory; + private readonly ThumbnailService _thumbnailService; public PlaylistController(Config config, ILogger logger, - HttpClient client, SrsApi srsApi, ViewCounter viewCounter, StreamManagerFactory streamManagerFactory) + HttpClient client, SrsApi srsApi, ViewCounter viewCounter, StreamManagerFactory streamManagerFactory, ThumbnailService thumbnailService) { _config = config; _logger = logger; @@ -25,6 +26,7 @@ public class PlaylistController : Controller _srsApi = srsApi; _viewCounter = viewCounter; _streamManagerFactory = streamManagerFactory; + _thumbnailService = thumbnailService; } [HttpGet("{variant}/{id}.m3u8")] @@ -80,20 +82,29 @@ public class PlaylistController : Controller } } - [HttpGet("{pubkey}.png")] - public async Task GetPreview([FromRoute] string pubkey) + [HttpGet("{id}.jpg")] + public async Task GetPreview([FromRoute] Guid id) { try { - var streamManager = await _streamManagerFactory.ForCurrentStream(pubkey); - var userStream = streamManager.GetStream(); - - var path = $"/{userStream.Endpoint.App}/{userStream.User.StreamKey}.png"; - await ProxyRequest(path); + var stream = _thumbnailService.GetThumbnail(id); + if (stream != default) + { + Response.ContentLength = stream.Length; + Response.ContentType = "image/jpg"; + Response.Headers.CacheControl = "public, max-age=60"; + await Response.StartAsync(); + await stream.CopyToAsync(Response.Body); + await Response.CompleteAsync(); + } + else + { + Response.StatusCode = 404; + } } catch (Exception ex) { - _logger.LogWarning("Failed to get preview image for {pubkey} {message}", pubkey, ex.Message); + _logger.LogWarning("Failed to get preview image for {id} {message}", id, ex.Message); Response.StatusCode = 404; } } diff --git a/NostrStreamer/NostrStreamer.csproj b/NostrStreamer/NostrStreamer.csproj index 33fc995..0b05ebb 100644 --- a/NostrStreamer/NostrStreamer.csproj +++ b/NostrStreamer/NostrStreamer.csproj @@ -29,6 +29,7 @@ + diff --git a/NostrStreamer/Program.cs b/NostrStreamer/Program.cs index b229a73..c9832b7 100644 --- a/NostrStreamer/Program.cs +++ b/NostrStreamer/Program.cs @@ -56,6 +56,8 @@ internal static class Program services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddHostedService(); // lnd services services.AddSingleton(); diff --git a/NostrStreamer/Services/StreamEventBuilder.cs b/NostrStreamer/Services/StreamEventBuilder.cs index 81f7c38..3e53acd 100644 --- a/NostrStreamer/Services/StreamEventBuilder.cs +++ b/NostrStreamer/Services/StreamEventBuilder.cs @@ -38,7 +38,7 @@ public class StreamEventBuilder new("title", user.Title ?? ""), new("summary", user.Summary ?? ""), new("streaming", new Uri(_config.DataHost, $"{user.PubKey}.m3u8").ToString()), - new("image", user.Image ?? new Uri(_config.DataHost, $"{user.PubKey}.png").ToString()), + new("image", string.IsNullOrEmpty(user.Image) ? new Uri(_config.DataHost, $"{stream.Id}.jpg").ToString() : user.Image), new("status", status), new("p", user.PubKey, "", "host"), new("relays", _config.Relays), @@ -73,7 +73,7 @@ public class StreamEventBuilder return ev.Sign(NostrPrivateKey.FromBech32(_config.PrivateKey)); } - + public NostrEvent CreateStreamChat(UserStream stream, string message) { var pk = NostrPrivateKey.FromBech32(_config.PrivateKey); diff --git a/NostrStreamer/Services/ThumbnailGenerator.cs b/NostrStreamer/Services/ThumbnailGenerator.cs new file mode 100644 index 0000000..0a974fe --- /dev/null +++ b/NostrStreamer/Services/ThumbnailGenerator.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore; +using NostrStreamer.Database; + +namespace NostrStreamer.Services; + +public class ThumbnailGenerator : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + + public ThumbnailGenerator(IServiceScopeFactory scopeFactory, ILogger logger) + { + _scopeFactory = scopeFactory; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + using var scope = _scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var gen = scope.ServiceProvider.GetRequiredService(); + + var streams = await db.Streams + .AsNoTracking() + .Include(a => a.Endpoint) + .Include(a => a.User) + .Where(a => a.State == UserStreamState.Live) + .ToListAsync(cancellationToken: stoppingToken); + + foreach (var stream in streams) + { + await gen.GenerateThumb(stream); + } + } + catch (Exception ex) + { + _logger.LogWarning("Failed to generate thumbnail {msg}", ex.Message); + } + + await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); + } + } +} diff --git a/NostrStreamer/Services/ThumbnailService.cs b/NostrStreamer/Services/ThumbnailService.cs new file mode 100644 index 0000000..e754e0f --- /dev/null +++ b/NostrStreamer/Services/ThumbnailService.cs @@ -0,0 +1,50 @@ +using FFMpegCore; +using NostrStreamer.Database; + +namespace NostrStreamer.Services; + +public class ThumbnailService +{ + private const string Dir = "thumbs"; + private readonly Config _config; + private readonly ILogger _logger; + + public ThumbnailService(Config config, ILogger logger) + { + _config = config; + _logger = logger; + if (!Directory.Exists(Dir)) + { + Directory.CreateDirectory(Dir); + } + } + + public async Task GenerateThumb(UserStream stream) + { + var path = MapPath(stream.Id); + try + { + var cmd = FFMpegArguments + .FromUrlInput(new Uri(_config.RtmpHost, $"{stream.Endpoint.App}/{stream.User.StreamKey}")) + .OutputToFile(path, true, o => { o.ForceFormat("image2").WithCustomArgument("-vframes 1"); }); + + _logger.LogInformation("Running command {cmd}", cmd.Arguments); + await cmd.ProcessAsynchronously(); + } + catch (Exception ex) + { + _logger.LogWarning("Failed to generate thumbnail {msg}", ex.Message); + } + } + + public System.IO.Stream? GetThumbnail(Guid id) + { + var path = MapPath(id); + return File.Exists(path) ? new FileStream(path, FileMode.Open, FileAccess.Read) : null; + } + + private string MapPath(Guid id) + { + return Path.Combine(Dir, $"{id}.jpg"); + } +} diff --git a/docker/srs-origin.conf b/docker/srs-origin.conf index 944faee..1a0a50c 100644 --- a/docker/srs-origin.conf +++ b/docker/srs-origin.conf @@ -36,20 +36,4 @@ vhost hls.zap.stream { on_unpublish http://10.100.2.226:5295/api/srs; on_hls http://10.100.2.226:5295/api/srs; } - - transcode { - enabled on; - ffmpeg ./objs/ffmpeg/bin/ffmpeg; - - engine { - enabled on; - vcodec png; - acodec an; - vparams { - vframes 1; - } - oformat image2; - output ./objs/nginx/html/[app]/[stream].png; - } - } } \ No newline at end of file