diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs index bedbcffe3..2e3a71f70 100644 --- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs +++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Dto; +using System; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.LiveTv; using System.Collections.Generic; using System.Threading; @@ -37,7 +38,7 @@ namespace MediaBrowser.Controller.LiveTv /// The stream identifier. /// The cancellation token. /// Task<MediaSourceInfo>. - Task GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken); + Task> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken); /// /// Gets the channel stream media sources. /// diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 06a35ac0d..dfcafa32d 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -226,11 +226,15 @@ namespace MediaBrowser.Model.Configuration public bool EnableDateLastRefresh { get; set; } + public string[] Migrations { get; set; } + /// /// Initializes a new instance of the class. /// public ServerConfiguration() { + Migrations = new string[] {}; + ImageSavingConvention = ImageSavingConvention.Compatible; PublicPort = 8096; PublicHttpsPort = 8920; diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 3ec64a017..0d4960795 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -487,6 +487,29 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { _logger.Info("Streaming Channel " + channelId); + foreach (var hostInstance in _liveTvManager.TunerHosts) + { + try + { + var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); + + result.Item2.Release(); + + return result.Item1; + } + catch (Exception e) + { + _logger.ErrorException("Error getting channel stream", e); + } + } + + throw new ApplicationException("Tuner not found."); + } + + private async Task> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken) + { + _logger.Info("Streaming Channel " + channelId); + foreach (var hostInstance in _liveTvManager.TunerHosts) { try @@ -653,40 +676,56 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV try { - var mediaStreamInfo = await GetChannelStream(timer.ChannelId, null, CancellationToken.None); + var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None); + var mediaStreamInfo = result.Item1; + var isResourceOpen = true; - // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg - await Task.Delay(3000, cancellationToken).ConfigureAwait(false); - - var duration = recordingEndDate - DateTime.UtcNow; - - HttpRequestOptions httpRequestOptions = new HttpRequestOptions() + // Unfortunately due to the semaphore we have to have a nested try/finally + try { - Url = mediaStreamInfo.Path - }; + // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg + await Task.Delay(3000, cancellationToken).ConfigureAwait(false); - recording.Path = recordPath; - recording.Status = RecordingStatus.InProgress; - recording.DateLastUpdated = DateTime.UtcNow; - _recordingProvider.Update(recording); + var duration = recordingEndDate - DateTime.UtcNow; - _logger.Info("Beginning recording."); - - httpRequestOptions.BufferContent = false; - var durationToken = new CancellationTokenSource(duration); - var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - httpRequestOptions.CancellationToken = linkedToken; - _logger.Info("Writing file to path: " + recordPath); - using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET")) - { - using (var output = _fileSystem.GetFileStream(recordPath, FileMode.Create, FileAccess.Write, FileShare.Read)) + HttpRequestOptions httpRequestOptions = new HttpRequestOptions() { - await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, linkedToken); + Url = mediaStreamInfo.Path + }; + + recording.Path = recordPath; + recording.Status = RecordingStatus.InProgress; + recording.DateLastUpdated = DateTime.UtcNow; + _recordingProvider.Update(recording); + + _logger.Info("Beginning recording."); + + httpRequestOptions.BufferContent = false; + var durationToken = new CancellationTokenSource(duration); + var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + httpRequestOptions.CancellationToken = linkedToken; + _logger.Info("Writing file to path: " + recordPath); + using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET")) + { + using (var output = _fileSystem.GetFileStream(recordPath, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + result.Item2.Release(); + isResourceOpen = false; + + await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, linkedToken); + } + } + + recording.Status = RecordingStatus.Completed; + _logger.Info("Recording completed"); + } + finally + { + if (isResourceOpen) + { + result.Item2.Release(); } } - - recording.Status = RecordingStatus.Completed; - _logger.Info("Recording completed"); } catch (OperationCanceledException) { diff --git a/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs index d8d91c2f9..3fb1d9661 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.LiveTv { - class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask, IHasKey + public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask, IHasKey { private readonly ILiveTvManager _liveTvManager; private readonly IConfigurationManager _config; diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 616d01a32..d811152c2 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -141,7 +141,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts protected abstract Task GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken); - public async Task GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) + public async Task> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) { if (IsValidChannelId(channelId)) { @@ -173,9 +173,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts try { var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false); - - await AddMediaInfo(stream, false, cancellationToken).ConfigureAwait(false); - return stream; + var resourcePool = GetLock(host.Url); + + await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false); + return new Tuple(stream, resourcePool); } catch (Exception ex) { @@ -187,7 +188,40 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts throw new LiveTvConflictException(); } - private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken) + /// + /// The _semaphoreLocks + /// + private readonly ConcurrentDictionary _semaphoreLocks = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + /// + /// Gets the lock. + /// + /// The filename. + /// System.Object. + private SemaphoreSlim GetLock(string url) + { + return _semaphoreLocks.GetOrAdd(url, key => new SemaphoreSlim(1, 1)); + } + + private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, SemaphoreSlim resourcePool, CancellationToken cancellationToken) + { + await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + await AddMediaInfoInternal(mediaSource, isAudio, cancellationToken).ConfigureAwait(false); + + // Leave the resource locked. it will be released upstream + } + catch (Exception) + { + // Release the resource if there's some kind of failure. + resourcePool.Release(); + + throw; + } + } + + private async Task AddMediaInfoInternal(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken) { var originalRuntime = mediaSource.RunTimeTicks; diff --git a/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs b/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs index 6259c61af..60b8c00bd 100644 --- a/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs @@ -1,5 +1,4 @@ -using MediaBrowser.Common.IO; -using MediaBrowser.Common.Progress; +using MediaBrowser.Common.Progress; using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -17,7 +16,7 @@ using MediaBrowser.Controller.Entities.Audio; namespace MediaBrowser.Server.Implementations.Persistence { - class CleanDatabaseScheduledTask : IScheduledTask + public class CleanDatabaseScheduledTask : IScheduledTask { private readonly ILibraryManager _libraryManager; private readonly IItemRepository _itemRepo; diff --git a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs index 840ccd8af..bcb75d9a0 100644 --- a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs +++ b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs @@ -333,18 +333,18 @@ namespace MediaBrowser.Server.Startup.Common }); LogManager.RemoveConsoleOutput(); + + PerformPostInitMigrations(); } - public override async Task Init(IProgress progress) + public override Task Init(IProgress progress) { HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber; HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber; PerformPreInitMigrations(); - await base.Init(progress).ConfigureAwait(false); - - PerformPostInitMigrations(); + return base.Init(progress); } private void PerformPreInitMigrations() @@ -362,7 +362,10 @@ namespace MediaBrowser.Server.Startup.Common private void PerformPostInitMigrations() { - var migrations = new List(); + var migrations = new List + { + new Release5767(ServerConfigurationManager, TaskManager) + }; foreach (var task in migrations) { diff --git a/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj b/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj index 9def89073..13b782e40 100644 --- a/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj +++ b/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj @@ -72,6 +72,7 @@ + diff --git a/MediaBrowser.Server.Startup.Common/Migrations/Release5767.cs b/MediaBrowser.Server.Startup.Common/Migrations/Release5767.cs new file mode 100644 index 000000000..9a4580c12 --- /dev/null +++ b/MediaBrowser.Server.Startup.Common/Migrations/Release5767.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.ScheduledTasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Server.Implementations.LiveTv; +using MediaBrowser.Server.Implementations.Persistence; +using MediaBrowser.Server.Implementations.ScheduledTasks; + +namespace MediaBrowser.Server.Startup.Common.Migrations +{ + public class Release5767 : IVersionMigration + { + private readonly IServerConfigurationManager _config; + private readonly ITaskManager _taskManager; + + public Release5767(IServerConfigurationManager config, ITaskManager taskManager) + { + _config = config; + _taskManager = taskManager; + } + + public void Run() + { + var name = "5767"; + + if (_config.Configuration.Migrations.Contains(name, StringComparer.OrdinalIgnoreCase)) + { + return; + } + + Task.Run(async () => + { + await Task.Delay(3000).ConfigureAwait(false); + + _taskManager.QueueScheduledTask(); + _taskManager.QueueScheduledTask(); + _taskManager.QueueScheduledTask(); + }); + + var list = _config.Configuration.Migrations.ToList(); + list.Add(name); + _config.Configuration.Migrations = list.ToArray(); + _config.SaveConfiguration(); + } + } +}