优化直播字幕时间轴处理

This commit is contained in:
nilaoda 2022-11-28 15:27:18 +08:00
parent d96a45251b
commit 31482c8d34
6 changed files with 109 additions and 72 deletions

View File

@ -164,6 +164,11 @@ namespace N_m3u8DL_RE.Common.Entity
} }
} }
private IEnumerable<SubCue> GetCues()
{
return this.Cues.Where(c => !string.IsNullOrEmpty(c.Payload));
}
private static TimeSpan ConvertToTS(string str) private static TimeSpan ConvertToTS(string str)
{ {
var ms = Convert.ToInt32(str.Split('.').Last()); var ms = Convert.ToInt32(str.Split('.').Last());
@ -180,7 +185,7 @@ namespace N_m3u8DL_RE.Common.Entity
public override string ToString() public override string ToString()
{ {
StringBuilder sb = new StringBuilder(); 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.StartTime.ToString(@"hh\:mm\:ss\.fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\.fff") + " " + c.Settings);
sb.AppendLine(c.Payload); sb.AppendLine(c.Payload);
@ -190,9 +195,32 @@ namespace N_m3u8DL_RE.Common.Entity
return sb.ToString(); return sb.ToString();
} }
public string ToStringWithHeader() public string ToVtt()
{ {
return "WEBVTT" + Environment.NewLine + Environment.NewLine + ToString(); 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;
}
} }
} }

View File

@ -62,10 +62,10 @@ namespace N_m3u8DL_RE.Parser.Extractor
//选中第一个SmoothStreamingMedia节点 //选中第一个SmoothStreamingMedia节点
var ssmElement = xmlDocument.Elements().First(e => e.Name.LocalName == "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 timeScaleStr = ssmElement.Attribute("TimeScale")?.Value ?? "10000000";
var durationStr = ssmElement.Attribute("Duration")?.Value; var durationStr = ssmElement.Attribute("Duration")?.Value;
var isLiveStr = ssmElement.Attribute("IsLive")?.Value; var isLiveStr = ssmElement.Attribute("IsLive")?.Value;
bool isLive = Convert.ToBoolean(isLiveStr ?? "FALSE");
var isProtection = false; var isProtection = false;
var protectionSystemId = ""; var protectionSystemId = "";
@ -121,6 +121,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
var channels = qualityLevel.Attribute("Channels")?.Value; var channels = qualityLevel.Attribute("Channels")?.Value;
StreamSpec streamSpec = new(); StreamSpec streamSpec = new();
streamSpec.PublishTime = DateTime.Now; //发布时间默认现在
streamSpec.Extension = "m4s"; streamSpec.Extension = "m4s";
streamSpec.OriginalUrl = ParserConfig.OriginalUrl; streamSpec.OriginalUrl = ParserConfig.OriginalUrl;
streamSpec.PeriodId = indexStr; streamSpec.PeriodId = indexStr;

View File

@ -122,6 +122,11 @@ namespace Mp4SubtitleParser
return MultiElementsFixRegex().Matches(xml).Select(m => m.Value).ToList(); 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<string> items, long segTimeMs, long baseTimestamp = 0L) public static WebVttSub ExtractFromMp4s(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)
{ {
//read ttmls //read ttmls
@ -163,6 +168,11 @@ namespace Mp4SubtitleParser
return ExtractSub(xmls, baseTimestamp); 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<string> items, long segTimeMs, long baseTimestamp = 0L) public static WebVttSub ExtractFromTTMLs(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)
{ {
//read ttmls //read ttmls

View File

@ -360,12 +360,12 @@ namespace N_m3u8DL_RE.DownloadManager
FileDic.Clear(); FileDic.Clear();
var index = 0; var index = 0;
var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); 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) if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
{ {
path = Path.ChangeExtension(path, ".srt"); path = Path.ChangeExtension(path, ".srt");
subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); subContentFixed = finalVtt.ToSrt();
} }
await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8);
FileDic[keys.First()] = new DownloadResult() FileDic[keys.First()] = new DownloadResult()
@ -394,12 +394,12 @@ namespace N_m3u8DL_RE.DownloadManager
FileDic.Clear(); FileDic.Clear();
var index = 0; var index = 0;
var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); 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) if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
{ {
path = Path.ChangeExtension(path, ".srt"); path = Path.ChangeExtension(path, ".srt");
subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); subContentFixed = finalVtt.ToSrt();
} }
await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8);
FileDic[firstKey] = new DownloadResult() 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); var keys = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Key);
foreach (var seg in keys) foreach (var seg in keys)
{ {
var vtt = MP4TtmlUtil.ExtractFromTTMLs(new string[] { FileDic[seg]!.ActualFilePath }, 0); var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0);
//手动计算MPEGTS //手动计算MPEGTS
if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)
{ {
@ -452,12 +452,12 @@ namespace N_m3u8DL_RE.DownloadManager
FileDic.Clear(); FileDic.Clear();
var index = 0; var index = 0;
var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); 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) if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
{ {
path = Path.ChangeExtension(path, ".srt"); path = Path.ChangeExtension(path, ".srt");
subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); subContentFixed = finalVtt.ToSrt();
} }
await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8);
FileDic[firstKey] = new DownloadResult() 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); var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key);
foreach (var seg in keys) foreach (var seg in keys)
{ {
var vtt = MP4TtmlUtil.ExtractFromMp4s(new string[] { FileDic[seg]!.ActualFilePath }, 0); var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0);
//手动计算MPEGTS //手动计算MPEGTS
if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)
{ {
@ -515,12 +515,12 @@ namespace N_m3u8DL_RE.DownloadManager
FileDic.Clear(); FileDic.Clear();
var index = 0; var index = 0;
var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); 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) if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
{ {
path = Path.ChangeExtension(path, ".srt"); path = Path.ChangeExtension(path, ".srt");
subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); subContentFixed = finalVtt.ToSrt();
} }
await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8);
FileDic[firstKey] = new DownloadResult() FileDic[firstKey] = new DownloadResult()

View File

@ -209,6 +209,7 @@ namespace N_m3u8DL_RE.DownloadManager
//TryReceiveAll可以稍微缓解一下 //TryReceiveAll可以稍微缓解一下
source.TryReceiveAll(out IList<List<MediaSegment>>? segmentsList); source.TryReceiveAll(out IList<List<MediaSegment>>? segmentsList);
var segments = segmentsList!.SelectMany(s => s); var segments = segmentsList!.SelectMany(s => s);
var segmentsDuration = segments.Sum(s => s.Duration);
Logger.DebugMarkUp(string.Join(",", segments.Select(sss => GetSegmentName(sss, false, false)))); Logger.DebugMarkUp(string.Join(",", segments.Select(sss => GetSegmentName(sss, false, false))));
//下载init //下载init
@ -355,8 +356,6 @@ namespace N_m3u8DL_RE.DownloadManager
} }
}); });
RecordingDurDic[task.Id] += (int)segments.Sum(s => s.Duration);
//自动修复VTT raw字幕 //自动修复VTT raw字幕
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
&& streamSpec.Extension != null && streamSpec.Extension.Contains("vtt")) && 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); vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration);
} }
if (firstSub) if (firstSub) { currentVtt = vtt; firstSub = false; }
{ else currentVtt.AddCuesFromOne(vtt);
currentVtt = vtt;
firstSub = false;
}
else
{
currentVtt.AddCuesFromOne(vtt);
}
} }
} }
@ -401,8 +393,8 @@ namespace N_m3u8DL_RE.DownloadManager
} }
else else
{ {
var finalVtt = MP4VttUtil.ExtractSub(mp4s, timescale); var vtt = MP4VttUtil.ExtractSub(mp4s, timescale);
currentVtt.AddCuesFromOne(finalVtt); currentVtt.AddCuesFromOne(vtt);
} }
} }
} }
@ -411,21 +403,40 @@ namespace N_m3u8DL_RE.DownloadManager
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
&& streamSpec.Extension != null && streamSpec.Extension.Contains("ttml")) && 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 (firstSub)
{ {
if (baseTimestamp != 0) if (baseTimestamp != 0)
{ {
var total = segments.Sum(s => s.Duration); var total = segmentsDuration;
baseTimestamp -= (long)TimeSpan.FromSeconds(total).TotalMilliseconds; 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; firstSub = false;
} }
else else
{ {
var finalVtt = MP4TtmlUtil.ExtractFromTTMLs(mp4s, 0, baseTimestamp); foreach (var seg in keys)
currentVtt.AddCuesFromOne(finalVtt); {
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,23 +449,44 @@ namespace N_m3u8DL_RE.DownloadManager
//var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); //var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault();
//var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); //var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);
//var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes); //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 (firstSub)
{ {
if (baseTimestamp != 0) if (baseTimestamp != 0)
{ {
var total = segments.Sum(s => s.Duration); var total = segmentsDuration;
baseTimestamp -= (long)TimeSpan.FromSeconds(total).TotalMilliseconds; 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; firstSub = false;
} }
else else
{ {
var finalVtt = MP4TtmlUtil.ExtractFromMp4s(mp4s, 0, baseTimestamp); foreach (var seg in keys)
currentVtt.AddCuesFromOne(finalVtt); {
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 /*//写出m3u8
if (DownloaderConfig.MyOptions.LiveWriteHLS) if (DownloaderConfig.MyOptions.LiveWriteHLS)
@ -538,10 +570,10 @@ namespace N_m3u8DL_RE.DownloadManager
File.Delete(inputFilePath); File.Delete(inputFilePath);
} }
} }
var subText = currentVtt.ToStringWithHeader(); var subText = currentVtt.ToVtt();
if (outputExt == ".srt") if (outputExt == ".srt")
{ {
subText = OtherUtil.WebVtt2Other(currentVtt, Enum.SubtitleFormat.SRT); subText = currentVtt.ToSrt();
} }
var subBytes = Encoding.UTF8.GetBytes(subText); var subBytes = Encoding.UTF8.GetBytes(subText);
fileOutputStream.Position = 0; fileOutputStream.Position = 0;

View File

@ -27,40 +27,6 @@ namespace N_m3u8DL_RE.Util
return dic; 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" 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(); .Split(',').Select(s => (char)int.Parse(s)).ToArray();
public static string GetValidFileName(string input, string re = ".", bool filterSlash = false) public static string GetValidFileName(string input, string re = ".", bool filterSlash = false)