Browse Source

restored live tv playback in the web client

Luke Pulverenti 11 năm trước cách đây
mục cha
commit
f756e39b9d
31 tập tin đã thay đổi với 1373 bổ sung192 xóa
  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>