Generate thumbnails
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
bin/
|
||||
obj/
|
||||
.idea/
|
||||
thumbs/
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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>();
|
||||
|
@ -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),
|
||||
|
47
NostrStreamer/Services/ThumbnailGenerator.cs
Normal file
47
NostrStreamer/Services/ThumbnailGenerator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
50
NostrStreamer/Services/ThumbnailService.cs
Normal file
50
NostrStreamer/Services/ThumbnailService.cs
Normal 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");
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user