diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
new file mode 100644
index 0000000000..a988c2f974
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
@@ -0,0 +1,91 @@
+using MediaBrowser.Model.Dlna;
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ public class EncodingJobOptions
+ {
+ public string OutputContainer { get; set; }
+
+ public long? StartTimeTicks { get; set; }
+ public int? Width { get; set; }
+ public int? Height { get; set; }
+ public int? MaxWidth { get; set; }
+ public int? MaxHeight { get; set; }
+ public bool Static = false;
+ public float? Framerate { get; set; }
+ public float? MaxFramerate { get; set; }
+ public string Profile { get; set; }
+ public int? Level { get; set; }
+
+ public string DeviceId { get; set; }
+ public string ItemId { get; set; }
+ public string MediaSourceId { get; set; }
+ public string AudioCodec { get; set; }
+
+ public bool EnableAutoStreamCopy { get; set; }
+
+ public int? MaxAudioChannels { get; set; }
+ public int? AudioChannels { get; set; }
+ public int? AudioBitRate { get; set; }
+ public int? AudioSampleRate { get; set; }
+
+ public DeviceProfile DeviceProfile { get; set; }
+ public EncodingContext Context { get; set; }
+
+ public string VideoCodec { get; set; }
+
+ public int? VideoBitRate { get; set; }
+ public int? AudioStreamIndex { get; set; }
+ public int? VideoStreamIndex { get; set; }
+ public int? SubtitleStreamIndex { get; set; }
+ public int? MaxRefFrames { get; set; }
+ public int? MaxVideoBitDepth { get; set; }
+ public SubtitleDeliveryMethod SubtitleMethod { get; set; }
+
+ ///
+ /// Gets a value indicating whether this instance has fixed resolution.
+ ///
+ /// true if this instance has fixed resolution; otherwise, false.
+ public bool HasFixedResolution
+ {
+ get
+ {
+ return Width.HasValue || Height.HasValue;
+ }
+ }
+
+ public bool? Cabac { get; set; }
+
+ public EncodingJobOptions()
+ {
+
+ }
+
+ public EncodingJobOptions(StreamInfo info, DeviceProfile deviceProfile)
+ {
+ OutputContainer = info.Container;
+ StartTimeTicks = info.StartPositionTicks;
+ MaxWidth = info.MaxWidth;
+ MaxHeight = info.MaxHeight;
+ MaxFramerate = info.MaxFramerate;
+ Profile = info.VideoProfile;
+ Level = info.VideoLevel;
+ ItemId = info.ItemId;
+ MediaSourceId = info.MediaSourceId;
+ AudioCodec = info.AudioCodec;
+ MaxAudioChannels = info.MaxAudioChannels;
+ AudioBitRate = info.AudioBitrate;
+ AudioSampleRate = info.TargetAudioSampleRate;
+ DeviceProfile = deviceProfile;
+ VideoCodec = info.VideoCodec;
+ VideoBitRate = info.VideoBitrate;
+ AudioStreamIndex = info.AudioStreamIndex;
+ SubtitleStreamIndex = info.SubtitleStreamIndex;
+ MaxRefFrames = info.MaxRefFrames;
+ MaxVideoBitDepth = info.MaxVideoBitDepth;
+ SubtitleMethod = info.SubtitleDeliveryMethod;
+ Cabac = info.Cabac;
+ Context = info.Context;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index 8f56bfda58..47544f972b 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -107,5 +107,16 @@ namespace MediaBrowser.Controller.MediaEncoding
Task EncodeAudio(EncodingJobOptions options,
IProgress progress,
CancellationToken cancellationToken);
+
+ ///
+ /// Encodes the video.
+ ///
+ /// The options.
+ /// The progress.
+ /// The cancellation token.
+ /// Task<System.String>.
+ Task EncodeVideo(EncodingJobOptions options,
+ IProgress progress,
+ CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs
new file mode 100644
index 0000000000..7054accfad
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs
@@ -0,0 +1,86 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.MediaEncoding.Encoder
+{
+ public class AudioEncoder : BaseEncoder
+ {
+ public AudioEncoder(MediaEncoder mediaEncoder, ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder) : base(mediaEncoder, logger, configurationManager, fileSystem, liveTvManager, isoManager, libraryManager, channelManager, sessionManager, subtitleEncoder)
+ {
+ }
+
+ protected override string GetCommandLineArguments(EncodingJob job)
+ {
+ var audioTranscodeParams = new List();
+
+ var bitrate = job.OutputAudioBitrate;
+
+ if (bitrate.HasValue)
+ {
+ audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(UsCulture));
+ }
+
+ if (job.OutputAudioChannels.HasValue)
+ {
+ audioTranscodeParams.Add("-ac " + job.OutputAudioChannels.Value.ToString(UsCulture));
+ }
+
+ if (job.OutputAudioSampleRate.HasValue)
+ {
+ audioTranscodeParams.Add("-ar " + job.OutputAudioSampleRate.Value.ToString(UsCulture));
+ }
+
+ var threads = GetNumberOfThreads(job, false);
+
+ var inputModifier = GetInputModifier(job);
+
+ return string.Format("{0} {1} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1 -y \"{5}\"",
+ inputModifier,
+ GetInputArgument(job),
+ threads,
+ " -vn",
+ string.Join(" ", audioTranscodeParams.ToArray()),
+ job.OutputFilePath).Trim();
+ }
+
+ protected override string GetOutputFileExtension(EncodingJob state)
+ {
+ var ext = base.GetOutputFileExtension(state);
+
+ if (!string.IsNullOrEmpty(ext))
+ {
+ return ext;
+ }
+
+ var audioCodec = state.Options.AudioCodec;
+
+ if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return ".aac";
+ }
+ if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return ".mp3";
+ }
+ if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return ".ogg";
+ }
+ if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return ".wma";
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs
new file mode 100644
index 0000000000..a350d05774
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs
@@ -0,0 +1,1049 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.MediaEncoding.Subtitles;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.MediaEncoding.Encoder
+{
+ public abstract class BaseEncoder
+ {
+ protected readonly MediaEncoder MediaEncoder;
+ protected readonly ILogger Logger;
+ protected readonly IServerConfigurationManager ConfigurationManager;
+ protected readonly IFileSystem FileSystem;
+ protected readonly ILiveTvManager LiveTvManager;
+ protected readonly IIsoManager IsoManager;
+ protected readonly ILibraryManager LibraryManager;
+ protected readonly IChannelManager ChannelManager;
+ protected readonly ISessionManager SessionManager;
+ protected readonly ISubtitleEncoder SubtitleEncoder;
+
+ protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ public BaseEncoder(MediaEncoder mediaEncoder,
+ ILogger logger,
+ IServerConfigurationManager configurationManager,
+ IFileSystem fileSystem,
+ ILiveTvManager liveTvManager,
+ IIsoManager isoManager,
+ ILibraryManager libraryManager,
+ IChannelManager channelManager,
+ ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder)
+ {
+ MediaEncoder = mediaEncoder;
+ Logger = logger;
+ ConfigurationManager = configurationManager;
+ FileSystem = fileSystem;
+ LiveTvManager = liveTvManager;
+ IsoManager = isoManager;
+ LibraryManager = libraryManager;
+ ChannelManager = channelManager;
+ SessionManager = sessionManager;
+ SubtitleEncoder = subtitleEncoder;
+ }
+
+ public async Task Start(EncodingJobOptions options,
+ IProgress progress,
+ CancellationToken cancellationToken)
+ {
+ var encodingJob = await new EncodingJobFactory(Logger, LiveTvManager, LibraryManager, ChannelManager)
+ .CreateJob(options, IsVideoEncoder, progress, cancellationToken).ConfigureAwait(false);
+
+ encodingJob.OutputFilePath = GetOutputFilePath(encodingJob);
+ Directory.CreateDirectory(Path.GetDirectoryName(encodingJob.OutputFilePath));
+
+ if (options.Context == EncodingContext.Static && encodingJob.IsInputVideo)
+ {
+ encodingJob.ReadInputAtNativeFramerate = true;
+ }
+
+ await AcquireResources(encodingJob, cancellationToken).ConfigureAwait(false);
+
+ var commandLineArgs = GetCommandLineArguments(encodingJob);
+
+ if (GetEncodingOptions().EnableDebugLogging)
+ {
+ commandLineArgs = "-loglevel debug " + commandLineArgs;
+ }
+
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+
+ // Must consume both stdout and stderr or deadlocks may occur
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ RedirectStandardInput = true,
+
+ FileName = MediaEncoder.EncoderPath,
+ Arguments = commandLineArgs,
+
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ },
+
+ EnableRaisingEvents = true
+ };
+
+ var workingDirectory = GetWorkingDirectory(options);
+ if (!string.IsNullOrWhiteSpace(workingDirectory))
+ {
+ process.StartInfo.WorkingDirectory = workingDirectory;
+ }
+
+ OnTranscodeBeginning(encodingJob);
+
+ var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
+ Logger.Info(commandLineLogMessage);
+
+ var logFilePath = Path.Combine(ConfigurationManager.CommonApplicationPaths.LogDirectoryPath, "transcode-" + Guid.NewGuid() + ".txt");
+ Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
+
+ // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
+ encodingJob.LogFileStream = FileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true);
+
+ var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(commandLineLogMessage + Environment.NewLine + Environment.NewLine);
+ await encodingJob.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationToken).ConfigureAwait(false);
+
+ process.Exited += (sender, args) => OnFfMpegProcessExited(process, encodingJob);
+
+ try
+ {
+ process.Start();
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error starting ffmpeg", ex);
+
+ OnTranscodeFailedToStart(encodingJob.OutputFilePath, encodingJob);
+
+ throw;
+ }
+
+ // MUST read both stdout and stderr asynchronously or a deadlock may occurr
+ 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);
+ }
+
+ return encodingJob;
+ }
+
+ ///
+ /// Processes the exited.
+ ///
+ /// The process.
+ /// The job.
+ private void OnFfMpegProcessExited(Process process, EncodingJob job)
+ {
+ job.HasExited = true;
+
+ Logger.Debug("Disposing stream resources");
+ job.Dispose();
+
+ try
+ {
+ Logger.Info("FFMpeg exited with code {0}", process.ExitCode);
+
+ try
+ {
+ job.TaskCompletionSource.TrySetResult(true);
+ }
+ catch
+ {
+ }
+ }
+ catch
+ {
+ Logger.Error("FFMpeg exited with an error.");
+
+ try
+ {
+ job.TaskCompletionSource.TrySetException(new ApplicationException());
+ }
+ catch
+ {
+ }
+ }
+
+ // This causes on exited to be called twice:
+ //try
+ //{
+ // // Dispose the process
+ // process.Dispose();
+ //}
+ //catch (Exception ex)
+ //{
+ // Logger.ErrorException("Error disposing ffmpeg.", ex);
+ //}
+ }
+
+ private void OnTranscodeBeginning(EncodingJob job)
+ {
+ job.ReportTranscodingProgress(null, null, null, null);
+ }
+
+ private void OnTranscodeFailedToStart(string path, EncodingJob job)
+ {
+ if (!string.IsNullOrWhiteSpace(job.Options.DeviceId))
+ {
+ SessionManager.ClearTranscodingInfo(job.Options.DeviceId);
+ }
+ }
+
+ protected virtual bool IsVideoEncoder
+ {
+ get { return false; }
+ }
+
+ protected virtual string GetWorkingDirectory(EncodingJobOptions options)
+ {
+ return null;
+ }
+
+ protected EncodingOptions GetEncodingOptions()
+ {
+ return ConfigurationManager.GetConfiguration("encoding");
+ }
+
+ protected abstract string GetCommandLineArguments(EncodingJob job);
+
+ private string GetOutputFilePath(EncodingJob state)
+ {
+ var folder = ConfigurationManager.ApplicationPaths.TranscodingTempPath;
+
+ var outputFileExtension = GetOutputFileExtension(state);
+ var context = state.Options.Context;
+
+ var filename = state.Id + (outputFileExtension ?? string.Empty).ToLower();
+ return Path.Combine(folder, context.ToString().ToLower(), filename);
+ }
+
+ protected virtual string GetOutputFileExtension(EncodingJob state)
+ {
+ if (!string.IsNullOrWhiteSpace(state.Options.OutputContainer))
+ {
+ return "." + state.Options.OutputContainer;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets the number of threads.
+ ///
+ /// System.Int32.
+ protected int GetNumberOfThreads(EncodingJob job, bool isWebm)
+ {
+ if (isWebm)
+ {
+ // Recommended per docs
+ return Math.Max(Environment.ProcessorCount - 1, 2);
+ }
+
+ // Use more when this is true. -re will keep cpu usage under control
+ if (job.ReadInputAtNativeFramerate)
+ {
+ if (isWebm)
+ {
+ return Math.Max(Environment.ProcessorCount - 1, 2);
+ }
+
+ return 0;
+ }
+
+ // Webm: http://www.webmproject.org/docs/encoder-parameters/
+ // The decoder will usually automatically use an appropriate number of threads according to how many cores are available but it can only use multiple threads
+ // for the coefficient data if the encoder selected --token-parts > 0 at encode time.
+
+ switch (GetQualitySetting())
+ {
+ case EncodingQuality.HighSpeed:
+ return 2;
+ case EncodingQuality.HighQuality:
+ return 2;
+ case EncodingQuality.MaxQuality:
+ return isWebm ? Math.Max(Environment.ProcessorCount - 1, 2) : 0;
+ default:
+ throw new Exception("Unrecognized MediaEncodingQuality value.");
+ }
+ }
+
+ protected EncodingQuality GetQualitySetting()
+ {
+ var quality = GetEncodingOptions().EncodingQuality;
+
+ if (quality == EncodingQuality.Auto)
+ {
+ var cpuCount = Environment.ProcessorCount;
+
+ if (cpuCount >= 4)
+ {
+ //return EncodingQuality.HighQuality;
+ }
+
+ return EncodingQuality.HighSpeed;
+ }
+
+ return quality;
+ }
+
+ protected string GetInputModifier(EncodingJob job, bool genPts = true)
+ {
+ var inputModifier = string.Empty;
+
+ var probeSize = GetProbeSizeArgument(job);
+ inputModifier += " " + probeSize;
+ inputModifier = inputModifier.Trim();
+
+ var userAgentParam = GetUserAgentParam(job);
+
+ if (!string.IsNullOrWhiteSpace(userAgentParam))
+ {
+ inputModifier += " " + userAgentParam;
+ }
+
+ inputModifier = inputModifier.Trim();
+
+ inputModifier += " " + GetFastSeekCommandLineParameter(job.Options);
+ inputModifier = inputModifier.Trim();
+
+ if (job.IsVideoRequest && genPts)
+ {
+ inputModifier += " -fflags +genpts";
+ }
+
+ if (!string.IsNullOrEmpty(job.InputAudioSync))
+ {
+ inputModifier += " -async " + job.InputAudioSync;
+ }
+
+ if (!string.IsNullOrEmpty(job.InputVideoSync))
+ {
+ inputModifier += " -vsync " + job.InputVideoSync;
+ }
+
+ if (job.ReadInputAtNativeFramerate)
+ {
+ inputModifier += " -re";
+ }
+
+ return inputModifier;
+ }
+
+ private string GetUserAgentParam(EncodingJob job)
+ {
+ string useragent = null;
+
+ job.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
+
+ if (!string.IsNullOrWhiteSpace(useragent))
+ {
+ return "-user-agent \"" + useragent + "\"";
+ }
+
+ return string.Empty;
+ }
+
+ ///
+ /// Gets the probe size argument.
+ ///
+ /// The job.
+ /// System.String.
+ private string GetProbeSizeArgument(EncodingJob job)
+ {
+ if (job.PlayableStreamFileNames.Count > 0)
+ {
+ return MediaEncoder.GetProbeSizeArgument(job.PlayableStreamFileNames.ToArray(), job.InputProtocol);
+ }
+
+ return MediaEncoder.GetProbeSizeArgument(new[] { job.MediaPath }, job.InputProtocol);
+ }
+
+ ///
+ /// Gets the fast seek command line parameter.
+ ///
+ /// The options.
+ /// System.String.
+ /// The fast seek command line parameter.
+ protected string GetFastSeekCommandLineParameter(EncodingJobOptions options)
+ {
+ var time = options.StartTimeTicks;
+
+ if (time.HasValue && time.Value > 0)
+ {
+ return string.Format("-ss {0}", MediaEncoder.GetTimeParameter(time.Value));
+ }
+
+ return string.Empty;
+ }
+
+ ///
+ /// Gets the input argument.
+ ///
+ /// The job.
+ /// System.String.
+ protected string GetInputArgument(EncodingJob job)
+ {
+ var arg = "-i " + GetInputPathArgument(job);
+
+ if (job.SubtitleStream != null)
+ {
+ if (job.SubtitleStream.IsExternal && !job.SubtitleStream.IsTextSubtitleStream)
+ {
+ arg += " -i " + job.SubtitleStream.Path;
+ }
+ }
+
+ return arg;
+ }
+
+ private string GetInputPathArgument(EncodingJob job)
+ {
+ //if (job.InputProtocol == MediaProtocol.File &&
+ // job.RunTimeTicks.HasValue &&
+ // job.VideoType == VideoType.VideoFile &&
+ // !string.Equals(job.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ //{
+ // if (job.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && job.IsInputVideo)
+ // {
+ // if (SupportsThrottleWithStream)
+ // {
+ // var url = "http://localhost:" + ServerConfigurationManager.Configuration.HttpServerPortNumber.ToString(UsCulture) + "/mediabrowser/videos/" + job.Request.Id + "/stream?static=true&Throttle=true&mediaSourceId=" + job.Request.MediaSourceId;
+
+ // url += "&transcodingJobId=" + transcodingJobId;
+
+ // return string.Format("\"{0}\"", url);
+ // }
+ // }
+ //}
+
+ var protocol = job.InputProtocol;
+
+ var inputPath = new[] { job.MediaPath };
+
+ if (job.IsInputVideo)
+ {
+ if (!(job.VideoType == VideoType.Iso && job.IsoMount == null))
+ {
+ inputPath = MediaEncoderHelpers.GetInputArgument(job.MediaPath, job.InputProtocol, job.IsoMount, job.PlayableStreamFileNames);
+ }
+ }
+
+ return MediaEncoder.GetInputArgument(inputPath, protocol);
+ }
+
+ private async Task AcquireResources(EncodingJob state, CancellationToken cancellationToken)
+ {
+ if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
+ {
+ state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationToken).ConfigureAwait(false);
+ }
+
+ if (string.IsNullOrEmpty(state.MediaPath))
+ {
+ var checkCodecs = false;
+
+ if (string.Equals(state.ItemType, typeof(LiveTvChannel).Name))
+ {
+ var streamInfo = await LiveTvManager.GetChannelStream(state.Options.ItemId, cancellationToken).ConfigureAwait(false);
+
+ state.LiveTvStreamId = streamInfo.Id;
+
+ state.MediaPath = streamInfo.Path;
+ state.InputProtocol = streamInfo.Protocol;
+
+ await Task.Delay(1500, cancellationToken).ConfigureAwait(false);
+
+ AttachMediaStreamInfo(state, streamInfo, state.Options);
+ checkCodecs = true;
+ }
+
+ else if (string.Equals(state.ItemType, typeof(LiveTvVideoRecording).Name) ||
+ string.Equals(state.ItemType, typeof(LiveTvAudioRecording).Name))
+ {
+ var streamInfo = await LiveTvManager.GetRecordingStream(state.Options.ItemId, cancellationToken).ConfigureAwait(false);
+
+ state.LiveTvStreamId = streamInfo.Id;
+
+ state.MediaPath = streamInfo.Path;
+ state.InputProtocol = streamInfo.Protocol;
+
+ await Task.Delay(1500, cancellationToken).ConfigureAwait(false);
+
+ AttachMediaStreamInfo(state, streamInfo, state.Options);
+ checkCodecs = true;
+ }
+
+ if (state.IsVideoRequest && checkCodecs)
+ {
+ if (state.VideoStream != null && EncodingJobFactory.CanStreamCopyVideo(state.Options, state.VideoStream))
+ {
+ state.OutputVideoCodec = "copy";
+ }
+
+ if (state.AudioStream != null && EncodingJobFactory.CanStreamCopyAudio(state.Options, state.AudioStream, state.SupportedAudioCodecs))
+ {
+ state.OutputAudioCodec = "copy";
+ }
+ }
+ }
+ }
+
+ private void AttachMediaStreamInfo(EncodingJob state,
+ ChannelMediaInfo mediaInfo,
+ EncodingJobOptions videoRequest)
+ {
+ var mediaSource = mediaInfo.ToMediaSource();
+
+ state.InputProtocol = mediaSource.Protocol;
+ state.MediaPath = mediaSource.Path;
+ state.RunTimeTicks = mediaSource.RunTimeTicks;
+ state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
+ state.InputBitrate = mediaSource.Bitrate;
+ state.InputFileSize = mediaSource.Size;
+ state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
+
+ if (state.ReadInputAtNativeFramerate)
+ {
+ state.OutputAudioSync = "1000";
+ state.InputVideoSync = "-1";
+ state.InputAudioSync = "1";
+ }
+
+ EncodingJobFactory.AttachMediaStreamInfo(state, mediaSource.MediaStreams, videoRequest);
+ }
+
+ ///
+ /// Gets the internal graphical subtitle param.
+ ///
+ /// The state.
+ /// The output video codec.
+ /// System.String.
+ protected string GetGraphicalSubtitleParam(EncodingJob state, string outputVideoCodec)
+ {
+ var outputSizeParam = string.Empty;
+
+ var request = state.Options;
+
+ // Add resolution params, if specified
+ if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue)
+ {
+ outputSizeParam = GetOutputSizeParam(state, outputVideoCodec).TrimEnd('"');
+ outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase));
+ }
+
+ var videoSizeParam = string.Empty;
+
+ if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue)
+ {
+ videoSizeParam = string.Format(",scale={0}:{1}", state.VideoStream.Width.Value.ToString(UsCulture), state.VideoStream.Height.Value.ToString(UsCulture));
+ }
+
+ var mapPrefix = state.SubtitleStream.IsExternal ?
+ 1 :
+ 0;
+
+ var subtitleStreamIndex = state.SubtitleStream.IsExternal
+ ? 0
+ : state.SubtitleStream.Index;
+
+ return string.Format(" -filter_complex \"[{0}:{1}]format=yuva444p{4},lut=u=128:v=128:y=gammaval(.3)[sub] ; [0:{2}] [sub] overlay{3}\"",
+ mapPrefix.ToString(UsCulture),
+ subtitleStreamIndex.ToString(UsCulture),
+ state.VideoStream.Index.ToString(UsCulture),
+ outputSizeParam,
+ videoSizeParam);
+ }
+
+ ///
+ /// Gets the video bitrate to specify on the command line
+ ///
+ /// The state.
+ /// The video codec.
+ /// if set to true [is HLS].
+ /// System.String.
+ protected string GetVideoQualityParam(EncodingJob state, string videoCodec, bool isHls)
+ {
+ var param = string.Empty;
+
+ var isVc1 = state.VideoStream != null &&
+ string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
+
+ var qualitySetting = GetQualitySetting();
+
+ if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
+ {
+ switch (qualitySetting)
+ {
+ case EncodingQuality.HighSpeed:
+ param = "-preset superfast";
+ break;
+ case EncodingQuality.HighQuality:
+ param = "-preset superfast";
+ break;
+ case EncodingQuality.MaxQuality:
+ param = "-preset superfast";
+ break;
+ }
+
+ switch (qualitySetting)
+ {
+ case EncodingQuality.HighSpeed:
+ param += " -crf 23";
+ break;
+ case EncodingQuality.HighQuality:
+ param += " -crf 20";
+ break;
+ case EncodingQuality.MaxQuality:
+ param += " -crf 18";
+ break;
+ }
+ }
+
+ // webm
+ else if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
+ {
+ // Values 0-3, 0 being highest quality but slower
+ var profileScore = 0;
+
+ string crf;
+ var qmin = "0";
+ var qmax = "50";
+
+ switch (qualitySetting)
+ {
+ case EncodingQuality.HighSpeed:
+ crf = "10";
+ break;
+ case EncodingQuality.HighQuality:
+ crf = "6";
+ break;
+ case EncodingQuality.MaxQuality:
+ crf = "4";
+ break;
+ default:
+ throw new ArgumentException("Unrecognized quality setting");
+ }
+
+ if (isVc1)
+ {
+ profileScore++;
+ }
+
+ // Max of 2
+ profileScore = Math.Min(profileScore, 2);
+
+ // http://www.webmproject.org/docs/encoder-parameters/
+ param = string.Format("-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
+ profileScore.ToString(UsCulture),
+ crf,
+ qmin,
+ qmax);
+ }
+
+ else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase))
+ {
+ param = "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
+ }
+
+ // asf/wmv
+ else if (string.Equals(videoCodec, "wmv2", StringComparison.OrdinalIgnoreCase))
+ {
+ param = "-qmin 2";
+ }
+
+ else if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
+ {
+ param = "-mbd 2";
+ }
+
+ param += GetVideoBitrateParam(state, videoCodec, isHls);
+
+ var framerate = GetFramerateParam(state);
+ if (framerate.HasValue)
+ {
+ param += string.Format(" -r {0}", framerate.Value.ToString(UsCulture));
+ }
+
+ if (!string.IsNullOrEmpty(state.OutputVideoSync))
+ {
+ param += " -vsync " + state.OutputVideoSync;
+ }
+
+ if (!string.IsNullOrEmpty(state.Options.Profile))
+ {
+ param += " -profile:v " + state.Options.Profile;
+ }
+
+ if (state.Options.Level.HasValue)
+ {
+ param += " -level " + state.Options.Level.Value.ToString(UsCulture);
+ }
+
+ return param;
+ }
+
+ protected string GetVideoBitrateParam(EncodingJob state, string videoCodec, bool isHls)
+ {
+ var bitrate = state.OutputVideoBitrate;
+
+ if (bitrate.HasValue)
+ {
+ var hasFixedResolution = state.Options.HasFixedResolution;
+
+ if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
+ {
+ if (hasFixedResolution)
+ {
+ return string.Format(" -minrate:v ({0}*.90) -maxrate:v ({0}*1.10) -bufsize:v {0} -b:v {0}", bitrate.Value.ToString(UsCulture));
+ }
+
+ // With vpx when crf is used, b:v becomes a max rate
+ // https://trac.ffmpeg.org/wiki/vpxEncodingGuide. But higher bitrate source files -b:v causes judder so limite the bitrate but dont allow it to "saturate" the bitrate. So dont contrain it down just up.
+ return string.Format(" -maxrate:v {0} -bufsize:v ({0}*2) -b:v {0}", bitrate.Value.ToString(UsCulture));
+ }
+
+ if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
+ {
+ return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
+ }
+
+ // H264
+ if (hasFixedResolution)
+ {
+ if (isHls)
+ {
+ return string.Format(" -b:v {0} -maxrate ({0}*.80) -bufsize {0}", bitrate.Value.ToString(UsCulture));
+ }
+
+ return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
+ }
+
+ return string.Format(" -maxrate {0} -bufsize {1}",
+ bitrate.Value.ToString(UsCulture),
+ (bitrate.Value * 2).ToString(UsCulture));
+ }
+
+ return string.Empty;
+ }
+
+ protected double? GetFramerateParam(EncodingJob state)
+ {
+ if (state.Options.Framerate.HasValue)
+ {
+ return state.Options.Framerate.Value;
+ }
+
+ var maxrate = state.Options.MaxFramerate;
+
+ if (maxrate.HasValue && state.VideoStream != null)
+ {
+ var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate;
+
+ if (contentRate.HasValue && contentRate.Value > maxrate.Value)
+ {
+ return maxrate;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets the map args.
+ ///
+ /// The state.
+ /// System.String.
+ protected virtual string GetMapArgs(EncodingJob state)
+ {
+ // If we don't have known media info
+ // If input is video, use -sn to drop subtitles
+ // Otherwise just return empty
+ if (state.VideoStream == null && state.AudioStream == null)
+ {
+ return state.IsInputVideo ? "-sn" : string.Empty;
+ }
+
+ // We have media info, but we don't know the stream indexes
+ if (state.VideoStream != null && state.VideoStream.Index == -1)
+ {
+ return "-sn";
+ }
+
+ // We have media info, but we don't know the stream indexes
+ if (state.AudioStream != null && state.AudioStream.Index == -1)
+ {
+ return state.IsInputVideo ? "-sn" : string.Empty;
+ }
+
+ var args = string.Empty;
+
+ if (state.VideoStream != null)
+ {
+ args += string.Format("-map 0:{0}", state.VideoStream.Index);
+ }
+ else
+ {
+ args += "-map -0:v";
+ }
+
+ if (state.AudioStream != null)
+ {
+ args += string.Format(" -map 0:{0}", state.AudioStream.Index);
+ }
+
+ else
+ {
+ args += " -map -0:a";
+ }
+
+ if (state.SubtitleStream == null)
+ {
+ args += " -map -0:s";
+ }
+ else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
+ {
+ args += " -map 1:0 -sn";
+ }
+
+ return args;
+ }
+
+ ///
+ /// 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;
+ }
+
+ ///
+ /// If we're going to put a fixed size on the command line, this will calculate it
+ ///
+ /// The state.
+ /// The output video codec.
+ /// if set to true [allow time stamp copy].
+ /// System.String.
+ protected string GetOutputSizeParam(EncodingJob state,
+ string outputVideoCodec,
+ bool allowTimeStampCopy = true)
+ {
+ // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
+
+ var request = state.Options;
+
+ var filters = new List();
+
+ if (state.DeInterlace)
+ {
+ filters.Add("yadif=0:-1:0");
+ }
+
+ // If fixed dimensions were supplied
+ if (request.Width.HasValue && request.Height.HasValue)
+ {
+ var widthParam = request.Width.Value.ToString(UsCulture);
+ var heightParam = request.Height.Value.ToString(UsCulture);
+
+ filters.Add(string.Format("scale=trunc({0}/2)*2:trunc({1}/2)*2", widthParam, heightParam));
+ }
+
+ // If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size
+ else if (request.MaxWidth.HasValue && request.MaxHeight.HasValue)
+ {
+ var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture);
+ var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture);
+
+ filters.Add(string.Format("scale=trunc(min(iw\\,{0})/2)*2:trunc(min((iw/dar)\\,{1})/2)*2", maxWidthParam, maxHeightParam));
+ }
+
+ // If a fixed width was requested
+ else if (request.Width.HasValue)
+ {
+ var widthParam = request.Width.Value.ToString(UsCulture);
+
+ filters.Add(string.Format("scale={0}:trunc(ow/a/2)*2", widthParam));
+ }
+
+ // If a fixed height was requested
+ else if (request.Height.HasValue)
+ {
+ var heightParam = request.Height.Value.ToString(UsCulture);
+
+ filters.Add(string.Format("scale=trunc(oh*a*2)/2:{0}", heightParam));
+ }
+
+ // If a max width was requested
+ else if (request.MaxWidth.HasValue && (!request.MaxHeight.HasValue || state.VideoStream == null))
+ {
+ var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture);
+
+ filters.Add(string.Format("scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam));
+ }
+
+ // If a max height was requested
+ else if (request.MaxHeight.HasValue && (!request.MaxWidth.HasValue || state.VideoStream == null))
+ {
+ var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture);
+
+ filters.Add(string.Format("scale=trunc(oh*a*2)/2:min(ih\\,{0})", maxHeightParam));
+ }
+
+ else if (request.MaxWidth.HasValue ||
+ request.MaxHeight.HasValue ||
+ request.Width.HasValue ||
+ request.Height.HasValue)
+ {
+ if (state.VideoStream != null)
+ {
+ // Need to perform calculations manually
+
+ // Try to account for bad media info
+ var currentHeight = state.VideoStream.Height ?? request.MaxHeight ?? request.Height ?? 0;
+ var currentWidth = state.VideoStream.Width ?? request.MaxWidth ?? request.Width ?? 0;
+
+ var outputSize = DrawingUtils.Resize(currentWidth, currentHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
+
+ var manualWidthParam = outputSize.Width.ToString(UsCulture);
+ var manualHeightParam = outputSize.Height.ToString(UsCulture);
+
+ filters.Add(string.Format("scale=trunc({0}/2)*2:trunc({1}/2)*2", manualWidthParam, manualHeightParam));
+ }
+ }
+
+ var output = string.Empty;
+
+ if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream)
+ {
+ var subParam = GetTextSubtitleParam(state);
+
+ filters.Add(subParam);
+
+ if (allowTimeStampCopy)
+ {
+ output += " -copyts";
+ }
+ }
+
+ if (filters.Count > 0)
+ {
+ output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray()));
+ }
+
+ return output;
+ }
+
+ ///
+ /// Gets the text subtitle param.
+ ///
+ /// The state.
+ /// System.String.
+ protected string GetTextSubtitleParam(EncodingJob state)
+ {
+ var seconds = Math.Round(TimeSpan.FromTicks(state.Options.StartTimeTicks ?? 0).TotalSeconds);
+
+ if (state.SubtitleStream.IsExternal)
+ {
+ var subtitlePath = state.SubtitleStream.Path;
+
+ var charsetParam = string.Empty;
+
+ if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
+ {
+ var charenc = SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.SubtitleStream.Language);
+
+ if (!string.IsNullOrEmpty(charenc))
+ {
+ charsetParam = ":charenc=" + charenc;
+ }
+ }
+
+ // TODO: Perhaps also use original_size=1920x800 ??
+ return string.Format("subtitles=filename='{0}'{1},setpts=PTS -{2}/TB",
+ subtitlePath.Replace('\\', '/').Replace(":/", "\\:/"),
+ charsetParam,
+ seconds.ToString(UsCulture));
+ }
+
+ return string.Format("subtitles='{0}:si={1}',setpts=PTS -{2}/TB",
+ state.MediaPath.Replace('\\', '/').Replace(":/", "\\:/"),
+ state.InternalSubtitleStreamOffset.ToString(UsCulture),
+ seconds.ToString(UsCulture));
+ }
+
+ protected string GetAudioFilterParam(EncodingJob state, bool isHls)
+ {
+ var volParam = string.Empty;
+ var audioSampleRate = string.Empty;
+
+ var channels = state.OutputAudioChannels;
+
+ // Boost volume to 200% when downsampling from 6ch to 2ch
+ if (channels.HasValue && channels.Value <= 2)
+ {
+ if (state.AudioStream != null && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5)
+ {
+ volParam = ",volume=" + GetEncodingOptions().DownMixAudioBoost.ToString(UsCulture);
+ }
+ }
+
+ if (state.OutputAudioSampleRate.HasValue)
+ {
+ audioSampleRate = state.OutputAudioSampleRate.Value + ":";
+ }
+
+ var adelay = isHls ? "adelay=1," : string.Empty;
+
+ var pts = string.Empty;
+
+ if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream)
+ {
+ var seconds = TimeSpan.FromTicks(state.Options.StartTimeTicks ?? 0).TotalSeconds;
+
+ pts = string.Format(",asetpts=PTS-{0}/TB", Math.Round(seconds).ToString(UsCulture));
+ }
+
+ return string.Format("-af \"{0}aresample={1}async={4}{2}{3}\"",
+
+ adelay,
+ audioSampleRate,
+ volParam,
+ pts,
+ state.OutputAudioSync);
+ }
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs
new file mode 100644
index 0000000000..40ca08c405
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs
@@ -0,0 +1,434 @@
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Net;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.MediaEncoding.Encoder
+{
+ public class EncodingJob : IDisposable
+ {
+ public bool HasExited { get; internal set; }
+
+ public Stream LogFileStream { get; set; }
+ public IProgress Progress { get; set; }
+ public TaskCompletionSource TaskCompletionSource;
+
+ public EncodingJobOptions Options { get; set; }
+ public string InputContainer { get; set; }
+ public List AllMediaStreams { get; set; }
+ public MediaStream AudioStream { get; set; }
+ public MediaStream VideoStream { get; set; }
+ public MediaStream SubtitleStream { get; set; }
+ public IIsoMount IsoMount { get; set; }
+
+ public bool ReadInputAtNativeFramerate { get; set; }
+ public bool IsVideoRequest { get; set; }
+ public string InputAudioSync { get; set; }
+ public string InputVideoSync { get; set; }
+ public string Id { get; set; }
+
+ public string MediaPath { get; set; }
+ public MediaProtocol InputProtocol { get; set; }
+ public bool IsInputVideo { get; set; }
+ public VideoType VideoType { get; set; }
+ public IsoType? IsoType { get; set; }
+ public List PlayableStreamFileNames { get; set; }
+
+ public List SupportedAudioCodecs { get; set; }
+ public Dictionary RemoteHttpHeaders { get; set; }
+ public TransportStreamTimestamp InputTimestamp { get; set; }
+
+ public bool DeInterlace { get; set; }
+ public string MimeType { get; set; }
+ public bool EstimateContentLength { get; set; }
+ public bool EnableMpegtsM2TsMode { get; set; }
+ public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
+ public long? EncodingDurationTicks { get; set; }
+ public string LiveTvStreamId { get; set; }
+ public long? RunTimeTicks;
+
+ public string ItemType { get; set; }
+
+ public long? InputBitrate { get; set; }
+ public long? InputFileSize { get; set; }
+ public string OutputAudioSync = "1";
+ public string OutputVideoSync = "vfr";
+
+ public string GetMimeType(string outputPath)
+ {
+ if (!string.IsNullOrEmpty(MimeType))
+ {
+ return MimeType;
+ }
+
+ return MimeTypes.GetMimeType(outputPath);
+ }
+
+ private readonly ILogger _logger;
+ private readonly ILiveTvManager _liveTvManager;
+
+ public EncodingJob(ILogger logger, ILiveTvManager liveTvManager)
+ {
+ _logger = logger;
+ _liveTvManager = liveTvManager;
+ Id = Guid.NewGuid().ToString("N");
+
+ RemoteHttpHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _logger = logger;
+ SupportedAudioCodecs = new List();
+ PlayableStreamFileNames = new List();
+ RemoteHttpHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ AllMediaStreams = new List();
+ TaskCompletionSource = new TaskCompletionSource();
+ }
+
+ public void Dispose()
+ {
+ DisposeLiveStream();
+ DisposeLogStream();
+ DisposeIsoMount();
+ }
+
+ private void DisposeLogStream()
+ {
+ if (LogFileStream != null)
+ {
+ try
+ {
+ LogFileStream.Dispose();
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error disposing log stream", ex);
+ }
+
+ LogFileStream = null;
+ }
+ }
+
+ private void DisposeIsoMount()
+ {
+ if (IsoMount != null)
+ {
+ try
+ {
+ IsoMount.Dispose();
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error disposing iso mount", ex);
+ }
+
+ IsoMount = null;
+ }
+ }
+
+ private async void DisposeLiveStream()
+ {
+ if (!string.IsNullOrEmpty(LiveTvStreamId))
+ {
+ try
+ {
+ await _liveTvManager.CloseLiveStream(LiveTvStreamId, CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error closing live tv stream", ex);
+ }
+ }
+ }
+
+ public int InternalSubtitleStreamOffset { get; set; }
+
+ public string OutputFilePath { get; set; }
+ public string OutputVideoCodec { get; set; }
+ public string OutputAudioCodec { get; set; }
+ public int? OutputAudioChannels;
+ public int? OutputAudioSampleRate;
+ public int? OutputAudioBitrate;
+ public int? OutputVideoBitrate;
+
+ public string ActualOutputVideoCodec
+ {
+ get
+ {
+ var codec = OutputVideoCodec;
+
+ if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ var stream = VideoStream;
+
+ if (stream != null)
+ {
+ return stream.Codec;
+ }
+
+ return null;
+ }
+
+ return codec;
+ }
+ }
+
+ public string ActualOutputAudioCodec
+ {
+ get
+ {
+ var codec = OutputAudioCodec;
+
+ if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ var stream = AudioStream;
+
+ if (stream != null)
+ {
+ return stream.Codec;
+ }
+
+ return null;
+ }
+
+ return codec;
+ }
+ }
+
+ public int? TotalOutputBitrate
+ {
+ get
+ {
+ return (OutputAudioBitrate ?? 0) + (OutputVideoBitrate ?? 0);
+ }
+ }
+
+ public int? OutputWidth
+ {
+ get
+ {
+ if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue)
+ {
+ var size = new ImageSize
+ {
+ Width = VideoStream.Width.Value,
+ Height = VideoStream.Height.Value
+ };
+
+ var newSize = DrawingUtils.Resize(size,
+ Options.Width,
+ Options.Height,
+ Options.MaxWidth,
+ Options.MaxHeight);
+
+ return Convert.ToInt32(newSize.Width);
+ }
+
+ if (!IsVideoRequest)
+ {
+ return null;
+ }
+
+ return Options.MaxWidth ?? Options.Width;
+ }
+ }
+
+ public int? OutputHeight
+ {
+ get
+ {
+ if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue)
+ {
+ var size = new ImageSize
+ {
+ Width = VideoStream.Width.Value,
+ Height = VideoStream.Height.Value
+ };
+
+ var newSize = DrawingUtils.Resize(size,
+ Options.Width,
+ Options.Height,
+ Options.MaxWidth,
+ Options.MaxHeight);
+
+ return Convert.ToInt32(newSize.Height);
+ }
+
+ if (!IsVideoRequest)
+ {
+ return null;
+ }
+
+ return Options.MaxHeight ?? Options.Height;
+ }
+ }
+
+ ///
+ /// Predicts the audio sample rate that will be in the output stream
+ ///
+ public int? TargetVideoBitDepth
+ {
+ get
+ {
+ var stream = VideoStream;
+ return stream == null || !Options.Static ? null : stream.BitDepth;
+ }
+ }
+
+ ///
+ /// Gets the target reference frames.
+ ///
+ /// The target reference frames.
+ public int? TargetRefFrames
+ {
+ get
+ {
+ var stream = VideoStream;
+ return stream == null || !Options.Static ? null : stream.RefFrames;
+ }
+ }
+
+ ///
+ /// Predicts the audio sample rate that will be in the output stream
+ ///
+ public float? TargetFramerate
+ {
+ get
+ {
+ var stream = VideoStream;
+ var requestedFramerate = Options.MaxFramerate ?? Options.Framerate;
+
+ return requestedFramerate.HasValue && !Options.Static
+ ? requestedFramerate
+ : stream == null ? null : stream.AverageFrameRate ?? stream.RealFrameRate;
+ }
+ }
+
+ ///
+ /// Predicts the audio sample rate that will be in the output stream
+ ///
+ public double? TargetVideoLevel
+ {
+ get
+ {
+ var stream = VideoStream;
+ return Options.Level.HasValue && !Options.Static
+ ? Options.Level.Value
+ : stream == null ? null : stream.Level;
+ }
+ }
+
+ public TransportStreamTimestamp TargetTimestamp
+ {
+ get
+ {
+ var defaultValue = string.Equals(Options.OutputContainer, "m2ts", StringComparison.OrdinalIgnoreCase) ?
+ TransportStreamTimestamp.Valid :
+ TransportStreamTimestamp.None;
+
+ return !Options.Static
+ ? defaultValue
+ : InputTimestamp;
+ }
+ }
+
+ ///
+ /// Predicts the audio sample rate that will be in the output stream
+ ///
+ public int? TargetPacketLength
+ {
+ get
+ {
+ var stream = VideoStream;
+ return !Options.Static
+ ? null
+ : stream == null ? null : stream.PacketLength;
+ }
+ }
+
+ ///
+ /// Predicts the audio sample rate that will be in the output stream
+ ///
+ public string TargetVideoProfile
+ {
+ get
+ {
+ var stream = VideoStream;
+ return !string.IsNullOrEmpty(Options.Profile) && !Options.Static
+ ? Options.Profile
+ : stream == null ? null : stream.Profile;
+ }
+ }
+
+ public bool? IsTargetAnamorphic
+ {
+ get
+ {
+ if (Options.Static)
+ {
+ return VideoStream == null ? null : VideoStream.IsAnamorphic;
+ }
+
+ return false;
+ }
+ }
+
+ public bool? IsTargetCabac
+ {
+ get
+ {
+ if (Options.Static)
+ {
+ return VideoStream == null ? null : VideoStream.IsCabac;
+ }
+
+ return true;
+ }
+ }
+
+ public void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded)
+ {
+ var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null;
+
+ // job.Framerate = framerate;
+
+ if (percentComplete.HasValue)
+ {
+ Progress.Report(percentComplete.Value);
+ }
+
+ // job.TranscodingPositionTicks = ticks;
+ // job.BytesTranscoded = bytesTranscoded;
+
+ var deviceId = Options.DeviceId;
+
+ if (!string.IsNullOrWhiteSpace(deviceId))
+ {
+ var audioCodec = ActualOutputVideoCodec;
+ var videoCodec = ActualOutputVideoCodec;
+
+ // SessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
+ // {
+ // Bitrate = job.TotalOutputBitrate,
+ // AudioCodec = audioCodec,
+ // VideoCodec = videoCodec,
+ // Container = job.Options.OutputContainer,
+ // Framerate = framerate,
+ // CompletionPercentage = percentComplete,
+ // Width = job.OutputWidth,
+ // Height = job.OutputHeight,
+ // AudioChannels = job.OutputAudioChannels,
+ // IsAudioDirect = string.Equals(job.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase),
+ // IsVideoDirect = string.Equals(job.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)
+ // });
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs
new file mode 100644
index 0000000000..00c7b61e3d
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs
@@ -0,0 +1,830 @@
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.MediaEncoding.Encoder
+{
+ public class EncodingJobFactory
+ {
+ private readonly ILogger _logger;
+ private readonly ILiveTvManager _liveTvManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IChannelManager _channelManager;
+
+ protected static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ public EncodingJobFactory(ILogger logger, ILiveTvManager liveTvManager, ILibraryManager libraryManager, IChannelManager channelManager)
+ {
+ _logger = logger;
+ _liveTvManager = liveTvManager;
+ _libraryManager = libraryManager;
+ _channelManager = channelManager;
+ }
+
+ public async Task CreateJob(EncodingJobOptions options, bool isVideoRequest, IProgress progress, CancellationToken cancellationToken)
+ {
+ var request = options;
+
+ if (string.IsNullOrEmpty(request.AudioCodec))
+ {
+ request.AudioCodec = InferAudioCodec(request.OutputContainer);
+ }
+
+ var state = new EncodingJob(_logger, _liveTvManager)
+ {
+ Options = options,
+ IsVideoRequest = isVideoRequest,
+ Progress = progress
+ };
+
+ if (!string.IsNullOrWhiteSpace(request.AudioCodec))
+ {
+ state.SupportedAudioCodecs = request.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
+ request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault();
+ }
+
+ var item = _libraryManager.GetItemById(request.ItemId);
+
+ List mediaStreams = null;
+
+ state.ItemType = item.GetType().Name;
+
+ if (item is ILiveTvRecording)
+ {
+ var recording = await _liveTvManager.GetInternalRecording(request.ItemId, cancellationToken).ConfigureAwait(false);
+
+ state.VideoType = VideoType.VideoFile;
+ state.IsInputVideo = string.Equals(recording.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
+
+ var path = recording.RecordingInfo.Path;
+ var mediaUrl = recording.RecordingInfo.Url;
+
+ var source = string.IsNullOrEmpty(request.MediaSourceId)
+ ? recording.GetMediaSources(false).First()
+ : recording.GetMediaSources(false).First(i => string.Equals(i.Id, request.MediaSourceId));
+
+ mediaStreams = source.MediaStreams;
+
+ // Just to prevent this from being null and causing other methods to fail
+ state.MediaPath = string.Empty;
+
+ if (!string.IsNullOrEmpty(path))
+ {
+ state.MediaPath = path;
+ state.InputProtocol = MediaProtocol.File;
+ }
+ else if (!string.IsNullOrEmpty(mediaUrl))
+ {
+ state.MediaPath = mediaUrl;
+ state.InputProtocol = MediaProtocol.Http;
+ }
+
+ state.RunTimeTicks = recording.RunTimeTicks;
+ state.DeInterlace = true;
+ state.OutputAudioSync = "1000";
+ state.InputVideoSync = "-1";
+ state.InputAudioSync = "1";
+ state.InputContainer = recording.Container;
+ state.ReadInputAtNativeFramerate = source.ReadAtNativeFramerate;
+ }
+ else if (item is LiveTvChannel)
+ {
+ var channel = _liveTvManager.GetInternalChannel(request.ItemId);
+
+ state.VideoType = VideoType.VideoFile;
+ state.IsInputVideo = string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
+ mediaStreams = new List();
+
+ state.DeInterlace = true;
+
+ // Just to prevent this from being null and causing other methods to fail
+ state.MediaPath = string.Empty;
+ }
+ else if (item is IChannelMediaItem)
+ {
+ var mediaSource = await GetChannelMediaInfo(request.ItemId, request.MediaSourceId, cancellationToken).ConfigureAwait(false);
+ state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
+ state.InputProtocol = mediaSource.Protocol;
+ state.MediaPath = mediaSource.Path;
+ state.RunTimeTicks = item.RunTimeTicks;
+ state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
+ state.InputBitrate = mediaSource.Bitrate;
+ state.InputFileSize = mediaSource.Size;
+ state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
+ mediaStreams = mediaSource.MediaStreams;
+ }
+ else
+ {
+ var hasMediaSources = (IHasMediaSources)item;
+ var mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
+ ? hasMediaSources.GetMediaSources(false).First()
+ : hasMediaSources.GetMediaSources(false).First(i => string.Equals(i.Id, request.MediaSourceId));
+
+ mediaStreams = mediaSource.MediaStreams;
+
+ state.MediaPath = mediaSource.Path;
+ state.InputProtocol = mediaSource.Protocol;
+ state.InputContainer = mediaSource.Container;
+ state.InputFileSize = mediaSource.Size;
+ state.InputBitrate = mediaSource.Bitrate;
+ state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
+
+ var video = item as Video;
+
+ if (video != null)
+ {
+ state.IsInputVideo = true;
+
+ if (mediaSource.VideoType.HasValue)
+ {
+ state.VideoType = mediaSource.VideoType.Value;
+ }
+
+ state.IsoType = mediaSource.IsoType;
+
+ state.PlayableStreamFileNames = mediaSource.PlayableStreamFileNames.ToList();
+
+ if (mediaSource.Timestamp.HasValue)
+ {
+ state.InputTimestamp = mediaSource.Timestamp.Value;
+ }
+ }
+
+ state.RunTimeTicks = mediaSource.RunTimeTicks;
+ }
+
+ AttachMediaStreamInfo(state, mediaStreams, request);
+
+ state.OutputAudioBitrate = GetAudioBitrateParam(request, state.AudioStream);
+ state.OutputAudioSampleRate = request.AudioSampleRate;
+
+ state.OutputAudioCodec = GetAudioCodec(request);
+
+ state.OutputAudioChannels = GetNumAudioChannelsParam(request, state.AudioStream, state.OutputAudioCodec);
+
+ if (isVideoRequest)
+ {
+ state.OutputVideoCodec = GetVideoCodec(request);
+ state.OutputVideoBitrate = GetVideoBitrateParamValue(request, state.VideoStream);
+
+ if (state.OutputVideoBitrate.HasValue)
+ {
+ var resolution = ResolutionNormalizer.Normalize(state.OutputVideoBitrate.Value,
+ state.OutputVideoCodec,
+ request.MaxWidth,
+ request.MaxHeight);
+
+ request.MaxWidth = resolution.MaxWidth;
+ request.MaxHeight = resolution.MaxHeight;
+ }
+ }
+
+ ApplyDeviceProfileSettings(state);
+
+ if (isVideoRequest)
+ {
+ if (state.VideoStream != null && CanStreamCopyVideo(request, state.VideoStream))
+ {
+ state.OutputVideoCodec = "copy";
+ }
+
+ if (state.AudioStream != null && CanStreamCopyAudio(request, state.AudioStream, state.SupportedAudioCodecs))
+ {
+ state.OutputAudioCodec = "copy";
+ }
+ }
+
+ return state;
+ }
+
+ internal static void AttachMediaStreamInfo(EncodingJob state,
+ List mediaStreams,
+ EncodingJobOptions videoRequest)
+ {
+ if (videoRequest != null)
+ {
+ if (string.IsNullOrEmpty(videoRequest.VideoCodec))
+ {
+ videoRequest.VideoCodec = InferVideoCodec(videoRequest.OutputContainer);
+ }
+
+ state.VideoStream = GetMediaStream(mediaStreams, videoRequest.VideoStreamIndex, MediaStreamType.Video);
+ state.SubtitleStream = GetMediaStream(mediaStreams, videoRequest.SubtitleStreamIndex, MediaStreamType.Subtitle, false);
+ state.AudioStream = GetMediaStream(mediaStreams, videoRequest.AudioStreamIndex, MediaStreamType.Audio);
+
+ if (state.SubtitleStream != null && !state.SubtitleStream.IsExternal)
+ {
+ state.InternalSubtitleStreamOffset = mediaStreams.Where(i => i.Type == MediaStreamType.Subtitle && !i.IsExternal).ToList().IndexOf(state.SubtitleStream);
+ }
+
+ if (state.VideoStream != null && state.VideoStream.IsInterlaced)
+ {
+ state.DeInterlace = true;
+ }
+
+ EnforceResolutionLimit(state, videoRequest);
+ }
+ else
+ {
+ state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
+ }
+
+ state.AllMediaStreams = mediaStreams;
+ }
+
+ ///
+ /// Infers the video codec.
+ ///
+ /// The container.
+ /// System.Nullable{VideoCodecs}.
+ private static string InferVideoCodec(string container)
+ {
+ if (string.Equals(container, "asf", StringComparison.OrdinalIgnoreCase))
+ {
+ return "wmv";
+ }
+ if (string.Equals(container, "webm", StringComparison.OrdinalIgnoreCase))
+ {
+ return "vpx";
+ }
+ if (string.Equals(container, "ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(container, "ogv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "theora";
+ }
+ if (string.Equals(container, "m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(container, "ts", StringComparison.OrdinalIgnoreCase))
+ {
+ return "h264";
+ }
+
+ return "copy";
+ }
+
+ private string InferAudioCodec(string container)
+ {
+ if (string.Equals(container, "mp3", StringComparison.OrdinalIgnoreCase))
+ {
+ return "mp3";
+ }
+ if (string.Equals(container, "aac", StringComparison.OrdinalIgnoreCase))
+ {
+ return "aac";
+ }
+ if (string.Equals(container, "wma", StringComparison.OrdinalIgnoreCase))
+ {
+ return "wma";
+ }
+ if (string.Equals(container, "ogg", StringComparison.OrdinalIgnoreCase))
+ {
+ return "vorbis";
+ }
+ if (string.Equals(container, "oga", StringComparison.OrdinalIgnoreCase))
+ {
+ return "vorbis";
+ }
+ if (string.Equals(container, "ogv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "vorbis";
+ }
+ if (string.Equals(container, "webm", StringComparison.OrdinalIgnoreCase))
+ {
+ return "vorbis";
+ }
+ if (string.Equals(container, "webma", StringComparison.OrdinalIgnoreCase))
+ {
+ return "vorbis";
+ }
+
+ return "copy";
+ }
+
+ ///
+ /// Determines which stream will be used for playback
+ ///
+ /// All stream.
+ /// Index of the desired.
+ /// The type.
+ /// if set to true [return first if no index].
+ /// MediaStream.
+ private static MediaStream GetMediaStream(IEnumerable allStream, int? desiredIndex, MediaStreamType type, bool returnFirstIfNoIndex = true)
+ {
+ var streams = allStream.Where(s => s.Type == type).OrderBy(i => i.Index).ToList();
+
+ if (desiredIndex.HasValue)
+ {
+ var stream = streams.FirstOrDefault(s => s.Index == desiredIndex.Value);
+
+ if (stream != null)
+ {
+ return stream;
+ }
+ }
+
+ if (type == MediaStreamType.Video)
+ {
+ streams = streams.Where(i => !string.Equals(i.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+
+ if (returnFirstIfNoIndex && type == MediaStreamType.Audio)
+ {
+ return streams.FirstOrDefault(i => i.Channels.HasValue && i.Channels.Value > 0) ??
+ streams.FirstOrDefault();
+ }
+
+ // Just return the first one
+ return returnFirstIfNoIndex ? streams.FirstOrDefault() : null;
+ }
+
+ ///
+ /// Enforces the resolution limit.
+ ///
+ /// The state.
+ /// The video request.
+ private static void EnforceResolutionLimit(EncodingJob state, EncodingJobOptions videoRequest)
+ {
+ // Switch the incoming params to be ceilings rather than fixed values
+ videoRequest.MaxWidth = videoRequest.MaxWidth ?? videoRequest.Width;
+ videoRequest.MaxHeight = videoRequest.MaxHeight ?? videoRequest.Height;
+
+ videoRequest.Width = null;
+ videoRequest.Height = null;
+ }
+
+ ///
+ /// Gets the number of audio channels to specify on the command line
+ ///
+ /// The request.
+ /// The audio stream.
+ /// The output audio codec.
+ /// System.Nullable{System.Int32}.
+ private int? GetNumAudioChannelsParam(EncodingJobOptions request, MediaStream audioStream, string outputAudioCodec)
+ {
+ if (audioStream != null)
+ {
+ var codec = outputAudioCodec ?? string.Empty;
+
+ if (audioStream.Channels > 2 && codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ // wmav2 currently only supports two channel output
+ return 2;
+ }
+ }
+
+ if (request.MaxAudioChannels.HasValue)
+ {
+ if (audioStream != null && audioStream.Channels.HasValue)
+ {
+ return Math.Min(request.MaxAudioChannels.Value, audioStream.Channels.Value);
+ }
+
+ // If we don't have any media info then limit it to 5 to prevent encoding errors due to asking for too many channels
+ return Math.Min(request.MaxAudioChannels.Value, 5);
+ }
+
+ return request.AudioChannels;
+ }
+
+ private int? GetVideoBitrateParamValue(EncodingJobOptions request, MediaStream videoStream)
+ {
+ var bitrate = request.VideoBitRate;
+
+ if (videoStream != null)
+ {
+ var isUpscaling = request.Height.HasValue && videoStream.Height.HasValue &&
+ request.Height.Value > videoStream.Height.Value;
+
+ if (request.Width.HasValue && videoStream.Width.HasValue &&
+ request.Width.Value > videoStream.Width.Value)
+ {
+ isUpscaling = true;
+ }
+
+ // Don't allow bitrate increases unless upscaling
+ if (!isUpscaling)
+ {
+ if (bitrate.HasValue && videoStream.BitRate.HasValue)
+ {
+ bitrate = Math.Min(bitrate.Value, videoStream.BitRate.Value);
+ }
+ }
+ }
+
+ return bitrate;
+ }
+
+ private async Task GetChannelMediaInfo(string id,
+ string mediaSourceId,
+ CancellationToken cancellationToken)
+ {
+ var channelMediaSources = await _channelManager.GetChannelItemMediaSources(id, true, cancellationToken)
+ .ConfigureAwait(false);
+
+ var list = channelMediaSources.ToList();
+
+ if (!string.IsNullOrWhiteSpace(mediaSourceId))
+ {
+ var source = list
+ .FirstOrDefault(i => string.Equals(mediaSourceId, i.Id));
+
+ if (source != null)
+ {
+ return source;
+ }
+ }
+
+ return list.First();
+ }
+
+ protected string GetVideoBitrateParam(EncodingJob state, string videoCodec, bool isHls)
+ {
+ var bitrate = state.OutputVideoBitrate;
+
+ if (bitrate.HasValue)
+ {
+ var hasFixedResolution = state.Options.HasFixedResolution;
+
+ if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
+ {
+ if (hasFixedResolution)
+ {
+ return string.Format(" -minrate:v ({0}*.90) -maxrate:v ({0}*1.10) -bufsize:v {0} -b:v {0}", bitrate.Value.ToString(UsCulture));
+ }
+
+ // With vpx when crf is used, b:v becomes a max rate
+ // https://trac.ffmpeg.org/wiki/vpxEncodingGuide. But higher bitrate source files -b:v causes judder so limite the bitrate but dont allow it to "saturate" the bitrate. So dont contrain it down just up.
+ return string.Format(" -maxrate:v {0} -bufsize:v ({0}*2) -b:v {0}", bitrate.Value.ToString(UsCulture));
+ }
+
+ if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
+ {
+ return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
+ }
+
+ // H264
+ if (hasFixedResolution)
+ {
+ if (isHls)
+ {
+ return string.Format(" -b:v {0} -maxrate ({0}*.80) -bufsize {0}", bitrate.Value.ToString(UsCulture));
+ }
+
+ return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
+ }
+
+ return string.Format(" -maxrate {0} -bufsize {1}",
+ bitrate.Value.ToString(UsCulture),
+ (bitrate.Value * 2).ToString(UsCulture));
+ }
+
+ return string.Empty;
+ }
+
+ private int? GetAudioBitrateParam(EncodingJobOptions request, MediaStream audioStream)
+ {
+ if (request.AudioBitRate.HasValue)
+ {
+ // Make sure we don't request a bitrate higher than the source
+ var currentBitrate = audioStream == null ? request.AudioBitRate.Value : audioStream.BitRate ?? request.AudioBitRate.Value;
+
+ return request.AudioBitRate.Value;
+ //return Math.Min(currentBitrate, request.AudioBitRate.Value);
+ }
+
+ return null;
+ }
+
+ ///
+ /// 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;
+ }
+
+ ///
+ /// Gets the name of the output audio codec
+ ///
+ /// The request.
+ /// System.String.
+ private string GetAudioCodec(EncodingJobOptions request)
+ {
+ var codec = request.AudioCodec;
+
+ if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase))
+ {
+ return "aac -strict experimental";
+ }
+ if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
+ {
+ return "libmp3lame";
+ }
+ if (string.Equals(codec, "vorbis", StringComparison.OrdinalIgnoreCase))
+ {
+ return "libvorbis";
+ }
+ if (string.Equals(codec, "wma", StringComparison.OrdinalIgnoreCase))
+ {
+ return "wmav2";
+ }
+
+ return (codec ?? string.Empty).ToLower();
+ }
+
+ ///
+ /// Gets the name of the output video codec
+ ///
+ /// The request.
+ /// System.String.
+ private string GetVideoCodec(EncodingJobOptions request)
+ {
+ var codec = request.VideoCodec;
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
+ {
+ return "libx264";
+ }
+ if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase))
+ {
+ return "libx265";
+ }
+ if (string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase))
+ {
+ return "libvpx";
+ }
+ if (string.Equals(codec, "wmv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "wmv2";
+ }
+ if (string.Equals(codec, "theora", StringComparison.OrdinalIgnoreCase))
+ {
+ return "libtheora";
+ }
+
+ return codec.ToLower();
+ }
+
+ return "copy";
+ }
+
+ internal static bool CanStreamCopyVideo(EncodingJobOptions request, MediaStream videoStream)
+ {
+ if (videoStream.IsInterlaced)
+ {
+ return false;
+ }
+
+ // Can't stream copy if we're burning in subtitles
+ if (request.SubtitleStreamIndex.HasValue)
+ {
+ if (request.SubtitleMethod == SubtitleDeliveryMethod.Encode)
+ {
+ return false;
+ }
+ }
+
+ // Source and target codecs must match
+ if (!string.Equals(request.VideoCodec, videoStream.Codec, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ // If client is requesting a specific video profile, it must match the source
+ if (!string.IsNullOrEmpty(request.Profile))
+ {
+ if (string.IsNullOrEmpty(videoStream.Profile))
+ {
+ return false;
+ }
+
+ if (!string.Equals(request.Profile, videoStream.Profile, StringComparison.OrdinalIgnoreCase))
+ {
+ var currentScore = GetVideoProfileScore(videoStream.Profile);
+ var requestedScore = GetVideoProfileScore(request.Profile);
+
+ if (currentScore == -1 || currentScore > requestedScore)
+ {
+ return false;
+ }
+ }
+ }
+
+ // Video width must fall within requested value
+ if (request.MaxWidth.HasValue)
+ {
+ if (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value)
+ {
+ return false;
+ }
+ }
+
+ // Video height must fall within requested value
+ if (request.MaxHeight.HasValue)
+ {
+ if (!videoStream.Height.HasValue || videoStream.Height.Value > request.MaxHeight.Value)
+ {
+ return false;
+ }
+ }
+
+ // Video framerate must fall within requested value
+ var requestedFramerate = request.MaxFramerate ?? request.Framerate;
+ if (requestedFramerate.HasValue)
+ {
+ var videoFrameRate = videoStream.AverageFrameRate ?? videoStream.RealFrameRate;
+
+ if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value)
+ {
+ return false;
+ }
+ }
+
+ // Video bitrate must fall within requested value
+ if (request.VideoBitRate.HasValue)
+ {
+ if (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value)
+ {
+ return false;
+ }
+ }
+
+ if (request.MaxVideoBitDepth.HasValue)
+ {
+ if (videoStream.BitDepth.HasValue && videoStream.BitDepth.Value > request.MaxVideoBitDepth.Value)
+ {
+ return false;
+ }
+ }
+
+ if (request.MaxRefFrames.HasValue)
+ {
+ if (videoStream.RefFrames.HasValue && videoStream.RefFrames.Value > request.MaxRefFrames.Value)
+ {
+ return false;
+ }
+ }
+
+ // If a specific level was requested, the source must match or be less than
+ if (request.Level.HasValue)
+ {
+ if (!videoStream.Level.HasValue)
+ {
+ return false;
+ }
+
+ if (videoStream.Level.Value > request.Level.Value)
+ {
+ return false;
+ }
+ }
+
+ if (request.Cabac.HasValue && request.Cabac.Value)
+ {
+ if (videoStream.IsCabac.HasValue && !videoStream.IsCabac.Value)
+ {
+ return false;
+ }
+ }
+
+ return request.EnableAutoStreamCopy;
+ }
+
+ 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));
+ }
+
+ internal static bool CanStreamCopyAudio(EncodingJobOptions request, MediaStream audioStream, List supportedAudioCodecs)
+ {
+ // Source and target codecs must match
+ if (string.IsNullOrEmpty(audioStream.Codec) || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ // Video bitrate must fall within requested value
+ if (request.AudioBitRate.HasValue)
+ {
+ if (!audioStream.BitRate.HasValue || audioStream.BitRate.Value <= 0)
+ {
+ return false;
+ }
+ if (audioStream.BitRate.Value > request.AudioBitRate.Value)
+ {
+ return false;
+ }
+ }
+
+ // Channels must fall within requested value
+ var channels = request.AudioChannels ?? request.MaxAudioChannels;
+ if (channels.HasValue)
+ {
+ if (!audioStream.Channels.HasValue || audioStream.Channels.Value <= 0)
+ {
+ return false;
+ }
+ if (audioStream.Channels.Value > channels.Value)
+ {
+ return false;
+ }
+ }
+
+ // Sample rate must fall within requested value
+ if (request.AudioSampleRate.HasValue)
+ {
+ if (!audioStream.SampleRate.HasValue || audioStream.SampleRate.Value <= 0)
+ {
+ return false;
+ }
+ if (audioStream.SampleRate.Value > request.AudioSampleRate.Value)
+ {
+ return false;
+ }
+ }
+
+ return request.EnableAutoStreamCopy;
+ }
+
+ private void ApplyDeviceProfileSettings(EncodingJob state)
+ {
+ var profile = state.Options.DeviceProfile;
+
+ if (profile == null)
+ {
+ // Don't use settings from the default profile.
+ // Only use a specific profile if it was requested.
+ return;
+ }
+
+ var audioCodec = state.ActualOutputAudioCodec;
+
+ var videoCodec = state.ActualOutputVideoCodec;
+ var outputContainer = state.Options.OutputContainer;
+
+ var mediaProfile = state.IsVideoRequest ?
+ profile.GetAudioMediaProfile(outputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate) :
+ profile.GetVideoMediaProfile(outputContainer,
+ audioCodec,
+ videoCodec,
+ state.OutputAudioBitrate,
+ state.OutputAudioChannels,
+ state.OutputWidth,
+ state.OutputHeight,
+ state.TargetVideoBitDepth,
+ state.OutputVideoBitrate,
+ state.TargetVideoProfile,
+ state.TargetVideoLevel,
+ state.TargetFramerate,
+ state.TargetPacketLength,
+ state.TargetTimestamp,
+ state.IsTargetAnamorphic,
+ state.IsTargetCabac,
+ state.TargetRefFrames);
+
+ if (mediaProfile != null)
+ {
+ state.MimeType = mediaProfile.MimeType;
+ }
+
+ var transcodingProfile = state.IsVideoRequest ?
+ profile.GetAudioTranscodingProfile(outputContainer, audioCodec) :
+ profile.GetVideoTranscodingProfile(outputContainer, audioCodec, videoCodec);
+
+ if (transcodingProfile != null)
+ {
+ state.EstimateContentLength = transcodingProfile.EstimateContentLength;
+ state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
+ state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Encoder/JobLogger.cs b/MediaBrowser.MediaEncoding/Encoder/JobLogger.cs
new file mode 100644
index 0000000000..6be8705192
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Encoder/JobLogger.cs
@@ -0,0 +1,122 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace MediaBrowser.MediaEncoding.Encoder
+{
+ public class JobLogger
+ {
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+ private readonly ILogger _logger;
+
+ public JobLogger(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public async void StartStreamingLog(EncodingJob transcodingJob, Stream source, Stream target)
+ {
+ try
+ {
+ using (var reader = new StreamReader(source))
+ {
+ while (!reader.EndOfStream)
+ {
+ var line = await reader.ReadLineAsync().ConfigureAwait(false);
+
+ ParseLogLine(line, transcodingJob);
+
+ var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
+
+ await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error reading ffmpeg log", ex);
+ }
+ }
+
+ private void ParseLogLine(string line, EncodingJob transcodingJob)
+ {
+ float? framerate = null;
+ double? percent = null;
+ TimeSpan? transcodingPosition = null;
+ long? bytesTranscoded = null;
+
+ var parts = line.Split(' ');
+
+ var totalMs = transcodingJob.RunTimeTicks.HasValue
+ ? TimeSpan.FromTicks(transcodingJob.RunTimeTicks.Value).TotalMilliseconds
+ : 0;
+
+ var startMs = transcodingJob.Options.StartTimeTicks.HasValue
+ ? TimeSpan.FromTicks(transcodingJob.Options.StartTimeTicks.Value).TotalMilliseconds
+ : 0;
+
+ for (var i = 0; i < parts.Length; i++)
+ {
+ var part = parts[i];
+
+ if (string.Equals(part, "fps=", StringComparison.OrdinalIgnoreCase) &&
+ (i + 1 < parts.Length))
+ {
+ var rate = parts[i + 1];
+ float val;
+
+ if (float.TryParse(rate, NumberStyles.Any, _usCulture, out val))
+ {
+ framerate = val;
+ }
+ }
+ else if (transcodingJob.RunTimeTicks.HasValue &&
+ part.StartsWith("time=", StringComparison.OrdinalIgnoreCase))
+ {
+ var time = part.Split(new[] { '=' }, 2).Last();
+ TimeSpan val;
+
+ if (TimeSpan.TryParse(time, _usCulture, out val))
+ {
+ var currentMs = startMs + val.TotalMilliseconds;
+
+ var percentVal = currentMs / totalMs;
+ percent = 100 * percentVal;
+
+ transcodingPosition = val;
+ }
+ }
+ else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase))
+ {
+ var size = part.Split(new[] { '=' }, 2).Last();
+
+ int? scale = null;
+ if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ scale = 1024;
+ size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase);
+ }
+
+ if (scale.HasValue)
+ {
+ long val;
+
+ if (long.TryParse(size, NumberStyles.Any, _usCulture, out val))
+ {
+ bytesTranscoded = val * scale.Value;
+ }
+ }
+ }
+ }
+
+ if (framerate.HasValue || percent.HasValue)
+ {
+ transcodingJob.ReportTranscodingProgress(transcodingPosition, framerate, percent, bytesTranscoded);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index a7d8b6f1d0..e800b4254a 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -64,8 +64,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
protected readonly ILibraryManager LibraryManager;
protected readonly IChannelManager ChannelManager;
protected readonly ISessionManager SessionManager;
-
- public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager)
+ protected readonly Func SubtitleEncoder;
+
+ public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, Func subtitleEncoder)
{
_logger = logger;
_jsonSerializer = jsonSerializer;
@@ -77,6 +78,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
LibraryManager = libraryManager;
ChannelManager = channelManager;
SessionManager = sessionManager;
+ SubtitleEncoder = subtitleEncoder;
FFProbePath = ffProbePath;
FFMpegPath = ffMpegPath;
}
@@ -494,7 +496,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
};
_logger.Info(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
-
+
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
process.Start();
@@ -537,15 +539,37 @@ namespace MediaBrowser.MediaEncoding.Encoder
IProgress progress,
CancellationToken cancellationToken)
{
- var job = await new AudioEncoder(this,
- _logger,
- ConfigurationManager,
- FileSystem,
+ var job = await new AudioEncoder(this,
+ _logger,
+ ConfigurationManager,
+ FileSystem,
LiveTvManager,
- IsoManager,
- LibraryManager,
- ChannelManager,
- SessionManager)
+ IsoManager,
+ LibraryManager,
+ ChannelManager,
+ SessionManager,
+ SubtitleEncoder())
+ .Start(options, progress, cancellationToken).ConfigureAwait(false);
+
+ await job.TaskCompletionSource.Task.ConfigureAwait(false);
+
+ return job.OutputFilePath;
+ }
+
+ public async Task EncodeVideo(EncodingJobOptions options,
+ IProgress progress,
+ CancellationToken cancellationToken)
+ {
+ var job = await new VideoEncoder(this,
+ _logger,
+ ConfigurationManager,
+ FileSystem,
+ LiveTvManager,
+ IsoManager,
+ LibraryManager,
+ ChannelManager,
+ SessionManager,
+ SubtitleEncoder())
.Start(options, progress, cancellationToken).ConfigureAwait(false);
await job.TaskCompletionSource.Task.ConfigureAwait(false);
diff --git a/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs
new file mode 100644
index 0000000000..36406e3a3f
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs
@@ -0,0 +1,177 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using System;
+using System.IO;
+
+namespace MediaBrowser.MediaEncoding.Encoder
+{
+ public class VideoEncoder : BaseEncoder
+ {
+ public VideoEncoder(MediaEncoder mediaEncoder, ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder) : base(mediaEncoder, logger, configurationManager, fileSystem, liveTvManager, isoManager, libraryManager, channelManager, sessionManager, subtitleEncoder)
+ {
+ }
+
+ protected override string GetCommandLineArguments(EncodingJob state)
+ {
+ // Get the output codec name
+ var videoCodec = state.OutputVideoCodec;
+
+ var format = string.Empty;
+ var keyFrame = string.Empty;
+
+ if (string.Equals(Path.GetExtension(state.OutputFilePath), ".mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ format = " -f mp4 -movflags frag_keyframe+empty_moov";
+ }
+
+ var threads = GetNumberOfThreads(state, string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase));
+
+ var inputModifier = GetInputModifier(state);
+
+ return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -threads {5} {6}{7} -y \"{8}\"",
+ inputModifier,
+ GetInputArgument(state),
+ keyFrame,
+ GetMapArgs(state),
+ GetVideoArguments(state, videoCodec),
+ threads,
+ GetAudioArguments(state),
+ format,
+ state.OutputFilePath
+ ).Trim();
+ }
+
+ ///
+ /// Gets video arguments to pass to ffmpeg
+ ///
+ /// The state.
+ /// The video codec.
+ /// System.String.
+ private string GetVideoArguments(EncodingJob state, string codec)
+ {
+ var args = "-codec:v:0 " + codec;
+
+ if (state.EnableMpegtsM2TsMode)
+ {
+ args += " -mpegts_m2ts_mode 1";
+ }
+
+ // See if we can save come cpu cycles by avoiding encoding
+ if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return state.VideoStream != null && IsH264(state.VideoStream) && string.Equals(state.Options.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) ?
+ args + " -bsf:v h264_mp4toannexb" :
+ args;
+ }
+
+ var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
+ 5.ToString(UsCulture));
+
+ args += keyFrameArg;
+
+ var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
+
+ // Add resolution params, if specified
+ if (!hasGraphicalSubs)
+ {
+ args += GetOutputSizeParam(state, codec);
+ }
+
+ var qualityParam = GetVideoQualityParam(state, codec, false);
+
+ if (!string.IsNullOrEmpty(qualityParam))
+ {
+ args += " " + qualityParam.Trim();
+ }
+
+ // This is for internal graphical subs
+ if (hasGraphicalSubs)
+ {
+ args += GetGraphicalSubtitleParam(state, codec);
+ }
+
+ return args;
+ }
+
+ ///
+ /// Gets audio arguments to pass to ffmpeg
+ ///
+ /// The state.
+ /// System.String.
+ private string GetAudioArguments(EncodingJob state)
+ {
+ // If the video doesn't have an audio stream, return a default.
+ if (state.AudioStream == null && state.VideoStream != null)
+ {
+ return string.Empty;
+ }
+
+ // Get the output codec name
+ var codec = state.OutputAudioCodec;
+
+ var args = "-codec:a:0 " + codec;
+
+ if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return args;
+ }
+
+ // Add the number of audio channels
+ var channels = state.OutputAudioChannels;
+
+ if (channels.HasValue)
+ {
+ args += " -ac " + channels.Value;
+ }
+
+ var bitrate = state.OutputAudioBitrate;
+
+ if (bitrate.HasValue)
+ {
+ args += " -ab " + bitrate.Value.ToString(UsCulture);
+ }
+
+ args += " " + GetAudioFilterParam(state, false);
+
+ return args;
+ }
+
+ protected override string GetOutputFileExtension(EncodingJob state)
+ {
+ var ext = base.GetOutputFileExtension(state);
+
+ if (!string.IsNullOrEmpty(ext))
+ {
+ return ext;
+ }
+
+ var videoCodec = state.Options.VideoCodec;
+
+ if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
+ {
+ return ".ts";
+ }
+ if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
+ {
+ return ".ogv";
+ }
+ if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
+ {
+ return ".webm";
+ }
+ if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
+ {
+ return ".asf";
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 9daa3319f9..38d8fa1383 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -64,6 +64,7 @@
+
diff --git a/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs b/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs
index 8e5b765a6a..896e49cb21 100644
--- a/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs
+++ b/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs
@@ -412,7 +412,7 @@ namespace MediaBrowser.Server.Implementations.Sync
jobItem.Status = SyncJobItemStatus.Converting;
await _syncRepo.Update(jobItem).ConfigureAwait(false);
- //jobItem.OutputPath = await MediaEncoder.EncodeAudio(new EncodingJobOptions(streamInfo, profile), new Progress(), cancellationToken);
+ jobItem.OutputPath = await MediaEncoder.EncodeVideo(new EncodingJobOptions(streamInfo, profile), new Progress(), cancellationToken);
}
else
{
@@ -420,7 +420,7 @@ namespace MediaBrowser.Server.Implementations.Sync
{
jobItem.OutputPath = mediaSource.Path;
}
- if (mediaSource.Protocol == MediaProtocol.Http)
+ else if (mediaSource.Protocol == MediaProtocol.Http)
{
jobItem.OutputPath = await DownloadFile(jobItem, mediaSource, cancellationToken).ConfigureAwait(false);
}
@@ -464,7 +464,7 @@ namespace MediaBrowser.Server.Implementations.Sync
{
jobItem.OutputPath = mediaSource.Path;
}
- if (mediaSource.Protocol == MediaProtocol.Http)
+ else if (mediaSource.Protocol == MediaProtocol.Http)
{
jobItem.OutputPath = await DownloadFile(jobItem, mediaSource, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs
index 8a1721b7e0..41f0a28060 100644
--- a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs
+++ b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs
@@ -185,6 +185,7 @@ namespace MediaBrowser.Server.Startup.Common
///
/// The media encoder.
private IMediaEncoder MediaEncoder { get; set; }
+ private ISubtitleEncoder SubtitleEncoder { get; set; }
private IConnectManager ConnectManager { get; set; }
private ISessionManager SessionManager { get; set; }
@@ -560,7 +561,8 @@ namespace MediaBrowser.Server.Startup.Common
RegisterSingleInstance(new SessionContext(UserManager, authContext, SessionManager));
RegisterSingleInstance(new AuthService(UserManager, authContext, ServerConfigurationManager, ConnectManager, SessionManager, DeviceManager));
- RegisterSingleInstance(new SubtitleEncoder(LibraryManager, LogManager.GetLogger("SubtitleEncoder"), ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer));
+ SubtitleEncoder = new SubtitleEncoder(LibraryManager, LogManager.GetLogger("SubtitleEncoder"), ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer);
+ RegisterSingleInstance(SubtitleEncoder);
await ConfigureDisplayPreferencesRepositories().ConfigureAwait(false);
await ConfigureItemRepositories().ConfigureAwait(false);
@@ -602,7 +604,8 @@ namespace MediaBrowser.Server.Startup.Common
IsoManager,
LibraryManager,
ChannelManager,
- SessionManager);
+ SessionManager,
+ () => SubtitleEncoder);
RegisterSingleInstance(MediaEncoder);
}