From 88081269057566dcf2786bab67c6fafb6fbee2a9 Mon Sep 17 00:00:00 2001 From: Amatsugu Date: Sun, 17 Aug 2025 03:16:19 -0400 Subject: [PATCH] testing using ffmpeg to generate thumbnails for avif --- AobaCore/Services/AobaService.cs | 12 +++++ AobaCore/Services/ThumbnailService.cs | 71 ++++++++++++++++++++++++++- AobaServer/Program.cs | 3 ++ AobaServer/Services/DebugService.cs | 19 +++++++ 4 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 AobaServer/Services/DebugService.cs diff --git a/AobaCore/Services/AobaService.cs b/AobaCore/Services/AobaService.cs index b48bfa9..a88c46a 100644 --- a/AobaCore/Services/AobaService.cs +++ b/AobaCore/Services/AobaService.cs @@ -40,6 +40,11 @@ public class AobaService(IMongoDatabase db) return new PagedResult(items, page, pageSize, total); } + public async Task> FindMediaWithExtAsync(string ext, CancellationToken cancellationToken = default) + { + var filter = Builders.Filter.Eq(m => m.Ext, ext); + return await _media.Find(filter).ToListAsync(); + } public Task AddMediaAsync(Media media, CancellationToken cancellationToken = default) { @@ -53,6 +58,13 @@ public class AobaService(IMongoDatabase db) await _media.UpdateOneAsync(m => m.MediaId == mediaId, upate, cancellationToken: cancellationToken); } + public async Task RemoveThumbnailAsync(ObjectId mediaId, ThumbnailSize size, CancellationToken cancellationToken = default) + { + var upate = Builders.Update.Unset(m => m.Thumbnails[size]); + + await _media.UpdateOneAsync(m => m.MediaId == mediaId, upate, cancellationToken: cancellationToken); + } + public async Task GetThumbnailIdAsync(ObjectId mediaId, ThumbnailSize size, CancellationToken cancellationToken = default) { var thumb = await _media.Find(m => m.MediaId == mediaId).Project(m => m.Thumbnails[size]).FirstOrDefaultAsync(cancellationToken); diff --git a/AobaCore/Services/ThumbnailService.cs b/AobaCore/Services/ThumbnailService.cs index 92108e0..ace49f0 100644 --- a/AobaCore/Services/ThumbnailService.cs +++ b/AobaCore/Services/ThumbnailService.cs @@ -27,6 +27,28 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService) { private readonly GridFSBucket _gridfs = new GridFSBucket(db); + public async Task DeleteThumbnailAsync(ObjectId mediaId, ThumbnailSize size) + { + var thumbId = await aobaService.GetThumbnailIdAsync(mediaId, size); + if (thumbId == default) + return null; + try + { + await _gridfs.DeleteAsync(thumbId); + await aobaService.RemoveThumbnailAsync(mediaId, size); + } + catch (GridFSFileNotFoundException) + { + //Ignore if the file was not found (somehow already deleted) + await aobaService.RemoveThumbnailAsync(mediaId, size); + } + catch (Exception e) + { + return new ExceptionError(e); + } + return null; + } + /// /// /// @@ -36,6 +58,7 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService) /// public async Task> GetOrCreateThumbnailAsync(ObjectId mediaId, ThumbnailSize size, CancellationToken cancellationToken = default) { + var existingThumb = await GetThumbnailAsync(mediaId, size, cancellationToken); if (existingThumb != null) return existingThumb; @@ -112,6 +135,9 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService) public static async Task> GenerateImageThumbnailAsync(Stream stream, ThumbnailSize size, string ext, CancellationToken cancellationToken = default) { + if(ext == ".avif") + return GenerateAvifThumbnail(stream, size, cancellationToken); + var img = LoadImage(stream, ext); if (img.HasError) return img.Error; @@ -132,7 +158,7 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService) return result; } - public Maybe GenerateVideoThumbnail(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default) + public static Maybe GenerateVideoThumbnail(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default) { var w = (int)size; var fn = ObjectId.GenerateNewId().ToString(); @@ -166,6 +192,49 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService) } } + public static Maybe GenerateAvifThumbnail(Stream data, ThumbnailSize size, CancellationToken cancellationToken) + { + var w = (int)size; + var fn = ObjectId.GenerateNewId().ToString(); + var filePath = $"/tmp/{fn}.in"; + using var source = new FileStream(filePath, FileMode.CreateNew); + data.CopyTo(source); + source.Flush(); + source.Dispose(); + data.Dispose(); + try + { + var output = new MemoryStream(); + FFMpegArguments.FromFileInput(filePath, false, opt => + { + //opt.WithCustomArgument("-vf "); + }) + .OutputToPipe(new StreamPipeSink(output), opt => + { + var tonemap = ",format=gbrpf32le,zscale=primaries=bt2020:transfer=smpte2084:matrix=gbr,tonemap=hable,zscale=primaries=bt709:transfer=bt709:matrix=bt709,format=yuv420p"; + var args = $"-vf \"crop='min(in_w,in_h)':'min(in_w,in_h)',scale={w}:{w}," + + $"{tonemap}\"" + //+ "zscale=primaries=bt2020:transfer=smpte2084:matrix=bt2020nc,format=gbrpf32le," + //+ "zscale=primaries=bt709:transfer=bt709:matrix=bt709:range=tv,tonemap=hable," + //+ "zscale=matrix=bt709:transfer=bt709:primaries=bt709," + //+ "format=yuv420p\" " + + "-colorspace bt709"; + opt.WithCustomArgument(args) + .ForceFormat("webp"); + }).ProcessSynchronously(); + output.Position = 0; + return output; + } + catch(Exception ex) + { + return ex; + } + finally + { + File.Delete(filePath); + } + } + public async Task> GenerateDocumentThumbnailAsync(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default) { return new NotImplementedException(); diff --git a/AobaServer/Program.cs b/AobaServer/Program.cs index 93fe3bd..8aafad5 100644 --- a/AobaServer/Program.cs +++ b/AobaServer/Program.cs @@ -121,6 +121,9 @@ builder.Services.AddAuthentication(options => builder.Services.AddAoba(); +#if DEBUG +builder.Services.AddHostedService(); +#endif builder.Services.Configure(opt => { opt.ValueLengthLimit = int.MaxValue; diff --git a/AobaServer/Services/DebugService.cs b/AobaServer/Services/DebugService.cs new file mode 100644 index 0000000..a139822 --- /dev/null +++ b/AobaServer/Services/DebugService.cs @@ -0,0 +1,19 @@ + +using AobaCore.Services; + +namespace AobaServer.Services; + +public class DebugService(AobaService aobaService, ThumbnailService thumbnailService) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var mediaItems = await aobaService.FindMediaWithExtAsync(".avif", stoppingToken); + foreach (var item in mediaItems) + { + foreach (var size in item.Thumbnails.Keys) + { + await thumbnailService.DeleteThumbnailAsync(item.MediaId, size); + } + } + } +}