testing using ffmpeg to generate thumbnails for avif

This commit is contained in:
2025-08-17 03:16:19 -04:00
parent d36aaac836
commit 8808126905
4 changed files with 104 additions and 1 deletions

View File

@@ -40,6 +40,11 @@ public class AobaService(IMongoDatabase db)
return new PagedResult<Media>(items, page, pageSize, total); return new PagedResult<Media>(items, page, pageSize, total);
} }
public async Task<List<Media>> FindMediaWithExtAsync(string ext, CancellationToken cancellationToken = default)
{
var filter = Builders<Media>.Filter.Eq(m => m.Ext, ext);
return await _media.Find(filter).ToListAsync();
}
public Task AddMediaAsync(Media media, CancellationToken cancellationToken = default) 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); await _media.UpdateOneAsync(m => m.MediaId == mediaId, upate, cancellationToken: cancellationToken);
} }
public async Task RemoveThumbnailAsync(ObjectId mediaId, ThumbnailSize size, CancellationToken cancellationToken = default)
{
var upate = Builders<Media>.Update.Unset(m => m.Thumbnails[size]);
await _media.UpdateOneAsync(m => m.MediaId == mediaId, upate, cancellationToken: cancellationToken);
}
public async Task<ObjectId> GetThumbnailIdAsync(ObjectId mediaId, ThumbnailSize size, CancellationToken cancellationToken = default) public async Task<ObjectId> GetThumbnailIdAsync(ObjectId mediaId, ThumbnailSize size, CancellationToken cancellationToken = default)
{ {
var thumb = await _media.Find(m => m.MediaId == mediaId).Project(m => m.Thumbnails[size]).FirstOrDefaultAsync(cancellationToken); var thumb = await _media.Find(m => m.MediaId == mediaId).Project(m => m.Thumbnails[size]).FirstOrDefaultAsync(cancellationToken);

View File

@@ -27,6 +27,28 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
{ {
private readonly GridFSBucket _gridfs = new GridFSBucket(db); private readonly GridFSBucket _gridfs = new GridFSBucket(db);
public async Task<Error?> 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;
}
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@@ -36,6 +58,7 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
/// <returns></returns> /// <returns></returns>
public async Task<Maybe<Stream>> GetOrCreateThumbnailAsync(ObjectId mediaId, ThumbnailSize size, CancellationToken cancellationToken = default) public async Task<Maybe<Stream>> GetOrCreateThumbnailAsync(ObjectId mediaId, ThumbnailSize size, CancellationToken cancellationToken = default)
{ {
var existingThumb = await GetThumbnailAsync(mediaId, size, cancellationToken); var existingThumb = await GetThumbnailAsync(mediaId, size, cancellationToken);
if (existingThumb != null) if (existingThumb != null)
return existingThumb; return existingThumb;
@@ -112,6 +135,9 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
public static async Task<Maybe<Stream>> GenerateImageThumbnailAsync(Stream stream, ThumbnailSize size, string ext, CancellationToken cancellationToken = default) public static async Task<Maybe<Stream>> GenerateImageThumbnailAsync(Stream stream, ThumbnailSize size, string ext, CancellationToken cancellationToken = default)
{ {
if(ext == ".avif")
return GenerateAvifThumbnail(stream, size, cancellationToken);
var img = LoadImage(stream, ext); var img = LoadImage(stream, ext);
if (img.HasError) if (img.HasError)
return img.Error; return img.Error;
@@ -132,7 +158,7 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
return result; return result;
} }
public Maybe<Stream> GenerateVideoThumbnail(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default) public static Maybe<Stream> GenerateVideoThumbnail(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default)
{ {
var w = (int)size; var w = (int)size;
var fn = ObjectId.GenerateNewId().ToString(); var fn = ObjectId.GenerateNewId().ToString();
@@ -166,6 +192,49 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
} }
} }
public static Maybe<Stream> 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<Maybe<Stream>> GenerateDocumentThumbnailAsync(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default) public async Task<Maybe<Stream>> GenerateDocumentThumbnailAsync(Stream data, ThumbnailSize size, CancellationToken cancellationToken = default)
{ {
return new NotImplementedException(); return new NotImplementedException();

View File

@@ -121,6 +121,9 @@ builder.Services.AddAuthentication(options =>
builder.Services.AddAoba(); builder.Services.AddAoba();
#if DEBUG
builder.Services.AddHostedService<DebugService>();
#endif
builder.Services.Configure<FormOptions>(opt => builder.Services.Configure<FormOptions>(opt =>
{ {
opt.ValueLengthLimit = int.MaxValue; opt.ValueLengthLimit = int.MaxValue;

View File

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