Generate thumbnails
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
.idea/
|
.idea/
|
||||||
|
thumbs/
|
@ -15,9 +15,10 @@ public class PlaylistController : Controller
|
|||||||
private readonly SrsApi _srsApi;
|
private readonly SrsApi _srsApi;
|
||||||
private readonly ViewCounter _viewCounter;
|
private readonly ViewCounter _viewCounter;
|
||||||
private readonly StreamManagerFactory _streamManagerFactory;
|
private readonly StreamManagerFactory _streamManagerFactory;
|
||||||
|
private readonly ThumbnailService _thumbnailService;
|
||||||
|
|
||||||
public PlaylistController(Config config, ILogger<PlaylistController> logger,
|
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;
|
_config = config;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -25,6 +26,7 @@ public class PlaylistController : Controller
|
|||||||
_srsApi = srsApi;
|
_srsApi = srsApi;
|
||||||
_viewCounter = viewCounter;
|
_viewCounter = viewCounter;
|
||||||
_streamManagerFactory = streamManagerFactory;
|
_streamManagerFactory = streamManagerFactory;
|
||||||
|
_thumbnailService = thumbnailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{variant}/{id}.m3u8")]
|
[HttpGet("{variant}/{id}.m3u8")]
|
||||||
@ -80,20 +82,29 @@ public class PlaylistController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{pubkey}.png")]
|
[HttpGet("{id}.jpg")]
|
||||||
public async Task GetPreview([FromRoute] string pubkey)
|
public async Task GetPreview([FromRoute] Guid id)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var streamManager = await _streamManagerFactory.ForCurrentStream(pubkey);
|
var stream = _thumbnailService.GetThumbnail(id);
|
||||||
var userStream = streamManager.GetStream();
|
if (stream != default)
|
||||||
|
{
|
||||||
var path = $"/{userStream.Endpoint.App}/{userStream.User.StreamKey}.png";
|
Response.ContentLength = stream.Length;
|
||||||
await ProxyRequest(path);
|
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)
|
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;
|
Response.StatusCode = 404;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FFMpegCore" Version="5.1.0" />
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.23.3" />
|
<PackageReference Include="Google.Protobuf" Version="3.23.3" />
|
||||||
<PackageReference Include="Grpc.Net.Client" Version="2.54.0" />
|
<PackageReference Include="Grpc.Net.Client" Version="2.54.0" />
|
||||||
<PackageReference Include="Grpc.Tools" Version="2.56.0">
|
<PackageReference Include="Grpc.Tools" Version="2.56.0">
|
||||||
|
@ -56,6 +56,8 @@ internal static class Program
|
|||||||
services.AddTransient<StreamEventBuilder>();
|
services.AddTransient<StreamEventBuilder>();
|
||||||
services.AddTransient<StreamManagerFactory>();
|
services.AddTransient<StreamManagerFactory>();
|
||||||
services.AddTransient<UserService>();
|
services.AddTransient<UserService>();
|
||||||
|
services.AddTransient<ThumbnailService>();
|
||||||
|
services.AddHostedService<ThumbnailGenerator>();
|
||||||
|
|
||||||
// lnd services
|
// lnd services
|
||||||
services.AddSingleton<LndNode>();
|
services.AddSingleton<LndNode>();
|
||||||
|
@ -38,7 +38,7 @@ public class StreamEventBuilder
|
|||||||
new("title", user.Title ?? ""),
|
new("title", user.Title ?? ""),
|
||||||
new("summary", user.Summary ?? ""),
|
new("summary", user.Summary ?? ""),
|
||||||
new("streaming", new Uri(_config.DataHost, $"{user.PubKey}.m3u8").ToString()),
|
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("status", status),
|
||||||
new("p", user.PubKey, "", "host"),
|
new("p", user.PubKey, "", "host"),
|
||||||
new("relays", _config.Relays),
|
new("relays", _config.Relays),
|
||||||
@ -73,7 +73,7 @@ public class StreamEventBuilder
|
|||||||
|
|
||||||
return ev.Sign(NostrPrivateKey.FromBech32(_config.PrivateKey));
|
return ev.Sign(NostrPrivateKey.FromBech32(_config.PrivateKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
public NostrEvent CreateStreamChat(UserStream stream, string message)
|
public NostrEvent CreateStreamChat(UserStream stream, string message)
|
||||||
{
|
{
|
||||||
var pk = NostrPrivateKey.FromBech32(_config.PrivateKey);
|
var pk = NostrPrivateKey.FromBech32(_config.PrivateKey);
|
||||||
|
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_unpublish http://10.100.2.226:5295/api/srs;
|
||||||
on_hls 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