优化直播字幕时间轴处理

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)
{
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;
}
}
}

View File

@ -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;

View File

@ -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<string> 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<string> items, long segTimeMs, long baseTimestamp = 0L)
{
//read ttmls

View File

@ -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()

View File

@ -209,6 +209,7 @@ namespace N_m3u8DL_RE.DownloadManager
//TryReceiveAll可以稍微缓解一下
source.TryReceiveAll(out IList<List<MediaSegment>>? 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,23 +449,44 @@ 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;

View File

@ -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)