Browse Source

Merge pull request #1058 from MediaBrowser/dev

3.0.5569.0
Luke 10 years ago
parent
commit
be74de0236
100 changed files with 2546 additions and 1004 deletions
  1. 30 25
      MediaBrowser.Api/ApiEntryPoint.cs
  2. 11 0
      MediaBrowser.Api/BaseApiService.cs
  3. 91 185
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  4. 4 4
      MediaBrowser.Api/Playback/Dash/MpegDashService.cs
  5. 3 3
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  6. 14 12
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  7. 66 7
      MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
  8. 3 31
      MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
  9. 315 19
      MediaBrowser.Api/Playback/MediaInfoService.cs
  10. 3 3
      MediaBrowser.Api/Playback/Progressive/AudioService.cs
  11. 1 1
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  12. 3 3
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  13. 2 3
      MediaBrowser.Api/Playback/StreamRequest.cs
  14. 45 14
      MediaBrowser.Api/Playback/StreamState.cs
  15. 18 8
      MediaBrowser.Api/Playback/TranscodingThrottler.cs
  16. 1 1
      MediaBrowser.Api/Subtitles/SubtitleService.cs
  17. 44 22
      MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
  18. 8 0
      MediaBrowser.Api/UserLibrary/UserLibraryService.cs
  19. 21 11
      MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs
  20. 20 17
      MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs
  21. 11 5
      MediaBrowser.Controller/Channels/ChannelAudioItem.cs
  22. 5 1
      MediaBrowser.Controller/Channels/ChannelMediaInfo.cs
  23. 11 5
      MediaBrowser.Controller/Channels/ChannelVideoItem.cs
  24. 3 3
      MediaBrowser.Controller/Channels/IChannelManager.cs
  25. 2 1
      MediaBrowser.Controller/Entities/Video.cs
  26. 42 0
      MediaBrowser.Controller/Library/IMediaSourceManager.cs
  27. 16 0
      MediaBrowser.Controller/Library/IMediaSourceProvider.cs
  28. 3 1
      MediaBrowser.Controller/LiveTv/ILiveTvItem.cs
  29. 16 0
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  30. 19 2
      MediaBrowser.Controller/LiveTv/LiveTvAudioRecording.cs
  31. 10 1
      MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
  32. 6 0
      MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
  33. 20 3
      MediaBrowser.Controller/LiveTv/LiveTvVideoRecording.cs
  34. 2 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  35. 5 3
      MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
  36. 73 4
      MediaBrowser.Controller/MediaEncoding/MediaStreamSelector.cs
  37. 3 4
      MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
  38. 18 0
      MediaBrowser.Controller/Sync/IHasDynamicAccess.cs
  39. 1 10
      MediaBrowser.Controller/Sync/IServerSyncProvider.cs
  40. 16 0
      MediaBrowser.Controller/Sync/ISyncDataProvider.cs
  41. 12 1
      MediaBrowser.Controller/Sync/SyncedFileInfo.cs
  42. 13 6
      MediaBrowser.Dlna/Didl/DidlBuilder.cs
  43. 6 4
      MediaBrowser.Dlna/PlayTo/PlayToController.cs
  44. 5 6
      MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
  45. 1 1
      MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs
  46. 18 65
      MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs
  47. 45 9
      MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs
  48. 69 112
      MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs
  49. 0 4
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  50. 7 4
      MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs
  51. 4 3
      MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
  52. 1 2
      MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
  53. 71 38
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  54. 14 2
      MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj
  55. 14 2
      MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
  56. 9 3
      MediaBrowser.Model/ApiClient/IApiClient.cs
  57. 6 0
      MediaBrowser.Model/ApiClient/IConnectionManager.cs
  58. 16 5
      MediaBrowser.Model/ApiClient/ServerCredentials.cs
  59. 6 2
      MediaBrowser.Model/ApiClient/ServerInfo.cs
  60. 4 0
      MediaBrowser.Model/Configuration/EncodingOptions.cs
  61. 1 0
      MediaBrowser.Model/Configuration/UserConfiguration.cs
  62. 11 2
      MediaBrowser.Model/Dlna/ConditionProcessor.cs
  63. 6 2
      MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
  64. 4 2
      MediaBrowser.Model/Dlna/DeviceProfile.cs
  65. 4 1
      MediaBrowser.Model/Dlna/ProfileConditionValue.cs
  66. 99 35
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  67. 190 111
      MediaBrowser.Model/Dlna/StreamInfo.cs
  68. 47 0
      MediaBrowser.Model/Dlna/StreamInfoSorter.cs
  69. 26 1
      MediaBrowser.Model/Dlna/SubtitleProfile.cs
  70. 1 0
      MediaBrowser.Model/Dlna/SubtitleStreamInfo.cs
  71. 47 0
      MediaBrowser.Model/Dto/MediaSourceInfo.cs
  72. 1 1
      MediaBrowser.Model/Dto/MediaSourceType.cs
  73. 11 0
      MediaBrowser.Model/Dto/NameValuePair.cs
  74. 30 1
      MediaBrowser.Model/Entities/MediaStream.cs
  75. 5 1
      MediaBrowser.Model/MediaBrowser.Model.csproj
  76. 35 0
      MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
  77. 9 0
      MediaBrowser.Model/MediaInfo/LiveStreamResponse.cs
  78. 25 0
      MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs
  79. 5 5
      MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs
  80. 5 0
      MediaBrowser.Model/Querying/ItemFields.cs
  81. 5 0
      MediaBrowser.Model/Session/PlaybackProgressInfo.cs
  82. 5 0
      MediaBrowser.Model/Session/PlaybackStopInfo.cs
  83. 11 0
      MediaBrowser.Model/Sync/LocalItem.cs
  84. 1 0
      MediaBrowser.Model/Sync/SyncDataRequest.cs
  85. 11 0
      MediaBrowser.Model/Sync/SyncedItem.cs
  86. 1 1
      MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
  87. 5 4
      MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
  88. 6 8
      MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs
  89. 43 0
      MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs
  90. 0 1
      MediaBrowser.Server.Implementations/Channels/ChannelImageProvider.cs
  91. 27 20
      MediaBrowser.Server.Implementations/Channels/ChannelManager.cs
  92. 25 39
      MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs
  93. 25 15
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  94. 6 2
      MediaBrowser.Server.Implementations/Library/LibraryManager.cs
  95. 295 28
      MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs
  96. 1 1
      MediaBrowser.Server.Implementations/Library/UserViewManager.cs
  97. 4 2
      MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs
  98. 1 1
      MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
  99. 98 38
      MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
  100. 108 0
      MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs

+ 30 - 25
MediaBrowser.Api/ApiEntryPoint.cs

@@ -132,7 +132,7 @@ namespace MediaBrowser.Api
         /// Called when [transcode beginning].
         /// Called when [transcode beginning].
         /// </summary>
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="path">The path.</param>
-        /// <param name="streamId">The stream identifier.</param>
+        /// <param name="playSessionId">The play session identifier.</param>
         /// <param name="transcodingJobId">The transcoding job identifier.</param>
         /// <param name="transcodingJobId">The transcoding job identifier.</param>
         /// <param name="type">The type.</param>
         /// <param name="type">The type.</param>
         /// <param name="process">The process.</param>
         /// <param name="process">The process.</param>
@@ -141,7 +141,7 @@ namespace MediaBrowser.Api
         /// <param name="cancellationTokenSource">The cancellation token source.</param>
         /// <param name="cancellationTokenSource">The cancellation token source.</param>
         /// <returns>TranscodingJob.</returns>
         /// <returns>TranscodingJob.</returns>
         public TranscodingJob OnTranscodeBeginning(string path,
         public TranscodingJob OnTranscodeBeginning(string path,
-            string streamId,
+            string playSessionId,
             string transcodingJobId,
             string transcodingJobId,
             TranscodingJobType type,
             TranscodingJobType type,
             Process process,
             Process process,
@@ -160,7 +160,7 @@ namespace MediaBrowser.Api
                     DeviceId = deviceId,
                     DeviceId = deviceId,
                     CancellationTokenSource = cancellationTokenSource,
                     CancellationTokenSource = cancellationTokenSource,
                     Id = transcodingJobId,
                     Id = transcodingJobId,
-                    StreamId = streamId
+                    PlaySessionId = playSessionId
                 };
                 };
 
 
                 _activeTranscodingJobs.Add(job);
                 _activeTranscodingJobs.Add(job);
@@ -187,7 +187,7 @@ namespace MediaBrowser.Api
 
 
             if (!string.IsNullOrWhiteSpace(deviceId))
             if (!string.IsNullOrWhiteSpace(deviceId))
             {
             {
-                var audioCodec = state.ActualOutputVideoCodec;
+                var audioCodec = state.ActualOutputAudioCodec;
                 var videoCodec = state.ActualOutputVideoCodec;
                 var videoCodec = state.ActualOutputVideoCodec;
 
 
                 _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
                 _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
@@ -274,32 +274,37 @@ namespace MediaBrowser.Api
                     return null;
                     return null;
                 }
                 }
 
 
-                job.ActiveRequestCount++;
-
-                job.DisposeKillTimer();
+                OnTranscodeBeginRequest(job);
 
 
                 return job;
                 return job;
             }
             }
         }
         }
 
 
+        public void OnTranscodeBeginRequest(TranscodingJob job)
+        {
+            job.ActiveRequestCount++;
+
+            job.DisposeKillTimer();
+        }
+        
         public void OnTranscodeEndRequest(TranscodingJob job)
         public void OnTranscodeEndRequest(TranscodingJob job)
         {
         {
             job.ActiveRequestCount--;
             job.ActiveRequestCount--;
 
 
             if (job.ActiveRequestCount == 0)
             if (job.ActiveRequestCount == 0)
             {
             {
-                if (job.Type == TranscodingJobType.Progressive)
-                {
-                    const int timerDuration = 1000;
+                // TODO: Lower this hls timeout
+                var timerDuration = job.Type == TranscodingJobType.Progressive ?
+                    1000 :
+                    7200000;
 
 
-                    if (job.KillTimer == null)
-                    {
-                        job.KillTimer = new Timer(OnTranscodeKillTimerStopped, job, timerDuration, Timeout.Infinite);
-                    }
-                    else
-                    {
-                        job.KillTimer.Change(timerDuration, Timeout.Infinite);
-                    }
+                if (job.KillTimer == null)
+                {
+                    job.KillTimer = new Timer(OnTranscodeKillTimerStopped, job, timerDuration, Timeout.Infinite);
+                }
+                else
+                {
+                    job.KillTimer.Change(timerDuration, Timeout.Infinite);
                 }
                 }
             }
             }
         }
         }
@@ -319,10 +324,10 @@ namespace MediaBrowser.Api
         /// Kills the single transcoding job.
         /// Kills the single transcoding job.
         /// </summary>
         /// </summary>
         /// <param name="deviceId">The device id.</param>
         /// <param name="deviceId">The device id.</param>
-        /// <param name="streamId">The stream identifier.</param>
+        /// <param name="playSessionId">The play session identifier.</param>
         /// <param name="deleteFiles">The delete files.</param>
         /// <param name="deleteFiles">The delete files.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
-        internal void KillTranscodingJobs(string deviceId, string streamId, Func<string, bool> deleteFiles)
+        internal void KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
         {
         {
             if (string.IsNullOrEmpty(deviceId))
             if (string.IsNullOrEmpty(deviceId))
             {
             {
@@ -333,7 +338,7 @@ namespace MediaBrowser.Api
             {
             {
                 if (string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase))
                 if (string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    return string.IsNullOrWhiteSpace(streamId) || string.Equals(streamId, j.StreamId, StringComparison.OrdinalIgnoreCase);
+                    return string.IsNullOrWhiteSpace(playSessionId) || string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase);
                 }
                 }
 
 
                 return false;
                 return false;
@@ -498,7 +503,7 @@ namespace MediaBrowser.Api
                 .ToList();
                 .ToList();
 
 
             Exception e = null;
             Exception e = null;
-            
+
             foreach (var file in filesToDelete)
             foreach (var file in filesToDelete)
             {
             {
                 try
                 try
@@ -534,10 +539,10 @@ namespace MediaBrowser.Api
     public class TranscodingJob
     public class TranscodingJob
     {
     {
         /// <summary>
         /// <summary>
-        /// Gets or sets the stream identifier.
+        /// Gets or sets the play session identifier.
         /// </summary>
         /// </summary>
-        /// <value>The stream identifier.</value>
-        public string StreamId { get; set; }
+        /// <value>The play session identifier.</value>
+        public string PlaySessionId { get; set; }
         /// <summary>
         /// <summary>
         /// Gets or sets the path.
         /// Gets or sets the path.
         /// </summary>
         /// </summary>

+ 11 - 0
MediaBrowser.Api/BaseApiService.cs

@@ -73,6 +73,17 @@ namespace MediaBrowser.Api
             return ResultFactory.GetOptimizedResultUsingCache(Request, cacheKey, lastDateModified, cacheDuration, factoryFn);
             return ResultFactory.GetOptimizedResultUsingCache(Request, cacheKey, lastDateModified, cacheDuration, factoryFn);
         }
         }
 
 
+        /// <summary>
+        /// Infers the server address from the url
+        /// </summary>
+        /// <returns></returns>
+        protected string GetServerAddress()
+        {
+            var index = Request.AbsoluteUri.IndexOf(Request.PathInfo, StringComparison.OrdinalIgnoreCase);
+
+            return Request.AbsoluteUri.Substring(0, index);
+        }
+
         protected void AssertCanUpdateUser(IUserManager userManager, string userId)
         protected void AssertCanUpdateUser(IUserManager userManager, string userId)
         {
         {
             var auth = AuthorizationContext.GetAuthorizationInfo(Request);
             var auth = AuthorizationContext.GetAuthorizationInfo(Request);

+ 91 - 185
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -1,12 +1,10 @@
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.IO;
-using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
@@ -65,7 +63,6 @@ namespace MediaBrowser.Api.Playback
 
 
         protected IFileSystem FileSystem { get; private set; }
         protected IFileSystem FileSystem { get; private set; }
 
 
-        protected ILiveTvManager LiveTvManager { get; private set; }
         protected IDlnaManager DlnaManager { get; private set; }
         protected IDlnaManager DlnaManager { get; private set; }
         protected IDeviceManager DeviceManager { get; private set; }
         protected IDeviceManager DeviceManager { get; private set; }
         protected ISubtitleEncoder SubtitleEncoder { get; private set; }
         protected ISubtitleEncoder SubtitleEncoder { get; private set; }
@@ -75,14 +72,13 @@ namespace MediaBrowser.Api.Playback
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
         /// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
         /// </summary>
         /// </summary>
-        protected BaseStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient)
+        protected BaseStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient)
         {
         {
             ZipClient = zipClient;
             ZipClient = zipClient;
             MediaSourceManager = mediaSourceManager;
             MediaSourceManager = mediaSourceManager;
             DeviceManager = deviceManager;
             DeviceManager = deviceManager;
             SubtitleEncoder = subtitleEncoder;
             SubtitleEncoder = subtitleEncoder;
             DlnaManager = dlnaManager;
             DlnaManager = dlnaManager;
-            LiveTvManager = liveTvManager;
             FileSystem = fileSystem;
             FileSystem = fileSystem;
             ServerConfigurationManager = serverConfig;
             ServerConfigurationManager = serverConfig;
             UserManager = userManager;
             UserManager = userManager;
@@ -95,11 +91,10 @@ namespace MediaBrowser.Api.Playback
         /// Gets the command line arguments.
         /// Gets the command line arguments.
         /// </summary>
         /// </summary>
         /// <param name="outputPath">The output path.</param>
         /// <param name="outputPath">The output path.</param>
-        /// <param name="transcodingJobId">The transcoding job identifier.</param>
         /// <param name="state">The state.</param>
         /// <param name="state">The state.</param>
         /// <param name="isEncoding">if set to <c>true</c> [is encoding].</param>
         /// <param name="isEncoding">if set to <c>true</c> [is encoding].</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        protected abstract string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding);
+        protected abstract string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding);
 
 
         /// <summary>
         /// <summary>
         /// Gets the type of the transcoding job.
         /// Gets the type of the transcoding job.
@@ -128,10 +123,10 @@ namespace MediaBrowser.Api.Playback
 
 
             var outputFileExtension = GetOutputFileExtension(state);
             var outputFileExtension = GetOutputFileExtension(state);
 
 
-            var data = GetCommandLineArguments("dummy\\dummy", "dummyTranscodingId", state, false);
+            var data = GetCommandLineArguments("dummy\\dummy", state, false);
 
 
             data += "-" + (state.Request.DeviceId ?? string.Empty);
             data += "-" + (state.Request.DeviceId ?? string.Empty);
-            data += "-" + (state.Request.StreamId ?? string.Empty);
+            data += "-" + (state.Request.PlaySessionId ?? string.Empty);
             data += "-" + (state.Request.ClientTime ?? string.Empty);
             data += "-" + (state.Request.ClientTime ?? string.Empty);
 
 
             var dataHash = data.GetMD5().ToString("N");
             var dataHash = data.GetMD5().ToString("N");
@@ -704,7 +699,7 @@ namespace MediaBrowser.Api.Playback
 
 
                 if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
                 if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
                 {
                 {
-                    var charenc = SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath);
+                    var charenc = SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.MediaSource.Protocol, CancellationToken.None).Result;
 
 
                     if (!string.IsNullOrEmpty(charenc))
                     if (!string.IsNullOrEmpty(charenc))
                     {
                     {
@@ -719,8 +714,10 @@ namespace MediaBrowser.Api.Playback
                     seconds.ToString(UsCulture));
                     seconds.ToString(UsCulture));
             }
             }
 
 
+            var mediaPath = state.MediaPath ?? string.Empty;
+
             return string.Format("subtitles='{0}:si={1}',setpts=PTS -{2}/TB",
             return string.Format("subtitles='{0}:si={1}',setpts=PTS -{2}/TB",
-                state.MediaPath.Replace('\\', '/').Replace(":/", "\\:/"),
+                mediaPath.Replace('\\', '/').Replace(":/", "\\:/"),
                 state.InternalSubtitleStreamOffset.ToString(UsCulture),
                 state.InternalSubtitleStreamOffset.ToString(UsCulture),
                 seconds.ToString(UsCulture));
                 seconds.ToString(UsCulture));
         }
         }
@@ -895,12 +892,11 @@ namespace MediaBrowser.Api.Playback
         /// <summary>
         /// <summary>
         /// Gets the input argument.
         /// Gets the input argument.
         /// </summary>
         /// </summary>
-        /// <param name="transcodingJobId">The transcoding job identifier.</param>
         /// <param name="state">The state.</param>
         /// <param name="state">The state.</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        protected string GetInputArgument(string transcodingJobId, StreamState state)
+        protected string GetInputArgument(StreamState state)
         {
         {
-            var arg = "-i " + GetInputPathArgument(transcodingJobId, state);
+            var arg = "-i " + GetInputPathArgument(state);
 
 
             if (state.SubtitleStream != null)
             if (state.SubtitleStream != null)
             {
             {
@@ -913,27 +909,18 @@ namespace MediaBrowser.Api.Playback
             return arg;
             return arg;
         }
         }
 
 
-        private string GetInputPathArgument(string transcodingJobId, StreamState state)
+        private string GetInputPathArgument(StreamState state)
         {
         {
-            //if (state.InputProtocol == MediaProtocol.File &&
-            //   state.RunTimeTicks.HasValue &&
-            //   state.VideoType == VideoType.VideoFile &&
-            //   !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
-            //{
-            //    if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo)
-            //    {
-            //    }
-            //}
-
             var protocol = state.InputProtocol;
             var protocol = state.InputProtocol;
+            var mediaPath = state.MediaPath ?? string.Empty;
 
 
-            var inputPath = new[] { state.MediaPath };
+            var inputPath = new[] { mediaPath };
 
 
             if (state.IsInputVideo)
             if (state.IsInputVideo)
             {
             {
                 if (!(state.VideoType == VideoType.Iso && state.IsoMount == null))
                 if (!(state.VideoType == VideoType.Iso && state.IsoMount == null))
                 {
                 {
-                    inputPath = MediaEncoderHelpers.GetInputArgument(state.MediaPath, state.InputProtocol, state.IsoMount, state.PlayableStreamFileNames);
+                    inputPath = MediaEncoderHelpers.GetInputArgument(mediaPath, state.InputProtocol, state.IsoMount, state.PlayableStreamFileNames);
                 }
                 }
             }
             }
 
 
@@ -947,55 +934,25 @@ namespace MediaBrowser.Api.Playback
                 state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
                 state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
             }
             }
 
 
-            if (string.IsNullOrEmpty(state.MediaPath))
+            if (state.MediaSource.RequiresOpening)
             {
             {
-                var checkCodecs = false;
-
-                if (string.Equals(state.ItemType, typeof(LiveTvChannel).Name))
+                var liveStreamResponse = await MediaSourceManager.OpenLiveStream(new LiveStreamRequest
                 {
                 {
-                    var streamInfo = await LiveTvManager.GetChannelStream(state.Request.Id, cancellationTokenSource.Token).ConfigureAwait(false);
-
-                    state.LiveTvStreamId = streamInfo.Id;
+                    OpenToken = state.MediaSource.OpenToken
 
 
-                    state.MediaPath = streamInfo.Path;
-                    state.InputProtocol = streamInfo.Protocol;
+                }, false, cancellationTokenSource.Token).ConfigureAwait(false);
 
 
-                    await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
-
-                    AttachMediaStreamInfo(state, streamInfo, state.VideoRequest, state.RequestedUrl);
-                    checkCodecs = true;
-                }
+                AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.VideoRequest, state.RequestedUrl);
 
 
-                else if (string.Equals(state.ItemType, typeof(LiveTvVideoRecording).Name) ||
-                    string.Equals(state.ItemType, typeof(LiveTvAudioRecording).Name))
+                if (state.VideoRequest != null)
                 {
                 {
-                    var streamInfo = await LiveTvManager.GetRecordingStream(state.Request.Id, cancellationTokenSource.Token).ConfigureAwait(false);
-
-                    state.LiveTvStreamId = streamInfo.Id;
-
-                    state.MediaPath = streamInfo.Path;
-                    state.InputProtocol = streamInfo.Protocol;
-
-                    await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
-
-                    AttachMediaStreamInfo(state, streamInfo, state.VideoRequest, state.RequestedUrl);
-                    checkCodecs = true;
+                    TryStreamCopy(state, state.VideoRequest);
                 }
                 }
+            }
 
 
-                var videoRequest = state.VideoRequest;
-
-                if (videoRequest != null && checkCodecs)
-                {
-                    if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream))
-                    {
-                        state.OutputVideoCodec = "copy";
-                    }
-
-                    if (state.AudioStream != null && CanStreamCopyAudio(videoRequest, state.AudioStream, state.SupportedAudioCodecs))
-                    {
-                        state.OutputAudioCodec = "copy";
-                    }
-                }
+            if (state.MediaSource.BufferMs.HasValue)
+            {
+                await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
             }
             }
         }
         }
 
 
@@ -1017,7 +974,7 @@ namespace MediaBrowser.Api.Playback
             await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
             await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
 
 
             var transcodingId = Guid.NewGuid().ToString("N");
             var transcodingId = Guid.NewGuid().ToString("N");
-            var commandLineArgs = GetCommandLineArguments(outputPath, transcodingId, state, true);
+            var commandLineArgs = GetCommandLineArguments(outputPath, state, true);
 
 
             if (ApiEntryPoint.Instance.GetEncodingOptions().EnableDebugLogging)
             if (ApiEntryPoint.Instance.GetEncodingOptions().EnableDebugLogging)
             {
             {
@@ -1052,7 +1009,7 @@ namespace MediaBrowser.Api.Playback
             }
             }
 
 
             var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath,
             var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath,
-                state.Request.StreamId,
+                state.Request.PlaySessionId,
                 transcodingId,
                 transcodingId,
                 TranscodingJobType,
                 TranscodingJobType,
                 process,
                 process,
@@ -1123,7 +1080,7 @@ namespace MediaBrowser.Api.Playback
             {
             {
                 if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo)
                 if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo)
                 {
                 {
-                    transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger);
+                    transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ServerConfigurationManager);
                     state.TranscodingThrottler.Start();
                     state.TranscodingThrottler.Start();
                 }
                 }
             }
             }
@@ -1554,7 +1511,11 @@ namespace MediaBrowser.Api.Playback
                 }
                 }
                 else if (i == 21)
                 else if (i == 21)
                 {
                 {
-                    request.StreamId = val;
+                    request.PlaySessionId = val;
+                }
+                else if (i == 22)
+                {
+                    request.LiveStreamId = val;
                 }
                 }
             }
             }
         }
         }
@@ -1644,7 +1605,7 @@ namespace MediaBrowser.Api.Playback
                 request.AudioCodec = InferAudioCodec(url);
                 request.AudioCodec = InferAudioCodec(url);
             }
             }
 
 
-            var state = new StreamState(LiveTvManager, Logger)
+            var state = new StreamState(MediaSourceManager, Logger)
             {
             {
                 Request = request,
                 Request = request,
                 RequestedUrl = url
                 RequestedUrl = url
@@ -1658,109 +1619,28 @@ namespace MediaBrowser.Api.Playback
 
 
             var item = LibraryManager.GetItemById(request.Id);
             var item = LibraryManager.GetItemById(request.Id);
 
 
-            List<MediaStream> mediaStreams = null;
+            state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
 
 
-            state.ItemType = item.GetType().Name;
-            state.ItemId = item.Id.ToString("N");
             var archivable = item as IArchivable;
             var archivable = item as IArchivable;
             state.IsInputArchive = archivable != null && archivable.IsArchive;
             state.IsInputArchive = archivable != null && archivable.IsArchive;
 
 
-            if (item is ILiveTvRecording)
-            {
-                var recording = await LiveTvManager.GetInternalRecording(request.Id, cancellationToken).ConfigureAwait(false);
-
-                state.VideoType = VideoType.VideoFile;
-                state.IsInputVideo = string.Equals(recording.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
-
-                var path = recording.RecordingInfo.Path;
-                var mediaUrl = recording.RecordingInfo.Url;
-
-                var source = string.IsNullOrEmpty(request.MediaSourceId)
-                    ? recording.GetMediaSources(false).First()
-                    : MediaSourceManager.GetStaticMediaSource(recording, request.MediaSourceId, false);
-
-                mediaStreams = source.MediaStreams;
-
-                // Just to prevent this from being null and causing other methods to fail
-                state.MediaPath = string.Empty;
-
-                if (!string.IsNullOrEmpty(path))
-                {
-                    state.MediaPath = path;
-                    state.InputProtocol = MediaProtocol.File;
-                }
-                else if (!string.IsNullOrEmpty(mediaUrl))
-                {
-                    state.MediaPath = mediaUrl;
-                    state.InputProtocol = MediaProtocol.Http;
-                }
-
-                state.RunTimeTicks = recording.RunTimeTicks;
-                state.DeInterlace = true;
-                state.OutputAudioSync = "1000";
-                state.InputVideoSync = "-1";
-                state.InputAudioSync = "1";
-                state.InputContainer = recording.Container;
-                state.ReadInputAtNativeFramerate = source.ReadAtNativeFramerate;
-            }
-            else if (item is LiveTvChannel)
+            MediaSourceInfo mediaSource = null;
+            if (string.IsNullOrWhiteSpace(request.LiveStreamId))
             {
             {
-                var channel = LiveTvManager.GetInternalChannel(request.Id);
-
-                state.VideoType = VideoType.VideoFile;
-                state.IsInputVideo = string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
-                mediaStreams = new List<MediaStream>();
-
-                state.DeInterlace = true;
+                var mediaSources = await MediaSourceManager.GetPlayackMediaSources(request.Id, false, cancellationToken).ConfigureAwait(false);
 
 
-                // Just to prevent this from being null and causing other methods to fail
-                state.MediaPath = string.Empty;
+                mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
+                   ? mediaSources.First()
+                   : mediaSources.First(i => string.Equals(i.Id, request.MediaSourceId));
             }
             }
             else
             else
             {
             {
-                var mediaSources = await MediaSourceManager.GetPlayackMediaSources(request.Id, false, cancellationToken).ConfigureAwait(false);
-
-                var mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
-                    ? mediaSources.First()
-                    : mediaSources.First(i => string.Equals(i.Id, request.MediaSourceId));
-
-                mediaStreams = mediaSource.MediaStreams;
-
-                state.MediaPath = mediaSource.Path;
-                state.InputProtocol = mediaSource.Protocol;
-                state.InputContainer = mediaSource.Container;
-                state.InputFileSize = mediaSource.Size;
-                state.InputBitrate = mediaSource.Bitrate;
-                state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
-                state.RunTimeTicks = mediaSource.RunTimeTicks;
-                state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
-
-                var video = item as Video;
-
-                if (video != null)
-                {
-                    state.IsInputVideo = true;
-
-                    if (mediaSource.VideoType.HasValue)
-                    {
-                        state.VideoType = mediaSource.VideoType.Value;
-                    }
-
-                    state.IsoType = mediaSource.IsoType;
-
-                    state.PlayableStreamFileNames = mediaSource.PlayableStreamFileNames.ToList();
-
-                    if (mediaSource.Timestamp.HasValue)
-                    {
-                        state.InputTimestamp = mediaSource.Timestamp.Value;
-                    }
-                }
-
+                mediaSource = await MediaSourceManager.GetLiveStream(request.LiveStreamId, cancellationToken).ConfigureAwait(false);
             }
             }
 
 
             var videoRequest = request as VideoStreamRequest;
             var videoRequest = request as VideoStreamRequest;
 
 
-            AttachMediaStreamInfo(state, mediaStreams, videoRequest, url);
+            AttachMediaSourceInfo(state, mediaSource, videoRequest, url);
 
 
             var container = Path.GetExtension(state.RequestedUrl);
             var container = Path.GetExtension(state.RequestedUrl);
 
 
@@ -1801,15 +1681,7 @@ namespace MediaBrowser.Api.Playback
 
 
             if (videoRequest != null)
             if (videoRequest != null)
             {
             {
-                if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream))
-                {
-                    state.OutputVideoCodec = "copy";
-                }
-
-                if (state.AudioStream != null && CanStreamCopyAudio(videoRequest, state.AudioStream, state.SupportedAudioCodecs))
-                {
-                    state.OutputAudioCodec = "copy";
-                }
+                TryStreamCopy(state, videoRequest);
             }
             }
 
 
             state.OutputFilePath = GetOutputFilePath(state);
             state.OutputFilePath = GetOutputFilePath(state);
@@ -1817,11 +1689,47 @@ namespace MediaBrowser.Api.Playback
             return state;
             return state;
         }
         }
 
 
-        private void AttachMediaStreamInfo(StreamState state,
+        private void TryStreamCopy(StreamState state, VideoStreamRequest videoRequest)
+        {
+            if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream))
+            {
+                state.OutputVideoCodec = "copy";
+            }
+
+            if (state.AudioStream != null && CanStreamCopyAudio(videoRequest, state.AudioStream, state.SupportedAudioCodecs))
+            {
+                state.OutputAudioCodec = "copy";
+            }
+        }
+
+        private void AttachMediaSourceInfo(StreamState state,
           MediaSourceInfo mediaSource,
           MediaSourceInfo mediaSource,
           VideoStreamRequest videoRequest,
           VideoStreamRequest videoRequest,
           string requestedUrl)
           string requestedUrl)
         {
         {
+            state.MediaPath = mediaSource.Path;
+            state.InputProtocol = mediaSource.Protocol;
+            state.InputContainer = mediaSource.Container;
+            state.InputFileSize = mediaSource.Size;
+            state.InputBitrate = mediaSource.Bitrate;
+            state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
+            state.RunTimeTicks = mediaSource.RunTimeTicks;
+            state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
+
+            if (mediaSource.VideoType.HasValue)
+            {
+                state.VideoType = mediaSource.VideoType.Value;
+            }
+
+            state.IsoType = mediaSource.IsoType;
+
+            state.PlayableStreamFileNames = mediaSource.PlayableStreamFileNames.ToList();
+
+            if (mediaSource.Timestamp.HasValue)
+            {
+                state.InputTimestamp = mediaSource.Timestamp.Value;
+            }
+
             state.InputProtocol = mediaSource.Protocol;
             state.InputProtocol = mediaSource.Protocol;
             state.MediaPath = mediaSource.Path;
             state.MediaPath = mediaSource.Path;
             state.RunTimeTicks = mediaSource.RunTimeTicks;
             state.RunTimeTicks = mediaSource.RunTimeTicks;
@@ -1830,21 +1738,16 @@ namespace MediaBrowser.Api.Playback
             state.InputFileSize = mediaSource.Size;
             state.InputFileSize = mediaSource.Size;
             state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
             state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
 
 
-            if (state.ReadInputAtNativeFramerate)
+            if (state.ReadInputAtNativeFramerate ||
+                mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))
             {
             {
                 state.OutputAudioSync = "1000";
                 state.OutputAudioSync = "1000";
                 state.InputVideoSync = "-1";
                 state.InputVideoSync = "-1";
                 state.InputAudioSync = "1";
                 state.InputAudioSync = "1";
             }
             }
 
 
-            AttachMediaStreamInfo(state, mediaSource.MediaStreams, videoRequest, requestedUrl);
-        }
+            var mediaStreams = mediaSource.MediaStreams;
 
 
-        private void AttachMediaStreamInfo(StreamState state,
-            List<MediaStream> mediaStreams,
-            VideoStreamRequest videoRequest,
-            string requestedUrl)
-        {
             if (videoRequest != null)
             if (videoRequest != null)
             {
             {
                 if (string.IsNullOrEmpty(videoRequest.VideoCodec))
                 if (string.IsNullOrEmpty(videoRequest.VideoCodec))
@@ -1873,7 +1776,7 @@ namespace MediaBrowser.Api.Playback
                 state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
                 state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
             }
             }
 
 
-            state.AllMediaStreams = mediaStreams;
+            state.MediaSource = mediaSource;
         }
         }
 
 
         private bool CanStreamCopyVideo(VideoStreamRequest request, MediaStream videoStream)
         private bool CanStreamCopyVideo(VideoStreamRequest request, MediaStream videoStream)
@@ -2109,7 +2012,6 @@ namespace MediaBrowser.Api.Playback
             }
             }
 
 
             var audioCodec = state.ActualOutputAudioCodec;
             var audioCodec = state.ActualOutputAudioCodec;
-
             var videoCodec = state.ActualOutputVideoCodec;
             var videoCodec = state.ActualOutputVideoCodec;
 
 
             var mediaProfile = state.VideoRequest == null ?
             var mediaProfile = state.VideoRequest == null ?
@@ -2130,7 +2032,9 @@ namespace MediaBrowser.Api.Playback
                 state.TargetTimestamp,
                 state.TargetTimestamp,
                 state.IsTargetAnamorphic,
                 state.IsTargetAnamorphic,
                 state.IsTargetCabac,
                 state.IsTargetCabac,
-                state.TargetRefFrames);
+                state.TargetRefFrames,
+                state.TargetVideoStreamCount,
+                state.TargetAudioStreamCount);
 
 
             if (mediaProfile != null)
             if (mediaProfile != null)
             {
             {
@@ -2215,7 +2119,9 @@ namespace MediaBrowser.Api.Playback
                     state.TranscodeSeekInfo,
                     state.TranscodeSeekInfo,
                     state.IsTargetAnamorphic,
                     state.IsTargetAnamorphic,
                     state.IsTargetCabac,
                     state.IsTargetCabac,
-                    state.TargetRefFrames
+                    state.TargetRefFrames,
+                    state.TargetVideoStreamCount,
+                    state.TargetAudioStreamCount
 
 
                     ).FirstOrDefault() ?? string.Empty;
                     ).FirstOrDefault() ?? string.Empty;
             }
             }

+ 4 - 4
MediaBrowser.Api/Playback/Dash/MpegDashService.cs

@@ -54,7 +54,7 @@ namespace MediaBrowser.Api.Playback.Dash
 
 
     public class MpegDashService : BaseHlsService
     public class MpegDashService : BaseHlsService
     {
     {
-        public MpegDashService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient)
+        public MpegDashService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient)
         {
         {
             NetworkManager = networkManager;
             NetworkManager = networkManager;
         }
         }
@@ -160,7 +160,7 @@ namespace MediaBrowser.Api.Playback.Dash
                             // If the playlist doesn't already exist, startup ffmpeg
                             // If the playlist doesn't already exist, startup ffmpeg
                             try
                             try
                             {
                             {
-                                ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.StreamId, p => false);
+                                ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, p => false);
 
 
                                 if (currentTranscodingIndex.HasValue)
                                 if (currentTranscodingIndex.HasValue)
                                 {
                                 {
@@ -447,7 +447,7 @@ namespace MediaBrowser.Api.Playback.Dash
             return args;
             return args;
         }
         }
 
 
-        protected override string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding)
+        protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
         {
         {
             // test url http://192.168.1.2:8096/videos/233e8905d559a8f230db9bffd2ac9d6d/master.mpd?mediasourceid=233e8905d559a8f230db9bffd2ac9d6d&videocodec=h264&audiocodec=aac&maxwidth=1280&videobitrate=500000&audiobitrate=128000&profile=baseline&level=3
             // test url http://192.168.1.2:8096/videos/233e8905d559a8f230db9bffd2ac9d6d/master.mpd?mediasourceid=233e8905d559a8f230db9bffd2ac9d6d&videocodec=h264&audiocodec=aac&maxwidth=1280&videobitrate=500000&audiobitrate=128000&profile=baseline&level=3
             // Good info on i-frames http://blog.streamroot.io/encode-multi-bitrate-videos-mpeg-dash-mse-based-media-players/
             // Good info on i-frames http://blog.streamroot.io/encode-multi-bitrate-videos-mpeg-dash-mse-based-media-players/
@@ -461,7 +461,7 @@ namespace MediaBrowser.Api.Playback.Dash
 
 
             var args = string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts {5} -f dash -init_seg_name \"{6}\" -media_seg_name \"{7}\" -use_template 0 -use_timeline 1 -min_seg_duration {8} -y \"{9}\"",
             var args = string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts {5} -f dash -init_seg_name \"{6}\" -media_seg_name \"{7}\" -use_template 0 -use_timeline 1 -min_seg_duration {8} -y \"{9}\"",
                 inputModifier,
                 inputModifier,
-                GetInputArgument(transcodingJobId, state),
+                GetInputArgument(state),
                 threads,
                 threads,
                 GetMapArgs(state),
                 GetMapArgs(state),
                 GetVideoArguments(state),
                 GetVideoArguments(state),

+ 3 - 3
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -22,7 +22,7 @@ namespace MediaBrowser.Api.Playback.Hls
     /// </summary>
     /// </summary>
     public abstract class BaseHlsService : BaseStreamingService
     public abstract class BaseHlsService : BaseStreamingService
     {
     {
-        protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient)
+        protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient)
         {
         {
         }
         }
 
 
@@ -212,7 +212,7 @@ namespace MediaBrowser.Api.Playback.Hls
             }
             }
         }
         }
 
 
-        protected override string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding)
+        protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
         {
         {
             var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream;
             var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream;
 
 
@@ -240,7 +240,7 @@ namespace MediaBrowser.Api.Playback.Hls
             var args = string.Format("{0} {1} {2} -map_metadata -1 -threads {3} {4} {5} -sc_threshold 0 {6} -hls_time {7} -start_number {8} -hls_list_size {9}{10} -y \"{11}\"",
             var args = string.Format("{0} {1} {2} -map_metadata -1 -threads {3} {4} {5} -sc_threshold 0 {6} -hls_time {7} -start_number {8} -hls_list_size {9}{10} -y \"{11}\"",
                 itsOffset,
                 itsOffset,
                 inputModifier,
                 inputModifier,
-                GetInputArgument(transcodingJobId, state),
+                GetInputArgument(state),
                 threads,
                 threads,
                 GetMapArgs(state),
                 GetMapArgs(state),
                 GetVideoArguments(state),
                 GetVideoArguments(state),

+ 14 - 12
MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

@@ -62,7 +62,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
 
     public class DynamicHlsService : BaseHlsService
     public class DynamicHlsService : BaseHlsService
     {
     {
-        public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient)
+        public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient)
         {
         {
             NetworkManager = networkManager;
             NetworkManager = networkManager;
         }
         }
@@ -114,7 +114,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
 
             if (File.Exists(segmentPath))
             if (File.Exists(segmentPath))
             {
             {
-                job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
+                job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
                 return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false);
                 return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false);
             }
             }
 
 
@@ -123,7 +123,7 @@ namespace MediaBrowser.Api.Playback.Hls
             {
             {
                 if (File.Exists(segmentPath))
                 if (File.Exists(segmentPath))
                 {
                 {
-                    job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
+                    job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
                     return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false);
                     return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false);
                 }
                 }
                 else
                 else
@@ -135,7 +135,7 @@ namespace MediaBrowser.Api.Playback.Hls
                         // If the playlist doesn't already exist, startup ffmpeg
                         // If the playlist doesn't already exist, startup ffmpeg
                         try
                         try
                         {
                         {
-                            ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.StreamId, p => false);
+                            ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, p => false);
 
 
                             if (currentTranscodingIndex.HasValue)
                             if (currentTranscodingIndex.HasValue)
                             {
                             {
@@ -145,6 +145,7 @@ namespace MediaBrowser.Api.Playback.Hls
                             request.StartTimeTicks = GetSeekPositionTicks(state, requestedIndex);
                             request.StartTimeTicks = GetSeekPositionTicks(state, requestedIndex);
 
 
                             job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
                             job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
+                            ApiEntryPoint.Instance.OnTranscodeBeginRequest(job);
                         }
                         }
                         catch
                         catch
                         {
                         {
@@ -168,7 +169,7 @@ namespace MediaBrowser.Api.Playback.Hls
             }
             }
 
 
             Logger.Info("returning {0}", segmentPath);
             Logger.Info("returning {0}", segmentPath);
-            job = job ?? ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
+            job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
             return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false);
             return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false);
         }
         }
 
 
@@ -368,8 +369,8 @@ namespace MediaBrowser.Api.Playback.Hls
                     if (transcodingJob != null)
                     if (transcodingJob != null)
                     {
                     {
                         transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
                         transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
+                        ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
                     }
                     }
-
                 }
                 }
             });
             });
         }
         }
@@ -413,7 +414,8 @@ namespace MediaBrowser.Api.Playback.Hls
 
 
             var request = (GetMasterHlsVideoStream)state.Request;
             var request = (GetMasterHlsVideoStream)state.Request;
 
 
-            var subtitleStreams = state.AllMediaStreams
+            var subtitleStreams = state.MediaSource
+                .MediaStreams
                 .Where(i => i.IsTextSubtitleStream)
                 .Where(i => i.IsTextSubtitleStream)
                 .ToList();
                 .ToList();
 
 
@@ -683,7 +685,7 @@ namespace MediaBrowser.Api.Playback.Hls
             return args;
             return args;
         }
         }
 
 
-        protected override string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding)
+        protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
         {
         {
             var threads = GetNumberOfThreads(state, false);
             var threads = GetNumberOfThreads(state, false);
 
 
@@ -696,9 +698,9 @@ namespace MediaBrowser.Api.Playback.Hls
             {
             {
                 var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d.ts";
                 var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d.ts";
 
 
-                return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts -flags -global_header {5} -f segment -segment_time {6} -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
+                return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts -flags -global_header -sc_threshold 0 {5} -f segment -segment_time {6} -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
                     inputModifier,
                     inputModifier,
-                    GetInputArgument(transcodingJobId, state),
+                    GetInputArgument(state),
                     threads,
                     threads,
                     GetMapArgs(state),
                     GetMapArgs(state),
                     GetVideoArguments(state),
                     GetVideoArguments(state),
@@ -710,9 +712,9 @@ namespace MediaBrowser.Api.Playback.Hls
                     ).Trim();
                     ).Trim();
             }
             }
 
 
-            return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts -flags -global_header {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"",
+            return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts -flags -global_header -sc_threshold 0 {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"",
                             inputModifier,
                             inputModifier,
-                            GetInputArgument(transcodingJobId, state),
+                            GetInputArgument(state),
                             threads,
                             threads,
                             GetMapArgs(state),
                             GetMapArgs(state),
                             GetVideoArguments(state),
                             GetVideoArguments(state),

+ 66 - 7
MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs

@@ -1,6 +1,10 @@
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Net;
 using ServiceStack;
 using ServiceStack;
+using System;
 using System.IO;
 using System.IO;
+using System.Linq;
 
 
 namespace MediaBrowser.Api.Playback.Hls
 namespace MediaBrowser.Api.Playback.Hls
 {
 {
@@ -32,6 +36,8 @@ namespace MediaBrowser.Api.Playback.Hls
     [Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
     [Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
     public class GetHlsPlaylist
     public class GetHlsPlaylist
     {
     {
+        // TODO: Deprecate with new iOS app
+
         /// <summary>
         /// <summary>
         /// Gets or sets the id.
         /// Gets or sets the id.
         /// </summary>
         /// </summary>
@@ -48,31 +54,48 @@ namespace MediaBrowser.Api.Playback.Hls
         [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
         [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
         public string DeviceId { get; set; }
         public string DeviceId { get; set; }
 
 
-        [ApiMember(Name = "StreamId", Description = "The stream id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string StreamId { get; set; }
+        [ApiMember(Name = "PlaySessionId", Description = "The play session id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
+        public string PlaySessionId { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetHlsVideoSegment
+    /// </summary>
+    [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.ts", "GET")]
+    [Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
+    public class GetHlsVideoSegment : VideoStreamRequest
+    {
+        public string PlaylistId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the segment id.
+        /// </summary>
+        /// <value>The segment id.</value>
+        public string SegmentId { get; set; }
     }
     }
 
 
     public class HlsSegmentService : BaseApiService
     public class HlsSegmentService : BaseApiService
     {
     {
         private readonly IServerApplicationPaths _appPaths;
         private readonly IServerApplicationPaths _appPaths;
+        private readonly IServerConfigurationManager _config;
 
 
-        public HlsSegmentService(IServerApplicationPaths appPaths)
+        public HlsSegmentService(IServerApplicationPaths appPaths, IServerConfigurationManager config)
         {
         {
             _appPaths = appPaths;
             _appPaths = appPaths;
+            _config = config;
         }
         }
 
 
         public object Get(GetHlsPlaylist request)
         public object Get(GetHlsPlaylist request)
         {
         {
             var file = request.PlaylistId + Path.GetExtension(Request.PathInfo);
             var file = request.PlaylistId + Path.GetExtension(Request.PathInfo);
-
             file = Path.Combine(_appPaths.TranscodingTempPath, file);
             file = Path.Combine(_appPaths.TranscodingTempPath, file);
 
 
-            return ResultFactory.GetStaticFileResult(Request, file, FileShare.ReadWrite);
+            return GetFileResult(file, file);
         }
         }
 
 
         public void Delete(StopEncodingProcess request)
         public void Delete(StopEncodingProcess request)
         {
         {
-            ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.StreamId, path => true);
+            ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, path => true);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -80,13 +103,49 @@ namespace MediaBrowser.Api.Playback.Hls
         /// </summary>
         /// </summary>
         /// <param name="request">The request.</param>
         /// <param name="request">The request.</param>
         /// <returns>System.Object.</returns>
         /// <returns>System.Object.</returns>
-        public object Get(GetHlsAudioSegment request)
+        public object Get(GetHlsVideoSegment request)
         {
         {
             var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
             var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
+            file = Path.Combine(_config.ApplicationPaths.TranscodingTempPath, file);
+
+            var normalizedPlaylistId = request.PlaylistId.Replace("-low", string.Empty);
+
+            var playlistPath = Directory.EnumerateFiles(_config.ApplicationPaths.TranscodingTempPath, "*")
+                .FirstOrDefault(i => string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase) && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1);
+
+            return GetFileResult(file, playlistPath);
+        }
 
 
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetHlsAudioSegment request)
+        {
+            // TODO: Deprecate with new iOS app
+            var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
             file = Path.Combine(_appPaths.TranscodingTempPath, file);
             file = Path.Combine(_appPaths.TranscodingTempPath, file);
 
 
             return ResultFactory.GetStaticFileResult(Request, file, FileShare.ReadWrite);
             return ResultFactory.GetStaticFileResult(Request, file, FileShare.ReadWrite);
         }
         }
+
+        private object GetFileResult(string path, string playlistPath)
+        {
+            var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
+
+            return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
+            {
+                Path = path,
+                FileShare = FileShare.ReadWrite,
+                OnComplete = () =>
+                {
+                    if (transcodingJob != null)
+                    {
+                        ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
+                    }
+                }
+            });
+        }
     }
     }
 }
 }

+ 3 - 31
MediaBrowser.Api/Playback/Hls/VideoHlsService.cs

@@ -19,6 +19,8 @@ namespace MediaBrowser.Api.Playback.Hls
     [Api(Description = "Gets a video stream using HTTP live streaming.")]
     [Api(Description = "Gets a video stream using HTTP live streaming.")]
     public class GetHlsVideoStream : VideoStreamRequest
     public class GetHlsVideoStream : VideoStreamRequest
     {
     {
+        // TODO: Deprecate with new iOS app
+        
         [ApiMember(Name = "BaselineStreamAudioBitRate", Description = "Optional. Specify the audio bitrate for the baseline stream.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
         [ApiMember(Name = "BaselineStreamAudioBitRate", Description = "Optional. Specify the audio bitrate for the baseline stream.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
         public int? BaselineStreamAudioBitRate { get; set; }
         public int? BaselineStreamAudioBitRate { get; set; }
 
 
@@ -35,43 +37,13 @@ namespace MediaBrowser.Api.Playback.Hls
     {
     {
     }
     }
 
 
-    /// <summary>
-    /// Class GetHlsVideoSegment
-    /// </summary>
-    [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.ts", "GET")]
-    [Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
-    public class GetHlsVideoSegment : VideoStreamRequest
-    {
-        public string PlaylistId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the segment id.
-        /// </summary>
-        /// <value>The segment id.</value>
-        public string SegmentId { get; set; }
-    }
-
     /// <summary>
     /// <summary>
     /// Class VideoHlsService
     /// Class VideoHlsService
     /// </summary>
     /// </summary>
     public class VideoHlsService : BaseHlsService
     public class VideoHlsService : BaseHlsService
     {
     {
-        public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient)
-        {
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetHlsVideoSegment request)
+        public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient)
         {
         {
-            var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
-
-            file = Path.Combine(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, file);
-
-            return ResultFactory.GetStaticFileResult(Request, file);
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 315 - 19
MediaBrowser.Api/Playback/MediaInfoService.cs

@@ -1,8 +1,12 @@
-using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Session;
 using ServiceStack;
 using ServiceStack;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -13,7 +17,7 @@ using System.Threading.Tasks;
 namespace MediaBrowser.Api.Playback
 namespace MediaBrowser.Api.Playback
 {
 {
     [Route("/Items/{Id}/MediaInfo", "GET", Summary = "Gets live playback media info for an item")]
     [Route("/Items/{Id}/MediaInfo", "GET", Summary = "Gets live playback media info for an item")]
-    public class GetLiveMediaInfo : IReturn<LiveMediaInfoResult>
+    public class GetLiveMediaInfo : IReturn<PlaybackInfoResponse>
     {
     {
         [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         public string Id { get; set; }
         public string Id { get; set; }
@@ -23,7 +27,7 @@ namespace MediaBrowser.Api.Playback
     }
     }
 
 
     [Route("/Items/{Id}/PlaybackInfo", "GET", Summary = "Gets live playback media info for an item")]
     [Route("/Items/{Id}/PlaybackInfo", "GET", Summary = "Gets live playback media info for an item")]
-    public class GetPlaybackInfo : IReturn<LiveMediaInfoResult>
+    public class GetPlaybackInfo : IReturn<PlaybackInfoResponse>
     {
     {
         [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         public string Id { get; set; }
         public string Id { get; set; }
@@ -32,45 +36,337 @@ namespace MediaBrowser.Api.Playback
         public string UserId { get; set; }
         public string UserId { get; set; }
     }
     }
 
 
+    [Route("/Items/{Id}/PlaybackInfo", "POST", Summary = "Gets live playback media info for an item")]
+    public class GetPostedPlaybackInfo : PlaybackInfoRequest, IReturn<PlaybackInfoResponse>
+    {
+    }
+
+    [Route("/LiveStreams/Open", "POST", Summary = "Opens a media source")]
+    public class OpenMediaSource : LiveStreamRequest, IReturn<LiveStreamResponse>
+    {
+    }
+
+    [Route("/LiveStreams/Close", "POST", Summary = "Closes a media source")]
+    public class CloseMediaSource : IReturnVoid
+    {
+        [ApiMember(Name = "LiveStreamId", Description = "LiveStreamId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string LiveStreamId { get; set; }
+    }
+
     [Authenticated]
     [Authenticated]
     public class MediaInfoService : BaseApiService
     public class MediaInfoService : BaseApiService
     {
     {
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IDeviceManager _deviceManager;
+        private readonly ILibraryManager _libraryManager;
 
 
-        public MediaInfoService(IMediaSourceManager mediaSourceManager)
+        public MediaInfoService(IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager)
         {
         {
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
+            _deviceManager = deviceManager;
+            _libraryManager = libraryManager;
         }
         }
 
 
-        public Task<object> Get(GetPlaybackInfo request)
+        public async Task<object> Get(GetPlaybackInfo request)
         {
         {
-            return GetPlaybackInfo(request.Id, request.UserId);
+            var result = await GetPlaybackInfo(request.Id, request.UserId).ConfigureAwait(false);
+            return ToOptimizedResult(result);
         }
         }
 
 
-        public Task<object> Get(GetLiveMediaInfo request)
+        public async Task<object> Get(GetLiveMediaInfo request)
         {
         {
-            return GetPlaybackInfo(request.Id, request.UserId);
+            var result = await GetPlaybackInfo(request.Id, request.UserId).ConfigureAwait(false);
+            return ToOptimizedResult(result);
         }
         }
 
 
-        private async Task<object> GetPlaybackInfo(string id, string userId)
+        public async Task<object> Post(OpenMediaSource request)
         {
         {
-            IEnumerable<MediaSourceInfo> mediaSources;
-            var result = new LiveMediaInfoResult();
+            var authInfo = AuthorizationContext.GetAuthorizationInfo(Request);
+
+            var result = await _mediaSourceManager.OpenLiveStream(request, false, CancellationToken.None).ConfigureAwait(false);
 
 
-            try
+            var profile = request.DeviceProfile;
+            if (profile == null)
             {
             {
-                mediaSources = await _mediaSourceManager.GetPlayackMediaSources(id, userId, true, CancellationToken.None).ConfigureAwait(false);
+                var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
+                if (caps != null)
+                {
+                    profile = caps.DeviceProfile;
+                }
             }
             }
-            catch (PlaybackException ex)
+
+            if (profile != null)
             {
             {
-                mediaSources = new List<MediaSourceInfo>();
-                result.ErrorCode = ex.ErrorCode;
-            }
+                var item = _libraryManager.GetItemById(request.ItemId);
 
 
-            result.MediaSources = mediaSources.ToList();
-            result.StreamId = Guid.NewGuid().ToString("N");
+                SetDeviceSpecificData(item, result.MediaSource, profile, authInfo, request.MaxStreamingBitrate,
+                    request.StartTimeTicks ?? 0, result.MediaSource.Id, request.AudioStreamIndex,
+                    request.SubtitleStreamIndex);
+            }
+            else
+            {
+                if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
+                {
+                    result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
+                }
+            }
 
 
             return ToOptimizedResult(result);
             return ToOptimizedResult(result);
         }
         }
+
+        public void Post(CloseMediaSource request)
+        {
+            var task = _mediaSourceManager.CloseLiveStream(request.LiveStreamId, CancellationToken.None);
+            Task.WaitAll(task);
+        }
+
+        public async Task<object> Post(GetPostedPlaybackInfo request)
+        {
+            var info = await GetPlaybackInfo(request.Id, request.UserId, request.MediaSourceId, request.LiveStreamId).ConfigureAwait(false);
+            var authInfo = AuthorizationContext.GetAuthorizationInfo(Request);
+
+            var profile = request.DeviceProfile;
+            if (profile == null)
+            {
+                var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
+                if (caps != null)
+                {
+                    profile = caps.DeviceProfile;
+                }
+            }
+
+            if (profile != null)
+            {
+                var mediaSourceId = request.MediaSourceId;
+                SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex);
+            }
+
+            return ToOptimizedResult(info);
+        }
+
+        private async Task<PlaybackInfoResponse> GetPlaybackInfo(string id, string userId, string mediaSourceId = null, string liveStreamId = null)
+        {
+            var result = new PlaybackInfoResponse();
+
+            if (string.IsNullOrWhiteSpace(liveStreamId))
+            {
+                IEnumerable<MediaSourceInfo> mediaSources;
+                try
+                {
+                    mediaSources = await _mediaSourceManager.GetPlayackMediaSources(id, userId, true, CancellationToken.None).ConfigureAwait(false);
+                }
+                catch (PlaybackException ex)
+                {
+                    mediaSources = new List<MediaSourceInfo>();
+                    result.ErrorCode = ex.ErrorCode;
+                }
+
+                result.MediaSources = mediaSources.ToList();
+
+                if (!string.IsNullOrWhiteSpace(mediaSourceId))
+                {
+                    result.MediaSources = result.MediaSources
+                        .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
+                        .ToList();
+                }
+            }
+            else
+            {
+                var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
+
+                result.MediaSources = new List<MediaSourceInfo> { mediaSource };
+            }
+
+            if (result.MediaSources.Count == 0)
+            {
+                if (!result.ErrorCode.HasValue)
+                {
+                    result.ErrorCode = PlaybackErrorCode.NoCompatibleStream;
+                }
+            }
+            else
+            {
+                result.PlaySessionId = Guid.NewGuid().ToString("N");
+            }
+
+            return result;
+        }
+
+        private void SetDeviceSpecificData(string itemId,
+            PlaybackInfoResponse result,
+            DeviceProfile profile,
+            AuthorizationInfo auth,
+            int? maxBitrate,
+            long startTimeTicks,
+            string mediaSourceId,
+            int? audioStreamIndex,
+            int? subtitleStreamIndex)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+
+            foreach (var mediaSource in result.MediaSources)
+            {
+                SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
+            }
+
+            SortMediaSources(result);
+        }
+
+        private void SetDeviceSpecificData(BaseItem item,
+            MediaSourceInfo mediaSource,
+            DeviceProfile profile,
+            AuthorizationInfo auth,
+            int? maxBitrate,
+            long startTimeTicks,
+            string mediaSourceId,
+            int? audioStreamIndex,
+            int? subtitleStreamIndex)
+        {
+            var streamBuilder = new StreamBuilder();
+
+            var baseUrl = GetServerAddress();
+
+            var options = new VideoOptions
+            {
+                MediaSources = new List<MediaSourceInfo> { mediaSource },
+                Context = EncodingContext.Streaming,
+                DeviceId = auth.DeviceId,
+                ItemId = item.Id.ToString("N"),
+                Profile = profile,
+                MaxBitrate = maxBitrate
+            };
+
+            if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
+            {
+                options.MediaSourceId = mediaSourceId;
+                options.AudioStreamIndex = audioStreamIndex;
+                options.SubtitleStreamIndex = subtitleStreamIndex;
+            }
+
+            if (mediaSource.SupportsDirectPlay)
+            {
+                var supportsDirectStream = mediaSource.SupportsDirectStream;
+
+                // Dummy this up to fool StreamBuilder
+                mediaSource.SupportsDirectStream = true;
+
+                // The MediaSource supports direct stream, now test to see if the client supports it
+                var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
+                    streamBuilder.BuildAudioItem(options) :
+                    streamBuilder.BuildVideoItem(options);
+
+                if (streamInfo == null || !streamInfo.IsDirectStream)
+                {
+                    mediaSource.SupportsDirectPlay = false;
+                }
+
+                // Set this back to what it was
+                mediaSource.SupportsDirectStream = supportsDirectStream;
+
+                if (streamInfo != null)
+                {
+                    SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, baseUrl, auth.Token);
+                }
+            }
+
+            if (mediaSource.SupportsDirectStream)
+            {
+                // The MediaSource supports direct stream, now test to see if the client supports it
+                var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
+                    streamBuilder.BuildAudioItem(options) :
+                    streamBuilder.BuildVideoItem(options);
+
+                if (streamInfo == null || !streamInfo.IsDirectStream)
+                {
+                    mediaSource.SupportsDirectStream = false;
+                }
+
+                if (streamInfo != null)
+                {
+                    SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, baseUrl, auth.Token);
+                }
+            }
+
+            if (mediaSource.SupportsTranscoding)
+            {
+                // The MediaSource supports direct stream, now test to see if the client supports it
+                var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
+                    streamBuilder.BuildAudioItem(options) :
+                    streamBuilder.BuildVideoItem(options);
+
+                if (streamInfo != null && streamInfo.PlayMethod == PlayMethod.Transcode)
+                {
+                    streamInfo.StartPositionTicks = startTimeTicks;
+                    mediaSource.TranscodingUrl = streamInfo.ToUrl(baseUrl, auth.Token);
+                    mediaSource.TranscodingContainer = streamInfo.Container;
+                    mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+                }
+
+                if (streamInfo != null)
+                {
+                    SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, baseUrl, auth.Token);
+                }
+            }
+        }
+
+        private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string baseUrl, string accessToken)
+        {
+            var profiles = info.GetSubtitleProfiles(false, baseUrl, accessToken);
+            mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
+
+            foreach (var profile in profiles)
+            {
+                foreach (var stream in mediaSource.MediaStreams)
+                {
+                    if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
+                    {
+                        stream.DeliveryMethod = profile.DeliveryMethod;
+
+                        if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
+                        {
+                            stream.DeliveryUrl = profile.Url;
+                        }
+                    }
+                }
+            }
+        }
+
+        private void SortMediaSources(PlaybackInfoResponse result)
+        {
+            var originalList = result.MediaSources.ToList();
+
+            result.MediaSources = result.MediaSources.OrderBy(i =>
+            {
+                // Nothing beats direct playing a file
+                if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
+                {
+                    return 0;
+                }
+
+                return 1;
+
+            }).ThenBy(i =>
+            {
+                // Let's assume direct streaming a file is just as desirable as direct playing a remote url
+                if (i.SupportsDirectPlay || i.SupportsDirectStream)
+                {
+                    return 0;
+                }
+
+                return 1;
+
+            }).ThenBy(i =>
+            {
+                switch (i.Protocol)
+                {
+                    case MediaProtocol.File:
+                        return 0;
+                    default:
+                        return 1;
+                }
+
+            }).ThenBy(originalList.IndexOf)
+            .ToList();
+        }
     }
     }
 }
 }

+ 3 - 3
MediaBrowser.Api/Playback/Progressive/AudioService.cs

@@ -31,7 +31,7 @@ namespace MediaBrowser.Api.Playback.Progressive
     /// </summary>
     /// </summary>
     public class AudioService : BaseProgressiveStreamingService
     public class AudioService : BaseProgressiveStreamingService
     {
     {
-        public AudioService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, imageProcessor, httpClient)
+        public AudioService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, imageProcessor, httpClient)
         {
         {
         }
         }
 
 
@@ -55,7 +55,7 @@ namespace MediaBrowser.Api.Playback.Progressive
             return ProcessRequest(request, true);
             return ProcessRequest(request, true);
         }
         }
 
 
-        protected override string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding)
+        protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
         {
         {
             var audioTranscodeParams = new List<string>();
             var audioTranscodeParams = new List<string>();
 
 
@@ -84,7 +84,7 @@ namespace MediaBrowser.Api.Playback.Progressive
 
 
             return string.Format("{0} {1} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1 -y \"{5}\"",
             return string.Format("{0} {1} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1 -y \"{5}\"",
                 inputModifier,
                 inputModifier,
-                GetInputArgument(transcodingJobId, state),
+                GetInputArgument(state),
                 threads,
                 threads,
                 vn,
                 vn,
                 string.Join(" ", audioTranscodeParams.ToArray()),
                 string.Join(" ", audioTranscodeParams.ToArray()),

+ 1 - 1
MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs

@@ -27,7 +27,7 @@ namespace MediaBrowser.Api.Playback.Progressive
         protected readonly IImageProcessor ImageProcessor;
         protected readonly IImageProcessor ImageProcessor;
         protected readonly IHttpClient HttpClient;
         protected readonly IHttpClient HttpClient;
 
 
-        protected BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient)
+        protected BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient)
         {
         {
             ImageProcessor = imageProcessor;
             ImageProcessor = imageProcessor;
             HttpClient = httpClient;
             HttpClient = httpClient;

+ 3 - 3
MediaBrowser.Api/Playback/Progressive/VideoService.cs

@@ -62,7 +62,7 @@ namespace MediaBrowser.Api.Playback.Progressive
     /// </summary>
     /// </summary>
     public class VideoService : BaseProgressiveStreamingService
     public class VideoService : BaseProgressiveStreamingService
     {
     {
-        public VideoService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, imageProcessor, httpClient)
+        public VideoService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, imageProcessor, httpClient)
         {
         {
         }
         }
 
 
@@ -86,7 +86,7 @@ namespace MediaBrowser.Api.Playback.Progressive
             return ProcessRequest(request, true);
             return ProcessRequest(request, true);
         }
         }
 
 
-        protected override string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding)
+        protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
         {
         {
             // Get the output codec name
             // Get the output codec name
             var videoCodec = state.OutputVideoCodec;
             var videoCodec = state.OutputVideoCodec;
@@ -106,7 +106,7 @@ namespace MediaBrowser.Api.Playback.Progressive
 
 
             return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -threads {5} {6}{7} -y \"{8}\"",
             return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -threads {5} {6}{7} -y \"{8}\"",
                 inputModifier,
                 inputModifier,
-                GetInputArgument(transcodingJobId, state),
+                GetInputArgument(state),
                 keyFrame,
                 keyFrame,
                 GetMapArgs(state),
                 GetMapArgs(state),
                 GetVideoArguments(state, videoCodec),
                 GetVideoArguments(state, videoCodec),

+ 2 - 3
MediaBrowser.Api/Playback/StreamRequest.cs

@@ -71,9 +71,8 @@ namespace MediaBrowser.Api.Playback
 
 
         public string Params { get; set; }
         public string Params { get; set; }
         public string ClientTime { get; set; }
         public string ClientTime { get; set; }
-        public string StreamId { get; set; }
-
-        public string TranscodingJobId { get; set; }
+        public string PlaySessionId { get; set; }
+        public string LiveStreamId { get; set; }
     }
     }
 
 
     public class VideoStreamRequest : StreamRequest
     public class VideoStreamRequest : StreamRequest

+ 45 - 14
MediaBrowser.Api/Playback/StreamState.cs

@@ -1,6 +1,7 @@
-using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
@@ -17,7 +18,7 @@ namespace MediaBrowser.Api.Playback
     public class StreamState : IDisposable
     public class StreamState : IDisposable
     {
     {
         private readonly ILogger _logger;
         private readonly ILogger _logger;
-        private readonly ILiveTvManager _liveTvManager;
+        private readonly IMediaSourceManager _mediaSourceManager;
 
 
         public string RequestedUrl { get; set; }
         public string RequestedUrl { get; set; }
 
 
@@ -39,7 +40,7 @@ namespace MediaBrowser.Api.Playback
 
 
         public string InputContainer { get; set; }
         public string InputContainer { get; set; }
 
 
-        public List<MediaStream> AllMediaStreams { get; set; }
+        public MediaSourceInfo MediaSource { get; set; }
         
         
         public MediaStream AudioStream { get; set; }
         public MediaStream AudioStream { get; set; }
         public MediaStream VideoStream { get; set; }
         public MediaStream VideoStream { get; set; }
@@ -64,8 +65,6 @@ namespace MediaBrowser.Api.Playback
 
 
         public List<string> PlayableStreamFileNames { get; set; }
         public List<string> PlayableStreamFileNames { get; set; }
 
 
-        public string LiveTvStreamId { get; set; }
-
         public int SegmentLength = 3;
         public int SegmentLength = 3;
         public bool EnableGenericHlsSegmenter = false;
         public bool EnableGenericHlsSegmenter = false;
         public int HlsListSize
         public int HlsListSize
@@ -86,14 +85,13 @@ namespace MediaBrowser.Api.Playback
 
 
         public List<string> SupportedAudioCodecs { get; set; }
         public List<string> SupportedAudioCodecs { get; set; }
 
 
-        public StreamState(ILiveTvManager liveTvManager, ILogger logger)
+        public StreamState(IMediaSourceManager mediaSourceManager, ILogger logger)
         {
         {
-            _liveTvManager = liveTvManager;
+            _mediaSourceManager = mediaSourceManager;
             _logger = logger;
             _logger = logger;
             SupportedAudioCodecs = new List<string>();
             SupportedAudioCodecs = new List<string>();
             PlayableStreamFileNames = new List<string>();
             PlayableStreamFileNames = new List<string>();
             RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
             RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-            AllMediaStreams = new List<MediaStream>();
         }
         }
 
 
         public string InputAudioSync { get; set; }
         public string InputAudioSync { get; set; }
@@ -113,9 +111,6 @@ namespace MediaBrowser.Api.Playback
 
 
         public long? EncodingDurationTicks { get; set; }
         public long? EncodingDurationTicks { get; set; }
 
 
-        public string ItemType { get; set; }
-        public string ItemId { get; set; }
-
         public string GetMimeType(string outputPath)
         public string GetMimeType(string outputPath)
         {
         {
             if (!string.IsNullOrEmpty(MimeType))
             if (!string.IsNullOrEmpty(MimeType))
@@ -187,15 +182,15 @@ namespace MediaBrowser.Api.Playback
 
 
         private async void DisposeLiveStream()
         private async void DisposeLiveStream()
         {
         {
-            if (!string.IsNullOrEmpty(LiveTvStreamId))
+            if (MediaSource.RequiresClosing && string.IsNullOrWhiteSpace(Request.LiveStreamId))
             {
             {
                 try
                 try
                 {
                 {
-                    await _liveTvManager.CloseLiveStream(LiveTvStreamId, CancellationToken.None).ConfigureAwait(false);
+                    await _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId, CancellationToken.None).ConfigureAwait(false);
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {
-                    _logger.ErrorException("Error closing live tv stream", ex);
+                    _logger.ErrorException("Error closing media source", ex);
                 }
                 }
             }
             }
         }
         }
@@ -351,6 +346,42 @@ namespace MediaBrowser.Api.Playback
             }
             }
         }
         }
 
 
+        public int? TargetVideoStreamCount
+        {
+            get
+            {
+                if (Request.Static)
+                {
+                    return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue);
+                }
+                return GetMediaStreamCount(MediaStreamType.Video, 1);
+            }
+        }
+
+        public int? TargetAudioStreamCount
+        {
+            get
+            {
+                if (Request.Static)
+                {
+                    return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue);
+                }
+                return GetMediaStreamCount(MediaStreamType.Audio, 1);
+            }
+        }
+
+        private int? GetMediaStreamCount(MediaStreamType type, int limit)
+        {
+            var count = MediaSource.GetStreamCount(type);
+
+            if (count.HasValue)
+            {
+                count = Math.Min(count.Value, limit);
+            }
+
+            return count;
+        }
+
         /// <summary>
         /// <summary>
         /// Predicts the audio sample rate that will be in the output stream
         /// Predicts the audio sample rate that will be in the output stream
         /// </summary>
         /// </summary>

+ 18 - 8
MediaBrowser.Api/Playback/TranscodingThrottler.cs

@@ -1,4 +1,6 @@
-using MediaBrowser.Model.Logging;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Logging;
 using System;
 using System;
 using System.IO;
 using System.IO;
 using System.Threading;
 using System.Threading;
@@ -11,13 +13,18 @@ namespace MediaBrowser.Api.Playback
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private Timer _timer;
         private Timer _timer;
         private bool _isPaused;
         private bool _isPaused;
+        private readonly IConfigurationManager _config;
 
 
-        private readonly long _gapLengthInTicks = TimeSpan.FromMinutes(2).Ticks;
-
-        public TranscodingThrottler(TranscodingJob job, ILogger logger)
+        public TranscodingThrottler(TranscodingJob job, ILogger logger, IConfigurationManager config)
         {
         {
             _job = job;
             _job = job;
             _logger = logger;
             _logger = logger;
+            _config = config;
+        }
+
+        private EncodingOptions GetOptions()
+        {
+            return _config.GetConfiguration<EncodingOptions>("encoding");
         }
         }
 
 
         public void Start()
         public void Start()
@@ -33,7 +40,9 @@ namespace MediaBrowser.Api.Playback
                 return;
                 return;
             }
             }
 
 
-            if (IsThrottleAllowed(_job))
+            var options = GetOptions();
+
+            if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleThresholdSeconds))
             {
             {
                 PauseTranscoding();
                 PauseTranscoding();
             }
             }
@@ -79,19 +88,20 @@ namespace MediaBrowser.Api.Playback
             }
             }
         }
         }
 
 
-        private bool IsThrottleAllowed(TranscodingJob job)
+        private bool IsThrottleAllowed(TranscodingJob job, int thresholdSeconds)
         {
         {
             var bytesDownloaded = job.BytesDownloaded ?? 0;
             var bytesDownloaded = job.BytesDownloaded ?? 0;
             var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
             var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
             var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
             var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
 
 
             var path = job.Path;
             var path = job.Path;
+            var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks;
 
 
             if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
             if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
             {
             {
                 // HLS - time-based consideration
                 // HLS - time-based consideration
 
 
-                var targetGap = _gapLengthInTicks;
+                var targetGap = gapLengthInTicks;
                 var gap = transcodingPositionTicks - downloadPositionTicks;
                 var gap = transcodingPositionTicks - downloadPositionTicks;
 
 
                 if (gap < targetGap)
                 if (gap < targetGap)
@@ -113,7 +123,7 @@ namespace MediaBrowser.Api.Playback
                     var bytesTranscoded = job.BytesTranscoded ?? new FileInfo(path).Length;
                     var bytesTranscoded = job.BytesTranscoded ?? new FileInfo(path).Length;
 
 
                     // Estimate the bytes the transcoder should be ahead
                     // Estimate the bytes the transcoder should be ahead
-                    double gapFactor = _gapLengthInTicks;
+                    double gapFactor = gapLengthInTicks;
                     gapFactor /= transcodingPositionTicks;
                     gapFactor /= transcodingPositionTicks;
                     var targetGap = bytesTranscoded * gapFactor;
                     var targetGap = bytesTranscoded * gapFactor;
 
 

+ 1 - 1
MediaBrowser.Api/Subtitles/SubtitleService.cs

@@ -192,7 +192,7 @@ namespace MediaBrowser.Api.Subtitles
             {
             {
                 var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
                 var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
 
 
-                var mediaSource = item.GetMediaSources(false)
+                var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false, null)
                     .First(i => string.Equals(i.Id, request.MediaSourceId ?? request.Id));
                     .First(i => string.Equals(i.Id, request.MediaSourceId ?? request.Id));
 
 
                 var subtitleStream = mediaSource.MediaStreams
                 var subtitleStream = mediaSource.MediaStreams

+ 44 - 22
MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs

@@ -55,21 +55,29 @@ namespace MediaBrowser.Api.UserLibrary
         /// <returns>Task{ItemsResult}.</returns>
         /// <returns>Task{ItemsResult}.</returns>
         protected ItemsResult GetResult(GetItemsByName request)
         protected ItemsResult GetResult(GetItemsByName request)
         {
         {
+            var dtoOptions = GetDtoOptions(request);
+
             User user = null;
             User user = null;
             BaseItem parentItem;
             BaseItem parentItem;
-            List<BaseItem> libraryItems;
+            List<BaseItem> libraryItems = null;
 
 
             if (request.UserId.HasValue)
             if (request.UserId.HasValue)
             {
             {
                 user = UserManager.GetUserById(request.UserId.Value);
                 user = UserManager.GetUserById(request.UserId.Value);
                 parentItem = string.IsNullOrEmpty(request.ParentId) ? user.RootFolder : LibraryManager.GetItemById(request.ParentId);
                 parentItem = string.IsNullOrEmpty(request.ParentId) ? user.RootFolder : LibraryManager.GetItemById(request.ParentId);
-                libraryItems = user.RootFolder.GetRecursiveChildren(user).ToList();
 
 
+                if (RequiresLibraryItems(request, dtoOptions))
+                {
+                    libraryItems = user.RootFolder.GetRecursiveChildren(user).ToList();
+                }
             }
             }
             else
             else
             {
             {
                 parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId);
                 parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId);
-                libraryItems = LibraryManager.RootFolder.GetRecursiveChildren().ToList();
+                if (RequiresLibraryItems(request, dtoOptions))
+                {
+                    libraryItems = LibraryManager.RootFolder.GetRecursiveChildren().ToList();
+                }
             }
             }
 
 
             IEnumerable<BaseItem> items;
             IEnumerable<BaseItem> items;
@@ -133,17 +141,46 @@ namespace MediaBrowser.Api.UserLibrary
 
 
             }
             }
 
 
-            var tuples = ibnItems.Select(i => new Tuple<TItemType, List<BaseItem>>(i, i.GetTaggedItems(libraryItems).ToList()));
-
-            var dtoOptions = GetDtoOptions(request);
+            IEnumerable<Tuple<TItemType, List<BaseItem>>> tuples;
+            if (dtoOptions.Fields.Contains(ItemFields.ItemCounts) || true)
+            {
+                tuples = ibnItems.Select(i => new Tuple<TItemType, List<BaseItem>>(i, i.GetTaggedItems(libraryItems).ToList()));
+            }
+            else
+            {
+                tuples = ibnItems.Select(i => new Tuple<TItemType, List<BaseItem>>(i, new List<BaseItem>()));
+            }
 
 
-            var dtos = tuples.Select(i => GetDto(i.Item1, user, dtoOptions, i.Item2));
+            var dtos = tuples.Select(i => DtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, user));
 
 
             result.Items = dtos.Where(i => i != null).ToArray();
             result.Items = dtos.Where(i => i != null).ToArray();
 
 
             return result;
             return result;
         }
         }
 
 
+        private bool RequiresLibraryItems(GetItemsByName request, DtoOptions options)
+        {
+            var filters = request.GetFilters().ToList();
+
+            if (filters.Contains(ItemFilter.IsPlayed))
+            {
+                return true;
+            }
+
+            if (filters.Contains(ItemFilter.IsUnplayed))
+            {
+                return true;
+            }
+
+            if (request.IsPlayed.HasValue)
+            {
+                return true;
+            }
+
+            return true;
+            return options.Fields.Contains(ItemFields.ItemCounts);
+        }
+
         private IEnumerable<TItemType> FilterByLibraryItems(GetItemsByName request, IEnumerable<TItemType> items, User user, IEnumerable<BaseItem> libraryItems)
         private IEnumerable<TItemType> FilterByLibraryItems(GetItemsByName request, IEnumerable<TItemType> items, User user, IEnumerable<BaseItem> libraryItems)
         {
         {
             var filters = request.GetFilters().ToList();
             var filters = request.GetFilters().ToList();
@@ -340,21 +377,6 @@ namespace MediaBrowser.Api.UserLibrary
         /// <param name="items">The items.</param>
         /// <param name="items">The items.</param>
         /// <returns>IEnumerable{Task{`0}}.</returns>
         /// <returns>IEnumerable{Task{`0}}.</returns>
         protected abstract IEnumerable<TItemType> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items);
         protected abstract IEnumerable<TItemType> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items);
-
-        /// <summary>
-        /// Gets the dto.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="user">The user.</param>
-        /// <param name="options">The options.</param>
-        /// <param name="libraryItems">The library items.</param>
-        /// <returns>Task{DtoBaseItem}.</returns>
-        private BaseItemDto GetDto(TItemType item, User user, DtoOptions options, List<BaseItem> libraryItems)
-        {
-            var dto = DtoService.GetItemByNameDto(item, options, libraryItems, user);
-
-            return dto;
-        }
     }
     }
 
 
     /// <summary>
     /// <summary>

+ 8 - 0
MediaBrowser.Api/UserLibrary/UserLibraryService.cs

@@ -304,6 +304,14 @@ namespace MediaBrowser.Api.UserLibrary
         {
         {
             var user = _userManager.GetUserById(request.UserId);
             var user = _userManager.GetUserById(request.UserId);
 
 
+            if (!request.IsPlayed.HasValue)
+            {
+                if (user.Configuration.HidePlayedInLatest)
+                {
+                    request.IsPlayed = false;
+                }
+            }
+
             var list = _userViewManager.GetLatestItems(new LatestItemsQuery
             var list = _userViewManager.GetLatestItems(new LatestItemsQuery
             {
             {
                 GroupItems = request.GroupItems,
                 GroupItems = request.GroupItems,

+ 21 - 11
MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs

@@ -107,30 +107,40 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
 
 
         private WebRequest GetRequest(HttpRequestOptions options, string method, bool enableHttpCompression)
         private WebRequest GetRequest(HttpRequestOptions options, string method, bool enableHttpCompression)
         {
         {
-            var request = (HttpWebRequest)WebRequest.Create(options.Url);
+            var request = WebRequest.Create(options.Url);
+            var httpWebRequest = request as HttpWebRequest;
 
 
-            AddRequestHeaders(request, options);
+            if (httpWebRequest != null)
+            {
+                AddRequestHeaders(httpWebRequest, options);
 
 
-            request.AutomaticDecompression = enableHttpCompression ? DecompressionMethods.Deflate : DecompressionMethods.None;
+                httpWebRequest.AutomaticDecompression = enableHttpCompression ? DecompressionMethods.Deflate : DecompressionMethods.None;
+            }
 
 
             request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
             request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
 
 
-            if (options.EnableKeepAlive)
+            if (httpWebRequest != null)
             {
             {
-                request.KeepAlive = true;
+                if (options.EnableKeepAlive)
+                {
+                    httpWebRequest.KeepAlive = true;
+                }
             }
             }
 
 
             request.Method = method;
             request.Method = method;
             request.Timeout = options.TimeoutMs;
             request.Timeout = options.TimeoutMs;
 
 
-            if (!string.IsNullOrEmpty(options.Host))
+            if (httpWebRequest != null)
             {
             {
-                request.Host = options.Host;
-            }
+                if (!string.IsNullOrEmpty(options.Host))
+                {
+                    httpWebRequest.Host = options.Host;
+                }
 
 
-            if (!string.IsNullOrEmpty(options.Referer))
-            {
-                request.Referer = options.Referer;
+                if (!string.IsNullOrEmpty(options.Referer))
+                {
+                    httpWebRequest.Referer = options.Referer;
+                }
             }
             }
 
 
             //request.ServicePoint.BindIPEndPointDelegate = BindIPEndPointCallback;
             //request.ServicePoint.BindIPEndPointDelegate = BindIPEndPointCallback;

+ 20 - 17
MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs

@@ -6,6 +6,7 @@ using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Tasks;
 using System;
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -29,7 +30,8 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         /// <summary>
         /// <summary>
         /// The _task queue
         /// The _task queue
         /// </summary>
         /// </summary>
-        private readonly SortedDictionary<Type, TaskExecutionOptions> _taskQueue = new SortedDictionary<Type, TaskExecutionOptions>();
+        private readonly ConcurrentQueue<Tuple<Type, TaskExecutionOptions>> _taskQueue =
+            new ConcurrentQueue<Tuple<Type, TaskExecutionOptions>>();
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the json serializer.
         /// Gets or sets the json serializer.
@@ -136,25 +138,17 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         {
         {
             var type = task.ScheduledTask.GetType();
             var type = task.ScheduledTask.GetType();
 
 
+            Logger.Info("Queueing task {0}", type.Name);
+
             lock (_taskQueue)
             lock (_taskQueue)
             {
             {
-                // If it's idle just execute immediately
                 if (task.State == TaskState.Idle)
                 if (task.State == TaskState.Idle)
                 {
                 {
                     Execute(task, options);
                     Execute(task, options);
                     return;
                     return;
                 }
                 }
 
 
-                if (!_taskQueue.ContainsKey(type))
-                {
-                    Logger.Info("Queueing task {0}", type.Name);
-                    _taskQueue.Add(type, options);
-                }
-                else
-                {
-                    _taskQueue[type] = options;
-                    Logger.Info("Task already queued: {0}", type.Name);
-                }
+                _taskQueue.Enqueue(new Tuple<Type, TaskExecutionOptions>(type, options));
             }
             }
         }
         }
 
 
@@ -241,15 +235,24 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
             // Execute queued tasks
             // Execute queued tasks
             lock (_taskQueue)
             lock (_taskQueue)
             {
             {
-                foreach (var enqueuedType in _taskQueue.ToList())
+                var list = new List<Tuple<Type, TaskExecutionOptions>>();
+
+                Tuple<Type, TaskExecutionOptions> item;
+                while (_taskQueue.TryDequeue(out item))
                 {
                 {
-                    var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == enqueuedType.Key);
+                    if (list.All(i => i.Item1 != item.Item1))
+                    {
+                        list.Add(item);
+                    }
+                }
+
+                foreach (var enqueuedType in list)
+                {
+                    var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == enqueuedType.Item1);
 
 
                     if (scheduledTask.State == TaskState.Idle)
                     if (scheduledTask.State == TaskState.Idle)
                     {
                     {
-                        Execute(scheduledTask, enqueuedType.Value);
-
-                        _taskQueue.Remove(enqueuedType.Key);
+                        Execute(scheduledTask, enqueuedType.Item2);
                     }
                     }
                 }
                 }
             }
             }

+ 11 - 5
MediaBrowser.Controller/Channels/ChannelAudioItem.cs

@@ -75,17 +75,23 @@ namespace MediaBrowser.Controller.Channels
 
 
         public override IEnumerable<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
         public override IEnumerable<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
         {
         {
-            var list = base.GetMediaSources(enablePathSubstitution).ToList();
-
-            var sources = ChannelManager.GetChannelItemMediaSources(Id.ToString("N"), false, CancellationToken.None)
-                    .Result.ToList();
+            var sources = ChannelManager.GetStaticMediaSources(this, false, CancellationToken.None)
+                       .Result.ToList();
 
 
             if (sources.Count > 0)
             if (sources.Count > 0)
             {
             {
                 return sources;
                 return sources;
             }
             }
 
 
-            list.InsertRange(0, sources);
+            var list = base.GetMediaSources(enablePathSubstitution).ToList();
+
+            foreach (var mediaSource in list)
+            {
+                if (string.IsNullOrWhiteSpace(mediaSource.Path))
+                {
+                    mediaSource.Type = MediaSourceType.Placeholder;
+                }
+            }
 
 
             return list;
             return list;
         }
         }

+ 5 - 1
MediaBrowser.Controller/Channels/ChannelMediaInfo.cs

@@ -38,6 +38,7 @@ namespace MediaBrowser.Controller.Channels
         public string Id { get; set; }
         public string Id { get; set; }
 
 
         public bool ReadAtNativeFramerate { get; set; }
         public bool ReadAtNativeFramerate { get; set; }
+        public bool SupportsDirectPlay { get; set; }
 
 
         public ChannelMediaInfo()
         public ChannelMediaInfo()
         {
         {
@@ -45,6 +46,7 @@ namespace MediaBrowser.Controller.Channels
 
 
             // This is most common
             // This is most common
             Protocol = MediaProtocol.Http;
             Protocol = MediaProtocol.Http;
+            SupportsDirectPlay = true;
         }
         }
 
 
         public MediaSourceInfo ToMediaSource()
         public MediaSourceInfo ToMediaSource()
@@ -62,7 +64,9 @@ namespace MediaBrowser.Controller.Channels
                 RunTimeTicks = RunTimeTicks,
                 RunTimeTicks = RunTimeTicks,
                 Name = id,
                 Name = id,
                 Id = id,
                 Id = id,
-                ReadAtNativeFramerate = ReadAtNativeFramerate
+                ReadAtNativeFramerate = ReadAtNativeFramerate,
+                SupportsDirectStream = Protocol == MediaProtocol.File || Protocol == MediaProtocol.Http,
+                SupportsDirectPlay = SupportsDirectPlay
             };
             };
 
 
             var bitrate = (AudioBitrate ?? 0) + (VideoBitrate ?? 0);
             var bitrate = (AudioBitrate ?? 0) + (VideoBitrate ?? 0);

+ 11 - 5
MediaBrowser.Controller/Channels/ChannelVideoItem.cs

@@ -90,17 +90,23 @@ namespace MediaBrowser.Controller.Channels
 
 
         public override IEnumerable<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
         public override IEnumerable<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
         {
         {
-            var list = base.GetMediaSources(enablePathSubstitution).ToList();
-
-            var sources = ChannelManager.GetChannelItemMediaSources(Id.ToString("N"), false, CancellationToken.None)
-                    .Result.ToList();
+            var sources = ChannelManager.GetStaticMediaSources(this, false, CancellationToken.None)
+                       .Result.ToList();
 
 
             if (sources.Count > 0)
             if (sources.Count > 0)
             {
             {
                 return sources;
                 return sources;
             }
             }
 
 
-            list.InsertRange(0, sources);
+            var list = base.GetMediaSources(enablePathSubstitution).ToList();
+
+            foreach (var mediaSource in list)
+            {
+                if (string.IsNullOrWhiteSpace(mediaSource.Path))
+                {
+                    mediaSource.Type = MediaSourceType.Placeholder;
+                }
+            }
 
 
             return list;
             return list;
         }
         }

+ 3 - 3
MediaBrowser.Controller/Channels/IChannelManager.cs

@@ -112,11 +112,11 @@ namespace MediaBrowser.Controller.Channels
         /// <summary>
         /// <summary>
         /// Gets the channel item media sources.
         /// Gets the channel item media sources.
         /// </summary>
         /// </summary>
-        /// <param name="id">The identifier.</param>
-        /// <param name="includeDynamicSources">if set to <c>true</c> [include dynamic sources].</param>
+        /// <param name="item">The item.</param>
+        /// <param name="includeCachedVersions">if set to <c>true</c> [include cached versions].</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{IEnumerable{MediaSourceInfo}}.</returns>
         /// <returns>Task{IEnumerable{MediaSourceInfo}}.</returns>
-        Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSources(string id, bool includeDynamicSources, CancellationToken cancellationToken);
+        Task<IEnumerable<MediaSourceInfo>> GetStaticMediaSources(IChannelMediaItem item, bool includeCachedVersions, CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
         /// Gets the channel folder.
         /// Gets the channel folder.

+ 2 - 1
MediaBrowser.Controller/Entities/Video.cs

@@ -501,7 +501,8 @@ namespace MediaBrowser.Controller.Entities
                 Formats = (i.FormatName ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(),
                 Formats = (i.FormatName ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(),
                 Timestamp = i.Timestamp,
                 Timestamp = i.Timestamp,
                 Type = type,
                 Type = type,
-                PlayableStreamFileNames = i.PlayableStreamFileNames.ToList()
+                PlayableStreamFileNames = i.PlayableStreamFileNames.ToList(),
+                SupportsDirectStream = i.VideoType == VideoType.VideoFile
             };
             };
 
 
             if (i.IsShortcut)
             if (i.IsShortcut)

+ 42 - 0
MediaBrowser.Controller/Library/IMediaSourceManager.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading;
@@ -64,6 +65,14 @@ namespace MediaBrowser.Controller.Library
         /// <returns>IEnumerable&lt;MediaSourceInfo&gt;.</returns>
         /// <returns>IEnumerable&lt;MediaSourceInfo&gt;.</returns>
         IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user);
         IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user);
 
 
+        /// <summary>
+        /// Gets the static media sources.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
+        /// <returns>IEnumerable&lt;MediaSourceInfo&gt;.</returns>
+        IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution);
+        
         /// <summary>
         /// <summary>
         /// Gets the static media source.
         /// Gets the static media source.
         /// </summary>
         /// </summary>
@@ -72,5 +81,38 @@ namespace MediaBrowser.Controller.Library
         /// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
         /// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
         /// <returns>MediaSourceInfo.</returns>
         /// <returns>MediaSourceInfo.</returns>
         MediaSourceInfo GetStaticMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution);
         MediaSourceInfo GetStaticMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution);
+
+        /// <summary>
+        /// Opens the media source.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="enableAutoClose">if set to <c>true</c> [enable automatic close].</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task&lt;MediaSourceInfo&gt;.</returns>
+        Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, bool enableAutoClose, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the live stream.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task&lt;MediaSourceInfo&gt;.</returns>
+        Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken);
+        
+        /// <summary>
+        /// Pings the media source.
+        /// </summary>
+        /// <param name="id">The live stream identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task PingLiveStream(string id, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Closes the media source.
+        /// </summary>
+        /// <param name="id">The live stream identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task CloseLiveStream(string id, CancellationToken cancellationToken);
     }
     }
 }
 }

+ 16 - 0
MediaBrowser.Controller/Library/IMediaSourceProvider.cs

@@ -15,5 +15,21 @@ namespace MediaBrowser.Controller.Library
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task&lt;IEnumerable&lt;MediaSourceInfo&gt;&gt;.</returns>
         /// <returns>Task&lt;IEnumerable&lt;MediaSourceInfo&gt;&gt;.</returns>
         Task<IEnumerable<MediaSourceInfo>> GetMediaSources(IHasMediaSources item, CancellationToken cancellationToken);
         Task<IEnumerable<MediaSourceInfo>> GetMediaSources(IHasMediaSources item, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Opens the media source.
+        /// </summary>
+        /// <param name="openToken">The open token.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task&lt;MediaSourceInfo&gt;.</returns>
+        Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Closes the media source.
+        /// </summary>
+        /// <param name="liveStreamId">The live stream identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken);
     }
     }
 }
 }

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

@@ -1,8 +1,10 @@
-
+using System;
+
 namespace MediaBrowser.Controller.LiveTv
 namespace MediaBrowser.Controller.LiveTv
 {
 {
     public interface ILiveTvItem
     public interface ILiveTvItem
     {
     {
+        Guid Id { get; }
         string ServiceName { get; set; }
         string ServiceName { get; set; }
     }
     }
 }
 }

+ 16 - 0
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -301,5 +301,21 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task&lt;QueryResult&lt;BaseItem&gt;&gt;.</returns>
         /// <returns>Task&lt;QueryResult&lt;BaseItem&gt;&gt;.</returns>
         Task<QueryResult<BaseItem>> GetInternalRecordings(RecordingQuery query, CancellationToken cancellationToken);
         Task<QueryResult<BaseItem>> GetInternalRecordings(RecordingQuery query, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the recording media sources.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task&lt;IEnumerable&lt;MediaSourceInfo&gt;&gt;.</returns>
+        Task<IEnumerable<MediaSourceInfo>> GetRecordingMediaSources(string id, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the channel media sources.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task&lt;IEnumerable&lt;MediaSourceInfo&gt;&gt;.</returns>
+        Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(string id, CancellationToken cancellationToken);
     }
     }
 }
 }

+ 19 - 2
MediaBrowser.Controller/LiveTv/LiveTvAudioRecording.cs

@@ -1,10 +1,12 @@
-using System.Runtime.Serialization;
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Users;
 using MediaBrowser.Model.Users;
+using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
+using System.Runtime.Serialization;
 
 
 namespace MediaBrowser.Controller.LiveTv
 namespace MediaBrowser.Controller.LiveTv
 {
 {
@@ -99,5 +101,20 @@ namespace MediaBrowser.Controller.LiveTv
         {
         {
             return user.Policy.EnableLiveTvManagement;
             return user.Policy.EnableLiveTvManagement;
         }
         }
+
+        public override IEnumerable<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
+        {
+            var list = base.GetMediaSources(enablePathSubstitution).ToList();
+
+            foreach (var mediaSource in list)
+            {
+                if (string.IsNullOrWhiteSpace(mediaSource.Path))
+                {
+                    mediaSource.Type = MediaSourceType.Placeholder;
+                }
+            }
+
+            return list;
+        }
     }
     }
 }
 }

+ 10 - 1
MediaBrowser.Controller/LiveTv/LiveTvChannel.cs

@@ -82,6 +82,15 @@ namespace MediaBrowser.Controller.LiveTv
         /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value>
         /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value>
         public bool? HasProviderImage { get; set; }
         public bool? HasProviderImage { get; set; }
 
 
+        public override LocationType LocationType
+        {
+            get
+            {
+                // TODO: This should be removed
+                return LocationType.Remote;
+            }
+        }
+
         protected override string CreateSortName()
         protected override string CreateSortName()
         {
         {
             double number = 0;
             double number = 0;
@@ -127,7 +136,7 @@ namespace MediaBrowser.Controller.LiveTv
                 Name = Name,
                 Name = Name,
                 Path = Path,
                 Path = Path,
                 RunTimeTicks = RunTimeTicks,
                 RunTimeTicks = RunTimeTicks,
-                Type = MediaSourceType.Default
+                Type = MediaSourceType.Placeholder
             };
             };
 
 
             list.Add(info);
             list.Add(info);

+ 6 - 0
MediaBrowser.Controller/LiveTv/LiveTvProgram.cs

@@ -34,6 +34,12 @@ namespace MediaBrowser.Controller.LiveTv
         /// <value>The channel identifier.</value>
         /// <value>The channel identifier.</value>
         public string ExternalChannelId { get; set; }
         public string ExternalChannelId { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the original air date.
+        /// </summary>
+        /// <value>The original air date.</value>
+        public DateTime? OriginalAirDate { get; set; }
+        
         /// <summary>
         /// <summary>
         /// Gets or sets the type of the channel.
         /// Gets or sets the type of the channel.
         /// </summary>
         /// </summary>

+ 20 - 3
MediaBrowser.Controller/LiveTv/LiveTvVideoRecording.cs

@@ -1,9 +1,11 @@
-using System.Runtime.Serialization;
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
-using System.Linq;
 using MediaBrowser.Model.Users;
 using MediaBrowser.Model.Users;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
 
 
 namespace MediaBrowser.Controller.LiveTv
 namespace MediaBrowser.Controller.LiveTv
 {
 {
@@ -97,5 +99,20 @@ namespace MediaBrowser.Controller.LiveTv
         {
         {
             return user.Policy.EnableLiveTvManagement;
             return user.Policy.EnableLiveTvManagement;
         }
         }
+
+        public override IEnumerable<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
+        {
+            var list = base.GetMediaSources(enablePathSubstitution).ToList();
+
+            foreach (var mediaSource in list)
+            {
+                if (string.IsNullOrWhiteSpace(mediaSource.Path))
+                {
+                    mediaSource.Type = MediaSourceType.Placeholder;
+                }
+            }
+
+            return list;
+        }
     }
     }
 }
 }

+ 2 - 1
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -393,12 +393,13 @@
     <Compile Include="Subtitles\SubtitleDownloadEventArgs.cs" />
     <Compile Include="Subtitles\SubtitleDownloadEventArgs.cs" />
     <Compile Include="Subtitles\SubtitleResponse.cs" />
     <Compile Include="Subtitles\SubtitleResponse.cs" />
     <Compile Include="Subtitles\SubtitleSearchRequest.cs" />
     <Compile Include="Subtitles\SubtitleSearchRequest.cs" />
+    <Compile Include="Sync\IHasDynamicAccess.cs" />
     <Compile Include="Sync\IServerSyncProvider.cs" />
     <Compile Include="Sync\IServerSyncProvider.cs" />
     <Compile Include="Sync\ISyncDataProvider.cs" />
     <Compile Include="Sync\ISyncDataProvider.cs" />
     <Compile Include="Sync\ISyncManager.cs" />
     <Compile Include="Sync\ISyncManager.cs" />
     <Compile Include="Sync\ISyncProvider.cs" />
     <Compile Include="Sync\ISyncProvider.cs" />
     <Compile Include="Sync\ISyncRepository.cs" />
     <Compile Include="Sync\ISyncRepository.cs" />
-    <Compile Include="Sync\SendFileResult.cs" />
+    <Compile Include="Sync\SyncedFileInfo.cs" />
     <Compile Include="Themes\IAppThemeManager.cs" />
     <Compile Include="Themes\IAppThemeManager.cs" />
     <Compile Include="Themes\InternalThemeImage.cs" />
     <Compile Include="Themes\InternalThemeImage.cs" />
     <Compile Include="TV\ITVSeriesManager.cs" />
     <Compile Include="TV\ITVSeriesManager.cs" />

+ 5 - 3
MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs

@@ -1,4 +1,5 @@
-using System.IO;
+using MediaBrowser.Model.MediaInfo;
+using System.IO;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
@@ -47,8 +48,9 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// Gets the subtitle language encoding parameter.
         /// Gets the subtitle language encoding parameter.
         /// </summary>
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="path">The path.</param>
+        /// <param name="protocol">The protocol.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        string GetSubtitleFileCharacterSet(string path);
-
+        Task<string> GetSubtitleFileCharacterSet(string path, MediaProtocol protocol, CancellationToken cancellationToken);
     }
     }
 }
 }

+ 73 - 4
MediaBrowser.Controller/MediaEncoding/MediaStreamSelector.cs

@@ -41,8 +41,6 @@ namespace MediaBrowser.Controller.MediaEncoding
             streams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages)
             streams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages)
                 .ToList();
                 .ToList();
 
 
-            var full = streams.Where(s => !s.IsForced);
-
             MediaStream stream = null;
             MediaStream stream = null;
 
 
             if (mode == SubtitlePlaybackMode.None)
             if (mode == SubtitlePlaybackMode.None)
@@ -55,13 +53,13 @@ namespace MediaBrowser.Controller.MediaEncoding
                 // if the audio language is not understood by the user, load their preferred subs, if there are any
                 // if the audio language is not understood by the user, load their preferred subs, if there are any
                 if (!ContainsOrdinal(preferredLanguages, audioTrackLanguage))
                 if (!ContainsOrdinal(preferredLanguages, audioTrackLanguage))
                 {
                 {
-                    stream = full.FirstOrDefault(s => ContainsOrdinal(preferredLanguages, s.Language));
+                    stream = streams.Where(s => !s.IsForced).FirstOrDefault(s => ContainsOrdinal(preferredLanguages, s.Language));
                 }
                 }
             }
             }
             else if (mode == SubtitlePlaybackMode.Always)
             else if (mode == SubtitlePlaybackMode.Always)
             {
             {
                 // always load the most suitable full subtitles
                 // always load the most suitable full subtitles
-                stream = full.FirstOrDefault();
+                stream = streams.FirstOrDefault(s => !s.IsForced);
             }
             }
 
 
             // load forced subs if we have found no suitable full subtitles
             // load forced subs if we have found no suitable full subtitles
@@ -97,6 +95,77 @@ namespace MediaBrowser.Controller.MediaEncoding
                  .ThenBy(i => i.Index);
                  .ThenBy(i => i.Index);
         }
         }
 
 
+        public static void SetSubtitleStreamScores(List<MediaStream> streams,
+            List<string> preferredLanguages,
+            SubtitlePlaybackMode mode,
+            string audioTrackLanguage)
+        {
+            if (mode == SubtitlePlaybackMode.None)
+            {
+                return;
+            }
+
+            streams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages)
+                .ToList();
+
+            var filteredStreams = new List<MediaStream>();
+
+            if (mode == SubtitlePlaybackMode.Default)
+            {
+                // if the audio language is not understood by the user, load their preferred subs, if there are any
+                if (!ContainsOrdinal(preferredLanguages, audioTrackLanguage))
+                {
+                    filteredStreams = streams.Where(s => !s.IsForced && ContainsOrdinal(preferredLanguages, s.Language))
+                        .ToList();
+                }
+            }
+            else if (mode == SubtitlePlaybackMode.Always)
+            {
+                // always load the most suitable full subtitles
+                filteredStreams = streams.Where(s => !s.IsForced)
+                    .ToList();
+            }
+
+            // load forced subs if we have found no suitable full subtitles
+            if (filteredStreams.Count == 0)
+            {
+                filteredStreams = streams
+                    .Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
+                    .ToList();
+            }
+
+            foreach (var stream in filteredStreams)
+            {
+                stream.Score = GetSubtitleScore(stream, preferredLanguages);
+            }
+        }
+
+        private static int GetSubtitleScore(MediaStream stream, List<string> languagePreferences)
+        {
+            var values = new List<int>();
+
+            var index = languagePreferences.FindIndex(l => string.Equals(stream.Language, l, StringComparison.OrdinalIgnoreCase));
+
+            values.Add(index == -1 ? 0 : 100 - index);
+
+            values.Add(stream.IsDefault ? 1 : 0);
+            values.Add(stream.SupportsExternalStream ? 1 : 0);
+            values.Add(stream.IsTextSubtitleStream ? 1 : 0);
+            values.Add(stream.IsExternal ? 1 : 0);
+
+            values.Reverse();
+            var scale = 1;
+            var score = 0;
+
+            foreach (var value in values)
+            {
+                score += scale * (value + 1);
+                scale *= 10;
+            }
+
+            return score;
+        }
+
         private static int GetBooleanOrderBy(bool value)
         private static int GetBooleanOrderBy(bool value)
         {
         {
             return value ? 0 : 1;
             return value ? 0 : 1;

+ 3 - 4
MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs

@@ -1,5 +1,4 @@
-using MediaBrowser.Common.Net;
-using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -75,12 +74,12 @@ namespace MediaBrowser.Controller.Net
                 throw new ArgumentNullException("message");
                 throw new ArgumentNullException("message");
             }
             }
 
 
-            if (message.MessageType.Equals(Name + "Start", StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(message.MessageType, Name + "Start", StringComparison.OrdinalIgnoreCase))
             {
             {
                 Start(message);
                 Start(message);
             }
             }
 
 
-            if (message.MessageType.Equals(Name + "Stop", StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(message.MessageType, Name + "Stop", StringComparison.OrdinalIgnoreCase))
             {
             {
                 Stop(message);
                 Stop(message);
             }
             }

+ 18 - 0
MediaBrowser.Controller/Sync/IHasDynamicAccess.cs

@@ -0,0 +1,18 @@
+using MediaBrowser.Model.Sync;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Sync
+{
+    public interface IHasDynamicAccess
+    {
+        /// <summary>
+        /// Gets the synced file information.
+        /// </summary>
+        /// <param name="remotePath">The remote path.</param>
+        /// <param name="target">The target.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task&lt;SyncedFileInfo&gt;.</returns>
+        Task<SyncedFileInfo> GetSyncedFileInfo(string remotePath, SyncTarget target, CancellationToken cancellationToken);
+    }
+}

+ 1 - 10
MediaBrowser.Controller/Sync/IServerSyncProvider.cs

@@ -18,7 +18,7 @@ namespace MediaBrowser.Controller.Sync
         /// <param name="progress">The progress.</param>
         /// <param name="progress">The progress.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
-        Task<SendFileResult> SendFile(Stream stream, string remotePath, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
+        Task<SyncedFileInfo> SendFile(Stream stream, string remotePath, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
         /// Deletes the file.
         /// Deletes the file.
@@ -54,14 +54,5 @@ namespace MediaBrowser.Controller.Sync
         /// <param name="target">The target.</param>
         /// <param name="target">The target.</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
         string GetParentDirectoryPath(string path, SyncTarget target);
         string GetParentDirectoryPath(string path, SyncTarget target);
-
-        /// <summary>
-        /// Gets the file system entries.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="target">The target.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task&lt;List&lt;DeviceFileInfo&gt;&gt;.</returns>
-        Task<List<DeviceFileInfo>> GetFileSystemEntries(string path, SyncTarget target, CancellationToken cancellationToken);
     }
     }
 }
 }

+ 16 - 0
MediaBrowser.Controller/Sync/ISyncDataProvider.cs

@@ -14,6 +14,14 @@ namespace MediaBrowser.Controller.Sync
         /// <returns>Task&lt;List&lt;System.String&gt;&gt;.</returns>
         /// <returns>Task&lt;List&lt;System.String&gt;&gt;.</returns>
         Task<List<string>> GetServerItemIds(SyncTarget target, string serverId);
         Task<List<string>> GetServerItemIds(SyncTarget target, string serverId);
 
 
+        /// <summary>
+        /// Gets the synchronize job item ids.
+        /// </summary>
+        /// <param name="target">The target.</param>
+        /// <param name="serverId">The server identifier.</param>
+        /// <returns>Task&lt;List&lt;System.String&gt;&gt;.</returns>
+        Task<List<string>> GetSyncJobItemIds(SyncTarget target, string serverId);
+        
         /// <summary>
         /// <summary>
         /// Adds the or update.
         /// Adds the or update.
         /// </summary>
         /// </summary>
@@ -46,5 +54,13 @@ namespace MediaBrowser.Controller.Sync
         /// <param name="itemId">The item identifier.</param>
         /// <param name="itemId">The item identifier.</param>
         /// <returns>Task&lt;LocalItem&gt;.</returns>
         /// <returns>Task&lt;LocalItem&gt;.</returns>
         Task<List<LocalItem>> GetCachedItems(SyncTarget target, string serverId, string itemId);
         Task<List<LocalItem>> GetCachedItems(SyncTarget target, string serverId, string itemId);
+        /// <summary>
+        /// Gets the cached items by synchronize job item identifier.
+        /// </summary>
+        /// <param name="target">The target.</param>
+        /// <param name="serverId">The server identifier.</param>
+        /// <param name="syncJobItemId">The synchronize job item identifier.</param>
+        /// <returns>Task&lt;List&lt;LocalItem&gt;&gt;.</returns>
+        Task<List<LocalItem>> GetCachedItemsBySyncJobItemId(SyncTarget target, string serverId, string syncJobItemId);
     }
     }
 }
 }

+ 12 - 1
MediaBrowser.Controller/Sync/SendFileResult.cs → MediaBrowser.Controller/Sync/SyncedFileInfo.cs

@@ -1,8 +1,9 @@
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
+using System.Collections.Generic;
 
 
 namespace MediaBrowser.Controller.Sync
 namespace MediaBrowser.Controller.Sync
 {
 {
-    public class SendFileResult
+    public class SyncedFileInfo
     {
     {
         /// <summary>
         /// <summary>
         /// Gets or sets the path.
         /// Gets or sets the path.
@@ -14,5 +15,15 @@ namespace MediaBrowser.Controller.Sync
         /// </summary>
         /// </summary>
         /// <value>The protocol.</value>
         /// <value>The protocol.</value>
         public MediaProtocol Protocol { get; set; }
         public MediaProtocol Protocol { get; set; }
+        /// <summary>
+        /// Gets or sets the required HTTP headers.
+        /// </summary>
+        /// <value>The required HTTP headers.</value>
+        public Dictionary<string, string> RequiredHttpHeaders { get; set; }
+
+        public SyncedFileInfo()
+        {
+            RequiredHttpHeaders = new Dictionary<string, string>();
+        }
     }
     }
 }
 }

+ 13 - 6
MediaBrowser.Dlna/Didl/DidlBuilder.cs

@@ -124,7 +124,7 @@ namespace MediaBrowser.Dlna.Didl
         {
         {
             if (streamInfo == null)
             if (streamInfo == null)
             {
             {
-                var sources = _user == null ? video.GetMediaSources(true).ToList() : _mediaSourceManager.GetStaticMediaSources(video, true, _user).ToList();
+                var sources = _user == null ? _mediaSourceManager.GetStaticMediaSources(video, true).ToList() : _mediaSourceManager.GetStaticMediaSources(video, true, _user).ToList();
 
 
                 streamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
                 streamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
                 {
                 {
@@ -158,16 +158,21 @@ namespace MediaBrowser.Dlna.Didl
                 streamInfo.TranscodeSeekInfo,
                 streamInfo.TranscodeSeekInfo,
                 streamInfo.IsTargetAnamorphic,
                 streamInfo.IsTargetAnamorphic,
                 streamInfo.IsTargetCabac,
                 streamInfo.IsTargetCabac,
-                streamInfo.TargetRefFrames);
+                streamInfo.TargetRefFrames,
+                streamInfo.TargetVideoStreamCount,
+                streamInfo.TargetAudioStreamCount);
 
 
             foreach (var contentFeature in contentFeatureList)
             foreach (var contentFeature in contentFeatureList)
             {
             {
                 AddVideoResource(container, video, deviceId, filter, contentFeature, streamInfo);
                 AddVideoResource(container, video, deviceId, filter, contentFeature, streamInfo);
             }
             }
 
 
-            foreach (var subtitle in streamInfo.GetExternalSubtitles(_serverAddress, _accessToken, false))
+            foreach (var subtitle in streamInfo.GetSubtitleProfiles(false, _serverAddress, _accessToken))
             {
             {
-                AddSubtitleElement(container, subtitle);
+                if (subtitle.DeliveryMethod == SubtitleDeliveryMethod.External)
+                {
+                    AddSubtitleElement(container, subtitle);
+                }
             }
             }
         }
         }
 
 
@@ -280,7 +285,9 @@ namespace MediaBrowser.Dlna.Didl
                 streamInfo.TargetTimestamp,
                 streamInfo.TargetTimestamp,
                 streamInfo.IsTargetAnamorphic,
                 streamInfo.IsTargetAnamorphic,
                 streamInfo.IsTargetCabac,
                 streamInfo.IsTargetCabac,
-                streamInfo.TargetRefFrames);
+                streamInfo.TargetRefFrames,
+                streamInfo.TargetVideoStreamCount,
+                streamInfo.TargetAudioStreamCount);
 
 
             var filename = url.Substring(0, url.IndexOf('?'));
             var filename = url.Substring(0, url.IndexOf('?'));
 
 
@@ -344,7 +351,7 @@ namespace MediaBrowser.Dlna.Didl
 
 
             if (streamInfo == null)
             if (streamInfo == null)
             {
             {
-                var sources = _user == null ? audio.GetMediaSources(true).ToList() : _mediaSourceManager.GetStaticMediaSources(audio, true, _user).ToList();
+                var sources = _user == null ? _mediaSourceManager.GetStaticMediaSources(audio, true).ToList() : _mediaSourceManager.GetStaticMediaSources(audio, true, _user).ToList();
 
 
                 streamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions
                 streamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions
                {
                {

+ 6 - 4
MediaBrowser.Dlna/PlayTo/PlayToController.cs

@@ -467,16 +467,16 @@ namespace MediaBrowser.Dlna.PlayTo
 
 
             var profile = _dlnaManager.GetProfile(deviceInfo.ToDeviceIdentification()) ??
             var profile = _dlnaManager.GetProfile(deviceInfo.ToDeviceIdentification()) ??
                 _dlnaManager.GetDefaultProfile();
                 _dlnaManager.GetDefaultProfile();
-
+            
             var hasMediaSources = item as IHasMediaSources;
             var hasMediaSources = item as IHasMediaSources;
             var mediaSources = hasMediaSources != null
             var mediaSources = hasMediaSources != null
-                ? (user == null ? hasMediaSources.GetMediaSources(true) : _mediaSourceManager.GetStaticMediaSources(hasMediaSources, true, user)).ToList()
+                ? (user == null ? _mediaSourceManager.GetStaticMediaSources(hasMediaSources, true) : _mediaSourceManager.GetStaticMediaSources(hasMediaSources, true, user)).ToList()
                 : new List<MediaSourceInfo>();
                 : new List<MediaSourceInfo>();
 
 
             var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
             var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
             playlistItem.StreamInfo.StartPositionTicks = startPostionTicks;
             playlistItem.StreamInfo.StartPositionTicks = startPostionTicks;
 
 
-            playlistItem.StreamUrl = playlistItem.StreamInfo.ToUrl(_serverAddress, _accessToken);
+            playlistItem.StreamUrl = playlistItem.StreamInfo.ToDlnaUrl(_serverAddress, _accessToken);
 
 
             var itemXml = new DidlBuilder(profile, user, _imageProcessor, _serverAddress, _accessToken, _userDataManager, _localization, _mediaSourceManager)
             var itemXml = new DidlBuilder(profile, user, _imageProcessor, _serverAddress, _accessToken, _userDataManager, _localization, _mediaSourceManager)
                 .GetItemDidl(item, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo);
                 .GetItemDidl(item, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo);
@@ -526,7 +526,9 @@ namespace MediaBrowser.Dlna.PlayTo
                     streamInfo.TranscodeSeekInfo,
                     streamInfo.TranscodeSeekInfo,
                     streamInfo.IsTargetAnamorphic,
                     streamInfo.IsTargetAnamorphic,
                     streamInfo.IsTargetCabac,
                     streamInfo.IsTargetCabac,
-                    streamInfo.TargetRefFrames);
+                    streamInfo.TargetRefFrames,
+                    streamInfo.TargetVideoStreamCount,
+                    streamInfo.TargetAudioStreamCount);
 
 
                 return list.FirstOrDefault();
                 return list.FirstOrDefault();
             }
             }

+ 5 - 6
MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs

@@ -186,18 +186,17 @@ namespace MediaBrowser.LocalMetadata.Images
                 names.Add("movie");
                 names.Add("movie");
             }
             }
 
 
-            foreach (var name in names)
-            {
-                AddImage(files, images, imagePrefix + name, ImageType.Primary);
-            }
-
             var fileNameWithoutExtension = item.FileNameWithoutExtension;
             var fileNameWithoutExtension = item.FileNameWithoutExtension;
-
             if (!string.IsNullOrEmpty(fileNameWithoutExtension))
             if (!string.IsNullOrEmpty(fileNameWithoutExtension))
             {
             {
                 AddImage(files, images, fileNameWithoutExtension, ImageType.Primary);
                 AddImage(files, images, fileNameWithoutExtension, ImageType.Primary);
             }
             }
 
 
+            foreach (var name in names)
+            {
+                AddImage(files, images, imagePrefix + name, ImageType.Primary);
+            }
+
             if (!isInMixedFolder)
             if (!isInMixedFolder)
             {
             {
                 foreach (var name in names)
                 foreach (var name in names)

+ 1 - 1
MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs

@@ -14,7 +14,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 {
 {
     public class AudioEncoder : BaseEncoder
     public class AudioEncoder : BaseEncoder
     {
     {
-        public AudioEncoder(MediaEncoder mediaEncoder, ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager) : base(mediaEncoder, logger, configurationManager, fileSystem, liveTvManager, isoManager, libraryManager, channelManager, sessionManager, subtitleEncoder, mediaSourceManager)
+        public AudioEncoder(MediaEncoder mediaEncoder, ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IIsoManager isoManager, ILibraryManager libraryManager, ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager) : base(mediaEncoder, logger, configurationManager, fileSystem, isoManager, libraryManager, sessionManager, subtitleEncoder, mediaSourceManager)
         {
         {
         }
         }
 
 

+ 18 - 65
MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs

@@ -1,6 +1,5 @@
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.IO;
-using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
@@ -14,6 +13,7 @@ using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics;
@@ -31,10 +31,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
         protected readonly ILogger Logger;
         protected readonly ILogger Logger;
         protected readonly IServerConfigurationManager ConfigurationManager;
         protected readonly IServerConfigurationManager ConfigurationManager;
         protected readonly IFileSystem FileSystem;
         protected readonly IFileSystem FileSystem;
-        protected readonly ILiveTvManager LiveTvManager;
         protected readonly IIsoManager IsoManager;
         protected readonly IIsoManager IsoManager;
         protected readonly ILibraryManager LibraryManager;
         protected readonly ILibraryManager LibraryManager;
-        protected readonly IChannelManager ChannelManager;
         protected readonly ISessionManager SessionManager;
         protected readonly ISessionManager SessionManager;
         protected readonly ISubtitleEncoder SubtitleEncoder;
         protected readonly ISubtitleEncoder SubtitleEncoder;
         protected readonly IMediaSourceManager MediaSourceManager;
         protected readonly IMediaSourceManager MediaSourceManager;
@@ -45,20 +43,18 @@ namespace MediaBrowser.MediaEncoding.Encoder
             ILogger logger,
             ILogger logger,
             IServerConfigurationManager configurationManager,
             IServerConfigurationManager configurationManager,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
-            ILiveTvManager liveTvManager,
             IIsoManager isoManager,
             IIsoManager isoManager,
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
-            IChannelManager channelManager,
-            ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager)
+            ISessionManager sessionManager, 
+            ISubtitleEncoder subtitleEncoder, 
+            IMediaSourceManager mediaSourceManager)
         {
         {
             MediaEncoder = mediaEncoder;
             MediaEncoder = mediaEncoder;
             Logger = logger;
             Logger = logger;
             ConfigurationManager = configurationManager;
             ConfigurationManager = configurationManager;
             FileSystem = fileSystem;
             FileSystem = fileSystem;
-            LiveTvManager = liveTvManager;
             IsoManager = isoManager;
             IsoManager = isoManager;
             LibraryManager = libraryManager;
             LibraryManager = libraryManager;
-            ChannelManager = channelManager;
             SessionManager = sessionManager;
             SessionManager = sessionManager;
             SubtitleEncoder = subtitleEncoder;
             SubtitleEncoder = subtitleEncoder;
             MediaSourceManager = mediaSourceManager;
             MediaSourceManager = mediaSourceManager;
@@ -68,7 +64,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             IProgress<double> progress,
             IProgress<double> progress,
             CancellationToken cancellationToken)
             CancellationToken cancellationToken)
         {
         {
-            var encodingJob = await new EncodingJobFactory(Logger, LiveTvManager, LibraryManager, ChannelManager, MediaSourceManager)
+            var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager)
                 .CreateJob(options, IsVideoEncoder, progress, cancellationToken).ConfigureAwait(false);
                 .CreateJob(options, IsVideoEncoder, progress, cancellationToken).ConfigureAwait(false);
 
 
             encodingJob.OutputFilePath = GetOutputFilePath(encodingJob);
             encodingJob.OutputFilePath = GetOutputFilePath(encodingJob);
@@ -477,53 +473,25 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationToken).ConfigureAwait(false);
                 state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationToken).ConfigureAwait(false);
             }
             }
 
 
-            if (string.IsNullOrEmpty(state.MediaPath))
+            if (state.MediaSource.RequiresOpening)
             {
             {
-                var checkCodecs = false;
-
-                if (string.Equals(state.ItemType, typeof(LiveTvChannel).Name))
+                var liveStreamResponse = await MediaSourceManager.OpenLiveStream(new LiveStreamRequest
                 {
                 {
-                    var streamInfo = await LiveTvManager.GetChannelStream(state.Options.ItemId, cancellationToken).ConfigureAwait(false);
-
-                    state.LiveTvStreamId = streamInfo.Id;
-
-                    state.MediaPath = streamInfo.Path;
-                    state.InputProtocol = streamInfo.Protocol;
+                    OpenToken = state.MediaSource.OpenToken
 
 
-                    await Task.Delay(1500, cancellationToken).ConfigureAwait(false);
+                }, false, cancellationToken).ConfigureAwait(false);
 
 
-                    AttachMediaStreamInfo(state, streamInfo, state.Options);
-                    checkCodecs = true;
-                }
+                AttachMediaStreamInfo(state, liveStreamResponse.MediaSource, state.Options);
 
 
-                else if (string.Equals(state.ItemType, typeof(LiveTvVideoRecording).Name) ||
-                    string.Equals(state.ItemType, typeof(LiveTvAudioRecording).Name))
+                if (state.IsVideoRequest)
                 {
                 {
-                    var streamInfo = await LiveTvManager.GetRecordingStream(state.Options.ItemId, cancellationToken).ConfigureAwait(false);
-
-                    state.LiveTvStreamId = streamInfo.Id;
-
-                    state.MediaPath = streamInfo.Path;
-                    state.InputProtocol = streamInfo.Protocol;
-
-                    await Task.Delay(1500, cancellationToken).ConfigureAwait(false);
-
-                    AttachMediaStreamInfo(state, streamInfo, state.Options);
-                    checkCodecs = true;
+                    EncodingJobFactory.TryStreamCopy(state, state.Options);
                 }
                 }
+            }
 
 
-                if (state.IsVideoRequest && checkCodecs)
-                {
-                    if (state.VideoStream != null && EncodingJobFactory.CanStreamCopyVideo(state.Options, state.VideoStream))
-                    {
-                        state.OutputVideoCodec = "copy";
-                    }
-
-                    if (state.AudioStream != null && EncodingJobFactory.CanStreamCopyAudio(state.Options, state.AudioStream, state.SupportedAudioCodecs))
-                    {
-                        state.OutputAudioCodec = "copy";
-                    }
-                }
+            if (state.MediaSource.BufferMs.HasValue)
+            {
+                await Task.Delay(state.MediaSource.BufferMs.Value, cancellationToken).ConfigureAwait(false);
             }
             }
         }
         }
 
 
@@ -531,22 +499,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
           MediaSourceInfo mediaSource,
           MediaSourceInfo mediaSource,
           EncodingJobOptions videoRequest)
           EncodingJobOptions videoRequest)
         {
         {
-            state.InputProtocol = mediaSource.Protocol;
-            state.MediaPath = mediaSource.Path;
-            state.RunTimeTicks = mediaSource.RunTimeTicks;
-            state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
-            state.InputBitrate = mediaSource.Bitrate;
-            state.InputFileSize = mediaSource.Size;
-            state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
-
-            if (state.ReadInputAtNativeFramerate)
-            {
-                state.OutputAudioSync = "1000";
-                state.InputVideoSync = "-1";
-                state.InputAudioSync = "1";
-            }
-
-            EncodingJobFactory.AttachMediaStreamInfo(state, mediaSource.MediaStreams, videoRequest);
+            EncodingJobFactory.AttachMediaStreamInfo(state, mediaSource, videoRequest);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -998,7 +951,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
 
                 if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
                 if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
                 {
                 {
-                    var charenc = SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath);
+                    var charenc = SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.MediaSource.Protocol, CancellationToken.None).Result;
 
 
                     if (!string.IsNullOrEmpty(charenc))
                     if (!string.IsNullOrEmpty(charenc))
                     {
                     {

+ 45 - 9
MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs

@@ -1,7 +1,8 @@
-using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
@@ -26,7 +27,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
 
         public EncodingJobOptions Options { get; set; }
         public EncodingJobOptions Options { get; set; }
         public string InputContainer { get; set; }
         public string InputContainer { get; set; }
-        public List<MediaStream> AllMediaStreams { get; set; }
+        public MediaSourceInfo MediaSource { get; set; }
         public MediaStream AudioStream { get; set; }
         public MediaStream AudioStream { get; set; }
         public MediaStream VideoStream { get; set; }
         public MediaStream VideoStream { get; set; }
         public MediaStream SubtitleStream { get; set; }
         public MediaStream SubtitleStream { get; set; }
@@ -76,12 +77,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
         }
         }
 
 
         private readonly ILogger _logger;
         private readonly ILogger _logger;
-        private readonly ILiveTvManager _liveTvManager;
+        private readonly IMediaSourceManager _mediaSourceManager;
 
 
-        public EncodingJob(ILogger logger, ILiveTvManager liveTvManager)
+        public EncodingJob(ILogger logger, IMediaSourceManager mediaSourceManager)
         {
         {
             _logger = logger;
             _logger = logger;
-            _liveTvManager = liveTvManager;
+            _mediaSourceManager = mediaSourceManager;
             Id = Guid.NewGuid().ToString("N");
             Id = Guid.NewGuid().ToString("N");
 
 
             RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
             RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@@ -89,7 +90,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             SupportedAudioCodecs = new List<string>();
             SupportedAudioCodecs = new List<string>();
             PlayableStreamFileNames = new List<string>();
             PlayableStreamFileNames = new List<string>();
             RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
             RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-            AllMediaStreams = new List<MediaStream>();
             TaskCompletionSource = new TaskCompletionSource<bool>();
             TaskCompletionSource = new TaskCompletionSource<bool>();
         }
         }
 
 
@@ -136,15 +136,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
 
         private async void DisposeLiveStream()
         private async void DisposeLiveStream()
         {
         {
-            if (!string.IsNullOrEmpty(LiveTvStreamId))
+            if (MediaSource.RequiresClosing)
             {
             {
                 try
                 try
                 {
                 {
-                    await _liveTvManager.CloseLiveStream(LiveTvStreamId, CancellationToken.None).ConfigureAwait(false);
+                    await _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId, CancellationToken.None).ConfigureAwait(false);
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {
-                    _logger.ErrorException("Error closing live tv stream", ex);
+                    _logger.ErrorException("Error closing media source", ex);
                 }
                 }
             }
             }
         }
         }
@@ -394,6 +394,42 @@ namespace MediaBrowser.MediaEncoding.Encoder
             }
             }
         }
         }
 
 
+        public int? TargetVideoStreamCount
+        {
+            get
+            {
+                if (Options.Static)
+                {
+                    return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue);
+                }
+                return GetMediaStreamCount(MediaStreamType.Video, 1);
+            }
+        }
+
+        public int? TargetAudioStreamCount
+        {
+            get
+            {
+                if (Options.Static)
+                {
+                    return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue);
+                }
+                return GetMediaStreamCount(MediaStreamType.Audio, 1);
+            }
+        }
+
+        private int? GetMediaStreamCount(MediaStreamType type, int limit)
+        {
+            var count = MediaSource.GetStreamCount(type);
+
+            if (count.HasValue)
+            {
+                count = Math.Min(count.Value, limit);
+            }
+
+            return count;
+        }
+
         public void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded)
         public void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded)
         {
         {
             var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null;
             var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null;

+ 69 - 112
MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs

@@ -1,9 +1,10 @@
-using MediaBrowser.Controller.Channels;
+using System.IO;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
@@ -19,19 +20,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
     public class EncodingJobFactory
     public class EncodingJobFactory
     {
     {
         private readonly ILogger _logger;
         private readonly ILogger _logger;
-        private readonly ILiveTvManager _liveTvManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
-        private readonly IChannelManager _channelManager;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaSourceManager _mediaSourceManager;
 
 
         protected static readonly CultureInfo UsCulture = new CultureInfo("en-US");
         protected static readonly CultureInfo UsCulture = new CultureInfo("en-US");
         
         
-        public EncodingJobFactory(ILogger logger, ILiveTvManager liveTvManager, ILibraryManager libraryManager, IChannelManager channelManager, IMediaSourceManager mediaSourceManager)
+        public EncodingJobFactory(ILogger logger, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager)
         {
         {
             _logger = logger;
             _logger = logger;
-            _liveTvManager = liveTvManager;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
-            _channelManager = channelManager;
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
         }
         }
 
 
@@ -42,9 +39,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
             if (string.IsNullOrEmpty(request.AudioCodec))
             if (string.IsNullOrEmpty(request.AudioCodec))
             {
             {
                 request.AudioCodec = InferAudioCodec(request.OutputContainer);
                 request.AudioCodec = InferAudioCodec(request.OutputContainer);
-            } 
-            
-            var state = new EncodingJob(_logger, _liveTvManager)
+            }
+
+            var state = new EncodingJob(_logger, _mediaSourceManager)
             {
             {
                 Options = options,
                 Options = options,
                 IsVideoRequest = isVideoRequest,
                 IsVideoRequest = isVideoRequest,
@@ -58,106 +55,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
             }
             }
 
 
             var item = _libraryManager.GetItemById(request.ItemId);
             var item = _libraryManager.GetItemById(request.ItemId);
-
-            List<MediaStream> mediaStreams = null;
-
             state.ItemType = item.GetType().Name;
             state.ItemType = item.GetType().Name;
 
 
-            if (item is ILiveTvRecording)
-            {
-                var recording = await _liveTvManager.GetInternalRecording(request.ItemId, cancellationToken).ConfigureAwait(false);
-
-                state.VideoType = VideoType.VideoFile;
-                state.IsInputVideo = string.Equals(recording.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
-
-                var path = recording.RecordingInfo.Path;
-                var mediaUrl = recording.RecordingInfo.Url;
-                
-                var source = string.IsNullOrEmpty(request.MediaSourceId)
-                    ? recording.GetMediaSources(false).First()
-                    : _mediaSourceManager.GetStaticMediaSource(recording, request.MediaSourceId, false);
-
-                mediaStreams = source.MediaStreams;
-
-                // Just to prevent this from being null and causing other methods to fail
-                state.MediaPath = string.Empty;
-
-                if (!string.IsNullOrEmpty(path))
-                {
-                    state.MediaPath = path;
-                    state.InputProtocol = MediaProtocol.File;
-                }
-                else if (!string.IsNullOrEmpty(mediaUrl))
-                {
-                    state.MediaPath = mediaUrl;
-                    state.InputProtocol = MediaProtocol.Http;
-                }
-
-                state.RunTimeTicks = recording.RunTimeTicks;
-                state.DeInterlace = true;
-                state.OutputAudioSync = "1000";
-                state.InputVideoSync = "-1";
-                state.InputAudioSync = "1";
-                state.InputContainer = recording.Container;
-                state.ReadInputAtNativeFramerate = source.ReadAtNativeFramerate;
-            }
-            else if (item is LiveTvChannel)
-            {
-                var channel = _liveTvManager.GetInternalChannel(request.ItemId);
-
-                state.VideoType = VideoType.VideoFile;
-                state.IsInputVideo = string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
-                mediaStreams = new List<MediaStream>();
-
-                state.DeInterlace = true;
-
-                // Just to prevent this from being null and causing other methods to fail
-                state.MediaPath = string.Empty;
-            }
-            else
-            {
-                var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(request.ItemId, false, cancellationToken).ConfigureAwait(false);
+            state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
 
 
-                var mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
-                    ? mediaSources.First()
-                    : mediaSources.First(i => string.Equals(i.Id, request.MediaSourceId));
-
-                mediaStreams = mediaSource.MediaStreams;
-
-                state.MediaPath = mediaSource.Path;
-                state.InputProtocol = mediaSource.Protocol;
-                state.InputContainer = mediaSource.Container;
-                state.InputFileSize = mediaSource.Size;
-                state.InputBitrate = mediaSource.Bitrate;
-                state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
-                state.RunTimeTicks = mediaSource.RunTimeTicks;
-                state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
-
-                var video = item as Video;
-
-                if (video != null)
-                {
-                    state.IsInputVideo = true;
-
-                    if (mediaSource.VideoType.HasValue)
-                    {
-                        state.VideoType = mediaSource.VideoType.Value;
-                    }
+            var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(request.ItemId, false, cancellationToken).ConfigureAwait(false);
 
 
-                    state.IsoType = mediaSource.IsoType;
+            var mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
+               ? mediaSources.First()
+               : mediaSources.First(i => string.Equals(i.Id, request.MediaSourceId));
 
 
-                    state.PlayableStreamFileNames = mediaSource.PlayableStreamFileNames.ToList();
-
-                    if (mediaSource.Timestamp.HasValue)
-                    {
-                        state.InputTimestamp = mediaSource.Timestamp.Value;
-                    }
-                }
-
-                state.RunTimeTicks = mediaSource.RunTimeTicks;
-            }
-
-            AttachMediaStreamInfo(state, mediaStreams, request);
+            AttachMediaStreamInfo(state, mediaSource, options);
 
 
             state.OutputAudioBitrate = GetAudioBitrateParam(request, state.AudioStream);
             state.OutputAudioBitrate = GetAudioBitrateParam(request, state.AudioStream);
             state.OutputAudioSampleRate = request.AudioSampleRate;
             state.OutputAudioSampleRate = request.AudioSampleRate;
@@ -185,26 +93,73 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
 
             ApplyDeviceProfileSettings(state);
             ApplyDeviceProfileSettings(state);
 
 
-            if (isVideoRequest)
+            TryStreamCopy(state, request);
+
+            return state;
+        }
+
+        internal static void TryStreamCopy(EncodingJob state,
+            EncodingJobOptions videoRequest)
+        {
+            if (state.IsVideoRequest)
             {
             {
-                if (state.VideoStream != null && CanStreamCopyVideo(request, state.VideoStream))
+                if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream))
                 {
                 {
                     state.OutputVideoCodec = "copy";
                     state.OutputVideoCodec = "copy";
                 }
                 }
 
 
-                if (state.AudioStream != null && CanStreamCopyAudio(request, state.AudioStream, state.SupportedAudioCodecs))
+                if (state.AudioStream != null && CanStreamCopyAudio(videoRequest, state.AudioStream, state.SupportedAudioCodecs))
                 {
                 {
                     state.OutputAudioCodec = "copy";
                     state.OutputAudioCodec = "copy";
                 }
                 }
             }
             }
-
-            return state;
         }
         }
 
 
         internal static void AttachMediaStreamInfo(EncodingJob state,
         internal static void AttachMediaStreamInfo(EncodingJob state,
-            List<MediaStream> mediaStreams,
+            MediaSourceInfo mediaSource,
             EncodingJobOptions videoRequest)
             EncodingJobOptions videoRequest)
         {
         {
+            state.MediaPath = mediaSource.Path;
+            state.InputProtocol = mediaSource.Protocol;
+            state.InputContainer = mediaSource.Container;
+            state.InputFileSize = mediaSource.Size;
+            state.InputBitrate = mediaSource.Bitrate;
+            state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
+            state.RunTimeTicks = mediaSource.RunTimeTicks;
+            state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
+
+            if (mediaSource.VideoType.HasValue)
+            {
+                state.VideoType = mediaSource.VideoType.Value;
+            }
+
+            state.IsoType = mediaSource.IsoType;
+
+            state.PlayableStreamFileNames = mediaSource.PlayableStreamFileNames.ToList();
+
+            if (mediaSource.Timestamp.HasValue)
+            {
+                state.InputTimestamp = mediaSource.Timestamp.Value;
+            }
+
+            state.InputProtocol = mediaSource.Protocol;
+            state.MediaPath = mediaSource.Path;
+            state.RunTimeTicks = mediaSource.RunTimeTicks;
+            state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
+            state.InputBitrate = mediaSource.Bitrate;
+            state.InputFileSize = mediaSource.Size;
+            state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
+
+            if (state.ReadInputAtNativeFramerate ||
+                mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))
+            {
+                state.OutputAudioSync = "1000";
+                state.InputVideoSync = "-1";
+                state.InputAudioSync = "1";
+            }
+
+            var mediaStreams = mediaSource.MediaStreams;
+
             if (videoRequest != null)
             if (videoRequest != null)
             {
             {
                 if (string.IsNullOrEmpty(videoRequest.VideoCodec))
                 if (string.IsNullOrEmpty(videoRequest.VideoCodec))
@@ -233,7 +188,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
                 state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
             }
             }
 
 
-            state.AllMediaStreams = mediaStreams;
+            state.MediaSource = mediaSource;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -771,7 +726,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 state.TargetTimestamp,
                 state.TargetTimestamp,
                 state.IsTargetAnamorphic,
                 state.IsTargetAnamorphic,
                 state.IsTargetCabac,
                 state.IsTargetCabac,
-                state.TargetRefFrames);
+                state.TargetRefFrames,
+                state.TargetVideoStreamCount,
+                state.TargetAudioStreamCount);
 
 
             if (mediaProfile != null)
             if (mediaProfile != null)
             {
             {

+ 0 - 4
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -577,10 +577,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 _logger,
                 _logger,
                 ConfigurationManager,
                 ConfigurationManager,
                 FileSystem,
                 FileSystem,
-                LiveTvManager,
                 IsoManager,
                 IsoManager,
                 LibraryManager,
                 LibraryManager,
-                ChannelManager,
                 SessionManager,
                 SessionManager,
                 SubtitleEncoder(),
                 SubtitleEncoder(),
                 MediaSourceManager())
                 MediaSourceManager())
@@ -599,10 +597,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 _logger,
                 _logger,
                 ConfigurationManager,
                 ConfigurationManager,
                 FileSystem,
                 FileSystem,
-                LiveTvManager,
                 IsoManager,
                 IsoManager,
                 LibraryManager,
                 LibraryManager,
-                ChannelManager,
                 SessionManager,
                 SessionManager,
                 SubtitleEncoder(),
                 SubtitleEncoder(),
                 MediaSourceManager())
                 MediaSourceManager())

+ 7 - 4
MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs

@@ -15,7 +15,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 {
 {
     public class VideoEncoder : BaseEncoder
     public class VideoEncoder : BaseEncoder
     {
     {
-        public VideoEncoder(MediaEncoder mediaEncoder, ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager) : base(mediaEncoder, logger, configurationManager, fileSystem, liveTvManager, isoManager, libraryManager, channelManager, sessionManager, subtitleEncoder, mediaSourceManager)
+        public VideoEncoder(MediaEncoder mediaEncoder, ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IIsoManager isoManager, ILibraryManager libraryManager, ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager) : base(mediaEncoder, logger, configurationManager, fileSystem, isoManager, libraryManager, sessionManager, subtitleEncoder, mediaSourceManager)
         {
         {
         }
         }
 
 
@@ -73,10 +73,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
                     args;
                     args;
             }
             }
 
 
-            var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
-                5.ToString(UsCulture));
+            if (state.Options.Context == EncodingContext.Streaming)
+            {
+                var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
+                    5.ToString(UsCulture));
 
 
-            args += keyFrameArg;
+                args += keyFrameArg;
+            }
 
 
             var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
             var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
 
 

+ 4 - 3
MediaBrowser.MediaEncoding/Subtitles/AssParser.cs

@@ -43,10 +43,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                     subEvent.StartPositionTicks = GetTicks(sections[headers["Start"]]);
                     subEvent.StartPositionTicks = GetTicks(sections[headers["Start"]]);
                     subEvent.EndPositionTicks = GetTicks(sections[headers["End"]]);
                     subEvent.EndPositionTicks = GetTicks(sections[headers["End"]]);
 
 
-                    //RemoteNativeFormatting(subEvent);
-                    
                     subEvent.Text = string.Join(",", sections.Skip(headers["Text"]));
                     subEvent.Text = string.Join(",", sections.Skip(headers["Text"]));
-                    subEvent.Text = subEvent.Text.Replace(@"\N", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
+                    RemoteNativeFormatting(subEvent);
+
+                    subEvent.Text = subEvent.Text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
+                    
                     subEvent.Text = Regex.Replace(subEvent.Text, @"\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}", string.Empty, RegexOptions.IgnoreCase);
                     subEvent.Text = Regex.Replace(subEvent.Text, @"\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}", string.Empty, RegexOptions.IgnoreCase);
 
 
                     trackInfo.TrackEvents.Add(subEvent);
                     trackInfo.TrackEvents.Add(subEvent);

+ 1 - 2
MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs

@@ -148,8 +148,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 
 
         public static string GetFormattedText(string text)
         public static string GetFormattedText(string text)
         {
         {
-            text = text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase)
-                .Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
+            text = text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
 
 
             bool italic = false;
             bool italic = false;
 
 

+ 71 - 38
MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
@@ -29,8 +30,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IJsonSerializer _json;
         private readonly IJsonSerializer _json;
+        private readonly IHttpClient _httpClient;
+        private readonly IMediaSourceManager _mediaSourceManager;
 
 
-        public SubtitleEncoder(ILibraryManager libraryManager, ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IJsonSerializer json)
+        public SubtitleEncoder(ILibraryManager libraryManager, ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IJsonSerializer json, IHttpClient httpClient, IMediaSourceManager mediaSourceManager)
         {
         {
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _logger = logger;
             _logger = logger;
@@ -38,6 +41,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _mediaEncoder = mediaEncoder;
             _mediaEncoder = mediaEncoder;
             _json = json;
             _json = json;
+            _httpClient = httpClient;
+            _mediaSourceManager = mediaSourceManager;
         }
         }
 
 
         private string SubtitleCachePath
         private string SubtitleCachePath
@@ -127,9 +132,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             int subtitleStreamIndex,
             int subtitleStreamIndex,
             CancellationToken cancellationToken)
             CancellationToken cancellationToken)
         {
         {
-            var item = (Video)_libraryManager.GetItemById(new Guid(itemId));
+            var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(itemId, false, cancellationToken).ConfigureAwait(false);
 
 
-            var mediaSource = item.GetMediaSources(false)
+            var mediaSource = mediaSources
                 .First(i => string.Equals(i.Id, mediaSourceId));
                 .First(i => string.Equals(i.Id, mediaSourceId));
 
 
             var subtitleStream = mediaSource.MediaStreams
             var subtitleStream = mediaSource.MediaStreams
@@ -149,20 +154,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 
 
             var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false);
             var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false);
 
 
-            var stream = await GetSubtitleStream(fileInfo.Item1, fileInfo.Item3).ConfigureAwait(false);
+            var stream = await GetSubtitleStream(fileInfo.Item1, fileInfo.Item2, fileInfo.Item4, cancellationToken).ConfigureAwait(false);
 
 
-            return new Tuple<Stream, string>(stream, fileInfo.Item2);
+            return new Tuple<Stream, string>(stream, fileInfo.Item3);
         }
         }
 
 
-        private async Task<Stream> GetSubtitleStream(string path, bool requiresCharset)
+        private async Task<Stream> GetSubtitleStream(string path, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken)
         {
         {
             if (requiresCharset)
             if (requiresCharset)
             {
             {
-                var charset = GetSubtitleFileCharacterSet(path);
+                var charset = await GetSubtitleFileCharacterSet(path, protocol, cancellationToken).ConfigureAwait(false);
 
 
                 if (!string.IsNullOrEmpty(charset))
                 if (!string.IsNullOrEmpty(charset))
                 {
                 {
-                    using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
+                    using (var fs = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false))
                     {
                     {
                         using (var reader = new StreamReader(fs, GetEncoding(charset)))
                         using (var reader = new StreamReader(fs, GetEncoding(charset)))
                         {
                         {
@@ -196,7 +201,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             }
             }
         }
         }
 
 
-        private async Task<Tuple<string, string, bool>> GetReadableFile(string mediaPath,
+        private async Task<Tuple<string, MediaProtocol, string, bool>> GetReadableFile(string mediaPath,
             string[] inputFiles,
             string[] inputFiles,
             MediaProtocol protocol,
             MediaProtocol protocol,
             MediaStream subtitleStream,
             MediaStream subtitleStream,
@@ -228,12 +233,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 }
                 }
 
 
                 // Extract    
                 // Extract    
-                var outputPath = GetSubtitleCachePath(mediaPath, subtitleStream.Index, "." + outputFormat);
+                var outputPath = GetSubtitleCachePath(mediaPath, protocol, subtitleStream.Index, "." + outputFormat);
 
 
                 await ExtractTextSubtitle(inputFiles, protocol, subtitleStream.Index, outputCodec, outputPath, cancellationToken)
                 await ExtractTextSubtitle(inputFiles, protocol, subtitleStream.Index, outputCodec, outputPath, cancellationToken)
                         .ConfigureAwait(false);
                         .ConfigureAwait(false);
 
 
-                return new Tuple<string, string, bool>(outputPath, outputFormat, false);
+                return new Tuple<string, MediaProtocol, string, bool>(outputPath, MediaProtocol.File, outputFormat, false);
             }
             }
 
 
             var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec)
             var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec)
@@ -242,14 +247,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             if (GetReader(currentFormat, false) == null)
             if (GetReader(currentFormat, false) == null)
             {
             {
                 // Convert    
                 // Convert    
-                var outputPath = GetSubtitleCachePath(mediaPath, subtitleStream.Index, ".srt");
+                var outputPath = GetSubtitleCachePath(mediaPath, protocol, subtitleStream.Index, ".srt");
 
 
-                await ConvertTextSubtitleToSrt(subtitleStream.Path, outputPath, cancellationToken).ConfigureAwait(false);
+                await ConvertTextSubtitleToSrt(subtitleStream.Path, protocol, outputPath, cancellationToken).ConfigureAwait(false);
 
 
-                return new Tuple<string, string, bool>(outputPath, "srt", true);
+                return new Tuple<string, MediaProtocol, string, bool>(outputPath, MediaProtocol.File, "srt", true);
             }
             }
 
 
-            return new Tuple<string, string, bool>(subtitleStream.Path, currentFormat, true);
+            return new Tuple<string, MediaProtocol, string, bool>(subtitleStream.Path, protocol, currentFormat, true);
         }
         }
 
 
         private async Task<SubtitleTrackInfo> GetTrackInfo(Stream stream,
         private async Task<SubtitleTrackInfo> GetTrackInfo(Stream stream,
@@ -336,10 +341,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         /// Converts the text subtitle to SRT.
         /// Converts the text subtitle to SRT.
         /// </summary>
         /// </summary>
         /// <param name="inputPath">The input path.</param>
         /// <param name="inputPath">The input path.</param>
+        /// <param name="inputProtocol">The input protocol.</param>
         /// <param name="outputPath">The output path.</param>
         /// <param name="outputPath">The output path.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
-        public async Task ConvertTextSubtitleToSrt(string inputPath, string outputPath, CancellationToken cancellationToken)
+        private async Task ConvertTextSubtitleToSrt(string inputPath, MediaProtocol inputProtocol, string outputPath, CancellationToken cancellationToken)
         {
         {
             var semaphore = GetLock(outputPath);
             var semaphore = GetLock(outputPath);
 
 
@@ -349,7 +355,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             {
             {
                 if (!File.Exists(outputPath))
                 if (!File.Exists(outputPath))
                 {
                 {
-                    await ConvertTextSubtitleToSrtInternal(inputPath, outputPath).ConfigureAwait(false);
+                    await ConvertTextSubtitleToSrtInternal(inputPath, inputProtocol, outputPath, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
             finally
             finally
@@ -362,13 +368,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         /// Converts the text subtitle to SRT internal.
         /// Converts the text subtitle to SRT internal.
         /// </summary>
         /// </summary>
         /// <param name="inputPath">The input path.</param>
         /// <param name="inputPath">The input path.</param>
+        /// <param name="inputProtocol">The input protocol.</param>
         /// <param name="outputPath">The output path.</param>
         /// <param name="outputPath">The output path.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
-        /// <exception cref="System.ArgumentNullException">inputPath
+        /// <exception cref="System.ArgumentNullException">
+        /// inputPath
         /// or
         /// or
-        /// outputPath</exception>
+        /// outputPath
+        /// </exception>
         /// <exception cref="System.ApplicationException"></exception>
         /// <exception cref="System.ApplicationException"></exception>
-        private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string outputPath)
+        private async Task ConvertTextSubtitleToSrtInternal(string inputPath, MediaProtocol inputProtocol, string outputPath, CancellationToken cancellationToken)
         {
         {
             if (string.IsNullOrEmpty(inputPath))
             if (string.IsNullOrEmpty(inputPath))
             {
             {
@@ -382,7 +392,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 
 
             Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
             Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
 
 
-            var encodingParam = GetSubtitleFileCharacterSet(inputPath);
+            var encodingParam = await GetSubtitleFileCharacterSet(inputPath, inputProtocol, cancellationToken).ConfigureAwait(false);
 
 
             if (!string.IsNullOrEmpty(encodingParam))
             if (!string.IsNullOrEmpty(encodingParam))
             {
             {
@@ -688,32 +698,41 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             }
             }
         }
         }
 
 
-        private string GetSubtitleCachePath(string mediaPath, int subtitleStreamIndex, string outputSubtitleExtension)
+        private string GetSubtitleCachePath(string mediaPath, MediaProtocol protocol, int subtitleStreamIndex, string outputSubtitleExtension)
         {
         {
-            var ticksParam = string.Empty;
+            if (protocol == MediaProtocol.File)
+            {
+                var ticksParam = string.Empty;
+
+                var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
 
 
-            var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
+                var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
 
 
-            var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
+                var prefix = filename.Substring(0, 1);
+
+                return Path.Combine(SubtitleCachePath, prefix, filename);
+            }
+            else
+            {
+                var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
 
 
-            var prefix = filename.Substring(0, 1);
+                var prefix = filename.Substring(0, 1);
 
 
-            return Path.Combine(SubtitleCachePath, prefix, filename);
+                return Path.Combine(SubtitleCachePath, prefix, filename);
+            }
         }
         }
 
 
-        /// <summary>
-        /// Gets the subtitle language encoding param.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <returns>System.String.</returns>
-        public string GetSubtitleFileCharacterSet(string path)
+        public async Task<string> GetSubtitleFileCharacterSet(string path, MediaProtocol protocol, CancellationToken cancellationToken)
         {
         {
-            if (GetFileEncoding(path).Equals(Encoding.UTF8))
+            if (protocol == MediaProtocol.File)
             {
             {
-                return string.Empty;
+                if (GetFileEncoding(path).Equals(Encoding.UTF8))
+                {
+                    return string.Empty;
+                }
             }
             }
 
 
-            var charset = DetectCharset(path);
+            var charset = await DetectCharset(path, protocol, cancellationToken).ConfigureAwait(false);
 
 
             if (!string.IsNullOrWhiteSpace(charset))
             if (!string.IsNullOrWhiteSpace(charset))
             {
             {
@@ -769,11 +788,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             }
             }
         }
         }
 
 
-        private string DetectCharset(string path)
+        private async Task<string> DetectCharset(string path, MediaProtocol protocol, CancellationToken cancellationToken)
         {
         {
             try
             try
             {
             {
-                using (var file = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+                using (var file = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false))
                 {
                 {
                     var detector = new CharsetDetector();
                     var detector = new CharsetDetector();
                     detector.Feed(file);
                     detector.Feed(file);
@@ -819,5 +838,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             // It's ok - anything aside from utf is ok since that's what we're looking for
             // It's ok - anything aside from utf is ok since that's what we're looking for
             return Encoding.Default;
             return Encoding.Default;
         }
         }
+
+        private async Task<Stream> GetStream(string path, MediaProtocol protocol, CancellationToken cancellationToken)
+        {
+            if (protocol == MediaProtocol.Http)
+            {
+                return await _httpClient.Get(path, cancellationToken).ConfigureAwait(false);
+            }
+            if (protocol == MediaProtocol.File)
+            {
+                return _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+            }
+
+            throw new ArgumentOutOfRangeException("protocol");
+        }
     }
     }
 }
 }

+ 14 - 2
MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj

@@ -413,6 +413,9 @@
     <Compile Include="..\MediaBrowser.Model\Dlna\StreamInfo.cs">
     <Compile Include="..\MediaBrowser.Model\Dlna\StreamInfo.cs">
       <Link>Dlna\StreamInfo.cs</Link>
       <Link>Dlna\StreamInfo.cs</Link>
     </Compile>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Dlna\StreamInfoSorter.cs">
+      <Link>Dlna\StreamInfoSorter.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Dlna\SubtitleDeliveryMethod.cs">
     <Compile Include="..\MediaBrowser.Model\Dlna\SubtitleDeliveryMethod.cs">
       <Link>Dlna\SubtitleDeliveryMethod.cs</Link>
       <Link>Dlna\SubtitleDeliveryMethod.cs</Link>
     </Compile>
     </Compile>
@@ -800,12 +803,21 @@
     <Compile Include="..\MediaBrowser.Model\MediaInfo\IBlurayExaminer.cs">
     <Compile Include="..\MediaBrowser.Model\MediaInfo\IBlurayExaminer.cs">
       <Link>MediaInfo\IBlurayExaminer.cs</Link>
       <Link>MediaInfo\IBlurayExaminer.cs</Link>
     </Compile>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\MediaInfo\LiveMediaInfoResult.cs">
-      <Link>MediaInfo\LiveMediaInfoResult.cs</Link>
+    <Compile Include="..\MediaBrowser.Model\MediaInfo\LiveStreamRequest.cs">
+      <Link>MediaInfo\LiveStreamRequest.cs</Link>
+    </Compile>
+    <Compile Include="..\MediaBrowser.Model\MediaInfo\LiveStreamResponse.cs">
+      <Link>MediaInfo\LiveStreamResponse.cs</Link>
     </Compile>
     </Compile>
     <Compile Include="..\MediaBrowser.Model\MediaInfo\MediaProtocol.cs">
     <Compile Include="..\MediaBrowser.Model\MediaInfo\MediaProtocol.cs">
       <Link>MediaInfo\MediaProtocol.cs</Link>
       <Link>MediaInfo\MediaProtocol.cs</Link>
     </Compile>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\MediaInfo\PlaybackInfoRequest.cs">
+      <Link>MediaInfo\PlaybackInfoRequest.cs</Link>
+    </Compile>
+    <Compile Include="..\MediaBrowser.Model\MediaInfo\PlaybackInfoResponse.cs">
+      <Link>MediaInfo\PlaybackInfoResponse.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\MediaInfo\SubtitleFormat.cs">
     <Compile Include="..\MediaBrowser.Model\MediaInfo\SubtitleFormat.cs">
       <Link>MediaInfo\SubtitleFormat.cs</Link>
       <Link>MediaInfo\SubtitleFormat.cs</Link>
     </Compile>
     </Compile>

+ 14 - 2
MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj

@@ -378,6 +378,9 @@
     <Compile Include="..\MediaBrowser.Model\Dlna\StreamInfo.cs">
     <Compile Include="..\MediaBrowser.Model\Dlna\StreamInfo.cs">
       <Link>Dlna\StreamInfo.cs</Link>
       <Link>Dlna\StreamInfo.cs</Link>
     </Compile>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Dlna\StreamInfoSorter.cs">
+      <Link>Dlna\StreamInfoSorter.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Dlna\SubtitleDeliveryMethod.cs">
     <Compile Include="..\MediaBrowser.Model\Dlna\SubtitleDeliveryMethod.cs">
       <Link>Dlna\SubtitleDeliveryMethod.cs</Link>
       <Link>Dlna\SubtitleDeliveryMethod.cs</Link>
     </Compile>
     </Compile>
@@ -756,12 +759,21 @@
     <Compile Include="..\MediaBrowser.Model\MediaInfo\IBlurayExaminer.cs">
     <Compile Include="..\MediaBrowser.Model\MediaInfo\IBlurayExaminer.cs">
       <Link>MediaInfo\IBlurayExaminer.cs</Link>
       <Link>MediaInfo\IBlurayExaminer.cs</Link>
     </Compile>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\MediaInfo\LiveMediaInfoResult.cs">
-      <Link>MediaInfo\LiveMediaInfoResult.cs</Link>
+    <Compile Include="..\MediaBrowser.Model\MediaInfo\LiveStreamRequest.cs">
+      <Link>MediaInfo\LiveStreamRequest.cs</Link>
+    </Compile>
+    <Compile Include="..\MediaBrowser.Model\MediaInfo\LiveStreamResponse.cs">
+      <Link>MediaInfo\LiveStreamResponse.cs</Link>
     </Compile>
     </Compile>
     <Compile Include="..\MediaBrowser.Model\MediaInfo\MediaProtocol.cs">
     <Compile Include="..\MediaBrowser.Model\MediaInfo\MediaProtocol.cs">
       <Link>MediaInfo\MediaProtocol.cs</Link>
       <Link>MediaInfo\MediaProtocol.cs</Link>
     </Compile>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\MediaInfo\PlaybackInfoRequest.cs">
+      <Link>MediaInfo\PlaybackInfoRequest.cs</Link>
+    </Compile>
+    <Compile Include="..\MediaBrowser.Model\MediaInfo\PlaybackInfoResponse.cs">
+      <Link>MediaInfo\PlaybackInfoResponse.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\MediaInfo\SubtitleFormat.cs">
     <Compile Include="..\MediaBrowser.Model\MediaInfo\SubtitleFormat.cs">
       <Link>MediaInfo\SubtitleFormat.cs</Link>
       <Link>MediaInfo\SubtitleFormat.cs</Link>
     </Compile>
     </Compile>

+ 9 - 3
MediaBrowser.Model/ApiClient/IApiClient.cs

@@ -248,10 +248,9 @@ namespace MediaBrowser.Model.ApiClient
         /// <summary>
         /// <summary>
         /// Gets the playback information.
         /// Gets the playback information.
         /// </summary>
         /// </summary>
-        /// <param name="itemId">The item identifier.</param>
-        /// <param name="userId">The user identifier.</param>
+        /// <param name="request">The request.</param>
         /// <returns>Task&lt;LiveMediaInfoResult&gt;.</returns>
         /// <returns>Task&lt;LiveMediaInfoResult&gt;.</returns>
-        Task<LiveMediaInfoResult> GetPlaybackInfo(string itemId, string userId);
+        Task<PlaybackInfoResponse> GetPlaybackInfo(PlaybackInfoRequest request);
 
 
         /// <summary>
         /// <summary>
         /// Gets the users async.
         /// Gets the users async.
@@ -1486,5 +1485,12 @@ namespace MediaBrowser.Model.ApiClient
         /// <param name="query">The query.</param>
         /// <param name="query">The query.</param>
         /// <returns>Task&lt;List&lt;RecommendationDto&gt;&gt;.</returns>
         /// <returns>Task&lt;List&lt;RecommendationDto&gt;&gt;.</returns>
         Task<List<RecommendationDto>> GetMovieRecommendations(MovieRecommendationQuery query);
         Task<List<RecommendationDto>> GetMovieRecommendations(MovieRecommendationQuery query);
+        /// <summary>
+        /// Opens the live stream.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task&lt;LiveStreamResponse&gt;.</returns>
+        Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken);
     }
     }
 }
 }

+ 6 - 0
MediaBrowser.Model/ApiClient/IConnectionManager.cs

@@ -172,5 +172,11 @@ namespace MediaBrowser.Model.ApiClient
         /// <param name="rememberCredentials">if set to <c>true</c> [remember credentials].</param>
         /// <param name="rememberCredentials">if set to <c>true</c> [remember credentials].</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         Task AuthenticateOffline(UserDto user, string password, bool rememberCredentials);
         Task AuthenticateOffline(UserDto user, string password, bool rememberCredentials);
+
+        /// <summary>
+        /// Gets the offline users.
+        /// </summary>
+        /// <returns>Task&lt;List&lt;UserDto&gt;&gt;.</returns>
+        Task<List<UserDto>> GetOfflineUsers();
     }
     }
 }
 }

+ 16 - 5
MediaBrowser.Model/ApiClient/ServerCredentials.cs

@@ -1,7 +1,6 @@
 using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.Extensions;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Linq;
 
 
 namespace MediaBrowser.Model.ApiClient
 namespace MediaBrowser.Model.ApiClient
 {
 {
@@ -24,7 +23,12 @@ namespace MediaBrowser.Model.ApiClient
                 throw new ArgumentNullException("server");
                 throw new ArgumentNullException("server");
             }
             }
 
 
-            var list = Servers.ToList();
+            // Clone the existing list of servers
+            var list = new List<ServerInfo>();
+            foreach (ServerInfo serverInfo in Servers)
+            {
+                list.Add(serverInfo);
+            }
 
 
             var index = FindIndex(list, server.Id);
             var index = FindIndex(list, server.Id);
 
 
@@ -32,8 +36,11 @@ namespace MediaBrowser.Model.ApiClient
             {
             {
                 var existing = list[index];
                 var existing = list[index];
 
 
-                // Merge the data
-                existing.DateLastAccessed = new[] { existing.DateLastAccessed, server.DateLastAccessed }.Max();
+                // Take the most recent DateLastAccessed
+                if (server.DateLastAccessed > existing.DateLastAccessed)
+                {
+                    existing.DateLastAccessed = server.DateLastAccessed;
+                }
 
 
                 existing.UserLinkType = server.UserLinkType;
                 existing.UserLinkType = server.UserLinkType;
 
 
@@ -64,7 +71,11 @@ namespace MediaBrowser.Model.ApiClient
                 }
                 }
                 if (server.WakeOnLanInfos != null && server.WakeOnLanInfos.Count > 0)
                 if (server.WakeOnLanInfos != null && server.WakeOnLanInfos.Count > 0)
                 {
                 {
-                    existing.WakeOnLanInfos = server.WakeOnLanInfos.ToList();
+                    existing.WakeOnLanInfos = new List<WakeOnLanInfo>();
+                    foreach (WakeOnLanInfo info in server.WakeOnLanInfos)
+                    {
+                        existing.WakeOnLanInfos.Add(info);
+                    }
                 }
                 }
                 if (server.LastConnectionMode.HasValue)
                 if (server.LastConnectionMode.HasValue)
                 {
                 {

+ 6 - 2
MediaBrowser.Model/ApiClient/ServerInfo.cs

@@ -3,7 +3,6 @@ using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.System;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Linq;
 
 
 namespace MediaBrowser.Model.ApiClient
 namespace MediaBrowser.Model.ApiClient
 {
 {
@@ -83,7 +82,12 @@ namespace MediaBrowser.Model.ApiClient
                 throw new ArgumentNullException("user");
                 throw new ArgumentNullException("user");
             }
             }
 
 
-            var list = Users.ToList();
+            // Clone the existing list of users
+            var list = new List<ServerUserInfo>();
+            foreach (ServerUserInfo serverUserInfo in Users)
+            {
+                list.Add(serverUserInfo);
+            }
 
 
             var index = FindIndex(list, user.Id);
             var index = FindIndex(list, user.Id);
 
 

+ 4 - 0
MediaBrowser.Model/Configuration/EncodingOptions.cs

@@ -8,12 +8,16 @@ namespace MediaBrowser.Model.Configuration
         public double DownMixAudioBoost { get; set; }
         public double DownMixAudioBoost { get; set; }
         public string H264Encoder { get; set; }
         public string H264Encoder { get; set; }
         public bool EnableDebugLogging { get; set; }
         public bool EnableDebugLogging { get; set; }
+        public bool EnableThrottling { get; set; }
+        public int ThrottleThresholdSeconds { get; set; }
 
 
         public EncodingOptions()
         public EncodingOptions()
         {
         {
             H264Encoder = "libx264";
             H264Encoder = "libx264";
             DownMixAudioBoost = 2;
             DownMixAudioBoost = 2;
             EncodingQuality = EncodingQuality.Auto;
             EncodingQuality = EncodingQuality.Auto;
+            EnableThrottling = true;
+            ThrottleThresholdSeconds = 120;
         }
         }
     }
     }
 }
 }

+ 1 - 0
MediaBrowser.Model/Configuration/UserConfiguration.cs

@@ -54,6 +54,7 @@ namespace MediaBrowser.Model.Configuration
         public string[] LatestItemsExcludes { get; set; }
         public string[] LatestItemsExcludes { get; set; }
 
 
         public bool HasMigratedToPolicy { get; set; }
         public bool HasMigratedToPolicy { get; set; }
+        public bool HidePlayedInLatest { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="UserConfiguration" /> class.
         /// Initializes a new instance of the <see cref="UserConfiguration" /> class.

+ 11 - 2
MediaBrowser.Model/Dlna/ConditionProcessor.cs

@@ -20,7 +20,9 @@ namespace MediaBrowser.Model.Dlna
             TransportStreamTimestamp? timestamp,
             TransportStreamTimestamp? timestamp,
             bool? isAnamorphic,
             bool? isAnamorphic,
             bool? isCabac,
             bool? isCabac,
-            int? refFrames)
+            int? refFrames,
+            int? numVideoStreams,
+            int? numAudioStreams)
         {
         {
             switch (condition.Property)
             switch (condition.Property)
             {
             {
@@ -56,6 +58,10 @@ namespace MediaBrowser.Model.Dlna
                     return IsConditionSatisfied(condition, width);
                     return IsConditionSatisfied(condition, width);
                 case ProfileConditionValue.RefFrames:
                 case ProfileConditionValue.RefFrames:
                     return IsConditionSatisfied(condition, refFrames);
                     return IsConditionSatisfied(condition, refFrames);
+                case ProfileConditionValue.NumAudioStreams:
+                    return IsConditionSatisfied(condition, numAudioStreams);
+                case ProfileConditionValue.NumVideoStreams:
+                    return IsConditionSatisfied(condition, numVideoStreams);
                 case ProfileConditionValue.VideoTimestamp:
                 case ProfileConditionValue.VideoTimestamp:
                     return IsConditionSatisfied(condition, timestamp);
                     return IsConditionSatisfied(condition, timestamp);
                 default:
                 default:
@@ -92,7 +98,8 @@ namespace MediaBrowser.Model.Dlna
         public bool IsVideoAudioConditionSatisfied(ProfileCondition condition, 
         public bool IsVideoAudioConditionSatisfied(ProfileCondition condition, 
             int? audioChannels, 
             int? audioChannels, 
             int? audioBitrate,
             int? audioBitrate,
-            string audioProfile)
+            string audioProfile,
+            bool? isSecondaryTrack)
         {
         {
             switch (condition.Property)
             switch (condition.Property)
             {
             {
@@ -102,6 +109,8 @@ namespace MediaBrowser.Model.Dlna
                     return IsConditionSatisfied(condition, audioBitrate);
                     return IsConditionSatisfied(condition, audioBitrate);
                 case ProfileConditionValue.AudioChannels:
                 case ProfileConditionValue.AudioChannels:
                     return IsConditionSatisfied(condition, audioChannels);
                     return IsConditionSatisfied(condition, audioChannels);
+                case ProfileConditionValue.IsSecondaryAudio:
+                    return IsConditionSatisfied(condition, isSecondaryTrack);
                 default:
                 default:
                     throw new ArgumentException("Unexpected condition on audio file: " + condition.Property);
                     throw new ArgumentException("Unexpected condition on audio file: " + condition.Property);
             }
             }

+ 6 - 2
MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs

@@ -117,7 +117,9 @@ namespace MediaBrowser.Model.Dlna
             TranscodeSeekInfo transcodeSeekInfo,
             TranscodeSeekInfo transcodeSeekInfo,
             bool? isAnamorphic,
             bool? isAnamorphic,
             bool? isCabac,
             bool? isCabac,
-            int? refFrames)
+            int? refFrames,
+            int? numVideoStreams,
+            int? numAudioStreams)
         {
         {
             // first bit means Time based seek supported, second byte range seek supported (not sure about the order now), so 01 = only byte seek, 10 = time based, 11 = both, 00 = none
             // first bit means Time based seek supported, second byte range seek supported (not sure about the order now), so 01 = only byte seek, 10 = time based, 11 = both, 00 = none
             string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetOrgOpValue(runtimeTicks.HasValue, isDirectStream, transcodeSeekInfo);
             string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetOrgOpValue(runtimeTicks.HasValue, isDirectStream, transcodeSeekInfo);
@@ -158,7 +160,9 @@ namespace MediaBrowser.Model.Dlna
                 timestamp,
                 timestamp,
                 isAnamorphic,
                 isAnamorphic,
                 isCabac,
                 isCabac,
-                refFrames);
+                refFrames,
+                numVideoStreams,
+                numAudioStreams);
 
 
             List<string> orgPnValues = new List<string>();
             List<string> orgPnValues = new List<string>();
 
 

+ 4 - 2
MediaBrowser.Model/Dlna/DeviceProfile.cs

@@ -281,7 +281,9 @@ namespace MediaBrowser.Model.Dlna
             TransportStreamTimestamp timestamp,
             TransportStreamTimestamp timestamp,
             bool? isAnamorphic,
             bool? isAnamorphic,
             bool? isCabac,
             bool? isCabac,
-            int? refFrames)
+            int? refFrames,
+            int? numVideoStreams,
+            int? numAudioStreams)
         {
         {
             container = StringHelper.TrimStart((container ?? string.Empty), '.');
             container = StringHelper.TrimStart((container ?? string.Empty), '.');
 
 
@@ -315,7 +317,7 @@ namespace MediaBrowser.Model.Dlna
                 var anyOff = false;
                 var anyOff = false;
                 foreach (ProfileCondition c in i.Conditions)
                 foreach (ProfileCondition c in i.Conditions)
                 {
                 {
-                    if (!conditionProcessor.IsVideoConditionSatisfied(c, audioBitrate, audioChannels, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isCabac, refFrames))
+                    if (!conditionProcessor.IsVideoConditionSatisfied(c, audioBitrate, audioChannels, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isCabac, refFrames, numVideoStreams, numAudioStreams))
                     {
                     {
                         anyOff = true;
                         anyOff = true;
                         break;
                         break;

+ 4 - 1
MediaBrowser.Model/Dlna/ProfileConditionValue.cs

@@ -17,6 +17,9 @@
         VideoTimestamp = 12,
         VideoTimestamp = 12,
         IsAnamorphic = 13,
         IsAnamorphic = 13,
         RefFrames = 14,
         RefFrames = 14,
-        IsCabac = 15
+        IsCabac = 15,
+        NumAudioStreams = 16,
+        NumVideoStreams = 17,
+        IsSecondaryAudio
     }
     }
 }
 }

+ 99 - 35
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -89,38 +89,14 @@ namespace MediaBrowser.Model.Dlna
 
 
         private StreamInfo GetOptimalStream(List<StreamInfo> streams)
         private StreamInfo GetOptimalStream(List<StreamInfo> streams)
         {
         {
-            // Grab the first one that can be direct streamed
-            // If that doesn't produce anything, just take the first
-            foreach (StreamInfo i in streams)
-            {
-                if (i.PlayMethod == PlayMethod.DirectPlay && i.MediaSource.Protocol == MediaProtocol.File)
-                {
-                    return i;
-                }
-            }
-            foreach (StreamInfo i in streams)
-            {
-                if (i.PlayMethod == PlayMethod.DirectPlay)
-                {
-                    return i;
-                }
-            }
-            foreach (StreamInfo i in streams)
-            {
-                if (i.PlayMethod == PlayMethod.DirectStream)
-                {
-                    return i;
-                }
-            }
+            streams = StreamInfoSorter.SortMediaSources(streams);
 
 
             foreach (StreamInfo stream in streams)
             foreach (StreamInfo stream in streams)
             {
             {
                 return stream;
                 return stream;
             }
             }
 
 
-            PlaybackException error = new PlaybackException();
-            error.ErrorCode = PlaybackErrorCode.NoCompatibleStream;
-            throw error;
+            return null;
         }
         }
 
 
         private StreamInfo BuildAudioItem(MediaSourceInfo item, AudioOptions options)
         private StreamInfo BuildAudioItem(MediaSourceInfo item, AudioOptions options)
@@ -221,7 +197,7 @@ namespace MediaBrowser.Model.Dlna
                 playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
                 playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
                 playlistItem.Container = transcodingProfile.Container;
                 playlistItem.Container = transcodingProfile.Container;
                 playlistItem.AudioCodec = transcodingProfile.AudioCodec;
                 playlistItem.AudioCodec = transcodingProfile.AudioCodec;
-                playlistItem.Protocol = transcodingProfile.Protocol;
+                playlistItem.SubProtocol = transcodingProfile.Protocol;
 
 
                 List<CodecProfile> audioCodecProfiles = new List<CodecProfile>();
                 List<CodecProfile> audioCodecProfiles = new List<CodecProfile>();
                 foreach (CodecProfile i in options.Profile.CodecProfiles)
                 foreach (CodecProfile i in options.Profile.CodecProfiles)
@@ -263,6 +239,16 @@ namespace MediaBrowser.Model.Dlna
             return playlistItem;
             return playlistItem;
         }
         }
 
 
+        private int? GetBitrateForDirectPlayCheck(MediaSourceInfo item, AudioOptions options)
+        {
+            if (item.Protocol == MediaProtocol.File)
+            {
+                return options.Profile.MaxStaticBitrate;
+            }
+
+            return options.GetMaxBitrate();
+        }
+
         private List<PlayMethod> GetAudioDirectPlayMethods(MediaSourceInfo item, MediaStream audioStream, AudioOptions options)
         private List<PlayMethod> GetAudioDirectPlayMethods(MediaSourceInfo item, MediaStream audioStream, AudioOptions options)
         {
         {
             DirectPlayProfile directPlayProfile = null;
             DirectPlayProfile directPlayProfile = null;
@@ -287,7 +273,7 @@ namespace MediaBrowser.Model.Dlna
                 
                 
                 // The profile describes what the device supports
                 // The profile describes what the device supports
                 // If device requirements are satisfied then allow both direct stream and direct play
                 // If device requirements are satisfied then allow both direct stream and direct play
-                if (IsAudioEligibleForDirectPlay(item, options.Profile.MaxStaticBitrate))
+                if (IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options)))
                 {
                 {
                     playMethods.Add(PlayMethod.DirectPlay);
                     playMethods.Add(PlayMethod.DirectPlay);
                 }
                 }
@@ -296,6 +282,49 @@ namespace MediaBrowser.Model.Dlna
             return playMethods;
             return playMethods;
         }
         }
 
 
+        private int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles)
+        {
+            int highestScore = -1;
+
+            foreach (MediaStream stream in item.MediaStreams)
+            {
+                if (stream.Type == MediaStreamType.Subtitle && stream.Score.HasValue)
+                {
+                    if (stream.Score.Value > highestScore)
+                    {
+                        highestScore = stream.Score.Value;
+                    }
+                }    
+            }
+
+            List<MediaStream> topStreams = new List<MediaStream>();
+            foreach (MediaStream stream in item.MediaStreams)
+            {
+                if (stream.Type == MediaStreamType.Subtitle && stream.Score.HasValue && stream.Score.Value == highestScore)
+                {
+                    topStreams.Add(stream);
+                }
+            }
+
+            // If multiple streams have an equal score, try to pick the most efficient one
+            if (topStreams.Count > 1)
+            {
+                foreach (MediaStream stream in topStreams)
+                {
+                    foreach (SubtitleProfile profile in subtitleProfiles)
+                    {
+                        if (profile.Method == SubtitleDeliveryMethod.External && StringHelper.EqualsIgnoreCase(profile.Format, stream.Codec))
+                        {
+                            return stream.Index;
+                        }
+                    }
+                }
+            }
+
+            // If no optimization panned out, just use the original default
+            return item.DefaultSubtitleStreamIndex;
+        }
+
         private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options)
         private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options)
         {
         {
             StreamInfo playlistItem = new StreamInfo
             StreamInfo playlistItem = new StreamInfo
@@ -308,16 +337,20 @@ namespace MediaBrowser.Model.Dlna
                 DeviceProfile = options.Profile
                 DeviceProfile = options.Profile
             };
             };
 
 
-            playlistItem.SubtitleStreamIndex = options.SubtitleStreamIndex ?? item.DefaultSubtitleStreamIndex;
+            playlistItem.SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles);
             MediaStream subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitle, playlistItem.SubtitleStreamIndex.Value) : null;
             MediaStream subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitle, playlistItem.SubtitleStreamIndex.Value) : null;
 
 
             MediaStream audioStream = item.GetDefaultAudioStream(options.AudioStreamIndex ?? item.DefaultAudioStreamIndex);
             MediaStream audioStream = item.GetDefaultAudioStream(options.AudioStreamIndex ?? item.DefaultAudioStreamIndex);
-            int? audioStreamIndex = audioStream == null ? (int?)null : audioStream.Index;
+            int? audioStreamIndex = null;
+            if (audioStream != null)
+            {
+                audioStreamIndex = audioStream.Index;
+            }
 
 
             MediaStream videoStream = item.VideoStream;
             MediaStream videoStream = item.VideoStream;
 
 
             // TODO: This doesn't accout for situation of device being able to handle media bitrate, but wifi connection not fast enough
             // TODO: This doesn't accout for situation of device being able to handle media bitrate, but wifi connection not fast enough
-            bool isEligibleForDirectPlay = IsEligibleForDirectPlay(item, options.Profile.MaxStaticBitrate, subtitleStream, options);
+            bool isEligibleForDirectPlay = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options), subtitleStream, options);
             bool isEligibleForDirectStream = IsEligibleForDirectPlay(item, options.GetMaxBitrate(), subtitleStream, options);
             bool isEligibleForDirectStream = IsEligibleForDirectPlay(item, options.GetMaxBitrate(), subtitleStream, options);
 
 
             if (isEligibleForDirectPlay || isEligibleForDirectStream)
             if (isEligibleForDirectPlay || isEligibleForDirectStream)
@@ -374,7 +407,7 @@ namespace MediaBrowser.Model.Dlna
                 playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
                 playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
                 playlistItem.AudioCodec = transcodingProfile.AudioCodec.Split(',')[0];
                 playlistItem.AudioCodec = transcodingProfile.AudioCodec.Split(',')[0];
                 playlistItem.VideoCodec = transcodingProfile.VideoCodec;
                 playlistItem.VideoCodec = transcodingProfile.VideoCodec;
-                playlistItem.Protocol = transcodingProfile.Protocol;
+                playlistItem.SubProtocol = transcodingProfile.Protocol;
                 playlistItem.AudioStreamIndex = audioStreamIndex;
                 playlistItem.AudioStreamIndex = audioStreamIndex;
 
 
                 List<ProfileCondition> videoTranscodingConditions = new List<ProfileCondition>();
                 List<ProfileCondition> videoTranscodingConditions = new List<ProfileCondition>();
@@ -509,10 +542,13 @@ namespace MediaBrowser.Model.Dlna
             int? packetLength = videoStream == null ? null : videoStream.PacketLength;
             int? packetLength = videoStream == null ? null : videoStream.PacketLength;
             int? refFrames = videoStream == null ? null : videoStream.RefFrames;
             int? refFrames = videoStream == null ? null : videoStream.RefFrames;
 
 
+            int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio);
+            int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video);
+
             // Check container conditions
             // Check container conditions
             foreach (ProfileCondition i in conditions)
             foreach (ProfileCondition i in conditions)
             {
             {
-                if (!conditionProcessor.IsVideoConditionSatisfied(i, audioBitrate, audioChannels, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isCabac, refFrames))
+                if (!conditionProcessor.IsVideoConditionSatisfied(i, audioBitrate, audioChannels, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isCabac, refFrames, numVideoStreams, numAudioStreams))
                 {
                 {
                     return null;
                     return null;
                 }
                 }
@@ -539,7 +575,7 @@ namespace MediaBrowser.Model.Dlna
 
 
             foreach (ProfileCondition i in conditions)
             foreach (ProfileCondition i in conditions)
             {
             {
-                if (!conditionProcessor.IsVideoConditionSatisfied(i, audioBitrate, audioChannels, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isCabac, refFrames))
+                if (!conditionProcessor.IsVideoConditionSatisfied(i, audioBitrate, audioChannels, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isCabac, refFrames, numVideoStreams, numAudioStreams))
                 {
                 {
                     return null;
                     return null;
                 }
                 }
@@ -568,7 +604,8 @@ namespace MediaBrowser.Model.Dlna
 
 
                 foreach (ProfileCondition i in conditions)
                 foreach (ProfileCondition i in conditions)
                 {
                 {
-                    if (!conditionProcessor.IsVideoAudioConditionSatisfied(i, audioChannels, audioBitrate, audioProfile))
+                    bool? isSecondaryAudio = audioStream == null ? null : mediaSource.IsSecondaryAudio(audioStream);
+                    if (!conditionProcessor.IsVideoAudioConditionSatisfied(i, audioChannels, audioBitrate, audioProfile, isSecondaryAudio))
                     {
                     {
                         return null;
                         return null;
                     }
                     }
@@ -628,8 +665,20 @@ namespace MediaBrowser.Model.Dlna
             // Look for an external profile that matches the stream type (text/graphical)
             // Look for an external profile that matches the stream type (text/graphical)
             foreach (SubtitleProfile profile in subtitleProfiles)
             foreach (SubtitleProfile profile in subtitleProfiles)
             {
             {
+                bool requiresConversion = !StringHelper.EqualsIgnoreCase(subtitleStream.Codec, profile.Format);
+
+                if (!profile.SupportsLanguage(subtitleStream.Language))
+                {
+                    continue;
+                }
+
                 if (profile.Method == SubtitleDeliveryMethod.External && subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format))
                 if (profile.Method == SubtitleDeliveryMethod.External && subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format))
                 {
                 {
+                    if (!requiresConversion)
+                    {
+                        return profile;
+                    }
+
                     if (subtitleStream.SupportsExternalStream)
                     if (subtitleStream.SupportsExternalStream)
                     {
                     {
                         return profile;
                         return profile;
@@ -645,8 +694,20 @@ namespace MediaBrowser.Model.Dlna
 
 
             foreach (SubtitleProfile profile in subtitleProfiles)
             foreach (SubtitleProfile profile in subtitleProfiles)
             {
             {
+                bool requiresConversion = !StringHelper.EqualsIgnoreCase(subtitleStream.Codec, profile.Format);
+
+                if (!profile.SupportsLanguage(subtitleStream.Language))
+                {
+                    continue;
+                }
+
                 if (profile.Method == SubtitleDeliveryMethod.Embed && subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format))
                 if (profile.Method == SubtitleDeliveryMethod.Embed && subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format))
                 {
                 {
+                    if (!requiresConversion)
+                    {
+                        return profile;
+                    }
+
                     return profile;
                     return profile;
                 }
                 }
             }
             }
@@ -756,6 +817,9 @@ namespace MediaBrowser.Model.Dlna
                     case ProfileConditionValue.AudioProfile:
                     case ProfileConditionValue.AudioProfile:
                     case ProfileConditionValue.Has64BitOffsets:
                     case ProfileConditionValue.Has64BitOffsets:
                     case ProfileConditionValue.PacketLength:
                     case ProfileConditionValue.PacketLength:
+                    case ProfileConditionValue.NumAudioStreams:
+                    case ProfileConditionValue.NumVideoStreams:
+                    case ProfileConditionValue.IsSecondaryAudio:
                     case ProfileConditionValue.VideoTimestamp:
                     case ProfileConditionValue.VideoTimestamp:
                         {
                         {
                             // Not supported yet
                             // Not supported yet

+ 190 - 111
MediaBrowser.Model/Dlna/StreamInfo.cs

@@ -24,7 +24,7 @@ namespace MediaBrowser.Model.Dlna
 
 
         public string Container { get; set; }
         public string Container { get; set; }
 
 
-        public string Protocol { get; set; }
+        public string SubProtocol { get; set; }
 
 
         public long StartPositionTicks { get; set; }
         public long StartPositionTicks { get; set; }
 
 
@@ -51,7 +51,7 @@ namespace MediaBrowser.Model.Dlna
 
 
         public int? MaxVideoBitDepth { get; set; }
         public int? MaxVideoBitDepth { get; set; }
         public int? MaxRefFrames { get; set; }
         public int? MaxRefFrames { get; set; }
-        
+
         public float? MaxFramerate { get; set; }
         public float? MaxFramerate { get; set; }
 
 
         public DeviceProfile DeviceProfile { get; set; }
         public DeviceProfile DeviceProfile { get; set; }
@@ -69,7 +69,7 @@ namespace MediaBrowser.Model.Dlna
         public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
         public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
         public string SubtitleFormat { get; set; }
         public string SubtitleFormat { get; set; }
 
 
-        public LiveMediaInfoResult PlaybackInfo { get; set; }
+        public PlaybackInfoResponse PlaybackInfo { get; set; }
 
 
         public string MediaSourceId
         public string MediaSourceId
         {
         {
@@ -81,7 +81,8 @@ namespace MediaBrowser.Model.Dlna
 
 
         public bool IsDirectStream
         public bool IsDirectStream
         {
         {
-            get { 
+            get
+            {
                 return PlayMethod == PlayMethod.DirectStream ||
                 return PlayMethod == PlayMethod.DirectStream ||
                     PlayMethod == PlayMethod.DirectPlay;
                     PlayMethod == PlayMethod.DirectPlay;
             }
             }
@@ -89,7 +90,47 @@ namespace MediaBrowser.Model.Dlna
 
 
         public string ToUrl(string baseUrl, string accessToken)
         public string ToUrl(string baseUrl, string accessToken)
         {
         {
-            return ToDlnaUrl(baseUrl, accessToken);
+            if (PlayMethod == PlayMethod.DirectPlay)
+            {
+                return MediaSource.Path;
+            }
+
+            if (string.IsNullOrEmpty(baseUrl))
+            {
+                throw new ArgumentNullException(baseUrl);
+            }
+
+            List<string> list = new List<string>();
+            foreach (NameValuePair pair in BuildParams(this, accessToken))
+            {
+                if (string.IsNullOrEmpty(pair.Value))
+                {
+                    continue;
+                }
+
+                // Try to keep the url clean by omitting defaults
+                if (StringHelper.EqualsIgnoreCase(pair.Name, "StartTimeTicks") &&
+                    StringHelper.EqualsIgnoreCase(pair.Value, "0"))
+                {
+                    continue;
+                }
+                if (StringHelper.EqualsIgnoreCase(pair.Name, "SubtitleStreamIndex") &&
+                    StringHelper.EqualsIgnoreCase(pair.Value, "-1"))
+                {
+                    continue;
+                }
+                if (StringHelper.EqualsIgnoreCase(pair.Name, "Static") &&
+                    StringHelper.EqualsIgnoreCase(pair.Value, "false"))
+                {
+                    continue;
+                }
+
+                list.Add(string.Format("{0}={1}", pair.Name, pair.Value));
+            }
+
+            string queryString = string.Join("&", list.ToArray());
+
+            return GetUrl(baseUrl, queryString);
         }
         }
 
 
         public string ToDlnaUrl(string baseUrl, string accessToken)
         public string ToDlnaUrl(string baseUrl, string accessToken)
@@ -99,115 +140,111 @@ namespace MediaBrowser.Model.Dlna
                 return MediaSource.Path;
                 return MediaSource.Path;
             }
             }
 
 
+            string dlnaCommand = BuildDlnaParam(this, accessToken);
+            return GetUrl(baseUrl, dlnaCommand);
+        }
+
+        private string GetUrl(string baseUrl, string queryString)
+        {
             if (string.IsNullOrEmpty(baseUrl))
             if (string.IsNullOrEmpty(baseUrl))
             {
             {
                 throw new ArgumentNullException(baseUrl);
                 throw new ArgumentNullException(baseUrl);
             }
             }
 
 
-            string dlnaCommand = BuildDlnaParam(this);
-
             string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
             string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
 
 
             baseUrl = baseUrl.TrimEnd('/');
             baseUrl = baseUrl.TrimEnd('/');
 
 
             if (MediaType == DlnaProfileType.Audio)
             if (MediaType == DlnaProfileType.Audio)
             {
             {
-                return string.Format("{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, dlnaCommand);
+                return string.Format("{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
             }
             }
 
 
-            if (StringHelper.EqualsIgnoreCase(Protocol, "hls"))
+            if (StringHelper.EqualsIgnoreCase(SubProtocol, "hls"))
             {
             {
-                return string.Format("{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, dlnaCommand);
+                return string.Format("{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
             }
             }
 
 
-            return string.Format("{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, dlnaCommand);
+            return string.Format("{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
         }
         }
 
 
-        private static string BuildDlnaParam(StreamInfo item)
+        private static string BuildDlnaParam(StreamInfo item, string accessToken)
         {
         {
-            List<string> list = new List<string>
-            {
-                item.DeviceProfileId ?? string.Empty,
-                item.DeviceId ?? string.Empty,
-                item.MediaSourceId ?? string.Empty,
-                (item.IsDirectStream).ToString().ToLower(),
-                item.VideoCodec ?? string.Empty,
-                item.AudioCodec ?? string.Empty,
-                item.AudioStreamIndex.HasValue ? StringHelper.ToStringCultureInvariant(item.AudioStreamIndex.Value) : string.Empty,
-                item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? StringHelper.ToStringCultureInvariant(item.SubtitleStreamIndex.Value) : string.Empty,
-                item.VideoBitrate.HasValue ? StringHelper.ToStringCultureInvariant(item.VideoBitrate.Value) : string.Empty,
-                item.AudioBitrate.HasValue ? StringHelper.ToStringCultureInvariant(item.AudioBitrate.Value) : string.Empty,
-                item.MaxAudioChannels.HasValue ? StringHelper.ToStringCultureInvariant(item.MaxAudioChannels.Value) : string.Empty,
-                item.MaxFramerate.HasValue ? StringHelper.ToStringCultureInvariant(item.MaxFramerate.Value) : string.Empty,
-                item.MaxWidth.HasValue ? StringHelper.ToStringCultureInvariant(item.MaxWidth.Value) : string.Empty,
-                item.MaxHeight.HasValue ? StringHelper.ToStringCultureInvariant(item.MaxHeight.Value) : string.Empty,
-                StringHelper.ToStringCultureInvariant(item.StartPositionTicks),
-                item.VideoLevel.HasValue ? StringHelper.ToStringCultureInvariant(item.VideoLevel.Value) : string.Empty
-            };
+            List<string> list = new List<string>();
 
 
-            list.Add(item.IsDirectStream ? string.Empty : DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture));
-            list.Add(item.MaxRefFrames.HasValue ? StringHelper.ToStringCultureInvariant(item.MaxRefFrames.Value) : string.Empty);
-            list.Add(item.MaxVideoBitDepth.HasValue ? StringHelper.ToStringCultureInvariant(item.MaxVideoBitDepth.Value) : string.Empty);
-            list.Add(item.VideoProfile ?? string.Empty);
-            list.Add(item.Cabac.HasValue ? item.Cabac.Value.ToString() : string.Empty);
+            foreach (NameValuePair pair in BuildParams(item, accessToken))
+            {
+                list.Add(pair.Value);
+            }
 
 
-            string streamId = item.PlaybackInfo == null ? null : item.PlaybackInfo.StreamId;
-            list.Add(streamId ?? string.Empty);
-            
             return string.Format("Params={0}", string.Join(";", list.ToArray()));
             return string.Format("Params={0}", string.Join(";", list.ToArray()));
         }
         }
 
 
-        public List<SubtitleStreamInfo> GetExternalSubtitles(bool includeSelectedTrackOnly)
-        {
-            List<SubtitleStreamInfo> list = new List<SubtitleStreamInfo>();
+        private static List<NameValuePair> BuildParams(StreamInfo item, string accessToken)
+        {
+            List<NameValuePair> list = new List<NameValuePair>();
+
+            list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
+            list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
+            list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
+            list.Add(new NameValuePair("Static", (item.IsDirectStream).ToString().ToLower()));
+            list.Add(new NameValuePair("VideoCodec", item.VideoCodec ?? string.Empty));
+            list.Add(new NameValuePair("AudioCodec", item.AudioCodec ?? string.Empty));
+            list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? StringHelper.ToStringCultureInvariant(item.AudioStreamIndex.Value) : string.Empty));
+            list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? StringHelper.ToStringCultureInvariant(item.SubtitleStreamIndex.Value) : string.Empty));
+            list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? StringHelper.ToStringCultureInvariant(item.VideoBitrate.Value) : string.Empty));
+            list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? StringHelper.ToStringCultureInvariant(item.AudioBitrate.Value) : string.Empty));
+            list.Add(new NameValuePair("MaxAudioChannels", item.MaxAudioChannels.HasValue ? StringHelper.ToStringCultureInvariant(item.MaxAudioChannels.Value) : string.Empty));
+            list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? StringHelper.ToStringCultureInvariant(item.MaxFramerate.Value) : string.Empty));
+            list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? StringHelper.ToStringCultureInvariant(item.MaxWidth.Value) : string.Empty));
+            list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? StringHelper.ToStringCultureInvariant(item.MaxHeight.Value) : string.Empty));
+            list.Add(new NameValuePair("StartTimeTicks", StringHelper.ToStringCultureInvariant(item.StartPositionTicks)));
+            list.Add(new NameValuePair("Level", item.VideoLevel.HasValue ? StringHelper.ToStringCultureInvariant(item.VideoLevel.Value) : string.Empty));
+
+            list.Add(new NameValuePair("ClientTime", item.IsDirectStream ? string.Empty : DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture)));
+            list.Add(new NameValuePair("MaxRefFrames", item.MaxRefFrames.HasValue ? StringHelper.ToStringCultureInvariant(item.MaxRefFrames.Value) : string.Empty));
+            list.Add(new NameValuePair("MaxVideoBitDepth", item.MaxVideoBitDepth.HasValue ? StringHelper.ToStringCultureInvariant(item.MaxVideoBitDepth.Value) : string.Empty));
+            list.Add(new NameValuePair("Profile", item.VideoProfile ?? string.Empty));
+            list.Add(new NameValuePair("Cabac", item.Cabac.HasValue ? item.Cabac.Value.ToString() : string.Empty));
+
+            string playSessionId = item.PlaybackInfo == null ? null : item.PlaybackInfo.PlaySessionId;
+            list.Add(new NameValuePair("PlaySessionId", playSessionId ?? string.Empty));
+            list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
+
+            string liveStreamId = item.MediaSource == null ? null : item.MediaSource.LiveStreamId;
+            list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
 
 
-            // First add the selected track
-            if (SubtitleStreamIndex.HasValue)
-            {
-                foreach (MediaStream stream in MediaSource.MediaStreams)
-                {
-                    if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value)
-                    {
-                        SubtitleStreamInfo info = GetSubtitleStreamInfo(stream);
+            return list;
+        }
 
 
-                        if (info != null)
-                        {
-                            list.Add(info);
-                        }
-                    }
-                }
-            }
+        public List<SubtitleStreamInfo> GetExternalSubtitles(bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
+        {
+            List<SubtitleStreamInfo> list = GetSubtitleProfiles(includeSelectedTrackOnly, enableAllProfiles, baseUrl, accessToken);
+            List<SubtitleStreamInfo> newList = new List<SubtitleStreamInfo>();
 
 
-            if (!includeSelectedTrackOnly)
+            // First add the selected track
+            foreach (SubtitleStreamInfo stream in list)
             {
             {
-                foreach (MediaStream stream in MediaSource.MediaStreams)
+                if (stream.DeliveryMethod == SubtitleDeliveryMethod.External)
                 {
                 {
-                    if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value))
-                    {
-                        SubtitleStreamInfo info = GetSubtitleStreamInfo(stream);
-
-                        if (info != null)
-                        {
-                            list.Add(info);
-                        }
-                    }
+                    newList.Add(stream);
                 }
                 }
             }
             }
 
 
-            return list;
+            return newList;
         }
         }
 
 
-        public List<SubtitleStreamInfo> GetExternalSubtitles(string baseUrl, string accessToken, bool includeSelectedTrackOnly)
+        public List<SubtitleStreamInfo> GetSubtitleProfiles(bool includeSelectedTrackOnly, string baseUrl, string accessToken)
         {
         {
-            if (string.IsNullOrEmpty(baseUrl))
-            {
-                throw new ArgumentNullException(baseUrl);
-            }
+            return GetSubtitleProfiles(includeSelectedTrackOnly, false, baseUrl, accessToken);
+        }
 
 
+        public List<SubtitleStreamInfo> GetSubtitleProfiles(bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
+        {
             List<SubtitleStreamInfo> list = new List<SubtitleStreamInfo>();
             List<SubtitleStreamInfo> list = new List<SubtitleStreamInfo>();
 
 
             // HLS will preserve timestamps so we can just grab the full subtitle stream
             // HLS will preserve timestamps so we can just grab the full subtitle stream
-            long startPositionTicks = StringHelper.EqualsIgnoreCase(Protocol, "hls")
+            long startPositionTicks = StringHelper.EqualsIgnoreCase(SubProtocol, "hls")
                 ? 0
                 ? 0
                 : StartPositionTicks;
                 : StartPositionTicks;
 
 
@@ -218,12 +255,7 @@ namespace MediaBrowser.Model.Dlna
                 {
                 {
                     if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value)
                     if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value)
                     {
                     {
-                        SubtitleStreamInfo info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks);
-
-                        if (info != null)
-                        {
-                            list.Add(info);
-                        }
+                        AddSubtitleProfiles(list, stream, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
                     }
                     }
                 }
                 }
             }
             }
@@ -234,54 +266,65 @@ namespace MediaBrowser.Model.Dlna
                 {
                 {
                     if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value))
                     if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value))
                     {
                     {
-                        SubtitleStreamInfo info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks);
-
-                        if (info != null)
-                        {
-                            list.Add(info);
-                        }
+                        AddSubtitleProfiles(list, stream, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
                     }
                     }
                 }
                 }
             }
             }
-            
+
             return list;
             return list;
         }
         }
 
 
-        private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks)
+        private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, bool enableAllProfiles, string baseUrl, string accessToken, long startPositionTicks)
         {
         {
-            SubtitleStreamInfo info = GetSubtitleStreamInfo(stream);
-
-            if (info != null)
+            if (enableAllProfiles)
             {
             {
-                info.Url = string.Format("{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
-                    baseUrl,
-                    ItemId,
-                    MediaSourceId,
-                    StringHelper.ToStringCultureInvariant(stream.Index),
-                    StringHelper.ToStringCultureInvariant(startPositionTicks),
-                    SubtitleFormat);
+                foreach (SubtitleProfile profile in DeviceProfile.SubtitleProfiles)
+                {
+                    SubtitleStreamInfo info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile });
+
+                    list.Add(info);
+                }
             }
             }
+            else
+            {
+                SubtitleStreamInfo info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles);
 
 
-            return info;
+                list.Add(info);
+            }
         }
         }
 
 
-        private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream)
+        private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles)
         {
         {
-            SubtitleProfile subtitleProfile = StreamBuilder.GetSubtitleProfile(stream, DeviceProfile.SubtitleProfiles, Context);
-
-            if (subtitleProfile.Method != SubtitleDeliveryMethod.External)
-            {
-                return null;
-            }
-
-            return new SubtitleStreamInfo
+            SubtitleProfile subtitleProfile = StreamBuilder.GetSubtitleProfile(stream, subtitleProfiles, Context);
+            SubtitleStreamInfo info = new SubtitleStreamInfo
             {
             {
                 IsForced = stream.IsForced,
                 IsForced = stream.IsForced,
                 Language = stream.Language,
                 Language = stream.Language,
                 Name = stream.Language ?? "Unknown",
                 Name = stream.Language ?? "Unknown",
-                Format = SubtitleFormat,
-                Index = stream.Index
+                Format = subtitleProfile.Format,
+                Index = stream.Index,
+                DeliveryMethod = subtitleProfile.Method
             };
             };
+
+            if (info.DeliveryMethod == SubtitleDeliveryMethod.External)
+            {
+                if (MediaSource.Protocol == MediaProtocol.File || !StringHelper.EqualsIgnoreCase(stream.Codec, subtitleProfile.Format))
+                {
+                    info.Url = string.Format("{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
+                        baseUrl,
+                        ItemId,
+                        MediaSourceId,
+                        StringHelper.ToStringCultureInvariant(stream.Index),
+                        StringHelper.ToStringCultureInvariant(startPositionTicks),
+                        subtitleProfile.Format);
+                }
+                else
+                {
+                    info.Url = stream.Path;
+                }
+            }
+
+            return info;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -613,6 +656,42 @@ namespace MediaBrowser.Model.Dlna
             }
             }
         }
         }
 
 
+        public int? TargetVideoStreamCount
+        {
+            get
+            {
+                if (IsDirectStream)
+                {
+                    return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue);
+                }
+                return GetMediaStreamCount(MediaStreamType.Video, 1);
+            }
+        }
+
+        public int? TargetAudioStreamCount
+        {
+            get
+            {
+                if (IsDirectStream)
+                {
+                    return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue);
+                }
+                return GetMediaStreamCount(MediaStreamType.Audio, 1);
+            }
+        }
+
+        private int? GetMediaStreamCount(MediaStreamType type, int limit)
+        {
+            var count = MediaSource.GetStreamCount(type);
+
+            if (count.HasValue)
+            {
+                count = Math.Min(count.Value, limit);
+            }
+
+            return count;
+        }
+
         public List<MediaStream> GetSelectableAudioStreams()
         public List<MediaStream> GetSelectableAudioStreams()
         {
         {
             return GetSelectableStreams(MediaStreamType.Audio);
             return GetSelectableStreams(MediaStreamType.Audio);

+ 47 - 0
MediaBrowser.Model/Dlna/StreamInfoSorter.cs

@@ -0,0 +1,47 @@
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Session;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Model.Dlna
+{
+    public class StreamInfoSorter
+    {
+        public static List<StreamInfo> SortMediaSources(List<StreamInfo> streams)
+        {
+            return streams.OrderBy(i =>
+            {
+                // Nothing beats direct playing a file
+                if (i.PlayMethod == PlayMethod.DirectPlay && i.MediaSource.Protocol == MediaProtocol.File)
+                {
+                    return 0;
+                }
+
+                return 1;
+
+            }).ThenBy(i =>
+            {
+                switch (i.PlayMethod)
+                {
+                    // Let's assume direct streaming a file is just as desirable as direct playing a remote url
+                    case PlayMethod.DirectStream:
+                    case PlayMethod.DirectPlay:
+                        return 0;
+                    default:
+                        return 1;
+                }
+
+            }).ThenBy(i =>
+            {
+                switch (i.MediaSource.Protocol)
+                {
+                    case MediaProtocol.File:
+                        return 0;
+                    default:
+                        return 1;
+                }
+
+            }).ToList();
+        }
+    }
+}

+ 26 - 1
MediaBrowser.Model/Dlna/SubtitleProfile.cs

@@ -1,4 +1,6 @@
-using System.Xml.Serialization;
+using MediaBrowser.Model.Extensions;
+using System.Collections.Generic;
+using System.Xml.Serialization;
 
 
 namespace MediaBrowser.Model.Dlna
 namespace MediaBrowser.Model.Dlna
 {
 {
@@ -13,5 +15,28 @@ namespace MediaBrowser.Model.Dlna
         [XmlAttribute("didlMode")]
         [XmlAttribute("didlMode")]
         public string DidlMode { get; set; }
         public string DidlMode { get; set; }
 
 
+        [XmlAttribute("language")]
+        public string Language { get; set; }
+
+        public List<string> GetLanguages()
+        {
+            List<string> list = new List<string>();
+            foreach (string i in (Language ?? string.Empty).Split(','))
+            {
+                if (!string.IsNullOrEmpty(i)) list.Add(i);
+            }
+            return list;
+        }
+
+        public bool SupportsLanguage(string language)
+        {
+            if (string.IsNullOrEmpty(language))
+            {
+                language = "und";
+            }
+
+            List<string> languages = GetLanguages();
+            return languages.Count == 0 || ListHelper.ContainsIgnoreCase(languages, language);
+        }
     }
     }
 }
 }

+ 1 - 0
MediaBrowser.Model/Dlna/SubtitleStreamInfo.cs

@@ -8,5 +8,6 @@ namespace MediaBrowser.Model.Dlna
         public bool IsForced { get; set; }
         public bool IsForced { get; set; }
         public string Format { get; set; }
         public string Format { get; set; }
         public int Index { get; set; }
         public int Index { get; set; }
+        public SubtitleDeliveryMethod DeliveryMethod { get; set; }
     }
     }
 }
 }

+ 47 - 0
MediaBrowser.Model/Dto/MediaSourceInfo.cs

@@ -24,6 +24,13 @@ namespace MediaBrowser.Model.Dto
         public bool ReadAtNativeFramerate { get; set; }
         public bool ReadAtNativeFramerate { get; set; }
         public bool SupportsTranscoding { get; set; }
         public bool SupportsTranscoding { get; set; }
         public bool SupportsDirectStream { get; set; }
         public bool SupportsDirectStream { get; set; }
+        public bool SupportsDirectPlay { get; set; }
+
+        public bool RequiresOpening { get; set; }
+        public string OpenToken { get; set; }
+        public bool RequiresClosing { get; set; }
+        public string LiveStreamId { get; set; }
+        public int? BufferMs { get; set; }
 
 
         public VideoType? VideoType { get; set; }
         public VideoType? VideoType { get; set; }
 
 
@@ -41,6 +48,10 @@ namespace MediaBrowser.Model.Dto
         public TransportStreamTimestamp? Timestamp { get; set; }
         public TransportStreamTimestamp? Timestamp { get; set; }
         public Dictionary<string, string> RequiredHttpHeaders { get; set; }
         public Dictionary<string, string> RequiredHttpHeaders { get; set; }
 
 
+        public string TranscodingUrl { get; set; }
+        public string TranscodingSubProtocol { get; set; }
+        public string TranscodingContainer { get; set; }
+
         public MediaSourceInfo()
         public MediaSourceInfo()
         {
         {
             Formats = new List<string>();
             Formats = new List<string>();
@@ -49,6 +60,7 @@ namespace MediaBrowser.Model.Dto
             PlayableStreamFileNames = new List<string>();
             PlayableStreamFileNames = new List<string>();
             SupportsTranscoding = true;
             SupportsTranscoding = true;
             SupportsDirectStream = true;
             SupportsDirectStream = true;
+            SupportsDirectPlay = true;
         }
         }
 
 
         public int? DefaultAudioStreamIndex { get; set; }
         public int? DefaultAudioStreamIndex { get; set; }
@@ -123,5 +135,40 @@ namespace MediaBrowser.Model.Dto
 
 
             return null;
             return null;
         }
         }
+
+        public int? GetStreamCount(MediaStreamType type)
+        {
+            int numMatches = 0;
+            int numStreams = 0;
+
+            foreach (MediaStream i in MediaStreams)
+            {
+                numStreams++;
+                if (i.Type == type)
+                {
+                    numMatches++;
+                }
+            }
+
+            if (numStreams == 0)
+            {
+                return null;
+            }
+
+            return numMatches;
+        }
+
+        public bool? IsSecondaryAudio(MediaStream stream)
+        {
+            foreach (MediaStream currentStream in MediaStreams)
+            {
+                if (currentStream.Type == MediaStreamType.Audio)
+                {
+                    return currentStream.Index != stream.Index;
+                }
+            }
+
+            return null;
+        }
     }
     }
 }
 }

+ 1 - 1
MediaBrowser.Model/Dto/MediaSourceType.cs

@@ -4,6 +4,6 @@ namespace MediaBrowser.Model.Dto
     {
     {
         Default = 0,
         Default = 0,
         Grouping = 1,
         Grouping = 1,
-        Cache = 2
+        Placeholder = 2
     }
     }
 }
 }

+ 11 - 0
MediaBrowser.Model/Dto/NameValuePair.cs

@@ -3,6 +3,17 @@ namespace MediaBrowser.Model.Dto
 {
 {
     public class NameValuePair
     public class NameValuePair
     {
     {
+        public NameValuePair()
+        {
+            
+        }
+
+        public NameValuePair(string name, string value)
+        {
+            Name = name;
+            Value = value;
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the name.
         /// Gets or sets the name.
         /// </summary>
         /// </summary>

+ 30 - 1
MediaBrowser.Model/Entities/MediaStream.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Extensions;
 using System.Diagnostics;
 using System.Diagnostics;
 
 
 namespace MediaBrowser.Model.Entities
 namespace MediaBrowser.Model.Entities
@@ -129,18 +130,40 @@ namespace MediaBrowser.Model.Entities
         /// <value>The index.</value>
         /// <value>The index.</value>
         public int Index { get; set; }
         public int Index { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the score.
+        /// </summary>
+        /// <value>The score.</value>
+        public int? Score { get; set; }
+        
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating whether this instance is external.
         /// Gets or sets a value indicating whether this instance is external.
         /// </summary>
         /// </summary>
         /// <value><c>true</c> if this instance is external; otherwise, <c>false</c>.</value>
         /// <value><c>true</c> if this instance is external; otherwise, <c>false</c>.</value>
         public bool IsExternal { get; set; }
         public bool IsExternal { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the method.
+        /// </summary>
+        /// <value>The method.</value>
+        public SubtitleDeliveryMethod? DeliveryMethod { get; set; }
+        /// <summary>
+        /// Gets or sets the delivery URL.
+        /// </summary>
+        /// <value>The delivery URL.</value>
+        public string DeliveryUrl { get; set; }
+
         public bool IsTextSubtitleStream
         public bool IsTextSubtitleStream
         {
         {
             get
             get
             {
             {
                 if (Type != MediaStreamType.Subtitle) return false;
                 if (Type != MediaStreamType.Subtitle) return false;
 
 
+                if (string.IsNullOrEmpty(Codec) && !IsExternal)
+                {
+                    return false;
+                }
+
                 return IsTextFormat(Codec);
                 return IsTextFormat(Codec);
             }
             }
         }
         }
@@ -168,6 +191,12 @@ namespace MediaBrowser.Model.Entities
         /// <value>The filename.</value>
         /// <value>The filename.</value>
         public string Path { get; set; }
         public string Path { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the external identifier.
+        /// </summary>
+        /// <value>The external identifier.</value>
+        public string ExternalId { get; set; }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the pixel format.
         /// Gets or sets the pixel format.
         /// </summary>
         /// </summary>

+ 5 - 1
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -126,6 +126,7 @@
     <Compile Include="Devices\DevicesOptions.cs" />
     <Compile Include="Devices\DevicesOptions.cs" />
     <Compile Include="Dlna\EncodingContext.cs" />
     <Compile Include="Dlna\EncodingContext.cs" />
     <Compile Include="Dlna\ILocalPlayer.cs" />
     <Compile Include="Dlna\ILocalPlayer.cs" />
+    <Compile Include="Dlna\StreamInfoSorter.cs" />
     <Compile Include="Dlna\NullLocalPlayer.cs" />
     <Compile Include="Dlna\NullLocalPlayer.cs" />
     <Compile Include="Dlna\PlaybackErrorCode.cs" />
     <Compile Include="Dlna\PlaybackErrorCode.cs" />
     <Compile Include="Dlna\PlaybackException.cs" />
     <Compile Include="Dlna\PlaybackException.cs" />
@@ -140,7 +141,10 @@
     <Compile Include="Dto\MetadataEditorInfo.cs" />
     <Compile Include="Dto\MetadataEditorInfo.cs" />
     <Compile Include="Dto\NameIdPair.cs" />
     <Compile Include="Dto\NameIdPair.cs" />
     <Compile Include="Dto\NameValuePair.cs" />
     <Compile Include="Dto\NameValuePair.cs" />
-    <Compile Include="MediaInfo\LiveMediaInfoResult.cs" />
+    <Compile Include="MediaInfo\LiveStreamRequest.cs" />
+    <Compile Include="MediaInfo\LiveStreamResponse.cs" />
+    <Compile Include="MediaInfo\PlaybackInfoRequest.cs" />
+    <Compile Include="MediaInfo\PlaybackInfoResponse.cs" />
     <Compile Include="Dto\MediaSourceType.cs" />
     <Compile Include="Dto\MediaSourceType.cs" />
     <Compile Include="Configuration\DynamicDayOfWeek.cs" />
     <Compile Include="Configuration\DynamicDayOfWeek.cs" />
     <Compile Include="Entities\ExtraType.cs" />
     <Compile Include="Entities\ExtraType.cs" />

+ 35 - 0
MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs

@@ -0,0 +1,35 @@
+using MediaBrowser.Model.Dlna;
+
+namespace MediaBrowser.Model.MediaInfo
+{
+    public class LiveStreamRequest
+    {
+        public string OpenToken { get; set; }
+        public string UserId { get; set; }
+        public int? MaxStreamingBitrate { get; set; }
+        public long? StartTimeTicks { get; set; }
+        public int? AudioStreamIndex { get; set; }
+        public int? SubtitleStreamIndex { get; set; }
+        public string ItemId { get; set; }
+        public DeviceProfile DeviceProfile { get; set; }
+
+        public LiveStreamRequest()
+        {
+            
+        }
+
+        public LiveStreamRequest(AudioOptions options)
+        {
+            MaxStreamingBitrate = options.MaxBitrate;
+            ItemId = options.ItemId;
+            DeviceProfile = options.Profile;
+
+            VideoOptions videoOptions = options as VideoOptions;
+            if (videoOptions != null)
+            {
+                AudioStreamIndex = videoOptions.AudioStreamIndex;
+                SubtitleStreamIndex = videoOptions.SubtitleStreamIndex;
+            }
+        }
+    }
+}

+ 9 - 0
MediaBrowser.Model/MediaInfo/LiveStreamResponse.cs

@@ -0,0 +1,9 @@
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Model.MediaInfo
+{
+    public class LiveStreamResponse
+    {
+        public MediaSourceInfo MediaSource { get; set; }
+    }
+}

+ 25 - 0
MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs

@@ -0,0 +1,25 @@
+using MediaBrowser.Model.Dlna;
+
+namespace MediaBrowser.Model.MediaInfo
+{
+    public class PlaybackInfoRequest
+    {
+        public string Id { get; set; }
+
+        public string UserId { get; set; }
+
+        public int? MaxStreamingBitrate { get; set; }
+
+        public long? StartTimeTicks { get; set; }
+
+        public int? AudioStreamIndex { get; set; }
+
+        public int? SubtitleStreamIndex { get; set; }
+
+        public string MediaSourceId { get; set; }
+
+        public string LiveStreamId { get; set; }
+        
+        public DeviceProfile DeviceProfile { get; set; }
+    }
+}

+ 5 - 5
MediaBrowser.Model/MediaInfo/LiveMediaInfoResult.cs → MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs

@@ -4,7 +4,7 @@ using System.Collections.Generic;
 
 
 namespace MediaBrowser.Model.MediaInfo
 namespace MediaBrowser.Model.MediaInfo
 {
 {
-    public class LiveMediaInfoResult
+    public class PlaybackInfoResponse
     {
     {
         /// <summary>
         /// <summary>
         /// Gets or sets the media sources.
         /// Gets or sets the media sources.
@@ -13,10 +13,10 @@ namespace MediaBrowser.Model.MediaInfo
         public List<MediaSourceInfo> MediaSources { get; set; }
         public List<MediaSourceInfo> MediaSources { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets the stream identifier.
+        /// Gets or sets the play session identifier.
         /// </summary>
         /// </summary>
-        /// <value>The stream identifier.</value>
-        public string StreamId { get; set; }
+        /// <value>The play session identifier.</value>
+        public string PlaySessionId { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the error code.
         /// Gets or sets the error code.
@@ -24,7 +24,7 @@ namespace MediaBrowser.Model.MediaInfo
         /// <value>The error code.</value>
         /// <value>The error code.</value>
         public PlaybackErrorCode? ErrorCode { get; set; }
         public PlaybackErrorCode? ErrorCode { get; set; }
 
 
-        public LiveMediaInfoResult()
+        public PlaybackInfoResponse()
         {
         {
             MediaSources = new List<MediaSourceInfo>();
             MediaSources = new List<MediaSourceInfo>();
         }
         }

+ 5 - 0
MediaBrowser.Model/Querying/ItemFields.cs

@@ -95,6 +95,11 @@
         /// </summary>
         /// </summary>
         IndexOptions,
         IndexOptions,
 
 
+        /// <summary>
+        /// The item counts
+        /// </summary>
+        ItemCounts,
+
         /// <summary>
         /// <summary>
         /// The keywords
         /// The keywords
         /// </summary>
         /// </summary>

+ 5 - 0
MediaBrowser.Model/Session/PlaybackProgressInfo.cs

@@ -78,5 +78,10 @@ namespace MediaBrowser.Model.Session
         /// </summary>
         /// </summary>
         /// <value>The play method.</value>
         /// <value>The play method.</value>
         public PlayMethod PlayMethod { get; set; }
         public PlayMethod PlayMethod { get; set; }
+        /// <summary>
+        /// Gets or sets the live stream identifier.
+        /// </summary>
+        /// <value>The live stream identifier.</value>
+        public string LiveStreamId { get; set; }
     }
     }
 }
 }

+ 5 - 0
MediaBrowser.Model/Session/PlaybackStopInfo.cs

@@ -36,5 +36,10 @@ namespace MediaBrowser.Model.Session
         /// </summary>
         /// </summary>
         /// <value>The position ticks.</value>
         /// <value>The position ticks.</value>
         public long? PositionTicks { get; set; }
         public long? PositionTicks { get; set; }
+        /// <summary>
+        /// Gets or sets the live stream identifier.
+        /// </summary>
+        /// <value>The live stream identifier.</value>
+        public string LiveStreamId { get; set; }
     }
     }
 }
 }

+ 11 - 0
MediaBrowser.Model/Sync/LocalItem.cs

@@ -31,13 +31,24 @@ namespace MediaBrowser.Model.Sync
         /// <value>The item identifier.</value>
         /// <value>The item identifier.</value>
         public string ItemId { get; set; }
         public string ItemId { get; set; }
         /// <summary>
         /// <summary>
+        /// Gets or sets the synchronize job item identifier.
+        /// </summary>
+        /// <value>The synchronize job item identifier.</value>
+        public string SyncJobItemId { get; set; }
+        /// <summary>
         /// Gets or sets the user ids with access.
         /// Gets or sets the user ids with access.
         /// </summary>
         /// </summary>
         /// <value>The user ids with access.</value>
         /// <value>The user ids with access.</value>
         public List<string> UserIdsWithAccess { get; set; }
         public List<string> UserIdsWithAccess { get; set; }
+        /// <summary>
+        /// Gets or sets the additional files.
+        /// </summary>
+        /// <value>The additional files.</value>
+        public List<string> AdditionalFiles { get; set; }
 
 
         public LocalItem()
         public LocalItem()
         {
         {
+            AdditionalFiles = new List<string>();
             UserIdsWithAccess = new List<string>();
             UserIdsWithAccess = new List<string>();
         }
         }
     }
     }

+ 1 - 0
MediaBrowser.Model/Sync/SyncDataRequest.cs

@@ -6,6 +6,7 @@ namespace MediaBrowser.Model.Sync
     {
     {
         public List<string> LocalItemIds { get; set; }
         public List<string> LocalItemIds { get; set; }
         public List<string> OfflineUserIds { get; set; }
         public List<string> OfflineUserIds { get; set; }
+        public List<string> SyncJobItemIds { get; set; }
 
 
         public string TargetId { get; set; }
         public string TargetId { get; set; }
 
 

+ 11 - 0
MediaBrowser.Model/Sync/SyncedItem.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
+using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 
 
 namespace MediaBrowser.Model.Sync
 namespace MediaBrowser.Model.Sync
@@ -16,6 +17,16 @@ namespace MediaBrowser.Model.Sync
         /// <value>The synchronize job identifier.</value>
         /// <value>The synchronize job identifier.</value>
         public string SyncJobId { get; set; }
         public string SyncJobId { get; set; }
         /// <summary>
         /// <summary>
+        /// Gets or sets the name of the synchronize job.
+        /// </summary>
+        /// <value>The name of the synchronize job.</value>
+        public string SyncJobName { get; set; }
+        /// <summary>
+        /// Gets or sets the synchronize job date created.
+        /// </summary>
+        /// <value>The synchronize job date created.</value>
+        public DateTime SyncJobDateCreated { get; set; }
+        /// <summary>
         /// Gets or sets the synchronize job item identifier.
         /// Gets or sets the synchronize job item identifier.
         /// </summary>
         /// </summary>
         /// <value>The synchronize job item identifier.</value>
         /// <value>The synchronize job item identifier.</value>

+ 1 - 1
MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs

@@ -96,7 +96,7 @@ namespace MediaBrowser.Providers.MediaInfo
             var album = item.Parent as MusicAlbum;
             var album = item.Parent as MusicAlbum;
 
 
             var filename = item.Album ?? string.Empty;
             var filename = item.Album ?? string.Empty;
-            filename += item.Artists.FirstOrDefault() ?? string.Empty;
+            filename += string.Join(",", item.Artists.ToArray());
             filename += album == null ? item.Id.ToString("N") + "_primary" + item.DateModified.Ticks : album.Id.ToString("N") + album.DateModified.Ticks + "_primary";
             filename += album == null ? item.Id.ToString("N") + "_primary" + item.DateModified.Ticks : album.Id.ToString("N") + album.DateModified.Ticks + "_primary";
 
 
             filename = filename.GetMD5() + ".jpg";
             filename = filename.GetMD5() + ".jpg";

+ 5 - 4
MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs

@@ -6,15 +6,14 @@ using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Controller.Subtitles;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Providers;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using MediaBrowser.Model.Providers;
 
 
 namespace MediaBrowser.Providers.MediaInfo
 namespace MediaBrowser.Providers.MediaInfo
 {
 {
@@ -23,14 +22,16 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly ISubtitleManager _subtitleManager;
         private readonly ISubtitleManager _subtitleManager;
+        private readonly IMediaSourceManager _mediaSourceManager;
         private readonly ILogger _logger;
         private readonly ILogger _logger;
 
 
-        public SubtitleScheduledTask(ILibraryManager libraryManager, IServerConfigurationManager config, ISubtitleManager subtitleManager, ILogger logger)
+        public SubtitleScheduledTask(ILibraryManager libraryManager, IServerConfigurationManager config, ISubtitleManager subtitleManager, ILogger logger, IMediaSourceManager mediaSourceManager)
         {
         {
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _config = config;
             _config = config;
             _subtitleManager = subtitleManager;
             _subtitleManager = subtitleManager;
             _logger = logger;
             _logger = logger;
+            _mediaSourceManager = mediaSourceManager;
         }
         }
 
 
         public string Name
         public string Name
@@ -107,7 +108,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 (options.DownloadMovieSubtitles &&
                 (options.DownloadMovieSubtitles &&
                 video is Movie))
                 video is Movie))
             {
             {
-                var mediaStreams = video.GetMediaSources(false).First().MediaStreams;
+                var mediaStreams = _mediaSourceManager.GetStaticMediaSources(video, false).First().MediaStreams;
 
 
                 var downloadedLanguages = await new SubtitleDownloader(_logger,
                 var downloadedLanguages = await new SubtitleDownloader(_logger,
                     _subtitleManager)
                     _subtitleManager)

+ 6 - 8
MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs

@@ -169,7 +169,7 @@ namespace MediaBrowser.Server.Implementations.Channels
 
 
             foreach (var item in result.Items)
             foreach (var item in result.Items)
             {
             {
-                var channelItem = (IChannelItem)item;
+                var channelItem = (IChannelMediaItem)item;
 
 
                 var channelFeatures = _manager.GetChannelFeatures(channelItem.ChannelId);
                 var channelFeatures = _manager.GetChannelFeatures(channelItem.ChannelId);
 
 
@@ -179,7 +179,7 @@ namespace MediaBrowser.Server.Implementations.Channels
                     {
                     {
                         try
                         try
                         {
                         {
-                            await DownloadChannelItem(item, options, cancellationToken, path);
+                            await DownloadChannelItem(channelItem, options, cancellationToken, path);
                         }
                         }
                         catch (OperationCanceledException)
                         catch (OperationCanceledException)
                         {
                         {
@@ -210,13 +210,13 @@ namespace MediaBrowser.Server.Implementations.Channels
             return channelOptions.DownloadSizeLimit;
             return channelOptions.DownloadSizeLimit;
         }
         }
 
 
-        private async Task DownloadChannelItem(BaseItem item,
+        private async Task DownloadChannelItem(IChannelMediaItem item,
             ChannelOptions channelOptions,
             ChannelOptions channelOptions,
             CancellationToken cancellationToken,
             CancellationToken cancellationToken,
             string path)
             string path)
         {
         {
             var itemId = item.Id.ToString("N");
             var itemId = item.Id.ToString("N");
-            var sources = await _manager.GetChannelItemMediaSources(itemId, false, cancellationToken)
+            var sources = await _manager.GetStaticMediaSources(item, true, cancellationToken)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
 
 
             var cachedVersions = sources.Where(i => i.Protocol == MediaProtocol.File).ToList();
             var cachedVersions = sources.Where(i => i.Protocol == MediaProtocol.File).ToList();
@@ -237,11 +237,9 @@ namespace MediaBrowser.Server.Implementations.Channels
                 }
                 }
             }
             }
 
 
-            var channelItem = (IChannelMediaItem)item;
+            var destination = Path.Combine(path, item.ChannelId, itemId);
 
 
-            var destination = Path.Combine(path, channelItem.ChannelId, itemId);
-
-            await _manager.DownloadChannelItem(channelItem, destination, new Progress<double>(), cancellationToken)
+            await _manager.DownloadChannelItem(item, destination, new Progress<double>(), cancellationToken)
                     .ConfigureAwait(false);
                     .ConfigureAwait(false);
 
 
             await RefreshMediaSourceItem(destination, cancellationToken).ConfigureAwait(false);
             await RefreshMediaSourceItem(destination, cancellationToken).ConfigureAwait(false);

+ 43 - 0
MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs

@@ -0,0 +1,43 @@
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Channels
+{
+    public class ChannelDynamicMediaSourceProvider : IMediaSourceProvider
+    {
+        private readonly ChannelManager _channelManager;
+
+        public ChannelDynamicMediaSourceProvider(IChannelManager channelManager)
+        {
+            _channelManager = (ChannelManager)channelManager;
+        }
+
+        public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(IHasMediaSources item, CancellationToken cancellationToken)
+        {
+            var channelItem = item as IChannelMediaItem;
+
+            if (channelItem != null)
+            {
+                return _channelManager.GetDynamicMediaSources(channelItem, cancellationToken);
+            }
+
+            return Task.FromResult<IEnumerable<MediaSourceInfo>>(new List<MediaSourceInfo>());
+        }
+
+        public Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
+        {
+            throw new NotImplementedException();
+        }
+
+        public Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 0 - 1
MediaBrowser.Server.Implementations/Channels/ChannelImageProvider.cs

@@ -2,7 +2,6 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
-using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;

+ 27 - 20
MediaBrowser.Server.Implementations/Channels/ChannelManager.cs

@@ -241,10 +241,25 @@ namespace MediaBrowser.Server.Implementations.Channels
             return item;
             return item;
         }
         }
 
 
-        public async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSources(string id, bool includeDynamicSources, CancellationToken cancellationToken)
+        public async Task<IEnumerable<MediaSourceInfo>> GetStaticMediaSources(IChannelMediaItem item, bool includeCachedVersions, CancellationToken cancellationToken)
         {
         {
-            var item = (IChannelMediaItem)_libraryManager.GetItemById(id);
+            IEnumerable<ChannelMediaInfo> results = item.ChannelMediaSources;
 
 
+            var sources = SortMediaInfoResults(results)
+                .Select(i => GetMediaSource(item, i))
+                .ToList();
+
+            if (includeCachedVersions)
+            {
+                var cachedVersions = GetCachedChannelItemMediaSources(item);
+                sources.InsertRange(0, cachedVersions);
+            }
+
+            return sources.Where(IsValidMediaSource);
+        }
+
+        public async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(IChannelMediaItem item, CancellationToken cancellationToken)
+        {
             var channel = GetChannel(item.ChannelId);
             var channel = GetChannel(item.ChannelId);
             var channelPlugin = GetChannelProvider(channel);
             var channelPlugin = GetChannelProvider(channel);
 
 
@@ -252,24 +267,25 @@ namespace MediaBrowser.Server.Implementations.Channels
 
 
             IEnumerable<ChannelMediaInfo> results;
             IEnumerable<ChannelMediaInfo> results;
 
 
-            if (requiresCallback != null && includeDynamicSources)
+            if (requiresCallback != null)
             {
             {
                 results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken)
                 results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken)
-                            .ConfigureAwait(false);
+                    .ConfigureAwait(false);
             }
             }
             else
             else
             {
             {
-                results = item.ChannelMediaSources;
+                results = new List<ChannelMediaInfo>();
             }
             }
 
 
-            var sources = SortMediaInfoResults(results).Select(i => GetMediaSource(item, i))
+            var list = SortMediaInfoResults(results)
+                .Select(i => GetMediaSource(item, i))
+                .Where(IsValidMediaSource)
                 .ToList();
                 .ToList();
 
 
             var cachedVersions = GetCachedChannelItemMediaSources(item);
             var cachedVersions = GetCachedChannelItemMediaSources(item);
+            list.InsertRange(0, cachedVersions);
 
 
-            sources.InsertRange(0, cachedVersions);
-
-            return sources.Where(IsValidMediaSource);
+            return list;
         }
         }
 
 
         private readonly ConcurrentDictionary<string, Tuple<DateTime, List<ChannelMediaInfo>>> _channelItemMediaInfo =
         private readonly ConcurrentDictionary<string, Tuple<DateTime, List<ChannelMediaInfo>>> _channelItemMediaInfo =
@@ -297,14 +313,7 @@ namespace MediaBrowser.Server.Implementations.Channels
             return list;
             return list;
         }
         }
 
 
-        public IEnumerable<MediaSourceInfo> GetCachedChannelItemMediaSources(string id)
-        {
-            var item = (IChannelMediaItem)_libraryManager.GetItemById(id);
-
-            return GetCachedChannelItemMediaSources(item);
-        }
-
-        public IEnumerable<MediaSourceInfo> GetCachedChannelItemMediaSources(IChannelMediaItem item)
+        private IEnumerable<MediaSourceInfo> GetCachedChannelItemMediaSources(IChannelMediaItem item)
         {
         {
             var filenamePrefix = item.Id.ToString("N");
             var filenamePrefix = item.Id.ToString("N");
             var parentPath = Path.Combine(ChannelDownloadPath, item.ChannelId);
             var parentPath = Path.Combine(ChannelDownloadPath, item.ChannelId);
@@ -339,7 +348,6 @@ namespace MediaBrowser.Server.Implementations.Channels
 
 
                             if (source != null)
                             if (source != null)
                             {
                             {
-                                source.Type = MediaSourceType.Cache;
                                 return new[] { source };
                                 return new[] { source };
                             }
                             }
                         }
                         }
@@ -1408,8 +1416,7 @@ namespace MediaBrowser.Server.Implementations.Channels
         public async Task DownloadChannelItem(IChannelMediaItem item, string destination,
         public async Task DownloadChannelItem(IChannelMediaItem item, string destination,
             IProgress<double> progress, CancellationToken cancellationToken)
             IProgress<double> progress, CancellationToken cancellationToken)
         {
         {
-            var itemId = item.Id.ToString("N");
-            var sources = await GetChannelItemMediaSources(itemId, true, cancellationToken)
+            var sources = await GetDynamicMediaSources(item, cancellationToken)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
 
 
             var list = sources.Where(i => i.Protocol == MediaProtocol.Http).ToList();
             var list = sources.Where(i => i.Protocol == MediaProtocol.Http).ToList();

+ 25 - 39
MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs

@@ -233,61 +233,45 @@ namespace MediaBrowser.Server.Implementations.Drawing
 
 
             await semaphore.WaitAsync().ConfigureAwait(false);
             await semaphore.WaitAsync().ConfigureAwait(false);
 
 
-            // Check again in case of lock contention
-            try
-            {
-                if (File.Exists(cacheFilePath))
-                {
-                    semaphore.Release();
-                    return cacheFilePath;
-                }
-            }
-            catch
-            {
-                semaphore.Release();
-                throw;
-            }
-
             try
             try
             {
             {
                 CheckDisposed();
                 CheckDisposed();
 
 
-                var newWidth = Convert.ToInt32(newSize.Width);
-                var newHeight = Convert.ToInt32(newSize.Height);
+                if (!File.Exists(cacheFilePath))
+                {
+                    var newWidth = Convert.ToInt32(newSize.Width);
+                    var newHeight = Convert.ToInt32(newSize.Height);
 
 
-                Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
+                    Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
 
 
-                if (string.IsNullOrWhiteSpace(options.BackgroundColor))
-                {
-                    using (var originalImage = new MagickWand(originalImagePath))
+                    if (string.IsNullOrWhiteSpace(options.BackgroundColor))
                     {
                     {
-                        originalImage.CurrentImage.ResizeImage(newWidth, newHeight);
-
-                        DrawIndicator(originalImage, newWidth, newHeight, options);
+                        using (var originalImage = new MagickWand(originalImagePath))
+                        {
+                            originalImage.CurrentImage.ResizeImage(newWidth, newHeight);
 
 
-                        originalImage.CurrentImage.CompressionQuality = quality;
+                            DrawIndicator(originalImage, newWidth, newHeight, options);
 
 
-                        originalImage.SaveImage(cacheFilePath);
+                            originalImage.CurrentImage.CompressionQuality = quality;
 
 
-                        return cacheFilePath;
+                            originalImage.SaveImage(cacheFilePath);
+                        }
                     }
                     }
-                }
-                else
-                {
-                    using (var wand = new MagickWand(newWidth, newHeight, options.BackgroundColor))
+                    else
                     {
                     {
-                        using (var originalImage = new MagickWand(originalImagePath))
+                        using (var wand = new MagickWand(newWidth, newHeight, options.BackgroundColor))
                         {
                         {
-                            originalImage.CurrentImage.ResizeImage(newWidth, newHeight);
+                            using (var originalImage = new MagickWand(originalImagePath))
+                            {
+                                originalImage.CurrentImage.ResizeImage(newWidth, newHeight);
 
 
-                            wand.CurrentImage.CompositeImage(originalImage, CompositeOperator.OverCompositeOp, 0, 0);
-                            DrawIndicator(wand, newWidth, newHeight, options);
+                                wand.CurrentImage.CompositeImage(originalImage, CompositeOperator.OverCompositeOp, 0, 0);
+                                DrawIndicator(wand, newWidth, newHeight, options);
 
 
-                            wand.CurrentImage.CompressionQuality = quality;
+                                wand.CurrentImage.CompressionQuality = quality;
 
 
-                            wand.SaveImage(cacheFilePath);
-
-                            return cacheFilePath;
+                                wand.SaveImage(cacheFilePath);
+                            }
                         }
                         }
                     }
                     }
                 }
                 }
@@ -296,6 +280,8 @@ namespace MediaBrowser.Server.Implementations.Drawing
             {
             {
                 semaphore.Release();
                 semaphore.Release();
             }
             }
+
+            return cacheFilePath;
         }
         }
 
 
         private ImageFormat GetOutputFormat(ImageFormat requestedFormat)
         private ImageFormat GetOutputFormat(ImageFormat requestedFormat)

+ 25 - 15
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -97,13 +97,16 @@ namespace MediaBrowser.Server.Implementations.Dto
 
 
                 if (byName != null && !(item is LiveTvChannel))
                 if (byName != null && !(item is LiveTvChannel))
                 {
                 {
-                    var itemFilter = byName.GetItemFilter();
+                    //if (options.Fields.Contains(ItemFields.ItemCounts))
+                    {
+                        var itemFilter = byName.GetItemFilter();
 
 
-                    var libraryItems = user != null ?
-                       user.RootFolder.GetRecursiveChildren(user, itemFilter) :
-                       _libraryManager.RootFolder.GetRecursiveChildren(itemFilter);
+                        var libraryItems = user != null ?
+                           user.RootFolder.GetRecursiveChildren(user, itemFilter) :
+                           _libraryManager.RootFolder.GetRecursiveChildren(itemFilter);
 
 
-                    SetItemByNameInfo(item, dto, libraryItems.ToList(), user);
+                        SetItemByNameInfo(item, dto, libraryItems.ToList(), user);
+                    }
                 }
                 }
 
 
                 FillSyncInfo(dto, item, itemIdsWithSyncJobs, options, user);
                 FillSyncInfo(dto, item, itemIdsWithSyncJobs, options, user);
@@ -122,13 +125,16 @@ namespace MediaBrowser.Server.Implementations.Dto
 
 
             if (byName != null && !(item is LiveTvChannel))
             if (byName != null && !(item is LiveTvChannel))
             {
             {
-                var itemFilter = byName.GetItemFilter();
+                //if (options.Fields.Contains(ItemFields.ItemCounts))
+                {
+                    var itemFilter = byName.GetItemFilter();
 
 
-                var libraryItems = user != null ?
-                   user.RootFolder.GetRecursiveChildren(user, itemFilter) :
-                   _libraryManager.RootFolder.GetRecursiveChildren(itemFilter);
+                    var libraryItems = user != null ?
+                       user.RootFolder.GetRecursiveChildren(user, itemFilter) :
+                       _libraryManager.RootFolder.GetRecursiveChildren(itemFilter);
 
 
-                SetItemByNameInfo(item, dto, libraryItems.ToList(), user);
+                    SetItemByNameInfo(item, dto, libraryItems.ToList(), user);
+                }
 
 
                 FillSyncInfo(dto, item, options, user);
                 FillSyncInfo(dto, item, options, user);
                 return dto;
                 return dto;
@@ -255,7 +261,7 @@ namespace MediaBrowser.Server.Implementations.Dto
                 {
                 {
                     if (user == null)
                     if (user == null)
                     {
                     {
-                        dto.MediaSources = hasMediaSources.GetMediaSources(true).ToList();
+                        dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(hasMediaSources, true).ToList();
                     }
                     }
                     else
                     else
                     {
                     {
@@ -263,7 +269,7 @@ namespace MediaBrowser.Server.Implementations.Dto
                     }
                     }
                 }
                 }
             }
             }
-
+            
             if (fields.Contains(ItemFields.Studios))
             if (fields.Contains(ItemFields.Studios))
             {
             {
                 AttachStudios(dto, item);
                 AttachStudios(dto, item);
@@ -311,7 +317,11 @@ namespace MediaBrowser.Server.Implementations.Dto
         {
         {
             var dto = GetBaseItemDtoInternal(item, options, user);
             var dto = GetBaseItemDtoInternal(item, options, user);
 
 
-            SetItemByNameInfo(item, dto, taggedItems, user);
+            //if (options.Fields.Contains(ItemFields.ItemCounts))
+            {
+                SetItemByNameInfo(item, dto, taggedItems, user);
+            }
+
             FillSyncInfo(dto, item, options, user);
             FillSyncInfo(dto, item, options, user);
 
 
             return dto;
             return dto;
@@ -1270,7 +1280,7 @@ namespace MediaBrowser.Server.Implementations.Dto
                     }
                     }
                     else
                     else
                     {
                     {
-                        mediaStreams = iHasMediaSources.GetMediaSources(true).First().MediaStreams;
+                        mediaStreams = _mediaSourceManager().GetStaticMediaSources(iHasMediaSources, true).First().MediaStreams;
                     }
                     }
 
 
                     dto.MediaStreams = mediaStreams;
                     dto.MediaStreams = mediaStreams;
@@ -1443,7 +1453,7 @@ namespace MediaBrowser.Server.Implementations.Dto
             var tvChannel = item as LiveTvChannel;
             var tvChannel = item as LiveTvChannel;
             if (tvChannel != null)
             if (tvChannel != null)
             {
             {
-                dto.MediaSources = tvChannel.GetMediaSources(true).ToList();
+                dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(tvChannel, true).ToList();
             }
             }
 
 
             var channelItem = item as IChannelItem;
             var channelItem = item as IChannelItem;

+ 6 - 2
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -1713,11 +1713,15 @@ namespace MediaBrowser.Server.Implementations.Library
                 isNew = true;
                 isNew = true;
             }
             }
 
 
-            var refresh = isNew || (DateTime.UtcNow - item.DateLastSaved).TotalHours >= 6;
+            var refresh = isNew || (DateTime.UtcNow - item.DateLastSaved).TotalHours >= 12;
 
 
             if (refresh)
             if (refresh)
             {
             {
-                _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions());
+                _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions
+                {
+                    // Need to force save to increment DateLastSaved
+                    ForceSave = true
+                });
             }
             }
 
 
             return item;
             return item;

+ 295 - 28
MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs

@@ -1,5 +1,4 @@
-using System.IO;
-using MediaBrowser.Controller.Channels;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
@@ -7,32 +6,35 @@ using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Serialization;
 using System;
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using MediaBrowser.Model.MediaInfo;
 
 
 namespace MediaBrowser.Server.Implementations.Library
 namespace MediaBrowser.Server.Implementations.Library
 {
 {
-    public class MediaSourceManager : IMediaSourceManager
+    public class MediaSourceManager : IMediaSourceManager, IDisposable
     {
     {
         private readonly IItemRepository _itemRepo;
         private readonly IItemRepository _itemRepo;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
-        private readonly IChannelManager _channelManager;
+        private readonly IJsonSerializer _jsonSerializer;
 
 
         private IMediaSourceProvider[] _providers;
         private IMediaSourceProvider[] _providers;
         private readonly ILogger _logger;
         private readonly ILogger _logger;
 
 
-        public MediaSourceManager(IItemRepository itemRepo, IUserManager userManager, ILibraryManager libraryManager, IChannelManager channelManager, ILogger logger)
+        public MediaSourceManager(IItemRepository itemRepo, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer)
         {
         {
             _itemRepo = itemRepo;
             _itemRepo = itemRepo;
             _userManager = userManager;
             _userManager = userManager;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
-            _channelManager = channelManager;
             _logger = logger;
             _logger = logger;
+            _jsonSerializer = jsonSerializer;
         }
         }
 
 
         public void AddParts(IEnumerable<IMediaSourceProvider> providers)
         public void AddParts(IEnumerable<IMediaSourceProvider> providers)
@@ -127,30 +129,27 @@ namespace MediaBrowser.Server.Implementations.Library
             return list;
             return list;
         }
         }
 
 
+        public Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, bool enablePathSubstitution, CancellationToken cancellationToken)
+        {
+            return GetPlayackMediaSources(id, null, enablePathSubstitution, cancellationToken);
+        }
+
         public async Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, CancellationToken cancellationToken)
         public async Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, CancellationToken cancellationToken)
         {
         {
             var item = _libraryManager.GetItemById(id);
             var item = _libraryManager.GetItemById(id);
             IEnumerable<MediaSourceInfo> mediaSources;
             IEnumerable<MediaSourceInfo> mediaSources;
 
 
             var hasMediaSources = (IHasMediaSources)item;
             var hasMediaSources = (IHasMediaSources)item;
-            var channelItem = item as IChannelMediaItem;
+            User user = null;
 
 
-            if (channelItem != null)
+            if (string.IsNullOrWhiteSpace(userId))
             {
             {
-                mediaSources = await _channelManager.GetChannelItemMediaSources(id, true, cancellationToken)
-                        .ConfigureAwait(false);
+                mediaSources = hasMediaSources.GetMediaSources(enablePathSubstitution);
             }
             }
             else
             else
             {
             {
-                if (string.IsNullOrWhiteSpace(userId))
-                {
-                    mediaSources = hasMediaSources.GetMediaSources(enablePathSubstitution);
-                }
-                else
-                {
-                    var user = _userManager.GetUserById(userId);
-                    mediaSources = GetStaticMediaSources(hasMediaSources, enablePathSubstitution, user);
-                }
+                user = _userManager.GetUserById(userId);
+                mediaSources = GetStaticMediaSources(hasMediaSources, enablePathSubstitution, user);
             }
             }
 
 
             var dynamicMediaSources = await GetDynamicMediaSources(hasMediaSources, cancellationToken).ConfigureAwait(false);
             var dynamicMediaSources = await GetDynamicMediaSources(hasMediaSources, cancellationToken).ConfigureAwait(false);
@@ -161,17 +160,30 @@ namespace MediaBrowser.Server.Implementations.Library
 
 
             foreach (var source in dynamicMediaSources)
             foreach (var source in dynamicMediaSources)
             {
             {
-                source.SupportsTranscoding = false;
-
+                if (user != null)
+                {
+                    SetUserProperties(source, user);
+                }
                 if (source.Protocol == MediaProtocol.File)
                 if (source.Protocol == MediaProtocol.File)
                 {
                 {
                     source.SupportsDirectStream = File.Exists(source.Path);
                     source.SupportsDirectStream = File.Exists(source.Path);
+
+                    // TODO: Path substitution
+                }
+                else if (source.Protocol == MediaProtocol.Http)
+                {
+                    // TODO: Allow this when the source is plain http, e.g. not HLS or Mpeg Dash
+                    source.SupportsDirectStream = false;
+                }
+                else
+                {
+                    source.SupportsDirectStream = false;
                 }
                 }
 
 
                 list.Add(source);
                 list.Add(source);
             }
             }
 
 
-            return SortMediaSources(list);
+            return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder);
         }
         }
 
 
         private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(IHasMediaSources item, CancellationToken cancellationToken)
         private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(IHasMediaSources item, CancellationToken cancellationToken)
@@ -186,7 +198,15 @@ namespace MediaBrowser.Server.Implementations.Library
         {
         {
             try
             try
             {
             {
-                return await provider.GetMediaSources(item, cancellationToken).ConfigureAwait(false);
+                var sources = await provider.GetMediaSources(item, cancellationToken).ConfigureAwait(false);
+                var list = sources.ToList();
+
+                foreach (var mediaSource in list)
+                {
+                    SetKeyProperties(provider, mediaSource);
+                }
+
+                return list;
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
@@ -195,9 +215,24 @@ namespace MediaBrowser.Server.Implementations.Library
             }
             }
         }
         }
 
 
-        public Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, bool enablePathSubstitution, CancellationToken cancellationToken)
+        private void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource)
         {
         {
-            return GetPlayackMediaSources(id, null, enablePathSubstitution, cancellationToken);
+            var prefix = provider.GetType().FullName.GetMD5().ToString("N") + "|";
+
+            if (!string.IsNullOrWhiteSpace(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+            {
+                mediaSource.OpenToken = prefix + mediaSource.OpenToken;
+            }
+
+            if (!string.IsNullOrWhiteSpace(mediaSource.LiveStreamId) && !mediaSource.LiveStreamId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+            {
+                mediaSource.LiveStreamId = prefix + mediaSource.LiveStreamId;
+            }
+        }
+
+        public MediaSourceInfo GetStaticMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution)
+        {
+            return GetStaticMediaSources(item, enablePathSubstitution).FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
         }
         }
 
 
         public IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution)
         public IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution)
@@ -263,6 +298,9 @@ namespace MediaBrowser.Server.Implementations.Library
                 preferredSubs,
                 preferredSubs,
                 user.Configuration.SubtitleMode,
                 user.Configuration.SubtitleMode,
                 audioLangage);
                 audioLangage);
+
+            MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs,
+                user.Configuration.SubtitleMode, audioLangage);
         }
         }
 
 
         private IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
         private IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
@@ -286,9 +324,238 @@ namespace MediaBrowser.Server.Implementations.Library
             .ToList();
             .ToList();
         }
         }
 
 
-        public MediaSourceInfo GetStaticMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution)
+        private readonly ConcurrentDictionary<string, LiveStreamInfo> _openStreams = new ConcurrentDictionary<string, LiveStreamInfo>(StringComparer.OrdinalIgnoreCase);
+        private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
+
+        public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, bool enableAutoClose, CancellationToken cancellationToken)
         {
         {
-            return GetStaticMediaSources(item, enablePathSubstitution).FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
+            await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            try
+            {
+                var tuple = GetProvider(request.OpenToken);
+                var provider = tuple.Item1;
+
+                var mediaSource = await provider.OpenMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false);
+
+                if (string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
+                {
+                    throw new InvalidOperationException(string.Format("{0} returned null LiveStreamId", provider.GetType().Name));
+                }
+
+                SetKeyProperties(provider, mediaSource);
+
+                var info = new LiveStreamInfo
+                {
+                    Date = DateTime.UtcNow,
+                    EnableCloseTimer = enableAutoClose,
+                    Id = mediaSource.LiveStreamId,
+                    MediaSource = mediaSource
+                };
+                _openStreams.AddOrUpdate(mediaSource.LiveStreamId, info, (key, i) => info);
+
+                if (enableAutoClose)
+                {
+                    StartCloseTimer();
+                }
+
+                var json = _jsonSerializer.SerializeToString(mediaSource);
+                var clone = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
+
+                if (!string.IsNullOrWhiteSpace(request.UserId))
+                {
+                    var user = _userManager.GetUserById(request.UserId);
+                    SetUserProperties(clone, user);
+                }
+
+                return new LiveStreamResponse
+                {
+                    MediaSource = clone
+                };
+            }
+            finally
+            {
+                _liveStreamSemaphore.Release();
+            }
+        }
+
+        public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
+        {
+            if (string.IsNullOrWhiteSpace(id))
+            {
+                throw new ArgumentNullException("id");
+            }
+
+            _logger.Debug("Getting live stream {0}", id);
+
+            await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            try
+            {
+                LiveStreamInfo info;
+                if (_openStreams.TryGetValue(id, out info))
+                {
+                    return info.MediaSource;
+                }
+                else
+                {
+                    throw new ResourceNotFoundException();
+                }
+            }
+            finally
+            {
+                _liveStreamSemaphore.Release();
+            }
+        }
+
+        public async Task PingLiveStream(string id, CancellationToken cancellationToken)
+        {
+            await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            try
+            {
+                LiveStreamInfo info;
+                if (_openStreams.TryGetValue(id, out info))
+                {
+                    info.Date = DateTime.UtcNow;
+                }
+                else
+                {
+                    _logger.Error("Failed to update MediaSource timestamp for {0}", id);
+                }
+            }
+            finally
+            {
+                _liveStreamSemaphore.Release();
+            }
+        }
+
+        public async Task CloseLiveStream(string id, CancellationToken cancellationToken)
+        {
+            await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            try
+            {
+                LiveStreamInfo current;
+                if (_openStreams.TryGetValue(id, out current))
+                {
+                    if (current.MediaSource.RequiresClosing)
+                    {
+                        var tuple = GetProvider(id);
+
+                        await tuple.Item1.CloseMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false);
+                    }
+                }
+
+                LiveStreamInfo removed;
+                if (_openStreams.TryRemove(id, out removed))
+                {
+                    removed.Closed = true;
+                }
+
+                if (_openStreams.Count == 0)
+                {
+                    StopCloseTimer();
+                }
+            }
+            finally
+            {
+                _liveStreamSemaphore.Release();
+            }
+        }
+
+        private Tuple<IMediaSourceProvider, string> GetProvider(string key)
+        {
+            var keys = key.Split(new[] { '|' }, 2);
+
+            var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), keys[0], StringComparison.OrdinalIgnoreCase));
+
+            return new Tuple<IMediaSourceProvider, string>(provider, keys[1]);
+        }
+
+        private Timer _closeTimer;
+        private readonly TimeSpan _openStreamMaxAge = TimeSpan.FromSeconds(40);
+
+        private void StartCloseTimer()
+        {
+            StopCloseTimer();
+
+            _closeTimer = new Timer(CloseTimerCallback, null, _openStreamMaxAge, _openStreamMaxAge);
+        }
+
+        private void StopCloseTimer()
+        {
+            var timer = _closeTimer;
+
+            if (timer != null)
+            {
+                _closeTimer = null;
+                timer.Dispose();
+            }
+        }
+
+        private async void CloseTimerCallback(object state)
+        {
+            var infos = _openStreams
+                .Values
+                .Where(i => i.EnableCloseTimer && (DateTime.UtcNow - i.Date) > _openStreamMaxAge)
+                .ToList();
+
+            foreach (var info in infos)
+            {
+                if (!info.Closed)
+                {
+                    try
+                    {
+                        await CloseLiveStream(info.Id, CancellationToken.None).ConfigureAwait(false);
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.ErrorException("Error closing media source", ex);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+        /// </summary>
+        public void Dispose()
+        {
+            StopCloseTimer();
+            Dispose(true);
+        }
+
+        private readonly object _disposeLock = new object();
+        /// <summary>
+        /// Releases unmanaged and - optionally - managed resources.
+        /// </summary>
+        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected virtual void Dispose(bool dispose)
+        {
+            if (dispose)
+            {
+                lock (_disposeLock)
+                {
+                    foreach (var key in _openStreams.Keys.ToList())
+                    {
+                        var task = CloseLiveStream(key, CancellationToken.None);
+
+                        Task.WaitAll(task);
+                    }
+
+                    _openStreams.Clear();
+                }
+            }
+        }
+
+        private class LiveStreamInfo
+        {
+            public DateTime Date;
+            public bool EnableCloseTimer;
+            public string Id;
+            public bool Closed;
+            public MediaSourceInfo MediaSource;
         }
         }
     }
     }
 }
 }

+ 1 - 1
MediaBrowser.Server.Implementations/Library/UserViewManager.cs

@@ -215,7 +215,7 @@ namespace MediaBrowser.Server.Implementations.Library
                 if (request.IsPlayed.HasValue)
                 if (request.IsPlayed.HasValue)
                 {
                 {
                     var val = request.IsPlayed.Value;
                     var val = request.IsPlayed.Value;
-                    if (i.IsPlayed(currentUser) != val)
+                    if (i is Video && i.IsPlayed(currentUser) != val)
                     {
                     {
                         return false;
                         return false;
                     }
                     }

+ 4 - 2
MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs

@@ -51,11 +51,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
 
                 var response = await _httpClient.GetResponse(options).ConfigureAwait(false);
                 var response = await _httpClient.GetResponse(options).ConfigureAwait(false);
 
 
-                if (response.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
+                var contentType = response.ContentType;
+
+                if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     imageResponse.HasImage = true;
                     imageResponse.HasImage = true;
                     imageResponse.Stream = response.Content;
                     imageResponse.Stream = response.Content;
-                    imageResponse.SetFormatFromMimeType(response.ContentType);
+                    imageResponse.SetFormatFromMimeType(contentType);
                 }
                 }
                 else
                 else
                 {
                 {

+ 1 - 1
MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs

@@ -373,7 +373,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 StartDate = item.StartDate,
                 StartDate = item.StartDate,
                 OfficialRating = item.OfficialRating,
                 OfficialRating = item.OfficialRating,
                 IsHD = item.IsHD,
                 IsHD = item.IsHD,
-                OriginalAirDate = item.PremiereDate,
+                OriginalAirDate = item.OriginalAirDate,
                 Audio = item.Audio,
                 Audio = item.Audio,
                 CommunityRating = GetClientCommunityRating(item.CommunityRating),
                 CommunityRating = GetClientCommunityRating(item.CommunityRating),
                 IsRepeat = item.IsRepeat,
                 IsRepeat = item.IsRepeat,

+ 98 - 38
MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -1,10 +1,8 @@
-using System.Globalization;
-using MediaBrowser.Common;
+using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Common.ScheduledTasks;
 using MediaBrowser.Common.ScheduledTasks;
-using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
@@ -15,7 +13,6 @@ using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Sorting;
-using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
@@ -88,8 +85,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             get { return _services; }
             get { return _services; }
         }
         }
 
 
-        public ILiveTvService ActiveService { get; private set; }
-
         private LiveTvOptions GetConfiguration()
         private LiveTvOptions GetConfiguration()
         {
         {
             return _config.GetConfiguration<LiveTvOptions>("livetv");
             return _config.GetConfiguration<LiveTvOptions>("livetv");
@@ -103,8 +98,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         {
         {
             _services.AddRange(services);
             _services.AddRange(services);
 
 
-            ActiveService = _services.FirstOrDefault();
-
             foreach (var service in _services)
             foreach (var service in _services)
             {
             {
                 service.DataSourceChanged += service_DataSourceChanged;
                 service.DataSourceChanged += service_DataSourceChanged;
@@ -316,6 +309,22 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return await GetLiveStream(id, true, cancellationToken).ConfigureAwait(false);
             return await GetLiveStream(id, true, cancellationToken).ConfigureAwait(false);
         }
         }
 
 
+        public async Task<IEnumerable<MediaSourceInfo>> GetRecordingMediaSources(string id, CancellationToken cancellationToken)
+        {
+            var item = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false);
+            var service = GetService(item);
+
+            return await service.GetRecordingStreamMediaSources(id, cancellationToken).ConfigureAwait(false);
+        }
+
+        public async Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(string id, CancellationToken cancellationToken)
+        {
+            var item = GetInternalChannel(id);
+            var service = GetService(item);
+
+            return await service.GetChannelStreamMediaSources(id, cancellationToken).ConfigureAwait(false);
+        }
+
         private ILiveTvService GetService(ILiveTvItem item)
         private ILiveTvService GetService(ILiveTvItem item)
         {
         {
             return GetService(item.ServiceName);
             return GetService(item.ServiceName);
@@ -333,30 +342,48 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             try
             try
             {
             {
                 MediaSourceInfo info;
                 MediaSourceInfo info;
+                bool isVideo;
 
 
                 if (isChannel)
                 if (isChannel)
                 {
                 {
                     var channel = GetInternalChannel(id);
                     var channel = GetInternalChannel(id);
+                    isVideo = channel.ChannelType == ChannelType.TV;
                     var service = GetService(channel);
                     var service = GetService(channel);
                     _logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId);
                     _logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId);
                     info = await service.GetChannelStream(channel.ExternalId, null, cancellationToken).ConfigureAwait(false);
                     info = await service.GetChannelStream(channel.ExternalId, null, cancellationToken).ConfigureAwait(false);
+                    info.RequiresClosing = true;
+
+                    if (info.RequiresClosing)
+                    {
+                        var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
+
+                        info.LiveStreamId = idPrefix + info.Id;
+                    }
                 }
                 }
                 else
                 else
                 {
                 {
                     var recording = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false);
                     var recording = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false);
+                    isVideo = !string.Equals(recording.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase);
                     var service = GetService(recording);
                     var service = GetService(recording);
 
 
                     _logger.Info("Opening recording stream from {0}, external recording Id: {1}", service.Name, recording.RecordingInfo.Id);
                     _logger.Info("Opening recording stream from {0}, external recording Id: {1}", service.Name, recording.RecordingInfo.Id);
                     info = await service.GetRecordingStream(recording.RecordingInfo.Id, null, cancellationToken).ConfigureAwait(false);
                     info = await service.GetRecordingStream(recording.RecordingInfo.Id, null, cancellationToken).ConfigureAwait(false);
+                    info.RequiresClosing = true;
+
+                    if (info.RequiresClosing)
+                    {
+                        var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
+
+                        info.LiveStreamId = idPrefix + info.Id;
+                    }
                 }
                 }
 
 
                 _logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(info));
                 _logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(info));
-                Sanitize(info);
+                Normalize(info, isVideo);
 
 
                 var data = new LiveStreamData
                 var data = new LiveStreamData
                 {
                 {
                     Info = info,
                     Info = info,
-                    ConsumerCount = 1,
                     IsChannel = isChannel,
                     IsChannel = isChannel,
                     ItemId = id
                     ItemId = id
                 };
                 };
@@ -377,25 +404,43 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             }
             }
         }
         }
 
 
-        private void Sanitize(MediaSourceInfo mediaSource)
+        private void Normalize(MediaSourceInfo mediaSource, bool isVideo)
         {
         {
             if (mediaSource.MediaStreams.Count == 0)
             if (mediaSource.MediaStreams.Count == 0)
             {
             {
-                mediaSource.MediaStreams.AddRange(new List<MediaStream>
+                if (isVideo)
                 {
                 {
-                    new MediaStream
+                    mediaSource.MediaStreams.AddRange(new List<MediaStream>
                     {
                     {
-                        Type = MediaStreamType.Video,
-                        // Set the index to -1 because we don't know the exact index of the video stream within the container
-                        Index = -1
-                    },
-                    new MediaStream
+                        new MediaStream
+                        {
+                            Type = MediaStreamType.Video,
+                            // Set the index to -1 because we don't know the exact index of the video stream within the container
+                            Index = -1,
+
+                            // Set to true if unknown to enable deinterlacing
+                            IsInterlaced = true
+                        },
+                        new MediaStream
+                        {
+                            Type = MediaStreamType.Audio,
+                            // Set the index to -1 because we don't know the exact index of the audio stream within the container
+                            Index = -1
+                        }
+                    });
+                }
+                else
+                {
+                    mediaSource.MediaStreams.AddRange(new List<MediaStream>
                     {
                     {
-                        Type = MediaStreamType.Audio,
-                        // Set the index to -1 because we don't know the exact index of the audio stream within the container
-                        Index = -1
-                    }
-                });
+                        new MediaStream
+                        {
+                            Type = MediaStreamType.Audio,
+                            // Set the index to -1 because we don't know the exact index of the audio stream within the container
+                            Index = -1
+                        }
+                    });
+                }
             }
             }
 
 
             // Clean some bad data coming from providers
             // Clean some bad data coming from providers
@@ -544,13 +589,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             item.Name = info.Name;
             item.Name = info.Name;
             item.OfficialRating = info.OfficialRating;
             item.OfficialRating = info.OfficialRating;
             item.Overview = info.Overview;
             item.Overview = info.Overview;
-            item.PremiereDate = info.OriginalAirDate;
+            item.OriginalAirDate = info.OriginalAirDate;
             item.ProviderImagePath = info.ImagePath;
             item.ProviderImagePath = info.ImagePath;
             item.ProviderImageUrl = info.ImageUrl;
             item.ProviderImageUrl = info.ImageUrl;
             item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
             item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
             item.StartDate = info.StartDate;
             item.StartDate = info.StartDate;
-            item.ProductionYear = info.ProductionYear;
 
 
+            item.ProductionYear = info.ProductionYear;
+            item.PremiereDate = item.PremiereDate ?? info.OriginalAirDate;
+            
             await item.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
             await item.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
 
 
             return item;
             return item;
@@ -1742,7 +1789,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         class LiveStreamData
         class LiveStreamData
         {
         {
             internal MediaSourceInfo Info;
             internal MediaSourceInfo Info;
-            internal int ConsumerCount;
             internal string ItemId;
             internal string ItemId;
             internal bool IsChannel;
             internal bool IsChannel;
         }
         }
@@ -1753,19 +1799,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
 
             try
             try
             {
             {
-                var service = ActiveService;
+                var parts = id.Split(new[] { '_' }, 2);
 
 
-                LiveStreamData data;
-                if (_openStreams.TryGetValue(id, out data))
-                {
-                    if (data.ConsumerCount > 1)
-                    {
-                        data.ConsumerCount--;
-                        _logger.Info("Decrementing live stream client count.");
-                        return;
-                    }
+                var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), parts[0], StringComparison.OrdinalIgnoreCase));
 
 
+                if (service == null)
+                {
+                    throw new ArgumentException("Service not found.");
                 }
                 }
+
+                id = parts[1];
+
+                LiveStreamData data;
                 _openStreams.TryRemove(id, out data);
                 _openStreams.TryRemove(id, out data);
 
 
                 _logger.Info("Closing live stream from {0}, stream Id: {1}", service.Name, id);
                 _logger.Info("Closing live stream from {0}, stream Id: {1}", service.Name, id);
@@ -1846,6 +1891,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 Name = service.Name
                 Name = service.Name
             };
             };
 
 
+            var tunerIdPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
+
             try
             try
             {
             {
                 var statusInfo = await service.GetStatusInfoAsync(cancellationToken).ConfigureAwait(false);
                 var statusInfo = await service.GetStatusInfoAsync(cancellationToken).ConfigureAwait(false);
@@ -1867,7 +1914,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                         channelName = channel == null ? null : channel.Name;
                         channelName = channel == null ? null : channel.Name;
                     }
                     }
 
 
-                    return _tvDtoService.GetTunerInfoDto(service.Name, i, channelName);
+                    var dto = _tvDtoService.GetTunerInfoDto(service.Name, i, channelName);
+
+                    dto.Id = tunerIdPrefix + dto.Id;
+
+                    return dto;
 
 
                 }).ToList();
                 }).ToList();
             }
             }
@@ -1920,7 +1971,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         public Task ResetTuner(string id, CancellationToken cancellationToken)
         public Task ResetTuner(string id, CancellationToken cancellationToken)
         {
         {
-            return ActiveService.ResetTuner(id, cancellationToken);
+            var parts = id.Split(new[] { '_' }, 2);
+
+            var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), parts[0], StringComparison.OrdinalIgnoreCase));
+
+            if (service == null)
+            {
+                throw new ArgumentException("Service not found.");
+            }
+
+            return service.ResetTuner(parts[1], cancellationToken);
         }
         }
 
 
         public async Task<BaseItemDto> GetLiveTvFolder(string userId, CancellationToken cancellationToken)
         public async Task<BaseItemDto> GetLiveTvFolder(string userId, CancellationToken cancellationToken)

+ 108 - 0
MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs

@@ -0,0 +1,108 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.LiveTv
+{
+    public class LiveTvMediaSourceProvider : IMediaSourceProvider
+    {
+        private readonly ILiveTvManager _liveTvManager;
+        private readonly IJsonSerializer _jsonSerializer;
+        private readonly ILogger _logger;
+        private readonly IMediaSourceManager _mediaSourceManager;
+
+        public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IJsonSerializer jsonSerializer, ILogManager logManager, IMediaSourceManager mediaSourceManager)
+        {
+            _liveTvManager = liveTvManager;
+            _jsonSerializer = jsonSerializer;
+            _mediaSourceManager = mediaSourceManager;
+            _logger = logManager.GetLogger(GetType().Name);
+        }
+
+        public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(IHasMediaSources item, CancellationToken cancellationToken)
+        {
+            var channelItem = item as ILiveTvItem;
+
+            if (channelItem != null)
+            {
+                var hasMetadata = (IHasMetadata)channelItem;
+
+                if (string.IsNullOrWhiteSpace(hasMetadata.Path))
+                {
+                    return GetMediaSourcesInternal(channelItem, cancellationToken);
+                }
+            }
+
+            return Task.FromResult<IEnumerable<MediaSourceInfo>>(new List<MediaSourceInfo>());
+        }
+
+        private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(ILiveTvItem item, CancellationToken cancellationToken)
+        {
+            IEnumerable<MediaSourceInfo> sources;
+
+            try
+            {
+                if (item is ILiveTvRecording)
+                {
+                    sources = await _liveTvManager.GetRecordingMediaSources(item.Id.ToString("N"), cancellationToken)
+                                .ConfigureAwait(false);
+                }
+                else
+                {
+                    sources = await _liveTvManager.GetChannelMediaSources(item.Id.ToString("N"), cancellationToken)
+                                .ConfigureAwait(false);
+                }
+            }
+            catch (NotImplementedException)
+            {
+                var hasMediaSources = (IHasMediaSources)item;
+
+                sources = _mediaSourceManager.GetStaticMediaSources(hasMediaSources, false)
+                   .ToList();
+            }
+
+            var list = sources.ToList();
+
+            foreach (var source in list)
+            {
+                source.Type = MediaSourceType.Default;
+                source.RequiresOpening = true;
+                source.BufferMs = source.BufferMs ?? 1500;
+
+                var openKeys = new List<string>();
+                openKeys.Add(item.GetType().Name);
+                openKeys.Add(item.Id.ToString("N"));
+                source.OpenToken = string.Join("|", openKeys.ToArray());
+            }
+
+            _logger.Debug("MediaSources: {0}", _jsonSerializer.SerializeToString(list));
+
+            return list;
+        }
+
+        public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
+        {
+            var keys = openToken.Split(new[] { '|' }, 2);
+
+            if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
+            {
+                return await _liveTvManager.GetChannelStream(keys[1], cancellationToken).ConfigureAwait(false);
+            }
+
+            return await _liveTvManager.GetRecordingStream(keys[1], cancellationToken).ConfigureAwait(false);
+        }
+
+        public Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken)
+        {
+            return _liveTvManager.CloseLiveStream(liveStreamId, cancellationToken);
+        }
+    }
+}

Some files were not shown because too many files changed in this diff