using System.Diagnostics; using System.Globalization; using VideoDownloader.Core.Config; using VideoDownloader.Core.Models; namespace VideoDownloader.Core.Dash; /// /// DASH(MPD) 다운로더. MPD 를 사전 파싱해 (a) DRM 여부 검증, (b) 진행률용 총 길이 산출한 뒤 /// ffmpeg 에게 네이티브 DASH 입력(-i mpd)으로 맡김 — 비디오/오디오 표현 선택·조합·fMP4 처리를 /// ffmpeg가 알아서 하므로 수동 세그먼트 병합보다 훨씬 견고. 클리어(비DRM) 전용. /// public class DashDownloader { private readonly HttpClient _http; private readonly AppConfig _config; private readonly Platform.IMediaMuxer _muxer; public DashDownloader(HttpClient http, AppConfig config) : this(http, config, Platform.DesktopMediaMuxer.Default) { } public DashDownloader(HttpClient http, AppConfig config, Platform.IMediaMuxer muxer) { _http = http; _config = config; _muxer = muxer; } public async Task DownloadAsync(Uri mpdUrl, string outputBase, IProgress? progress = null, CancellationToken ct = default) { string? dir = Path.GetDirectoryName(outputBase); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); // 사전 검증: 파싱(또는 DRM) + 총 길이. double durationSec = 0; try { var xml = await _http.GetStringAsync(mpdUrl, ct); var manifest = DashParser.Parse(xml, mpdUrl); if (manifest == null) throw new InvalidOperationException("DASH 매니페스트 파싱 실패 (DRM 암호화이거나 미지원 형식)"); durationSec = manifest.DurationSec; } catch (OperationCanceledException) { throw; } catch (InvalidOperationException) { throw; } catch (Exception e) { throw new InvalidOperationException($"MPD 조회/파싱 실패: {e.Message}", e); } var ffmpeg = _muxer.ResolveFfmpeg(_config) ?? throw new InvalidOperationException("DASH 다운로드에는 ffmpeg가 필요합니다 (시스템 PATH 또는 설정의 ffmpeg 경로)."); var final = outputBase + ".mp4"; progress?.Report(new DownloadProgress(0, 0, "downloading", 0, "DASH 준비")); bool success = false; try { await _muxer.DownloadDashAsync(ffmpeg, mpdUrl, final, durationSec, _http, progress, ct); if (!File.Exists(final) || new FileInfo(final).Length == 0) throw new InvalidOperationException("ffmpeg 출력 파일이 비었거나 없습니다."); success = true; progress?.Report(new DownloadProgress(0, 0, "downloading", 100, null)); return final; } finally { if (!success) { try { if (File.Exists(final)) File.Delete(final); } catch { } } } } }