diff --git a/src/N_m3u8DL-RE.Common/Entity/WebVttSub.cs b/src/N_m3u8DL-RE.Common/Entity/WebVttSub.cs index 51db7ad..42283c0 100644 --- a/src/N_m3u8DL-RE.Common/Entity/WebVttSub.cs +++ b/src/N_m3u8DL-RE.Common/Entity/WebVttSub.cs @@ -164,6 +164,11 @@ namespace N_m3u8DL_RE.Common.Entity } } + private IEnumerable GetCues() + { + return this.Cues.Where(c => !string.IsNullOrEmpty(c.Payload)); + } + private static TimeSpan ConvertToTS(string str) { var ms = Convert.ToInt32(str.Split('.').Last()); @@ -180,7 +185,7 @@ namespace N_m3u8DL_RE.Common.Entity public override string ToString() { StringBuilder sb = new StringBuilder(); - foreach (var c in this.Cues) + foreach (var c in GetCues()) //输出时去除空串 { sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\.fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\.fff") + " " + c.Settings); sb.AppendLine(c.Payload); @@ -190,9 +195,32 @@ namespace N_m3u8DL_RE.Common.Entity return sb.ToString(); } - public string ToStringWithHeader() + public string ToVtt() { return "WEBVTT" + Environment.NewLine + Environment.NewLine + ToString(); } + + public string ToSrt() + { + StringBuilder sb = new StringBuilder(); + int index = 1; + foreach (var c in GetCues()) + { + sb.AppendLine($"{index++}"); + sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\,fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\,fff")); + sb.AppendLine(c.Payload); + sb.AppendLine(); + } + sb.AppendLine(); + + var srt = sb.ToString(); + + if (string.IsNullOrEmpty(srt.Trim())) + { + srt = "1\r\n00:00:00,000 --> 00:00:01,000"; //空字幕 + } + + return srt; + } } } diff --git a/src/N_m3u8DL-RE.Parser/Extractor/MSSExtractor.cs b/src/N_m3u8DL-RE.Parser/Extractor/MSSExtractor.cs index 2b9c040..29b81a6 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/MSSExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/MSSExtractor.cs @@ -62,10 +62,10 @@ namespace N_m3u8DL_RE.Parser.Extractor //选中第一个SmoothStreamingMedia节点 var ssmElement = xmlDocument.Elements().First(e => e.Name.LocalName == "SmoothStreamingMedia"); - bool isLive = Convert.ToBoolean(ssmElement.Attribute("IsLive")?.Value ?? "FALSE"); var timeScaleStr = ssmElement.Attribute("TimeScale")?.Value ?? "10000000"; var durationStr = ssmElement.Attribute("Duration")?.Value; var isLiveStr = ssmElement.Attribute("IsLive")?.Value; + bool isLive = Convert.ToBoolean(isLiveStr ?? "FALSE"); var isProtection = false; var protectionSystemId = ""; @@ -121,6 +121,7 @@ namespace N_m3u8DL_RE.Parser.Extractor var channels = qualityLevel.Attribute("Channels")?.Value; StreamSpec streamSpec = new(); + streamSpec.PublishTime = DateTime.Now; //发布时间默认现在 streamSpec.Extension = "m4s"; streamSpec.OriginalUrl = ParserConfig.OriginalUrl; streamSpec.PeriodId = indexStr; diff --git a/src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs b/src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs index e23f8f1..936f48d 100644 --- a/src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs +++ b/src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs @@ -122,6 +122,11 @@ namespace Mp4SubtitleParser return MultiElementsFixRegex().Matches(xml).Select(m => m.Value).ToList(); } + public static WebVttSub ExtractFromMp4(string item, long segTimeMs, long baseTimestamp = 0L) + { + return ExtractFromMp4s(new string[] { item }, segTimeMs, baseTimestamp); + } + public static WebVttSub ExtractFromMp4s(IEnumerable items, long segTimeMs, long baseTimestamp = 0L) { //read ttmls @@ -163,6 +168,11 @@ namespace Mp4SubtitleParser return ExtractSub(xmls, baseTimestamp); } + public static WebVttSub ExtractFromTTML(string item, long segTimeMs, long baseTimestamp = 0L) + { + return ExtractFromTTMLs(new string[] { item }, segTimeMs, baseTimestamp); + } + public static WebVttSub ExtractFromTTMLs(IEnumerable items, long segTimeMs, long baseTimestamp = 0L) { //read ttmls diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs index 55146b8..61d5ecd 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs @@ -360,12 +360,12 @@ namespace N_m3u8DL_RE.DownloadManager FileDic.Clear(); var index = 0; var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); - var subContentFixed = finalVtt.ToStringWithHeader(); + var subContentFixed = finalVtt.ToVtt(); //转换字幕格式 if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) { path = Path.ChangeExtension(path, ".srt"); - subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); + subContentFixed = finalVtt.ToSrt(); } await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); FileDic[keys.First()] = new DownloadResult() @@ -394,12 +394,12 @@ namespace N_m3u8DL_RE.DownloadManager FileDic.Clear(); var index = 0; var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); - var subContentFixed = finalVtt.ToStringWithHeader(); + var subContentFixed = finalVtt.ToVtt(); //转换字幕格式 if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) { path = Path.ChangeExtension(path, ".srt"); - subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); + subContentFixed = finalVtt.ToSrt(); } await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); FileDic[firstKey] = new DownloadResult() @@ -420,7 +420,7 @@ namespace N_m3u8DL_RE.DownloadManager var keys = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Key); foreach (var seg in keys) { - var vtt = MP4TtmlUtil.ExtractFromTTMLs(new string[] { FileDic[seg]!.ActualFilePath }, 0); + var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0); //手动计算MPEGTS if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) { @@ -452,12 +452,12 @@ namespace N_m3u8DL_RE.DownloadManager FileDic.Clear(); var index = 0; var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); - var subContentFixed = finalVtt.ToStringWithHeader(); + var subContentFixed = finalVtt.ToVtt(); //转换字幕格式 if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) { path = Path.ChangeExtension(path, ".srt"); - subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); + subContentFixed = finalVtt.ToSrt(); } await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); FileDic[firstKey] = new DownloadResult() @@ -482,7 +482,7 @@ namespace N_m3u8DL_RE.DownloadManager var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key); foreach (var seg in keys) { - var vtt = MP4TtmlUtil.ExtractFromMp4s(new string[] { FileDic[seg]!.ActualFilePath }, 0); + var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0); //手动计算MPEGTS if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) { @@ -515,12 +515,12 @@ namespace N_m3u8DL_RE.DownloadManager FileDic.Clear(); var index = 0; var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); - var subContentFixed = finalVtt.ToStringWithHeader(); + var subContentFixed = finalVtt.ToVtt(); //转换字幕格式 if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) { path = Path.ChangeExtension(path, ".srt"); - subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); + subContentFixed = finalVtt.ToSrt(); } await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); FileDic[firstKey] = new DownloadResult() diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs index 9d02a10..540ba2b 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs @@ -209,6 +209,7 @@ namespace N_m3u8DL_RE.DownloadManager //TryReceiveAll可以稍微缓解一下 source.TryReceiveAll(out IList>? segmentsList); var segments = segmentsList!.SelectMany(s => s); + var segmentsDuration = segments.Sum(s => s.Duration); Logger.DebugMarkUp(string.Join(",", segments.Select(sss => GetSegmentName(sss, false, false)))); //下载init @@ -355,8 +356,6 @@ namespace N_m3u8DL_RE.DownloadManager } }); - RecordingDurDic[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")) @@ -372,15 +371,8 @@ namespace N_m3u8DL_RE.DownloadManager { vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration); } - if (firstSub) - { - currentVtt = vtt; - firstSub = false; - } - else - { - currentVtt.AddCuesFromOne(vtt); - } + if (firstSub) { currentVtt = vtt; firstSub = false; } + else currentVtt.AddCuesFromOne(vtt); } } @@ -401,8 +393,8 @@ namespace N_m3u8DL_RE.DownloadManager } else { - var finalVtt = MP4VttUtil.ExtractSub(mp4s, timescale); - currentVtt.AddCuesFromOne(finalVtt); + var vtt = MP4VttUtil.ExtractSub(mp4s, timescale); + currentVtt.AddCuesFromOne(vtt); } } } @@ -411,21 +403,40 @@ namespace N_m3u8DL_RE.DownloadManager if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES && streamSpec.Extension != null && streamSpec.Extension.Contains("ttml")) { - var mp4s = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".ttml")).ToArray(); + var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key); if (firstSub) { if (baseTimestamp != 0) { - var total = segments.Sum(s => s.Duration); + var total = segmentsDuration; baseTimestamp -= (long)TimeSpan.FromSeconds(total).TotalMilliseconds; } - currentVtt = MP4TtmlUtil.ExtractFromTTMLs(mp4s, 0, baseTimestamp); + var first = true; + foreach (var seg in keys) + { + var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0, first ? baseTimestamp : 0); + //手动计算MPEGTS + if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) + { + vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration); + } + if (first) { currentVtt = vtt; first = false; } + else currentVtt.AddCuesFromOne(vtt); + } firstSub = false; } else { - var finalVtt = MP4TtmlUtil.ExtractFromTTMLs(mp4s, 0, baseTimestamp); - currentVtt.AddCuesFromOne(finalVtt); + foreach (var seg in keys) + { + var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0); + //手动计算MPEGTS + if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) + { + vtt.MpegtsTimestamp = 90000 * (RecordingDurDic[task.Id] + (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); + } + currentVtt.AddCuesFromOne(vtt); + } } } @@ -438,24 +449,45 @@ namespace N_m3u8DL_RE.DownloadManager //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.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".m4s")).ToArray(); + var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key); if (firstSub) { if (baseTimestamp != 0) { - var total = segments.Sum(s => s.Duration); + var total = segmentsDuration; baseTimestamp -= (long)TimeSpan.FromSeconds(total).TotalMilliseconds; } - currentVtt = MP4TtmlUtil.ExtractFromMp4s(mp4s, 0, baseTimestamp); + var first = true; + foreach (var seg in keys) + { + var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0, first ? baseTimestamp : 0); + //手动计算MPEGTS + if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) + { + vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration); + } + if (first) { currentVtt = vtt; first = false; } + else currentVtt.AddCuesFromOne(vtt); + } firstSub = false; } else { - var finalVtt = MP4TtmlUtil.ExtractFromMp4s(mp4s, 0, baseTimestamp); - currentVtt.AddCuesFromOne(finalVtt); + foreach (var seg in keys) + { + var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0); + //手动计算MPEGTS + if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) + { + vtt.MpegtsTimestamp = 90000 * (RecordingDurDic[task.Id] + (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); + } + currentVtt.AddCuesFromOne(vtt); + } } } + RecordingDurDic[task.Id] += (int)segmentsDuration; + /*//写出m3u8 if (DownloaderConfig.MyOptions.LiveWriteHLS) { @@ -538,10 +570,10 @@ namespace N_m3u8DL_RE.DownloadManager File.Delete(inputFilePath); } } - var subText = currentVtt.ToStringWithHeader(); + var subText = currentVtt.ToVtt(); if (outputExt == ".srt") { - subText = OtherUtil.WebVtt2Other(currentVtt, Enum.SubtitleFormat.SRT); + subText = currentVtt.ToSrt(); } var subBytes = Encoding.UTF8.GetBytes(subText); fileOutputStream.Position = 0; diff --git a/src/N_m3u8DL-RE/Util/OtherUtil.cs b/src/N_m3u8DL-RE/Util/OtherUtil.cs index bc0a8c2..01ad877 100644 --- a/src/N_m3u8DL-RE/Util/OtherUtil.cs +++ b/src/N_m3u8DL-RE/Util/OtherUtil.cs @@ -27,40 +27,6 @@ namespace N_m3u8DL_RE.Util return dic; } - private static string WebVtt2Srt(WebVttSub vtt) - { - StringBuilder sb = new StringBuilder(); - int index = 1; - foreach (var c in vtt.Cues) - { - sb.AppendLine($"{index++}"); - sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\,fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\,fff")); - sb.AppendLine(c.Payload); - sb.AppendLine(); - } - sb.AppendLine(); - - var srt = sb.ToString(); - - if (string.IsNullOrEmpty(srt.Trim())) - { - srt = "1\r\n00:00:00,000 --> 00:00:01,000"; //空字幕 - } - - return srt; - } - - public static string WebVtt2Other(WebVttSub vtt, SubtitleFormat toFormat) - { - Logger.Debug($"Convert {SubtitleFormat.VTT} ==> {toFormat}"); - return toFormat switch - { - SubtitleFormat.VTT => vtt.ToStringWithHeader(), - SubtitleFormat.SRT => WebVtt2Srt(vtt), - _ => throw new NotSupportedException($"{toFormat} not supported!") - }; - } - private static char[] InvalidChars = "34,60,62,124,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,58,42,63,92,47" .Split(',').Select(s => (char)int.Parse(s)).ToArray(); public static string GetValidFileName(string input, string re = ".", bool filterSlash = false)