Procházet zdrojové kódy

restored live tv playback in the web client

Luke Pulverenti před 11 roky
rodič
revize
f756e39b9d
31 změnil soubory, kde provedl 1373 přidání a 192 odebrání
  1. 1 0
      MediaBrowser.Api/MediaBrowser.Api.csproj
  2. 23 52
      MediaBrowser.Api/Music/InstantMixService.cs
  3. 3 47
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  4. 73 0
      MediaBrowser.Api/Playback/ProgressiveStreamService.cs
  5. 0 5
      MediaBrowser.Api/Playback/StreamRequest.cs
  6. 0 2
      MediaBrowser.Api/Playback/StreamState.cs
  7. 7 0
      MediaBrowser.Controller/Dto/IDtoService.cs
  8. 38 0
      MediaBrowser.Controller/Library/IMusicManager.cs
  9. 3 1
      MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs
  10. 4 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  11. 79 0
      MediaBrowser.Controller/MediaEncoding/EncodingOptions.cs
  12. 13 0
      MediaBrowser.Controller/MediaEncoding/EncodingResult.cs
  13. 26 0
      MediaBrowser.Controller/MediaEncoding/VideoEncodingOptions.cs
  14. 91 0
      MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs
  15. 233 0
      MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
  16. 168 0
      MediaBrowser.MediaEncoding/Encoder/FFMpegProcess.cs
  17. 95 0
      MediaBrowser.MediaEncoding/Encoder/InternalEncodingTask.cs
  18. 323 0
      MediaBrowser.MediaEncoding/Encoder/InternalEncodingTaskFactory.cs
  19. 3 61
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  20. 5 0
      MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
  21. 3 0
      MediaBrowser.Model/LiveTv/ChannelInfoDto.cs
  22. 7 4
      MediaBrowser.Model/LiveTv/RecordingInfoDto.cs
  23. 44 4
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  24. 67 0
      MediaBrowser.Server.Implementations/Library/MusicManager.cs
  25. 1 2
      MediaBrowser.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  26. 5 6
      MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
  27. 1 0
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
  28. 53 6
      MediaBrowser.Server.Implementations/Session/SessionManager.cs
  29. 2 0
      MediaBrowser.ServerApplication/ApplicationHost.cs
  30. 1 1
      MediaBrowser.WebDashboard/ApiClient.js
  31. 1 1
      MediaBrowser.WebDashboard/packages.config

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

@@ -103,6 +103,7 @@
     <Compile Include="Playback\Hls\DynamicHlsService.cs" />
     <Compile Include="Playback\Hls\HlsSegmentService.cs" />
     <Compile Include="Playback\Hls\VideoHlsService.cs" />
+    <Compile Include="Playback\ProgressiveStreamService.cs" />
     <Compile Include="Playback\Progressive\AudioService.cs" />
     <Compile Include="Playback\Progressive\BaseProgressiveStreamingService.cs" />
     <Compile Include="Playback\BaseStreamingService.cs" />

+ 23 - 52
MediaBrowser.Api/Music/InstantMixService.cs

@@ -1,9 +1,9 @@
 using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Querying;
 using ServiceStack;
-using System;
 using System.Collections.Generic;
 using System.Linq;
 
@@ -36,103 +36,74 @@ namespace MediaBrowser.Api.Music
     public class InstantMixService : BaseApiService
     {
         private readonly IUserManager _userManager;
-        private readonly ILibraryManager _libraryManager;
 
         private readonly IDtoService _dtoService;
+        private readonly IMusicManager _musicManager;
 
-        public InstantMixService(IUserManager userManager, ILibraryManager libraryManager, IDtoService dtoService)
+        public InstantMixService(IUserManager userManager, IDtoService dtoService, IMusicManager musicManager)
         {
             _userManager = userManager;
-            _libraryManager = libraryManager;
             _dtoService = dtoService;
+            _musicManager = musicManager;
         }
 
         public object Get(GetInstantMixFromSong request)
         {
-            var item = _dtoService.GetItemByDtoId(request.Id);
+            var item = (Audio)_dtoService.GetItemByDtoId(request.Id);
 
-            var result = GetInstantMixResult(request, item.Genres);
+            var user = _userManager.GetUserById(request.UserId.Value);
 
-            return ToOptimizedSerializedResultUsingCache(result);
+            var items = _musicManager.GetInstantMixFromSong(item, user);
+
+            return GetResult(items, user, request);
         }
 
         public object Get(GetInstantMixFromAlbum request)
         {
             var album = (MusicAlbum)_dtoService.GetItemByDtoId(request.Id);
 
-            var genres = album
-               .RecursiveChildren
-               .OfType<Audio>()
-               .SelectMany(i => i.Genres)
-               .Concat(album.Genres)
-               .Distinct(StringComparer.OrdinalIgnoreCase);
+            var user = _userManager.GetUserById(request.UserId.Value);
 
-            var result = GetInstantMixResult(request, genres);
+            var items = _musicManager.GetInstantMixFromAlbum(album, user);
 
-            return ToOptimizedSerializedResultUsingCache(result);
+            return GetResult(items, user, request);
         }
 
         public object Get(GetInstantMixFromMusicGenre request)
         {
-            var genre = GetMusicGenre(request.Name, _libraryManager);
+            var user = _userManager.GetUserById(request.UserId.Value);
 
-            var result = GetInstantMixResult(request, new[] { genre.Name });
+            var items = _musicManager.GetInstantMixFromGenres(new[] { request.Name }, user);
 
-            return ToOptimizedSerializedResultUsingCache(result);
+            return GetResult(items, user, request);
         }
 
         public object Get(GetInstantMixFromArtist request)
         {
-            var artist = GetArtist(request.Name, _libraryManager);
-
-            var genres = _libraryManager.RootFolder
-                .RecursiveChildren
-                .OfType<Audio>()
-                .Where(i => i.HasArtist(artist.Name))
-                .SelectMany(i => i.Genres)
-                .Concat(artist.Genres)
-                .Distinct(StringComparer.OrdinalIgnoreCase);
+            var user = _userManager.GetUserById(request.UserId.Value);
 
-            var result = GetInstantMixResult(request, genres);
+            var items = _musicManager.GetInstantMixFromArtist(request.Name, user);
 
-            return ToOptimizedSerializedResultUsingCache(result);
+            return GetResult(items, user, request);
         }
 
-        private ItemsResult GetInstantMixResult(BaseGetSimilarItems request, IEnumerable<string> genres)
+        private object GetResult(IEnumerable<Audio> items, User user, BaseGetSimilarItems request)
         {
-            var user = request.UserId.HasValue ? _userManager.GetUserById(request.UserId.Value) : null;
-
             var fields = request.GetItemFields().ToList();
 
-            var inputItems = user == null
-                                 ? _libraryManager.RootFolder.RecursiveChildren
-                                 : user.RootFolder.GetRecursiveChildren(user);
-
-            var genresDictionary = genres.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
-
-            var limit = request.Limit.HasValue ? request.Limit.Value * 2 : 100;
-
-            var items = inputItems
-                .OfType<Audio>()
-                .Select(i => new Tuple<Audio, int>(i, i.Genres.Count(genresDictionary.ContainsKey)))
-                .OrderByDescending(i => i.Item2)
-                .ThenBy(i => Guid.NewGuid())
-                .Select(i => i.Item1)
-                .Take(limit)
-                .OrderBy(i => Guid.NewGuid())
-                .ToList();
+            var list = items.ToList();
 
             var result = new ItemsResult
             {
-                TotalRecordCount = items.Count
+                TotalRecordCount = list.Count
             };
 
-            var dtos = items.Take(request.Limit ?? items.Count)
+            var dtos = list.Take(request.Limit ?? list.Count)
                 .Select(i => _dtoService.GetBaseItemDto(i, fields, user));
 
             result.Items = dtos.ToArray();
 
-            return result;
+            return ToOptimizedResult(result);
         }
 
     }

+ 3 - 47
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -835,11 +835,6 @@ namespace MediaBrowser.Api.Playback
         /// <returns>System.String.</returns>
         protected string GetInputArgument(StreamState state)
         {
-            if (state.SendInputOverStandardInput)
-            {
-                return "-";
-            }
-
             var type = InputType.File;
 
             var inputPath = new[] { state.MediaPath };
@@ -898,9 +893,7 @@ namespace MediaBrowser.Api.Playback
                     Arguments = commandLineArgs,
 
                     WindowStyle = ProcessWindowStyle.Hidden,
-                    ErrorDialog = false,
-
-                    RedirectStandardInput = state.SendInputOverStandardInput
+                    ErrorDialog = false
                 },
 
                 EnableRaisingEvents = true
@@ -933,11 +926,6 @@ namespace MediaBrowser.Api.Playback
                 throw;
             }
 
-            if (state.SendInputOverStandardInput)
-            {
-                StreamToStandardInput(process, state);
-            }
-
             // MUST read both stdout and stderr asynchronously or a deadlock may occurr
             process.BeginOutputReadLine();
 
@@ -965,32 +953,6 @@ namespace MediaBrowser.Api.Playback
             }
         }
 
-        private async void StreamToStandardInput(Process process, StreamState state)
-        {
-            try
-            {
-                await StreamToStandardInputInternal(process, state).ConfigureAwait(false);
-            }
-            catch (OperationCanceledException)
-            {
-                Logger.Debug("Stream to standard input closed normally.");
-            }
-            catch (Exception ex)
-            {
-                Logger.ErrorException("Error writing to standard input", ex);
-            }
-        }
-
-        private async Task StreamToStandardInputInternal(Process process, StreamState state)
-        {
-            state.StandardInputCancellationTokenSource = new CancellationTokenSource();
-
-            using (var fileStream = FileSystem.GetFileStream(state.MediaPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
-            {
-                await new EndlessStreamCopy().CopyStream(fileStream, process.StandardInput.BaseStream, state.StandardInputCancellationTokenSource.Token).ConfigureAwait(false);
-            }
-        }
-
         protected int? GetVideoBitrateParamValue(StreamState state)
         {
             var bitrate = state.VideoRequest.VideoBitRate;
@@ -1315,11 +1277,6 @@ namespace MediaBrowser.Api.Playback
                 ParseParams(request);
             }
 
-            if (request.ThrowDebugError)
-            {
-                throw new InvalidOperationException("You asked for a debug error, you got one.");
-            }
-
             var user = AuthorizationRequestFilterAttribute.GetCurrentUser(Request, UserManager);
 
             var url = Request.PathInfo;
@@ -1369,8 +1326,6 @@ namespace MediaBrowser.Api.Playback
                 {
                     state.MediaPath = path;
                     state.IsRemote = false;
-
-                    state.SendInputOverStandardInput = recording.RecordingInfo.Status == RecordingStatus.InProgress;
                 }
                 else if (!string.IsNullOrEmpty(mediaUrl))
                 {
@@ -1378,7 +1333,8 @@ namespace MediaBrowser.Api.Playback
                     state.IsRemote = true;
                 }
 
-                //state.RunTimeTicks = recording.RunTimeTicks;
+                state.RunTimeTicks = recording.RunTimeTicks;
+
                 if (recording.RecordingInfo.Status == RecordingStatus.InProgress && !state.IsRemote)
                 {
                     await Task.Delay(1000, cancellationToken).ConfigureAwait(false);

+ 73 - 0
MediaBrowser.Api/Playback/ProgressiveStreamService.cs

@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Api.Playback.Progressive;
+
+namespace MediaBrowser.Api.Playback
+{
+    //public class GetProgressiveAudioStream : StreamRequest
+    //{
+
+    //}
+    
+    //public class ProgressiveStreamService : BaseApiService
+    //{
+    //    public object Get(GetProgressiveAudioStream request)
+    //    {
+    //        return ProcessRequest(request, false);
+    //    }
+
+    //    /// <summary>
+    //    /// Gets the specified request.
+    //    /// </summary>
+    //    /// <param name="request">The request.</param>
+    //    /// <returns>System.Object.</returns>
+    //    public object Head(GetProgressiveAudioStream request)
+    //    {
+    //        return ProcessRequest(request, true);
+    //    }
+
+    //    protected object ProcessRequest(StreamRequest request, bool isHeadRequest)
+    //    {
+    //        var state = GetState(request, CancellationToken.None).Result;
+
+    //        var responseHeaders = new Dictionary<string, string>();
+
+    //        if (request.Static && state.IsRemote)
+    //        {
+    //            AddDlnaHeaders(state, responseHeaders, true);
+
+    //            return GetStaticRemoteStreamResult(state.MediaPath, responseHeaders, isHeadRequest).Result;
+    //        }
+
+    //        var outputPath = GetOutputFilePath(state);
+    //        var outputPathExists = File.Exists(outputPath);
+
+    //        var isStatic = request.Static ||
+    //                       (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive));
+
+    //        AddDlnaHeaders(state, responseHeaders, isStatic);
+
+    //        if (request.Static)
+    //        {
+    //            var contentType = state.GetMimeType(state.MediaPath);
+
+    //            return ResultFactory.GetStaticFileResult(Request, state.MediaPath, contentType, FileShare.Read, responseHeaders, isHeadRequest);
+    //        }
+
+    //        if (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive))
+    //        {
+    //            var contentType = state.GetMimeType(outputPath);
+
+    //            return ResultFactory.GetStaticFileResult(Request, outputPath, contentType, FileShare.Read, responseHeaders, isHeadRequest);
+    //        }
+
+    //        return GetStreamResult(state, responseHeaders, isHeadRequest).Result;
+    //    }
+
+    //}
+}

+ 0 - 5
MediaBrowser.Api/Playback/StreamRequest.cs

@@ -67,11 +67,6 @@ namespace MediaBrowser.Api.Playback
 
         [ApiMember(Name = "DeviceProfileId", Description = "Optional. The dlna device profile id to utilize.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string DeviceProfileId { get; set; }
-        
-        /// <summary>
-        /// For testing purposes
-        /// </summary>
-        public bool ThrowDebugError { get; set; }
 
         public string Params { get; set; }
     }

+ 0 - 2
MediaBrowser.Api/Playback/StreamState.cs

@@ -51,8 +51,6 @@ namespace MediaBrowser.Api.Playback
 
         public bool HasMediaStreams { get; set; }
 
-        public bool SendInputOverStandardInput { get; set; }
-
         public CancellationTokenSource StandardInputCancellationTokenSource { get; set; }
 
         public string LiveTvStreamId { get; set; }

+ 7 - 0
MediaBrowser.Controller/Dto/IDtoService.cs

@@ -85,6 +85,13 @@ namespace MediaBrowser.Controller.Dto
         /// <returns>ChapterInfoDto.</returns>
         ChapterInfoDto GetChapterInfoDto(ChapterInfo chapterInfo, BaseItem item);
 
+        /// <summary>
+        /// Gets the media sources.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>List{MediaSourceInfo}.</returns>
+        List<MediaSourceInfo> GetMediaSources(BaseItem item);
+
         /// <summary>
         /// Gets the item by name dto.
         /// </summary>

+ 38 - 0
MediaBrowser.Controller/Library/IMusicManager.cs

@@ -0,0 +1,38 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Library
+{
+    public interface IMusicManager
+    {
+        /// <summary>
+        /// Gets the instant mix from song.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable{Audio}.</returns>
+        IEnumerable<Audio> GetInstantMixFromSong(Audio item, User user);
+        /// <summary>
+        /// Gets the instant mix from artist.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable{Audio}.</returns>
+        IEnumerable<Audio> GetInstantMixFromArtist(string name, User user);
+        /// <summary>
+        /// Gets the instant mix from album.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable{Audio}.</returns>
+        IEnumerable<Audio> GetInstantMixFromAlbum(MusicAlbum item, User user);
+        /// <summary>
+        /// Gets the instant mix from genre.
+        /// </summary>
+        /// <param name="genres">The genres.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable{Audio}.</returns>
+        IEnumerable<Audio> GetInstantMixFromGenres(IEnumerable<string> genres, User user);
+    }
+}

+ 3 - 1
MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs

@@ -1,8 +1,8 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Library;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Model.Library;
 
 namespace MediaBrowser.Controller.LiveTv
 {
@@ -14,6 +14,8 @@ namespace MediaBrowser.Controller.LiveTv
 
         RecordingInfo RecordingInfo { get; set; }
 
+        long? RunTimeTicks { get; set; }
+
         string GetClientTypeName();
 
         string GetUserDataKey();

+ 4 - 0
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -134,6 +134,7 @@
     <Compile Include="Library\DeleteOptions.cs" />
     <Compile Include="Library\ILibraryPostScanTask.cs" />
     <Compile Include="Library\IMetadataSaver.cs" />
+    <Compile Include="Library\IMusicManager.cs" />
     <Compile Include="Library\ItemUpdateType.cs" />
     <Compile Include="Library\IUserDataManager.cs" />
     <Compile Include="Library\UserDataSaveEventArgs.cs" />
@@ -155,11 +156,14 @@
     <Compile Include="LiveTv\SeriesTimerInfo.cs" />
     <Compile Include="LiveTv\TimerInfo.cs" />
     <Compile Include="Localization\ILocalizationManager.cs" />
+    <Compile Include="MediaEncoding\EncodingOptions.cs" />
     <Compile Include="MediaEncoding\ChapterImageRefreshOptions.cs" />
+    <Compile Include="MediaEncoding\EncodingResult.cs" />
     <Compile Include="MediaEncoding\IEncodingManager.cs" />
     <Compile Include="MediaEncoding\ImageEncodingOptions.cs" />
     <Compile Include="MediaEncoding\IMediaEncoder.cs" />
     <Compile Include="MediaEncoding\InternalMediaInfoResult.cs" />
+    <Compile Include="MediaEncoding\VideoEncodingOptions.cs" />
     <Compile Include="Net\IHasResultFactory.cs" />
     <Compile Include="Net\IHttpResultFactory.cs" />
     <Compile Include="Net\IHttpServer.cs" />

+ 79 - 0
MediaBrowser.Controller/MediaEncoding/EncodingOptions.cs

@@ -0,0 +1,79 @@
+using MediaBrowser.Controller.Dlna;
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+    public class EncodingOptions
+    {
+        /// <summary>
+        /// Gets or sets the item identifier.
+        /// </summary>
+        /// <value>The item identifier.</value>
+        public string ItemId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the media source identifier.
+        /// </summary>
+        /// <value>The media source identifier.</value>
+        public string MediaSourceId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the device profile.
+        /// </summary>
+        /// <value>The device profile.</value>
+        public DeviceProfile DeviceProfile { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the output path.
+        /// </summary>
+        /// <value>The output path.</value>
+        public string OutputPath { get; set; }
+
+        /// <summary>
+        /// Gets or sets the container.
+        /// </summary>
+        /// <value>The container.</value>
+        public string Container { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the audio codec.
+        /// </summary>
+        /// <value>The audio codec.</value>
+        public string AudioCodec { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the start time ticks.
+        /// </summary>
+        /// <value>The start time ticks.</value>
+        public long? StartTimeTicks { get; set; }
+
+        /// <summary>
+        /// Gets or sets the maximum channels.
+        /// </summary>
+        /// <value>The maximum channels.</value>
+        public int? MaxAudioChannels { get; set; }
+
+        /// <summary>
+        /// Gets or sets the channels.
+        /// </summary>
+        /// <value>The channels.</value>
+        public int? AudioChannels { get; set; }
+
+        /// <summary>
+        /// Gets or sets the sample rate.
+        /// </summary>
+        /// <value>The sample rate.</value>
+        public int? AudioSampleRate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the bit rate.
+        /// </summary>
+        /// <value>The bit rate.</value>
+        public int? AudioBitRate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the maximum audio bit rate.
+        /// </summary>
+        /// <value>The maximum audio bit rate.</value>
+        public int? MaxAudioBitRate { get; set; }
+    }
+}

+ 13 - 0
MediaBrowser.Controller/MediaEncoding/EncodingResult.cs

@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+    public class EncodingResult
+    {
+        public string OutputPath { get; set; }
+    }
+}

+ 26 - 0
MediaBrowser.Controller/MediaEncoding/VideoEncodingOptions.cs

@@ -0,0 +1,26 @@
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+    public class VideoEncodingOptions : EncodingOptions
+    {
+        public string VideoCodec { get; set; }
+
+        public string VideoProfile { get; set; }
+
+        public double? VideoLevel { get; set; }
+        
+        public int? VideoStreamIndex { get; set; }
+
+        public int? AudioStreamIndex { get; set; }
+
+        public int? SubtitleStreamIndex { get; set; }
+
+        public int? MaxWidth { get; set; }
+
+        public int? MaxHeight { get; set; }
+
+        public int? Height { get; set; }
+
+        public int? Width { get; set; }
+    }
+}

+ 91 - 0
MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs

@@ -0,0 +1,91 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.MediaEncoding.Encoder
+{
+    public class AudioEncoder
+    {
+        private readonly string _ffmpegPath;
+        private readonly ILogger _logger;
+        private readonly IFileSystem _fileSystem;
+        private readonly IApplicationPaths _appPaths;
+        private readonly IIsoManager _isoManager;
+        private readonly ILiveTvManager _liveTvManager;
+
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+        public AudioEncoder(string ffmpegPath, ILogger logger, IFileSystem fileSystem, IApplicationPaths appPaths, IIsoManager isoManager, ILiveTvManager liveTvManager)
+        {
+            _ffmpegPath = ffmpegPath;
+            _logger = logger;
+            _fileSystem = fileSystem;
+            _appPaths = appPaths;
+            _isoManager = isoManager;
+            _liveTvManager = liveTvManager;
+        }
+
+        public Task BeginEncoding(InternalEncodingTask task)
+        {
+            return new FFMpegProcess(_ffmpegPath, _logger, _fileSystem, _appPaths, _isoManager, _liveTvManager).Start(task, GetArguments);
+        }
+
+        private string GetArguments(InternalEncodingTask task, string mountedPath)
+        {
+            var options = task.Request;
+
+            return string.Format("{0} -i {1} {2} -id3v2_version 3 -write_id3v1 1 \"{3}\"",
+                GetInputModifier(task),
+                GetInputArgument(task),
+                GetOutputModifier(task),
+                options.OutputPath).Trim();
+        }
+
+        private string GetInputModifier(InternalEncodingTask task)
+        {
+            return EncodingUtils.GetInputModifier(task);
+        }
+
+        private string GetInputArgument(InternalEncodingTask task)
+        {
+            return EncodingUtils.GetInputArgument(new List<string> { task.MediaPath }, task.IsInputRemote);
+        }
+
+        private string GetOutputModifier(InternalEncodingTask task)
+        {
+            var options = task.Request;
+
+            var audioTranscodeParams = new List<string>
+            {
+                "-threads " + EncodingUtils.GetNumberOfThreads(task, false).ToString(_usCulture),
+                "-vn"
+            };
+
+            var bitrate = EncodingUtils.GetAudioBitrateParam(task);
+
+            if (bitrate.HasValue)
+            {
+                audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(_usCulture));
+            }
+
+            var channels = EncodingUtils.GetNumAudioChannelsParam(options, task.AudioStream);
+
+            if (channels.HasValue)
+            {
+                audioTranscodeParams.Add("-ac " + channels.Value);
+            }
+
+            if (options.AudioSampleRate.HasValue)
+            {
+                audioTranscodeParams.Add("-ar " + options.AudioSampleRate.Value);
+            }
+
+            return string.Join(" ", audioTranscodeParams.ToArray());
+        }
+    }
+}

+ 233 - 0
MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs

@@ -0,0 +1,233 @@
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+
+namespace MediaBrowser.MediaEncoding.Encoder
+{
+    public static class EncodingUtils
+    {
+        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+        
+        public static string GetInputArgument(List<string> inputFiles, bool isRemote)
+        {
+            if (isRemote)
+            {
+                return GetHttpInputArgument(inputFiles);
+            }
+
+            return GetConcatInputArgument(inputFiles);
+        }
+
+        /// <summary>
+        /// Gets the concat input argument.
+        /// </summary>
+        /// <param name="inputFiles">The input files.</param>
+        /// <returns>System.String.</returns>
+        private static string GetConcatInputArgument(List<string> inputFiles)
+        {
+            // Get all streams
+            // If there's more than one we'll need to use the concat command
+            if (inputFiles.Count > 1)
+            {
+                var files = string.Join("|", inputFiles);
+
+                return string.Format("concat:\"{0}\"", files);
+            }
+
+            // Determine the input path for video files
+            return GetFileInputArgument(inputFiles[0]);
+        }
+
+        /// <summary>
+        /// Gets the file input argument.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <returns>System.String.</returns>
+        private static string GetFileInputArgument(string path)
+        {
+            return string.Format("file:\"{0}\"", path);
+        }
+
+        /// <summary>
+        /// Gets the HTTP input argument.
+        /// </summary>
+        /// <param name="inputFiles">The input files.</param>
+        /// <returns>System.String.</returns>
+        private static string GetHttpInputArgument(IEnumerable<string> inputFiles)
+        {
+            var url = inputFiles.First();
+
+            return string.Format("\"{0}\"", url);
+        }
+
+        public static string GetAudioInputModifier(InternalEncodingTask options)
+        {
+            return GetCommonInputModifier(options);
+        }
+
+        public static string GetInputModifier(InternalEncodingTask options)
+        {
+            var inputModifier = GetCommonInputModifier(options);
+
+            //if (state.VideoRequest != null)
+            //{
+            //    inputModifier += " -fflags genpts";
+            //}
+
+            //if (!string.IsNullOrEmpty(state.InputVideoCodec))
+            //{
+            //    inputModifier += " -vcodec " + state.InputVideoCodec;
+            //}
+
+            //if (!string.IsNullOrEmpty(state.InputVideoSync))
+            //{
+            //    inputModifier += " -vsync " + state.InputVideoSync;
+            //}
+
+            return inputModifier;
+        }
+
+        private static string GetCommonInputModifier(InternalEncodingTask options)
+        {
+            var inputModifier = string.Empty;
+
+            if (options.EnableDebugLogging)
+            {
+                inputModifier += "-loglevel debug";
+            }
+
+            var probeSize = GetProbeSizeArgument(options.InputVideoType.HasValue && options.InputVideoType.Value == VideoType.Dvd);
+            inputModifier += " " + probeSize;
+            inputModifier = inputModifier.Trim();
+
+            if (!string.IsNullOrWhiteSpace(options.UserAgent))
+            {
+                inputModifier += " -user-agent \"" + options.UserAgent + "\"";
+            }
+
+            inputModifier += " " + GetFastSeekValue(options.Request);
+            inputModifier = inputModifier.Trim();
+
+            if (!string.IsNullOrEmpty(options.InputFormat))
+            {
+                inputModifier += " -f " + options.InputFormat;
+            }
+
+            if (!string.IsNullOrEmpty(options.InputAudioCodec))
+            {
+                inputModifier += " -acodec " + options.InputAudioCodec;
+            }
+
+            if (!string.IsNullOrEmpty(options.InputAudioSync))
+            {
+                inputModifier += " -async " + options.InputAudioSync;
+            }
+
+            if (options.ReadInputAtNativeFramerate)
+            {
+                inputModifier += " -re";
+            }
+
+            return inputModifier;
+        }
+
+        private static string GetFastSeekValue(EncodingOptions options)
+        {
+            var time = options.StartTimeTicks;
+
+            if (time.HasValue)
+            {
+                var seconds = TimeSpan.FromTicks(time.Value).TotalSeconds;
+
+                if (seconds > 0)
+                {
+                    return string.Format("-ss {0}", seconds.ToString(UsCulture));
+                }
+            }
+
+            return string.Empty;
+        }
+
+        public static string GetProbeSizeArgument(bool isDvd)
+        {
+            return isDvd ? "-probesize 1G -analyzeduration 200M" : string.Empty;
+        }
+
+        public static int? GetAudioBitrateParam(InternalEncodingTask task)
+        {
+            if (task.Request.AudioBitRate.HasValue)
+            {
+                // Make sure we don't request a bitrate higher than the source
+                var currentBitrate = task.AudioStream == null ? task.Request.AudioBitRate.Value : task.AudioStream.BitRate ?? task.Request.AudioBitRate.Value;
+
+                return Math.Min(currentBitrate, task.Request.AudioBitRate.Value);
+            }
+
+            return 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>
+        /// <returns>System.Nullable{System.Int32}.</returns>
+        public static int? GetNumAudioChannelsParam(EncodingOptions request, MediaStream audioStream)
+        {
+            if (audioStream != null)
+            {
+                if (audioStream.Channels > 2 && string.Equals(request.AudioCodec, "wma", StringComparison.OrdinalIgnoreCase))
+                {
+                    // 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);
+                }
+
+                return request.MaxAudioChannels.Value;
+            }
+
+            return request.AudioChannels;
+        }
+
+        public static int GetNumberOfThreads(InternalEncodingTask state, bool isWebm)
+        {
+            // Use more when this is true. -re will keep cpu usage under control
+            if (state.ReadInputAtNativeFramerate)
+            {
+                if (isWebm)
+                {
+                    return Math.Max(Environment.ProcessorCount - 1, 1);
+                }
+
+                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 (state.QualitySetting)
+            {
+                case EncodingQuality.HighSpeed:
+                    return 2;
+                case EncodingQuality.HighQuality:
+                    return 2;
+                case EncodingQuality.MaxQuality:
+                    return isWebm ? 2 : 0;
+                default:
+                    throw new Exception("Unrecognized MediaEncodingQuality value.");
+            }
+        }
+    }
+}

+ 168 - 0
MediaBrowser.MediaEncoding/Encoder/FFMpegProcess.cs

@@ -0,0 +1,168 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.MediaEncoding.Encoder
+{
+    public class FFMpegProcess : IDisposable
+    {
+        private readonly string _ffmpegPath;
+        private readonly ILogger _logger;
+        private readonly IFileSystem _fileSystem;
+        private readonly IApplicationPaths _appPaths;
+        private readonly IIsoManager _isoManager;
+        private readonly ILiveTvManager _liveTvManager;
+
+        private Stream _logFileStream;
+        private InternalEncodingTask _task;
+        private IIsoMount _isoMount;
+
+        public FFMpegProcess(string ffmpegPath, ILogger logger, IFileSystem fileSystem, IApplicationPaths appPaths, IIsoManager isoManager, ILiveTvManager liveTvManager)
+        {
+            _ffmpegPath = ffmpegPath;
+            _logger = logger;
+            _fileSystem = fileSystem;
+            _appPaths = appPaths;
+            _isoManager = isoManager;
+            _liveTvManager = liveTvManager;
+        }
+
+        public async Task Start(InternalEncodingTask task, Func<InternalEncodingTask,string,string> argumentsFactory)
+        {
+            _task = task;
+            if (!File.Exists(_ffmpegPath))
+            {
+                throw new InvalidOperationException("ffmpeg was not found at " + _ffmpegPath);
+            }
+
+            Directory.CreateDirectory(Path.GetDirectoryName(task.Request.OutputPath));
+
+            string mountedPath = null;
+            if (task.InputVideoType.HasValue && task.InputVideoType == VideoType.Iso && task.IsoType.HasValue)
+            {
+                if (_isoManager.CanMount(task.MediaPath))
+                {
+                    _isoMount = await _isoManager.Mount(task.MediaPath, CancellationToken.None).ConfigureAwait(false);
+                    mountedPath = _isoMount.MountedPath;
+                }
+            }
+            
+            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,
+
+                    FileName = _ffmpegPath,
+                    WorkingDirectory = Path.GetDirectoryName(_ffmpegPath),
+                    Arguments = argumentsFactory(task, mountedPath),
+
+                    WindowStyle = ProcessWindowStyle.Hidden,
+                    ErrorDialog = false
+                },
+
+                EnableRaisingEvents = true
+            };
+
+            _logger.Info(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
+
+            var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-" + task.Id + ".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.
+            _logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true);
+
+            process.Exited += process_Exited;
+
+            try
+            {
+                process.Start();
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error starting ffmpeg", ex);
+
+                task.OnError();
+
+                DisposeLogFileStream();
+
+                process.Dispose();
+
+                throw;
+            }
+
+            task.OnBegin();
+
+            // MUST read both stdout and stderr asynchronously or a deadlock may occurr
+            process.BeginOutputReadLine();
+
+#pragma warning disable 4014
+            // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
+            process.StandardError.BaseStream.CopyToAsync(_logFileStream);
+#pragma warning restore 4014
+        }
+
+        async void process_Exited(object sender, EventArgs e)
+        {
+            var process = (Process)sender;
+
+            if (_isoMount != null)
+            {
+                _isoMount.Dispose();
+                _isoMount = null;
+            }
+
+            DisposeLogFileStream();
+
+            try
+            {
+                _logger.Info("FFMpeg exited with code {0} for {1}", process.ExitCode, _task.Request.OutputPath);
+            }
+            catch
+            {
+                _logger.Info("FFMpeg exited with an error for {0}", _task.Request.OutputPath);
+            }
+
+            _task.OnCompleted();
+            
+            if (!string.IsNullOrEmpty(_task.LiveTvStreamId))
+            {
+                try
+                {
+                    await _liveTvManager.CloseLiveStream(_task.LiveTvStreamId, CancellationToken.None).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error closing live tv stream", ex);
+                }
+            }
+        }
+
+        public void Dispose()
+        {
+            DisposeLogFileStream();
+        }
+
+        private void DisposeLogFileStream()
+        {
+            if (_logFileStream != null)
+            {
+                _logFileStream.Dispose();
+                _logFileStream = null;
+            }
+        }
+    }
+}

+ 95 - 0
MediaBrowser.MediaEncoding/Encoder/InternalEncodingTask.cs

@@ -0,0 +1,95 @@
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace MediaBrowser.MediaEncoding.Encoder
+{
+    public class InternalEncodingTask
+    {
+        public string Id { get; set; }
+
+        public CancellationTokenSource CancellationTokenSource { get; set; }
+
+        public double ProgressPercentage { get; set; }
+
+        public EncodingOptions Request { get; set; }
+
+        public VideoEncodingOptions VideoRequest
+        {
+            get { return Request as VideoEncodingOptions; }
+        }
+
+        public string MediaPath { get; set; }
+        public List<string> StreamFileNames { get; set; }
+        public bool IsInputRemote { get; set; }
+
+        public VideoType? InputVideoType { get; set; }
+        public IsoType? IsoType { get; set; }
+        public long? InputRunTimeTicks;
+
+        public string AudioSync = "1";
+        public string VideoSync = "vfr";
+
+        public string InputAudioSync { get; set; }
+        public string InputVideoSync { get; set; }
+
+        public bool DeInterlace { get; set; }
+
+        public bool ReadInputAtNativeFramerate { get; set; }
+
+        public string InputFormat { get; set; }
+
+        public string InputVideoCodec { get; set; }
+
+        public string InputAudioCodec { get; set; }
+
+        public string LiveTvStreamId { get; set; }
+
+        public MediaStream AudioStream { get; set; }
+        public MediaStream VideoStream { get; set; }
+        public MediaStream SubtitleStream { get; set; }
+        public bool HasMediaStreams { get; set; }
+
+        public int SegmentLength = 10;
+        public int HlsListSize;
+
+        public string MimeType { get; set; }
+        public string OrgPn { get; set; }
+        public bool EnableMpegtsM2TsMode { get; set; }
+
+        /// <summary>
+        /// Gets or sets the user agent.
+        /// </summary>
+        /// <value>The user agent.</value>
+        public string UserAgent { get; set; }
+
+        public EncodingQuality QualitySetting { get; set; }
+
+        public InternalEncodingTask()
+        {
+            Id = Guid.NewGuid().ToString("N");
+            CancellationTokenSource = new CancellationTokenSource();
+            StreamFileNames = new List<string>();
+        }
+
+        public bool EnableDebugLogging { get; set; }
+
+        internal void OnBegin()
+        {
+            
+        }
+
+        internal void OnCompleted()
+        {
+            
+        }
+
+        internal void OnError()
+        {
+            
+        }
+    }
+}

+ 323 - 0
MediaBrowser.MediaEncoding/Encoder/InternalEncodingTaskFactory.cs

@@ -0,0 +1,323 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.LiveTv;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.MediaEncoding.Encoder
+{
+    public class InternalEncodingTaskFactory
+    {
+        private readonly ILibraryManager _libraryManager;
+        private readonly ILiveTvManager _liveTvManager;
+        private readonly IItemRepository _itemRepo;
+        private readonly IServerConfigurationManager _config;
+
+        public InternalEncodingTaskFactory(ILibraryManager libraryManager, ILiveTvManager liveTvManager, IItemRepository itemRepo, IServerConfigurationManager config)
+        {
+            _libraryManager = libraryManager;
+            _liveTvManager = liveTvManager;
+            _itemRepo = itemRepo;
+            _config = config;
+        }
+
+        public async Task<InternalEncodingTask> Create(EncodingOptions request, CancellationToken cancellationToken)
+        {
+            ValidateInput(request);
+
+            var state = new InternalEncodingTask
+            {
+                Request = request
+            };
+
+            var item = string.IsNullOrEmpty(request.MediaSourceId) ?
+                _libraryManager.GetItemById(new Guid(request.ItemId)) :
+                _libraryManager.GetItemById(new Guid(request.MediaSourceId));
+
+            if (item is ILiveTvRecording)
+            {
+                var recording = await _liveTvManager.GetInternalRecording(request.ItemId, cancellationToken).ConfigureAwait(false);
+
+                if (string.Equals(recording.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+                {
+                    state.InputVideoType = VideoType.VideoFile;
+                }
+
+                var path = recording.RecordingInfo.Path;
+                var mediaUrl = recording.RecordingInfo.Url;
+
+                if (string.IsNullOrWhiteSpace(path) && string.IsNullOrWhiteSpace(mediaUrl))
+                {
+                    var streamInfo = await _liveTvManager.GetRecordingStream(request.ItemId, cancellationToken).ConfigureAwait(false);
+
+                    state.LiveTvStreamId = streamInfo.Id;
+
+                    path = streamInfo.Path;
+                    mediaUrl = streamInfo.Url;
+                }
+
+                if (!string.IsNullOrEmpty(path) && File.Exists(path))
+                {
+                    state.MediaPath = path;
+                    state.IsInputRemote = false;
+                }
+                else if (!string.IsNullOrEmpty(mediaUrl))
+                {
+                    state.MediaPath = mediaUrl;
+                    state.IsInputRemote = true;
+                }
+
+                state.InputRunTimeTicks = recording.RunTimeTicks;
+                if (recording.RecordingInfo.Status == RecordingStatus.InProgress && !state.IsInputRemote)
+                {
+                    await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
+                }
+
+                state.ReadInputAtNativeFramerate = recording.RecordingInfo.Status == RecordingStatus.InProgress;
+                state.AudioSync = "1000";
+                state.DeInterlace = true;
+                state.InputVideoSync = "-1";
+                state.InputAudioSync = "1";
+            }
+            else if (item is LiveTvChannel)
+            {
+                var channel = _liveTvManager.GetInternalChannel(request.ItemId);
+
+                if (string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+                {
+                    state.InputVideoType = VideoType.VideoFile;
+                }
+
+                var streamInfo = await _liveTvManager.GetChannelStream(request.ItemId, cancellationToken).ConfigureAwait(false);
+
+                state.LiveTvStreamId = streamInfo.Id;
+
+                if (!string.IsNullOrEmpty(streamInfo.Path) && File.Exists(streamInfo.Path))
+                {
+                    state.MediaPath = streamInfo.Path;
+                    state.IsInputRemote = false;
+
+                    await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
+                }
+                else if (!string.IsNullOrEmpty(streamInfo.Url))
+                {
+                    state.MediaPath = streamInfo.Url;
+                    state.IsInputRemote = true;
+                }
+
+                state.ReadInputAtNativeFramerate = true;
+                state.AudioSync = "1000";
+                state.DeInterlace = true;
+                state.InputVideoSync = "-1";
+                state.InputAudioSync = "1";
+            }
+            else
+            {
+                state.MediaPath = item.Path;
+                state.IsInputRemote = item.LocationType == LocationType.Remote;
+
+                var video = item as Video;
+
+                if (video != null)
+                {
+                    state.InputVideoType = video.VideoType;
+                    state.IsoType = video.IsoType;
+
+                    state.StreamFileNames = video.PlayableStreamFileNames.ToList();
+                }
+
+                state.InputRunTimeTicks = item.RunTimeTicks;
+            }
+
+            var videoRequest = request as VideoEncodingOptions;
+
+            var mediaStreams = _itemRepo.GetMediaStreams(new MediaStreamQuery
+            {
+                ItemId = item.Id
+
+            }).ToList();
+
+            if (videoRequest != null)
+            {
+                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.VideoStream != null && state.VideoStream.IsInterlaced)
+                {
+                    state.DeInterlace = true;
+                }
+            }
+            else
+            {
+                state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
+            }
+
+            state.HasMediaStreams = mediaStreams.Count > 0;
+
+            state.SegmentLength = state.ReadInputAtNativeFramerate ? 5 : 10;
+            state.HlsListSize = state.ReadInputAtNativeFramerate ? 100 : 1440;
+
+            state.QualitySetting = GetQualitySetting();
+
+            ApplyDeviceProfileSettings(state);
+
+            return state;
+        }
+
+        private void ValidateInput(EncodingOptions request)
+        {
+            if (string.IsNullOrWhiteSpace(request.ItemId))
+            {
+                throw new ArgumentException("ItemId is required.");
+            }
+            if (string.IsNullOrWhiteSpace(request.OutputPath))
+            {
+                throw new ArgumentException("OutputPath is required.");
+            }
+            if (string.IsNullOrWhiteSpace(request.Container))
+            {
+                throw new ArgumentException("Container is required.");
+            }
+            if (string.IsNullOrWhiteSpace(request.AudioCodec))
+            {
+                throw new ArgumentException("AudioCodec is required.");
+            }
+
+            var videoRequest = request as VideoEncodingOptions;
+
+            if (videoRequest == null)
+            {
+                return;
+            }
+        }
+
+        /// <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 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 (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;
+        }
+
+        private void ApplyDeviceProfileSettings(InternalEncodingTask state)
+        {
+            var profile = state.Request.DeviceProfile;
+
+            if (profile == null)
+            {
+                // Don't use settings from the default profile. 
+                // Only use a specific profile if it was requested.
+                return;
+            }
+
+            var container = state.Request.Container;
+
+            var audioCodec = state.Request.AudioCodec;
+
+            if (string.Equals(audioCodec, "copy", StringComparison.OrdinalIgnoreCase) && state.AudioStream != null)
+            {
+                audioCodec = state.AudioStream.Codec;
+            }
+
+            var videoCodec = state.VideoRequest == null ? null : state.VideoRequest.VideoCodec;
+
+            if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase) && state.VideoStream != null)
+            {
+                videoCodec = state.VideoStream.Codec;
+            }
+
+            var mediaProfile = state.VideoRequest == null ?
+                profile.GetAudioMediaProfile(container, audioCodec, state.AudioStream) :
+                profile.GetVideoMediaProfile(container, audioCodec, videoCodec, state.AudioStream, state.VideoStream);
+
+            if (mediaProfile != null)
+            {
+                state.MimeType = mediaProfile.MimeType;
+                state.OrgPn = mediaProfile.OrgPn;
+            }
+
+            var transcodingProfile = state.VideoRequest == null ?
+                profile.GetAudioTranscodingProfile(container, audioCodec) :
+                profile.GetVideoTranscodingProfile(container, audioCodec, videoCodec);
+
+            if (transcodingProfile != null)
+            {
+                //state.EstimateContentLength = transcodingProfile.EstimateContentLength;
+                state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
+                //state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
+
+                foreach (var setting in transcodingProfile.Settings)
+                {
+                    switch (setting.Name)
+                    {
+                        case TranscodingSettingType.VideoProfile:
+                            {
+                                if (state.VideoRequest != null && string.IsNullOrWhiteSpace(state.VideoRequest.VideoProfile))
+                                {
+                                    state.VideoRequest.VideoProfile = setting.Value;
+                                }
+                                break;
+                            }
+                        default:
+                            throw new ArgumentException("Unrecognized TranscodingSettingType");
+                    }
+                }
+            }
+        }
+
+        private EncodingQuality GetQualitySetting()
+        {
+            var quality = _config.Configuration.MediaEncodingQuality;
+
+            if (quality == EncodingQuality.Auto)
+            {
+                var cpuCount = Environment.ProcessorCount;
+
+                if (cpuCount >= 4)
+                {
+                    //return EncodingQuality.HighQuality;
+                }
+
+                return EncodingQuality.HighSpeed;
+            }
+
+            return quality;
+        }
+    }
+}

+ 3 - 61
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -6,10 +6,10 @@ using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Serialization;
 using System;
 using System.Collections.Concurrent;
-using System.ComponentModel;
 using System.Diagnostics;
 using System.Globalization;
 using System.IO;
+using System.Linq;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
@@ -122,35 +122,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// <exception cref="System.ArgumentException">Unrecognized InputType</exception>
         public string GetInputArgument(string[] inputFiles, InputType type)
         {
-            string inputPath;
-
-            switch (type)
-            {
-                case InputType.Bluray:
-                case InputType.Dvd:
-                case InputType.File:
-                    inputPath = GetConcatInputArgument(inputFiles);
-                    break;
-                case InputType.Url:
-                    inputPath = GetHttpInputArgument(inputFiles);
-                    break;
-                default:
-                    throw new ArgumentException("Unrecognized InputType");
-            }
-
-            return inputPath;
-        }
-
-        /// <summary>
-        /// Gets the HTTP input argument.
-        /// </summary>
-        /// <param name="inputFiles">The input files.</param>
-        /// <returns>System.String.</returns>
-        private string GetHttpInputArgument(string[] inputFiles)
-        {
-            var url = inputFiles[0];
-
-            return string.Format("\"{0}\"", url);
+            return EncodingUtils.GetInputArgument(inputFiles.ToList(), type == InputType.Url);
         }
 
         /// <summary>
@@ -160,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// <returns>System.String.</returns>
         public string GetProbeSizeArgument(InputType type)
         {
-            return type == InputType.Dvd ? "-probesize 1G -analyzeduration 200M" : string.Empty;
+            return EncodingUtils.GetProbeSizeArgument(type == InputType.Dvd);
         }
 
         /// <summary>
@@ -879,36 +851,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return memoryStream;
         }
 
-        /// <summary>
-        /// Gets the file input argument.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <returns>System.String.</returns>
-        private string GetFileInputArgument(string path)
-        {
-            return string.Format("file:\"{0}\"", path);
-        }
-
-        /// <summary>
-        /// Gets the concat input argument.
-        /// </summary>
-        /// <param name="playableStreamFiles">The playable stream files.</param>
-        /// <returns>System.String.</returns>
-        private string GetConcatInputArgument(string[] playableStreamFiles)
-        {
-            // Get all streams
-            // If there's more than one we'll need to use the concat command
-            if (playableStreamFiles.Length > 1)
-            {
-                var files = string.Join("|", playableStreamFiles);
-
-                return string.Format("concat:\"{0}\"", files);
-            }
-
-            // Determine the input path for video files
-            return GetFileInputArgument(playableStreamFiles[0]);
-        }
-
         public Task<Stream> EncodeImage(ImageEncodingOptions options, CancellationToken cancellationToken)
         {
             return new ImageEncoder(FFMpegPath, _logger, _fileSystem, _appPaths).EncodeImage(options, cancellationToken);

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

@@ -48,7 +48,12 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="BdInfo\BdInfoExaminer.cs" />
+    <Compile Include="Encoder\AudioEncoder.cs" />
+    <Compile Include="Encoder\EncodingUtils.cs" />
+    <Compile Include="Encoder\FFMpegProcess.cs" />
     <Compile Include="Encoder\ImageEncoder.cs" />
+    <Compile Include="Encoder\InternalEncodingTask.cs" />
+    <Compile Include="Encoder\InternalEncodingTaskFactory.cs" />
     <Compile Include="Encoder\MediaEncoder.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
   </ItemGroup>

+ 3 - 0
MediaBrowser.Model/LiveTv/ChannelInfoDto.cs

@@ -33,6 +33,8 @@ namespace MediaBrowser.Model.LiveTv
         /// <value>The external identifier.</value>
         public string ExternalId { get; set; }
 
+        public List<MediaSourceInfo> MediaSources { get; set; }
+        
         /// <summary>
         /// Gets or sets the image tags.
         /// </summary>
@@ -112,6 +114,7 @@ namespace MediaBrowser.Model.LiveTv
         public ChannelInfoDto()
         {
             ImageTags = new Dictionary<ImageType, Guid>();
+            MediaSources = new List<MediaSourceInfo>();
         }
 
         public event PropertyChangedEventHandler PropertyChanged;

+ 7 - 4
MediaBrowser.Model/LiveTv/RecordingInfoDto.cs

@@ -1,11 +1,11 @@
-using System.Diagnostics;
-using System.Runtime.Serialization;
-using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Library;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
-using MediaBrowser.Model.Library;
+using System.Diagnostics;
+using System.Runtime.Serialization;
 
 namespace MediaBrowser.Model.LiveTv
 {
@@ -248,10 +248,13 @@ namespace MediaBrowser.Model.LiveTv
         /// <value>The type.</value>
         public string Type { get; set; }
 
+        public List<MediaSourceInfo> MediaSources { get; set; }
+        
         public RecordingInfoDto()
         {
             Genres = new List<string>();
             ImageTags = new Dictionary<ImageType, Guid>();
+            MediaSources = new List<MediaSourceInfo>();
         }
 
         public event PropertyChangedEventHandler PropertyChanged;

+ 44 - 4
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -8,6 +8,7 @@ using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Session;
@@ -1064,7 +1065,7 @@ namespace MediaBrowser.Server.Implementations.Dto
                     dto.AlbumPrimaryImageTag = GetImageCacheTag(albumParent, ImageType.Primary);
                 }
 
-                dto.MediaSources = GetMediaSources(audio);
+                dto.MediaSources = GetAudioMediaSources(audio);
                 dto.MediaSourceCount = 1;
             }
 
@@ -1100,7 +1101,7 @@ namespace MediaBrowser.Server.Implementations.Dto
 
                 if (fields.Contains(ItemFields.MediaSources))
                 {
-                    dto.MediaSources = GetMediaSources(video);
+                    dto.MediaSources = GetVideoMediaSources(video);
                 }
 
                 if (fields.Contains(ItemFields.Chapters))
@@ -1266,9 +1267,48 @@ namespace MediaBrowser.Server.Implementations.Dto
             {
                 SetBookProperties(dto, book);
             }
+
+            var tvChannel = item as LiveTvChannel;
+
+            if (tvChannel != null)
+            {
+                dto.MediaSources = GetMediaSources(tvChannel);
+            }
+        }
+
+        public List<MediaSourceInfo> GetMediaSources(BaseItem item)
+        {
+            var video = item as Video;
+
+            if (video != null)
+            {
+                return GetVideoMediaSources(video);
+            }
+
+            var audio = item as Audio;
+
+            if (audio != null)
+            {
+                return GetAudioMediaSources(audio);
+            }
+
+            var result = new List<MediaSourceInfo>
+            {
+                new MediaSourceInfo
+                {
+                    Id = item.Id.ToString("N"),
+                    LocationType = item.LocationType,
+                    Name = item.Name,
+                    Path = GetMappedPath(item),
+                    MediaStreams = _itemRepo.GetMediaStreams(new MediaStreamQuery { ItemId = item.Id }).ToList(),
+                    RunTimeTicks = item.RunTimeTicks
+                }            
+            };
+
+            return result;
         }
 
-        private List<MediaSourceInfo> GetMediaSources(Video item)
+        private List<MediaSourceInfo> GetVideoMediaSources(Video item)
         {
             var result = item.GetAlternateVersions().Select(GetVersionInfo).ToList();
 
@@ -1293,7 +1333,7 @@ namespace MediaBrowser.Server.Implementations.Dto
             .ToList();
         }
 
-        private List<MediaSourceInfo> GetMediaSources(Audio item)
+        private List<MediaSourceInfo> GetAudioMediaSources(Audio item)
         {
             var result = new List<MediaSourceInfo>
             {

+ 67 - 0
MediaBrowser.Server.Implementations/Library/MusicManager.cs

@@ -0,0 +1,67 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Server.Implementations.Library
+{
+    public class MusicManager : IMusicManager
+    {
+        private readonly ILibraryManager _libraryManager;
+
+        public MusicManager(ILibraryManager libraryManager)
+        {
+            _libraryManager = libraryManager;
+        }
+
+        public IEnumerable<Audio> GetInstantMixFromSong(Audio item, User user)
+        {
+            return GetInstantMixFromGenres(item.Genres, user);
+        }
+
+        public IEnumerable<Audio> GetInstantMixFromArtist(string name, User user)
+        {
+            var artist = _libraryManager.GetArtist(name);
+
+            var genres = _libraryManager.RootFolder
+                .RecursiveChildren
+                .OfType<Audio>()
+                .Where(i => i.HasArtist(name))
+                .SelectMany(i => i.Genres)
+                .Concat(artist.Genres)
+                .Distinct(StringComparer.OrdinalIgnoreCase);
+
+            return GetInstantMixFromGenres(genres, user);
+        }
+
+        public IEnumerable<Audio> GetInstantMixFromAlbum(MusicAlbum item, User user)
+        {
+            var genres = item
+               .RecursiveChildren
+               .OfType<Audio>()
+               .SelectMany(i => i.Genres)
+               .Concat(item.Genres)
+               .Distinct(StringComparer.OrdinalIgnoreCase);
+
+            return GetInstantMixFromGenres(genres, user);
+        }
+
+        public IEnumerable<Audio> GetInstantMixFromGenres(IEnumerable<string> genres, User user)
+        {
+            var inputItems = user.RootFolder.GetRecursiveChildren(user);
+
+            var genresDictionary = genres.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
+
+            return inputItems
+                .OfType<Audio>()
+                .Select(i => new Tuple<Audio, int>(i, i.Genres.Count(genresDictionary.ContainsKey)))
+                .OrderByDescending(i => i.Item2)
+                .ThenBy(i => Guid.NewGuid())
+                .Select(i => i.Item1)
+                .Take(100)
+                .OrderBy(i => Guid.NewGuid());
+        }
+    }
+}

+ 1 - 2
MediaBrowser.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs

@@ -34,8 +34,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio
                 {
                     var collectionType = args.GetCollectionType();
 
-                    if (string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase) ||
-                        string.IsNullOrEmpty(collectionType))
+                    if (string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase))
                     {
                         return new Controller.Entities.Audio.Audio();
                     }

+ 5 - 6
MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs

@@ -222,13 +222,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 RunTimeTicks = (info.EndDate - info.StartDate).Ticks,
                 OriginalAirDate = info.OriginalAirDate,
 
-                MediaStreams = _itemRepo.GetMediaStreams(new MediaStreamQuery
-                {
-                    ItemId = recording.Id
-
-                }).ToList()
+                MediaSources = _dtoService.GetMediaSources((BaseItem)recording)
             };
 
+            dto.MediaStreams = dto.MediaSources.SelectMany(i => i.MediaStreams).ToList();
+
             if (info.Status == RecordingStatus.InProgress)
             {
                 var now = DateTime.UtcNow.Ticks;
@@ -317,7 +315,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 Type = info.GetClientTypeName(),
                 Id = info.Id.ToString("N"),
                 MediaType = info.MediaType,
-                ExternalId = info.ExternalId
+                ExternalId = info.ExternalId,
+                MediaSources = _dtoService.GetMediaSources(info)
             };
 
             if (user != null)

+ 1 - 0
MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj

@@ -141,6 +141,7 @@
     <Compile Include="IO\LibraryMonitor.cs" />
     <Compile Include="Library\CoreResolutionIgnoreRule.cs" />
     <Compile Include="Library\LibraryManager.cs" />
+    <Compile Include="Library\MusicManager.cs" />
     <Compile Include="Library\Resolvers\PhotoResolver.cs" />
     <Compile Include="Library\SearchEngine.cs" />
     <Compile Include="Library\ResolverHelper.cs" />

+ 53 - 6
MediaBrowser.Server.Implementations/Session/SessionManager.cs

@@ -3,8 +3,6 @@ using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Session;
@@ -43,6 +41,7 @@ namespace MediaBrowser.Server.Implementations.Session
 
         private readonly ILibraryManager _libraryManager;
         private readonly IUserManager _userManager;
+        private readonly IMusicManager _musicManager;
 
         /// <summary>
         /// Gets or sets the configuration manager.
@@ -688,9 +687,22 @@ namespace MediaBrowser.Server.Implementations.Session
 
             var user = session.UserId.HasValue ? _userManager.GetUserById(session.UserId.Value) : null;
 
-            var items = command.ItemIds.SelectMany(i => TranslateItemForPlayback(i, user))
-                .Where(i => i.LocationType != LocationType.Virtual)
-                .ToList();
+            List<BaseItem> items;
+
+            if (command.PlayCommand == PlayCommand.PlayInstantMix)
+            {
+                items = command.ItemIds.SelectMany(i => TranslateItemForInstantMix(i, user))
+                    .Where(i => i.LocationType != LocationType.Virtual)
+                    .ToList();
+
+                command.PlayCommand = PlayCommand.PlayNow;
+            }
+            else
+            {
+                items = command.ItemIds.SelectMany(i => TranslateItemForPlayback(i, user))
+                   .Where(i => i.LocationType != LocationType.Virtual)
+                   .ToList();
+            }
 
             if (command.PlayCommand == PlayCommand.PlayShuffle)
             {
@@ -741,7 +753,7 @@ namespace MediaBrowser.Server.Implementations.Session
             {
                 var folder = (Folder)item;
 
-                var items = user == null ? folder.RecursiveChildren:
+                var items = user == null ? folder.RecursiveChildren :
                     folder.GetRecursiveChildren(user);
 
                 items = items.Where(i => !i.IsFolder);
@@ -754,6 +766,41 @@ namespace MediaBrowser.Server.Implementations.Session
             return new[] { item };
         }
 
+        private IEnumerable<BaseItem> TranslateItemForInstantMix(string id, User user)
+        {
+            var item = _libraryManager.GetItemById(new Guid(id));
+
+            var audio = item as Audio;
+
+            if (audio != null)
+            {
+                return _musicManager.GetInstantMixFromSong(audio, user);
+            }
+
+            var artist = item as MusicArtist;
+
+            if (artist != null)
+            {
+                return _musicManager.GetInstantMixFromArtist(artist.Name, user);
+            }
+
+            var album = item as MusicAlbum;
+
+            if (album != null)
+            {
+                return _musicManager.GetInstantMixFromAlbum(album, user);
+            }
+
+            var genre = item as MusicGenre;
+
+            if (genre != null)
+            {
+                return _musicManager.GetInstantMixFromGenres(new[] { genre.Name }, user);
+            }
+
+            return new BaseItem[] { };
+        }
+
         public Task SendBrowseCommand(Guid controllingSessionId, Guid sessionId, BrowseRequest command, CancellationToken cancellationToken)
         {
             var session = GetSessionForRemoteControl(sessionId);

+ 2 - 0
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -449,6 +449,8 @@ namespace MediaBrowser.ServerApplication
             LibraryManager = new LibraryManager(Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager);
             RegisterSingleInstance(LibraryManager);
 
+            RegisterSingleInstance<IMusicManager>(new MusicManager(LibraryManager));
+
             LibraryMonitor = new LibraryMonitor(LogManager, TaskManager, LibraryManager, ServerConfigurationManager, FileSystemManager);
             RegisterSingleInstance(LibraryMonitor);
 

+ 1 - 1
MediaBrowser.WebDashboard/ApiClient.js

@@ -4058,7 +4058,7 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi
                 options.ForceTitle = true;
             }
 
-            var url = self.getUrl("Packages/" + packageId + "Reviews", options);
+            var url = self.getUrl("Packages/" + packageId + "/Reviews", options);
 
             return self.ajax({
                 type: "GET",

+ 1 - 1
MediaBrowser.WebDashboard/packages.config

@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="MediaBrowser.ApiClient.Javascript" version="3.0.248" targetFramework="net45" />
+  <package id="MediaBrowser.ApiClient.Javascript" version="3.0.249" targetFramework="net45" />
 </packages>