diff options
Diffstat (limited to 'MediaBrowser.Server.Implementations')
11 files changed, 339 insertions, 249 deletions
diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs index bcc4e5dcf..76f0e6a1d 100644 --- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -43,16 +43,14 @@ namespace MediaBrowser.Server.Implementations.IO // WMC temp recording directories that will constantly be written to "TempRec", - "TempSBE", - "@eaDir", - "eaDir", - "#recycle" + "TempSBE" }; private readonly IReadOnlyList<string> _alwaysIgnoreSubstrings = new List<string> { // Synology - "@eaDir", + "eaDir", + "#recycle", ".wd_tv", ".actors" }; diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index 7c3196065..b076996df 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -2803,6 +2803,17 @@ namespace MediaBrowser.Server.Implementations.Library } } + private bool ValidateNetworkPath(string path) + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT || !path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase)) + { + return Directory.Exists(path); + } + + // Without native support for unc, we cannot validate this when running under mono + return true; + } + private const string ShortcutFileExtension = ".mblink"; private const string ShortcutFileSearch = "*" + ShortcutFileExtension; public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo) @@ -2829,12 +2840,7 @@ namespace MediaBrowser.Server.Implementations.Library throw new DirectoryNotFoundException("The path does not exist."); } - if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !_fileSystem.DirectoryExists(pathInfo.NetworkPath)) - { - throw new DirectoryNotFoundException("The network path does not exist."); - } - - if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !_fileSystem.DirectoryExists(pathInfo.NetworkPath)) + if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath)) { throw new DirectoryNotFoundException("The network path does not exist."); } @@ -2877,7 +2883,7 @@ namespace MediaBrowser.Server.Implementations.Library throw new ArgumentNullException("path"); } - if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !_fileSystem.DirectoryExists(pathInfo.NetworkPath)) + if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath)) { throw new DirectoryNotFoundException("The network path does not exist."); } diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index 2e3edf3e9..0d043669a 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -69,11 +69,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } private const int BufferSize = 81920; - public static async Task CopyUntilCancelled(Stream source, Stream target, CancellationToken cancellationToken) + public static Task CopyUntilCancelled(Stream source, Stream target, CancellationToken cancellationToken) + { + return CopyUntilCancelled(source, target, null, cancellationToken); + } + public static async Task CopyUntilCancelled(Stream source, Stream target, Action onStarted, CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { - var bytesRead = await CopyToAsyncInternal(source, target, BufferSize, cancellationToken).ConfigureAwait(false); + 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); @@ -85,7 +91,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - private static async Task<int> CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, CancellationToken cancellationToken) + private static async Task<int> CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, Action onStarted, CancellationToken cancellationToken) { byte[] buffer = new byte[bufferSize]; int bytesRead; @@ -96,6 +102,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); totalBytesRead += bytesRead; + + if (onStarted != null) + { + onStarted(); + } + onStarted = null; } return totalBytesRead; diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index e358f9d25..6585e92be 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -746,33 +746,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV throw new NotImplementedException(); } + private readonly SemaphoreSlim _liveStreamsSemaphore = new SemaphoreSlim(1, 1); + private readonly Dictionary<string, LiveStream> _liveStreams = new Dictionary<string, LiveStream>(); + public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) { - _logger.Info("Streaming Channel " + channelId); + var result = await GetChannelStreamInternal(channelId, streamId, cancellationToken).ConfigureAwait(false); - foreach (var hostInstance in _liveTvManager.TunerHosts) - { - try - { - var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); - - result.Item2.Release(); - - return result.Item1; - } - catch (FileNotFoundException) - { - } - catch (Exception e) - { - _logger.ErrorException("Error getting channel stream", e); - } - } - - throw new ApplicationException("Tuner not found."); + return result.Item1.PublicMediaSource; } - private async Task<Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken) + private async Task<Tuple<LiveStream, ITunerHost>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken) { _logger.Info("Streaming Channel " + channelId); @@ -782,7 +766,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); - return new Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>(result.Item1, hostInstance, result.Item2); + await _liveStreamsSemaphore.WaitAsync().ConfigureAwait(false); + _liveStreams[result.Id] = result; + _liveStreamsSemaphore.Release(); + + return new Tuple<LiveStream, ITunerHost>(result, hostInstance); } catch (FileNotFoundException) { @@ -823,9 +811,31 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV throw new NotImplementedException(); } - public Task CloseLiveStream(string id, CancellationToken cancellationToken) + public async Task CloseLiveStream(string id, CancellationToken cancellationToken) { - return Task.FromResult(0); + await _liveStreamsSemaphore.WaitAsync().ConfigureAwait(false); + + try + { + LiveStream stream; + if (_liveStreams.TryGetValue(id, out stream)) + { + _liveStreams.Remove(id); + + try + { + await stream.Close().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error closing live stream", ex); + } + } + } + finally + { + _liveStreamsSemaphore.Release(); + } } public Task RecordLiveStream(string id, CancellationToken cancellationToken) @@ -999,15 +1009,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV string seriesPath = null; var recordPath = GetRecordingPath(timer, out seriesPath); var recordingStatus = RecordingStatus.New; - var isResourceOpen = false; - SemaphoreSlim semaphore = null; + + LiveStream liveStream = null; try { - var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false); - isResourceOpen = true; - semaphore = result.Item3; - var mediaStreamInfo = result.Item1; + var allMediaSources = await GetChannelStreamMediaSources(timer.ChannelId, CancellationToken.None).ConfigureAwait(false); + + var liveStreamInfo = await GetChannelStreamInternal(timer.ChannelId, allMediaSources[0].Id, CancellationToken.None).ConfigureAwait(false); + liveStream = liveStreamInfo.Item1; + var mediaStreamInfo = liveStreamInfo.Item1.PublicMediaSource; + var tunerHost = liveStreamInfo.Item2; // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg //await Task.Delay(3000, cancellationToken).ConfigureAwait(false); @@ -1034,13 +1046,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV timer.Status = RecordingStatus.InProgress; _timerProvider.AddOrUpdate(timer, false); - result.Item3.Release(); - isResourceOpen = false; - SaveNfo(timer, recordPath, seriesPath); }; - var pathWithDuration = result.Item2.ApplyDuration(mediaStreamInfo.Path, duration); + var pathWithDuration = tunerHost.ApplyDuration(mediaStreamInfo.Path, duration); // If it supports supplying duration via url if (!string.Equals(pathWithDuration, mediaStreamInfo.Path, StringComparison.OrdinalIgnoreCase)) @@ -1064,19 +1073,24 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _logger.ErrorException("Error recording to {0}", ex, recordPath); recordingStatus = RecordingStatus.Error; } - finally + + if (liveStream != null) { - if (isResourceOpen && semaphore != null) + try + { + await CloseLiveStream(liveStream.Id, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) { - semaphore.Release(); + _logger.ErrorException("Error closing live stream", ex); } + } - _libraryManager.UnRegisterIgnoredPath(recordPath); - _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true); + _libraryManager.UnRegisterIgnoredPath(recordPath); + _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true); - ActiveRecordingInfo removed; - _activeRecordings.TryRemove(timer.Id, out removed); - } + ActiveRecordingInfo removed; + _activeRecordings.TryRemove(timer.Id, out removed); if (recordingStatus == RecordingStatus.Completed) { diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 4e7f637b1..f74a76e3f 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -68,18 +68,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { - if (mediaSource.Path.IndexOf("m3u8", StringComparison.OrdinalIgnoreCase) != -1) - { - await RecordWithoutTempFile(mediaSource, targetFile, duration, onStarted, cancellationToken) - .ConfigureAwait(false); - - return; - } + var durationToken = new CancellationTokenSource(duration); + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - var tempfile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts"); + await RecordFromFile(mediaSource, mediaSource.Path, targetFile, false, duration, onStarted, cancellationToken).ConfigureAwait(false); - await RecordWithTempFile(mediaSource, tempfile, targetFile, duration, onStarted, cancellationToken) - .ConfigureAwait(false); + _logger.Info("Recording completed to file {0}", targetFile); } private async void DeleteTempFile(string path) @@ -108,76 +102,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - private async Task RecordWithoutTempFile(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) - { - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - - await RecordFromFile(mediaSource, mediaSource.Path, targetFile, false, duration, onStarted, cancellationToken).ConfigureAwait(false); - - _logger.Info("Recording completed to file {0}", targetFile); - } - - private async Task RecordWithTempFile(MediaSourceInfo mediaSource, string tempFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) - { - var httpRequestOptions = new HttpRequestOptions() - { - Url = mediaSource.Path - }; - - httpRequestOptions.BufferContent = false; - - using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false)) - { - _logger.Info("Opened recording stream from tuner provider"); - - Directory.CreateDirectory(Path.GetDirectoryName(tempFile)); - - using (var output = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - //onStarted(); - - _logger.Info("Copying recording stream to file {0}", tempFile); - - var bufferMs = 5000; - - if (mediaSource.RunTimeTicks.HasValue) - { - // The media source already has a fixed duration - // But add another stop 1 minute later just in case the recording gets stuck for any reason - var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMinutes(1))); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - } - else - { - // The media source if infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMilliseconds(bufferMs))); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - } - - var tempFileTask = DirectRecorder.CopyUntilCancelled(response.Content, output, cancellationToken); - - // Give the temp file a little time to build up - await Task.Delay(bufferMs, cancellationToken).ConfigureAwait(false); - - var recordTask = Task.Run(() => RecordFromFile(mediaSource, tempFile, targetFile, true, duration, onStarted, cancellationToken), CancellationToken.None); - - try - { - await tempFileTask.ConfigureAwait(false); - } - catch (OperationCanceledException) - { - - } - - await recordTask.ConfigureAwait(false); - } - } - - _logger.Info("Recording completed to file {0}", targetFile); - } - private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, bool deleteInputFileAfterCompletion, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { _targetPath = targetFile; diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 3f6bb140b..6beea352a 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -10,6 +10,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Serialization; @@ -18,7 +19,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { public abstract class BaseTunerHost { - protected readonly IConfigurationManager Config; + protected readonly IServerConfigurationManager Config; protected readonly ILogger Logger; protected IJsonSerializer JsonSerializer; protected readonly IMediaEncoder MediaEncoder; @@ -26,7 +27,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts private readonly ConcurrentDictionary<string, ChannelCache> _channelCache = new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase); - protected BaseTunerHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder) + protected BaseTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder) { Config = config; Logger = logger; @@ -125,12 +126,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts foreach (var host in hostsWithChannel) { - var resourcePool = GetLock(host.Url); - Logger.Debug("GetChannelStreamMediaSources - Waiting on tuner resource pool"); - - await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - Logger.Debug("GetChannelStreamMediaSources - Unlocked resource pool"); - try { // Check to make sure the tuner is available @@ -156,93 +151,63 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { Logger.Error("Error opening tuner", ex); } - finally - { - resourcePool.Release(); - } } } return new List<MediaSourceInfo>(); } - protected abstract Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken); + protected abstract Task<LiveStream> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken); - public async Task<Tuple<MediaSourceInfo, SemaphoreSlim>> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) + public async Task<LiveStream> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) { - if (IsValidChannelId(channelId)) + if (!IsValidChannelId(channelId)) { - var hosts = GetTunerHosts(); - - var hostsWithChannel = new List<TunerHostInfo>(); + throw new FileNotFoundException(); + } - foreach (var host in hosts) - { - if (string.IsNullOrWhiteSpace(streamId)) - { - try - { - var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false); + var hosts = GetTunerHosts(); - if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase))) - { - hostsWithChannel.Add(host); - } - } - catch (Exception ex) - { - Logger.Error("Error getting channels", ex); - } - } - else if (streamId.StartsWith(host.Id, StringComparison.OrdinalIgnoreCase)) - { - hostsWithChannel = new List<TunerHostInfo> {host}; - streamId = streamId.Substring(host.Id.Length); - break; - } - } + var hostsWithChannel = new List<TunerHostInfo>(); - foreach (var host in hostsWithChannel) + foreach (var host in hosts) + { + if (string.IsNullOrWhiteSpace(streamId)) { - var resourcePool = GetLock(host.Url); - Logger.Debug("GetChannelStream - Waiting on tuner resource pool"); - await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - Logger.Debug("GetChannelStream - Unlocked resource pool"); try { - // Check to make sure the tuner is available - // If there's only one tuner, don't bother with the check and just let the tuner be the one to throw an error - // If a streamId is specified then availibility has already been checked in GetChannelStreamMediaSources - if (string.IsNullOrWhiteSpace(streamId) && hostsWithChannel.Count > 1) - { - if (!await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false)) - { - Logger.Error("Tuner is not currently available"); - resourcePool.Release(); - continue; - } - } - - var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false); + var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false); - if (EnableMediaProbing) + if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase))) { - await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false); + hostsWithChannel.Add(host); } - - return new Tuple<MediaSourceInfo, SemaphoreSlim>(stream, resourcePool); } catch (Exception ex) { - Logger.Error("Error opening tuner", ex); - - resourcePool.Release(); + Logger.Error("Error getting channels", ex); } } + else if (streamId.StartsWith(host.Id, StringComparison.OrdinalIgnoreCase)) + { + hostsWithChannel = new List<TunerHostInfo> { host }; + streamId = streamId.Substring(host.Id.Length); + break; + } } - else + + foreach (var host in hostsWithChannel) { - throw new FileNotFoundException(); + try + { + var liveStream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false); + await liveStream.Open(cancellationToken).ConfigureAwait(false); + return liveStream; + } + catch (Exception ex) + { + Logger.Error("Error opening tuner", ex); + } } throw new LiveTvConflictException(); @@ -268,37 +233,23 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts protected abstract Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken); - /// <summary> - /// The _semaphoreLocks - /// </summary> - private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = new ConcurrentDictionary<string, SemaphoreSlim>(StringComparer.OrdinalIgnoreCase); - /// <summary> - /// Gets the lock. - /// </summary> - /// <param name="url">The filename.</param> - /// <returns>System.Object.</returns> - 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) + private async Task AddMediaInfo(LiveStream stream, bool isAudio, CancellationToken cancellationToken) { - await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + //await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - await AddMediaInfoInternal(mediaSource, isAudio, 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(); + // // Leave the resource locked. it will be released upstream + //} + //catch (Exception) + //{ + // // Release the resource if there's some kind of failure. + // resourcePool.Release(); - throw; - } + // throw; + //} } private async Task AddMediaInfoInternal(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken) diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index c5bd648cf..b40b74436 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -14,7 +14,10 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Net; @@ -24,11 +27,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IServerApplicationHost _appHost; - public HdHomerunHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient) + public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost) : base(config, logger, jsonSerializer, mediaEncoder) { _httpClient = httpClient; + _fileSystem = fileSystem; + _appHost = appHost; } public string Name @@ -355,6 +362,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun url += "?transcode=" + profile; } + var id = profile; + if (string.IsNullOrWhiteSpace(id)) + { + id = "native"; + } + id += "_" + url.GetMD5().ToString("N"); + var mediaSource = new MediaSourceInfo { Path = url, @@ -387,9 +401,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun RequiresClosing = false, BufferMs = 0, Container = "ts", - Id = profile, - SupportsDirectPlay = true, - SupportsDirectStream = false, + Id = id, + SupportsDirectPlay = false, + SupportsDirectStream = true, SupportsTranscoding = true }; @@ -452,9 +466,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase); } - protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) + protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) { - Logger.Info("GetChannelStream: channel id: {0}. stream id: {1}", channelId, streamId ?? string.Empty); + var profile = streamId.Split('_')[0]; + + Logger.Info("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelId, streamId, profile); if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase)) { @@ -462,7 +478,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun } var hdhrId = GetHdHrIdFromChannelId(channelId); - return await GetMediaSource(info, hdhrId, streamId).ConfigureAwait(false); + var mediaSource = await GetMediaSource(info, hdhrId, profile).ConfigureAwait(false); + + var liveStream = new HdHomerunLiveStream(mediaSource, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost); + return liveStream; } public async Task Validate(TunerHostInfo info) diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs new file mode 100644 index 000000000..6078c4a70 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs @@ -0,0 +1,156 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Server.Implementations.LiveTv.EmbyTV; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun +{ + public class HdHomerunLiveStream : LiveStream + { + private readonly ILogger _logger; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IServerApplicationPaths _appPaths; + private readonly IServerApplicationHost _appHost; + + private readonly CancellationTokenSource _liveStreamCancellationTokenSource = new CancellationTokenSource(); + + public HdHomerunLiveStream(MediaSourceInfo mediaSource, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost) + : base(mediaSource) + { + _fileSystem = fileSystem; + _httpClient = httpClient; + _logger = logger; + _appPaths = appPaths; + _appHost = appHost; + } + + public override async Task Open(CancellationToken openCancellationToken) + { + _liveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested(); + + var mediaSource = OriginalMediaSource; + + var url = mediaSource.Path; + var tempFile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts"); + Directory.CreateDirectory(Path.GetDirectoryName(tempFile)); + + _logger.Info("Opening HDHR Live stream from {0} to {1}", url, tempFile); + + var output = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read); + + var taskCompletionSource = new TaskCompletionSource<bool>(); + + StartStreamingToTempFile(output, tempFile, url, taskCompletionSource, _liveStreamCancellationTokenSource.Token); + + await taskCompletionSource.Task.ConfigureAwait(false); + + PublicMediaSource.Path = _appHost.GetLocalApiUrl("localhost") + "/LiveTv/LiveStreamFiles/" + Path.GetFileNameWithoutExtension(tempFile) + "/stream.ts"; + + PublicMediaSource.Protocol = MediaProtocol.Http; + } + + public override Task Close() + { + _liveStreamCancellationTokenSource.Cancel(); + + return base.Close(); + } + + private async Task StartStreamingToTempFile(Stream outputStream, string tempFilePath, string url, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) + { + await Task.Run(async () => + { + using (outputStream) + { + var isFirstAttempt = true; + + while (!cancellationToken.IsCancellationRequested) + { + try + { + using (var response = await _httpClient.SendAsync(new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + BufferContent = false + + }, "GET").ConfigureAwait(false)) + { + _logger.Info("Opened HDHR stream from {0}", url); + + if (!cancellationToken.IsCancellationRequested) + { + _logger.Info("Beginning DirectRecorder.CopyUntilCancelled"); + + Action onStarted = null; + if (isFirstAttempt) + { + onStarted = () => openTaskCompletionSource.TrySetResult(true); + } + await DirectRecorder.CopyUntilCancelled(response.Content, outputStream, onStarted, cancellationToken).ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + if (isFirstAttempt) + { + _logger.ErrorException("Error opening live stream:", ex); + openTaskCompletionSource.TrySetException(ex); + break; + } + + _logger.ErrorException("Error copying live stream, will reopen", ex); + } + + isFirstAttempt = false; + } + } + + await Task.Delay(5000).ConfigureAwait(false); + + DeleteTempFile(tempFilePath); + + }).ConfigureAwait(false); + } + + private async void DeleteTempFile(string path) + { + for (var i = 0; i < 10; i++) + { + try + { + File.Delete(path); + return; + } + catch (FileNotFoundException) + { + return; + } + catch (DirectoryNotFoundException) + { + return; + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting temp file {0}", ex, path); + } + + await Task.Delay(1000).ConfigureAwait(false); + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 5c508aacd..d9c0bb8bf 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -13,8 +13,10 @@ using System.Threading; using System.Threading.Tasks; using CommonIO; using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Serialization; +using MediaBrowser.Server.Implementations.LiveTv.EmbyTV; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { @@ -23,7 +25,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts private readonly IFileSystem _fileSystem; private readonly IHttpClient _httpClient; - public M3UTunerHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) + public M3UTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) : base(config, logger, jsonSerializer, mediaEncoder) { _fileSystem = fileSystem; @@ -63,11 +65,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts return Task.FromResult(list); } - protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) + protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) { var sources = await GetChannelStreamMediaSources(info, channelId, cancellationToken).ConfigureAwait(false); - return sources.First(); + var liveStream = new LiveStream(sources.First()); + return liveStream; } public async Task Validate(TunerHostInfo info) @@ -136,7 +139,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts RequiresOpening = false, RequiresClosing = false, - ReadAtNativeFramerate = false + ReadAtNativeFramerate = false, + + Id = channel.Path.GetMD5().ToString("N") }; return new List<MediaSourceInfo> { mediaSource }; diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs index b1e349a86..81deb2995 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs @@ -8,6 +8,7 @@ using CommonIO; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dto; @@ -16,6 +17,7 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Logging; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; +using MediaBrowser.Server.Implementations.LiveTv.EmbyTV; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp { @@ -24,7 +26,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp private readonly IFileSystem _fileSystem; private readonly IHttpClient _httpClient; - public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) + public SatIpHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) : base(config, logger, jsonSerializer, mediaEncoder) { _fileSystem = fileSystem; @@ -113,11 +115,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp return new List<MediaSourceInfo>(); } - protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken) + protected override async Task<LiveStream> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken) { var sources = await GetChannelStreamMediaSources(tuner, channelId, cancellationToken).ConfigureAwait(false); - return sources.First(); + var liveStream = new LiveStream(sources.First()); + + return liveStream; } protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index eb3da1a12..12691a69b 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -241,6 +241,7 @@ <Compile Include="LiveTv\TunerHosts\BaseTunerHost.cs" /> <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHost.cs" /> <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunDiscovery.cs" /> + <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunLiveStream.cs" /> <Compile Include="LiveTv\TunerHosts\M3uParser.cs" /> <Compile Include="LiveTv\TunerHosts\M3UTunerHost.cs" /> <Compile Include="LiveTv\ProgramImageProvider.cs" /> |
