diff options
| author | 7illusions <z@7illusions.com> | 2014-08-30 19:06:58 +0200 |
|---|---|---|
| committer | 7illusions <z@7illusions.com> | 2014-08-30 19:06:58 +0200 |
| commit | 66ad1699e22029b605e17735e8d9450285d8748a (patch) | |
| tree | ffc92c88d24850b2f82b6b3a8bdd904a2ccc77a5 /MediaBrowser.MediaEncoding | |
| parent | 34bc54263e886aae777a3537dc50a6535b51330a (diff) | |
| parent | 9d36f518182bc075c19d78084870f5115fa62d1e (diff) | |
Merge pull request #1 from MediaBrowser/master
Update to latest
Diffstat (limited to 'MediaBrowser.MediaEncoding')
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 169 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj | 8 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs | 4 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Subtitles/AssParser.cs | 71 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs | 27 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs | 2 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs | 2 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs | 400 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 149 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs | 59 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs | 13 |
11 files changed, 735 insertions, 169 deletions
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 5ee119e13..82d5e0344 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -1,5 +1,3 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.IO; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; @@ -11,7 +9,6 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -28,11 +25,6 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly ILogger _logger; /// <summary> - /// The _app paths - /// </summary> - private readonly IApplicationPaths _appPaths; - - /// <summary> /// Gets the json serializer. /// </summary> /// <value>The json serializer.</value> @@ -53,23 +45,17 @@ namespace MediaBrowser.MediaEncoding.Encoder /// </summary> private readonly SemaphoreSlim _ffProbeResourcePool = new SemaphoreSlim(2, 2); - private readonly IFileSystem _fileSystem; - public string FFMpegPath { get; private set; } public string FFProbePath { get; private set; } public string Version { get; private set; } - public MediaEncoder(ILogger logger, IApplicationPaths appPaths, - IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version, - IFileSystem fileSystem) + public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version) { _logger = logger; - _appPaths = appPaths; _jsonSerializer = jsonSerializer; Version = version; - _fileSystem = fileSystem; FFProbePath = ffProbePath; FFMpegPath = ffMpegPath; } @@ -84,22 +70,6 @@ namespace MediaBrowser.MediaEncoding.Encoder } /// <summary> - /// The _semaphoreLocks - /// </summary> - private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = - new ConcurrentDictionary<string, SemaphoreSlim>(); - - /// <summary> - /// Gets the lock. - /// </summary> - /// <param name="filename">The filename.</param> - /// <returns>System.Object.</returns> - private SemaphoreSlim GetLock(string filename) - { - return _semaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); - } - - /// <summary> /// Gets the media info. /// </summary> /// <param name="inputFiles">The input files.</param> @@ -166,7 +136,7 @@ namespace MediaBrowser.MediaEncoding.Encoder RedirectStandardError = true, FileName = FFProbePath, Arguments = string.Format(args, - probeSizeArgument, inputPath).Trim(), + probeSizeArgument, inputPath).Trim(), WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false @@ -200,8 +170,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { process.BeginErrorReadLine(); - result = - _jsonSerializer.DeserializeFromStream<InternalMediaInfoResult>(process.StandardOutput.BaseStream); + result = _jsonSerializer.DeserializeFromStream<InternalMediaInfoResult>(process.StandardOutput.BaseStream); } catch { @@ -263,30 +232,6 @@ namespace MediaBrowser.MediaEncoding.Encoder ((Process)sender).Dispose(); } - private const int FastSeekOffsetSeconds = 1; - - protected string GetFastSeekCommandLineParameter(TimeSpan offset) - { - var seconds = offset.TotalSeconds - FastSeekOffsetSeconds; - - if (seconds > 0) - { - return string.Format("-ss {0} ", seconds.ToString(UsCulture)); - } - - return string.Empty; - } - - protected string GetSlowSeekCommandLineParameter(TimeSpan offset) - { - if (offset.TotalSeconds - FastSeekOffsetSeconds > 0) - { - return string.Format(" -ss {0}", FastSeekOffsetSeconds.ToString(UsCulture)); - } - - return string.Empty; - } - public Task<Stream> ExtractAudioImage(string path, CancellationToken cancellationToken) { return ExtractImage(new[] { path }, MediaProtocol.File, true, null, null, cancellationToken); @@ -330,7 +275,6 @@ namespace MediaBrowser.MediaEncoding.Encoder // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar then scale to width 600. // This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex. commercials @ diff ar var vf = "scale=600:trunc(600/dar/2)*2"; - //crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,scale=600:(600/dar),thumbnail" -f image2 if (threedFormat.HasValue) { @@ -368,7 +312,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (offset.HasValue) { - args = string.Format("-ss {0} ", Convert.ToInt32(offset.Value.TotalSeconds)).ToString(UsCulture) + args; + args = string.Format("-ss {0} ", GetTimeParameter(offset.Value)) + args; } var process = new Process @@ -382,7 +326,8 @@ namespace MediaBrowser.MediaEncoding.Encoder WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false, RedirectStandardOutput = true, - RedirectStandardError = true + RedirectStandardError = true, + RedirectStandardInput = true } }; @@ -408,7 +353,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { _logger.Info("Killing ffmpeg process"); - process.Kill(); + process.StandardInput.WriteLine("q"); process.WaitForExit(1000); } @@ -463,5 +408,105 @@ namespace MediaBrowser.MediaEncoding.Encoder _videoImageResourcePool.Dispose(); } } + + public string GetTimeParameter(long ticks) + { + var time = TimeSpan.FromTicks(ticks); + + return GetTimeParameter(time); + } + + public string GetTimeParameter(TimeSpan time) + { + return time.ToString(@"hh\:mm\:ss\.fff", UsCulture); + } + + public async Task ExtractVideoImagesOnInterval(string[] inputFiles, + MediaProtocol protocol, + Video3DFormat? threedFormat, + TimeSpan interval, + string targetDirectory, + string filenamePrefix, + int? maxWidth, + CancellationToken cancellationToken) + { + var resourcePool = _videoImageResourcePool; + + var inputArgument = GetInputArgument(inputFiles, protocol); + + var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(UsCulture); + + if (maxWidth.HasValue) + { + var maxWidthParam = maxWidth.Value.ToString(UsCulture); + + vf += string.Format(",scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam); + } + + Directory.CreateDirectory(targetDirectory); + var outputPath = Path.Combine(targetDirectory, filenamePrefix + "%05d.jpg"); + + var args = string.Format("-i {0} -threads 0 -v quiet -vf \"{2}\" -f image2 \"{1}\"", inputArgument, outputPath, vf); + + var probeSize = GetProbeSizeArgument(new[] { inputArgument }, protocol); + + if (!string.IsNullOrEmpty(probeSize)) + { + args = probeSize + " " + args; + } + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + FileName = FFMpegPath, + Arguments = args, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false, + RedirectStandardInput = true + } + }; + + _logger.Info(process.StartInfo.FileName + " " + process.StartInfo.Arguments); + + await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + process.Start(); + + var ranToCompletion = process.WaitForExit(120000); + + if (!ranToCompletion) + { + try + { + _logger.Info("Killing ffmpeg process"); + + process.StandardInput.WriteLine("q"); + + process.WaitForExit(1000); + } + catch (Exception ex) + { + _logger.ErrorException("Error killing process", ex); + } + } + + resourcePool.Release(); + + var exitCode = ranToCompletion ? process.ExitCode : -1; + + process.Dispose(); + + if (exitCode == -1) + { + var msg = string.Format("ffmpeg image extraction failed for {0}", inputArgument); + + _logger.Error(msg); + + throw new ApplicationException(msg); + } + } } } diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index d2c21639c..9263a3187 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -52,17 +52,23 @@ <Reference Include="System.Xml" /> </ItemGroup> <ItemGroup> + <Compile Include="..\SharedVersion.cs"> + <Link>Properties\SharedVersion.cs</Link> + </Compile> <Compile Include="BdInfo\BdInfoExaminer.cs" /> <Compile Include="Encoder\EncodingUtils.cs" /> <Compile Include="Encoder\MediaEncoder.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Subtitles\ISubtitleParser.cs" /> <Compile Include="Subtitles\ISubtitleWriter.cs" /> + <Compile Include="Subtitles\JsonWriter.cs" /> <Compile Include="Subtitles\SrtParser.cs" /> <Compile Include="Subtitles\SrtWriter.cs" /> + <Compile Include="Subtitles\AssParser.cs" /> <Compile Include="Subtitles\SsaParser.cs" /> <Compile Include="Subtitles\SubtitleEncoder.cs" /> <Compile Include="Subtitles\SubtitleTrackInfo.cs" /> + <Compile Include="Subtitles\TtmlWriter.cs" /> <Compile Include="Subtitles\VttWriter.cs" /> </ItemGroup> <ItemGroup> @@ -84,7 +90,7 @@ </ItemGroup> <ItemGroup /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " /> + <Import Project="$(SolutionDir)\.nuget\NuGet.targets" /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets. <Target Name="BeforeBuild"> diff --git a/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs b/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs index 6616e46ac..6b456b98d 100644 --- a/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs +++ b/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs @@ -31,6 +31,4 @@ using System.Runtime.InteropServices; // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +// [assembly: AssemblyVersion("1.0.*")]
\ No newline at end of file diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs new file mode 100644 index 000000000..e5a727428 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; + +namespace MediaBrowser.MediaEncoding.Subtitles +{ + public class AssParser : ISubtitleParser + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) + { + var trackInfo = new SubtitleTrackInfo(); + var eventIndex = 1; + using (var reader = new StreamReader(stream)) + { + string line; + while (reader.ReadLine() != "[Events]") + {} + var headers = ParseFieldHeaders(reader.ReadLine()); + + while ((line = reader.ReadLine()) != null) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + if(line.StartsWith("[")) + break; + if(string.IsNullOrEmpty(line)) + continue; + var subEvent = new SubtitleTrackEvent { Id = eventIndex.ToString(_usCulture) }; + eventIndex++; + var sections = line.Substring(10).Split(','); + + subEvent.StartPositionTicks = GetTicks(sections[headers["Start"]]); + subEvent.EndPositionTicks = GetTicks(sections[headers["End"]]); + subEvent.Text = string.Join(",", sections.Skip(headers["Text"])); + subEvent.Text = Regex.Replace(subEvent.Text, @"\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}", string.Empty, RegexOptions.IgnoreCase); + + trackInfo.TrackEvents.Add(subEvent); + } + } + return trackInfo; + } + + long GetTicks(string time) + { + TimeSpan span; + return TimeSpan.TryParseExact(time, @"h\:mm\:ss\.ff", _usCulture, out span) + ? span.Ticks: 0; + } + + private Dictionary<string,int> ParseFieldHeaders(string line) { + var fields = line.Substring(8).Split(',').Select(x=>x.Trim()).ToList(); + + var result = new Dictionary<string, int> { + {"Start", fields.IndexOf("Start")}, + {"End", fields.IndexOf("End")}, + {"Text", fields.IndexOf("Text")} + }; + return result; + } + } +} diff --git a/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs new file mode 100644 index 000000000..a4fc5d795 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs @@ -0,0 +1,27 @@ +using MediaBrowser.Model.Serialization; +using System.IO; +using System.Text; +using System.Threading; + +namespace MediaBrowser.MediaEncoding.Subtitles +{ + public class JsonWriter : ISubtitleWriter + { + private readonly IJsonSerializer _json; + + public JsonWriter(IJsonSerializer json) + { + _json = json; + } + + public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) + { + using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + { + var json = _json.SerializeToString(info); + + writer.Write(json); + } + } + } +} diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs index f94fae9e9..84cd1eb2d 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs @@ -48,7 +48,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles } multiline.Add(line); } - subEvent.Text = string.Join(@"\N", multiline); + subEvent.Text = string.Join(@"\n", multiline); subEvent.Text = Regex.Replace(subEvent.Text, @"\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}", string.Empty, RegexOptions.IgnoreCase); subEvent.Text = Regex.Replace(subEvent.Text, "<", "<", RegexOptions.IgnoreCase); subEvent.Text = Regex.Replace(subEvent.Text, ">", ">", RegexOptions.IgnoreCase); diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs index d0d0819dd..3e574f931 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var text = trackEvent.Text; // TODO: Not sure how to handle these - text = Regex.Replace(text, @"\\N", " ", RegexOptions.IgnoreCase); + text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase); writer.WriteLine(text); writer.WriteLine(string.Empty); diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs index e21804f6c..559a05bc8 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs @@ -1,71 +1,391 @@ using System; -using System.Collections.Generic; -using System.Globalization; using System.IO; -using System.Linq; -using System.Text.RegularExpressions; +using System.Text; using System.Threading; namespace MediaBrowser.MediaEncoding.Subtitles { + /// <summary> + /// Credit to https://github.com/SubtitleEdit/subtitleedit/blob/a299dc4407a31796364cc6ad83f0d3786194ba22/src/Logic/SubtitleFormats/SubStationAlpha.cs + /// </summary> public class SsaParser : ISubtitleParser { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) { var trackInfo = new SubtitleTrackInfo(); - var eventIndex = 1; + using (var reader = new StreamReader(stream)) { + bool eventsStarted = false; + + string[] format = "Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text".Split(','); + int indexLayer = 0; + int indexStart = 1; + int indexEnd = 2; + int indexStyle = 3; + int indexName = 4; + int indexEffect = 8; + int indexText = 9; + int lineNumber = 0; + + var header = new StringBuilder(); + string line; - while (reader.ReadLine() != "[Events]") - {} - var headers = ParseFieldHeaders(reader.ReadLine()); while ((line = reader.ReadLine()) != null) { cancellationToken.ThrowIfCancellationRequested(); - - if (string.IsNullOrWhiteSpace(line)) + + lineNumber++; + if (!eventsStarted) + header.AppendLine(line); + + if (line.Trim().ToLower() == "[events]") { - continue; + eventsStarted = true; } - if(line.StartsWith("[")) - break; - if(string.IsNullOrEmpty(line)) - continue; - var subEvent = new SubtitleTrackEvent { Id = eventIndex.ToString(_usCulture) }; - eventIndex++; - var sections = line.Substring(10).Split(','); - - subEvent.StartPositionTicks = GetTicks(sections[headers["Start"]]); - subEvent.EndPositionTicks = GetTicks(sections[headers["End"]]); - subEvent.Text = string.Join(",", sections.Skip(headers["Text"])); - subEvent.Text = Regex.Replace(subEvent.Text, @"\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}", string.Empty, RegexOptions.IgnoreCase); - - trackInfo.TrackEvents.Add(subEvent); - } + else if (!string.IsNullOrEmpty(line) && line.Trim().StartsWith(";")) + { + // skip comment lines + } + else if (eventsStarted && line.Trim().Length > 0) + { + string s = line.Trim().ToLower(); + if (s.StartsWith("format:")) + { + if (line.Length > 10) + { + format = line.ToLower().Substring(8).Split(','); + for (int i = 0; i < format.Length; i++) + { + if (format[i].Trim().ToLower() == "layer") + indexLayer = i; + else if (format[i].Trim().ToLower() == "start") + indexStart = i; + else if (format[i].Trim().ToLower() == "end") + indexEnd = i; + else if (format[i].Trim().ToLower() == "text") + indexText = i; + else if (format[i].Trim().ToLower() == "effect") + indexEffect = i; + else if (format[i].Trim().ToLower() == "style") + indexStyle = i; + } + } + } + else if (!string.IsNullOrEmpty(s)) + { + string text = string.Empty; + string start = string.Empty; + string end = string.Empty; + string style = string.Empty; + string layer = string.Empty; + string effect = string.Empty; + string name = string.Empty; + + string[] splittedLine; + + if (s.StartsWith("dialogue:")) + splittedLine = line.Substring(10).Split(','); + else + splittedLine = line.Split(','); + + for (int i = 0; i < splittedLine.Length; i++) + { + if (i == indexStart) + start = splittedLine[i].Trim(); + else if (i == indexEnd) + end = splittedLine[i].Trim(); + else if (i == indexLayer) + layer = splittedLine[i]; + else if (i == indexEffect) + effect = splittedLine[i]; + else if (i == indexText) + text = splittedLine[i]; + else if (i == indexStyle) + style = splittedLine[i]; + else if (i == indexName) + name = splittedLine[i]; + else if (i > indexText) + text += "," + splittedLine[i]; + } + + try + { + var p = new SubtitleTrackEvent(); + + p.StartPositionTicks = GetTimeCodeFromString(start); + p.EndPositionTicks = GetTimeCodeFromString(end); + p.Text = GetFormattedText(text); + + trackInfo.TrackEvents.Add(p); + } + catch + { + } + } + } + } + + //if (header.Length > 0) + //subtitle.Header = header.ToString(); + + //subtitle.Renumber(1); } return trackInfo; } - long GetTicks(string time) + private static long GetTimeCodeFromString(string time) + { + // h:mm:ss.cc + string[] timeCode = time.Split(':', '.'); + return new TimeSpan(0, int.Parse(timeCode[0]), + int.Parse(timeCode[1]), + int.Parse(timeCode[2]), + int.Parse(timeCode[3]) * 10).Ticks; + } + + public static string GetFormattedText(string text) + { + text = text.Replace("\\n", Environment.NewLine).Replace("\\n", Environment.NewLine); + bool italic = false; + + for (int i = 0; i < 10; i++) // just look ten times... + { + if (text.Contains(@"{\fn")) + { + int start = text.IndexOf(@"{\fn"); + int end = text.IndexOf('}', start); + if (end > 0 && !text.Substring(start).StartsWith("{\\fn}")) + { + string fontName = text.Substring(start + 4, end - (start + 4)); + string extraTags = string.Empty; + CheckAndAddSubTags(ref fontName, ref extraTags, out italic); + text = text.Remove(start, end - start + 1); + if (italic) + text = text.Insert(start, "<font face=\"" + fontName + "\"" + extraTags + "><i>"); + else + text = text.Insert(start, "<font face=\"" + fontName + "\"" + extraTags + ">"); + + int indexOfEndTag = text.IndexOf("{\\fn}", start); + if (indexOfEndTag > 0) + text = text.Remove(indexOfEndTag, "{\\fn}".Length).Insert(indexOfEndTag, "</font>"); + else + text += "</font>"; + } + } + + if (text.Contains(@"{\fs")) + { + int start = text.IndexOf(@"{\fs"); + int end = text.IndexOf('}', start); + if (end > 0 && !text.Substring(start).StartsWith("{\\fs}")) + { + string fontSize = text.Substring(start + 4, end - (start + 4)); + string extraTags = string.Empty; + CheckAndAddSubTags(ref fontSize, ref extraTags, out italic); + if (IsInteger(fontSize)) + { + text = text.Remove(start, end - start + 1); + if (italic) + text = text.Insert(start, "<font size=\"" + fontSize + "\"" + extraTags + "><i>"); + else + text = text.Insert(start, "<font size=\"" + fontSize + "\"" + extraTags + ">"); + + int indexOfEndTag = text.IndexOf("{\\fs}", start); + if (indexOfEndTag > 0) + text = text.Remove(indexOfEndTag, "{\\fs}".Length).Insert(indexOfEndTag, "</font>"); + else + text += "</font>"; + } + } + } + + if (text.Contains(@"{\c")) + { + int start = text.IndexOf(@"{\c"); + int end = text.IndexOf('}', start); + if (end > 0 && !text.Substring(start).StartsWith("{\\c}")) + { + string color = text.Substring(start + 4, end - (start + 4)); + string extraTags = string.Empty; + CheckAndAddSubTags(ref color, ref extraTags, out italic); + + color = color.Replace("&", string.Empty).TrimStart('H'); + color = color.PadLeft(6, '0'); + + // switch to rrggbb from bbggrr + color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2); + color = color.ToLower(); + + text = text.Remove(start, end - start + 1); + if (italic) + text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + "><i>"); + else + text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + ">"); + int indexOfEndTag = text.IndexOf("{\\c}", start); + if (indexOfEndTag > 0) + text = text.Remove(indexOfEndTag, "{\\c}".Length).Insert(indexOfEndTag, "</font>"); + else + text += "</font>"; + } + } + + if (text.Contains(@"{\1c")) // "1" specifices primary color + { + int start = text.IndexOf(@"{\1c"); + int end = text.IndexOf('}', start); + if (end > 0 && !text.Substring(start).StartsWith("{\\1c}")) + { + string color = text.Substring(start + 5, end - (start + 5)); + string extraTags = string.Empty; + CheckAndAddSubTags(ref color, ref extraTags, out italic); + + color = color.Replace("&", string.Empty).TrimStart('H'); + color = color.PadLeft(6, '0'); + + // switch to rrggbb from bbggrr + color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2); + color = color.ToLower(); + + text = text.Remove(start, end - start + 1); + if (italic) + text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + "><i>"); + else + text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + ">"); + text += "</font>"; + } + } + + } + + text = text.Replace(@"{\i1}", "<i>"); + text = text.Replace(@"{\i0}", "</i>"); + text = text.Replace(@"{\i}", "</i>"); + if (CountTagInText(text, "<i>") > CountTagInText(text, "</i>")) + text += "</i>"; + + text = text.Replace(@"{\u1}", "<u>"); + text = text.Replace(@"{\u0}", "</u>"); + text = text.Replace(@"{\u}", "</u>"); + if (CountTagInText(text, "<u>") > CountTagInText(text, "</u>")) + text += "</u>"; + + text = text.Replace(@"{\b1}", "<b>"); + text = text.Replace(@"{\b0}", "</b>"); + text = text.Replace(@"{\b}", "</b>"); + if (CountTagInText(text, "<b>") > CountTagInText(text, "</b>")) + text += "</b>"; + + return text; + } + + private static bool IsInteger(string s) { - TimeSpan span; - return TimeSpan.TryParseExact(time, @"h\:mm\:ss\.ff", _usCulture, out span) - ? span.Ticks: 0; + int i; + if (int.TryParse(s, out i)) + return true; + return false; } - private Dictionary<string,int> ParseFieldHeaders(string line) { - var fields = line.Substring(8).Split(',').Select(x=>x.Trim()).ToList(); + private static int CountTagInText(string text, string tag) + { + int count = 0; + int index = text.IndexOf(tag); + while (index >= 0) + { + count++; + if (index == text.Length) + return count; + index = text.IndexOf(tag, index + 1); + } + return count; + } + + private static void CheckAndAddSubTags(ref string tagName, ref string extraTags, out bool italic) + { + italic = false; + int indexOfSPlit = tagName.IndexOf(@"\"); + if (indexOfSPlit > 0) + { + string rest = tagName.Substring(indexOfSPlit).TrimStart('\\'); + tagName = tagName.Remove(indexOfSPlit); + + for (int i = 0; i < 10; i++) + { + if (rest.StartsWith("fs") && rest.Length > 2) + { + indexOfSPlit = rest.IndexOf(@"\"); + string fontSize = rest; + if (indexOfSPlit > 0) + { + fontSize = rest.Substring(0, indexOfSPlit); + rest = rest.Substring(indexOfSPlit).TrimStart('\\'); + } + else + { + rest = string.Empty; + } + extraTags += " size=\"" + fontSize.Substring(2) + "\""; + } + else if (rest.StartsWith("fn") && rest.Length > 2) + { + indexOfSPlit = rest.IndexOf(@"\"); + string fontName = rest; + if (indexOfSPlit > 0) + { + fontName = rest.Substring(0, indexOfSPlit); + rest = rest.Substring(indexOfSPlit).TrimStart('\\'); + } + else + { + rest = string.Empty; + } + extraTags += " face=\"" + fontName.Substring(2) + "\""; + } + else if (rest.StartsWith("c") && rest.Length > 2) + { + indexOfSPlit = rest.IndexOf(@"\"); + string fontColor = rest; + if (indexOfSPlit > 0) + { + fontColor = rest.Substring(0, indexOfSPlit); + rest = rest.Substring(indexOfSPlit).TrimStart('\\'); + } + else + { + rest = string.Empty; + } + + string color = fontColor.Substring(2); + color = color.Replace("&", string.Empty).TrimStart('H'); + color = color.PadLeft(6, '0'); + // switch to rrggbb from bbggrr + color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2); + color = color.ToLower(); - var result = new Dictionary<string, int> { - {"Start", fields.IndexOf("Start")}, - {"End", fields.IndexOf("End")}, - {"Text", fields.IndexOf("Text")} - }; - return result; + extraTags += " color=\"" + color + "\""; + } + else if (rest.StartsWith("i1") && rest.Length > 1) + { + indexOfSPlit = rest.IndexOf(@"\"); + italic = true; + if (indexOfSPlit > 0) + { + rest = rest.Substring(indexOfSPlit).TrimStart('\\'); + } + else + { + rest = string.Empty; + } + } + else if (rest.Length > 0 && rest.Contains("\\")) + { + indexOfSPlit = rest.IndexOf(@"\"); + rest = rest.Substring(indexOfSPlit).TrimStart('\\'); + } + } + } } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 154541316..7a3a7d2d0 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -7,6 +7,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Serialization; using System; using System.Collections.Concurrent; using System.Diagnostics; @@ -26,14 +27,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; private readonly IMediaEncoder _mediaEncoder; + private readonly IJsonSerializer _json; - public SubtitleEncoder(ILibraryManager libraryManager, ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder) + public SubtitleEncoder(ILibraryManager libraryManager, ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IJsonSerializer json) { _libraryManager = libraryManager; _logger = logger; _appPaths = appPaths; _fileSystem = fileSystem; _mediaEncoder = mediaEncoder; + _json = json; } private string SubtitleCachePath @@ -48,6 +51,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles string inputFormat, string outputFormat, long startTimeTicks, + long? endTimeTicks, CancellationToken cancellationToken) { var ms = new MemoryStream(); @@ -56,6 +60,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { // Return the original without any conversions, if possible if (startTimeTicks == 0 && + !endTimeTicks.HasValue && string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase)) { await stream.CopyToAsync(ms, 81920, cancellationToken).ConfigureAwait(false); @@ -64,7 +69,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { var trackInfo = await GetTrackInfo(stream, inputFormat, cancellationToken).ConfigureAwait(false); - UpdateStartingPosition(trackInfo, startTimeTicks); + FilterEvents(trackInfo, startTimeTicks, endTimeTicks, false); var writer = GetWriter(outputFormat); @@ -81,19 +86,30 @@ namespace MediaBrowser.MediaEncoding.Subtitles return ms; } - private void UpdateStartingPosition(SubtitleTrackInfo track, long startPositionTicks) + private void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long? endTimeTicks, bool preserveTimestamps) { - if (startPositionTicks == 0) return; + // Drop subs that are earlier than what we're looking for + track.TrackEvents = track.TrackEvents + .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 || (i.EndPositionTicks - startPositionTicks) < 0) + .ToList(); - foreach (var trackEvent in track.TrackEvents) + if (endTimeTicks.HasValue) { - trackEvent.EndPositionTicks -= startPositionTicks; - trackEvent.StartPositionTicks -= startPositionTicks; + var endTime = endTimeTicks.Value; + + track.TrackEvents = track.TrackEvents + .TakeWhile(i => i.StartPositionTicks <= endTime) + .ToList(); } - track.TrackEvents = track.TrackEvents - .SkipWhile(i => i.StartPositionTicks < 0 || i.EndPositionTicks < 0) - .ToList(); + if (!preserveTimestamps) + { + foreach (var trackEvent in track.TrackEvents) + { + trackEvent.EndPositionTicks -= startPositionTicks; + trackEvent.StartPositionTicks -= startPositionTicks; + } + } } public async Task<Stream> GetSubtitles(string itemId, @@ -101,6 +117,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles int subtitleStreamIndex, string outputFormat, long startTimeTicks, + long? endTimeTicks, CancellationToken cancellationToken) { var subtitle = await GetSubtitleStream(itemId, mediaSourceId, subtitleStreamIndex, cancellationToken) @@ -110,7 +127,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { var inputFormat = subtitle.Item2; - return await ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, cancellationToken).ConfigureAwait(false); + return await ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, cancellationToken).ConfigureAwait(false); } } @@ -179,13 +196,36 @@ namespace MediaBrowser.MediaEncoding.Subtitles { if (!subtitleStream.IsExternal) { + string outputFormat; + string outputCodec; + + if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)) + { + // Extract + outputCodec = "copy"; + outputFormat = "ass"; + } + else if (string.Equals(subtitleStream.Codec, "subrip", StringComparison.OrdinalIgnoreCase) || + string.Equals(subtitleStream.Codec, "srt", StringComparison.OrdinalIgnoreCase)) + { + // Extract + outputCodec = "copy"; + outputFormat = "srt"; + } + else + { + // Extract + outputCodec = "srt"; + outputFormat = "srt"; + } + // Extract - var outputPath = GetSubtitleCachePath(mediaPath, subtitleStream.Index, ".ass"); + var outputPath = GetSubtitleCachePath(mediaPath, subtitleStream.Index, "." + outputFormat); - await ExtractTextSubtitle(inputFiles, protocol, subtitleStream.Index, false, outputPath, cancellationToken) + await ExtractTextSubtitle(inputFiles, protocol, subtitleStream.Index, outputCodec, outputPath, cancellationToken) .ConfigureAwait(false); - return new Tuple<string, string>(outputPath, "ass"); + return new Tuple<string, string>(outputPath, outputFormat); } var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec) @@ -194,12 +234,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (GetReader(currentFormat, false) == null) { // Convert - var outputPath = GetSubtitleCachePath(mediaPath, subtitleStream.Index, ".ass"); + var outputPath = GetSubtitleCachePath(mediaPath, subtitleStream.Index, ".srt"); - await ConvertTextSubtitleToAss(subtitleStream.Path, outputPath, subtitleStream.Language, cancellationToken) + await ConvertTextSubtitleToSrt(subtitleStream.Path, outputPath, subtitleStream.Language, cancellationToken) .ConfigureAwait(false); - return new Tuple<string, string>(outputPath, "ass"); + return new Tuple<string, string>(outputPath, "srt"); } return new Tuple<string, string>(subtitleStream.Path, currentFormat); @@ -225,11 +265,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles { return new SrtParser(); } - if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase) || - string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase)) { return new SsaParser(); } + if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)) + { + return new AssParser(); + } if (throwIfMissing) { @@ -246,6 +289,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles throw new ArgumentNullException("format"); } + if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase)) + { + return new JsonWriter(_json); + } if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)) { return new SrtWriter(); @@ -254,6 +301,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles { return new VttWriter(); } + if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase)) + { + return new TtmlWriter(); + } throw new ArgumentException("Unsupported format: " + format); } @@ -275,14 +326,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles } /// <summary> - /// Converts the text subtitle to ass. + /// Converts the text subtitle to SRT. /// </summary> /// <param name="inputPath">The input path.</param> /// <param name="outputPath">The output path.</param> /// <param name="language">The language.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - public async Task ConvertTextSubtitleToAss(string inputPath, string outputPath, string language, + public async Task ConvertTextSubtitleToSrt(string inputPath, string outputPath, string language, CancellationToken cancellationToken) { var semaphore = GetLock(outputPath); @@ -293,7 +344,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { if (!File.Exists(outputPath)) { - await ConvertTextSubtitleToAssInternal(inputPath, outputPath, language).ConfigureAwait(false); + await ConvertTextSubtitleToSrtInternal(inputPath, outputPath, language).ConfigureAwait(false); } } finally @@ -303,17 +354,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles } /// <summary> - /// Converts the text subtitle to ass. + /// Converts the text subtitle to SRT internal. /// </summary> /// <param name="inputPath">The input path.</param> /// <param name="outputPath">The output path.</param> /// <param name="language">The language.</param> /// <returns>Task.</returns> - /// <exception cref="System.ArgumentNullException">inputPath + /// <exception cref="System.ArgumentNullException"> + /// inputPath /// or - /// outputPath</exception> + /// outputPath + /// </exception> /// <exception cref="System.ApplicationException"></exception> - private async Task ConvertTextSubtitleToAssInternal(string inputPath, string outputPath, string language) + private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string outputPath, string language) { if (string.IsNullOrEmpty(inputPath)) { @@ -342,12 +395,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles { RedirectStandardOutput = false, RedirectStandardError = true, + RedirectStandardInput = true, CreateNoWindow = true, UseShellExecute = false, FileName = _mediaEncoder.EncoderPath, - Arguments = - string.Format("{0} -i \"{1}\" -c:s ass \"{2}\"", encodingParam, inputPath, outputPath), + Arguments = string.Format("{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath), WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false @@ -385,8 +438,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { _logger.Info("Killing ffmpeg subtitle conversion process"); - process.Kill(); - + process.StandardInput.WriteLine("q"); process.WaitForExit(1000); await logTask.ConfigureAwait(false); @@ -446,13 +498,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <param name="inputFiles">The input files.</param> /// <param name="protocol">The protocol.</param> /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param> - /// <param name="copySubtitleStream">if set to true, copy stream instead of converting.</param> + /// <param name="outputCodec">The output codec.</param> /// <param name="outputPath">The output path.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> /// <exception cref="System.ArgumentException">Must use inputPath list overload</exception> private async Task ExtractTextSubtitle(string[] inputFiles, MediaProtocol protocol, int subtitleStreamIndex, - bool copySubtitleStream, string outputPath, CancellationToken cancellationToken) + string outputCodec, string outputPath, CancellationToken cancellationToken) { var semaphore = GetLock(outputPath); @@ -463,7 +515,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (!File.Exists(outputPath)) { await ExtractTextSubtitleInternal(_mediaEncoder.GetInputArgument(inputFiles, protocol), subtitleStreamIndex, - copySubtitleStream, outputPath, cancellationToken).ConfigureAwait(false); + outputCodec, outputPath, cancellationToken).ConfigureAwait(false); } } finally @@ -472,23 +524,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - /// <summary> - /// Extracts the text subtitle. - /// </summary> - /// <param name="inputPath">The input path.</param> - /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param> - /// <param name="copySubtitleStream">if set to true, copy stream instead of converting.</param> - /// <param name="outputPath">The output path.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="System.ArgumentNullException">inputPath - /// or - /// outputPath - /// or - /// cancellationToken</exception> - /// <exception cref="System.ApplicationException"></exception> private async Task ExtractTextSubtitleInternal(string inputPath, int subtitleStreamIndex, - bool copySubtitleStream, string outputPath, CancellationToken cancellationToken) + string outputCodec, string outputPath, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(inputPath)) { @@ -502,14 +539,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); - var processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s ass \"{2}\"", inputPath, - subtitleStreamIndex, outputPath); - - if (copySubtitleStream) - { - processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s copy \"{2}\"", inputPath, - subtitleStreamIndex, outputPath); - } + var processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"", inputPath, + subtitleStreamIndex, outputCodec, outputPath); var process = new Process { @@ -520,6 +551,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles RedirectStandardOutput = false, RedirectStandardError = true, + RedirectStandardInput = true, FileName = _mediaEncoder.EncoderPath, Arguments = processArgs, @@ -559,8 +591,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { _logger.Info("Killing ffmpeg subtitle extraction process"); - process.Kill(); - + process.StandardInput.WriteLine("q"); process.WaitForExit(1000); } catch (Exception ex) diff --git a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs new file mode 100644 index 000000000..955b36ecd --- /dev/null +++ b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; + +namespace MediaBrowser.MediaEncoding.Subtitles +{ + public class TtmlWriter : ISubtitleWriter + { + public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) + { + // Example: https://github.com/zmalltalker/ttml2vtt/blob/master/data/sample.xml + // Parser example: https://github.com/mozilla/popcorn-js/blob/master/parsers/parserTTML/popcorn.parserTTML.js + + using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + { + writer.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>"); + writer.WriteLine("<tt xmlns=\"http://www.w3.org/ns/ttml\" xmlns:tts=\"http://www.w3.org/2006/04/ttaf1#styling\" lang=\"no\">"); + + writer.WriteLine("<head>"); + writer.WriteLine("<styling>"); + writer.WriteLine("<style id=\"italic\" tts:fontStyle=\"italic\" />"); + writer.WriteLine("<style id=\"left\" tts:textAlign=\"left\" />"); + writer.WriteLine("<style id=\"center\" tts:textAlign=\"center\" />"); + writer.WriteLine("<style id=\"right\" tts:textAlign=\"right\" />"); + writer.WriteLine("</styling>"); + writer.WriteLine("</head>"); + + writer.WriteLine("<body>"); + writer.WriteLine("<div>"); + + foreach (var trackEvent in info.TrackEvents) + { + var text = trackEvent.Text; + + text = Regex.Replace(text, @"\\n", "<br/>", RegexOptions.IgnoreCase); + + writer.WriteLine("<p begin=\"{0}\" dur=\"{1}\">{2}</p>", + trackEvent.StartPositionTicks, + (trackEvent.EndPositionTicks - trackEvent.StartPositionTicks), + text); + } + + writer.WriteLine("</div>"); + writer.WriteLine("</body>"); + + writer.WriteLine("</tt>"); + } + } + + private string FormatTime(long ticks) + { + var time = TimeSpan.FromTicks(ticks); + + return string.Format(@"{0:hh\:mm\:ss\,fff}", time); + } + } +} diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs index 09f45aa61..fa53e4d13 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs @@ -18,12 +18,21 @@ namespace MediaBrowser.MediaEncoding.Subtitles { cancellationToken.ThrowIfCancellationRequested(); - writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff}", TimeSpan.FromTicks(trackEvent.StartPositionTicks), TimeSpan.FromTicks(trackEvent.EndPositionTicks)); + TimeSpan startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks); + TimeSpan endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks); + + // make sure the start and end times are different and seqential + if (endTime.TotalMilliseconds <= startTime.TotalMilliseconds) + { + endTime = startTime.Add(TimeSpan.FromMilliseconds(1)); + } + + writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff}", startTime, endTime); var text = trackEvent.Text; // TODO: Not sure how to handle these - text = Regex.Replace(text, @"\\N", " ", RegexOptions.IgnoreCase); + text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase); writer.WriteLine(text); writer.WriteLine(string.Empty); |
