Browse Source

sync video transcoding

Luke Pulverenti 10 years ago
parent
commit
c63c39ce57

+ 91 - 0
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; }
+
+        /// <summary>
+        /// Gets a value indicating whether this instance has fixed resolution.
+        /// </summary>
+        /// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value>
+        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;
+        }
+    }
+}

+ 11 - 0
MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs

@@ -107,5 +107,16 @@ namespace MediaBrowser.Controller.MediaEncoding
         Task<string> EncodeAudio(EncodingJobOptions options,
             IProgress<double> progress,
             CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Encodes the video.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task&lt;System.String&gt;.</returns>
+        Task<string> EncodeVideo(EncodingJobOptions options,
+            IProgress<double> progress,
+            CancellationToken cancellationToken);
     }
 }

+ 86 - 0
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<string>();
+
+            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;
+        }
+    }
+}

+ 1049 - 0
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<EncodingJob> Start(EncodingJobOptions options,
+            IProgress<double> 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;
+        }
+
+        /// <summary>
+        /// Processes the exited.
+        /// </summary>
+        /// <param name="process">The process.</param>
+        /// <param name="job">The job.</param>
+        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<EncodingOptions>("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;
+        }
+
+        /// <summary>
+        /// Gets the number of threads.
+        /// </summary>
+        /// <returns>System.Int32.</returns>
+        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;
+        }
+
+        /// <summary>
+        /// Gets the probe size argument.
+        /// </summary>
+        /// <param name="job">The job.</param>
+        /// <returns>System.String.</returns>
+        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);
+        }
+
+        /// <summary>
+        /// Gets the fast seek command line parameter.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <returns>System.String.</returns>
+        /// <value>The fast seek command line parameter.</value>
+        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;
+        }
+
+        /// <summary>
+        /// Gets the input argument.
+        /// </summary>
+        /// <param name="job">The job.</param>
+        /// <returns>System.String.</returns>
+        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);
+        }
+
+        /// <summary>
+        /// Gets the internal graphical subtitle param.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <param name="outputVideoCodec">The output video codec.</param>
+        /// <returns>System.String.</returns>
+        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);
+        }
+
+        /// <summary>
+        /// Gets the video bitrate to specify on the command line
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <param name="videoCodec">The video codec.</param>
+        /// <param name="isHls">if set to <c>true</c> [is HLS].</param>
+        /// <returns>System.String.</returns>
+        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;
+        }
+
+        /// <summary>
+        /// Gets the map args.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <returns>System.String.</returns>
+        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;
+        }
+
+        /// <summary>
+        /// Determines whether the specified stream is H264.
+        /// </summary>
+        /// <param name="stream">The stream.</param>
+        /// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns>
+        protected bool IsH264(MediaStream stream)
+        {
+            var codec = stream.Codec ?? string.Empty;
+
+            return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 ||
+                   codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
+        }
+
+        /// <summary>
+        /// If we're going to put a fixed size on the command line, this will calculate it
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <param name="outputVideoCodec">The output video codec.</param>
+        /// <param name="allowTimeStampCopy">if set to <c>true</c> [allow time stamp copy].</param>
+        /// <returns>System.String.</returns>
+        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<string>();
+
+            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;
+        }
+
+        /// <summary>
+        /// Gets the text subtitle param.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <returns>System.String.</returns>
+        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);
+        }
+    }
+}

+ 434 - 0
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<double> Progress { get; set; }
+        public TaskCompletionSource<bool> TaskCompletionSource;
+
+        public EncodingJobOptions Options { get; set; }
+        public string InputContainer { get; set; }
+        public List<MediaStream> 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<string> PlayableStreamFileNames { get; set; }
+
+        public List<string> SupportedAudioCodecs { get; set; }
+        public Dictionary<string, string> 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<string, string>(StringComparer.OrdinalIgnoreCase);
+            _logger = logger;
+            SupportedAudioCodecs = new List<string>();
+            PlayableStreamFileNames = new List<string>();
+            RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            AllMediaStreams = new List<MediaStream>();
+            TaskCompletionSource = new TaskCompletionSource<bool>();
+        }
+
+        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;
+            }
+        }
+
+        /// <summary>
+        /// Predicts the audio sample rate that will be in the output stream
+        /// </summary>
+        public int? TargetVideoBitDepth
+        {
+            get
+            {
+                var stream = VideoStream;
+                return stream == null || !Options.Static ? null : stream.BitDepth;
+            }
+        }
+
+        /// <summary>
+        /// Gets the target reference frames.
+        /// </summary>
+        /// <value>The target reference frames.</value>
+        public int? TargetRefFrames
+        {
+            get
+            {
+                var stream = VideoStream;
+                return stream == null || !Options.Static ? null : stream.RefFrames;
+            }
+        }
+
+        /// <summary>
+        /// Predicts the audio sample rate that will be in the output stream
+        /// </summary>
+        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;
+            }
+        }
+
+        /// <summary>
+        /// Predicts the audio sample rate that will be in the output stream
+        /// </summary>
+        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;
+            }
+        }
+
+        /// <summary>
+        /// Predicts the audio sample rate that will be in the output stream
+        /// </summary>
+        public int? TargetPacketLength
+        {
+            get
+            {
+                var stream = VideoStream;
+                return !Options.Static
+                    ? null
+                    : stream == null ? null : stream.PacketLength;
+            }
+        }
+
+        /// <summary>
+        /// Predicts the audio sample rate that will be in the output stream
+        /// </summary>
+        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)
+                //    });
+            }
+        }
+    }
+}

+ 830 - 0
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<EncodingJob> CreateJob(EncodingJobOptions options, bool isVideoRequest, IProgress<double> 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<MediaStream> 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<MediaStream>();
+
+                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<MediaStream> 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;
+        }
+
+        /// <summary>
+        /// Infers the video codec.
+        /// </summary>
+        /// <param name="container">The container.</param>
+        /// <returns>System.Nullable{VideoCodecs}.</returns>
+        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";
+        }
+
+        /// <summary>
+        /// Determines which stream will be used for playback
+        /// </summary>
+        /// <param name="allStream">All stream.</param>
+        /// <param name="desiredIndex">Index of the desired.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="returnFirstIfNoIndex">if set to <c>true</c> [return first if no index].</param>
+        /// <returns>MediaStream.</returns>
+        private static MediaStream GetMediaStream(IEnumerable<MediaStream> 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;
+        }
+
+        /// <summary>
+        /// Enforces the resolution limit.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <param name="videoRequest">The video request.</param>
+        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;
+        }
+
+        /// <summary>
+        /// Gets the number of audio channels to specify on the command line
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="audioStream">The audio stream.</param>
+        /// <param name="outputAudioCodec">The output audio codec.</param>
+        /// <returns>System.Nullable{System.Int32}.</returns>
+        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<MediaSourceInfo> 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;
+        }
+
+        /// <summary>
+        /// Determines whether the specified stream is H264.
+        /// </summary>
+        /// <param name="stream">The stream.</param>
+        /// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns>
+        protected bool IsH264(MediaStream stream)
+        {
+            var codec = stream.Codec ?? string.Empty;
+
+            return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 ||
+                   codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
+        }
+
+        /// <summary>
+        /// Gets the name of the output audio codec
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.String.</returns>
+        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();
+        }
+
+        /// <summary>
+        /// Gets the name of the output video codec
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.String.</returns>
+        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<string>
+            {
+                "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<string> 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;
+            }
+        }
+    }
+}

+ 122 - 0
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);
+            }
+        }
+    }
+}

+ 35 - 11
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<ISubtitleEncoder> 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<ISubtitleEncoder> 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<double> 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,
+                SubtitleEncoder())
+                .Start(options, progress, cancellationToken).ConfigureAwait(false);
+
+            await job.TaskCompletionSource.Task.ConfigureAwait(false);
+
+            return job.OutputFilePath;
+        }
+
+        public async Task<string> EncodeVideo(EncodingJobOptions options,
+            IProgress<double> progress,
+            CancellationToken cancellationToken)
+        {
+            var job = await new VideoEncoder(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);

+ 177 - 0
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();
+        }
+
+        /// <summary>
+        /// Gets video arguments to pass to ffmpeg
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <param name="codec">The video codec.</param>
+        /// <returns>System.String.</returns>
+        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;
+        }
+
+        /// <summary>
+        /// Gets audio arguments to pass to ffmpeg
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <returns>System.String.</returns>
+        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;
+        }
+    }
+}

+ 1 - 0
MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj

@@ -64,6 +64,7 @@
     <Compile Include="Encoder\EncodingUtils.cs" />
     <Compile Include="Encoder\JobLogger.cs" />
     <Compile Include="Encoder\MediaEncoder.cs" />
+    <Compile Include="Encoder\VideoEncoder.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Subtitles\ISubtitleParser.cs" />
     <Compile Include="Subtitles\ISubtitleWriter.cs" />

+ 3 - 3
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<double>(), cancellationToken);
+                jobItem.OutputPath = await MediaEncoder.EncodeVideo(new EncodingJobOptions(streamInfo, profile), new Progress<double>(), 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);
                 }

+ 5 - 2
MediaBrowser.Server.Startup.Common/ApplicationHost.cs

@@ -185,6 +185,7 @@ namespace MediaBrowser.Server.Startup.Common
         /// </summary>
         /// <value>The media encoder.</value>
         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<ISessionContext>(new SessionContext(UserManager, authContext, SessionManager));
             RegisterSingleInstance<IAuthService>(new AuthService(UserManager, authContext, ServerConfigurationManager, ConnectManager, SessionManager, DeviceManager));
 
-            RegisterSingleInstance<ISubtitleEncoder>(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);
         }