Selaa lähdekoodia

update stream generation

Luke Pulverenti 10 vuotta sitten
vanhempi
sitoutus
578dec0c71

+ 20 - 11
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -5,7 +5,6 @@ using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dlna;
@@ -937,7 +936,7 @@ namespace MediaBrowser.Api.Playback
 
             if (state.MediaSource.RequiresOpening)
             {
-                var mediaSource = await MediaSourceManager.OpenMediaSource(state.MediaSource.OpenKey, cancellationTokenSource.Token)
+                var mediaSource = await MediaSourceManager.OpenLiveStream(state.MediaSource.OpenToken, false, cancellationTokenSource.Token)
                             .ConfigureAwait(false);
 
                 AttachMediaSourceInfo(state, mediaSource, state.VideoRequest, state.RequestedUrl);
@@ -946,9 +945,11 @@ namespace MediaBrowser.Api.Playback
                 {
                     TryStreamCopy(state, state.VideoRequest);
                 }
+            }
 
-                // TODO: This is only needed for live tv
-                await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
+            if (state.MediaSource.BufferMs.HasValue)
+            {
+                await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
             }
         }
 
@@ -1616,12 +1617,20 @@ namespace MediaBrowser.Api.Playback
             var archivable = item as IArchivable;
             state.IsInputArchive = archivable != null && archivable.IsArchive;
 
-            var mediaSources = await MediaSourceManager.GetPlayackMediaSources(request.Id, false, cancellationToken).ConfigureAwait(false);
+            MediaSourceInfo mediaSource = null;
+            if (string.IsNullOrWhiteSpace(request.LiveStreamId))
+            {
+                var mediaSources = await MediaSourceManager.GetPlayackMediaSources(request.Id, false, cancellationToken).ConfigureAwait(false);
+
+                mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
+                   ? mediaSources.First()
+                   : mediaSources.First(i => string.Equals(i.Id, request.MediaSourceId));
+            }
+            else
+            {
+                mediaSource = await MediaSourceManager.GetLiveStream(request.LiveStreamId, cancellationToken).ConfigureAwait(false);
+            }
 
-            var mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
-                ? mediaSources.First()
-                : mediaSources.First(i => string.Equals(i.Id, request.MediaSourceId));
-            
             var videoRequest = request as VideoStreamRequest;
 
             AttachMediaSourceInfo(state, mediaSource, videoRequest, url);
@@ -1699,7 +1708,7 @@ namespace MediaBrowser.Api.Playback
             state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
             state.RunTimeTicks = mediaSource.RunTimeTicks;
             state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
-            
+
             if (mediaSource.VideoType.HasValue)
             {
                 state.VideoType = mediaSource.VideoType.Value;
@@ -1713,7 +1722,7 @@ namespace MediaBrowser.Api.Playback
             {
                 state.InputTimestamp = mediaSource.Timestamp.Value;
             }
-            
+
             state.InputProtocol = mediaSource.Protocol;
             state.MediaPath = mediaSource.Path;
             state.RunTimeTicks = mediaSource.RunTimeTicks;

+ 101 - 59
MediaBrowser.Api/Playback/MediaInfoService.cs

@@ -45,6 +45,9 @@ namespace MediaBrowser.Api.Playback
         [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
         public string UserId { get; set; }
 
+        [ApiMember(Name = "MaxStreamingBitrate", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
+        public int? MaxStreamingBitrate { get; set; }
+
         [ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
         public long? StartTimeTicks { get; set; }
 
@@ -58,6 +61,20 @@ namespace MediaBrowser.Api.Playback
         public string MediaSourceId { get; set; }
     }
 
+    [Route("/MediaSources/Open", "POST", Summary = "Opens a media source")]
+    public class OpenMediaSource : IReturn<MediaSourceInfo>
+    {
+        [ApiMember(Name = "OpenToken", Description = "OpenToken", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string OpenToken { get; set; }
+    }
+
+    [Route("/MediaSources/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]
     public class MediaInfoService : BaseApiService
     {
@@ -84,6 +101,18 @@ namespace MediaBrowser.Api.Playback
             return ToOptimizedResult(result);
         }
 
+        public async Task<object> Post(OpenMediaSource request)
+        {
+            var result = await _mediaSourceManager.OpenLiveStream(request.OpenToken, false, CancellationToken.None).ConfigureAwait(false);
+            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).ConfigureAwait(false);
@@ -102,7 +131,7 @@ namespace MediaBrowser.Api.Playback
             if (profile != null)
             {
                 var mediaSourceId = request.MediaSourceId;
-                SetDeviceSpecificData(request.Id, info, profile, authInfo, null, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex);
+                SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex);
             }
 
             return ToOptimizedResult(info);
@@ -158,81 +187,94 @@ namespace MediaBrowser.Api.Playback
             int? audioStreamIndex,
             int? subtitleStreamIndex)
         {
-            var streamBuilder = new StreamBuilder();
-
             var item = _libraryManager.GetItemById(itemId);
 
             foreach (var mediaSource in result.MediaSources)
             {
-                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;
-                }
+                SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
+            }
 
-                if (mediaSource.SupportsDirectPlay)
-                {
-                    var supportsDirectStream = mediaSource.SupportsDirectStream;
+            SortMediaSources(result);
+        }
 
-                    // Dummy this up to fool StreamBuilder
-                    mediaSource.SupportsDirectStream = true;
+        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();
 
-                    // 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);
+            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 (streamInfo == null || !streamInfo.IsDirectStream)
-                    {
-                        mediaSource.SupportsDirectPlay = false;
-                    }
+            if (mediaSource.SupportsDirectPlay)
+            {
+                var supportsDirectStream = mediaSource.SupportsDirectStream;
 
-                    // Set this back to what it was
-                    mediaSource.SupportsDirectStream = supportsDirectStream;
-                }
+                // Dummy this up to fool StreamBuilder
+                mediaSource.SupportsDirectStream = true;
 
-                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)
                 {
-                    // 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;
-                    }
+                    mediaSource.SupportsDirectPlay = false;
                 }
 
-                if (mediaSource.SupportsTranscoding)
+                // Set this back to what it was
+                mediaSource.SupportsDirectStream = supportsDirectStream;
+            }
+
+            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)
                 {
-                    // 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("-", auth.Token).Substring(1);
-                        mediaSource.TranscodingContainer = streamInfo.Container;
-                        mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-                    }
+                    mediaSource.SupportsDirectStream = false;
                 }
             }
 
-            SortMediaSources(result);
+            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("-", auth.Token).Substring(1);
+                    mediaSource.TranscodingContainer = streamInfo.Container;
+                    mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+                }
+            }
         }
 
         private void SortMediaSources(PlaybackInfoResponse result)

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

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

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

@@ -182,11 +182,11 @@ namespace MediaBrowser.Api.Playback
 
         private async void DisposeLiveStream()
         {
-            if (MediaSource.RequiresClosing)
+            if (MediaSource.RequiresClosing && string.IsNullOrWhiteSpace(Request.LiveStreamId))
             {
                 try
                 {
-                    await _mediaSourceManager.CloseMediaSource(MediaSource.CloseKey, CancellationToken.None).ConfigureAwait(false);
+                    await _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId, CancellationToken.None).ConfigureAwait(false);
                 }
                 catch (Exception ex)
                 {

+ 21 - 4
MediaBrowser.Controller/Library/IMediaSourceManager.cs

@@ -84,17 +84,34 @@ namespace MediaBrowser.Controller.Library
         /// <summary>
         /// Opens the media source.
         /// </summary>
-        /// <param name="openKey">The open key.</param>
+        /// <param name="openToken">The open token.</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<MediaSourceInfo> OpenMediaSource(string openKey, CancellationToken cancellationToken);
+        Task<MediaSourceInfo> OpenLiveStream(string openToken, 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="closeKey">The close key.</param>
+        /// <param name="id">The live stream identifier.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        Task CloseMediaSource(string closeKey, CancellationToken cancellationToken);
+        Task CloseLiveStream(string id, CancellationToken cancellationToken);
     }
 }

+ 4 - 4
MediaBrowser.Controller/Library/IMediaSourceProvider.cs

@@ -19,17 +19,17 @@ namespace MediaBrowser.Controller.Library
         /// <summary>
         /// Opens the media source.
         /// </summary>
-        /// <param name="openKey">The open key.</param>
+        /// <param name="openToken">The open token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task&lt;MediaSourceInfo&gt;.</returns>
-        Task<MediaSourceInfo> OpenMediaSource(string openKey, CancellationToken cancellationToken);
+        Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken);
 
         /// <summary>
         /// Closes the media source.
         /// </summary>
-        /// <param name="closeKey">The close key.</param>
+        /// <param name="liveStreamId">The live stream identifier.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        Task CloseMediaSource(string closeKey, CancellationToken cancellationToken);
+        Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken);
     }
 }

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

@@ -301,5 +301,21 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task&lt;QueryResult&lt;BaseItem&gt;&gt;.</returns>
         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);
     }
 }

+ 4 - 3
MediaBrowser.Model/Dto/MediaSourceInfo.cs

@@ -27,10 +27,11 @@ namespace MediaBrowser.Model.Dto
         public bool SupportsDirectPlay { get; set; }
 
         public bool RequiresOpening { get; set; }
-        public string OpenKey { get; set; }
+        public string OpenToken { get; set; }
         public bool RequiresClosing { get; set; }
-        public string CloseKey { get; set; }
-   
+        public string LiveStreamId { get; set; }
+        public int? BufferMs { get; set; }
+
         public VideoType? VideoType { get; set; }
 
         public IsoType? IsoType { get; set; }

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

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

+ 139 - 16
MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs

@@ -207,14 +207,14 @@ namespace MediaBrowser.Server.Implementations.Library
         {
             var prefix = provider.GetType().FullName.GetMD5().ToString("N") + "|";
 
-            if (!string.IsNullOrWhiteSpace(mediaSource.OpenKey) && !mediaSource.OpenKey.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+            if (!string.IsNullOrWhiteSpace(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
             {
-                mediaSource.OpenKey = prefix + mediaSource.OpenKey;
+                mediaSource.OpenToken = prefix + mediaSource.OpenToken;
             }
 
-            if (!string.IsNullOrWhiteSpace(mediaSource.CloseKey) && !mediaSource.CloseKey.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+            if (!string.IsNullOrWhiteSpace(mediaSource.LiveStreamId) && !mediaSource.LiveStreamId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
             {
-                mediaSource.CloseKey = prefix + mediaSource.CloseKey;
+                mediaSource.LiveStreamId = prefix + mediaSource.LiveStreamId;
             }
         }
 
@@ -314,24 +314,41 @@ namespace MediaBrowser.Server.Implementations.Library
             return GetStaticMediaSources(item, enablePathSubstitution).FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
         }
 
-        private readonly ConcurrentDictionary<string, string> _openStreams =
-         new ConcurrentDictionary<string, string>();
+        private readonly ConcurrentDictionary<string, LiveStreamInfo> _openStreams = new ConcurrentDictionary<string, LiveStreamInfo>();
         private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
-        public async Task<MediaSourceInfo> OpenMediaSource(string openKey, CancellationToken cancellationToken)
+
+        public async Task<MediaSourceInfo> OpenLiveStream(string openToken, bool enableAutoClose, CancellationToken cancellationToken)
         {
             await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
 
             try
             {
-                var tuple = GetProvider(openKey);
+                var tuple = GetProvider(openToken);
                 var provider = tuple.Item1;
 
                 var mediaSource = await provider.OpenMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false);
 
                 SetKeyProperties(provider, mediaSource);
 
-                _openStreams.AddOrUpdate(mediaSource.CloseKey, mediaSource.CloseKey, (key, i) => mediaSource.CloseKey);
-                
+                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();
+                }
+
+                if (!string.IsNullOrWhiteSpace(mediaSource.TranscodingUrl))
+                {
+                    mediaSource.TranscodingUrl += "&LiveStreamId=" + mediaSource.LiveStreamId;
+                }
+
                 return mediaSource;
             }
             finally
@@ -340,18 +357,70 @@ namespace MediaBrowser.Server.Implementations.Library
             }
         }
 
-        public async Task CloseMediaSource(string closeKey, CancellationToken cancellationToken)
+        public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
+        {
+            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
             {
-                var tuple = GetProvider(closeKey);
+                var tuple = GetProvider(id);
 
-                await tuple.Item1.OpenMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false);
+                await tuple.Item1.CloseMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false);
 
-                string removedKey;
-                _openStreams.TryRemove(closeKey, out removedKey);
+                LiveStreamInfo removed;
+                if (_openStreams.TryRemove(id, out removed))
+                {
+                    removed.Closed = true;
+                }
+
+                if (_openStreams.Count == 0)
+                {
+                    StopCloseTimer();
+                }
             }
             finally
             {
@@ -368,11 +437,56 @@ namespace MediaBrowser.Server.Implementations.Library
             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);
         }
 
@@ -389,7 +503,7 @@ namespace MediaBrowser.Server.Implementations.Library
                 {
                     foreach (var key in _openStreams.Keys.ToList())
                     {
-                        var task = CloseMediaSource(key, CancellationToken.None);
+                        var task = CloseLiveStream(key, CancellationToken.None);
 
                         Task.WaitAll(task);
                     }
@@ -398,5 +512,14 @@ namespace MediaBrowser.Server.Implementations.Library
                 }
             }
         }
+
+        private class LiveStreamInfo
+        {
+            public DateTime Date;
+            public bool EnableCloseTimer;
+            public string Id;
+            public bool Closed;
+            public MediaSourceInfo MediaSource;
+        }
     }
 }

+ 23 - 4
MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -313,6 +313,22 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             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)
         {
             return GetService(item.ServiceName);
@@ -330,7 +346,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             try
             {
                 MediaSourceInfo info;
-                var isVideo = true;
+                bool isVideo;
 
                 if (isChannel)
                 {
@@ -340,7 +356,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                     _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.RequiresClosing = true;
-                    info.CloseKey = info.Id;
+                    info.LiveStreamId = info.Id;
                 }
                 else
                 {
@@ -351,7 +367,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                     _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.RequiresClosing = true;
-                    info.CloseKey = info.Id;
+                    info.LiveStreamId = info.Id;
                 }
 
                 _logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(info));
@@ -393,7 +409,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                         {
                             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
+                            Index = -1,
+
+                            // Set to true if unknown to enable deinterlacing
+                            IsInterlaced = true
                         },
                         new MediaStream
                         {

+ 40 - 11
MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs

@@ -2,6 +2,8 @@
 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;
@@ -13,10 +15,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
     public class LiveTvMediaSourceProvider : IMediaSourceProvider
     {
         private readonly ILiveTvManager _liveTvManager;
+        private readonly IJsonSerializer _jsonSerializer;
+        private readonly ILogger _logger;
 
-        public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager)
+        public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IJsonSerializer jsonSerializer, ILogManager logManager)
         {
             _liveTvManager = liveTvManager;
+            _jsonSerializer = jsonSerializer;
+            _logger = logManager.GetLogger(GetType().Name);
         }
 
         public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(IHasMediaSources item, CancellationToken cancellationToken)
@@ -38,28 +44,51 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
         private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(ILiveTvItem item, CancellationToken cancellationToken)
         {
-            var hasMediaSources = (IHasMediaSources)item;
+            IEnumerable<MediaSourceInfo> sources;
 
-            var sources = hasMediaSources.GetMediaSources(false)
-                .ToList();
+            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 = hasMediaSources.GetMediaSources(false)
+                   .ToList();
+            }
 
-            foreach (var source in sources)
+            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.OpenKey = string.Join("|", openKeys.ToArray());
+                source.OpenToken = string.Join("|", openKeys.ToArray());
             }
 
-            return sources;
+            _logger.Debug("MediaSources: {0}", _jsonSerializer.SerializeToString(list));
+
+            return list;
         }
 
-        public async Task<MediaSourceInfo> OpenMediaSource(string openKey, CancellationToken cancellationToken)
+        public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
         {
-            var keys = openKey.Split(new[] { '|' }, 2);
+            var keys = openToken.Split(new[] { '|' }, 2);
 
             if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
             {
@@ -69,9 +98,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return await _liveTvManager.GetRecordingStream(keys[1], cancellationToken).ConfigureAwait(false);
         }
 
-        public Task CloseMediaSource(string closeKey, CancellationToken cancellationToken)
+        public Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken)
         {
-            return _liveTvManager.CloseLiveStream(closeKey, cancellationToken);
+            return _liveTvManager.CloseLiveStream(liveStreamId, cancellationToken);
         }
     }
 }

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

@@ -90,13 +90,13 @@ namespace MediaBrowser.Server.Implementations.Sync
                 keyList.Add(provider.GetType().FullName.GetMD5().ToString("N"));
                 keyList.Add(target.Id.GetMD5().ToString("N"));
                 keyList.Add(item.Id);
-                mediaSource.OpenKey = string.Join("|", keyList.ToArray());
+                mediaSource.OpenToken = string.Join("|", keyList.ToArray());
             }
         }
 
-        public async Task<MediaSourceInfo> OpenMediaSource(string openKey, CancellationToken cancellationToken)
+        public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
         {
-            var openKeys = openKey.Split(new[] { '|' }, 3);
+            var openKeys = openToken.Split(new[] { '|' }, 3);
 
             var provider = _syncManager.ServerSyncProviders
                 .FirstOrDefault(i => string.Equals(openKeys[0], i.GetType().FullName.GetMD5().ToString("N"), StringComparison.OrdinalIgnoreCase));
@@ -133,7 +133,7 @@ namespace MediaBrowser.Server.Implementations.Sync
             mediaSource.SupportsTranscoding = false;
         }
 
-        public Task CloseMediaSource(string closeKey, CancellationToken cancellationToken)
+        public Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken)
         {
             throw new NotImplementedException();
         }