diff --git a/uniapp/Google Chrome/Form1.cs b/uniapp/Google Chrome/Form1.cs index 6fbfd76..86ea0e5 100644 --- a/uniapp/Google Chrome/Form1.cs +++ b/uniapp/Google Chrome/Form1.cs @@ -10,7 +10,8 @@ namespace WinFormsApp3 /// 可直接 HTTP 访问的视频地址;PlaylistText 不为空时作为相对地址基准。 /// 只在内存里拿到 m3u8 文本时使用(抖音这类 MSE 播放)。 /// 发出这个请求的文档地址;跨域 iframe 播放器必须用它,不然会被防盗链拒掉。 - public record VideoCandidate(string Url, string? PlaylistText = null, string? Referer = null); + /// DASH 分离音轨地址;不为空时需要和 Url 一起下载后用 ffmpeg 合并。 + public record VideoCandidate(string Url, string? PlaylistText = null, string? Referer = null, string? AudioUrl = null); public partial class Form1 : Form { @@ -503,16 +504,19 @@ namespace WinFormsApp3 } /// - /// 只取当前正在播放的那一个视频地址,按三层递降: + /// 只取当前正在播放的那一个视频地址,按四层递降: /// 1. video 标签自己的播放源(最准确,就是正在播的这个); - /// 2. 抖音这类 blob+MSE 播放、或播放器在 iframe 里取不到源时, - /// 退回用本页最近嗅探到的那一个整片地址(m3u8 或直链,分片不算); - /// 3. 只在内存里拿到播放列表文本时,用页面地址做分片相对地址的基准。 + /// 2. DASH 分离轨(抖音这类 blob+MSE):视频轨 + 音频轨配对,下载后合并; + /// 3. 播放器在 iframe 里取不到源时,退回用本页最近嗅探到的那一个整片地址(m3u8 或直链,分片不算); + /// 4. 只在内存里拿到播放列表文本时,用页面地址做分片相对地址的基准。 /// private VideoCandidate? CollectCurrentCandidate(string grabResultJson) { string current = string.Empty; string pageUrl = string.Empty; + string dashVideo = string.Empty; + string dashAudio = string.Empty; + string videoId = string.Empty; List sniffed = new List(); try @@ -524,6 +528,9 @@ namespace WinFormsApp3 JsonNode? node = JsonNode.Parse(inner); current = node?["current"]?.GetValue() ?? string.Empty; pageUrl = node?["pageUrl"]?.GetValue() ?? string.Empty; + dashVideo = node?["dashVideo"]?.GetValue() ?? string.Empty; + dashAudio = node?["dashAudio"]?.GetValue() ?? string.Empty; + videoId = node?["videoId"]?.GetValue() ?? string.Empty; if (node?["sniffed"] is JsonArray arr) { @@ -540,23 +547,44 @@ namespace WinFormsApp3 // 页面没有返回可解析结果时,只用宿主侧嗅探到的地址 } + // 第一层:video 标签自己的播放源 if (VideoDownloader.LooksLikeMedia(current) && !VideoDownloader.IsSegmentUrl(current)) { return new VideoCandidate(current, null, pageUrl); } - // 嗅探结果里最后一个整片地址最接近“正在播的这个”,前面翻页留下的旧地址和单个分片都不取 + // 第二层:DASH 分离轨配对(抖音)—— JS 端已按 videoId 过滤 + if (!string.IsNullOrEmpty(dashVideo) && VideoDownloader.LooksLikeMedia(dashVideo)) + { + return new VideoCandidate(dashVideo, null, RefererFor(dashVideo, pageUrl), + string.IsNullOrEmpty(dashAudio) ? null : dashAudio); + } + + // 第三层:从嗅探结果里找当前视频的地址 List pool = sniffed.Concat(SnapshotSniffedUrls()) .Where(VideoDownloader.LooksLikeMedia) + .Where(u => !VideoDownloader.IsDashAudioOnly(u)) // 纯音频轨不能单独当视频下 .Distinct() .ToList(); - int index = pool.FindLastIndex(u => !VideoDownloader.IsSegmentUrl(u)); + int index; + if (!string.IsNullOrEmpty(videoId)) + { + // 有视频 ID 时,优先找 URL 里包含该 ID 的整片地址 + index = pool.FindLastIndex(u => !VideoDownloader.IsSegmentUrl(u) && u.Contains(videoId)); + } + else + { + // 没有 ID 就退回最后一个整片地址 + index = pool.FindLastIndex(u => !VideoDownloader.IsSegmentUrl(u)); + } if (index >= 0) { string url = pool[index]; - return new VideoCandidate(url, null, RefererFor(url, pageUrl)); + // 看看能不能从嗅探记录里配到同组音频轨 + string? pairedAudio = FindPairedAudio(url, videoId); + return new VideoCandidate(url, null, RefererFor(url, pageUrl), pairedAudio); } string? text = sniffedPlaylists.Concat(ReadBlobPlaylists(grabResultJson)) @@ -566,6 +594,34 @@ namespace WinFormsApp3 return text == null ? null : new VideoCandidate(pageUrl, text, pageUrl); } + /// + /// 从已嗅探地址里找和视频轨同组的 DASH 音频轨。 + /// 有 videoId 时优先选 URL 里包含该 ID 的音频轨,否则取最后一条 media-audio。 + /// + private string? FindPairedAudio(string videoUrl, string videoId = "") + { + lock (sniffedUrls) + { + // 有视频 ID 时,优先找同 ID 的音频轨 + if (!string.IsNullOrEmpty(videoId)) + { + for (int i = sniffedUrls.Count - 1; i >= 0; i--) + { + if (VideoDownloader.IsDashAudioOnly(sniffedUrls[i]) && + sniffedUrls[i].Contains(videoId)) + return sniffedUrls[i]; + } + } + // 退回:从后往前找最后一条音频轨 + for (int i = sniffedUrls.Count - 1; i >= 0; i--) + { + if (VideoDownloader.IsDashAudioOnly(sniffedUrls[i])) + return sniffedUrls[i]; + } + } + return null; + } + private static List ReadBlobPlaylists(string grabResultJson) { List result = new List(); @@ -634,9 +690,19 @@ namespace WinFormsApp3 try { - DownloadResult result = candidate.PlaylistText != null - ? await downloader.DownloadFromPlaylistTextAsync(candidate.PlaylistText, candidate.Url, filePath, progress, downloadCts.Token) - : await downloader.DownloadAsync(candidate.Url, filePath, progress, downloadCts.Token); + DownloadResult result; + if (candidate.PlaylistText != null) + { + result = await downloader.DownloadFromPlaylistTextAsync(candidate.PlaylistText, candidate.Url, filePath, progress, downloadCts.Token); + } + else if (!string.IsNullOrEmpty(candidate.AudioUrl)) + { + result = await downloader.DownloadDashPairAsync(candidate.Url, candidate.AudioUrl, filePath, progress, downloadCts.Token); + } + else + { + result = await downloader.DownloadAsync(candidate.Url, filePath, progress, downloadCts.Token); + } SetStatus($"已下载 {Path.GetFileName(result.FilePath)}({VideoDownloader.FormatSize(result.Bytes)})"); diff --git a/uniapp/Google Chrome/Google Chrome.csproj b/uniapp/Google Chrome/Google Chrome.csproj index b48ee02..5830b8d 100644 --- a/uniapp/Google Chrome/Google Chrome.csproj +++ b/uniapp/Google Chrome/Google Chrome.csproj @@ -8,6 +8,12 @@ enable + + + + + + @@ -27,6 +33,9 @@ PreserveNewest + + PreserveNewest + @@ -44,4 +53,4 @@ - \ No newline at end of file + diff --git a/uniapp/Google Chrome/Program.cs b/uniapp/Google Chrome/Program.cs index c36c9ef..b2a90ba 100644 --- a/uniapp/Google Chrome/Program.cs +++ b/uniapp/Google Chrome/Program.cs @@ -19,6 +19,13 @@ namespace WinFormsApp3 [STAThread] // ← 加上这个,99% 立刻解决你的错误 static int Main(string[] args) { + // 全局未处理异常兜底:写日志而不是直接闪退,放到最前面 + AppDomain.CurrentDomain.UnhandledException += (s, e) => + { + try { File.WriteAllText("crash.log", $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {(e.IsTerminating ? "FATAL" : "NON-FATAL")}\n{e.ExceptionObject}"); } + catch { } + }; + // 命令行下载:WinFormsApp3.exe --download <视频地址> [--out 保存路径] [--referer 页面地址] if (args.Length > 0 && string.Equals(args[0], "--download", StringComparison.OrdinalIgnoreCase)) { @@ -27,10 +34,15 @@ namespace WinFormsApp3 } ApplicationConfiguration.Initialize(); - - // 这里不能提前 form.Show():那会让 Form.Load 在消息循环开始之前执行, - // WebView2 在没有消息泵时初始化会报 RPC_E_CHANGED_MODE,交给 Run 自己显示窗体 - Application.Run(new Form1()); + try + { + Application.Run(new Form1()); + } + catch (Exception ex) + { + File.WriteAllText("crash.log", $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Application.Run\n{ex}"); + throw; + } return 0; } diff --git a/uniapp/Google Chrome/Resources/video_grab.js b/uniapp/Google Chrome/Resources/video_grab.js index 1c9505a..a9746ee 100644 --- a/uniapp/Google Chrome/Resources/video_grab.js +++ b/uniapp/Google Chrome/Resources/video_grab.js @@ -1,13 +1,70 @@ -// 点击"下载视频"时执行:读取当前