Selaa lähdekoodia

update live streams

Luke Pulverenti 8 vuotta sitten
vanhempi
sitoutus
b9cacd8076
35 muutettua tiedostoa jossa 293 lisäystä ja 625 poistoa
  1. 0 30
      MediaBrowser.Api/Library/ChapterService.cs
  2. 1 1
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  3. 0 1
      MediaBrowser.Api/MediaBrowser.Api.csproj
  4. 3 1
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  5. 18 17
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  6. 26 3
      MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
  7. 1 0
      MediaBrowser.Api/Playback/StreamState.cs
  8. 0 19
      MediaBrowser.Controller/Chapters/ChapterResponse.cs
  9. 0 31
      MediaBrowser.Controller/Chapters/ChapterSearchRequest.cs
  10. 1 46
      MediaBrowser.Controller/Chapters/IChapterManager.cs
  11. 0 39
      MediaBrowser.Controller/Chapters/IChapterProvider.cs
  12. 8 0
      MediaBrowser.Controller/Library/IMediaSourceManager.cs
  13. 2 1
      MediaBrowser.Controller/Library/IMediaSourceProvider.cs
  14. 2 1
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  15. 6 0
      MediaBrowser.Controller/LiveTv/ILiveTvService.cs
  16. 0 3
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  17. 0 9
      MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj
  18. 0 9
      MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
  19. 0 8
      MediaBrowser.Model/Chapters/ChapterProviderInfo.cs
  20. 0 18
      MediaBrowser.Model/Chapters/RemoteChapterInfo.cs
  21. 0 48
      MediaBrowser.Model/Chapters/RemoteChapterResult.cs
  22. 4 12
      MediaBrowser.Model/Configuration/ChapterOptions.cs
  23. 0 3
      MediaBrowser.Model/MediaBrowser.Model.csproj
  24. 0 220
      MediaBrowser.Providers/Chapters/ChapterManager.cs
  25. 0 62
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  26. 70 20
      MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
  27. 1 1
      MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs
  28. 19 0
      MediaBrowser.Server.Implementations/Library/LibraryManager.cs
  29. 15 4
      MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs
  30. 9 2
      MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  31. 21 7
      MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
  32. 7 4
      MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
  33. 77 2
      MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs
  34. 2 2
      MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs
  35. 0 1
      MediaBrowser.Server.Startup.Common/ApplicationHost.cs

+ 0 - 30
MediaBrowser.Api/Library/ChapterService.cs

@@ -1,30 +0,0 @@
-using MediaBrowser.Controller.Chapters;
-using MediaBrowser.Controller.Net;
-using ServiceStack;
-using System.Linq;
-
-namespace MediaBrowser.Api.Library
-{
-    [Route("/Providers/Chapters", "GET")]
-    public class GetChapterProviders : IReturnVoid
-    {
-    }
-
-    [Authenticated]
-    public class ChapterService : BaseApiService
-    {
-        private readonly IChapterManager _chapterManager;
-
-        public ChapterService(IChapterManager chapterManager)
-        {
-            _chapterManager = chapterManager;
-        }
-
-        public object Get(GetChapterProviders request)
-        {
-            var result = _chapterManager.GetProviders().ToList();
-
-            return ToOptimizedResult(result);
-        }
-    }
-}

+ 1 - 1
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -705,7 +705,7 @@ namespace MediaBrowser.Api.LiveTv
 
             var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
-            outputHeaders["Content-Type"] = MimeTypes.GetMimeType(filePath);
+            outputHeaders["Content-Type"] = MediaBrowser.Model.Net.MimeTypes.GetMimeType(filePath);
 
             long startPosition = 0;
 

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

@@ -79,7 +79,6 @@
     <Compile Include="Dlna\DlnaService.cs" />
     <Compile Include="FilterService.cs" />
     <Compile Include="IHasDtoOptions.cs" />
-    <Compile Include="Library\ChapterService.cs" />
     <Compile Include="Playback\MediaInfoService.cs" />
     <Compile Include="Playback\TranscodingThrottler.cs" />
     <Compile Include="PlaylistService.cs" />

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

@@ -1888,7 +1888,9 @@ namespace MediaBrowser.Api.Playback
             }
             else
             {
-                mediaSource = await MediaSourceManager.GetLiveStream(request.LiveStreamId, cancellationToken).ConfigureAwait(false);
+                var liveStreamInfo = await MediaSourceManager.GetLiveStreamWithDirectStreamProvider(request.LiveStreamId, cancellationToken).ConfigureAwait(false);
+                mediaSource = liveStreamInfo.Item1;
+                state.DirectStreamProvider = liveStreamInfo.Item2;
             }
 
             var videoRequest = request as VideoStreamRequest;

+ 18 - 17
MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs

@@ -121,32 +121,33 @@ namespace MediaBrowser.Api.Playback.Progressive
 
             var responseHeaders = new Dictionary<string, string>();
 
-            // Static remote stream
-            if (request.Static && state.InputProtocol == MediaProtocol.Http)
+            if (request.Static && state.DirectStreamProvider != null)
             {
                 AddDlnaHeaders(state, responseHeaders, true);
 
                 using (state)
                 {
-                    if (state.MediaPath.IndexOf("/livestreamfiles/", StringComparison.OrdinalIgnoreCase) != -1)
-                    {
-                        var parts = state.MediaPath.Split('/');
-                        var filename = parts[parts.Length - 2] + Path.GetExtension(parts[parts.Length - 1]);
-                        var filePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, filename);
+                    var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
-                        var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+                    // TODO: Don't hardcode this
+                    outputHeaders["Content-Type"] = MediaBrowser.Model.Net.MimeTypes.GetMimeType("file.ts");
 
-                        outputHeaders["Content-Type"] = MimeTypes.GetMimeType(filePath);
+                    var streamSource = new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, Logger, CancellationToken.None)
+                    {
+                        AllowEndOfFile = false
+                    };
+                    return ResultFactory.GetAsyncStreamWriter(streamSource);
+                }
+            }
 
-                        var streamSource = new ProgressiveFileCopier(FileSystem, filePath, outputHeaders, null, Logger, CancellationToken.None)
-                        {
-                            AllowEndOfFile = false
-                        };
-                        return ResultFactory.GetAsyncStreamWriter(streamSource);
-                    }
+            // Static remote stream
+            if (request.Static && state.InputProtocol == MediaProtocol.Http)
+            {
+                AddDlnaHeaders(state, responseHeaders, true);
 
-                    return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource)
-                                .ConfigureAwait(false);
+                using (state)
+                {
+                    return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
                 }
             }
 

+ 26 - 3
MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs

@@ -7,6 +7,7 @@ using CommonIO;
 using MediaBrowser.Controller.Net;
 using System.Collections.Generic;
 using ServiceStack.Web;
+using MediaBrowser.Controller.Library;
 
 namespace MediaBrowser.Api.Playback.Progressive
 {
@@ -26,6 +27,8 @@ namespace MediaBrowser.Api.Playback.Progressive
         public long StartPosition { get; set; }
         public bool AllowEndOfFile = true;
 
+        private IDirectStreamProvider _directStreamProvider;
+
         public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
         {
             _fileSystem = fileSystem;
@@ -36,6 +39,15 @@ namespace MediaBrowser.Api.Playback.Progressive
             _cancellationToken = cancellationToken;
         }
 
+        public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
+        {
+            _directStreamProvider = directStreamProvider;
+            _outputHeaders = outputHeaders;
+            _job = job;
+            _logger = logger;
+            _cancellationToken = cancellationToken;
+        }
+
         public IDictionary<string, string> Options
         {
             get
@@ -44,22 +56,33 @@ namespace MediaBrowser.Api.Playback.Progressive
             }
         }
 
+        private Stream GetInputStream()
+        {
+            return _fileSystem.GetFileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
+        }
+
         public async Task WriteToAsync(Stream outputStream)
         {
             try
             {
+                if (_directStreamProvider != null)
+                {
+                    await _directStreamProvider.CopyToAsync(outputStream, _cancellationToken).ConfigureAwait(false);
+                    return;
+                }
+
                 var eofCount = 0;
 
-                using (var fs = _fileSystem.GetFileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
+                using (var inputStream = GetInputStream())
                 {
                     if (StartPosition > 0)
                     {
-                        fs.Position = StartPosition;
+                        inputStream.Position = StartPosition;
                     }
 
                     while (eofCount < 15 || !AllowEndOfFile)
                     {
-                        var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, _cancellationToken).ConfigureAwait(false);
+                        var bytesRead = await CopyToAsyncInternal(inputStream, outputStream, BufferSize, _cancellationToken).ConfigureAwait(false);
 
                         //var position = fs.Position;
                         //_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);

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

@@ -37,6 +37,7 @@ namespace MediaBrowser.Api.Playback
         /// </summary>
         /// <value>The log file stream.</value>
         public Stream LogFileStream { get; set; }
+        public IDirectStreamProvider DirectStreamProvider { get; set; }
 
         public string InputContainer { get; set; }
 

+ 0 - 19
MediaBrowser.Controller/Chapters/ChapterResponse.cs

@@ -1,19 +0,0 @@
-using MediaBrowser.Model.Chapters;
-using System.Collections.Generic;
-
-namespace MediaBrowser.Controller.Chapters
-{
-    public class ChapterResponse
-    {
-        /// <summary>
-        /// Gets or sets the chapters.
-        /// </summary>
-        /// <value>The chapters.</value>
-        public List<RemoteChapterInfo> Chapters { get; set; }
-
-        public ChapterResponse()
-        {
-            Chapters = new List<RemoteChapterInfo>();
-        }
-    }
-}

+ 0 - 31
MediaBrowser.Controller/Chapters/ChapterSearchRequest.cs

@@ -1,31 +0,0 @@
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using System;
-using System.Collections.Generic;
-
-namespace MediaBrowser.Controller.Chapters
-{
-    public class ChapterSearchRequest : IHasProviderIds
-    {
-        public string Language { get; set; }
-
-        public VideoContentType ContentType { get; set; }
-
-        public string MediaPath { get; set; }
-        public string SeriesName { get; set; }
-        public string Name { get; set; }
-        public int? IndexNumber { get; set; }
-        public int? IndexNumberEnd { get; set; }
-        public int? ParentIndexNumber { get; set; }
-        public int? ProductionYear { get; set; }
-        public long? RuntimeTicks { get; set; }
-        public Dictionary<string, string> ProviderIds { get; set; }
-
-        public bool SearchAllProviders { get; set; }
-
-        public ChapterSearchRequest()
-        {
-            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-        }
-    }
-}

+ 1 - 46
MediaBrowser.Controller/Chapters/IChapterManager.cs

@@ -1,6 +1,4 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Chapters;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Model.Configuration;
@@ -13,12 +11,6 @@ namespace MediaBrowser.Controller.Chapters
     /// </summary>
     public interface IChapterManager
     {
-        /// <summary>
-        /// Adds the parts.
-        /// </summary>
-        /// <param name="chapterProviders">The chapter providers.</param>
-        void AddParts(IEnumerable<IChapterProvider> chapterProviders);
-
         /// <summary>
         /// Gets the chapters.
         /// </summary>
@@ -35,43 +27,6 @@ namespace MediaBrowser.Controller.Chapters
         /// <returns>Task.</returns>
         Task SaveChapters(string itemId, List<ChapterInfo> chapters, CancellationToken cancellationToken);
 
-        /// <summary>
-        /// Searches the specified video.
-        /// </summary>
-        /// <param name="video">The video.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{IEnumerable{RemoteChapterResult}}.</returns>
-        Task<IEnumerable<RemoteChapterResult>> Search(Video video, CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Searches the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{IEnumerable{RemoteChapterResult}}.</returns>
-        Task<IEnumerable<RemoteChapterResult>> Search(ChapterSearchRequest request, CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Gets the chapters.
-        /// </summary>
-        /// <param name="id">The identifier.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{ChapterResponse}.</returns>
-        Task<ChapterResponse> GetChapters(string id, CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Gets the providers.
-        /// </summary>
-        /// <param name="itemId">The item identifier.</param>
-        /// <returns>IEnumerable{ChapterProviderInfo}.</returns>
-        IEnumerable<ChapterProviderInfo> GetProviders(string itemId);
-
-        /// <summary>
-        /// Gets the providers.
-        /// </summary>
-        /// <returns>IEnumerable{ChapterProviderInfo}.</returns>
-        IEnumerable<ChapterProviderInfo> GetProviders();
-
         /// <summary>
         /// Gets the configuration.
         /// </summary>

+ 0 - 39
MediaBrowser.Controller/Chapters/IChapterProvider.cs

@@ -1,39 +0,0 @@
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Chapters;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Chapters
-{
-    public interface IChapterProvider
-    {
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        string Name { get; }
-
-        /// <summary>
-        /// Gets the supported media types.
-        /// </summary>
-        /// <value>The supported media types.</value>
-        IEnumerable<VideoContentType> SupportedMediaTypes { get; }
-
-        /// <summary>
-        /// Searches the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{IEnumerable{RemoteChapterResult}}.</returns>
-        Task<IEnumerable<RemoteChapterResult>> Search(ChapterSearchRequest request, CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Gets the chapters.
-        /// </summary>
-        /// <param name="id">The identifier.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{ChapterResponse}.</returns>
-        Task<ChapterResponse> GetChapters(string id, CancellationToken cancellationToken);
-    }
-}

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

@@ -7,6 +7,7 @@ using System;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
+using System.IO;
 
 namespace MediaBrowser.Controller.Library
 {
@@ -79,6 +80,8 @@ namespace MediaBrowser.Controller.Library
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task&lt;MediaSourceInfo&gt;.</returns>
         Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken);
+
+        Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken);
         
         /// <summary>
         /// Pings the media source.
@@ -95,4 +98,9 @@ namespace MediaBrowser.Controller.Library
         /// <returns>Task.</returns>
         Task CloseLiveStream(string id);
     }
+
+    public interface IDirectStreamProvider
+    {
+        Task CopyToAsync(Stream stream, CancellationToken cancellationToken);
+    }
 }

+ 2 - 1
MediaBrowser.Controller/Library/IMediaSourceProvider.cs

@@ -3,6 +3,7 @@ using MediaBrowser.Model.Dto;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
+using System;
 
 namespace MediaBrowser.Controller.Library
 {
@@ -22,7 +23,7 @@ namespace MediaBrowser.Controller.Library
         /// <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);
+        Task<Tuple<MediaSourceInfo,IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken);
 
         /// <summary>
         /// Closes the media source.

+ 2 - 1
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -9,6 +9,7 @@ using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Model.Events;
+using MediaBrowser.Controller.Library;
 
 namespace MediaBrowser.Controller.LiveTv
 {
@@ -156,7 +157,7 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="mediaSourceId">The media source identifier.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{StreamResponseInfo}.</returns>
-        Task<MediaSourceInfo> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken);
+        Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken);
 
         /// <summary>
         /// Gets the program.

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

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Controller.Library;
 
 namespace MediaBrowser.Controller.LiveTv
 {
@@ -245,4 +246,9 @@ namespace MediaBrowser.Controller.LiveTv
         /// <returns>Task.</returns>
         Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken);
     }
+
+    public interface ISupportsDirectStreamProvider
+    {
+        Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, CancellationToken cancellationToken);
+    }
 }

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

@@ -91,10 +91,7 @@
     <Compile Include="Channels\InternalChannelItemQuery.cs" />
     <Compile Include="Channels\IRequiresMediaInfoCallback.cs" />
     <Compile Include="Channels\ISearchableChannel.cs" />
-    <Compile Include="Chapters\ChapterSearchRequest.cs" />
     <Compile Include="Chapters\IChapterManager.cs" />
-    <Compile Include="Chapters\IChapterProvider.cs" />
-    <Compile Include="Chapters\ChapterResponse.cs" />
     <Compile Include="Collections\CollectionCreationOptions.cs" />
     <Compile Include="Collections\CollectionEvents.cs" />
     <Compile Include="Collections\ICollectionManager.cs" />

+ 0 - 9
MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj

@@ -160,15 +160,6 @@
     <Compile Include="..\MediaBrowser.Model\Channels\ChannelQuery.cs">
       <Link>Channels\ChannelQuery.cs</Link>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\Chapters\ChapterProviderInfo.cs">
-      <Link>Chapters\ChapterProviderInfo.cs</Link>
-    </Compile>
-    <Compile Include="..\MediaBrowser.Model\Chapters\RemoteChapterInfo.cs">
-      <Link>Chapters\RemoteChapterInfo.cs</Link>
-    </Compile>
-    <Compile Include="..\MediaBrowser.Model\Chapters\RemoteChapterResult.cs">
-      <Link>Chapters\RemoteChapterResult.cs</Link>
-    </Compile>
     <Compile Include="..\MediaBrowser.Model\Collections\CollectionCreationResult.cs">
       <Link>Collections\CollectionCreationResult.cs</Link>
     </Compile>

+ 0 - 9
MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj

@@ -132,15 +132,6 @@
     <Compile Include="..\MediaBrowser.Model\Channels\ChannelQuery.cs">
       <Link>Channels\ChannelQuery.cs</Link>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\Chapters\ChapterProviderInfo.cs">
-      <Link>Chapters\ChapterProviderInfo.cs</Link>
-    </Compile>
-    <Compile Include="..\MediaBrowser.Model\Chapters\RemoteChapterInfo.cs">
-      <Link>Chapters\RemoteChapterInfo.cs</Link>
-    </Compile>
-    <Compile Include="..\MediaBrowser.Model\Chapters\RemoteChapterResult.cs">
-      <Link>Chapters\RemoteChapterResult.cs</Link>
-    </Compile>
     <Compile Include="..\MediaBrowser.Model\Collections\CollectionCreationResult.cs">
       <Link>Collections\CollectionCreationResult.cs</Link>
     </Compile>

+ 0 - 8
MediaBrowser.Model/Chapters/ChapterProviderInfo.cs

@@ -1,8 +0,0 @@
-namespace MediaBrowser.Model.Chapters
-{
-    public class ChapterProviderInfo
-    {
-        public string Name { get; set; }
-        public string Id { get; set; }
-    }
-}

+ 0 - 18
MediaBrowser.Model/Chapters/RemoteChapterInfo.cs

@@ -1,18 +0,0 @@
-
-namespace MediaBrowser.Model.Chapters
-{
-    public class RemoteChapterInfo
-    {
-        /// <summary>
-        /// Gets or sets the start position ticks.
-        /// </summary>
-        /// <value>The start position ticks.</value>
-        public long StartPositionTicks { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-    }
-}

+ 0 - 48
MediaBrowser.Model/Chapters/RemoteChapterResult.cs

@@ -1,48 +0,0 @@
-
-namespace MediaBrowser.Model.Chapters
-{
-    public class RemoteChapterResult
-    {
-        /// <summary>
-        /// Gets or sets the identifier.
-        /// </summary>
-        /// <value>The identifier.</value>
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the run time ticks.
-        /// </summary>
-        /// <value>The run time ticks.</value>
-        public long? RunTimeTicks { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name of the provider.
-        /// </summary>
-        /// <value>The name of the provider.</value>
-        public string ProviderName { get; set; }
-        
-        /// <summary>
-        /// Gets or sets the community rating.
-        /// </summary>
-        /// <value>The community rating.</value>
-        public float? CommunityRating { get; set; }
-
-        /// <summary>
-        /// Gets or sets the chapter count.
-        /// </summary>
-        /// <value>The chapter count.</value>
-        public int? ChapterCount { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name of the three letter iso language.
-        /// </summary>
-        /// <value>The name of the three letter iso language.</value>
-        public string ThreeLetterISOLanguageName { get; set; }
-    }
-}

+ 4 - 12
MediaBrowser.Model/Configuration/ChapterOptions.cs

@@ -2,18 +2,10 @@
 {
     public class ChapterOptions
     {
-        public bool DownloadMovieChapters { get; set; }
-        public bool DownloadEpisodeChapters { get; set; }
+        public bool EnableMovieChapterImageExtraction { get; set; }
+        public bool EnableEpisodeChapterImageExtraction { get; set; }
+        public bool EnableOtherVideoChapterImageExtraction { get; set; }
 
-        public string[] FetcherOrder { get; set; }
-        public string[] DisabledFetchers { get; set; }
-      
-        public ChapterOptions()
-        {
-            DownloadMovieChapters = true;
-
-            DisabledFetchers = new string[] { };
-            FetcherOrder = new string[] { };
-        }
+        public bool ExtractDuringLibraryScan { get; set; }
     }
 }

+ 0 - 3
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -84,9 +84,6 @@
     <Compile Include="Channels\ChannelMediaContentType.cs" />
     <Compile Include="Channels\ChannelMediaType.cs" />
     <Compile Include="Channels\ChannelQuery.cs" />
-    <Compile Include="Chapters\ChapterProviderInfo.cs" />
-    <Compile Include="Chapters\RemoteChapterInfo.cs" />
-    <Compile Include="Chapters\RemoteChapterResult.cs" />
     <Compile Include="Collections\CollectionCreationResult.cs" />
     <Compile Include="Configuration\AccessSchedule.cs" />
     <Compile Include="Configuration\ChannelOptions.cs" />

+ 0 - 220
MediaBrowser.Providers/Chapters/ChapterManager.cs

@@ -8,7 +8,6 @@ using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Chapters;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
@@ -22,7 +21,6 @@ namespace MediaBrowser.Providers.Chapters
 {
     public class ChapterManager : IChapterManager
     {
-        private IChapterProvider[] _providers;
         private readonly ILibraryManager _libraryManager;
         private readonly ILogger _logger;
         private readonly IServerConfigurationManager _config;
@@ -36,224 +34,6 @@ namespace MediaBrowser.Providers.Chapters
             _itemRepo = itemRepo;
         }
 
-        public void AddParts(IEnumerable<IChapterProvider> chapterProviders)
-        {
-            _providers = chapterProviders.ToArray();
-        }
-
-        public Task<IEnumerable<RemoteChapterResult>> Search(Video video, CancellationToken cancellationToken)
-        {
-            VideoContentType mediaType;
-
-            if (video is Episode)
-            {
-                mediaType = VideoContentType.Episode;
-            }
-            else if (video is Movie)
-            {
-                mediaType = VideoContentType.Movie;
-            }
-            else
-            {
-                // These are the only supported types
-                return Task.FromResult<IEnumerable<RemoteChapterResult>>(new List<RemoteChapterResult>());
-            }
-
-            var request = new ChapterSearchRequest
-            {
-                ContentType = mediaType,
-                IndexNumber = video.IndexNumber,
-                Language = video.GetPreferredMetadataLanguage(),
-                MediaPath = video.Path,
-                Name = video.Name,
-                ParentIndexNumber = video.ParentIndexNumber,
-                ProductionYear = video.ProductionYear,
-                ProviderIds = video.ProviderIds,
-                RuntimeTicks = video.RunTimeTicks,
-                SearchAllProviders = false
-            };
-
-            var episode = video as Episode;
-
-            if (episode != null)
-            {
-                request.IndexNumberEnd = episode.IndexNumberEnd;
-                request.SeriesName = episode.SeriesName;
-            }
-
-            return Search(request, cancellationToken);
-        }
-
-        public async Task<IEnumerable<RemoteChapterResult>> Search(ChapterSearchRequest request, CancellationToken cancellationToken)
-        {
-            var contentType = request.ContentType;
-            var providers = GetInternalProviders(false)
-                .Where(i => i.SupportedMediaTypes.Contains(contentType))
-                .ToList();
-
-            // If not searching all, search one at a time until something is found
-            if (!request.SearchAllProviders)
-            {
-                foreach (var provider in providers)
-                {
-                    try
-                    {
-                        var currentResults = await Search(request, provider, cancellationToken).ConfigureAwait(false);
-
-                        if (currentResults.Count > 0)
-                        {
-                            return currentResults;
-                        }
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.ErrorException("Error downloading subtitles from {0}", ex, provider.Name);
-                    }
-                }
-                return new List<RemoteChapterResult>();
-            }
-
-            var tasks = providers.Select(async i =>
-            {
-                try
-                {
-                    return await Search(request, i, cancellationToken).ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    _logger.ErrorException("Error downloading subtitles from {0}", ex, i.Name);
-                    return new List<RemoteChapterResult>();
-                }
-            });
-
-            var results = await Task.WhenAll(tasks).ConfigureAwait(false);
-
-            return results.SelectMany(i => i);
-        }
-
-        private async Task<List<RemoteChapterResult>> Search(ChapterSearchRequest request,
-            IChapterProvider provider,
-            CancellationToken cancellationToken)
-        {
-            var searchResults = await provider.Search(request, cancellationToken).ConfigureAwait(false);
-
-            var list = searchResults.ToList();
-
-            foreach (var result in list)
-            {
-                result.Id = GetProviderId(provider.Name) + "_" + result.Id;
-                result.ProviderName = provider.Name;
-            }
-
-            return list;
-        }
-
-        public Task<ChapterResponse> GetChapters(string id, CancellationToken cancellationToken)
-        {
-            var parts = id.Split(new[] { '_' }, 2);
-
-            var provider = GetProvider(parts.First());
-            id = parts.Last();
-
-            return provider.GetChapters(id, cancellationToken);
-        }
-
-        public IEnumerable<ChapterProviderInfo> GetProviders(string itemId)
-        {
-            var video = _libraryManager.GetItemById(itemId) as Video;
-            VideoContentType mediaType;
-
-            if (video is Episode)
-            {
-                mediaType = VideoContentType.Episode;
-            }
-            else if (video is Movie)
-            {
-                mediaType = VideoContentType.Movie;
-            }
-            else
-            {
-                // These are the only supported types
-                return new List<ChapterProviderInfo>();
-            }
-
-            var providers = GetInternalProviders(false)
-                .Where(i => i.SupportedMediaTypes.Contains(mediaType));
-
-            return GetInfos(providers);
-        }
-
-        public IEnumerable<ChapterProviderInfo> GetProviders()
-        {
-            return GetInfos(GetInternalProviders(true));
-        }
-
-        private IEnumerable<IChapterProvider> GetInternalProviders(bool includeDisabledProviders)
-        {
-            var providers = _providers;
-
-            if (!includeDisabledProviders)
-            {
-                var options = GetConfiguration();
-
-                providers = providers
-                    .Where(i => !options.DisabledFetchers.Contains(i.Name))
-                    .ToArray();
-            }
-
-            return providers
-                .OrderBy(GetConfiguredOrder)
-                .ThenBy(GetDefaultOrder)
-                .ToArray();
-        }
-
-        private IEnumerable<ChapterProviderInfo> GetInfos(IEnumerable<IChapterProvider> providers)
-        {
-            return providers.Select(i => new ChapterProviderInfo
-            {
-                Name = i.Name,
-                Id = GetProviderId(i.Name)
-            });
-        }
-
-        private string GetProviderId(string name)
-        {
-            return name.ToLower().GetMD5().ToString("N");
-        }
-
-        private IChapterProvider GetProvider(string id)
-        {
-            return _providers.First(i => string.Equals(id, GetProviderId(i.Name)));
-        }
-
-        private int GetConfiguredOrder(IChapterProvider provider)
-        {
-            var options = GetConfiguration();
-            
-            // See if there's a user-defined order
-            var index = Array.IndexOf(options.FetcherOrder, provider.Name);
-
-            if (index != -1)
-            {
-                return index;
-            }
-
-            // Not configured. Just return some high number to put it at the end.
-            return 100;
-        }
-
-        private int GetDefaultOrder(IChapterProvider provider)
-        {
-            var hasOrder = provider as IHasOrder;
-
-            if (hasOrder != null)
-            {
-                return hasOrder.Order;
-            }
-
-            return 0;
-        }
-
         public IEnumerable<ChapterInfo> GetChapters(string itemId)
         {
             return _itemRepo.GetChapters(new Guid(itemId));

+ 0 - 62
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -238,22 +238,6 @@ namespace MediaBrowser.Providers.MediaInfo
             if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
                 options.MetadataRefreshMode == MetadataRefreshMode.Default)
             {
-                var chapterOptions = _chapterManager.GetConfiguration();
-
-                try
-                {
-                    var remoteChapters = await DownloadChapters(video, chapters, chapterOptions, cancellationToken).ConfigureAwait(false);
-
-                    if (remoteChapters.Count > 0)
-                    {
-                        chapters = remoteChapters;
-                    }
-                }
-                catch (Exception ex)
-                {
-                    _logger.ErrorException("Error downloading chapters", ex);
-                }
-
                 if (chapters.Count == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
                 {
                     AddDummyChapters(video, chapters);
@@ -561,52 +545,6 @@ namespace MediaBrowser.Providers.MediaInfo
             currentStreams.AddRange(externalSubtitleStreams);
         }
 
-        private async Task<List<ChapterInfo>> DownloadChapters(Video video, List<ChapterInfo> currentChapters, ChapterOptions options, CancellationToken cancellationToken)
-        {
-            if ((options.DownloadEpisodeChapters &&
-                 video is Episode) ||
-                (options.DownloadMovieChapters &&
-                 video is Movie))
-            {
-                var results = await _chapterManager.Search(video, cancellationToken).ConfigureAwait(false);
-
-                var result = results.FirstOrDefault();
-
-                if (result != null)
-                {
-                    var chapters = await _chapterManager.GetChapters(result.Id, cancellationToken).ConfigureAwait(false);
-
-                    var chapterInfos = chapters.Chapters.Select(i => new ChapterInfo
-                    {
-                        Name = i.Name,
-                        StartPositionTicks = i.StartPositionTicks
-
-                    }).ToList();
-
-                    if (chapterInfos.All(i => i.StartPositionTicks == 0))
-                    {
-                        if (currentChapters.Count >= chapterInfos.Count)
-                        {
-                            var index = 0;
-                            foreach (var info in chapterInfos)
-                            {
-                                info.StartPositionTicks = currentChapters[index].StartPositionTicks;
-                                index++;
-                            }
-                        }
-                        else
-                        {
-                            chapterInfos.Clear();
-                        }
-                    }
-
-                    return chapterInfos;
-                }
-            }
-
-            return new List<ChapterInfo>();
-        }
-
         /// <summary>
         /// The dummy chapter duration
         /// </summary>

+ 70 - 20
MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs

@@ -14,6 +14,8 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using System.IO;
+using MediaBrowser.Model.Serialization;
 
 namespace MediaBrowser.Providers.MediaInfo
 {
@@ -24,14 +26,16 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly ISubtitleManager _subtitleManager;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly ILogger _logger;
+        private IJsonSerializer _json;
 
-        public SubtitleScheduledTask(ILibraryManager libraryManager, IServerConfigurationManager config, ISubtitleManager subtitleManager, ILogger logger, IMediaSourceManager mediaSourceManager)
+        public SubtitleScheduledTask(ILibraryManager libraryManager, IJsonSerializer json, IServerConfigurationManager config, ISubtitleManager subtitleManager, ILogger logger, IMediaSourceManager mediaSourceManager)
         {
             _libraryManager = libraryManager;
             _config = config;
             _subtitleManager = subtitleManager;
             _logger = logger;
             _mediaSourceManager = mediaSourceManager;
+            _json = json;
         }
 
         public string Name
@@ -58,38 +62,65 @@ namespace MediaBrowser.Providers.MediaInfo
         {
             var options = GetOptions();
 
-            var videos = _libraryManager.RootFolder
-                .GetRecursiveChildren(i =>
-                {
-                    if (!(i is Video))
-                    {
-                        return false;
-                    }
+            var types = new List<string>();
 
-                    if (i.LocationType == LocationType.Remote || i.LocationType == LocationType.Virtual)
-                    {
-                        return false;
-                    }
+            if (options.DownloadEpisodeSubtitles)
+            {
+                types.Add("Episode");
+            }
+            if (options.DownloadMovieSubtitles)
+            {
+                types.Add("Movie");
+            }
 
-                    return (options.DownloadEpisodeSubtitles &&
-                            i is Episode) ||
-                           (options.DownloadMovieSubtitles &&
-                            i is Movie);
-                })
-                .Cast<Video>()
+            if (types.Count == 0)
+            {
+                return;
+            }
+
+            var videos = _libraryManager.GetItemList(new InternalItemsQuery
+            {
+                MediaTypes = new string[] { MediaType.Video },
+                IsVirtualItem = false,
+                ExcludeLocationTypes = new LocationType[] { LocationType.Remote, LocationType.Virtual },
+                IncludeItemTypes = types.ToArray()
+
+            }).OfType<Video>()
                 .ToList();
 
+            var failHistoryPath = Path.Combine(_config.ApplicationPaths.CachePath, "subtitlehistory.json");
+            var history = GetHistory(failHistoryPath);
+
             var numComplete = 0;
 
             foreach (var video in videos)
             {
+                DateTime lastAttempt;
+                if (history.TryGetValue(video.Id.ToString("N"), out lastAttempt))
+                {
+                    if ((DateTime.UtcNow - lastAttempt).TotalDays <= 7)
+                    {
+                        continue;
+                    }
+                }
+
                 try
                 {
-                    await DownloadSubtitles(video, options, cancellationToken).ConfigureAwait(false);
+                    var shouldRetry = await DownloadSubtitles(video, options, cancellationToken).ConfigureAwait(false);
+
+                    if (shouldRetry)
+                    {
+                        history[video.Id.ToString("N")] = DateTime.UtcNow;
+                    }
+                    else
+                    {
+                        history.Remove(video.Id.ToString("N"));
+                    }
                 }
                 catch (Exception ex)
                 {
                     _logger.ErrorException("Error downloading subtitles for {0}", ex, video.Path);
+                    history[video.Id.ToString("N")] = DateTime.UtcNow;
                 }
 
                 // Update progress
@@ -99,9 +130,23 @@ namespace MediaBrowser.Providers.MediaInfo
 
                 progress.Report(100 * percent);
             }
+
+            _json.SerializeToFile(history, failHistoryPath);
         }
 
-        private async Task DownloadSubtitles(Video video, SubtitleOptions options, CancellationToken cancellationToken)
+        private Dictionary<string,DateTime> GetHistory(string path)
+        {
+            try
+            {
+                return _json.DeserializeFromFile<Dictionary<string, DateTime>>(path);
+            }
+            catch
+            {
+                return new Dictionary<string, DateTime>();
+            }
+        }
+
+        private async Task<bool> DownloadSubtitles(Video video, SubtitleOptions options, CancellationToken cancellationToken)
         {
             if ((options.DownloadEpisodeSubtitles &&
                 video is Episode) ||
@@ -124,8 +169,13 @@ namespace MediaBrowser.Providers.MediaInfo
                 if (downloadedLanguages.Count > 0)
                 {
                     await video.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+                    return false;
                 }
+
+                return true;
             }
+
+            return false;
         }
 
         public IEnumerable<ITaskTrigger> GetDefaultTriggers()

+ 1 - 1
MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs

@@ -30,7 +30,7 @@ namespace MediaBrowser.Server.Implementations.Channels
             return Task.FromResult<IEnumerable<MediaSourceInfo>>(new List<MediaSourceInfo>());
         }
 
-        public Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
+        public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken)
         {
             throw new NotImplementedException();
         }

+ 19 - 0
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -43,6 +43,7 @@ using MediaBrowser.Server.Implementations.Library.Resolvers;
 using MoreLinq;
 using SortOrder = MediaBrowser.Model.Entities.SortOrder;
 using VideoResolver = MediaBrowser.Naming.Video.VideoResolver;
+using MediaBrowser.Common.Configuration;
 
 namespace MediaBrowser.Server.Implementations.Library
 {
@@ -1900,6 +1901,24 @@ namespace MediaBrowser.Server.Implementations.Library
                 options.EnableInternetProviders = ConfigurationManager.Configuration.EnableInternetProviders;
             }
 
+            if (options.SchemaVersion < 2)
+            {
+                var chapterOptions = ConfigurationManager.GetConfiguration<ChapterOptions>("chapters");
+                options.ExtractChapterImagesDuringLibraryScan = chapterOptions.ExtractDuringLibraryScan;
+
+                if (collectionFolder != null)
+                {
+                    if (string.Equals(collectionFolder.CollectionType, "movies", StringComparison.OrdinalIgnoreCase))
+                    {
+                        options.EnableChapterImageExtraction = chapterOptions.EnableMovieChapterImageExtraction;
+                    }
+                    else if (string.Equals(collectionFolder.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+                    {
+                        options.EnableChapterImageExtraction = chapterOptions.EnableEpisodeChapterImageExtraction;
+                    }
+                }
+            }
+
             return options;
         }
 

+ 15 - 4
MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs

@@ -367,7 +367,9 @@ namespace MediaBrowser.Server.Implementations.Library
                 var tuple = GetProvider(request.OpenToken);
                 var provider = tuple.Item1;
 
-                var mediaSource = await provider.OpenMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false);
+                var mediaSourceTuple = await provider.OpenMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false);
+
+                var mediaSource = mediaSourceTuple.Item1;
 
                 if (string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
                 {
@@ -381,8 +383,10 @@ namespace MediaBrowser.Server.Implementations.Library
                     Date = DateTime.UtcNow,
                     EnableCloseTimer = enableAutoClose,
                     Id = mediaSource.LiveStreamId,
-                    MediaSource = mediaSource
+                    MediaSource = mediaSource,
+                    DirectStreamProvider = mediaSourceTuple.Item2
                 };
+
                 _openStreams[mediaSource.LiveStreamId] = info;
 
                 if (enableAutoClose)
@@ -414,7 +418,7 @@ namespace MediaBrowser.Server.Implementations.Library
             }
         }
 
-        public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
+        public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
         {
             if (string.IsNullOrWhiteSpace(id))
             {
@@ -430,7 +434,7 @@ namespace MediaBrowser.Server.Implementations.Library
                 LiveStreamInfo info;
                 if (_openStreams.TryGetValue(id, out info))
                 {
-                    return info.MediaSource;
+                    return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info.DirectStreamProvider);
                 }
                 else
                 {
@@ -443,6 +447,12 @@ namespace MediaBrowser.Server.Implementations.Library
             }
         }
 
+        public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
+        {
+            var result = await GetLiveStreamWithDirectStreamProvider(id, cancellationToken).ConfigureAwait(false);
+            return result.Item1;
+        }
+
         public async Task PingLiveStream(string id, CancellationToken cancellationToken)
         {
             await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -630,6 +640,7 @@ namespace MediaBrowser.Server.Implementations.Library
             public string Id;
             public bool Closed;
             public MediaSourceInfo MediaSource;
+            public IDirectStreamProvider DirectStreamProvider;
         }
     }
 }

+ 9 - 2
MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -36,7 +36,7 @@ using Microsoft.Win32;
 
 namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 {
-    public class EmbyTV : ILiveTvService, ISupportsNewTimerIds, IDisposable
+    public class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable
     {
         private readonly IApplicationHost _appHpst;
         private readonly ILogger _logger;
@@ -828,10 +828,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
         private readonly Dictionary<string, LiveStream> _liveStreams = new Dictionary<string, LiveStream>();
 
         public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
+        {
+            var result = await GetChannelStreamWithDirectStreamProvider(channelId, streamId, cancellationToken).ConfigureAwait(false);
+
+            return result.Item1;
+        }
+
+        public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, CancellationToken cancellationToken)
         {
             var result = await GetChannelStreamInternal(channelId, streamId, cancellationToken).ConfigureAwait(false);
 
-            return result.Item2;
+            return new Tuple<MediaSourceInfo, IDirectStreamProvider>(result.Item2, result.Item1 as IDirectStreamProvider);
         }
 
         private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, int consumerId, bool enableStreamSharing)

+ 21 - 7
MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -227,10 +227,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
         public async Task<MediaSourceInfo> GetRecordingStream(string id, CancellationToken cancellationToken)
         {
-            return await GetLiveStream(id, null, false, cancellationToken).ConfigureAwait(false);
+            var info = await GetLiveStream(id, null, false, cancellationToken).ConfigureAwait(false);
+
+            return info.Item1;
         }
 
-        public async Task<MediaSourceInfo> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken)
+        public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken)
         {
             return await GetLiveStream(id, mediaSourceId, true, cancellationToken).ConfigureAwait(false);
         }
@@ -280,7 +282,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return _services.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
         }
 
-        private async Task<MediaSourceInfo> GetLiveStream(string id, string mediaSourceId, bool isChannel, CancellationToken cancellationToken)
+        private async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStream(string id, string mediaSourceId, bool isChannel, CancellationToken cancellationToken)
         {
             await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
 
@@ -294,6 +296,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 MediaSourceInfo info;
                 bool isVideo;
                 ILiveTvService service;
+                IDirectStreamProvider directStreamProvider = null;
 
                 if (isChannel)
                 {
@@ -301,7 +304,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                     isVideo = channel.ChannelType == ChannelType.TV;
                     service = GetService(channel);
                     _logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId);
-                    info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
+
+                    var supportsManagedStream = service as ISupportsDirectStreamProvider;
+                    if (supportsManagedStream != null)
+                    {
+                        var streamInfo = await supportsManagedStream.GetChannelStreamWithDirectStreamProvider(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
+                        info = streamInfo.Item1;
+                        directStreamProvider = streamInfo.Item2;
+                    }
+                    else
+                    {
+                        info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
+                    }
                     info.RequiresClosing = true;
 
                     if (info.RequiresClosing)
@@ -332,7 +346,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 _logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(info));
                 Normalize(info, service, isVideo);
 
-                return info;
+                return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info, directStreamProvider);
             }
             catch (Exception ex)
             {
@@ -1881,11 +1895,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             {
                 if (query.IsScheduled.Value)
                 {
-                    timers = timers.Where(i => i.Item1.Status == RecordingStatus.New || i.Item1.Status == RecordingStatus.Scheduled);
+                    timers = timers.Where(i => i.Item1.Status == RecordingStatus.New);
                 }
                 else
                 {
-                    timers = timers.Where(i => !(i.Item1.Status == RecordingStatus.New || i.Item1.Status == RecordingStatus.Scheduled));
+                    timers = timers.Where(i => !(i.Item1.Status == RecordingStatus.New));
                 }
             }
 

+ 7 - 4
MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs

@@ -116,17 +116,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return list;
         }
 
-        public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
+        public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken)
         {
-            MediaSourceInfo stream;
+            MediaSourceInfo stream = null;
             const bool isAudio = false;
 
             var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
             var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
+            IDirectStreamProvider directStreamProvider = null;
 
             if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
             {
-                stream = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false);
+                var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false);
+                stream = info.Item1;
+                directStreamProvider = info.Item2;
             }
             else
             {
@@ -142,7 +145,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 _logger.ErrorException("Error probing live tv stream", ex);
             }
 
-            return stream;
+            return new Tuple<MediaSourceInfo, IDirectStreamProvider>(stream, directStreamProvider);
         }
 
         private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)

+ 77 - 2
MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs

@@ -6,14 +6,16 @@ using CommonIO;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
+using System.Collections.Generic;
 
 namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 {
-    public class HdHomerunLiveStream : LiveStream
+    public class HdHomerunLiveStream : LiveStream, IDirectStreamProvider
     {
         private readonly ILogger _logger;
         private readonly IHttpClient _httpClient;
@@ -106,7 +108,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                                     {
                                         onStarted = () => ResolveWhenExists(openTaskCompletionSource, tempFilePath, cancellationToken);
                                     }
-                                    await DirectRecorder.CopyUntilCancelled(response.Content, outputStream, onStarted, cancellationToken).ConfigureAwait(false);
+                                    await CopyUntilCancelled(response.Content, outputStream, onStarted, cancellationToken).ConfigureAwait(false);
                                 }
                             }
                         }
@@ -137,6 +139,79 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             }).ConfigureAwait(false);
         }
 
+        private List<Tuple<Stream, CancellationToken, TaskCompletionSource<bool>>> _additionalStreams = new List<Tuple<Stream, CancellationToken, TaskCompletionSource<bool>>>();
+
+        public Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
+        {
+            var taskCompletionSource = new TaskCompletionSource<bool>();
+            _additionalStreams.Add(new Tuple<Stream, CancellationToken, TaskCompletionSource<bool>>(stream, cancellationToken, taskCompletionSource));
+
+            return taskCompletionSource.Task;
+        }
+
+        private void PopAdditionalStream(Tuple<Stream, CancellationToken, TaskCompletionSource<bool>> stream, Exception exception)
+        {
+            if (_additionalStreams.Remove(stream))
+            {
+                stream.Item3.TrySetException(exception);
+            }
+        }
+
+        private const int BufferSize = 81920;
+        private async Task CopyUntilCancelled(Stream source, Stream target, Action onStarted, CancellationToken cancellationToken)
+        {
+            while (!cancellationToken.IsCancellationRequested)
+            {
+                var bytesRead = await CopyToAsyncInternal(source, target, BufferSize, onStarted, cancellationToken).ConfigureAwait(false);
+
+                onStarted = null;
+
+                //var position = fs.Position;
+                //_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
+
+                if (bytesRead == 0)
+                {
+                    await Task.Delay(100).ConfigureAwait(false);
+                }
+            }
+        }
+
+        private async Task<int> CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, Action onStarted, CancellationToken cancellationToken)
+        {
+            byte[] buffer = new byte[bufferSize];
+            int bytesRead;
+            int totalBytesRead = 0;
+
+            while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
+            {
+                await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
+
+                foreach (var additionalStream in _additionalStreams)
+                {
+                    cancellationToken.ThrowIfCancellationRequested();
+
+                    try
+                    {
+                        await additionalStream.Item1.WriteAsync(buffer, 0, bytesRead, additionalStream.Item2).ConfigureAwait(false);
+                    }
+                    catch (Exception ex)
+                    {
+                        PopAdditionalStream(additionalStream, ex);
+                    }
+                }
+
+                totalBytesRead += bytesRead;
+
+                if (onStarted != null)
+                {
+                    onStarted();
+                }
+                onStarted = null;
+            }
+
+            return totalBytesRead;
+        }
+
         private async void ResolveWhenExists(TaskCompletionSource<bool> taskCompletionSource, string file, CancellationToken cancellationToken)
         {
             while (!File.Exists(file) && !cancellationToken.IsCancellationRequested)

+ 2 - 2
MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs

@@ -99,7 +99,7 @@ namespace MediaBrowser.Server.Implementations.Sync
         // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
         private const string StreamIdDelimeterString = "_";
 
-        public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
+        public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken)
         {
             var openKeys = openToken.Split(new[] { StreamIdDelimeterString[0] }, 3);
 
@@ -137,7 +137,7 @@ namespace MediaBrowser.Server.Implementations.Sync
             mediaSource.Protocol = dynamicInfo.Protocol;
             mediaSource.RequiredHttpHeaders = dynamicInfo.RequiredHttpHeaders;
 
-            return mediaSource;
+            return new Tuple<MediaSourceInfo, IDirectStreamProvider>(mediaSource, null);
         }
 
         private void SetStaticMediaSourceInfo(LocalItem item, MediaSourceInfo mediaSource)

+ 0 - 1
MediaBrowser.Server.Startup.Common/ApplicationHost.cs

@@ -826,7 +826,6 @@ namespace MediaBrowser.Server.Startup.Common
             LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
 
             SubtitleManager.AddParts(GetExports<ISubtitleProvider>());
-            ChapterManager.AddParts(GetExports<IChapterProvider>());
 
             SessionManager.AddParts(GetExports<ISessionControllerFactory>());