diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-06-11 10:42:03 -0400 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-06-11 10:42:03 -0400 |
| commit | 77ad0fc3365d9e880a47472f5780796570a06cab (patch) | |
| tree | 3b9d99d272e6119f4e7d1e7510498a468632009a | |
| parent | 437062b29e3e3456c15659666d6015356695913c (diff) | |
fixes #674 - Support converting subtitles to webvtt
18 files changed, 726 insertions, 39 deletions
diff --git a/MediaBrowser.Api/Library/SubtitleService.cs b/MediaBrowser.Api/Library/SubtitleService.cs index 78ae627ea..12e3ef138 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 { /// <summary> @@ -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<Stream> 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 85b9b1f38..d85a2fd1e 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 f171d6f77..8f85895f0 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<Stream> ConvertTextSubtitle(String stream, + Task<Stream> ConvertSubtitles( + Stream stream, string inputFormat, string outputFormat, CancellationToken cancellationToken); + + Task<Stream> 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 5abc509d0..882e211d4 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -60,6 +60,7 @@ <Compile Include="Subtitles\ISubtitleWriter.cs" /> <Compile Include="Subtitles\SrtParser.cs" /> <Compile Include="Subtitles\SsaParser.cs" /> + <Compile Include="Subtitles\SubtitleEncoder.cs" /> <Compile Include="Subtitles\SubtitleTrackInfo.cs" /> <Compile Include="Subtitles\VttWriter.cs" /> </ItemGroup> diff --git a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs index b983bc5d4..5b072a450 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); + /// <summary> + /// Parses the specified stream. + /// </summary> + /// <param name="stream">The stream.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>SubtitleTrackInfo.</returns> + SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs index 9cbd09e7b..eb29e6c17 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 /// </summary> /// <param name="info">The information.</param> /// <param name="stream">The stream.</param> - void Write(SubtitleTrackInfo info, Stream stream); + /// <param name="cancellationToken">The cancellation token.</param> + void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs index 09bc52df4..80fd0d602 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 996ef1c4e..72c8076e7 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 000000000..7b783711a --- /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<Stream> 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<Stream> 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<Tuple<Stream, string>> 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, string>(stream, fileInfo.Item2); + } + + private async Task<Tuple<string, string>> 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<string, string>(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<string, string>(outputPath, "ass"); + } + + return new Tuple<string, string>(subtitleStream.Path, currentFormat); + } + + private async Task<SubtitleTrackInfo> 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); + } + + /// <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> + /// Converts the text subtitle to ass. + /// </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, + 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(); + } + } + + /// <summary> + /// Converts the text subtitle to ass. + /// </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 + /// or + /// outputPath</exception> + /// <exception cref="System.ApplicationException"></exception> + 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); + } + + /// <summary> + /// Extracts the text subtitle. + /// </summary> + /// <param name="inputFiles">The input files.</param> + /// <param name="type">The type.</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.ArgumentException">Must use inputPath list overload</exception> + 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(); + } + } + + /// <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) + { + 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); + } + + /// <summary> + /// Sets the ass font. + /// </summary> + /// <param name="file">The file.</param> + /// <returns>Task.</returns> + 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 dc750fb6b..4768b9632 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 f4b867169..1f2db0dcf 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 724641b24..183900171 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<ISubtitleEncoder>(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<ISubtitleProvider>()); ChapterManager.AddParts(GetExports<IChapterProvider>()); - + SessionManager.AddParts(GetExports<ISessionControllerFactory>()); ChannelManager.AddParts(GetExports<IChannel>(), GetExports<IChannelFactory>()); diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs index 0d86fbdcd..2c2c944b1 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 51dc7f959..3c278ae41 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 5292ad3d2..7a4823ecf 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 d1e669b41..8aa32dc81 100644 --- a/Nuget/MediaBrowser.Common.Internal.nuspec +++ b/Nuget/MediaBrowser.Common.Internal.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"> <metadata> <id>MediaBrowser.Common.Internal</id> - <version>3.0.400</version> + <version>3.0.401</version> <title>MediaBrowser.Common.Internal</title> <authors>Luke</authors> <owners>ebr,Luke,scottisafool</owners> @@ -12,7 +12,7 @@ <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description> <copyright>Copyright © Media Browser 2013</copyright> <dependencies> - <dependency id="MediaBrowser.Common" version="3.0.400" /> + <dependency id="MediaBrowser.Common" version="3.0.401" /> <dependency id="NLog" version="2.1.0" /> <dependency id="SimpleInjector" version="2.5.0" /> <dependency id="sharpcompress" version="0.10.2" /> diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index 873c308ed..0c97d2113 100644 --- a/Nuget/MediaBrowser.Common.nuspec +++ b/Nuget/MediaBrowser.Common.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"> <metadata> <id>MediaBrowser.Common</id> - <version>3.0.400</version> + <version>3.0.401</version> <title>MediaBrowser.Common</title> <authors>Media Browser Team</authors> <owners>ebr,Luke,scottisafool</owners> diff --git a/Nuget/MediaBrowser.Server.Core.nuspec b/Nuget/MediaBrowser.Server.Core.nuspec index e97a0c7d8..ed1c21b13 100644 --- a/Nuget/MediaBrowser.Server.Core.nuspec +++ b/Nuget/MediaBrowser.Server.Core.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <metadata> <id>MediaBrowser.Server.Core</id> - <version>3.0.400</version> + <version>3.0.401</version> <title>Media Browser.Server.Core</title> <authors>Media Browser Team</authors> <owners>ebr,Luke,scottisafool</owners> @@ -12,7 +12,7 @@ <description>Contains core components required to build plugins for Media Browser Server.</description> <copyright>Copyright © Media Browser 2013</copyright> <dependencies> - <dependency id="MediaBrowser.Common" version="3.0.400" /> + <dependency id="MediaBrowser.Common" version="3.0.401" /> </dependencies> </metadata> <files> |
