This commit is contained in:
owenchen committed 2026-11-06 14:57:55 +08:00
1 parent 90bb20e75f
commit bcf82d0f45
8 files changed
+342 -37

No files matched your search

+137 -1
View File
@@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace WinFormsApp3
@@ -59,6 +60,20 @@ namespace WinFormsApp3
public static bool IsHlsUrl(string url) =>
!string.IsNullOrEmpty(url) && Regex.IsMatch(url, @"\.m3u8(\?|#|$)", RegexOptions.IgnoreCase);
/// <summary>
/// 判断是否是 DASH 纯音频轨地址(抖音 .../media-audio-xxx/...)。
/// 这种地址单独下载出来没有画面,必须和视频轨合并。
/// </summary>
public static bool IsDashAudioOnly(string? url) =>
!string.IsNullOrEmpty(url) && Regex.IsMatch(url, @"/media-audio[/-]", RegexOptions.IgnoreCase);
/// <summary>
/// 判断是否是 DASH 纯视频轨地址(抖音 .../media-video-xxx/...)。
/// 这种地址单独下载出来没有声音,需要配音频轨。
/// </summary>
public static bool IsDashVideoOnly(string? url) =>
!string.IsNullOrEmpty(url) && Regex.IsMatch(url, @"/media-video[/-]", RegexOptions.IgnoreCase);
/// <summary>
/// 判断是否是 HLS 分片(合并播放列表可用,但单独下载没有意义)。
/// 只认分片特有的后缀与编号形式,不能拿路径里的 video / seg 字样当依据,
@@ -177,6 +192,105 @@ namespace WinFormsApp3
return await WriteHlsAsync(client, playlist, filePath, progress, ct);
}
/// <summary>
/// 下载 DASH 分离的视频轨和音频轨,再用 ffmpeg 合并成单个 mp4。
/// 抖音等站点把视频和音频分成两个独立的 mp4 轨道返回,必须合并才能有声有画。
/// </summary>
public async Task<DownloadResult> DownloadDashPairAsync(string videoUrl, string audioUrl, string filePath, IProgress<DownloadProgress>? progress, CancellationToken ct)
{
var ffmpeg = FindFfmpeg();
if (ffmpeg == null)
{
throw new NotSupportedException("下载抖音 DASH 分离视频轨需要 ffmpeg 合并音视频,但未找到 ffmpeg.exe。请把 ffmpeg.exe 放到程序目录下。");
}
using var client = CreateClient();
// 临时文件
var tmpDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tmpDir);
var videoTmp = Path.Combine(tmpDir, "video.mp4");
var audioTmp = Path.Combine(tmpDir, "audio.mp4");
try
{
// 下载视频轨(占总进度 0~50%)
progress?.Report(new DownloadProgress("下载视频轨…", 0));
await DownloadSingleToFileAsync(client, videoUrl, videoTmp, ct,
p => progress?.Report(new DownloadProgress($"下载视频轨 {FormatSize(p)}", null)));
var videoSize = new FileInfo(videoTmp).Length;
// 下载音频轨(占总进度 50~100%)
progress?.Report(new DownloadProgress("下载音频轨…", 0.5));
await DownloadSingleToFileAsync(client, audioUrl, audioTmp, ct,
p => progress?.Report(new DownloadProgress($"下载音频轨 {FormatSize(p)}", null)));
var audioSize = new FileInfo(audioTmp).Length;
// 合并
progress?.Report(new DownloadProgress("合并音视频…", 0.95));
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(filePath))!);
var ok = await MuxDashAsync(ffmpeg, videoTmp, audioTmp, filePath, ct);
if (!ok)
throw new InvalidOperationException("ffmpeg 合并音视频失败");
var finalSize = new FileInfo(filePath).Length;
progress?.Report(new DownloadProgress($"完成 {FormatSize(finalSize)}", 1));
return new DownloadResult(filePath, finalSize, true);
}
finally
{
try { Directory.Delete(tmpDir, true); } catch { /* 临时目录删不掉不影响结果 */ }
}
}
/// <summary>
/// 下载单个文件到指定路径(不做 .part 重命名,内部用)。
/// </summary>
private async Task DownloadSingleToFileAsync(HttpClient client, string url, string filePath, CancellationToken ct, Action<long>? onBytes = null)
{
using var resp = await GetFollowRedirectAsync(client, url, ct);
EnsureVideoResponse(resp);
await using var input = await resp.Content.ReadAsStreamAsync(ct);
await using var output = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 1024 * 64, true);
var buffer = new byte[1024 * 64];
long total = 0;
int read;
while ((read = await input.ReadAsync(buffer, ct)) > 0)
{
await output.WriteAsync(buffer.AsMemory(0, read), ct);
total += read;
onBytes?.Invoke(total);
}
}
/// <summary>
/// 用 ffmpeg 把 DASH 视频轨和音频轨合并成 mp4(直接 copy 不重编码)。
/// </summary>
private static async Task<bool> MuxDashAsync(string ffmpeg, string videoFile, string audioFile, string outputFile, CancellationToken ct)
{
var args = $"-hide_banner -loglevel error -y -i \"{videoFile}\" -i \"{audioFile}\" -c:v copy -c:a aac -strict experimental -movflags +faststart \"{outputFile}\"";
var psi = new ProcessStartInfo
{
FileName = ffmpeg,
Arguments = args,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardError = true,
RedirectStandardOutput = true
};
using var proc = Process.Start(psi);
if (proc == null) return false;
using (ct.Register(() => { try { if (!proc.HasExited) proc.Kill(true); } catch { } }))
{
await proc.WaitForExitAsync(ct);
}
return proc.ExitCode == 0 && File.Exists(outputFile) && new FileInfo(outputFile).Length > 0;
}
private HttpClient CreateClient()
{
var client = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false })
@@ -529,13 +643,35 @@ namespace WinFormsApp3
}
/// <summary>
/// 查找 ffmpeg:先看程序目录,再看 PATH。找到就能把 HLS 转成 mp4,找不到则只合并 ts。
/// 查找 ffmpeg:顺序为 1) ffmpeg.json 配置文件指定的路径;2) 程序目录 ffmpeg.exe;3) PATH。
/// 配置文件格式:{"ffmpegPath": "D:\\NET8\\Exe\\ffmpeg\\bin\\ffmpeg.exe"}
/// </summary>
public static string? FindFfmpeg()
{
// 1. 配置文件指定的路径(优先级最高)
var configPath = Path.Combine(AppContext.BaseDirectory, "ffmpeg.json");
if (File.Exists(configPath))
{
try
{
var json = File.ReadAllText(configPath);
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("ffmpegPath", out var prop) &&
prop.ValueKind == JsonValueKind.String)
{
var configured = prop.GetString();
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured))
return configured;
}
}
catch { /* 配置文件损坏就跳过,继续找别的 */ }
}
// 2. 程序目录
var local = Path.Combine(AppContext.BaseDirectory, "ffmpeg.exe");
if (File.Exists(local)) return local;
// 3. PATH
foreach (var dir in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(Path.PathSeparator))
{
if (string.IsNullOrWhiteSpace(dir)) continue;