fix: 修复flv视频无法解析下载 (#306)

* fix: 修复flv视频无法解析下载
This commit is contained in:
to_mo_to
2025-06-06 19:19:20 +08:00
committed by GitHub
parent 7aa5df0301
commit 6b465381bd
11 changed files with 352 additions and 116 deletions

View File

@@ -34,6 +34,10 @@ public class PlayUrl : BaseModel
[JsonProperty("durl")] public List<PlayUrlDurl> Durl { get; set; }
[JsonProperty("dash")] public PlayUrlDash Dash { get; set; }
[JsonProperty("quality")] public int Quality { get; set; }
[JsonProperty("video_codecid")] public int VideoCodecid { get; set; }
[JsonProperty("support_formats")] public List<PlayUrlSupportFormat> SupportFormats { get; set; }
// high_format
}

View File

@@ -163,4 +163,61 @@ public class FFMpeg
.NotifyOnError(action.Invoke)
.ProcessSynchronously(false);
}
/// <summary>
/// 合并多个FLV视频片段为一个完整视频
/// </summary>
/// <param name="inputFlvs">FLV片段路径列表(按顺序)</param>
/// <param name="outputVideo">输出视频路径</param>
/// <param name="action">进度回调</param>
/// <returns>是否成功</returns>
public bool ConcatVideos(List<string> inputFlvs, string outputVideo, Action<string> action)
{
try
{
if (inputFlvs == null || inputFlvs.Count == 0)
{
return false;
}
// 验证所有输入文件都存在
foreach (var video in inputFlvs)
{
if (!File.Exists(video))
{
action?.Invoke($"文件不存在: {video}");
return false;
}
}
LogManager.Debug(Tag, $"开始合并 {inputFlvs.Count} 个视频到 {outputVideo}");
var listFile = Path.Combine(Path.GetTempPath(), $"flvlist_{DateTime.Now:yyyyMMddHHmmss}.txt");
File.WriteAllLines(listFile, inputFlvs.Select(f => $"file '{f.Replace("'", "'\\''")}'"));
FFMpegArguments
.FromFileInput(listFile, false, options => options
.WithCustomArgument("-f concat -safe 0"))
.OutputToFile(outputVideo, true, options => options
.WithVideoCodec("libx264")
.WithAudioCodec("aac")
.WithCustomArgument("-movflags +faststart")
.WithCustomArgument("-avoid_negative_ts make_zero")
)
.NotifyOnOutput(action.Invoke)
.NotifyOnError(action.Invoke)
.ProcessSynchronously(false);
try { File.Delete(listFile); } catch { }
LogManager.Debug(Tag, "视频合并完成");
return true;
}
catch (Exception ex)
{
LogManager.Error(Tag, ex);
return false;
}
}
}

View File

@@ -149,6 +149,7 @@
<system:String x:Key="Parsing">正在解析……</system:String>
<system:String x:Key="WhileDownloading">下载中……</system:String>
<system:String x:Key="MixedFlow">混流中……</system:String>
<system:String x:Key="ConcatVideos">视频合并中……</system:String>
<system:String x:Key="Pausing">暂停中……</system:String>
<system:String x:Key="Waiting">等待中……</system:String>
<system:String x:Key="DownloadFailed">下载失败</system:String>

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DownKyi.Models
{
public class VideoPlayUrlBasic
{
public List<string> BackupUrl { get; set; }
public string BaseUrl { get; set; }
public int Id { get; set; }
public string Codecs { get; set; }
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
@@ -287,6 +288,8 @@ public class AddToDownloadService
// 如果存在正在下载列表,则跳过,并提示
var isDownloading = false;
foreach (var item in App.DownloadingList)
{
if (item.DownloadBase == null)
@@ -294,9 +297,15 @@ public class AddToDownloadService
continue;
}
if (item.DownloadBase.Cid == page.Cid && item.Resolution.Id == page.VideoQuality.Quality &&
item.AudioCodec.Name == page.AudioQualityFormat &&
item.VideoCodecName == page.VideoQuality.SelectedVideoCodec)
bool f = item.DownloadBase.Cid == page.Cid &&
item.Resolution.Id == page.VideoQuality.Quality &&
item.VideoCodecName == page.VideoQuality.SelectedVideoCodec &&
(
(page.PlayUrl.Dash != null && item.AudioCodec.Name == page.AudioQualityFormat) ||
(page.PlayUrl.Dash == null && page.PlayUrl.Durl != null)
);
if (f)
{
eventAggregator.GetEvent<MessageEvent>()
.Publish($"{page.Name}{DictionaryResource.GetString("TipAlreadyToAddDownloading")}");
@@ -319,8 +328,15 @@ public class AddToDownloadService
continue;
}
if (item.DownloadBase.Cid == page.Cid && item.Resolution.Id == page.VideoQuality.Quality && item.AudioCodec.Name == page.AudioQualityFormat &&
item.VideoCodecName == page.VideoQuality.SelectedVideoCodec)
bool f = item.DownloadBase.Cid == page.Cid &&
item.Resolution.Id == page.VideoQuality.Quality &&
item.VideoCodecName == page.VideoQuality.SelectedVideoCodec &&
(
(page.PlayUrl.Dash != null && item.AudioCodec.Name == page.AudioQualityFormat) ||
(page.PlayUrl.Dash == null && page.PlayUrl.Durl != null)
);
if (f)
{
// eventAggregator.GetEvent<MessageEvent>().Publish($"{page.Name}{DictionaryResource.GetString("TipAlreadyToAddDownloaded")}");
// isDownloaded = true;
@@ -328,30 +344,30 @@ public class AddToDownloadService
switch (repeatDownloadStrategy)
{
case RepeatDownloadStrategy.Ask:
{
var result = ButtonResult.Cancel;
await Dispatcher.UIThread.Invoke(async () =>
{
var param = new DialogParameters
var result = ButtonResult.Cancel;
await Dispatcher.UIThread.Invoke(async () =>
{
var param = new DialogParameters
{
{ "message", $"{item.Name}已下载,是否重新下载" },
};
await dialogService.ShowDialogAsync(ViewAlreadyDownloadedDialogViewModel.Tag, param, buttonResult => { result = buttonResult.Result; });
});
await dialogService.ShowDialogAsync(ViewAlreadyDownloadedDialogViewModel.Tag, param, buttonResult => { result = buttonResult.Result; });
});
if (result == ButtonResult.OK)
{
App.PropertyChangeAsync(() => { App.DownloadedList.Remove(item); });
isDownloaded = false;
}
else
{
isDownloaded = true;
}
if (result == ButtonResult.OK)
{
App.PropertyChangeAsync(() => { App.DownloadedList.Remove(item); });
isDownloaded = false;
}
else
{
isDownloaded = true;
}
break;
}
break;
}
case RepeatDownloadStrategy.ReDownload:
isDownloaded = false;
break;

View File

@@ -64,6 +64,17 @@ public class AriaDownloadService : DownloadService, IDownloadService
/// <param name="downloading"></param>
/// <param name="downloadVideo"></param>
/// <returns></returns>
private string DownloadVideo(DownloadingItem downloading, VideoPlayUrlBasic? downloadVideo)
{
return DownloadVideo(downloading,new PlayUrlDashVideo
{
Id = downloadVideo.Id,
Codecs = downloadVideo.Codecs,
BaseUrl = downloadVideo.BaseUrl,
BackupUrl = downloadVideo.BackupUrl
} );
}
private string DownloadVideo(DownloadingItem downloading, PlayUrlDashVideo? downloadVideo)
{
// 如果为空,说明没有匹配到可下载的音频视频

View File

@@ -10,6 +10,7 @@ using DownKyi.Core.BiliApi.VideoStream.Models;
using DownKyi.Core.Logging;
using DownKyi.Core.Settings;
using DownKyi.Core.Utils;
using DownKyi.Models;
using DownKyi.PrismExtension.Dialog;
using DownKyi.Utils;
using DownKyi.ViewModels.DownloadManager;
@@ -57,6 +58,17 @@ public class BuiltinDownloadService : DownloadService, IDownloadService
return DownloadVideo(downloading, downloadVideo);
}
private string DownloadVideo(DownloadingItem downloading, VideoPlayUrlBasic? downloadVideo)
{
return DownloadVideo(downloading, new PlayUrlDashVideo
{
Id = downloadVideo.Id,
Codecs = downloadVideo.Codecs,
BaseUrl = downloadVideo.BaseUrl,
BackupUrl = downloadVideo.BackupUrl
});
}
/// <summary>
/// 将下载音频和视频的函数中相同代码抽象出来
/// </summary>

View File

@@ -58,6 +58,17 @@ public class CustomAriaDownloadService : DownloadService, IDownloadService
return DownloadVideo(downloading, downloadVideo);
}
private string DownloadVideo(DownloadingItem downloading, VideoPlayUrlBasic? downloadVideo)
{
return DownloadVideo(downloading, new PlayUrlDashVideo
{
Id = downloadVideo.Id,
Codecs = downloadVideo.Codecs,
BaseUrl = downloadVideo.BaseUrl,
BackupUrl = downloadVideo.BackupUrl
});
}
/// <summary>
/// 将下载音频和视频的函数中相同代码抽象出来
/// </summary>

View File

@@ -19,6 +19,7 @@ using DownKyi.Models;
using DownKyi.PrismExtension.Dialog;
using DownKyi.Utils;
using DownKyi.ViewModels.DownloadManager;
using ImTools;
using Console = DownKyi.Core.Utils.Debugging.Console;
namespace DownKyi.Services.Download;
@@ -114,7 +115,7 @@ public abstract class DownloadService
return downloadAudio;
}
protected PlayUrlDashVideo? BaseDownloadVideo(DownloadingItem downloading)
protected VideoPlayUrlBasic? BaseDownloadVideo(DownloadingItem downloading)
{
// 更新状态显示
downloading.DownloadStatusTitle = DictionaryResource.GetString("WhileDownloading");
@@ -125,35 +126,37 @@ public abstract class DownloadService
// 下载速度
downloading.SpeedDisplay = string.Empty;
// 如果没有Dash返回null
if (downloading.PlayUrl == null || downloading.PlayUrl.Dash == null)
if (downloading.PlayUrl?.Dash?.Video?.Count > 0)
{
return null;
}
// 如果Video列表没有内容则返回null
if (downloading.PlayUrl.Dash.Video == null)
{
return null;
}
else if (downloading.PlayUrl.Dash.Video.Count == 0)
{
return null;
}
// 根据视频编码匹配
PlayUrlDashVideo? downloadVideo = null;
foreach (var video in downloading.PlayUrl.Dash.Video)
{
var codecs = Constant.GetCodecIds().FirstOrDefault(t => t.Id == video.CodecId);
if (video.Id == downloading.Resolution.Id && codecs?.Name == downloading.VideoCodecName)
foreach (var video in downloading.PlayUrl.Dash.Video)
{
downloadVideo = video;
break;
var codecs = Constant.GetCodecIds().FirstOrDefault(t => t.Id == video.CodecId);
if (video.Id == downloading.Resolution.Id && codecs?.Name == downloading.VideoCodecName)
{
return new()
{
BackupUrl = video.BackupUrl,
Codecs = video.Codecs,
Id = video.Id,
BaseUrl = video.BaseUrl
};
}
}
}
return downloadVideo;
if (downloading?.PlayUrl?.Durl?.Count > 0)
{
var durl = downloading.PlayUrl.Durl.First();
return new()
{
BackupUrl = durl.BackupUrl,
BaseUrl = durl.Url,
Codecs = downloading.PlayUrl.VideoCodecid.GetHashCode().ToString(),
Id = downloading.DownloadBase.Bvid.GetHashCode()
};
}
return null;
}
protected string BaseDownloadCover(DownloadingItem downloading, string coverUrl, string fileName)
@@ -240,7 +243,8 @@ public abstract class DownloadService
return assFile;
}
protected List<string> BaseDownloadSubtitle(DownloadingItem downloading)
{
// 更新状态显示
@@ -330,6 +334,29 @@ public abstract class DownloadService
return finalFile;
}
private string ConcatVideos(DownloadingItem downloading,List<string> videoUids)
{
downloading.DownloadStatusTitle = DictionaryResource.GetString("ConcatVideos");
downloading.DownloadContent = DictionaryResource.GetString("DownloadingVideo");
downloading.DownloadingFileSize = string.Empty;
downloading.SpeedDisplay = string.Empty;
var finalFile = $"{downloading.DownloadBase.FilePath}.mp4";
FFMpeg.Instance.ConcatVideos(videoUids,finalFile,(x)=>{});
if (File.Exists(finalFile))
{
var info = new FileInfo(finalFile);
downloading.FileSize = Format.FormatFileSize(info.Length);
}
else
{
downloading.FileSize = Format.FormatFileSize(0);
}
return finalFile;
}
protected void BaseParse(DownloadingItem downloading)
{
@@ -491,7 +518,7 @@ public abstract class DownloadService
try
{
await Task.Run(() =>
await Task.Run(async () =>
{
// 初始化
downloading.DownloadStatusTitle = string.Empty;
@@ -504,54 +531,141 @@ public abstract class DownloadService
// 暂停
Pause(downloading);
string? audioUid = null;
// 如果需要下载音频
if (downloading.DownloadBase.NeedDownloadContent["downloadAudio"])
var isMediaSuccess = true;
if (downloading.PlayUrl.Dash != null)
{
//audioUid = DownloadAudio(downloading);
for (var i = 0; i < retry; i++)
string? audioUid = null;
string? videoUid = null;
// 如果需要下载音频
if (downloading.DownloadBase.NeedDownloadContent["downloadAudio"])
{
audioUid = DownloadAudio(downloading);
if (audioUid != null && audioUid != nullMark)
for (var i = 0; i < retry; i++)
{
break;
audioUid = DownloadAudio(downloading);
if (audioUid != null && audioUid != nullMark)
{
break;
}
}
}
}
if (audioUid == nullMark)
{
DownloadFailed(downloading);
return;
}
// 暂停
Pause(downloading);
string? videoUid = null;
// 如果需要下载视频
if (downloading.DownloadBase.NeedDownloadContent["downloadVideo"])
{
//videoUid = DownloadVideo(downloading);
for (var i = 0; i < retry; i++)
if (audioUid == nullMark)
{
videoUid = DownloadVideo(downloading);
if (videoUid != null && videoUid != nullMark)
DownloadFailed(downloading);
return;
}
Pause(downloading);
// 如果需要下载视频
if (downloading.DownloadBase.NeedDownloadContent["downloadVideo"])
{
//videoUid = DownloadVideo(downloading);
for (var i = 0; i < retry; i++)
{
break;
videoUid = DownloadVideo(downloading);
if (videoUid != null && videoUid != nullMark)
{
break;
}
}
}
}
if (videoUid == nullMark)
if (videoUid == nullMark)
{
DownloadFailed(downloading);
return;
}
Pause(downloading);
// 混流
var outputMedia = string.Empty;
if (downloading.DownloadBase.NeedDownloadContent["downloadAudio"] ||
downloading.DownloadBase.NeedDownloadContent["downloadVideo"])
{
outputMedia = MixedFlow(downloading, audioUid, videoUid);
}
// 检测音频、视频是否下载成功
if (downloading.DownloadBase.NeedDownloadContent["downloadAudio"] ||
downloading.DownloadBase.NeedDownloadContent["downloadVideo"])
{
// 只有下载音频不下载视频时才输出aac
// 只要下载视频就输出mp4
// 成功
isMediaSuccess = File.Exists(outputMedia);
}
}
else if(downloading.PlayUrl.Durl != null)
{
DownloadFailed(downloading);
return;
if (downloading.DownloadBase.NeedDownloadContent["downloadAudio"] ||
downloading.DownloadBase.NeedDownloadContent["downloadVideo"] )
{
var durls = downloading.PlayUrl.Durl.ToList();
var downloadStatus = durls
.Select((durl, index) => new { Durl = durl, Index = index })
.ToDictionary(x => x.Index, x => new { Durl = x.Durl, Result = string.Empty });
for (int i = 0; i < durls.Count; i++)
{
downloading.PlayUrl.Durl = new List<PlayUrlDurl> { durls[i] };
var result = DownloadVideo(downloading);
downloadStatus[i] = new { Durl = durls[i], Result = result ?? nullMark };
}
int retryCount = 0;
while (retryCount < retry && downloadStatus.Values
.Any(x => x.Result == nullMark))
{
var toRetry = downloadStatus
.Where(x => retryCount == 0 || x.Value.Result == nullMark)
.ToList();
foreach (var item in toRetry)
{
downloading.PlayUrl.Durl = new List<PlayUrlDurl> { item.Value.Durl };
var result = DownloadVideo(downloading);
downloadStatus[item.Key] = new { item.Value.Durl, Result = result };
}
retryCount++;
await Task.Delay(1000);
}
if(downloadStatus.Values.Any(x => x.Result == nullMark))
{
DownloadFailed(downloading);
return;
}
Pause(downloading);
if (durls.Count > 1)
{
var output = ConcatVideos(downloading,downloadStatus.Values
.Select(x => x.Result).ToList());
isMediaSuccess = File.Exists(output);
}
else
{
var outputMedia = MixedFlow(downloading, null, downloadStatus.First().Value.Result);
isMediaSuccess = File.Exists(outputMedia);
}
}
if(downloading.DownloadBase.NeedDownloadContent["downloadAudio"] &&
!downloading.DownloadBase.NeedDownloadContent["downloadVideo"])
{
//音频分离?
}
Pause(downloading);
}
// 暂停
Pause(downloading);
string? outputDanmaku = null;
// 如果需要下载弹幕
if (downloading.DownloadBase.NeedDownloadContent["downloadDanmaku"])
@@ -591,13 +705,6 @@ public abstract class DownloadService
// 暂停
Pause(downloading);
// 混流
var outputMedia = string.Empty;
if (downloading.DownloadBase.NeedDownloadContent["downloadAudio"] || downloading.DownloadBase.NeedDownloadContent["downloadVideo"])
{
outputMedia = MixedFlow(downloading, audioUid, videoUid);
}
// 这里本来只有IsExist没有pause不知道怎么处理
// 是否存在
//isExist = IsExist(downloading);
@@ -606,16 +713,6 @@ public abstract class DownloadService
// return;
//}
// 检测音频、视频是否下载成功
var isMediaSuccess = true;
if (downloading.DownloadBase.NeedDownloadContent["downloadAudio"] || downloading.DownloadBase.NeedDownloadContent["downloadVideo"])
{
// 只有下载音频不下载视频时才输出aac
// 只要下载视频就输出mp4
// 成功
isMediaSuccess = File.Exists(outputMedia);
}
// 检测弹幕是否下载成功
var isDanmakuSuccess = true;
if (downloading.DownloadBase.NeedDownloadContent["downloadDanmaku"])
@@ -827,7 +924,7 @@ public abstract class DownloadService
workTask = Task.Run(DoWork);
}
#region
public abstract void Parse(DownloadingItem downloading);

View File

@@ -8,6 +8,8 @@ public interface IDownloadService
void Parse(DownloadingItem downloading);
string DownloadAudio(DownloadingItem downloading);
string DownloadVideo(DownloadingItem downloading);
string DownloadDanmaku(DownloadingItem downloading);
List<string> DownloadSubtitle(DownloadingItem downloading);
string DownloadCover(DownloadingItem downloading, string coverUrl, string fileName);

View File

@@ -51,19 +51,6 @@ internal static class Utils
}
}
if (playUrl.Durl != null)
{
// 音质
// 画质
// 视频编码
// 时长
return;
}
if (playUrl.Dash != null)
{
// 如果video列表或者audio列表没有内容则返回false
@@ -96,6 +83,26 @@ internal static class Utils
return;
}
if (playUrl.Durl?.Count > 0)
{
var codeIds = Constant.GetCodecIds();
var qns = Constant.GetResolutions();
var quality = new VideoQuality
{
Quality = playUrl.Quality,
QualityFormat = qns.First(x => x.Id == playUrl.Quality).Name,
VideoCodecList = new(codeIds.Where(x => x.Id == playUrl.VideoCodecid)
.Select(x => x.Name).ToList()),
SelectedVideoCodec = codeIds.First(x => x.Id == playUrl.VideoCodecid).Name
};
page.VideoQualityList = new List<VideoQuality> { quality };
page.VideoQuality = page.VideoQualityList[0];
page.Duration = Format.FormatDuration(playUrl.Durl.Select(x => x.Length).Sum() / 1000);
return;
}
}
/// <summary>