From 33f4b2ed53b90af6dca441cdd52c6f41a66cac17 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 29 Apr 2013 12:01:23 -0400 Subject: [PATCH] subtitle extraction fixes --- .../Playback/BaseStreamingService.cs | 29 +++-- .../Playback/Hls/AudioHlsService.cs | 28 +++- .../Playback/Hls/BaseHlsService.cs | 26 +++- .../Playback/Hls/VideoHlsService.cs | 39 +++++- .../Playback/Progressive/AudioService.cs | 11 +- .../Playback/Progressive/VideoService.cs | 29 ++++- .../MediaEncoder/MediaEncoder.cs | 120 ++++++++++++++++-- 7 files changed, 244 insertions(+), 38 deletions(-) diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index a2894e1e70..3c30dfb260 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -77,8 +77,9 @@ namespace MediaBrowser.Api.Playback /// /// The output path. /// The state. + /// if set to true [perform subtitle conversions]. /// System.String. - protected abstract string GetCommandLineArguments(string outputPath, StreamState state); + protected abstract string GetCommandLineArguments(string outputPath, StreamState state, bool performSubtitleConversions); /// /// Gets the type of the transcoding job. @@ -104,7 +105,7 @@ namespace MediaBrowser.Api.Playback protected string GetOutputFilePath(StreamState state) { var folder = ApplicationPaths.EncodedMediaCachePath; - return Path.Combine(folder, GetCommandLineArguments("dummy\\dummy", state).GetMD5() + GetOutputFileExtension(state).ToLower()); + return Path.Combine(folder, GetCommandLineArguments("dummy\\dummy", state, false).GetMD5() + GetOutputFileExtension(state).ToLower()); } /// @@ -222,8 +223,9 @@ namespace MediaBrowser.Api.Playback /// /// The state. /// The output video codec. + /// if set to true [perform text subtitle conversion]. /// System.String. - protected string GetOutputSizeParam(StreamState state, string outputVideoCodec) + protected string GetOutputSizeParam(StreamState state, string outputVideoCodec, bool performTextSubtitleConversion) { // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/ @@ -235,7 +237,7 @@ namespace MediaBrowser.Api.Playback { if (state.SubtitleStream.Codec.IndexOf("srt", StringComparison.OrdinalIgnoreCase) != -1 || state.SubtitleStream.Codec.IndexOf("subrip", StringComparison.OrdinalIgnoreCase) != -1) { - assSubtitleParam = GetTextSubtitleParam((Video)state.Item, state.SubtitleStream, request.StartTimeTicks); + assSubtitleParam = GetTextSubtitleParam((Video)state.Item, state.SubtitleStream, request.StartTimeTicks, performTextSubtitleConversion); } } @@ -287,10 +289,11 @@ namespace MediaBrowser.Api.Playback /// The video. /// The subtitle stream. /// The start time ticks. + /// if set to true [perform conversion]. /// System.String. - protected string GetTextSubtitleParam(Video video, MediaStream subtitleStream, long? startTimeTicks) + protected string GetTextSubtitleParam(Video video, MediaStream subtitleStream, long? startTimeTicks, bool performConversion) { - var path = subtitleStream.IsExternal ? GetConvertedAssPath(video, subtitleStream, startTimeTicks) : GetExtractedAssPath(video, subtitleStream, startTimeTicks); + var path = subtitleStream.IsExternal ? GetConvertedAssPath(video, subtitleStream, startTimeTicks, performConversion) : GetExtractedAssPath(video, subtitleStream, startTimeTicks, performConversion); if (string.IsNullOrEmpty(path)) { @@ -306,14 +309,15 @@ namespace MediaBrowser.Api.Playback /// The video. /// The subtitle stream. /// The start time ticks. + /// if set to true [perform conversion]. /// System.String. - private string GetExtractedAssPath(Video video, MediaStream subtitleStream, long? startTimeTicks) + private string GetExtractedAssPath(Video video, MediaStream subtitleStream, long? startTimeTicks, bool performConversion) { var offset = TimeSpan.FromTicks(startTimeTicks ?? 0); var path = Kernel.Instance.FFMpegManager.GetSubtitleCachePath(video, subtitleStream.Index, offset, ".ass"); - if (!File.Exists(path)) + if (performConversion && !File.Exists(path)) { InputType type; @@ -340,8 +344,9 @@ namespace MediaBrowser.Api.Playback /// The video. /// The subtitle stream. /// The start time ticks. + /// if set to true [perform conversion]. /// System.String. - private string GetConvertedAssPath(Video video, MediaStream subtitleStream, long? startTimeTicks) + private string GetConvertedAssPath(Video video, MediaStream subtitleStream, long? startTimeTicks, bool performConversion) { var offset = startTimeTicks.HasValue ? TimeSpan.FromTicks(startTimeTicks.Value) @@ -349,7 +354,7 @@ namespace MediaBrowser.Api.Playback var path = Kernel.Instance.FFMpegManager.GetSubtitleCachePath(video, subtitleStream.Index, offset, ".ass"); - if (!File.Exists(path)) + if (performConversion && !File.Exists(path)) { try { @@ -381,7 +386,7 @@ namespace MediaBrowser.Api.Playback // Add resolution params, if specified if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue) { - outputSizeParam = GetOutputSizeParam(state, outputVideoCodec).TrimEnd('"'); + outputSizeParam = GetOutputSizeParam(state, outputVideoCodec, false).TrimEnd('"'); outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase)); } @@ -552,7 +557,7 @@ namespace MediaBrowser.Api.Playback FileName = MediaEncoder.EncoderPath, WorkingDirectory = Path.GetDirectoryName(MediaEncoder.EncoderPath), - Arguments = GetCommandLineArguments(outputPath, state), + Arguments = GetCommandLineArguments(outputPath, state, true), WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false diff --git a/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs b/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs index 989d99765c..f72006a235 100644 --- a/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs @@ -19,13 +19,24 @@ namespace MediaBrowser.Api.Playback.Hls } + /// + /// Class GetHlsAudioSegment + /// [Route("/Audio/{Id}/segments/{SegmentId}/stream.mp3", "GET")] [Route("/Audio/{Id}/segments/{SegmentId}/stream.aac", "GET")] [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] public class GetHlsAudioSegment { + /// + /// Gets or sets the id. + /// + /// The id. public string Id { get; set; } + /// + /// Gets or sets the segment id. + /// + /// The segment id. public string SegmentId { get; set; } } @@ -34,11 +45,24 @@ namespace MediaBrowser.Api.Playback.Hls /// public class AudioHlsService : BaseHlsService { + /// + /// Initializes a new instance of the class. + /// + /// The app paths. + /// The user manager. + /// The library manager. + /// The iso manager. + /// The media encoder. public AudioHlsService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder) : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder) { } + /// + /// Gets the specified request. + /// + /// The request. + /// System.Object. public object Get(GetHlsAudioSegment request) { var file = SegmentFilePrefix + request.SegmentId + Path.GetExtension(RequestContext.PathInfo); @@ -93,8 +117,9 @@ namespace MediaBrowser.Api.Playback.Hls /// Gets the video arguments. /// /// The state. + /// if set to true [perform subtitle conversion]. /// System.String. - protected override string GetVideoArguments(StreamState state) + protected override string GetVideoArguments(StreamState state, bool performSubtitleConversion) { // No video return string.Empty; @@ -105,6 +130,7 @@ namespace MediaBrowser.Api.Playback.Hls /// /// The state. /// System.String. + /// Must specify either aac or mp3 audio codec. /// Only aac and mp3 audio codecs are supported. protected override string GetSegmentFileExtension(StreamState state) { diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 6f4eae58cc..235eea7ecf 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -11,6 +11,9 @@ using System.Threading.Tasks; namespace MediaBrowser.Api.Playback.Hls { + /// + /// Class BaseHlsService + /// public abstract class BaseHlsService : BaseStreamingService { /// @@ -18,6 +21,14 @@ namespace MediaBrowser.Api.Playback.Hls /// public const string SegmentFilePrefix = "segment-"; + /// + /// Initializes a new instance of the class. + /// + /// The app paths. + /// The user manager. + /// The library manager. + /// The iso manager. + /// The media encoder. protected BaseHlsService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder) : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder) { @@ -26,13 +37,16 @@ namespace MediaBrowser.Api.Playback.Hls /// /// Gets the audio arguments. /// + /// The state. /// System.String. protected abstract string GetAudioArguments(StreamState state); /// /// Gets the video arguments. /// + /// The state. + /// if set to true [perform subtitle conversion]. /// System.String. - protected abstract string GetVideoArguments(StreamState state); + protected abstract string GetVideoArguments(StreamState state, bool performSubtitleConversion); /// /// Gets the segment file extension. @@ -40,7 +54,7 @@ namespace MediaBrowser.Api.Playback.Hls /// The state. /// System.String. protected abstract string GetSegmentFileExtension(StreamState state); - + /// /// Gets the type of the transcoding job. /// @@ -53,6 +67,7 @@ namespace MediaBrowser.Api.Playback.Hls /// /// Processes the request. /// + /// The request. /// System.Object. protected object ProcessRequest(StreamRequest request) { @@ -162,14 +177,15 @@ namespace MediaBrowser.Api.Playback.Hls } return count; } - + /// /// Gets the command line arguments. /// /// The output path. /// The state. + /// if set to true [perform subtitle conversions]. /// System.String. - protected override string GetCommandLineArguments(string outputPath, StreamState state) + protected override string GetCommandLineArguments(string outputPath, StreamState state, bool performSubtitleConversions) { var segmentOutputPath = Path.GetDirectoryName(outputPath); var segmentOutputName = SegmentFilePrefix + Path.GetFileNameWithoutExtension(outputPath); @@ -184,7 +200,7 @@ namespace MediaBrowser.Api.Playback.Hls GetInputArgument(state.Item, state.IsoMount), GetSlowSeekCommandLineParameter(state.Request), GetMapArgs(state), - GetVideoArguments(state), + GetVideoArguments(state, performSubtitleConversions), GetAudioArguments(state), outputPath, segmentOutputPath diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index 6888a1639e..42a1bf839f 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -8,6 +8,9 @@ using ServiceStack.ServiceHost; namespace MediaBrowser.Api.Playback.Hls { + /// + /// Class GetHlsVideoStream + /// [Route("/Videos/{Id}/stream.m3u8", "GET")] [Api(Description = "Gets a video stream using HTTP live streaming.")] public class GetHlsVideoStream : VideoStreamRequest @@ -15,22 +18,49 @@ namespace MediaBrowser.Api.Playback.Hls } + /// + /// Class GetHlsVideoSegment + /// [Route("/Videos/{Id}/segments/{SegmentId}/stream.ts", "GET")] [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] public class GetHlsVideoSegment { + /// + /// Gets or sets the id. + /// + /// The id. public string Id { get; set; } + /// + /// Gets or sets the segment id. + /// + /// The segment id. public string SegmentId { get; set; } } - + + /// + /// Class VideoHlsService + /// public class VideoHlsService : BaseHlsService { + /// + /// Initializes a new instance of the class. + /// + /// The app paths. + /// The user manager. + /// The library manager. + /// The iso manager. + /// The media encoder. public VideoHlsService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder) : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder) { } + /// + /// Gets the specified request. + /// + /// The request. + /// System.Object. public object Get(GetHlsVideoSegment request) { var file = SegmentFilePrefix + request.SegmentId + Path.GetExtension(RequestContext.PathInfo); @@ -49,7 +79,7 @@ namespace MediaBrowser.Api.Playback.Hls { return ProcessRequest(request); } - + /// /// Gets the audio arguments. /// @@ -105,8 +135,9 @@ namespace MediaBrowser.Api.Playback.Hls /// Gets the video arguments. /// /// The state. + /// if set to true [perform subtitle conversion]. /// System.String. - protected override string GetVideoArguments(StreamState state) + protected override string GetVideoArguments(StreamState state, bool performSubtitleConversion) { var codec = GetVideoCodec(state.VideoRequest); @@ -128,7 +159,7 @@ namespace MediaBrowser.Api.Playback.Hls // Add resolution params, if specified if (state.VideoRequest.Width.HasValue || state.VideoRequest.Height.HasValue || state.VideoRequest.MaxHeight.HasValue || state.VideoRequest.MaxWidth.HasValue) { - args += GetOutputSizeParam(state, codec); + args += GetOutputSizeParam(state, codec, performSubtitleConversion); } // Get the output framerate based on the FrameRate param diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs index 50c8210fad..f5a95d898f 100644 --- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs +++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs @@ -37,6 +37,14 @@ namespace MediaBrowser.Api.Playback.Progressive /// public class AudioService : BaseProgressiveStreamingService { + /// + /// Initializes a new instance of the class. + /// + /// The app paths. + /// The user manager. + /// The library manager. + /// The iso manager. + /// The media encoder. public AudioService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder) : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder) { @@ -67,9 +75,10 @@ namespace MediaBrowser.Api.Playback.Progressive /// /// The output path. /// The state. + /// if set to true [perform subtitle conversions]. /// System.String. /// Only aac and mp3 audio codecs are supported. - protected override string GetCommandLineArguments(string outputPath, StreamState state) + protected override string GetCommandLineArguments(string outputPath, StreamState state, bool performSubtitleConversions) { var request = state.Request; diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index 487f3286d7..a291c17aeb 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -1,11 +1,11 @@ -using System.IO; -using MediaBrowser.Common.IO; +using MediaBrowser.Common.IO; using MediaBrowser.Common.MediaInfo; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; -using System; using MediaBrowser.Controller.Library; using ServiceStack.ServiceHost; +using System; +using System.IO; namespace MediaBrowser.Api.Playback.Progressive { @@ -51,6 +51,14 @@ namespace MediaBrowser.Api.Playback.Progressive /// public class VideoService : BaseProgressiveStreamingService { + /// + /// Initializes a new instance of the class. + /// + /// The app paths. + /// The user manager. + /// The library manager. + /// The iso manager. + /// The media encoder. public VideoService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder) : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder) { @@ -66,6 +74,11 @@ namespace MediaBrowser.Api.Playback.Progressive return ProcessRequest(request, false); } + /// + /// Heads the specified request. + /// + /// The request. + /// System.Object. public object Head(GetVideoStream request) { return ProcessRequest(request, true); @@ -76,8 +89,9 @@ namespace MediaBrowser.Api.Playback.Progressive /// /// The output path. /// The state. + /// if set to true [perform subtitle conversions]. /// System.String. - protected override string GetCommandLineArguments(string outputPath, StreamState state) + protected override string GetCommandLineArguments(string outputPath, StreamState state, bool performSubtitleConversions) { var video = (Video)state.Item; @@ -103,7 +117,7 @@ namespace MediaBrowser.Api.Playback.Progressive GetSlowSeekCommandLineParameter(state.Request), keyFrame, GetMapArgs(state), - GetVideoArguments(state, videoCodec), + GetVideoArguments(state, videoCodec, performSubtitleConversions), threads, GetAudioArguments(state), format, @@ -116,8 +130,9 @@ namespace MediaBrowser.Api.Playback.Progressive /// /// The state. /// The video codec. + /// if set to true [perform subtitle conversion]. /// System.String. - private string GetVideoArguments(StreamState state, string codec) + private string GetVideoArguments(StreamState state, string codec, bool performSubtitleConversion) { var args = "-vcodec " + codec; @@ -136,7 +151,7 @@ namespace MediaBrowser.Api.Playback.Progressive // Add resolution params, if specified if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue) { - args += GetOutputSizeParam(state, codec); + args += GetOutputSizeParam(state, codec, performSubtitleConversion); } if (request.Framerate.HasValue) diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs b/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs index b52b0c93c7..001ba1e293 100644 --- a/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs +++ b/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs @@ -558,6 +558,9 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder { StartInfo = new ProcessStartInfo { + RedirectStandardOutput = false, + RedirectStandardError = true, + CreateNoWindow = true, UseShellExecute = false, FileName = FFMpegPath, @@ -571,9 +574,57 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder await _subtitleExtractionResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - var ranToCompletion = StartAndWaitForProcess(process); + var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-sub-convert-" + Guid.NewGuid() + ".txt"); + + var logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous); + + try + { + process.Start(); + } + catch (Exception ex) + { + _subtitleExtractionResourcePool.Release(); + + 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 process"); + + process.Kill(); - _subtitleExtractionResourcePool.Release(); + process.WaitForExit(1000); + } + catch (Win32Exception ex) + { + _logger.ErrorException("Error killing process", ex); + } + catch (InvalidOperationException ex) + { + _logger.ErrorException("Error killing process", ex); + } + catch (NotSupportedException ex) + { + _logger.ErrorException("Error killing process", ex); + } + finally + { + logFileStream.Dispose(); + _subtitleExtractionResourcePool.Release(); + } + } var exitCode = ranToCompletion ? process.ExitCode : -1; @@ -594,7 +645,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder } catch (IOException ex) { - _logger.ErrorException("Error deleting converted subtitle {0}", ex, outputPath); + _logger.ErrorException("Error converted extracted subtitle {0}", ex, outputPath); } } } @@ -605,7 +656,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder if (failed) { - var msg = string.Format("ffmpeg subtitle conversion failed for {0}", inputPath); + var msg = string.Format("ffmpeg subtitle converted failed for {0}", inputPath); _logger.Error(msg); @@ -669,6 +720,10 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder { CreateNoWindow = true, UseShellExecute = false, + + RedirectStandardOutput = false, + RedirectStandardError = true, + FileName = FFMpegPath, Arguments = string.Format("{0}-i {1} -map 0:{2} -an -vn -c:s ass \"{3}\"", offsetParam, inputPath, subtitleStreamIndex, outputPath), WindowStyle = ProcessWindowStyle.Hidden, @@ -680,9 +735,57 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder await _subtitleExtractionResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - var ranToCompletion = StartAndWaitForProcess(process); + var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-sub-extract-" + Guid.NewGuid() + ".txt"); + + var logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous); + + try + { + process.Start(); + } + catch (Exception ex) + { + _subtitleExtractionResourcePool.Release(); + + 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 process"); + + process.Kill(); - _subtitleExtractionResourcePool.Release(); + process.WaitForExit(1000); + } + catch (Win32Exception ex) + { + _logger.ErrorException("Error killing process", ex); + } + catch (InvalidOperationException ex) + { + _logger.ErrorException("Error killing process", ex); + } + catch (NotSupportedException ex) + { + _logger.ErrorException("Error killing process", ex); + } + finally + { + logFileStream.Dispose(); + _subtitleExtractionResourcePool.Release(); + } + } var exitCode = ranToCompletion ? process.ExitCode : -1; @@ -857,12 +960,13 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder /// Starts the and wait for process. /// /// The process. + /// The timeout. /// true if XXXX, false otherwise - private bool StartAndWaitForProcess(Process process) + private bool StartAndWaitForProcess(Process process, int timeout = 10000) { process.Start(); - var ranToCompletion = process.WaitForExit(10000); + var ranToCompletion = process.WaitForExit(timeout); if (!ranToCompletion) {