Generate thumbnails

This commit is contained in:
2023-07-27 20:56:48 +01:00
parent 9053e3eb1f
commit ff430675f3
8 changed files with 123 additions and 27 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
bin/
obj/
.idea/
thumbs/

View File

@ -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<PlaylistController> 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;
}
}

View File

@ -29,6 +29,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FFMpegCore" Version="5.1.0" />
<PackageReference Include="Google.Protobuf" Version="3.23.3" />
<PackageReference Include="Grpc.Net.Client" Version="2.54.0" />
<PackageReference Include="Grpc.Tools" Version="2.56.0">

View File

@ -56,6 +56,8 @@ internal static class Program
services.AddTransient<StreamEventBuilder>();
services.AddTransient<StreamManagerFactory>();
services.AddTransient<UserService>();
services.AddTransient<ThumbnailService>();
services.AddHostedService<ThumbnailGenerator>();
// lnd services
services.AddSingleton<LndNode>();

View File

@ -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);

View File

@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore;
using NostrStreamer.Database;
namespace NostrStreamer.Services;
public class ThumbnailGenerator : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<ThumbnailGenerator> _logger;
public ThumbnailGenerator(IServiceScopeFactory scopeFactory, ILogger<ThumbnailGenerator> 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<StreamerContext>();
var gen = scope.ServiceProvider.GetRequiredService<ThumbnailService>();
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);
}
}
}

View File

@ -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<ThumbnailService> _logger;
public ThumbnailService(Config config, ILogger<ThumbnailService> 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");
}
}

View File

@ -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;
}
}
}