Upload thumbnails to S3

This commit is contained in:
2023-08-01 23:08:57 +01:00
parent 99ad1dc439
commit d2b56c14f9
20 changed files with 482 additions and 110 deletions

View File

@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore;
using NostrStreamer.Database;
using NostrStreamer.Services.StreamManager;
namespace NostrStreamer.Services;
namespace NostrStreamer.Services.Background;
public class BackgroundStreamManager : BackgroundService
{

View File

@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore;
using Nostr.Client.Utils;
using NostrStreamer.Database;
namespace NostrStreamer.Services;
namespace NostrStreamer.Services.Background;
public class LndInvoicesStream : BackgroundService
{

View File

@ -5,7 +5,7 @@ using Nostr.Client.Communicator;
using Nostr.Client.Requests;
using Websocket.Client.Models;
namespace NostrStreamer.Services;
namespace NostrStreamer.Services.Background;
public class NostrListener : IDisposable
{

View File

@ -1,7 +1,8 @@
using Microsoft.EntityFrameworkCore;
using NostrStreamer.Database;
using NostrStreamer.Services.Thumbnail;
namespace NostrStreamer.Services;
namespace NostrStreamer.Services.Background;
public class ThumbnailGenerator : BackgroundService
{
@ -22,7 +23,7 @@ public class ThumbnailGenerator : BackgroundService
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<StreamerContext>();
var gen = scope.ServiceProvider.GetRequiredService<ThumbnailService>();
var gen = scope.ServiceProvider.GetRequiredService<IThumbnailService>();
var streams = await db.Streams
.AsNoTracking()

View File

@ -17,8 +17,8 @@ public class S3DvrStore : IDvrStore
{
_httpClient = httpClient;
_logger = logger;
_config = config.DvrStore;
_client = config.DvrStore.CreateClient();
_config = config.S3Store;
_client = config.S3Store.CreateClient();
}
public async Task<UploadResult> UploadRecording(UserStream stream, Uri source)

View File

@ -37,7 +37,7 @@ public class StreamEventBuilder
new("d", stream.Id.ToString()),
new("title", user.Title ?? ""),
new("summary", user.Summary ?? ""),
new("image", new Uri(_config.DataHost, $"{stream.Id}.jpg").ToString()),
new("image", stream.Thumbnail ?? user.Image ?? ""),
new("status", status),
new("p", user.PubKey, "", "host"),
new("relays", _config.Relays),

View File

@ -12,7 +12,6 @@ public class NostrStreamManager : IStreamManager
private readonly StreamManagerContext _context;
private readonly StreamEventBuilder _eventBuilder;
private readonly IDvrStore _dvrStore;
private readonly ThumbnailService _thumbnailService;
private readonly Config _config;
public NostrStreamManager(ILogger<NostrStreamManager> logger, StreamManagerContext context, IServiceProvider serviceProvider)
@ -21,7 +20,6 @@ public class NostrStreamManager : IStreamManager
_context = context;
_eventBuilder = serviceProvider.GetRequiredService<StreamEventBuilder>();
_dvrStore = serviceProvider.GetRequiredService<IDvrStore>();
_thumbnailService = serviceProvider.GetRequiredService<ThumbnailService>();
_config = serviceProvider.GetRequiredService<Config>();
}
@ -56,11 +54,8 @@ public class NostrStreamManager : IStreamManager
{
_logger.LogInformation("Stream started for: {pubkey}", _context.User.PubKey);
TestCanStream();
await UpdateStreamState(UserStreamState.Live);
#pragma warning disable CS4014
Task.Run(async () => await _thumbnailService.GenerateThumb(_context.UserStream));
#pragma warning restore CS4014
await UpdateStreamState(UserStreamState.Live);
}
public async Task StreamStopped()

View File

@ -0,0 +1,29 @@
using FFMpegCore;
using NostrStreamer.Database;
namespace NostrStreamer.Services.Thumbnail;
public abstract class BaseThumbnailService
{
protected readonly ILogger Logger;
protected readonly Config Config;
protected BaseThumbnailService(Config config, ILogger logger)
{
Config = config;
Logger = logger;
}
protected async Task<string> GenerateThumbnail(UserStream stream)
{
var path = Path.ChangeExtension(Path.GetTempFileName(), ".jpg");
var cmd = FFMpegArguments
.FromUrlInput(new Uri(Config.RtmpHost, $"{stream.Endpoint.App}/source/{stream.User.StreamKey}?vhost=hls.zap.stream"))
.OutputToFile(path, true, o => { o.ForceFormat("image2").WithCustomArgument("-vframes 1"); })
.CancellableThrough(new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token);
Logger.LogInformation("Running command {cmd}", cmd.Arguments);
await cmd.ProcessAsynchronously();
return path;
}
}

View File

@ -0,0 +1,8 @@
using NostrStreamer.Database;
namespace NostrStreamer.Services.Thumbnail;
public interface IThumbnailService
{
Task GenerateThumb(UserStream stream);
}

View File

@ -0,0 +1,80 @@
using System.Diagnostics;
using Amazon.S3;
using Microsoft.EntityFrameworkCore;
using NostrStreamer.Database;
namespace NostrStreamer.Services.Thumbnail;
public class S3ThumbnailService : BaseThumbnailService, IThumbnailService
{
private readonly AmazonS3Client _client;
private readonly StreamerContext _context;
public S3ThumbnailService(Config config, ILogger<S3ThumbnailService> logger, StreamerContext context) : base(config, logger)
{
_client = config.S3Store.CreateClient();
_context = context;
}
public async Task GenerateThumb(UserStream stream)
{
try
{
var sw = Stopwatch.StartNew();
var path = await GenerateThumbnail(stream);
var tGen = sw.Elapsed;
var s3Path = MapPath(stream.Id);
sw.Restart();
await using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
await _client.PutObjectAsync(new()
{
BucketName = Config.S3Store.BucketName,
Key = s3Path,
InputStream = fs,
AutoCloseStream = false,
AutoResetStreamPosition = false,
ContentType = "image/jpeg",
DisablePayloadSigning = Config.S3Store.DisablePayloadSigning
});
var uri = _client.GetPreSignedURL(new()
{
BucketName = Config.S3Store.BucketName,
Key = s3Path,
Expires = DateTime.UtcNow.AddYears(1000)
});
var ub = new UriBuilder(uri)
{
Scheme = Config.S3Store.PublicHost.Scheme,
Host = Config.S3Store.PublicHost.Host,
Port = Config.S3Store.PublicHost.Port
};
var tUpload = sw.Elapsed;
sw.Restart();
await _context.Streams.Where(a => a.Id == stream.Id)
.ExecuteUpdateAsync(o => o.SetProperty(v => v.Thumbnail, ub.Uri.ToString()));
var tDbUpdate = sw.Elapsed;
stream.Thumbnail = ub.Uri.ToString();
fs.Close();
File.Delete(path);
Logger.LogInformation("{id} generated={tg:#,##0}ms, uploaded={tu:#,##0}ms, db={td:#,##0}ms", stream.Id, tGen.TotalMilliseconds,
tUpload.TotalMilliseconds, tDbUpdate.TotalMilliseconds);
}
catch (Exception ex)
{
Logger.LogWarning("Failed to generate {id} thumbnail {msg}", stream.Id, ex.Message);
}
}
private string MapPath(Guid id)
{
return $"{id}/thumb.jpg";
}
}

View File

@ -1,55 +0,0 @@
using System.Diagnostics;
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 sw = Stopwatch.StartNew();
var cmd = FFMpegArguments
.FromUrlInput(new Uri(_config.RtmpHost, $"{stream.Endpoint.App}/source/{stream.User.StreamKey}?vhost=hls.zap.stream"))
.OutputToFile(path, true, o => { o.ForceFormat("image2").WithCustomArgument("-vframes 1"); })
.CancellableThrough(new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token);
_logger.LogInformation("Running command {cmd}", cmd.Arguments);
await cmd.ProcessAsynchronously();
sw.Stop();
_logger.LogInformation("Generated {id} thumb in {n:#,##0}ms", stream.Id, sw.Elapsed.TotalMilliseconds);
}
catch (Exception ex)
{
_logger.LogWarning("Failed to generate {id} thumbnail {msg}", stream.Id, 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");
}
}