From 578dec0c71361be17eed68f82f20840807a9c9f4 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 29 Mar 2015 00:56:39 -0400 Subject: [PATCH] update stream generation --- .../Playback/BaseStreamingService.cs | 31 +-- MediaBrowser.Api/Playback/MediaInfoService.cs | 180 +++++++++++------- MediaBrowser.Api/Playback/StreamRequest.cs | 3 +- MediaBrowser.Api/Playback/StreamState.cs | 4 +- .../Library/IMediaSourceManager.cs | 25 ++- .../Library/IMediaSourceProvider.cs | 8 +- .../LiveTv/ILiveTvManager.cs | 16 ++ MediaBrowser.Model/Dto/MediaSourceInfo.cs | 7 +- .../ChannelDynamicMediaSourceProvider.cs | 4 +- .../Library/MediaSourceManager.cs | 155 +++++++++++++-- .../LiveTv/LiveTvManager.cs | 27 ++- .../LiveTv/LiveTvMediaSourceProvider.cs | 51 +++-- .../Sync/SyncedMediaSourceProvider.cs | 8 +- 13 files changed, 387 insertions(+), 132 deletions(-) diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 435bda2c44..2e7c9a5a7b 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/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; diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs index cef8a34e57..d954c5b197 100644 --- a/MediaBrowser.Api/Playback/MediaInfoService.cs +++ b/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 + { + [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 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 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,83 +187,96 @@ 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 { mediaSource }, - Context = EncodingContext.Streaming, - DeviceId = auth.DeviceId, - ItemId = item.Id.ToString("N"), - Profile = profile, - MaxBitrate = maxBitrate - }; - - if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) - { - options.MediaSourceId = mediaSourceId; - options.AudioStreamIndex = audioStreamIndex; - options.SubtitleStreamIndex = subtitleStreamIndex; - } - - if (mediaSource.SupportsDirectPlay) - { - var supportsDirectStream = mediaSource.SupportsDirectStream; - - // Dummy this up to fool StreamBuilder - mediaSource.SupportsDirectStream = true; - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? - streamBuilder.BuildAudioItem(options) : - streamBuilder.BuildVideoItem(options); - - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectPlay = false; - } - - // Set this back to what it was - mediaSource.SupportsDirectStream = supportsDirectStream; - } - - if (mediaSource.SupportsDirectStream) - { - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? - streamBuilder.BuildAudioItem(options) : - streamBuilder.BuildVideoItem(options); - - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectStream = false; - } - } - - if (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; - } - } + SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex); } SortMediaSources(result); } + private void SetDeviceSpecificData(BaseItem item, + MediaSourceInfo mediaSource, + DeviceProfile profile, + AuthorizationInfo auth, + int? maxBitrate, + long startTimeTicks, + string mediaSourceId, + int? audioStreamIndex, + int? subtitleStreamIndex) + { + var streamBuilder = new StreamBuilder(); + + var options = new VideoOptions + { + MediaSources = new List { mediaSource }, + Context = EncodingContext.Streaming, + DeviceId = auth.DeviceId, + ItemId = item.Id.ToString("N"), + Profile = profile, + MaxBitrate = maxBitrate + }; + + if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) + { + options.MediaSourceId = mediaSourceId; + options.AudioStreamIndex = audioStreamIndex; + options.SubtitleStreamIndex = subtitleStreamIndex; + } + + if (mediaSource.SupportsDirectPlay) + { + var supportsDirectStream = mediaSource.SupportsDirectStream; + + // Dummy this up to fool StreamBuilder + mediaSource.SupportsDirectStream = true; + + // The MediaSource supports direct stream, now test to see if the client supports it + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? + streamBuilder.BuildAudioItem(options) : + streamBuilder.BuildVideoItem(options); + + if (streamInfo == null || !streamInfo.IsDirectStream) + { + mediaSource.SupportsDirectPlay = false; + } + + // Set this back to what it was + mediaSource.SupportsDirectStream = supportsDirectStream; + } + + if (mediaSource.SupportsDirectStream) + { + // The MediaSource supports direct stream, now test to see if the client supports it + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? + streamBuilder.BuildAudioItem(options) : + streamBuilder.BuildVideoItem(options); + + if (streamInfo == null || !streamInfo.IsDirectStream) + { + mediaSource.SupportsDirectStream = false; + } + } + + if (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) { var originalList = result.MediaSources.ToList(); diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs index b52260b506..7ed4fcd965 100644 --- a/MediaBrowser.Api/Playback/StreamRequest.cs +++ b/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 diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index 37f2c7702c..b097f3b6af 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/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) { diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index fda17aa279..292205c033 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -84,17 +84,34 @@ namespace MediaBrowser.Controller.Library /// /// Opens the media source. /// - /// The open key. + /// The open token. + /// if set to true [enable automatic close]. /// The cancellation token. /// Task<MediaSourceInfo>. - Task OpenMediaSource(string openKey, CancellationToken cancellationToken); + Task OpenLiveStream(string openToken, bool enableAutoClose, CancellationToken cancellationToken); + + /// + /// Gets the live stream. + /// + /// The identifier. + /// The cancellation token. + /// Task<MediaSourceInfo>. + Task GetLiveStream(string id, CancellationToken cancellationToken); + + /// + /// Pings the media source. + /// + /// The live stream identifier. + /// The cancellation token. + /// Task. + Task PingLiveStream(string id, CancellationToken cancellationToken); /// /// Closes the media source. /// - /// The close key. + /// The live stream identifier. /// The cancellation token. /// Task. - Task CloseMediaSource(string closeKey, CancellationToken cancellationToken); + Task CloseLiveStream(string id, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs index c5f5b54010..5b033af4af 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs @@ -19,17 +19,17 @@ namespace MediaBrowser.Controller.Library /// /// Opens the media source. /// - /// The open key. + /// The open token. /// The cancellation token. /// Task<MediaSourceInfo>. - Task OpenMediaSource(string openKey, CancellationToken cancellationToken); + Task OpenMediaSource(string openToken, CancellationToken cancellationToken); /// /// Closes the media source. /// - /// The close key. + /// The live stream identifier. /// The cancellation token. /// Task. - Task CloseMediaSource(string closeKey, CancellationToken cancellationToken); + Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index 0b58a92328..d5b5d92a6e 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -301,5 +301,21 @@ namespace MediaBrowser.Controller.LiveTv /// The cancellation token. /// Task<QueryResult<BaseItem>>. Task> GetInternalRecordings(RecordingQuery query, CancellationToken cancellationToken); + + /// + /// Gets the recording media sources. + /// + /// The identifier. + /// The cancellation token. + /// Task<IEnumerable<MediaSourceInfo>>. + Task> GetRecordingMediaSources(string id, CancellationToken cancellationToken); + + /// + /// Gets the channel media sources. + /// + /// The identifier. + /// The cancellation token. + /// Task<IEnumerable<MediaSourceInfo>>. + Task> GetChannelMediaSources(string id, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index 92af8d671c..3b45137241 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/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; } diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs index 6a7163bb3b..dac3a80f2c 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs @@ -30,12 +30,12 @@ namespace MediaBrowser.Server.Implementations.Channels return Task.FromResult>(new List()); } - public Task OpenMediaSource(string openKey, CancellationToken cancellationToken) + public Task 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(); } diff --git a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs b/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs index 40cf240d7e..3dbcf4aad4 100644 --- a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs +++ b/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 _openStreams = - new ConcurrentDictionary(); + private readonly ConcurrentDictionary _openStreams = new ConcurrentDictionary(); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); - public async Task OpenMediaSource(string openKey, CancellationToken cancellationToken) + + public async Task 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 GetLiveStream(string id, CancellationToken cancellationToken) { await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { - var tuple = GetProvider(closeKey); + LiveStreamInfo info; + if (_openStreams.TryGetValue(id, out info)) + { + return info.MediaSource; + } + else + { + throw new ResourceNotFoundException(); + } + } + finally + { + _liveStreamSemaphore.Release(); + } + } - await tuple.Item1.OpenMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false); + public async Task PingLiveStream(string id, CancellationToken cancellationToken) + { + await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - string removedKey; - _openStreams.TryRemove(closeKey, out removedKey); + 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(id); + + await tuple.Item1.CloseMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false); + + LiveStreamInfo removed; + if (_openStreams.TryRemove(id, out removed)) + { + removed.Closed = true; + } + + if (_openStreams.Count == 0) + { + StopCloseTimer(); + } } finally { @@ -368,11 +437,56 @@ namespace MediaBrowser.Server.Implementations.Library return new Tuple(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); + } + } + } + } + /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// 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; + } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index 202a051e3b..86b31e0e5d 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/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> 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> 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 { diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index 186bc499db..5de4cf4998 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/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> GetMediaSources(IHasMediaSources item, CancellationToken cancellationToken) @@ -38,28 +44,51 @@ namespace MediaBrowser.Server.Implementations.LiveTv private async Task> GetMediaSourcesInternal(ILiveTvItem item, CancellationToken cancellationToken) { - var hasMediaSources = (IHasMediaSources)item; + IEnumerable 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; - foreach (var source in sources) + sources = hasMediaSources.GetMediaSources(false) + .ToList(); + } + + var list = sources.ToList(); + + foreach (var source in list) { source.Type = MediaSourceType.Default; source.RequiresOpening = true; + source.BufferMs = source.BufferMs ?? 1500; var openKeys = new List(); 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 OpenMediaSource(string openKey, CancellationToken cancellationToken) + public async Task 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); } } } diff --git a/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs index 25a52fb950..1c17b99936 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs +++ b/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 OpenMediaSource(string openKey, CancellationToken cancellationToken) + public async Task 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(); }