diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager.cs new file mode 100644 index 0000000..6b065db --- /dev/null +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager.cs @@ -0,0 +1,595 @@ +using Mp4SubtitleParser; +using N_m3u8DL_RE.Column; +using N_m3u8DL_RE.Common.Entity; +using N_m3u8DL_RE.Common.Enum; +using N_m3u8DL_RE.Common.Log; +using N_m3u8DL_RE.Common.Resource; +using N_m3u8DL_RE.Common.Util; +using N_m3u8DL_RE.Config; +using N_m3u8DL_RE.Downloader; +using N_m3u8DL_RE.Entity; +using N_m3u8DL_RE.Parser; +using N_m3u8DL_RE.Util; +using NiL.JS; +using NiL.JS.BaseLibrary; +using Spectre.Console; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using static System.Net.Mime.MediaTypeNames; + +namespace N_m3u8DL_RE.DownloadManager +{ + internal class SimpleLiveRecordManager + { + IDownloader Downloader; + DownloaderConfig DownloaderConfig; + StreamExtractor StreamExtractor; + List SelectedSteams; + DateTime NowDateTime; + bool STOP_FLAG = false; + + public SimpleLiveRecordManager(DownloaderConfig downloaderConfig, List selectedSteams, StreamExtractor streamExtractor) + { + this.DownloaderConfig = downloaderConfig; + Downloader = new SimpleDownloader(DownloaderConfig); + NowDateTime = DateTime.Now; + StreamExtractor = streamExtractor; + SelectedSteams = selectedSteams; + } + + private string? ReadInit(byte[] data) + { + var info = MP4InitUtil.ReadInit(data); + if (info.Scheme != null) Logger.WarnMarkUp($"[grey]Type: {info.Scheme}[/]"); + if (info.PSSH != null) Logger.WarnMarkUp($"[grey]PSSH(WV): {info.PSSH}[/]"); + if (info.KID != null) Logger.WarnMarkUp($"[grey]KID: {info.KID}[/]"); + return info.KID; + } + + private string? ReadInit(string output) + { + using (var fs = File.OpenRead(output)) + { + var header = new byte[4096]; //4KB + fs.Read(header); + return ReadInit(header); + } + } + + //从文件读取KEY + private async Task SearchKeyAsync(string? currentKID) + { + var _key = await MP4DecryptUtil.SearchKeyFromFile(DownloaderConfig.MyOptions.KeyTextFile, currentKID); + if (_key != null) + { + if (DownloaderConfig.MyOptions.Keys == null) + DownloaderConfig.MyOptions.Keys = new string[] { _key }; + else + DownloaderConfig.MyOptions.Keys = DownloaderConfig.MyOptions.Keys.Concat(new string[] { _key }).ToArray(); + } + } + + private void ChangeSpecInfo(StreamSpec streamSpec, List mediainfos, ref bool useAACFilter) + { + if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison == true)) + { + DownloaderConfig.MyOptions.BinaryMerge = true; + Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge2}[/]"); + } + + if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison == true)) + { + DownloaderConfig.MyOptions.MuxAfterDone = false; + Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]"); + } + + if (mediainfos.Where(m => m.Type == "Audio").All(m => m.BaseInfo!.Contains("aac"))) + { + useAACFilter = true; + } + + if (mediainfos.All(m => m.Type == "Audio")) + { + streamSpec.MediaType = MediaType.AUDIO; + } + else if (mediainfos.All(m => m.Type == "Subtitle")) + { + streamSpec.MediaType = MediaType.SUBTITLES; + if (streamSpec.Extension == null || streamSpec.Extension == "ts") + streamSpec.Extension = "vtt"; + } + } + + private async Task RecordStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer, ConcurrentDictionary dic, ConcurrentDictionary recodingDurDic) + { + //mp4decrypt + var mp4decrypt = DownloaderConfig.MyOptions.DecryptionBinaryPath!; + var mp4InitFile = ""; + var currentKID = ""; + var readInfo = false; //是否读取过 + bool useAACFilter = false; //ffmpeg合并flag + bool initDownloaded = false; //是否下载过init文件 + bool hls = StreamExtractor.ExtractorType == ExtractorType.HLS; + int waitSec = 0; //刷新间隔 + ConcurrentDictionary FileDic = new(); + List mediaInfos = new(); + FileStream? fileOutputStream = null; + task.MaxValue = 0; + task.StartTask(); + + var name = streamSpec.ToShortString(); + var type = streamSpec.MediaType ?? Common.Enum.MediaType.VIDEO; + var dirName = $"{DownloaderConfig.MyOptions.SaveName ?? NowDateTime.ToString("yyyy-MM-dd_HH-mm-ss")}_{task.Id}_{streamSpec.GroupId}_{streamSpec.Codecs}_{streamSpec.Bandwidth}_{streamSpec.Language}"; + var tmpDir = Path.Combine(DownloaderConfig.MyOptions.TmpDir ?? Environment.CurrentDirectory, dirName); + var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; + var saveName = DownloaderConfig.MyOptions.SaveName != null ? $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.') : dirName; + var headers = DownloaderConfig.Headers; + + Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}"); + + //创建文件夹 + if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir); + if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir); + + while (!STOP_FLAG) + { + //过滤不需要下载的片段 + FilterMediaSegments(streamSpec, dic[name]); + + //只下载没下过的片段 + var segments = streamSpec.Playlist!.MediaParts[0].MediaSegments.AsEnumerable(); + task.MaxValue += segments.Count(); + + //设置等待时间 + if (waitSec == 0) + { + waitSec = (int)segments.Sum(s => s.Duration) / 2; + Logger.WarnMarkUp($"set refresh interval to {waitSec} seconds"); + } + + //下载init + if (!initDownloaded && streamSpec.Playlist?.MediaInit != null) + { + task.MaxValue += 1; + //对于fMP4,自动开启二进制合并 + if (!DownloaderConfig.MyOptions.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES) + { + DownloaderConfig.MyOptions.BinaryMerge = true; + Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge}[/]"); + } + + var path = Path.Combine(tmpDir, "_init.mp4.tmp"); + var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers); + FileDic[streamSpec.Playlist.MediaInit] = result; + if (result == null || !result.Success) + { + throw new Exception("Download init file failed!"); + } + mp4InitFile = result.ActualFilePath; + task.Increment(1); + + //读取mp4信息 + if (result != null && result.Success) + { + var data = File.ReadAllBytes(result.ActualFilePath); + currentKID = ReadInit(data); + //从文件读取KEY + await SearchKeyAsync(currentKID); + //实时解密 + if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID)) + { + var enc = result.ActualFilePath; + var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID); + if (dResult) + { + FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec; + } + } + //ffmpeg读取信息 + if (!readInfo) + { + Logger.WarnMarkUp(ResString.readingInfo); + mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result.ActualFilePath); + mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); + ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter); + readInfo = true; + } + initDownloaded = true; + } + } + + //计算填零个数 + var pad = "0".PadLeft(segments.Count().ToString().Length, '0'); + + //下载第一个分片 + if (!readInfo) + { + var seg = segments.First(); + segments = segments.Skip(1); + //记录最新url + dic[name] = seg.Url; + //获取文件名 + var filename = hls ? seg.Index.ToString(pad) : OtherUtil.GetFileNameFromInput(seg.Url, false); + var index = seg.Index; + var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp"); + var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); + FileDic[seg] = result; + if (result == null || !result.Success) + { + throw new Exception("Download first segment failed!"); + } + task.Increment(1); + if (result != null && result.Success) + { + //读取init信息 + if (string.IsNullOrEmpty(currentKID)) + { + currentKID = ReadInit(result.ActualFilePath); + } + //从文件读取KEY + await SearchKeyAsync(currentKID); + //实时解密 + if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID)) + { + var enc = result.ActualFilePath; + var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile); + if (dResult) + { + File.Delete(enc); + result.ActualFilePath = dec; + } + } + //ffmpeg读取信息 + Logger.WarnMarkUp(ResString.readingInfo); + mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath); + mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); + ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter); + readInfo = true; + } + } + + //开始下载 + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = DownloaderConfig.MyOptions.ThreadCount + }; + await Parallel.ForEachAsync(segments, options, async (seg, _) => + { + //获取文件名 + var filename = hls ? seg.Index.ToString(pad) : OtherUtil.GetFileNameFromInput(seg.Url, false); + var index = seg.Index; + var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp"); + var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); + FileDic[seg] = result; + task.Increment(1); + //实时解密 + if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && result != null && result.Success && !string.IsNullOrEmpty(currentKID)) + { + var enc = result.ActualFilePath; + var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile); + if (dResult) + { + File.Delete(enc); + result.ActualFilePath = dec; + } + } + }); + + //记录最新url + if (segments.Any()) + dic[name] = segments.Last().Url; + + recodingDurDic[task.Id] += (int)segments.Sum(s => s.Duration); + + //自动修复VTT raw字幕 + if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES + && streamSpec.Extension != null && streamSpec.Extension.Contains("vtt")) + { + Logger.WarnMarkUp(ResString.fixingVTT); + //排序字幕并修正时间戳 + bool first = true; + var finalVtt = new WebVttSub(); + var keys = FileDic.Keys.OrderBy(k => k.Index); + foreach (var seg in keys) + { + var vttContent = File.ReadAllText(FileDic[seg]!.ActualFilePath); + var vtt = WebVttSub.Parse(vttContent); + //手动计算MPEGTS + if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) + { + vtt.MpegtsTimestamp = 90 * (long)(seg.Duration * 1000) * seg.Index; + } + if (first) + { + finalVtt = vtt; + first = false; + } + else + { + finalVtt.AddCuesFromOne(vtt); + } + } + //写出字幕 + var files = FileDic.Values.Where(v => !Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).Select(v => v!.ActualFilePath).OrderBy(s => s).ToArray(); + foreach (var item in files) File.Delete(item); + FileDic.Clear(); + var path = Path.Combine(tmpDir, Path.GetFileNameWithoutExtension(files.Last()) + ".vtt"); + var subContentFixed = finalVtt.ToString(); + await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false)); + FileDic[keys.First()] = new DownloadResult() + { + ActualContentLength = subContentFixed.Length, + ActualFilePath = path + }; + } + + //自动修复VTT mp4字幕 + if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES + && streamSpec.Codecs != "stpp" && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s")) + { + var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); + var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); + var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes); + if (sawVtt) + { + Logger.WarnMarkUp(ResString.fixingVTTmp4); + var mp4s = FileDic.Values.Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".m4s")).OrderBy(s => s).ToArray(); + var finalVtt = MP4VttUtil.ExtractSub(mp4s, timescale); + //写出字幕 + var firstKey = FileDic.Keys.First(); + foreach (var item in mp4s) File.Delete(item); + FileDic.Clear(); + var path = Path.Combine(tmpDir, Path.GetFileNameWithoutExtension(mp4s.Last()) + ".vtt"); + var subContentFixed = finalVtt.ToString(); + await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false)); + FileDic[firstKey] = new DownloadResult() + { + ActualContentLength = subContentFixed.Length, + ActualFilePath = path + }; + } + } + + //自动修复TTML raw字幕 + if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES + && streamSpec.Extension != null && streamSpec.Extension.Contains("ttml")) + { + Logger.WarnMarkUp(ResString.fixingTTML); + var mp4s = FileDic.Values.Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".ttml")).OrderBy(s => s).ToArray(); + var finalVtt = MP4TtmlUtil.ExtractFromTTMLs(mp4s, 0); + //写出字幕 + var firstKey = FileDic.Keys.First(); + foreach (var item in mp4s) File.Delete(item); + FileDic.Clear(); + var path = Path.Combine(tmpDir, Path.GetFileNameWithoutExtension(mp4s.Last()) + ".vtt"); + var subContentFixed = finalVtt.ToString(); + await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false)); + FileDic[firstKey] = new DownloadResult() + { + ActualContentLength = subContentFixed.Length, + ActualFilePath = path + }; + } + + //自动修复TTML mp4字幕 + if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES + && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s") + && streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp")) + { + Logger.WarnMarkUp(ResString.fixingTTMLmp4); + //sawTtml暂时不判断 + //var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); + //var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); + //var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes); + var mp4s = FileDic.Values.Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".m4s")).OrderBy(s => s).ToArray(); + var finalVtt = MP4TtmlUtil.ExtractFromMp4s(mp4s, 0); + //写出字幕 + var firstKey = FileDic.Keys.First(); + foreach (var item in mp4s) File.Delete(item); + FileDic.Clear(); + var path = Path.Combine(tmpDir, Path.GetFileNameWithoutExtension(mp4s.Last()) + ".vtt"); + var subContentFixed = finalVtt.ToString(); + await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false)); + FileDic[firstKey] = new DownloadResult() + { + ActualContentLength = subContentFixed.Length, + ActualFilePath = path + }; + } + + //合并逻辑 + if (DownloaderConfig.MyOptions.LiveRealTimeMerge) + { + //合并 + var outputExt = "." + streamSpec.Extension; + if (streamSpec.Extension == null) outputExt = ".ts"; + else if (streamSpec.MediaType == MediaType.AUDIO && streamSpec.Extension == "m4s") outputExt = ".m4a"; + else if (streamSpec.MediaType != MediaType.SUBTITLES && streamSpec.Extension == "m4s") outputExt = ".mp4"; + else if (streamSpec.MediaType == MediaType.SUBTITLES) outputExt = ".vtt"; + + var output = Path.Combine(saveDir, saveName + outputExt); + + //移除无效片段 + var badKeys = FileDic.Where(i => i.Value == null).Select(i => i.Key); + foreach (var badKey in badKeys) + { + FileDic!.Remove(badKey, out _); + } + + //检测目标文件是否存在 + while (!readInfo && File.Exists(output)) + { + Logger.WarnMarkUp($"{Path.GetFileName(output)} => {Path.GetFileName(output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(output)))}"); + } + + //设置输出流 + if (fileOutputStream == null) + { + fileOutputStream = new FileStream(output, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + } + + if (streamSpec.MediaType != MediaType.SUBTITLES) + { + var initResult = streamSpec.Playlist!.MediaInit != null ? FileDic[streamSpec.Playlist!.MediaInit!]! : null; + var files = FileDic.Where(f => f.Key != streamSpec.Playlist!.MediaInit).Select(f => f.Value).Select(v => v!.ActualFilePath).OrderBy(s => s).ToArray(); + if (initResult != null && mp4InitFile != "") + { + //shaka实时解密不需要init文件用于合并,mp4decrpyt需要 + if (!DownloaderConfig.MyOptions.UseShakaPackager) + { + files = new string[] { initResult.ActualFilePath }.Concat(files).ToArray(); + } + } + foreach (var inputFilePath in files) + { + using (var inputStream = File.OpenRead(inputFilePath)) + { + inputStream.CopyTo(fileOutputStream); + } + if (!DownloaderConfig.MyOptions.LiveKeepSegments && !Path.GetFileName(inputFilePath).StartsWith("_init")) + { + File.Delete(inputFilePath); + } + } + FileDic.Clear(); + if (initResult != null) + { + FileDic[streamSpec.Playlist!.MediaInit!] = initResult; + } + } + else + { + var files = FileDic.Select(f => f.Value).Select(v => v!.ActualFilePath).OrderBy(s => s).ToArray(); + foreach (var inputFilePath in files) + { + using (var inputStream = File.OpenRead(inputFilePath)) + { + inputStream.CopyTo(fileOutputStream); + } + if (!DownloaderConfig.MyOptions.LiveKeepSegments && !Path.GetFileName(inputFilePath).StartsWith("_init")) + { + File.Delete(inputFilePath); + } + } + fileOutputStream.Flush(); + } + } + + //检测时长限制 + if (recodingDurDic.All(d => d.Value >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds)) + { + Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]"); + STOP_FLAG = true; + } + + if (!STOP_FLAG) + { + //Logger.WarnMarkUp($"wait {waitSec}s"); + await Task.Delay(waitSec * 1000); + //刷新列表 + await StreamExtractor.RefreshPlayListAsync(new List() { streamSpec }); + } + } + + if (fileOutputStream != null) + { + fileOutputStream.Close(); + fileOutputStream.Dispose(); + } + + return true; + } + + private void FilterMediaSegments(StreamSpec streamSpec, string lastUrl) + { + if (string.IsNullOrEmpty(lastUrl)) return; + + var index = streamSpec.Playlist!.MediaParts[0].MediaSegments.FindIndex(s => s.Url == lastUrl); + if (index > -1) + { + streamSpec.Playlist!.MediaParts[0].MediaSegments = streamSpec.Playlist!.MediaParts[0].MediaSegments.Skip(index + 1).ToList(); + } + } + + public async Task StartRecordAsync() + { + var takeLastCount = 15; + ConcurrentDictionary SpeedContainerDic = new(); //速度计算 + ConcurrentDictionary recordingDurDic = new(); //已录制时长 + ConcurrentDictionary lastUrlDic = new(); //上次下载的url + ConcurrentDictionary Results = new(); + //取最后15个分片 + var minIndex = SelectedSteams.Max(s => s.Playlist!.MediaParts.Min(p => p.MediaSegments.Min(s => s.Index))); + foreach (var item in SelectedSteams) + { + foreach (var part in item.Playlist!.MediaParts) + { + part.MediaSegments = part.MediaSegments.Where(s => s.Index >= minIndex).TakeLast(takeLastCount).ToList(); + } + } + //初始化dic + foreach (var item in SelectedSteams) + { + lastUrlDic[item.ToShortString()] = ""; + } + + var progress = AnsiConsole.Progress().AutoClear(true); + + //进度条的列定义 + progress.Columns(new ProgressColumn[] + { + new TaskDescriptionColumn() { Alignment = Justify.Left }, + new RecordingDurationColumn(recordingDurDic), //时长显示 + new RecordingStatusColumn(), + new PercentageColumn(), + new DownloadSpeedColumn(SpeedContainerDic), //速度计算 + new SpinnerColumn(), + }); + + await progress.StartAsync(async ctx => + { + //创建任务 + var dic = SelectedSteams.Select(item => + { + var task = ctx.AddTask(item.ToShortString(), autoStart: false); + SpeedContainerDic[task.Id] = new SpeedContainer(); //速度计算 + recordingDurDic[task.Id] = 0; + return (item, task); + }).ToDictionary(item => item.item, item => item.task); + + DownloaderConfig.MyOptions.ConcurrentDownload = true; + DownloaderConfig.MyOptions.MP4RealTimeDecryption = true; + DownloaderConfig.MyOptions.LiveRecordLimit = DownloaderConfig.MyOptions.LiveRecordLimit ?? TimeSpan.MaxValue; + var limit = DownloaderConfig.MyOptions.LiveRecordLimit; + if (limit != TimeSpan.MaxValue) + Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]"); + //并发下载 + await Parallel.ForEachAsync(dic, async (kp, _) => + { + var task = kp.Value; + var result = await RecordStreamAsync(kp.Key, task, SpeedContainerDic[task.Id], lastUrlDic, recordingDurDic); + Results[kp.Key] = result; + }); + }); + + var success = Results.Values.All(v => v == true); + + //混流 + if (success && DownloaderConfig.MyOptions.MuxAfterDone) + { + Logger.Error("Not supported yet!"); + } + + return success; + } + } +}