refactor(2): Core Platform abstractions (IProcessRunner/IMediaMuxer + Desktop impls) - Hls/Dash/YtDlp through injection seams, threading contract documented (66 tests GREEN)
This commit is contained in:
parent
9eb988440f
commit
6df9f81b4a
6 changed files with 402 additions and 220 deletions
|
|
@ -17,7 +17,11 @@ public class HlsDownloader
|
|||
private readonly AppConfig _config;
|
||||
private readonly ConcurrentDictionary<Uri, byte[]> _keys = new();
|
||||
|
||||
public HlsDownloader(HttpClient http, AppConfig config)
|
||||
private readonly Platform.IMediaMuxer _muxer;
|
||||
|
||||
public HlsDownloader(HttpClient http, AppConfig config) : this(http, config, Platform.DesktopMediaMuxer.Default) { }
|
||||
|
||||
public HlsDownloader(HttpClient http, AppConfig config, Platform.IMediaMuxer muxer)
|
||||
{
|
||||
_http = http;
|
||||
_config = config;
|
||||
|
|
@ -182,7 +186,7 @@ public class HlsDownloader
|
|||
string merged, string outputBase, bool isFmp4,
|
||||
IProgress<DownloadProgress>? progress, CancellationToken ct)
|
||||
{
|
||||
var ffmpeg = ResolveFfmpeg();
|
||||
var ffmpeg = _muxer.ResolveFfmpeg(_config);
|
||||
if (ffmpeg != null)
|
||||
{
|
||||
var final = outputBase + ".mp4";
|
||||
|
|
@ -191,7 +195,7 @@ public class HlsDownloader
|
|||
progress?.Report(new DownloadProgress(0, 0, "remuxing"));
|
||||
try
|
||||
{
|
||||
await RunFfmpegAsync(ffmpeg, merged, final, ct);
|
||||
await _muxer.RemuxAsync(ffmpeg, merged, final, ct);
|
||||
if (File.Exists(final) && new FileInfo(final).Length > 0)
|
||||
{
|
||||
File.Delete(merged);
|
||||
|
|
@ -213,49 +217,5 @@ public class HlsDownloader
|
|||
return keep;
|
||||
}
|
||||
|
||||
private string? ResolveFfmpeg()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_config.FfmpegPath) && File.Exists(_config.FfmpegPath))
|
||||
return _config.FfmpegPath;
|
||||
var name = OperatingSystem.IsWindows() ? "ffmpeg.exe" : "ffmpeg";
|
||||
foreach (var d in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(Path.PathSeparator))
|
||||
{
|
||||
var p = Path.Combine(d.Trim('"'), name);
|
||||
if (File.Exists(p)) return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// #2 stderr/stdout 파이프를 동시에 드레인(데드락 방지), 종료 보장(좀비 방지), 실패 시 예외 throw
|
||||
// #10 stdout 드레인 태스크도 관측 — 취소 시 미관측(faulted) 태스크가 되지 않도록 finally 에서 await.
|
||||
private static async Task RunFfmpegAsync(string ffmpeg, string input, string output, CancellationToken ct)
|
||||
{
|
||||
Process? proc = null;
|
||||
Task<string>? stdoutTask = null;
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo(ffmpeg)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true,
|
||||
};
|
||||
foreach (var a in new[] { "-y", "-hide_banner", "-loglevel", "error", "-i", input, "-c", "copy", output })
|
||||
psi.ArgumentList.Add(a);
|
||||
proc = Process.Start(psi) ?? throw new InvalidOperationException("ffmpeg 시작 실패");
|
||||
var stderrTask = proc.StandardError.ReadToEndAsync(ct);
|
||||
stdoutTask = proc.StandardOutput.ReadToEndAsync(ct); // stdout 도 드레인(데드락 방지)
|
||||
await proc.WaitForExitAsync(ct);
|
||||
var stderr = await stderrTask;
|
||||
if (proc.ExitCode != 0)
|
||||
throw new InvalidOperationException($"ffmpeg 리먹스 실패(exit {proc.ExitCode}): {stderr.Trim()}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (proc is { HasExited: false }) proc.Kill(entireProcessTree: true); } catch { }
|
||||
// #10 드레인 태스크 관측 (취소/OCE/EOF 시 폭발 무시)
|
||||
try { if (stdoutTask is not null) await stdoutTask; } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue