diff --git a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs b/Emby.Server.Implementations/Diagnostics/CommonProcess.cs index 55539eafc..78b22bda3 100644 --- a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs +++ b/Emby.Server.Implementations/Diagnostics/CommonProcess.cs @@ -105,7 +105,7 @@ namespace Emby.Server.Implementations.Diagnostics { return _process.WaitForExit(timeMs); } - + public Task WaitForExitAsync(int timeMs) { //Note: For this function to work correctly, the option EnableRisingEvents needs to be set to true. diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 1fe8856cc..916d691b8 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -10,19 +10,13 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; -using Microsoft.Extensions.Logging; -using System.IO; using MediaBrowser.Model.Net; -using MediaBrowser.Controller.Library; -using System.Threading.Tasks; namespace MediaBrowser.Controller.MediaEncoding { // For now, a common base class until the API and MediaEncoding classes are unified public class EncodingJobInfo { - protected readonly IMediaSourceManager MediaSourceManager; - public MediaStream VideoStream { get; set; } public VideoType VideoType { get; set; } public Dictionary RemoteHttpHeaders { get; set; } @@ -49,7 +43,6 @@ namespace MediaBrowser.Controller.MediaEncoding public string OutputFilePath { get; set; } public string MimeType { get; set; } - public long? EncodingDurationTicks { get; set; } public string GetMimeType(string outputPath, bool enableStreamDefault = true) { @@ -68,7 +61,12 @@ namespace MediaBrowser.Controller.MediaEncoding { if (_transcodeReasons == null) { - _transcodeReasons = (BaseRequest.TranscodeReasons ?? string.Empty) + if (BaseRequest.TranscodeReasons == null) + { + return Array.Empty(); + } + + _transcodeReasons = BaseRequest.TranscodeReasons .Split(',') .Where(i => !string.IsNullOrEmpty(i)) .Select(v => (TranscodeReason)Enum.Parse(typeof(TranscodeReason), v, true)) @@ -98,7 +96,8 @@ namespace MediaBrowser.Controller.MediaEncoding get { // For live tv + in progress recordings - if (string.Equals(InputContainer, "mpegts", StringComparison.OrdinalIgnoreCase) || string.Equals(InputContainer, "ts", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(InputContainer, "mpegts", StringComparison.OrdinalIgnoreCase) + || string.Equals(InputContainer, "ts", StringComparison.OrdinalIgnoreCase)) { if (!MediaSource.RunTimeTicks.HasValue) { @@ -155,15 +154,7 @@ namespace MediaBrowser.Controller.MediaEncoding } } - if (forceDeinterlaceIfSourceIsInterlaced) - { - if (isInputInterlaced) - { - return true; - } - } - - return false; + return forceDeinterlaceIfSourceIsInterlaced && isInputInterlaced; } public string[] GetRequestedProfiles(string codec) @@ -211,7 +202,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "maxrefframes"); - if (!string.IsNullOrEmpty(value) && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (!string.IsNullOrEmpty(value) + && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -230,7 +222,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "videobitdepth"); - if (!string.IsNullOrEmpty(value) && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (!string.IsNullOrEmpty(value) + && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -249,7 +242,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "audiobitdepth"); - if (!string.IsNullOrEmpty(value) && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (!string.IsNullOrEmpty(value) + && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -264,6 +258,7 @@ namespace MediaBrowser.Controller.MediaEncoding { return BaseRequest.MaxAudioChannels; } + if (BaseRequest.AudioChannels.HasValue) { return BaseRequest.AudioChannels; @@ -272,7 +267,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "audiochannels"); - if (!string.IsNullOrEmpty(value) && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (!string.IsNullOrEmpty(value) + && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -294,7 +290,8 @@ namespace MediaBrowser.Controller.MediaEncoding SupportedSubtitleCodecs = Array.Empty(); } - public bool IsSegmentedLiveStream => TranscodingType != TranscodingJobType.Progressive && !RunTimeTicks.HasValue; + public bool IsSegmentedLiveStream + => TranscodingType != TranscodingJobType.Progressive && !RunTimeTicks.HasValue; public bool EnableBreakOnNonKeyFrames(string videoCodec) { @@ -428,11 +425,12 @@ namespace MediaBrowser.Controller.MediaEncoding { if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - return VideoStream == null ? null : VideoStream.Level; + return VideoStream?.Level; } var level = GetRequestedLevel(ActualOutputVideoCodec); - if (!string.IsNullOrEmpty(level) && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (!string.IsNullOrEmpty(level) + && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -450,7 +448,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - return VideoStream == null ? null : VideoStream.BitDepth; + return VideoStream?.BitDepth; } return null; @@ -467,7 +465,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - return VideoStream == null ? null : VideoStream.RefFrames; + return VideoStream?.RefFrames; } return null; @@ -494,13 +492,14 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - var defaultValue = string.Equals(OutputContainer, "m2ts", StringComparison.OrdinalIgnoreCase) ? + if (BaseRequest.Static) + { + return InputTimestamp; + } + + return string.Equals(OutputContainer, "m2ts", StringComparison.OrdinalIgnoreCase) ? TransportStreamTimestamp.Valid : TransportStreamTimestamp.None; - - return !BaseRequest.Static - ? defaultValue - : InputTimestamp; } } @@ -513,7 +512,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - return VideoStream == null ? null : VideoStream.PacketLength; + return VideoStream?.PacketLength; } return null; @@ -529,7 +528,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - return VideoStream == null ? null : VideoStream.Profile; + return VideoStream?.Profile; } var requestedProfile = GetRequestedProfiles(ActualOutputVideoCodec).FirstOrDefault(); @@ -542,42 +541,13 @@ namespace MediaBrowser.Controller.MediaEncoding } } - /// - /// Predicts the audio sample rate that will be in the output stream - /// - public string TargetVideoRange - { - get - { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) - { - return VideoStream == null ? null : VideoStream.VideoRange; - } - - return "SDR"; - } - } - - public string TargetAudioProfile - { - get - { - if (BaseRequest.Static || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) - { - return AudioStream == null ? null : AudioStream.Profile; - } - - return null; - } - } - public string TargetVideoCodecTag { get { if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - return VideoStream == null ? null : VideoStream.CodecTag; + return VideoStream?.CodecTag; } return null; @@ -590,7 +560,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - return VideoStream == null ? null : VideoStream.IsAnamorphic; + return VideoStream?.IsAnamorphic; } return false; @@ -605,14 +575,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) { - var stream = VideoStream; - - if (stream != null) - { - return stream.Codec; - } - - return null; + return VideoStream?.Codec; } return codec; @@ -627,14 +590,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) { - var stream = AudioStream; - - if (stream != null) - { - return stream.Codec; - } - - return null; + return AudioStream?.Codec; } return codec; @@ -647,7 +603,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - return VideoStream == null ? (bool?)null : VideoStream.IsInterlaced; + return VideoStream?.IsInterlaced; } if (DeInterlace(ActualOutputVideoCodec, true)) @@ -655,7 +611,7 @@ namespace MediaBrowser.Controller.MediaEncoding return false; } - return VideoStream == null ? (bool?)null : VideoStream.IsInterlaced; + return VideoStream?.IsInterlaced; } } @@ -665,7 +621,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - return VideoStream == null ? null : VideoStream.IsAVC; + return VideoStream?.IsAVC; } return false; diff --git a/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs index 81f7c16d3..b231938b5 100644 --- a/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs @@ -1,5 +1,5 @@ using System; -using System.Globalization; +using System.Diagnostics; using System.IO; using System.Text; using System.Threading; @@ -31,11 +31,10 @@ namespace MediaBrowser.MediaEncoding.Encoder protected readonly IMediaSourceManager MediaSourceManager; protected IProcessFactory ProcessFactory; - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - protected EncodingHelper EncodingHelper; - protected BaseEncoder(MediaEncoder mediaEncoder, + protected BaseEncoder( + MediaEncoder mediaEncoder, ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, @@ -43,7 +42,8 @@ namespace MediaBrowser.MediaEncoding.Encoder ILibraryManager libraryManager, ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder, - IMediaSourceManager mediaSourceManager, IProcessFactory processFactory) + IMediaSourceManager mediaSourceManager, + IProcessFactory processFactory) { MediaEncoder = mediaEncoder; Logger = logger; @@ -59,11 +59,12 @@ namespace MediaBrowser.MediaEncoding.Encoder EncodingHelper = new EncodingHelper(MediaEncoder, FileSystem, SubtitleEncoder); } - public async Task Start(EncodingJobOptions options, + public async Task Start( + EncodingJobOptions options, IProgress progress, CancellationToken cancellationToken) { - var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager, ConfigurationManager, MediaEncoder) + var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager, MediaEncoder) .CreateJob(options, EncodingHelper, IsVideoEncoder, progress, cancellationToken).ConfigureAwait(false); encodingJob.OutputFilePath = GetOutputFilePath(encodingJob); @@ -75,8 +76,9 @@ namespace MediaBrowser.MediaEncoding.Encoder var commandLineArgs = GetCommandLineArguments(encodingJob); - var process = ProcessFactory.Create(new ProcessOptions + Process process = Process.Start(new ProcessStartInfo { + WindowStyle = ProcessWindowStyle.Hidden, CreateNoWindow = true, UseShellExecute = false, @@ -88,11 +90,11 @@ namespace MediaBrowser.MediaEncoding.Encoder FileName = MediaEncoder.EncoderPath, Arguments = commandLineArgs, - IsHidden = true, - ErrorDialog = false, - EnableRaisingEvents = true + ErrorDialog = false }); + process.EnableRaisingEvents = true; + var workingDirectory = GetWorkingDirectory(options); if (!string.IsNullOrWhiteSpace(workingDirectory)) { @@ -128,29 +130,60 @@ namespace MediaBrowser.MediaEncoding.Encoder throw; } - cancellationToken.Register(() => Cancel(process, encodingJob)); + cancellationToken.Register(async () => await Cancel(process, encodingJob)); - // MUST read both stdout and stderr asynchronously or a deadlock may occurr + // MUST read both stdout and stderr asynchronously or a deadlock may occur //process.BeginOutputReadLine(); // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback new JobLogger(Logger).StartStreamingLog(encodingJob, process.StandardError.BaseStream, encodingJob.LogFileStream); - // Wait for the file to exist before proceeeding - while (!File.Exists(encodingJob.OutputFilePath) && !encodingJob.HasExited) - { - await Task.Delay(100, cancellationToken).ConfigureAwait(false); - } + // Wait for the file to or for the process to stop + Task file = WaitForFileAsync(encodingJob.OutputFilePath); + await Task.WhenAny(encodingJob.TaskCompletionSource.Task, file).ConfigureAwait(false); return encodingJob; } - private void Cancel(IProcess process, EncodingJob job) + public static Task WaitForFileAsync(string path) + { + if (File.Exists(path)) + { + return Task.CompletedTask; + } + + var tcs = new TaskCompletionSource(); + FileSystemWatcher watcher = new FileSystemWatcher(Path.GetDirectoryName(path)); + + watcher.Created += (s, e) => + { + if (e.Name == Path.GetFileName(path)) + { + watcher.Dispose(); + tcs.TrySetResult(true); + } + }; + + watcher.Renamed += (s, e) => + { + if (e.Name == Path.GetFileName(path)) + { + watcher.Dispose(); + tcs.TrySetResult(true); + } + }; + + watcher.EnableRaisingEvents = true; + + return tcs.Task; + } + + private async Task Cancel(Process process, EncodingJob job) { Logger.LogInformation("Killing ffmpeg process for {0}", job.OutputFilePath); //process.Kill(); - process.StandardInput.WriteLine("q"); + await process.StandardInput.WriteLineAsync("q"); job.IsCancelled = true; } @@ -160,28 +193,28 @@ namespace MediaBrowser.MediaEncoding.Encoder /// /// The process. /// The job. - private void OnFfMpegProcessExited(IProcess process, EncodingJob job) + private void OnFfMpegProcessExited(Process process, EncodingJob job) { job.HasExited = true; Logger.LogDebug("Disposing stream resources"); job.Dispose(); - var isSuccesful = false; + var isSuccessful = false; try { var exitCode = process.ExitCode; Logger.LogInformation("FFMpeg exited with code {0}", exitCode); - isSuccesful = exitCode == 0; + isSuccessful = exitCode == 0; } catch (Exception ex) { Logger.LogError(ex, "FFMpeg exited with an error."); } - if (isSuccesful && !job.IsCancelled) + if (isSuccessful && !job.IsCancelled) { job.TaskCompletionSource.TrySetResult(true); } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs index d4040cd31..cd7de94ce 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Encoder @@ -40,7 +39,6 @@ namespace MediaBrowser.MediaEncoding.Encoder _mediaSourceManager = mediaSourceManager; Id = Guid.NewGuid(); - _logger = logger; TaskCompletionSource = new TaskCompletionSource(); } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs index 95454c447..5f84a0322 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs @@ -1,13 +1,10 @@ using System; -using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using Microsoft.Extensions.Logging; @@ -19,17 +16,13 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IMediaSourceManager _mediaSourceManager; - private readonly IConfigurationManager _config; private readonly IMediaEncoder _mediaEncoder; - protected static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - public EncodingJobFactory(ILogger logger, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, IConfigurationManager config, IMediaEncoder mediaEncoder) + public EncodingJobFactory(ILogger logger, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder) { _logger = logger; _libraryManager = libraryManager; _mediaSourceManager = mediaSourceManager; - _config = config; _mediaEncoder = mediaEncoder; } @@ -118,11 +111,11 @@ namespace MediaBrowser.MediaEncoding.Encoder if (state.OutputVideoBitrate.HasValue) { var resolution = ResolutionNormalizer.Normalize( - state.VideoStream == null ? (int?)null : state.VideoStream.BitRate, - state.VideoStream == null ? (int?)null : state.VideoStream.Width, - state.VideoStream == null ? (int?)null : state.VideoStream.Height, + state.VideoStream?.BitRate, + state.VideoStream?.Width, + state.VideoStream?.Height, state.OutputVideoBitrate.Value, - state.VideoStream == null ? null : state.VideoStream.Codec, + state.VideoStream?.Codec, state.OutputVideoCodec, videoRequest.MaxWidth, videoRequest.MaxHeight); @@ -144,40 +137,6 @@ namespace MediaBrowser.MediaEncoding.Encoder return state; } - protected EncodingOptions GetEncodingOptions() - { - return _config.GetConfiguration("encoding"); - } - - /// - /// Infers the video codec. - /// - /// The container. - /// System.Nullable{VideoCodecs}. - private static string InferVideoCodec(string container) - { - var ext = "." + (container ?? string.Empty); - - if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase)) - { - return "wmv"; - } - if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase)) - { - return "vpx"; - } - if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase)) - { - return "theora"; - } - if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase)) - { - return "h264"; - } - - return "copy"; - } - private string InferAudioCodec(string container) { var ext = "." + (container ?? string.Empty); @@ -186,31 +145,19 @@ namespace MediaBrowser.MediaEncoding.Encoder { return "mp3"; } - if (string.Equals(ext, ".aac", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(ext, ".aac", StringComparison.OrdinalIgnoreCase)) { return "aac"; } - if (string.Equals(ext, ".wma", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(ext, ".wma", StringComparison.OrdinalIgnoreCase)) { return "wma"; } - if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase)) - { - return "vorbis"; - } - if (string.Equals(ext, ".oga", StringComparison.OrdinalIgnoreCase)) - { - return "vorbis"; - } - if (string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase)) - { - return "vorbis"; - } - if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase)) - { - return "vorbis"; - } - if (string.Equals(ext, ".webma", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) + || string.Equals(ext, ".oga", StringComparison.OrdinalIgnoreCase) + || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase) + || string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase) + || string.Equals(ext, ".webma", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; } @@ -218,35 +165,6 @@ namespace MediaBrowser.MediaEncoding.Encoder return "copy"; } - /// - /// Determines whether the specified stream is H264. - /// - /// The stream. - /// true if the specified stream is H264; otherwise, false. - protected bool IsH264(MediaStream stream) - { - var codec = stream.Codec ?? string.Empty; - - return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 || - codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1; - } - - private static int GetVideoProfileScore(string profile) - { - var list = new List - { - "Constrained Baseline", - "Baseline", - "Extended", - "Main", - "High", - "Progressive High", - "Constrained High" - }; - - return Array.FindIndex(list.ToArray(), t => string.Equals(t, profile, StringComparison.OrdinalIgnoreCase)); - } - private void ApplyDeviceProfileSettings(EncodingJob state) { var profile = state.Options.DeviceProfile;