From 77ad0fc3365d9e880a47472f5780796570a06cab Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 11 Jun 2014 10:42:03 -0400 Subject: [PATCH] fixes #674 - Support converting subtitles to webvtt --- MediaBrowser.Api/Library/SubtitleService.cs | 48 +- MediaBrowser.Common/Net/MimeTypes.cs | 5 + .../MediaEncoding/ISubtitleEncoder.cs | 12 +- .../MediaBrowser.MediaEncoding.csproj | 1 + .../Subtitles/ISubtitleParser.cs | 9 +- .../Subtitles/ISubtitleWriter.cs | 4 +- .../Subtitles/SrtParser.cs | 12 +- .../Subtitles/SsaParser.cs | 5 +- .../Subtitles/SubtitleEncoder.cs | 619 ++++++++++++++++++ .../Subtitles/VttWriter.cs | 20 +- .../MediaEncoder/EncodingManager.cs | 6 +- .../ApplicationHost.cs | 5 +- .../MediaEncoding/Subtitles/SrtParserTests.cs | 3 +- .../MediaEncoding/Subtitles/SsaParserTests.cs | 3 +- .../MediaEncoding/Subtitles/VttWriterTest.cs | 3 +- Nuget/MediaBrowser.Common.Internal.nuspec | 4 +- Nuget/MediaBrowser.Common.nuspec | 2 +- Nuget/MediaBrowser.Server.Core.nuspec | 4 +- 18 files changed, 726 insertions(+), 39 deletions(-) create mode 100644 MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs diff --git a/MediaBrowser.Api/Library/SubtitleService.cs b/MediaBrowser.Api/Library/SubtitleService.cs index 78ae627eab..12e3ef1388 100644 --- a/MediaBrowser.Api/Library/SubtitleService.cs +++ b/MediaBrowser.Api/Library/SubtitleService.cs @@ -1,7 +1,6 @@ -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Entities; @@ -9,6 +8,7 @@ using MediaBrowser.Model.Providers; using ServiceStack; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -16,6 +16,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Api.Library { [Route("/Videos/{Id}/Subtitles/{Index}", "GET", Summary = "Gets an external subtitle file")] + [Route("/Videos/{Id}/Subtitles/{Index}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format (vtt).")] public class GetSubtitle { /// @@ -25,8 +26,14 @@ namespace MediaBrowser.Api.Library [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string Id { get; set; } + [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] + public string MediaSourceId { get; set; } + [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")] public int Index { get; set; } + + [ApiMember(Name = "Format", Description = "Format", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Format { get; set; } } [Route("/Videos/{Id}/Subtitles/{Index}", "DELETE", Summary = "Deletes an external subtitle file")] @@ -81,13 +88,13 @@ namespace MediaBrowser.Api.Library { private readonly ILibraryManager _libraryManager; private readonly ISubtitleManager _subtitleManager; - private readonly IItemRepository _itemRepo; + private readonly ISubtitleEncoder _subtitleEncoder; - public SubtitleService(ILibraryManager libraryManager, ISubtitleManager subtitleManager, IItemRepository itemRepo) + public SubtitleService(ILibraryManager libraryManager, ISubtitleManager subtitleManager, ISubtitleEncoder subtitleEncoder) { _libraryManager = libraryManager; _subtitleManager = subtitleManager; - _itemRepo = itemRepo; + _subtitleEncoder = subtitleEncoder; } public object Get(SearchRemoteSubtitles request) @@ -100,21 +107,30 @@ namespace MediaBrowser.Api.Library } public object Get(GetSubtitle request) { - var subtitleStream = _itemRepo.GetMediaStreams(new MediaStreamQuery + if (string.IsNullOrEmpty(request.Format)) { + var item = (Video)_libraryManager.GetItemById(new Guid(request.Id)); - Index = request.Index, - ItemId = new Guid(request.Id), - Type = MediaStreamType.Subtitle + var mediaSource = item.GetMediaSources(false) + .First(i => string.Equals(i.Id, request.MediaSourceId ?? request.Id)); - }).FirstOrDefault(); + var subtitleStream = mediaSource.MediaStreams + .First(i => i.Type == MediaStreamType.Subtitle && i.Index == request.Index); - if (subtitleStream == null) - { - throw new ResourceNotFoundException(); + return ToStaticFileResult(subtitleStream.Path); } - return ToStaticFileResult(subtitleStream.Path); + var stream = GetSubtitles(request).Result; + + return ResultFactory.GetResult(stream, Common.Net.MimeTypes.GetMimeType("file." + request.Format)); + } + + private async Task GetSubtitles(GetSubtitle request) + { + var stream = await _subtitleEncoder.GetSubtitles(request.Id, request.MediaSourceId, request.Index, request.Format, + CancellationToken.None); + + return stream; } public void Delete(DeleteSubtitle request) @@ -135,7 +151,7 @@ namespace MediaBrowser.Api.Library { var result = _subtitleManager.GetRemoteSubtitles(request.Id, CancellationToken.None).Result; - return ResultFactory.GetResult(result.Stream, MimeTypes.GetMimeType("file." + result.Format)); + return ResultFactory.GetResult(result.Stream, Common.Net.MimeTypes.GetMimeType("file." + result.Format)); } public void Post(DownloadRemoteSubtitles request) diff --git a/MediaBrowser.Common/Net/MimeTypes.cs b/MediaBrowser.Common/Net/MimeTypes.cs index 85b9b1f38f..d85a2fd1e0 100644 --- a/MediaBrowser.Common/Net/MimeTypes.cs +++ b/MediaBrowser.Common/Net/MimeTypes.cs @@ -223,6 +223,11 @@ namespace MediaBrowser.Common.Net return "text/plain"; } + if (ext.Equals(".vtt", StringComparison.OrdinalIgnoreCase)) + { + return "text/vtt"; + } + throw new ArgumentException("Argument not supported: " + path); } } diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs index f171d6f77c..8f85895f0c 100644 --- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.IO; using System.Threading; using System.Threading.Tasks; @@ -7,9 +6,16 @@ namespace MediaBrowser.Controller.MediaEncoding { public interface ISubtitleEncoder { - Task ConvertTextSubtitle(String stream, + Task ConvertSubtitles( + Stream stream, string inputFormat, string outputFormat, CancellationToken cancellationToken); + + Task GetSubtitles(string itemId, + string mediaSourceId, + int subtitleStreamIndex, + string outputFormat, + CancellationToken cancellationToken); } } diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 5abc509d0c..882e211d40 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -60,6 +60,7 @@ + diff --git a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs index b983bc5d4a..5b072a450b 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs @@ -1,9 +1,16 @@ using System.IO; +using System.Threading; namespace MediaBrowser.MediaEncoding.Subtitles { public interface ISubtitleParser { - SubtitleTrackInfo Parse(Stream stream); + /// + /// Parses the specified stream. + /// + /// The stream. + /// The cancellation token. + /// SubtitleTrackInfo. + SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs index 9cbd09e7be..eb29e6c17f 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading; namespace MediaBrowser.MediaEncoding.Subtitles { @@ -12,6 +13,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// /// The information. /// The stream. - void Write(SubtitleTrackInfo info, Stream stream); + /// The cancellation token. + void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs index 09bc52df41..80fd0d6022 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs @@ -3,25 +3,35 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text.RegularExpressions; +using System.Threading; namespace MediaBrowser.MediaEncoding.Subtitles { public class SrtParser : ISubtitleParser { private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public SubtitleTrackInfo Parse(Stream stream) { + public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) + { var trackInfo = new SubtitleTrackInfo(); using ( var reader = new StreamReader(stream)) { string line; while ((line = reader.ReadLine()) != null) { + cancellationToken.ThrowIfCancellationRequested(); + if (string.IsNullOrWhiteSpace(line)) { continue; } var subEvent = new SubtitleTrackEvent {Id = line}; line = reader.ReadLine(); + + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + var time = Regex.Split(line, @"[\t ]*-->[\t ]*"); subEvent.StartPositionTicks = GetTicks(time[0]); var endTime = time[1]; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs index 996ef1c4e2..72c8076e7c 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; namespace MediaBrowser.MediaEncoding.Subtitles { @@ -11,7 +12,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public SubtitleTrackInfo Parse(Stream stream) + public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) { var trackInfo = new SubtitleTrackInfo(); var eventIndex = 1; @@ -24,6 +25,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles while ((line = reader.ReadLine()) != null) { + cancellationToken.ThrowIfCancellationRequested(); + if (string.IsNullOrWhiteSpace(line)) { continue; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs new file mode 100644 index 0000000000..7b783711a9 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -0,0 +1,619 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.MediaEncoding.Subtitles +{ + public class SubtitleEncoder : ISubtitleEncoder + { + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + private readonly IMediaEncoder _mediaEncoder; + + public SubtitleEncoder(ILibraryManager libraryManager, ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder) + { + _libraryManager = libraryManager; + _logger = logger; + _appPaths = appPaths; + _fileSystem = fileSystem; + _mediaEncoder = mediaEncoder; + } + + private string SubtitleCachePath + { + get + { + return Path.Combine(_appPaths.CachePath, "subtitles"); + } + } + + public async Task ConvertSubtitles(Stream stream, + string inputFormat, + string outputFormat, + CancellationToken cancellationToken) + { + var ms = new MemoryStream(); + + try + { + if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase)) + { + await stream.CopyToAsync(ms, 81920, cancellationToken).ConfigureAwait(false); + } + else + { + var trackInfo = await GetTrackInfo(stream, inputFormat, cancellationToken).ConfigureAwait(false); + + var writer = GetWriter(outputFormat); + + writer.Write(trackInfo, ms, cancellationToken); + } + ms.Position = 0; + } + catch + { + ms.Dispose(); + throw; + } + + return ms; + } + + public async Task GetSubtitles(string itemId, + string mediaSourceId, + int subtitleStreamIndex, + string outputFormat, + CancellationToken cancellationToken) + { + var subtitle = await GetSubtitleStream(itemId, mediaSourceId, subtitleStreamIndex, cancellationToken) + .ConfigureAwait(false); + + using (var stream = subtitle.Item1) + { + var inputFormat = subtitle.Item2; + + return await ConvertSubtitles(stream, inputFormat, outputFormat, cancellationToken).ConfigureAwait(false); + } + } + + private async Task> GetSubtitleStream(string itemId, + string mediaSourceId, + int subtitleStreamIndex, + CancellationToken cancellationToken) + { + var item = (Video)_libraryManager.GetItemById(new Guid(itemId)); + + var mediaSource = item.GetMediaSources(false) + .First(i => string.Equals(i.Id, mediaSourceId)); + + var subtitleStream = mediaSource.MediaStreams + .First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex); + + var inputType = mediaSource.LocationType == LocationType.Remote ? InputType.Url : InputType.File; + var inputFiles = new[] { mediaSource.Path }; + + if (mediaSource.VideoType.HasValue) + { + if (mediaSource.VideoType.Value == VideoType.BluRay) + { + inputType = InputType.Bluray; + var mediaSourceItem = (Video)_libraryManager.GetItemById(new Guid(mediaSourceId)); + inputFiles = mediaSourceItem.GetPlayableStreamFiles().ToArray(); + } + else if (mediaSource.VideoType.Value == VideoType.Dvd) + { + inputType = InputType.Dvd; + var mediaSourceItem = (Video)_libraryManager.GetItemById(new Guid(mediaSourceId)); + inputFiles = mediaSourceItem.GetPlayableStreamFiles().ToArray(); + } + } + + var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, inputType, subtitleStream, cancellationToken).ConfigureAwait(false); + + var stream = File.OpenRead(fileInfo.Item1); + + return new Tuple(stream, fileInfo.Item2); + } + + private async Task> GetReadableFile(string mediaPath, + string[] inputFiles, + InputType type, + MediaStream subtitleStream, + CancellationToken cancellationToken) + { + if (!subtitleStream.IsExternal) + { + // Extract + var outputPath = GetSubtitleCachePath(mediaPath, subtitleStream.Index, ".ass"); + + await ExtractTextSubtitle(inputFiles, type, subtitleStream.Index, false, outputPath, cancellationToken) + .ConfigureAwait(false); + + return new Tuple(outputPath, "ass"); + } + + var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec) + .TrimStart('.'); + + if (GetReader(currentFormat, false) == null) + { + // Convert + var outputPath = GetSubtitleCachePath(mediaPath, subtitleStream.Index, ".ass"); + + await ConvertTextSubtitleToAss(subtitleStream.Path, outputPath, subtitleStream.Language, cancellationToken) + .ConfigureAwait(false); + + return new Tuple(outputPath, "ass"); + } + + return new Tuple(subtitleStream.Path, currentFormat); + } + + private async Task GetTrackInfo(Stream stream, + string inputFormat, + CancellationToken cancellationToken) + { + var reader = GetReader(inputFormat, true); + + return reader.Parse(stream, cancellationToken); + } + + private ISubtitleParser GetReader(string format, bool throwIfMissing) + { + if (string.IsNullOrEmpty(format)) + { + throw new ArgumentNullException("format"); + } + + if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)) + { + return new SrtParser(); + } + if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase) || + string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)) + { + return new SsaParser(); + } + + if (throwIfMissing) + { + throw new ArgumentException("Unsupported format: " + format); + } + + return null; + } + + private ISubtitleWriter GetWriter(string format) + { + if (string.IsNullOrEmpty(format)) + { + throw new ArgumentNullException("format"); + } + + if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase)) + { + return new VttWriter(); + } + + throw new ArgumentException("Unsupported format: " + format); + } + + /// + /// The _semaphoreLocks + /// + private readonly ConcurrentDictionary _semaphoreLocks = + new ConcurrentDictionary(); + + /// + /// Gets the lock. + /// + /// The filename. + /// System.Object. + private SemaphoreSlim GetLock(string filename) + { + return _semaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); + } + + /// + /// Converts the text subtitle to ass. + /// + /// The input path. + /// The output path. + /// The language. + /// The cancellation token. + /// Task. + public async Task ConvertTextSubtitleToAss(string inputPath, string outputPath, string language, + CancellationToken cancellationToken) + { + var semaphore = GetLock(outputPath); + + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + if (!File.Exists(outputPath)) + { + await ConvertTextSubtitleToAssInternal(inputPath, outputPath, language).ConfigureAwait(false); + } + } + finally + { + semaphore.Release(); + } + } + + /// + /// Converts the text subtitle to ass. + /// + /// The input path. + /// The output path. + /// The language. + /// Task. + /// inputPath + /// or + /// outputPath + /// + private async Task ConvertTextSubtitleToAssInternal(string inputPath, string outputPath, string language) + { + if (string.IsNullOrEmpty(inputPath)) + { + throw new ArgumentNullException("inputPath"); + } + + if (string.IsNullOrEmpty(outputPath)) + { + throw new ArgumentNullException("outputPath"); + } + + + var encodingParam = string.IsNullOrEmpty(language) + ? string.Empty + : _mediaEncoder.GetSubtitleLanguageEncodingParam(inputPath, language); + + if (!string.IsNullOrEmpty(encodingParam)) + { + encodingParam = " -sub_charenc " + encodingParam; + } + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + RedirectStandardOutput = false, + RedirectStandardError = true, + + CreateNoWindow = true, + UseShellExecute = false, + FileName = _mediaEncoder.EncoderPath, + Arguments = + string.Format("{0} -i \"{1}\" -c:s ass \"{2}\"", encodingParam, inputPath, outputPath), + + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + } + }; + + _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); + + var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-sub-convert-" + Guid.NewGuid() + ".txt"); + Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); + + var logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, + true); + + try + { + process.Start(); + } + catch (Exception ex) + { + logFileStream.Dispose(); + + _logger.ErrorException("Error starting ffmpeg", ex); + + throw; + } + + var logTask = process.StandardError.BaseStream.CopyToAsync(logFileStream); + + var ranToCompletion = process.WaitForExit(60000); + + if (!ranToCompletion) + { + try + { + _logger.Info("Killing ffmpeg subtitle conversion process"); + + process.Kill(); + + process.WaitForExit(1000); + + await logTask.ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error killing subtitle conversion process", ex); + } + finally + { + logFileStream.Dispose(); + } + } + + var exitCode = ranToCompletion ? process.ExitCode : -1; + + process.Dispose(); + + var failed = false; + + if (exitCode == -1) + { + failed = true; + + if (File.Exists(outputPath)) + { + try + { + _logger.Info("Deleting converted subtitle due to failure: ", outputPath); + File.Delete(outputPath); + } + catch (IOException ex) + { + _logger.ErrorException("Error deleting converted subtitle {0}", ex, outputPath); + } + } + } + else if (!File.Exists(outputPath)) + { + failed = true; + } + + if (failed) + { + var msg = string.Format("ffmpeg subtitle converted failed for {0}", inputPath); + + _logger.Error(msg); + + throw new ApplicationException(msg); + } + await SetAssFont(outputPath).ConfigureAwait(false); + } + + /// + /// Extracts the text subtitle. + /// + /// The input files. + /// The type. + /// Index of the subtitle stream. + /// if set to true, copy stream instead of converting. + /// The output path. + /// The cancellation token. + /// Task. + /// Must use inputPath list overload + private async Task ExtractTextSubtitle(string[] inputFiles, InputType type, int subtitleStreamIndex, + bool copySubtitleStream, string outputPath, CancellationToken cancellationToken) + { + var semaphore = GetLock(outputPath); + + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + if (!File.Exists(outputPath)) + { + await ExtractTextSubtitleInternal(_mediaEncoder.GetInputArgument(inputFiles, type), subtitleStreamIndex, + copySubtitleStream, outputPath, cancellationToken).ConfigureAwait(false); + } + } + finally + { + semaphore.Release(); + } + } + + /// + /// Extracts the text subtitle. + /// + /// The input path. + /// Index of the subtitle stream. + /// if set to true, copy stream instead of converting. + /// The output path. + /// The cancellation token. + /// Task. + /// inputPath + /// or + /// outputPath + /// or + /// cancellationToken + /// + private async Task ExtractTextSubtitleInternal(string inputPath, int subtitleStreamIndex, + bool copySubtitleStream, string outputPath, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(inputPath)) + { + throw new ArgumentNullException("inputPath"); + } + + if (string.IsNullOrEmpty(outputPath)) + { + throw new ArgumentNullException("outputPath"); + } + + string 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 process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + + RedirectStandardOutput = false, + RedirectStandardError = true, + + FileName = _mediaEncoder.EncoderPath, + Arguments = processArgs, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + } + }; + + _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); + + var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-sub-extract-" + Guid.NewGuid() + ".txt"); + Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); + + var logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, + true); + + try + { + process.Start(); + } + catch (Exception ex) + { + logFileStream.Dispose(); + + _logger.ErrorException("Error starting ffmpeg", ex); + + throw; + } + + process.StandardError.BaseStream.CopyToAsync(logFileStream); + + var ranToCompletion = process.WaitForExit(60000); + + if (!ranToCompletion) + { + try + { + _logger.Info("Killing ffmpeg subtitle extraction process"); + + process.Kill(); + + process.WaitForExit(1000); + } + catch (Exception ex) + { + _logger.ErrorException("Error killing subtitle extraction process", ex); + } + finally + { + logFileStream.Dispose(); + } + } + + var exitCode = ranToCompletion ? process.ExitCode : -1; + + process.Dispose(); + + var failed = false; + + if (exitCode == -1) + { + failed = true; + + if (File.Exists(outputPath)) + { + try + { + _logger.Info("Deleting extracted subtitle due to failure: ", outputPath); + File.Delete(outputPath); + } + catch (IOException ex) + { + _logger.ErrorException("Error deleting extracted subtitle {0}", ex, outputPath); + } + } + } + else if (!File.Exists(outputPath)) + { + failed = true; + } + + if (failed) + { + var msg = string.Format("ffmpeg subtitle extraction failed for {0} to {1}", inputPath, outputPath); + + _logger.Error(msg); + + throw new ApplicationException(msg); + } + else + { + var msg = string.Format("ffmpeg subtitle extraction completed for {0} to {1}", inputPath, outputPath); + + _logger.Info(msg); + } + + await SetAssFont(outputPath).ConfigureAwait(false); + } + + /// + /// Sets the ass font. + /// + /// The file. + /// Task. + private async Task SetAssFont(string file) + { + _logger.Info("Setting ass font within {0}", file); + + string text; + Encoding encoding; + + using (var reader = new StreamReader(file, true)) + { + encoding = reader.CurrentEncoding; + + text = await reader.ReadToEndAsync().ConfigureAwait(false); + } + + var newText = text.Replace(",Arial,", ",Arial Unicode MS,"); + + if (!string.Equals(text, newText)) + { + using (var writer = new StreamWriter(file, false, encoding)) + { + writer.Write(newText); + } + } + } + + private string GetSubtitleCachePath(string mediaPath, int subtitleStreamIndex, string outputSubtitleExtension) + { + var ticksParam = string.Empty; + + var date = _fileSystem.GetLastWriteTimeUtc(mediaPath); + + var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension; + + var prefix = filename.Substring(0, 1); + + return Path.Combine(SubtitleCachePath, prefix, filename); + } + } +} diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs index dc750fb6b1..4768b9632f 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs @@ -1,26 +1,34 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Threading; namespace MediaBrowser.MediaEncoding.Subtitles { public class VttWriter : ISubtitleWriter { - public void Write(SubtitleTrackInfo info, Stream stream) { - using (var writer = new StreamWriter(stream)) + public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) + { + var writer = new StreamWriter(stream); + + try { writer.WriteLine("WEBVTT"); writer.WriteLine(string.Empty); foreach (var trackEvent in info.TrackEvents) { + cancellationToken.ThrowIfCancellationRequested(); + writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff}", TimeSpan.FromTicks(trackEvent.StartPositionTicks), TimeSpan.FromTicks(trackEvent.EndPositionTicks)); writer.WriteLine(trackEvent.Text); writer.WriteLine(string.Empty); } } + catch + { + writer.Dispose(); + + throw; + } } } } diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs index f4b8671698..1f2db0dcf1 100644 --- a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -26,7 +26,11 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder private readonly IMediaEncoder _encoder; private readonly IChapterManager _chapterManager; - public EncodingManager(IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger, IMediaEncoder encoder, IChapterManager chapterManager) + public EncodingManager(IServerConfigurationManager config, + IFileSystem fileSystem, + ILogger logger, + IMediaEncoder encoder, + IChapterManager chapterManager) { _config = config; _fileSystem = fileSystem; diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 724641b242..183900171e 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -40,6 +40,7 @@ using MediaBrowser.Dlna.ContentDirectory; using MediaBrowser.Dlna.Main; using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.Encoder; +using MediaBrowser.MediaEncoding.Subtitles; using MediaBrowser.Model.Logging; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.System; @@ -550,6 +551,8 @@ namespace MediaBrowser.ServerApplication MediaEncoder, ChapterManager); RegisterSingleInstance(EncodingManager); + RegisterSingleInstance(new SubtitleEncoder(LibraryManager, LogManager.GetLogger("SubtitleEncoder"), ApplicationPaths, FileSystemManager, MediaEncoder)); + var displayPreferencesTask = Task.Run(async () => await ConfigureDisplayPreferencesRepositories().ConfigureAwait(false)); var itemsTask = Task.Run(async () => await ConfigureItemRepositories().ConfigureAwait(false)); var userdataTask = Task.Run(async () => await ConfigureUserDataRepositories().ConfigureAwait(false)); @@ -732,7 +735,7 @@ namespace MediaBrowser.ServerApplication SubtitleManager.AddParts(GetExports()); ChapterManager.AddParts(GetExports()); - + SessionManager.AddParts(GetExports()); ChannelManager.AddParts(GetExports(), GetExports()); diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs index 0d86fbdcd8..2c2c944b12 100644 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs +++ b/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using MediaBrowser.MediaEncoding.Subtitles; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -91,7 +92,7 @@ namespace MediaBrowser.Tests.MediaEncoding.Subtitles { var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\unit.srt"); - var result = sut.Parse(stream); + var result = sut.Parse(stream, CancellationToken.None); Assert.IsNotNull(result); Assert.AreEqual(expectedSubs.TrackEvents.Count,result.TrackEvents.Count); diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/SsaParserTests.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/SsaParserTests.cs index 51dc7f959e..3c278ae418 100644 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/SsaParserTests.cs +++ b/MediaBrowser.Tests/MediaEncoding/Subtitles/SsaParserTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using MediaBrowser.MediaEncoding.Subtitles; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -42,7 +43,7 @@ namespace MediaBrowser.Tests.MediaEncoding.Subtitles { var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\data.ssa"); - var result = sut.Parse(stream); + var result = sut.Parse(stream, CancellationToken.None); Assert.IsNotNull(result); Assert.AreEqual(expectedSubs.TrackEvents.Count,result.TrackEvents.Count); diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs index 5292ad3d22..7a4823ecf1 100644 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs +++ b/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Threading; using MediaBrowser.MediaEncoding.Subtitles; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -91,7 +92,7 @@ namespace MediaBrowser.Tests.MediaEncoding.Subtitles { File.Delete("testVTT.vtt"); using (var file = File.OpenWrite("testVTT.vtt")) { - sut.Write(infoSubs,file); + sut.Write(infoSubs, file, CancellationToken.None); } var result = File.ReadAllText("testVTT.vtt"); diff --git a/Nuget/MediaBrowser.Common.Internal.nuspec b/Nuget/MediaBrowser.Common.Internal.nuspec index d1e669b414..8aa32dc813 100644 --- a/Nuget/MediaBrowser.Common.Internal.nuspec +++ b/Nuget/MediaBrowser.Common.Internal.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Common.Internal - 3.0.400 + 3.0.401 MediaBrowser.Common.Internal Luke ebr,Luke,scottisafool @@ -12,7 +12,7 @@ Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption. Copyright © Media Browser 2013 - + diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index 873c308edc..0c97d21130 100644 --- a/Nuget/MediaBrowser.Common.nuspec +++ b/Nuget/MediaBrowser.Common.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Common - 3.0.400 + 3.0.401 MediaBrowser.Common Media Browser Team ebr,Luke,scottisafool diff --git a/Nuget/MediaBrowser.Server.Core.nuspec b/Nuget/MediaBrowser.Server.Core.nuspec index e97a0c7d8a..ed1c21b139 100644 --- a/Nuget/MediaBrowser.Server.Core.nuspec +++ b/Nuget/MediaBrowser.Server.Core.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Server.Core - 3.0.400 + 3.0.401 Media Browser.Server.Core Media Browser Team ebr,Luke,scottisafool @@ -12,7 +12,7 @@ Contains core components required to build plugins for Media Browser Server. Copyright © Media Browser 2013 - +