1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Security.Cryptography;
namespace AdamsToolkit.Core;
/// <summary>
/// Garante ffmpeg.exe em %LOCALAPPDATA%\AdamsToolkit\editor\ffmpeg\ (reaproveita o do Adams Clips se existir). Não vem embebido no exe
/// (145MB) — é descarregado uma vez do nosso servidor (zip 56MB, build BtbN gpl
/// com ddagrab + nvenc/amf/qsv/x264) e verificado por sha256.
/// </summary>
public static class FfmpegManager
{
public const string ZipUrl = "https://adams-62-238-22-77.sslip.io/files/ffmpeg7-win64.zip"; // v1.79: build shared 7.1 (DLLs p/ o leitor FFME + ffmpeg.exe)
public const string ZipSha256 = "4acb8e2bfa52f6906c73bce33b0b10bfd26a3299a9fe33dc0e13aa41158f3e5d";
public static string Dir => Path.Combine(EditorPaths.LocalDir, "ffmpeg7");
public static string ExePath => Path.Combine(Dir, "ffmpeg.exe");
/// <summary>ffmpeg.exe + DLLs (avcodec-61 é a maior; sem ela nem o exe arranca).</summary>
public static bool IsReady
{
get
{
try { return File.Exists(ExePath) && File.Exists(Path.Combine(Dir, "avcodec-61.dll")) && new FileInfo(Path.Combine(Dir, "avcodec-61.dll")).Length > 50_000_000; }
catch { return false; }
}
}
private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromMinutes(20) };
public static async Task EnsureAsync(IProgress<(double pct, string msg)> progress, CancellationToken ct = default)
{
if (IsReady) return;
Directory.CreateDirectory(Dir);
var tmp = Path.Combine(Dir, "ffmpeg.zip.part");
progress.Report((0, "A descarregar o motor de vídeo (60 MB)…"));
using (var resp = await Http.GetAsync(ZipUrl, HttpCompletionOption.ResponseHeadersRead, ct))
{
resp.EnsureSuccessStatusCode();
var total = resp.Content.Headers.ContentLength ?? -1L;
await using var src = await resp.Content.ReadAsStreamAsync(ct);
await using var dst = File.Create(tmp);
var buf = new byte[1 << 16];
long done = 0; int n;
var last = DateTime.UtcNow;
while ((n = await src.ReadAsync(buf, ct)) > 0)
{
await dst.WriteAsync(buf.AsMemory(0, n), ct);
done += n;
if ((DateTime.UtcNow - last).TotalMilliseconds > 150)
{
last = DateTime.UtcNow;
var pct = total > 0 ? done * 100.0 / total : 0;
progress.Report((pct, $"A descarregar o motor de vídeo… {done / 1048576} / {(total > 0 ? total / 1048576 : 60)} MB"));
}
}
}
progress.Report((100, "A verificar…"));
await using (var fs = File.OpenRead(tmp))
{
var hash = Convert.ToHexString(await SHA256.HashDataAsync(fs, ct));
if (!hash.Equals(ZipSha256, StringComparison.OrdinalIgnoreCase))
{
File.Delete(tmp);
throw new InvalidDataException("Download do FFmpeg corrompido (sha256 inválido). Tenta outra vez.");
}
}
progress.Report((100, "A extrair…"));
using (var zip = ZipFile.OpenRead(tmp))
{
foreach (var entry in zip.Entries)
{
if (string.IsNullOrEmpty(entry.Name)) continue;
if (!entry.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && !entry.Name.Equals("ffmpeg.exe", StringComparison.OrdinalIgnoreCase)) continue;
entry.ExtractToFile(Path.Combine(Dir, entry.Name), true);
}
}
File.Delete(tmp);
progress.Report((100, "FFmpeg pronto."));
}
/// <summary>
/// Escapa um valor de opção para dentro de um filtergraph do ffmpeg. O ffmpeg faz DOIS
/// níveis de parse: o graph (separadores , ; [ ] e aspas) e depois as opções do filtro
/// (separador :). Um só "\:" era desfeito no 1º nível e o 2º partia o caminho no ':'
/// do disco ("m=C" + "/Users/...") -> "Error parsing a filter description" (bug v1.8-1.11:
/// o RNNoise nunca chegou a correr). Agora: 1º nível p/ ':' '\' ''' e 2º nível p/ os do
/// graph -> "C\\:/Users/...". Também aguenta usernames com ' [ ] , ;.
/// </summary>
public static string EscapeFilterValue(string v)
{
var l1 = new System.Text.StringBuilder();
foreach (var c in v) { if (c is '\\' or ':' or '\'') l1.Append('\\'); l1.Append(c); }
var l2 = new System.Text.StringBuilder();
foreach (var c in l1.ToString()) { if (c is '\\' or '\'' or '[' or ']' or ',' or ';') l2.Append('\\'); l2.Append(c); }
return l2.ToString();
}
/// <summary>Lista encoders compilados (nome → true) para pré-filtrar candidatos.</summary>
public static async Task<HashSet<string>> ListEncodersAsync()
{
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
try
{
var psi = new ProcessStartInfo(ExePath, "-hide_banner -encoders")
{
RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true,
};
using var p = Process.Start(psi)!;
var txt = await p.StandardOutput.ReadToEndAsync();
await p.WaitForExitAsync();
foreach (var line in txt.Split('\n'))
{
var parts = line.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 2 && parts[0].Length == 6 && parts[0][0] == 'V') set.Add(parts[1]);
}
}
catch { }
return set;
}
}