mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-21 16:12:01 +00:00
168 lines
4.7 KiB
C#
168 lines
4.7 KiB
C#
using System.Diagnostics;
|
|
using Avalonia;
|
|
using Avalonia.Media.Imaging;
|
|
using Avalonia.Platform;
|
|
|
|
namespace Splitter_UI.Services;
|
|
|
|
public sealed class ThumbnailService : IThumbnailService
|
|
{
|
|
private const int _thumbWidth = 160;
|
|
private const int _thumbHeight = 90;
|
|
|
|
private readonly byte [] _bgrBuffer = new byte[_thumbWidth * _thumbHeight * 3];
|
|
private readonly byte [] _bgraBuffer = new byte[_thumbWidth * _thumbHeight * 4];
|
|
|
|
public SemaphoreSlim _lock = new(1,1);
|
|
|
|
public async Task<Bitmap?> CreateThumbnailAsync(
|
|
string file,
|
|
VideoInfo probe,
|
|
TimeSpan? skip = null,
|
|
int? width = null,
|
|
int? height = null,
|
|
int? rotateDegree = null)
|
|
{
|
|
await _lock.WaitAsync();
|
|
try
|
|
{
|
|
return await CreateThumbnailInternal(
|
|
file,
|
|
probe,
|
|
skip,
|
|
width,
|
|
height,
|
|
rotateDegree
|
|
);
|
|
}
|
|
finally
|
|
{
|
|
_lock.Release();
|
|
}
|
|
}
|
|
|
|
private async Task<Bitmap?> CreateThumbnailInternal(
|
|
string file,
|
|
VideoInfo probe,
|
|
TimeSpan? skip = null,
|
|
int? width = null,
|
|
int? height = null,
|
|
int? rotateDegree = null)
|
|
{
|
|
width ??= _thumbWidth;
|
|
height ??= _thumbHeight;
|
|
skip ??= TimeSpan.Zero;
|
|
|
|
// buffer for BGR24 → 3 bytes per pixel
|
|
|
|
var canUseStaticBuffers =
|
|
width.Value == _thumbWidth &&
|
|
height.Value == _thumbHeight;
|
|
|
|
var bgrBuffer = canUseStaticBuffers ? _bgrBuffer : new byte[width.Value * height.Value * 3];
|
|
var bgraBuffer = canUseStaticBuffers ? _bgraBuffer : new byte[width.Value * height.Value * 4];
|
|
|
|
// Decode a single frame using ffmpeg → raw BGR24 into _bgrBuffer
|
|
var ok = await DecodeFrameAsync(bgrBuffer, file, skip.Value, width.Value, height.Value, rotateDegree);
|
|
if (!ok)
|
|
return null;
|
|
|
|
// Convert BGR24 → BGRA32
|
|
ConvertBgrToBgra(bgrBuffer, bgraBuffer, width.Value, height.Value);
|
|
|
|
// Create Avalonia Bitmap
|
|
return CreateBitmap(bgraBuffer, width.Value, height.Value, rotateDegree == 90 || rotateDegree == 270);
|
|
}
|
|
|
|
private static async Task<bool> DecodeFrameAsync(byte [] bgrBuffer, string file, TimeSpan skip, int width, int height, int? rotateDegree)
|
|
{
|
|
var rotationStr = TrackingSplitter.GetRorationArg(rotateDegree);
|
|
|
|
// ffmpeg command: decode one frame, resize, output raw BGR24
|
|
var args =
|
|
$"-ss {skip.TotalSeconds} -t 0.1 -i \"{file}\" " +
|
|
"-an -sn " +
|
|
$"-vf \"scale={width}:{height}:force_original_aspect_ratio=decrease," +
|
|
$"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2,format=bgr24{rotationStr}\" " +
|
|
"-f rawvideo -";
|
|
|
|
var psi = new ProcessStartInfo
|
|
{
|
|
FileName = "ffmpeg",
|
|
Arguments = args,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
var p = new Process { StartInfo = psi };
|
|
p.Start();
|
|
|
|
var needed = bgrBuffer.Length;
|
|
var read = 0;
|
|
|
|
using var stdout = p.StandardOutput.BaseStream;
|
|
|
|
while (read < needed)
|
|
{
|
|
var r = await stdout.ReadAsync(bgrBuffer, read, needed - read);
|
|
if (r == 0)
|
|
{
|
|
TryKill(p);
|
|
return false;
|
|
}
|
|
read += r;
|
|
}
|
|
|
|
TryKill(p);
|
|
return true;
|
|
}
|
|
|
|
private static void TryKill(Process p)
|
|
{
|
|
try { p.Kill(); } catch { }
|
|
}
|
|
|
|
private static void ConvertBgrToBgra(byte[] bgr, byte[] bgra, int width, int height)
|
|
{
|
|
var si = 0;
|
|
var di = 0;
|
|
|
|
var totalPixels = width * height;
|
|
|
|
for (var i = 0; i < totalPixels; i++)
|
|
{
|
|
bgra[di + 0] = bgr[si + 0]; // B
|
|
bgra[di + 1] = bgr[si + 1]; // G
|
|
bgra[di + 2] = bgr[si + 2]; // R
|
|
bgra[di + 3] = 255; // A
|
|
|
|
si += 3;
|
|
di += 4;
|
|
}
|
|
}
|
|
|
|
private static unsafe Bitmap CreateBitmap(byte[] bgra, int width, int height, bool isRotated)
|
|
{
|
|
if (isRotated)
|
|
{
|
|
(height, width) = (width, height);
|
|
}
|
|
|
|
var stride = width * 4;
|
|
|
|
fixed (byte* p = bgra)
|
|
{
|
|
return new Bitmap(
|
|
PixelFormat.Bgra8888,
|
|
AlphaFormat.Premul,
|
|
(nint)p,
|
|
new PixelSize(width, height),
|
|
new Vector(96, 96),
|
|
stride);
|
|
}
|
|
}
|
|
|
|
}
|