137 lines
5.1 KiB
C#
Raw Normal View History

using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
namespace FileShare_Services.Services.FileLibrary
{
public sealed class VideoThumbnailService : IVideoThumbnailService
{
private readonly string _thumbnailDir;
private readonly string _ffmpegPath;
private readonly string _ffprobePath;
public VideoThumbnailService(ThumbnailStorageOptions options)
{
_thumbnailDir = Path.IsPathRooted(options.RootPath)
? options.RootPath
: Path.Combine(AppContext.BaseDirectory, options.RootPath);
_ffmpegPath = ResolveExecutablePath(options.FfmpegPath);
_ffprobePath = ResolveExecutablePath(options.FfprobePath);
Directory.CreateDirectory(_thumbnailDir);
}
public Task<GeneratedThumbnail?> GenerateThumbnailAsync(int libraryRootId, string videoPath, CancellationToken ct = default)
{
var hash = ComputeHash(videoPath);
var relativePath = Path.Combine($"root-{libraryRootId}", $"{hash}.jpg");
var outputPath = GetAbsolutePath(relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
if (File.Exists(outputPath))
return Task.FromResult<GeneratedThumbnail?>(new GeneratedThumbnail(relativePath, "image/jpeg"));
try
{
var args = $"-ss 00:00:01 -i \"{videoPath}\" -vframes 1 -q:v 5 -vf \"scale=320:-2\" \"{outputPath}\" -y";
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = _ffmpegPath,
Arguments = args,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
},
EnableRaisingEvents = true,
};
process.Start();
using (ct.Register(() =>
{
try { process.Kill(); } catch { /* 进程可能已退出 */ }
}))
{
process.WaitForExit(10000);
}
if (File.Exists(outputPath) && new FileInfo(outputPath).Length > 0)
return Task.FromResult<GeneratedThumbnail?>(new GeneratedThumbnail(relativePath, "image/jpeg"));
return Task.FromResult<GeneratedThumbnail?>(null);
}
catch (Exception ex)
{
Serilog.Log.Warning(ex, "生成视频缩略图失败 {VideoPath}", videoPath);
return Task.FromResult<GeneratedThumbnail?>(null);
}
}
public string GetAbsolutePath(string relativePath)
{
var root = Path.GetFullPath(_thumbnailDir);
var fullPath = Path.GetFullPath(Path.Combine(root, relativePath));
if (!fullPath.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)
&& !string.Equals(fullPath, root, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Thumbnail path is outside of the configured storage root.");
}
return fullPath;
}
private static string ResolveExecutablePath(string executablePath)
{
if (Path.IsPathRooted(executablePath))
{
return executablePath;
}
return executablePath.Contains(Path.DirectorySeparatorChar)
|| executablePath.Contains(Path.AltDirectorySeparatorChar)
? Path.Combine(AppContext.BaseDirectory, executablePath)
: executablePath;
}
public double? GetVideoDuration(string videoPath)
{
try
{
var args = $"-v error -show_entries format=duration -of csv=p=0 \"{videoPath}\"";
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = _ffprobePath,
Arguments = args,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
},
};
process.Start();
var output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(5000);
if (double.TryParse(output, out var duration) && duration > 0)
return Math.Round(duration, 1);
return null;
}
catch (Exception ex)
{
Serilog.Log.Warning(ex, "获取视频时长失败 {VideoPath}", videoPath);
return null;
}
}
public string ComputeHash(string videoPath) =>
Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(videoPath))).ToLowerInvariant();
}
}