aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Server.Implementations/LiveTv
diff options
context:
space:
mode:
authorLuke <luke.pulverenti@gmail.com>2016-10-12 15:43:28 -0400
committerGitHub <noreply@github.com>2016-10-12 15:43:28 -0400
commit25ef9777cafee83c46ff53ede2caa04e3295e98a (patch)
tree5f1e6045d0b4d4d5b7d8dcaadf035b326f36e672 /MediaBrowser.Server.Implementations/LiveTv
parent0677d4ec990aee9a3bf7bda39dda01eb6fa66281 (diff)
parent5be6cf05e34459a046aceaa16c891f3034859476 (diff)
Merge pull request #2224 from MediaBrowser/beta
Beta
Diffstat (limited to 'MediaBrowser.Server.Implementations/LiveTv')
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs65
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs977
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs113
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs47
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs72
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs19
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveStreamHelper.cs110
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs38
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs614
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs30
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs221
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs12
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs127
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs145
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs19
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs96
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs93
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs11
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs10
19 files changed, 1972 insertions, 847 deletions
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index b21aa904b..0f8c15e71 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -47,25 +47,60 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_logger.Info("Copying recording stream to file {0}", targetFile);
- 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);
- cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
- }
-
- await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
+ // The media source if infinite so we need to handle stopping ourselves
+ var durationToken = new CancellationTokenSource(duration);
+ cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+
+ await CopyUntilCancelled(response.Content, output, cancellationToken).ConfigureAwait(false);
}
}
_logger.Info("Recording completed to file {0}", targetFile);
}
+
+ private const int BufferSize = 81920;
+ 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, onStarted, cancellationToken).ConfigureAwait(false);
+
+ onStarted = null;
+
+ //var position = fs.Position;
+ //_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
+
+ if (bytesRead == 0)
+ {
+ await Task.Delay(100).ConfigureAwait(false);
+ }
+ }
+ }
+
+ private static async Task<int> CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, Action onStarted, CancellationToken cancellationToken)
+ {
+ byte[] buffer = new byte[bufferSize];
+ int bytesRead;
+ int totalBytesRead = 0;
+
+ while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
+ {
+ await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
+
+ 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 8fa34109d..136a9e195 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -22,20 +22,25 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using System.Xml;
using CommonIO;
+using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Power;
using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.FileOrganization;
using Microsoft.Win32;
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
- public class EmbyTV : ILiveTvService, ISupportsNewTimerIds, IDisposable
+ public class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable
{
- private readonly IApplicationHost _appHpst;
+ private readonly IServerApplicationHost _appHost;
private readonly ILogger _logger;
private readonly IHttpClient _httpClient;
private readonly IServerConfigurationManager _config;
@@ -55,17 +60,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
public static EmbyTV Current;
- public event EventHandler DataSourceChanged { add { } remove { } }
- public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged { add { } remove { } }
+ public event EventHandler DataSourceChanged;
+ public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged;
private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
- public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder, IPowerManagement powerManagement)
+ public EmbyTV(IServerApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder)
{
Current = this;
- _appHpst = appHost;
+ _appHost = appHost;
_logger = logger;
_httpClient = httpClient;
_config = config;
@@ -79,7 +84,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_jsonSerializer = jsonSerializer;
_seriesTimerProvider = new SeriesTimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
- _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), powerManagement, _logger);
+ _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), _logger);
_timerProvider.TimerFired += _timerProvider_TimerFired;
_config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
@@ -140,9 +145,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
continue;
}
+ var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo { Path = i }).ToArray();
+
+ var libraryOptions = new LibraryOptions
+ {
+ PathInfos = mediaPathInfos
+ };
try
{
- _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, pathsToCreate.ToArray(), new LibraryOptions(), true);
+ _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, libraryOptions, true);
}
catch (Exception ex)
{
@@ -284,7 +295,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
status.Tuners = list;
status.Status = LiveTvServiceStatus.Ok;
- status.Version = _appHpst.ApplicationVersion.ToString();
+ status.Version = _appHost.ApplicationVersion.ToString();
status.IsVisible = false;
return status;
}
@@ -321,27 +332,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
if (DateTime.UtcNow > timer.EndDate && !_activeRecordings.ContainsKey(timer.Id))
{
- _timerProvider.Delete(timer);
+ OnTimerOutOfDate(timer);
}
}
}
- private List<ChannelInfo> _channelCache = null;
- private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken)
+ private void OnTimerOutOfDate(TimerInfo timer)
{
- if (enableCache && _channelCache != null)
- {
-
- return _channelCache.ToList();
- }
+ _timerProvider.Delete(timer);
+ }
+ private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken)
+ {
var list = new List<ChannelInfo>();
foreach (var hostInstance in _liveTvManager.TunerHosts)
{
try
{
- var channels = await hostInstance.GetChannels(cancellationToken).ConfigureAwait(false);
+ var channels = await hostInstance.GetChannels(enableCache, cancellationToken).ConfigureAwait(false);
list.AddRange(channels);
}
@@ -374,7 +383,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
- _channelCache = list.ToList();
return list;
}
@@ -386,7 +394,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
try
{
- var channels = await hostInstance.GetChannels(cancellationToken).ConfigureAwait(false);
+ var channels = await hostInstance.GetChannels(false, cancellationToken).ConfigureAwait(false);
list.AddRange(channels);
}
@@ -415,7 +423,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
foreach (var timer in timers)
{
- CancelTimerInternal(timer.Id);
+ CancelTimerInternal(timer.Id, true);
}
var remove = _seriesTimerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
@@ -426,12 +434,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return Task.FromResult(true);
}
- private void CancelTimerInternal(string timerId)
+ private void CancelTimerInternal(string timerId, bool isSeriesCancelled)
{
- var remove = _timerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
- if (remove != null)
+ var timer = _timerProvider.GetTimer(timerId);
+ if (timer != null)
{
- _timerProvider.Delete(remove);
+ if (string.IsNullOrWhiteSpace(timer.SeriesTimerId) || isSeriesCancelled)
+ {
+ _timerProvider.Delete(timer);
+ }
+ else
+ {
+ timer.Status = RecordingStatus.Cancelled;
+ _timerProvider.AddOrUpdate(timer, false);
+ }
}
ActiveRecordingInfo activeRecordingInfo;
@@ -443,7 +459,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken)
{
- CancelTimerInternal(timerId);
+ CancelTimerInternal(timerId, false);
return Task.FromResult(true);
}
@@ -452,21 +468,57 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return Task.FromResult(true);
}
- public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
+ public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
{
- return CreateTimer(info, cancellationToken);
+ throw new NotImplementedException();
}
- public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
+ public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
{
- return CreateSeriesTimer(info, cancellationToken);
+ throw new NotImplementedException();
}
- public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken)
+ public Task<string> CreateTimer(TimerInfo timer, CancellationToken cancellationToken)
{
- info.Id = Guid.NewGuid().ToString("N");
- _timerProvider.Add(info);
- return Task.FromResult(info.Id);
+ var existingTimer = _timerProvider.GetAll()
+ .FirstOrDefault(i => string.Equals(timer.ProgramId, i.ProgramId, StringComparison.OrdinalIgnoreCase));
+
+ if (existingTimer != null)
+ {
+ if (existingTimer.Status == RecordingStatus.Cancelled ||
+ existingTimer.Status == RecordingStatus.Completed)
+ {
+ existingTimer.Status = RecordingStatus.New;
+ _timerProvider.Update(existingTimer);
+ return Task.FromResult(existingTimer.Id);
+ }
+ else
+ {
+ throw new ArgumentException("A scheduled recording already exists for this program.");
+ }
+ }
+
+ timer.Id = Guid.NewGuid().ToString("N");
+
+ ProgramInfo programInfo = null;
+
+ if (!string.IsNullOrWhiteSpace(timer.ProgramId))
+ {
+ programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
+ }
+ if (programInfo == null)
+ {
+ _logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
+ programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
+ }
+
+ if (programInfo != null)
+ {
+ RecordingHelper.CopyProgramInfoToTimerInfo(programInfo, timer);
+ }
+
+ _timerProvider.Add(timer);
+ return Task.FromResult(timer.Id);
}
public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken)
@@ -520,6 +572,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
instance.RecordAnyChannel = info.RecordAnyChannel;
instance.RecordAnyTime = info.RecordAnyTime;
instance.RecordNewOnly = info.RecordNewOnly;
+ instance.SkipEpisodesInLibrary = info.SkipEpisodesInLibrary;
+ instance.KeepUpTo = info.KeepUpTo;
+ instance.KeepUntil = info.KeepUntil;
instance.StartDate = info.StartDate;
_seriesTimerProvider.Update(instance);
@@ -540,12 +595,55 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
- public Task UpdateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
+ public Task UpdateTimerAsync(TimerInfo updatedTimer, CancellationToken cancellationToken)
{
- _timerProvider.Update(info);
+ var existingTimer = _timerProvider.GetTimer(updatedTimer.Id);
+
+ if (existingTimer == null)
+ {
+ throw new ResourceNotFoundException();
+ }
+
+ // Only update if not currently active
+ ActiveRecordingInfo activeRecordingInfo;
+ if (!_activeRecordings.TryGetValue(updatedTimer.Id, out activeRecordingInfo))
+ {
+ existingTimer.PrePaddingSeconds = updatedTimer.PrePaddingSeconds;
+ existingTimer.PostPaddingSeconds = updatedTimer.PostPaddingSeconds;
+ existingTimer.IsPostPaddingRequired = updatedTimer.IsPostPaddingRequired;
+ existingTimer.IsPrePaddingRequired = updatedTimer.IsPrePaddingRequired;
+ }
+
return Task.FromResult(true);
}
+ private void UpdateExistingTimerWithNewMetadata(TimerInfo existingTimer, TimerInfo updatedTimer)
+ {
+ // Update the program info but retain the status
+ existingTimer.ChannelId = updatedTimer.ChannelId;
+ existingTimer.CommunityRating = updatedTimer.CommunityRating;
+ existingTimer.EndDate = updatedTimer.EndDate;
+ existingTimer.EpisodeNumber = updatedTimer.EpisodeNumber;
+ existingTimer.EpisodeTitle = updatedTimer.EpisodeTitle;
+ existingTimer.Genres = updatedTimer.Genres;
+ existingTimer.HomePageUrl = updatedTimer.HomePageUrl;
+ existingTimer.IsKids = updatedTimer.IsKids;
+ existingTimer.IsNews = updatedTimer.IsNews;
+ existingTimer.IsMovie = updatedTimer.IsMovie;
+ existingTimer.IsProgramSeries = updatedTimer.IsProgramSeries;
+ existingTimer.IsRepeat = updatedTimer.IsRepeat;
+ existingTimer.IsSports = updatedTimer.IsSports;
+ existingTimer.Name = updatedTimer.Name;
+ existingTimer.OfficialRating = updatedTimer.OfficialRating;
+ existingTimer.OriginalAirDate = updatedTimer.OriginalAirDate;
+ existingTimer.Overview = updatedTimer.Overview;
+ existingTimer.ProductionYear = updatedTimer.ProductionYear;
+ existingTimer.ProgramId = updatedTimer.ProgramId;
+ existingTimer.SeasonNumber = updatedTimer.SeasonNumber;
+ existingTimer.ShortOverview = updatedTimer.ShortOverview;
+ existingTimer.StartDate = updatedTimer.StartDate;
+ }
+
public Task<ImageStream> GetChannelImageAsync(string channelId, CancellationToken cancellationToken)
{
throw new NotImplementedException();
@@ -563,12 +661,76 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
public async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken)
{
- return new List<RecordingInfo>();
+ return _activeRecordings.Values.ToList().Select(GetRecordingInfo).ToList();
+ }
+
+ public string GetActiveRecordingPath(string id)
+ {
+ ActiveRecordingInfo info;
+
+ if (_activeRecordings.TryGetValue(id, out info))
+ {
+ return info.Path;
+ }
+ return null;
+ }
+
+ private RecordingInfo GetRecordingInfo(ActiveRecordingInfo info)
+ {
+ var timer = info.Timer;
+ var program = info.Program;
+
+ var result = new RecordingInfo
+ {
+ ChannelId = timer.ChannelId,
+ CommunityRating = timer.CommunityRating,
+ DateLastUpdated = DateTime.UtcNow,
+ EndDate = timer.EndDate,
+ EpisodeTitle = timer.EpisodeTitle,
+ Genres = timer.Genres,
+ Id = "recording" + timer.Id,
+ IsKids = timer.IsKids,
+ IsMovie = timer.IsMovie,
+ IsNews = timer.IsNews,
+ IsRepeat = timer.IsRepeat,
+ IsSeries = timer.IsProgramSeries,
+ IsSports = timer.IsSports,
+ Name = timer.Name,
+ OfficialRating = timer.OfficialRating,
+ OriginalAirDate = timer.OriginalAirDate,
+ Overview = timer.Overview,
+ ProgramId = timer.ProgramId,
+ SeriesTimerId = timer.SeriesTimerId,
+ StartDate = timer.StartDate,
+ Status = RecordingStatus.InProgress,
+ TimerId = timer.Id
+ };
+
+ if (program != null)
+ {
+ result.Audio = program.Audio;
+ result.ImagePath = program.ImagePath;
+ result.ImageUrl = program.ImageUrl;
+ result.IsHD = program.IsHD;
+ result.IsLive = program.IsLive;
+ result.IsPremiere = program.IsPremiere;
+ result.ShowId = program.ShowId;
+ }
+
+ return result;
}
public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
{
- return Task.FromResult((IEnumerable<TimerInfo>)_timerProvider.GetAll());
+ var excludeStatues = new List<RecordingStatus>
+ {
+ RecordingStatus.Completed
+ };
+
+ var timers = _timerProvider.GetAll()
+ .Where(i => !excludeStatues.Contains(i.Status));
+
+ return Task.FromResult(timers);
}
public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null)
@@ -581,7 +743,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0),
RecordAnyChannel = true,
RecordAnyTime = true,
- RecordNewOnly = false,
+ RecordNewOnly = true,
Days = new List<DayOfWeek>
{
@@ -601,6 +763,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
defaults.ProgramId = program.Id;
}
+ defaults.SkipEpisodesInLibrary = true;
+ defaults.KeepUntil = KeepUntil.UntilDeleted;
+
return Task.FromResult(defaults);
}
@@ -717,46 +882,108 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
throw new NotImplementedException();
}
+ private readonly SemaphoreSlim _liveStreamsSemaphore = new SemaphoreSlim(1, 1);
+ private readonly List<LiveStream> _liveStreams = new List<LiveStream>();
+
public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
{
- _logger.Info("Streaming Channel " + channelId);
+ var result = await GetChannelStreamWithDirectStreamProvider(channelId, streamId, cancellationToken).ConfigureAwait(false);
- foreach (var hostInstance in _liveTvManager.TunerHosts)
- {
- try
- {
- var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
+ return result.Item1;
+ }
- result.Item2.Release();
+ public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, CancellationToken cancellationToken)
+ {
+ var result = await GetChannelStreamInternal(channelId, streamId, cancellationToken).ConfigureAwait(false);
- return result.Item1;
- }
- catch (Exception e)
- {
- _logger.ErrorException("Error getting channel stream", e);
- }
+ return new Tuple<MediaSourceInfo, IDirectStreamProvider>(result.Item2, result.Item1 as IDirectStreamProvider);
+ }
+
+ private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing)
+ {
+ var json = _jsonSerializer.SerializeToString(mediaSource);
+ mediaSource = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
+
+ mediaSource.Id = Guid.NewGuid().ToString("N") + "_" + mediaSource.Id;
+
+ //if (mediaSource.DateLiveStreamOpened.HasValue && enableStreamSharing)
+ //{
+ // var ticks = (DateTime.UtcNow - mediaSource.DateLiveStreamOpened.Value).Ticks - TimeSpan.FromSeconds(10).Ticks;
+ // ticks = Math.Max(0, ticks);
+ // mediaSource.Path += "?t=" + ticks.ToString(CultureInfo.InvariantCulture) + "&s=" + mediaSource.DateLiveStreamOpened.Value.Ticks.ToString(CultureInfo.InvariantCulture);
+ //}
+
+ return mediaSource;
+ }
+
+ public async Task<LiveStream> GetLiveStream(string uniqueId)
+ {
+ await _liveStreamsSemaphore.WaitAsync().ConfigureAwait(false);
+
+ try
+ {
+ return _liveStreams
+ .FirstOrDefault(i => string.Equals(i.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase));
+ }
+ finally
+ {
+ _liveStreamsSemaphore.Release();
}
- throw new ApplicationException("Tuner not found.");
}
- private async Task<Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
+ private async Task<Tuple<LiveStream, MediaSourceInfo, ITunerHost>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
{
_logger.Info("Streaming Channel " + channelId);
- foreach (var hostInstance in _liveTvManager.TunerHosts)
+ await _liveStreamsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ var result = _liveStreams.FirstOrDefault(i => string.Equals(i.OriginalStreamId, streamId, StringComparison.OrdinalIgnoreCase));
+
+ if (result != null && result.EnableStreamSharing)
{
- try
- {
- var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
+ var openedMediaSource = CloneMediaSource(result.OpenedMediaSource, result.EnableStreamSharing);
+ result.SharedStreamIds.Add(openedMediaSource.Id);
+ _liveStreamsSemaphore.Release();
- return new Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>(result.Item1, hostInstance, result.Item2);
- }
- catch (Exception e)
+ _logger.Info("Live stream {0} consumer count is now {1}", streamId, result.ConsumerCount);
+
+ return new Tuple<LiveStream, MediaSourceInfo, ITunerHost>(result, openedMediaSource, result.TunerHost);
+ }
+
+ try
+ {
+ foreach (var hostInstance in _liveTvManager.TunerHosts)
{
- _logger.ErrorException("Error getting channel stream", e);
+ try
+ {
+ result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
+
+ var openedMediaSource = CloneMediaSource(result.OpenedMediaSource, result.EnableStreamSharing);
+
+ result.SharedStreamIds.Add(openedMediaSource.Id);
+ _liveStreams.Add(result);
+
+ result.TunerHost = hostInstance;
+ result.OriginalStreamId = streamId;
+
+ _logger.Info("Returning mediasource streamId {0}, mediaSource.Id {1}, mediaSource.LiveStreamId {2}",
+ streamId, openedMediaSource.Id, openedMediaSource.LiveStreamId);
+
+ return new Tuple<LiveStream, MediaSourceInfo, ITunerHost>(result, openedMediaSource, hostInstance);
+ }
+ catch (FileNotFoundException)
+ {
+ }
+ catch (OperationCanceledException)
+ {
+ }
}
}
+ finally
+ {
+ _liveStreamsSemaphore.Release();
+ }
throw new ApplicationException("Tuner not found.");
}
@@ -783,14 +1010,82 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
throw new NotImplementedException();
}
- public Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(string recordingId, CancellationToken cancellationToken)
+ public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(string recordingId, CancellationToken cancellationToken)
{
- throw new NotImplementedException();
+ ActiveRecordingInfo info;
+
+ recordingId = recordingId.Replace("recording", string.Empty);
+
+ if (_activeRecordings.TryGetValue(recordingId, out info))
+ {
+ var stream = new MediaSourceInfo
+ {
+ Path = _appHost.GetLocalApiUrl("localhost") + "/LiveTv/LiveRecordings/" + recordingId + "/stream",
+ Id = recordingId,
+ SupportsDirectPlay = false,
+ SupportsDirectStream = true,
+ SupportsTranscoding = true,
+ IsInfiniteStream = true,
+ RequiresOpening = false,
+ RequiresClosing = false,
+ Protocol = Model.MediaInfo.MediaProtocol.Http,
+ BufferMs = 0
+ };
+
+ var isAudio = false;
+ await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false);
+
+ return new List<MediaSourceInfo>
+ {
+ stream
+ };
+ }
+
+ throw new FileNotFoundException();
}
- public Task CloseLiveStream(string id, CancellationToken cancellationToken)
+ public async Task CloseLiveStream(string id, CancellationToken cancellationToken)
{
- return Task.FromResult(0);
+ // Ignore the consumer id
+ //id = id.Substring(id.IndexOf('_') + 1);
+
+ await _liveStreamsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ var stream = _liveStreams.FirstOrDefault(i => i.SharedStreamIds.Contains(id));
+ if (stream != null)
+ {
+ stream.SharedStreamIds.Remove(id);
+
+ _logger.Info("Live stream {0} consumer count is now {1}", id, stream.ConsumerCount);
+
+ if (stream.ConsumerCount <= 0)
+ {
+ _liveStreams.Remove(stream);
+
+ _logger.Info("Closing live stream {0}", id);
+
+ await stream.Close().ConfigureAwait(false);
+ _logger.Info("Live stream {0} closed successfully", id);
+ }
+ }
+ else
+ {
+ _logger.Warn("Live stream not found: {0}, unable to close", id);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error closing live stream", ex);
+ }
+ finally
+ {
+ _liveStreamsSemaphore.Release();
+ }
}
public Task RecordLiveStream(string id, CancellationToken cancellationToken)
@@ -815,14 +1110,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
if (recordingEndDate <= DateTime.UtcNow)
{
- _logger.Warn("Recording timer fired for timer {0}, Id: {1}, but the program has already ended.", timer.Name, timer.Id);
+ _logger.Warn("Recording timer fired for updatedTimer {0}, Id: {1}, but the program has already ended.", timer.Name, timer.Id);
+ OnTimerOutOfDate(timer);
return;
}
var activeRecordingInfo = new ActiveRecordingInfo
{
CancellationTokenSource = new CancellationTokenSource(),
- TimerId = timer.Id
+ Timer = timer
};
if (_activeRecordings.TryAdd(timer.Id, activeRecordingInfo))
@@ -844,12 +1140,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
- private string GetRecordingPath(TimerInfo timer, ProgramInfo info)
+ private string GetRecordingPath(TimerInfo timer, out string seriesPath)
{
var recordPath = RecordingPath;
var config = GetConfiguration();
+ seriesPath = null;
- if (info.IsSeries)
+ if (timer.IsProgramSeries)
{
var customRecordingPath = config.SeriesRecordingPath;
var allowSubfolder = true;
@@ -864,29 +1161,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
recordPath = Path.Combine(recordPath, "Series");
}
- var folderName = _fileSystem.GetValidFilename(info.Name).Trim();
- var folderNameWithYear = folderName;
- if (info.ProductionYear.HasValue)
- {
- folderNameWithYear += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
- }
+ var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
- if (Directory.Exists(Path.Combine(recordPath, folderName)))
- {
- recordPath = Path.Combine(recordPath, folderName);
- }
- else
- {
- recordPath = Path.Combine(recordPath, folderNameWithYear);
- }
+ // Can't use the year here in the folder name because it is the year of the episode, not the series.
+ recordPath = Path.Combine(recordPath, folderName);
+
+ seriesPath = recordPath;
- if (info.SeasonNumber.HasValue)
+ if (timer.SeasonNumber.HasValue)
{
- folderName = string.Format("Season {0}", info.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
+ folderName = string.Format("Season {0}", timer.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
recordPath = Path.Combine(recordPath, folderName);
}
}
- else if (info.IsMovie)
+ else if (timer.IsMovie)
{
var customRecordingPath = config.MovieRecordingPath;
var allowSubfolder = true;
@@ -901,34 +1189,34 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
recordPath = Path.Combine(recordPath, "Movies");
}
- var folderName = _fileSystem.GetValidFilename(info.Name).Trim();
- if (info.ProductionYear.HasValue)
+ var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
+ if (timer.ProductionYear.HasValue)
{
- folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
+ folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
}
recordPath = Path.Combine(recordPath, folderName);
}
- else if (info.IsKids)
+ else if (timer.IsKids)
{
if (config.EnableRecordingSubfolders)
{
recordPath = Path.Combine(recordPath, "Kids");
}
- var folderName = _fileSystem.GetValidFilename(info.Name).Trim();
- if (info.ProductionYear.HasValue)
+ var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
+ if (timer.ProductionYear.HasValue)
{
- folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
+ folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
}
recordPath = Path.Combine(recordPath, folderName);
}
- else if (info.IsSports)
+ else if (timer.IsSports)
{
if (config.EnableRecordingSubfolders)
{
recordPath = Path.Combine(recordPath, "Sports");
}
- recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim());
+ recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim());
}
else
{
@@ -936,54 +1224,57 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
recordPath = Path.Combine(recordPath, "Other");
}
- recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim());
+ recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim());
}
- var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts";
+ var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer)).Trim() + ".ts";
return Path.Combine(recordPath, recordingFileName);
}
- private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
+ private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate,
+ ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
{
if (timer == null)
{
throw new ArgumentNullException("timer");
}
- ProgramInfo info = null;
+ ProgramInfo programInfo = null;
- if (string.IsNullOrWhiteSpace(timer.ProgramId))
+ if (!string.IsNullOrWhiteSpace(timer.ProgramId))
{
- _logger.Info("Timer {0} has null programId", timer.Id);
+ programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
}
- else
- {
- info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
- }
-
- if (info == null)
+ if (programInfo == null)
{
_logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
- info = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
+ programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
}
- if (info == null)
+ if (programInfo != null)
{
- throw new InvalidOperationException(string.Format("Program with Id {0} not found", timer.ProgramId));
+ RecordingHelper.CopyProgramInfoToTimerInfo(programInfo, timer);
+ activeRecordingInfo.Program = programInfo;
}
- var recordPath = GetRecordingPath(timer, info);
+ string seriesPath = null;
+ var recordPath = GetRecordingPath(timer, out seriesPath);
var recordingStatus = RecordingStatus.New;
- var isResourceOpen = false;
- SemaphoreSlim semaphore = null;
+
+ string liveStreamId = null;
+
+ OnRecordingStatusChanged();
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);
+
+ var mediaStreamInfo = liveStreamInfo.Item2;
+ liveStreamId = mediaStreamInfo.Id;
// HDHR doesn't seem to release the tuner right away after first probing with ffmpeg
//await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
@@ -1000,7 +1291,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
var duration = recordingEndDate - DateTime.UtcNow;
- _logger.Info("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
+ _logger.Info("Beginning recording. Will record for {0} minutes.",
+ duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
_logger.Info("Writing file to path: " + recordPath);
_logger.Info("Opening recording stream from tuner provider");
@@ -1010,20 +1302,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
timer.Status = RecordingStatus.InProgress;
_timerProvider.AddOrUpdate(timer, false);
- result.Item3.Release();
- isResourceOpen = false;
+ SaveNfo(timer, recordPath, seriesPath);
+ EnforceKeepUpTo(timer);
};
- var pathWithDuration = result.Item2.ApplyDuration(mediaStreamInfo.Path, duration);
-
- // If it supports supplying duration via url
- if (!string.Equals(pathWithDuration, mediaStreamInfo.Path, StringComparison.OrdinalIgnoreCase))
- {
- mediaStreamInfo.Path = pathWithDuration;
- mediaStreamInfo.RunTimeTicks = duration.Ticks;
- }
-
- await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken).ConfigureAwait(false);
+ await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken)
+ .ConfigureAwait(false);
recordingStatus = RecordingStatus.Completed;
_logger.Info("Recording completed: {0}", recordPath);
@@ -1038,28 +1322,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_logger.ErrorException("Error recording to {0}", ex, recordPath);
recordingStatus = RecordingStatus.Error;
}
- finally
+
+ if (!string.IsNullOrWhiteSpace(liveStreamId))
{
- if (isResourceOpen && semaphore != null)
+ try
{
- semaphore.Release();
+ await CloseLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error closing live stream", ex);
}
-
- _libraryManager.UnRegisterIgnoredPath(recordPath);
- _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true);
-
- ActiveRecordingInfo removed;
- _activeRecordings.TryRemove(timer.Id, out removed);
}
- if (recordingStatus == RecordingStatus.Completed)
- {
- timer.Status = RecordingStatus.Completed;
- _timerProvider.Delete(timer);
+ _libraryManager.UnRegisterIgnoredPath(recordPath);
+ _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true);
- OnSuccessfulRecording(info.IsSeries, recordPath);
- }
- else if (DateTime.UtcNow < timer.EndDate)
+ ActiveRecordingInfo removed;
+ _activeRecordings.TryRemove(timer.Id, out removed);
+
+ if (recordingStatus != RecordingStatus.Completed && DateTime.UtcNow < timer.EndDate)
{
const int retryIntervalSeconds = 60;
_logger.Info("Retrying recording in {0} seconds.", retryIntervalSeconds);
@@ -1068,10 +1350,123 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
timer.StartDate = DateTime.UtcNow.AddSeconds(retryIntervalSeconds);
_timerProvider.AddOrUpdate(timer);
}
+ else if (File.Exists(recordPath))
+ {
+ timer.RecordingPath = recordPath;
+ timer.Status = RecordingStatus.Completed;
+ _timerProvider.AddOrUpdate(timer, false);
+ OnSuccessfulRecording(timer, recordPath);
+ }
else
{
_timerProvider.Delete(timer);
}
+
+ OnRecordingStatusChanged();
+ }
+
+ private void OnRecordingStatusChanged()
+ {
+ EventHelper.FireEventIfNotNull(RecordingStatusChanged, this, new RecordingStatusChangedEventArgs
+ {
+
+ }, _logger);
+ }
+
+ private async void EnforceKeepUpTo(TimerInfo timer)
+ {
+ if (string.IsNullOrWhiteSpace(timer.SeriesTimerId))
+ {
+ return;
+ }
+
+ var seriesTimerId = timer.SeriesTimerId;
+ var seriesTimer = _seriesTimerProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, seriesTimerId, StringComparison.OrdinalIgnoreCase));
+
+ if (seriesTimer == null || seriesTimer.KeepUpTo <= 1)
+ {
+ return;
+ }
+
+ if (_disposed)
+ {
+ return;
+ }
+
+ await _recordingDeleteSemaphore.WaitAsync().ConfigureAwait(false);
+
+ try
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ var timersToDelete = _timerProvider.GetAll()
+ .Where(i => i.Status == RecordingStatus.Completed && !string.IsNullOrWhiteSpace(i.RecordingPath))
+ .Where(i => string.Equals(i.SeriesTimerId, seriesTimerId, StringComparison.OrdinalIgnoreCase))
+ .OrderByDescending(i => i.EndDate)
+ .Where(i => File.Exists(i.RecordingPath))
+ .Skip(seriesTimer.KeepUpTo - 1)
+ .ToList();
+
+ await DeleteLibraryItemsForTimers(timersToDelete).ConfigureAwait(false);
+ }
+ finally
+ {
+ _recordingDeleteSemaphore.Release();
+ }
+ }
+
+ private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1);
+ private async Task DeleteLibraryItemsForTimers(List<TimerInfo> timers)
+ {
+ foreach (var timer in timers)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ try
+ {
+ await DeleteLibraryItemForTimer(timer).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error deleting recording", ex);
+ }
+ }
+ }
+
+ private async Task DeleteLibraryItemForTimer(TimerInfo timer)
+ {
+ var libraryItem = _libraryManager.FindByPath(timer.RecordingPath, false);
+
+ if (libraryItem != null)
+ {
+ await _libraryManager.DeleteItem(libraryItem, new DeleteOptions
+ {
+ DeleteFileLocation = true
+ });
+ }
+ else
+ {
+ try
+ {
+ File.Delete(timer.RecordingPath);
+ }
+ catch (DirectoryNotFoundException)
+ {
+
+ }
+ catch (FileNotFoundException)
+ {
+
+ }
+ }
+
+ _timerProvider.Delete(timer);
}
private string EnsureFileUnique(string path, string timerId)
@@ -1099,7 +1494,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return true;
}
- var hasRecordingAtPath = _activeRecordings.Values.ToList().Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.TimerId, timerId, StringComparison.OrdinalIgnoreCase));
+ var hasRecordingAtPath = _activeRecordings
+ .Values
+ .ToList()
+ .Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
if (hasRecordingAtPath)
{
@@ -1125,30 +1523,180 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return new DirectRecorder(_logger, _httpClient, _fileSystem);
}
- private async void OnSuccessfulRecording(bool isSeries, string path)
+ private async void OnSuccessfulRecording(TimerInfo timer, string path)
{
- if (GetConfiguration().EnableAutoOrganize)
+ if (timer.IsProgramSeries && GetConfiguration().EnableAutoOrganize)
{
- if (isSeries)
+ try
{
- try
+ // this is to account for the library monitor holding a lock for additional time after the change is complete.
+ // ideally this shouldn't be hard-coded
+ await Task.Delay(30000).ConfigureAwait(false);
+
+ var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager);
+
+ var result = await organize.OrganizeEpisodeFile(path, _config.GetAutoOrganizeOptions(), false, CancellationToken.None).ConfigureAwait(false);
+
+ if (result.Status == FileSortingStatus.Success)
{
- // this is to account for the library monitor holding a lock for additional time after the change is complete.
- // ideally this shouldn't be hard-coded
- await Task.Delay(30000).ConfigureAwait(false);
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error processing new recording", ex);
+ }
+ }
+ }
- var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager);
+ private void SaveNfo(TimerInfo timer, string recordingPath, string seriesPath)
+ {
+ try
+ {
+ if (timer.IsProgramSeries)
+ {
+ SaveSeriesNfo(timer, recordingPath, seriesPath);
+ }
+ else if (!timer.IsMovie || timer.IsSports || timer.IsNews)
+ {
+ SaveVideoNfo(timer, recordingPath);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error saving nfo", ex);
+ }
+ }
+
+ private void SaveSeriesNfo(TimerInfo timer, string recordingPath, string seriesPath)
+ {
+ var nfoPath = Path.Combine(seriesPath, "tvshow.nfo");
+
+ if (File.Exists(nfoPath))
+ {
+ return;
+ }
- var result = await organize.OrganizeEpisodeFile(path, _config.GetAutoOrganizeOptions(), false, CancellationToken.None).ConfigureAwait(false);
+ using (var stream = _fileSystem.GetFileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
+ {
+ var settings = new XmlWriterSettings
+ {
+ Indent = true,
+ Encoding = Encoding.UTF8,
+ CloseOutput = false
+ };
+
+ using (XmlWriter writer = XmlWriter.Create(stream, settings))
+ {
+ writer.WriteStartDocument(true);
+ writer.WriteStartElement("tvshow");
+
+ if (!string.IsNullOrWhiteSpace(timer.Name))
+ {
+ writer.WriteElementString("title", timer.Name);
}
- catch (Exception ex)
+
+ writer.WriteEndElement();
+ writer.WriteEndDocument();
+ }
+ }
+ }
+
+ public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
+ private void SaveVideoNfo(TimerInfo timer, string recordingPath)
+ {
+ var nfoPath = Path.ChangeExtension(recordingPath, ".nfo");
+
+ if (File.Exists(nfoPath))
+ {
+ return;
+ }
+
+ using (var stream = _fileSystem.GetFileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
+ {
+ var settings = new XmlWriterSettings
+ {
+ Indent = true,
+ Encoding = Encoding.UTF8,
+ CloseOutput = false
+ };
+
+ using (XmlWriter writer = XmlWriter.Create(stream, settings))
+ {
+ writer.WriteStartDocument(true);
+ writer.WriteStartElement("movie");
+
+ if (!string.IsNullOrWhiteSpace(timer.Name))
+ {
+ writer.WriteElementString("title", timer.Name);
+ }
+
+ writer.WriteElementString("dateadded", DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat));
+
+ if (timer.ProductionYear.HasValue)
+ {
+ writer.WriteElementString("year", timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture));
+ }
+ if (!string.IsNullOrEmpty(timer.OfficialRating))
+ {
+ writer.WriteElementString("mpaa", timer.OfficialRating);
+ }
+
+ var overview = (timer.Overview ?? string.Empty)
+ .StripHtml()
+ .Replace("&quot;", "'");
+
+ writer.WriteElementString("plot", overview);
+ writer.WriteElementString("lockdata", true.ToString().ToLower());
+
+ if (timer.CommunityRating.HasValue)
+ {
+ writer.WriteElementString("rating", timer.CommunityRating.Value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ if (timer.IsSports)
+ {
+ AddGenre(timer.Genres, "Sports");
+ }
+ if (timer.IsKids)
+ {
+ AddGenre(timer.Genres, "Kids");
+ AddGenre(timer.Genres, "Children");
+ }
+ if (timer.IsNews)
+ {
+ AddGenre(timer.Genres, "News");
+ }
+
+ foreach (var genre in timer.Genres)
+ {
+ writer.WriteElementString("genre", genre);
+ }
+
+ if (!string.IsNullOrWhiteSpace(timer.ShortOverview))
+ {
+ writer.WriteElementString("outline", timer.ShortOverview);
+ }
+
+ if (!string.IsNullOrWhiteSpace(timer.HomePageUrl))
{
- _logger.ErrorException("Error processing new recording", ex);
+ writer.WriteElementString("website", timer.HomePageUrl);
}
+
+ writer.WriteEndElement();
+ writer.WriteEndDocument();
}
}
}
+ private void AddGenre(List<string> genres, string genre)
+ {
+ if (!genres.Contains(genre, StringComparer.OrdinalIgnoreCase))
+ {
+ genres.Add(genre);
+ }
+ }
+
private ProgramInfo GetProgramInfoFromCache(string channelId, string programId)
{
var epgData = GetEpgDataForChannel(channelId);
@@ -1168,41 +1716,101 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return _config.GetConfiguration<LiveTvOptions>("livetv");
}
+ private bool ShouldCancelTimerForSeriesTimer(SeriesTimerInfo seriesTimer, TimerInfo timer)
+ {
+ if (!seriesTimer.RecordAnyTime)
+ {
+ if (Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(5).Ticks)
+ {
+ return true;
+ }
+
+ if (!seriesTimer.Days.Contains(timer.StartDate.ToLocalTime().DayOfWeek))
+ {
+ return true;
+ }
+ }
+
+ if (seriesTimer.RecordNewOnly && timer.IsRepeat)
+ {
+ return true;
+ }
+
+ if (!seriesTimer.RecordAnyChannel && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ return seriesTimer.SkipEpisodesInLibrary && IsProgramAlreadyInLibrary(timer);
+ }
+
private async Task UpdateTimersForSeriesTimer(List<ProgramInfo> epgData, SeriesTimerInfo seriesTimer, bool deleteInvalidTimers)
{
- var newTimers = GetTimersForSeries(seriesTimer, epgData, true).ToList();
-
+ var allTimers = GetTimersForSeries(seriesTimer, epgData)
+ .ToList();
+
var registration = await _liveTvManager.GetRegistrationInfo("seriesrecordings").ConfigureAwait(false);
if (registration.IsValid)
{
- foreach (var timer in newTimers)
+ foreach (var timer in allTimers)
{
- _timerProvider.AddOrUpdate(timer);
+ var existingTimer = _timerProvider.GetTimer(timer.Id);
+
+ if (existingTimer == null)
+ {
+ if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
+ {
+ timer.Status = RecordingStatus.Cancelled;
+ }
+ _timerProvider.Add(timer);
+ }
+ else
+ {
+ // Only update if not currently active
+ ActiveRecordingInfo activeRecordingInfo;
+ if (!_activeRecordings.TryGetValue(timer.Id, out activeRecordingInfo))
+ {
+ UpdateExistingTimerWithNewMetadata(existingTimer, timer);
+
+ if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
+ {
+ existingTimer.Status = RecordingStatus.Cancelled;
+ }
+
+ existingTimer.SeriesTimerId = seriesTimer.Id;
+ _timerProvider.Update(existingTimer);
+ }
+ }
}
}
if (deleteInvalidTimers)
{
- var allTimers = GetTimersForSeries(seriesTimer, epgData, false)
+ var allTimerIds = allTimers
.Select(i => i.Id)
.ToList();
+ var deleteStatuses = new List<RecordingStatus>
+ {
+ RecordingStatus.New
+ };
+
var deletes = _timerProvider.GetAll()
.Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase))
- .Where(i => !allTimers.Contains(i.Id, StringComparer.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow)
+ .Where(i => !allTimerIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow)
+ .Where(i => deleteStatuses.Contains(i.Status))
.ToList();
foreach (var timer in deletes)
{
- await CancelTimerAsync(timer.Id, CancellationToken.None).ConfigureAwait(false);
+ CancelTimerInternal(timer.Id, false);
}
}
}
private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer,
- IEnumerable<ProgramInfo> allPrograms,
- bool filterByCurrentRecordings)
+ IEnumerable<ProgramInfo> allPrograms)
{
if (seriesTimer == null)
{
@@ -1214,19 +1822,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
// Exclude programs that have already ended
- allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow && i.StartDate > DateTime.UtcNow);
+ allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow);
allPrograms = GetProgramsForSeries(seriesTimer, allPrograms);
- if (filterByCurrentRecordings)
- {
- allPrograms = allPrograms.Where(i => !IsProgramAlreadyInLibrary(i));
- }
-
return allPrograms.Select(i => RecordingHelper.CreateTimer(i, seriesTimer));
}
- private bool IsProgramAlreadyInLibrary(ProgramInfo program)
+ private bool IsProgramAlreadyInLibrary(TimerInfo program)
{
if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle))
{
@@ -1281,23 +1884,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
private IEnumerable<ProgramInfo> GetProgramsForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms)
{
- if (!seriesTimer.RecordAnyTime)
- {
- allPrograms = allPrograms.Where(epg => Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - epg.StartDate.TimeOfDay.Ticks) < TimeSpan.FromMinutes(5).Ticks);
-
- allPrograms = allPrograms.Where(i => seriesTimer.Days.Contains(i.StartDate.ToLocalTime().DayOfWeek));
- }
-
- if (seriesTimer.RecordNewOnly)
- {
- allPrograms = allPrograms.Where(epg => !epg.IsRepeat);
- }
-
- if (!seriesTimer.RecordAnyChannel)
- {
- allPrograms = allPrograms.Where(epg => string.Equals(epg.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase));
- }
-
if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId))
{
_logger.Error("seriesTimer.SeriesId is null. Cannot find programs for series");
@@ -1341,8 +1927,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return channelIds.SelectMany(GetEpgDataForChannel).ToList();
}
+ private bool _disposed;
public void Dispose()
{
+ _disposed = true;
foreach (var pair in _activeRecordings.ToList())
{
pair.Value.CancellationTokenSource.Cancel();
@@ -1393,7 +1981,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
class ActiveRecordingInfo
{
public string Path { get; set; }
- public string TimerId { get; set; }
+ public TimerInfo Timer { get; set; }
+ public ProgramInfo Program { get; set; }
public CancellationTokenSource CancellationTokenSource { get; set; }
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index 75ad3de59..3e9d186e3 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -68,36 +68,6 @@ 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 tempfile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts");
-
- try
- {
- await RecordWithTempFile(mediaSource, tempfile, targetFile, duration, onStarted, cancellationToken)
- .ConfigureAwait(false);
- }
- finally
- {
- try
- {
- File.Delete(tempfile);
- }
- catch (Exception ex)
- {
- _logger.ErrorException("Error deleting recording temp file", ex);
- }
- }
- }
-
- 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;
@@ -106,59 +76,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_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 = response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, 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, duration, onStarted, cancellationToken), cancellationToken);
-
- await tempFileTask.ConfigureAwait(false);
-
- await recordTask.ConfigureAwait(false);
- }
- }
-
- _logger.Info("Recording completed to file {0}", targetFile);
- }
-
private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
_targetPath = targetFile;
@@ -200,7 +117,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
_logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length);
- process.Exited += (sender, args) => OnFfMpegProcessExited(process);
+ process.Exited += (sender, args) => OnFfMpegProcessExited(process, inputFile);
process.Start();
@@ -214,6 +131,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
StartStreamingLog(process.StandardError.BaseStream, _logFileStream);
+ _logger.Info("ffmpeg recording process started for {0}", _targetPath);
+
return _taskCompletionSource.Task;
}
@@ -234,16 +153,33 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks);
- var commandLineArgs = "-fflags +genpts -async 1 -vsync -1 -re -i \"{0}\"{4} -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
+ var inputModifiers = "-fflags +genpts -async 1 -vsync -1";
+ var commandLineArgs = "-i \"{0}\"{4} -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
+
+ long startTimeTicks = 0;
+ //if (mediaSource.DateLiveStreamOpened.HasValue)
+ //{
+ // var elapsed = DateTime.UtcNow - mediaSource.DateLiveStreamOpened.Value;
+ // elapsed -= TimeSpan.FromSeconds(10);
+ // if (elapsed.TotalSeconds >= 0)
+ // {
+ // startTimeTicks = elapsed.Ticks + startTimeTicks;
+ // }
+ //}
if (mediaSource.ReadAtNativeFramerate)
{
- commandLineArgs = "-re " + commandLineArgs;
+ inputModifiers += " -re";
+ }
+
+ if (startTimeTicks > 0)
+ {
+ inputModifiers = "-ss " + _mediaEncoder.GetTimeParameter(startTimeTicks) + " " + inputModifiers;
}
commandLineArgs = string.Format(commandLineArgs, inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), durationParam);
- return commandLineArgs;
+ return inputModifiers + " " + commandLineArgs;
}
private string GetAudioArgs(MediaSourceInfo mediaSource)
@@ -309,8 +245,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
/// <summary>
/// Processes the exited.
/// </summary>
- /// <param name="process">The process.</param>
- private void OnFfMpegProcessExited(Process process)
+ private void OnFfMpegProcessExited(Process process, string inputFile)
{
_hasExited = true;
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
index 9485e0325..f7b4b3fde 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
@@ -2,6 +2,7 @@
using MediaBrowser.Controller.LiveTv;
using System;
using System.Globalization;
+using MediaBrowser.Model.LiveTv;
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
@@ -12,37 +13,55 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return timer.StartDate.AddSeconds(-timer.PrePaddingSeconds);
}
- public static TimerInfo CreateTimer(ProgramInfo parent, SeriesTimerInfo series)
+ public static TimerInfo CreateTimer(ProgramInfo parent, SeriesTimerInfo seriesTimer)
{
var timer = new TimerInfo();
timer.ChannelId = parent.ChannelId;
- timer.Id = (series.Id + parent.Id).GetMD5().ToString("N");
+ timer.Id = (seriesTimer.Id + parent.Id).GetMD5().ToString("N");
timer.StartDate = parent.StartDate;
timer.EndDate = parent.EndDate;
timer.ProgramId = parent.Id;
- timer.PrePaddingSeconds = series.PrePaddingSeconds;
- timer.PostPaddingSeconds = series.PostPaddingSeconds;
- timer.IsPostPaddingRequired = series.IsPostPaddingRequired;
- timer.IsPrePaddingRequired = series.IsPrePaddingRequired;
- timer.Priority = series.Priority;
+ timer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds;
+ timer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds;
+ timer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired;
+ timer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired;
+ timer.KeepUntil = seriesTimer.KeepUntil;
+ timer.Priority = seriesTimer.Priority;
timer.Name = parent.Name;
timer.Overview = parent.Overview;
- timer.SeriesTimerId = series.Id;
+ timer.SeriesTimerId = seriesTimer.Id;
+
+ CopyProgramInfoToTimerInfo(parent, timer);
return timer;
}
- public static string GetRecordingName(TimerInfo timer, ProgramInfo info)
+ public static void CopyProgramInfoToTimerInfo(ProgramInfo programInfo, TimerInfo timerInfo)
{
- if (info == null)
- {
- return timer.ProgramId;
- }
+ timerInfo.SeasonNumber = programInfo.SeasonNumber;
+ timerInfo.EpisodeNumber = programInfo.EpisodeNumber;
+ timerInfo.IsMovie = programInfo.IsMovie;
+ timerInfo.IsKids = programInfo.IsKids;
+ timerInfo.IsNews = programInfo.IsNews;
+ timerInfo.IsSports = programInfo.IsSports;
+ timerInfo.ProductionYear = programInfo.ProductionYear;
+ timerInfo.EpisodeTitle = programInfo.EpisodeTitle;
+ timerInfo.OriginalAirDate = programInfo.OriginalAirDate;
+ timerInfo.IsProgramSeries = programInfo.IsSeries;
+ timerInfo.HomePageUrl = programInfo.HomePageUrl;
+ timerInfo.CommunityRating = programInfo.CommunityRating;
+ timerInfo.ShortOverview = programInfo.ShortOverview;
+ timerInfo.OfficialRating = programInfo.OfficialRating;
+ timerInfo.IsRepeat = programInfo.IsRepeat;
+ }
+
+ public static string GetRecordingName(TimerInfo info)
+ {
var name = info.Name;
- if (info.IsSeries)
+ if (info.IsProgramSeries)
{
var addHyphen = true;
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
index 423358906..bddce0420 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -9,7 +9,6 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using CommonIO;
-using MediaBrowser.Controller.Power;
using MediaBrowser.Model.LiveTv;
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
@@ -17,15 +16,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
public class TimerManager : ItemDataProvider<TimerInfo>
{
private readonly ConcurrentDictionary<string, Timer> _timers = new ConcurrentDictionary<string, Timer>(StringComparer.OrdinalIgnoreCase);
- private readonly IPowerManagement _powerManagement;
private readonly ILogger _logger;
public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired;
- public TimerManager(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath, IPowerManagement powerManagement, ILogger logger1)
+ public TimerManager(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath, ILogger logger1)
: base(fileSystem, jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
{
- _powerManagement = powerManagement;
_logger = logger1;
}
@@ -35,7 +32,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
foreach (var item in GetAll().ToList())
{
- AddTimer(item);
+ AddOrUpdateSystemTimer(item);
}
}
@@ -58,18 +55,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
public override void Update(TimerInfo item)
{
base.Update(item);
-
- Timer timer;
- if (_timers.TryGetValue(item.Id, out timer))
- {
- var timespan = RecordingHelper.GetStartTime(item) - DateTime.UtcNow;
- timer.Change(timespan, TimeSpan.Zero);
- ScheduleWake(item);
- }
- else
- {
- AddTimer(item);
- }
+ AddOrUpdateSystemTimer(item);
}
public void AddOrUpdate(TimerInfo item, bool resetTimer)
@@ -100,13 +86,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
base.Add(item);
- AddTimer(item);
- ScheduleWake(item);
+ AddOrUpdateSystemTimer(item);
+ }
+
+ private bool ShouldStartTimer(TimerInfo item)
+ {
+ if (item.Status == RecordingStatus.Completed ||
+ item.Status == RecordingStatus.Cancelled)
+ {
+ return false;
+ }
+
+ return true;
}
- private void AddTimer(TimerInfo item)
+ private void AddOrUpdateSystemTimer(TimerInfo item)
{
- if (item.Status == RecordingStatus.Completed)
+ StopTimer(item);
+
+ if (!ShouldStartTimer(item))
{
return;
}
@@ -120,33 +118,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return;
}
- var timerLength = startDate - now;
- StartTimer(item, timerLength);
+ var dueTime = startDate - now;
+ StartTimer(item, dueTime);
}
- private void ScheduleWake(TimerInfo info)
+ private void StartTimer(TimerInfo item, TimeSpan dueTime)
{
- var startDate = RecordingHelper.GetStartTime(info).AddMinutes(-5);
-
- try
- {
- _powerManagement.ScheduleWake(startDate);
- _logger.Info("Scheduled system wake timer at {0} (UTC)", startDate);
- }
- catch (NotImplementedException)
- {
-
- }
- catch (Exception ex)
- {
- _logger.ErrorException("Error scheduling wake timer", ex);
- }
- }
-
- public void StartTimer(TimerInfo item, TimeSpan dueTime)
- {
- StopTimer(item);
-
var timer = new Timer(TimerCallback, item.Id, dueTime, TimeSpan.Zero);
if (_timers.TryAdd(item.Id, timer))
@@ -179,5 +156,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
EventHelper.FireEventIfNotNull(TimerFired, this, new GenericEventArgs<TimerInfo> { Argument = timer }, Logger);
}
}
+
+ public TimerInfo GetTimer(string id)
+ {
+ return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase));
+ }
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index d1d8df2e8..d3549aef5 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -11,6 +11,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Emby.XmlTv.Classes;
+using Emby.XmlTv.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
@@ -115,7 +116,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
var reader = new XmlTvReader(path, GetLanguage(), null);
var results = reader.GetProgrammes(channelNumber, startDateUtc, endDateUtc, cancellationToken);
- return results.Select(p => new ProgramInfo()
+ return results.Select(p => GetProgramInfo(p, info));
+ }
+
+ private ProgramInfo GetProgramInfo(XmlTvProgram p, ListingsProviderInfo info)
+ {
+ var programInfo = new ProgramInfo
{
ChannelId = p.ChannelId,
EndDate = GetDate(p.EndDate),
@@ -141,7 +147,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
OfficialRating = p.Rating != null && !String.IsNullOrEmpty(p.Rating.Value) ? p.Rating.Value : null,
CommunityRating = p.StarRating.HasValue ? p.StarRating.Value : (float?)null,
SeriesId = p.Episode != null ? p.Title.GetMD5().ToString("N") : null
- });
+ };
+
+ if (programInfo.IsMovie)
+ {
+ programInfo.IsSeries = false;
+ programInfo.EpisodeNumber = null;
+ programInfo.EpisodeTitle = null;
+ }
+
+ return programInfo;
}
private DateTime GetDate(DateTime date)
diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveStreamHelper.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveStreamHelper.cs
new file mode 100644
index 000000000..336c32bae
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveStreamHelper.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Logging;
+
+namespace MediaBrowser.Server.Implementations.LiveTv
+{
+ public class LiveStreamHelper
+ {
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly ILogger _logger;
+
+ public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger)
+ {
+ _mediaEncoder = mediaEncoder;
+ _logger = logger;
+ }
+
+ public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
+ {
+ var originalRuntime = mediaSource.RunTimeTicks;
+
+ var now = DateTime.UtcNow;
+
+ var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
+ {
+ InputPath = mediaSource.Path,
+ Protocol = mediaSource.Protocol,
+ MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
+ ExtractChapters = false,
+ AnalyzeDurationSections = 2
+
+ }, cancellationToken).ConfigureAwait(false);
+
+ _logger.Info("Live tv media info probe took {0} seconds", (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture));
+
+ mediaSource.Bitrate = info.Bitrate;
+ mediaSource.Container = info.Container;
+ mediaSource.Formats = info.Formats;
+ mediaSource.MediaStreams = info.MediaStreams;
+ mediaSource.RunTimeTicks = info.RunTimeTicks;
+ mediaSource.Size = info.Size;
+ mediaSource.Timestamp = info.Timestamp;
+ mediaSource.Video3DFormat = info.Video3DFormat;
+ mediaSource.VideoType = info.VideoType;
+
+ mediaSource.DefaultSubtitleStreamIndex = null;
+
+ // Null this out so that it will be treated like a live stream
+ if (!originalRuntime.HasValue)
+ {
+ mediaSource.RunTimeTicks = null;
+ }
+
+ var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Audio);
+
+ if (audioStream == null || audioStream.Index == -1)
+ {
+ mediaSource.DefaultAudioStreamIndex = null;
+ }
+ else
+ {
+ mediaSource.DefaultAudioStreamIndex = audioStream.Index;
+ }
+
+ var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Video);
+ if (videoStream != null)
+ {
+ if (!videoStream.BitRate.HasValue)
+ {
+ var width = videoStream.Width ?? 1920;
+
+ if (width >= 1900)
+ {
+ videoStream.BitRate = 8000000;
+ }
+
+ else if (width >= 1260)
+ {
+ videoStream.BitRate = 3000000;
+ }
+
+ else if (width >= 700)
+ {
+ videoStream.BitRate = 1000000;
+ }
+ }
+
+ // This is coming up false and preventing stream copy
+ videoStream.IsAVC = null;
+ }
+
+ // Try to estimate this
+ if (!mediaSource.Bitrate.HasValue)
+ {
+ var total = mediaSource.MediaStreams.Select(i => i.BitRate ?? 0).Sum();
+
+ if (total > 0)
+ {
+ mediaSource.Bitrate = total;
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
index 683377c61..8c46b4597 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
@@ -10,6 +10,7 @@ using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Logging;
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -52,6 +53,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
PostPaddingSeconds = info.PostPaddingSeconds,
IsPostPaddingRequired = info.IsPostPaddingRequired,
IsPrePaddingRequired = info.IsPrePaddingRequired,
+ KeepUntil = info.KeepUntil,
ExternalChannelId = info.ChannelId,
ExternalSeriesTimerId = info.SeriesTimerId,
ServiceName = service.Name,
@@ -71,6 +73,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
dto.ProgramInfo = _dtoService.GetBaseItemDto(program, new DtoOptions());
dto.ProgramInfo.TimerId = dto.Id;
+
dto.ProgramInfo.SeriesTimerId = dto.SeriesTimerId;
}
@@ -100,6 +103,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
Priority = info.Priority,
RecordAnyChannel = info.RecordAnyChannel,
RecordAnyTime = info.RecordAnyTime,
+ SkipEpisodesInLibrary = info.SkipEpisodesInLibrary,
+ KeepUpTo = info.KeepUpTo,
+ KeepUntil = info.KeepUntil,
RecordNewOnly = info.RecordNewOnly,
ExternalChannelId = info.ChannelId,
ExternalProgramId = info.ProgramId,
@@ -120,6 +126,34 @@ namespace MediaBrowser.Server.Implementations.LiveTv
dto.DayPattern = info.Days == null ? null : GetDayPattern(info.Days);
+ if (!string.IsNullOrWhiteSpace(info.SeriesId))
+ {
+ var program = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+ ExternalSeriesId = info.SeriesId,
+ Limit = 1,
+ ImageTypes = new ImageType[] { ImageType.Primary }
+
+ }).FirstOrDefault();
+
+ if (program != null)
+ {
+ var image = program.GetImageInfo(ImageType.Primary, 0);
+ if (image != null)
+ {
+ try
+ {
+ dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image);
+ dto.ParentPrimaryImageItemId = program.Id.ToString("N");
+ }
+ catch (Exception ex)
+ {
+ }
+ }
+ }
+ }
+
return dto;
}
@@ -244,6 +278,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
PostPaddingSeconds = dto.PostPaddingSeconds,
IsPostPaddingRequired = dto.IsPostPaddingRequired,
IsPrePaddingRequired = dto.IsPrePaddingRequired,
+ KeepUntil = dto.KeepUntil,
Priority = dto.Priority,
SeriesTimerId = dto.ExternalSeriesTimerId,
ProgramId = dto.ExternalProgramId,
@@ -308,6 +343,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
Priority = dto.Priority,
RecordAnyChannel = dto.RecordAnyChannel,
RecordAnyTime = dto.RecordAnyTime,
+ SkipEpisodesInLibrary = dto.SkipEpisodesInLibrary,
+ KeepUpTo = dto.KeepUpTo,
+ KeepUntil = dto.KeepUntil,
RecordNewOnly = dto.RecordNewOnly,
ProgramId = dto.ExternalProgramId,
ChannelId = dto.ExternalChannelId,
diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
index b3ced55a5..bac9789b5 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -62,9 +62,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
private readonly List<ILiveTvService> _services = new List<ILiveTvService>();
- private readonly ConcurrentDictionary<string, LiveStreamData> _openStreams =
- new ConcurrentDictionary<string, LiveStreamData>();
-
private readonly SemaphoreSlim _refreshRecordingsLock = new SemaphoreSlim(1, 1);
private readonly List<ITunerHost> _tunerHosts = new List<ITunerHost>();
@@ -124,9 +121,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv
foreach (var service in _services)
{
service.DataSourceChanged += service_DataSourceChanged;
+ service.RecordingStatusChanged += Service_RecordingStatusChanged;
}
}
+ private void Service_RecordingStatusChanged(object sender, RecordingStatusChangedEventArgs e)
+ {
+ _lastRecordingRefreshTime = DateTime.MinValue;
+ }
+
public List<ITunerHost> TunerHosts
{
get { return _tunerHosts; }
@@ -151,112 +154,40 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var topFolder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false);
- var channels = _libraryManager.GetItemList(new InternalItemsQuery
+ var internalQuery = new InternalItemsQuery(user)
{
+ IsMovie = query.IsMovie,
+ IsNews = query.IsNews,
+ IsKids = query.IsKids,
+ IsSports = query.IsSports,
+ IsSeries = query.IsSeries,
IncludeItemTypes = new[] { typeof(LiveTvChannel).Name },
- SortBy = new[] { ItemSortBy.SortName },
- TopParentIds = new[] { topFolder.Id.ToString("N") }
+ SortOrder = query.SortOrder ?? SortOrder.Ascending,
+ TopParentIds = new[] { topFolder.Id.ToString("N") },
+ IsFavorite = query.IsFavorite,
+ IsLiked = query.IsLiked,
+ StartIndex = query.StartIndex,
+ Limit = query.Limit
+ };
- }).Cast<LiveTvChannel>();
+ internalQuery.OrderBy.AddRange(query.SortBy.Select(i => new Tuple<string, SortOrder>(i, query.SortOrder ?? SortOrder.Ascending)));
- if (user != null)
+ if (query.EnableFavoriteSorting)
{
- // Avoid implicitly captured closure
- var currentUser = user;
-
- channels = channels
- .Where(i => i.IsVisible(currentUser))
- .OrderBy(i =>
- {
- double number = 0;
-
- if (!string.IsNullOrEmpty(i.Number))
- {
- double.TryParse(i.Number, out number);
- }
-
- return number;
-
- });
-
- if (query.IsFavorite.HasValue)
- {
- var val = query.IsFavorite.Value;
-
- channels = channels
- .Where(i => _userDataManager.GetUserData(user, i).IsFavorite == val);
- }
-
- if (query.IsLiked.HasValue)
- {
- var val = query.IsLiked.Value;
-
- channels = channels
- .Where(i =>
- {
- var likes = _userDataManager.GetUserData(user, i).Likes;
-
- return likes.HasValue && likes.Value == val;
- });
- }
-
- if (query.IsDisliked.HasValue)
- {
- var val = query.IsDisliked.Value;
-
- channels = channels
- .Where(i =>
- {
- var likes = _userDataManager.GetUserData(user, i).Likes;
-
- return likes.HasValue && likes.Value != val;
- });
- }
+ internalQuery.OrderBy.Insert(0, new Tuple<string, SortOrder>(ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending));
}
- var enableFavoriteSorting = query.EnableFavoriteSorting;
-
- channels = channels.OrderBy(i =>
+ if (!internalQuery.OrderBy.Any(i => string.Equals(i.Item1, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase)))
{
- if (enableFavoriteSorting)
- {
- var userData = _userDataManager.GetUserData(user, i);
-
- if (userData.IsFavorite)
- {
- return 0;
- }
- if (userData.Likes.HasValue)
- {
- if (!userData.Likes.Value)
- {
- return 3;
- }
-
- return 1;
- }
- }
-
- return 2;
- });
-
- var allChannels = channels.ToList();
- IEnumerable<LiveTvChannel> allEnumerable = allChannels;
-
- if (query.StartIndex.HasValue)
- {
- allEnumerable = allEnumerable.Skip(query.StartIndex.Value);
+ internalQuery.OrderBy.Add(new Tuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending));
}
- if (query.Limit.HasValue)
- {
- allEnumerable = allEnumerable.Take(query.Limit.Value);
- }
+ var channelResult = _libraryManager.GetItemsResult(internalQuery);
var result = new QueryResult<LiveTvChannel>
{
- Items = allEnumerable.ToArray(),
- TotalRecordCount = allChannels.Count
+ Items = channelResult.Items.Cast<LiveTvChannel>().ToArray(),
+ TotalRecordCount = channelResult.TotalRecordCount
};
return result;
@@ -298,32 +229,32 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return result.Items.FirstOrDefault();
}
- private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
-
public async Task<MediaSourceInfo> GetRecordingStream(string id, CancellationToken cancellationToken)
{
- return await GetLiveStream(id, null, false, cancellationToken).ConfigureAwait(false);
+ var info = await GetLiveStream(id, null, false, cancellationToken).ConfigureAwait(false);
+
+ return info.Item1;
}
- public async Task<MediaSourceInfo> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken)
+ public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken)
{
return await GetLiveStream(id, mediaSourceId, true, cancellationToken).ConfigureAwait(false);
}
- public async Task<IEnumerable<MediaSourceInfo>> GetRecordingMediaSources(string id, CancellationToken cancellationToken)
+ public async Task<IEnumerable<MediaSourceInfo>> GetRecordingMediaSources(IHasMediaSources item, CancellationToken cancellationToken)
{
- var item = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false);
- var service = GetService(item);
+ var baseItem = (BaseItem)item;
+ var service = GetService(baseItem);
- return await service.GetRecordingStreamMediaSources(id, cancellationToken).ConfigureAwait(false);
+ return await service.GetRecordingStreamMediaSources(baseItem.ExternalId, cancellationToken).ConfigureAwait(false);
}
- public async Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(string id, CancellationToken cancellationToken)
+ public async Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(IHasMediaSources item, CancellationToken cancellationToken)
{
- var item = GetInternalChannel(id);
- var service = GetService(item);
+ var baseItem = (LiveTvChannel)item;
+ var service = GetService(baseItem);
- var sources = await service.GetChannelStreamMediaSources(item.ExternalId, cancellationToken).ConfigureAwait(false);
+ var sources = await service.GetChannelStreamMediaSources(baseItem.ExternalId, cancellationToken).ConfigureAwait(false);
if (sources.Count == 0)
{
@@ -334,7 +265,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
foreach (var source in list)
{
- Normalize(source, service, item.ChannelType == ChannelType.TV);
+ Normalize(source, service, baseItem.ChannelType == ChannelType.TV);
}
return list;
@@ -355,79 +286,67 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return _services.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
}
- private async Task<MediaSourceInfo> GetLiveStream(string id, string mediaSourceId, bool isChannel, CancellationToken cancellationToken)
+ private async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStream(string id, string mediaSourceId, bool isChannel, CancellationToken cancellationToken)
{
- await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
if (string.Equals(id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
{
mediaSourceId = null;
}
- try
+ MediaSourceInfo info;
+ bool isVideo;
+ ILiveTvService service;
+ IDirectStreamProvider directStreamProvider = null;
+
+ if (isChannel)
{
- MediaSourceInfo info;
- bool isVideo;
- ILiveTvService service;
+ var channel = GetInternalChannel(id);
+ isVideo = channel.ChannelType == ChannelType.TV;
+ service = GetService(channel);
+ _logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId);
- if (isChannel)
+ var supportsManagedStream = service as ISupportsDirectStreamProvider;
+ if (supportsManagedStream != null)
{
- var channel = GetInternalChannel(id);
- isVideo = channel.ChannelType == ChannelType.TV;
- service = GetService(channel);
- _logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId);
- info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
- info.RequiresClosing = true;
-
- if (info.RequiresClosing)
- {
- var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
-
- info.LiveStreamId = idPrefix + info.Id;
- }
+ var streamInfo = await supportsManagedStream.GetChannelStreamWithDirectStreamProvider(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
+ info = streamInfo.Item1;
+ directStreamProvider = streamInfo.Item2;
}
else
{
- var recording = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false);
- isVideo = !string.Equals(recording.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase);
- service = GetService(recording);
-
- _logger.Info("Opening recording stream from {0}, external recording Id: {1}", service.Name, recording.ExternalId);
- info = await service.GetRecordingStream(recording.ExternalId, null, cancellationToken).ConfigureAwait(false);
- info.RequiresClosing = true;
+ info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
+ }
+ info.RequiresClosing = true;
- if (info.RequiresClosing)
- {
- var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
+ if (info.RequiresClosing)
+ {
+ var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
- info.LiveStreamId = idPrefix + info.Id;
- }
+ info.LiveStreamId = idPrefix + info.Id;
}
+ }
+ else
+ {
+ var recording = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false);
+ isVideo = !string.Equals(recording.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase);
+ service = GetService(recording);
- _logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(info));
- Normalize(info, service, isVideo);
+ _logger.Info("Opening recording stream from {0}, external recording Id: {1}", service.Name, recording.ExternalId);
+ info = await service.GetRecordingStream(recording.ExternalId, null, cancellationToken).ConfigureAwait(false);
+ info.RequiresClosing = true;
- var data = new LiveStreamData
+ if (info.RequiresClosing)
{
- Info = info,
- IsChannel = isChannel,
- ItemId = id
- };
-
- _openStreams.AddOrUpdate(info.Id, data, (key, i) => data);
+ var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
- return info;
+ info.LiveStreamId = idPrefix + info.Id;
+ }
}
- catch (Exception ex)
- {
- _logger.ErrorException("Error getting channel stream", ex);
- throw;
- }
- finally
- {
- _liveStreamSemaphore.Release();
- }
+ _logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(info));
+ Normalize(info, service, isVideo);
+
+ return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info, directStreamProvider);
}
private void Normalize(MediaSourceInfo mediaSource, ILiveTvService service, bool isVideo)
@@ -628,11 +547,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return item;
}
- private async Task<LiveTvProgram> GetProgram(ProgramInfo info, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken)
+ private Tuple<LiveTvProgram, bool, bool> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken)
{
var id = _tvDtoService.GetInternalProgramId(serviceName, info.Id);
- var item = _libraryManager.GetItemById(id) as LiveTvProgram;
+ LiveTvProgram item = null;
+ allExistingPrograms.TryGetValue(id, out item);
+
var isNew = false;
var forceUpdate = false;
@@ -649,6 +570,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
};
}
+ var seriesId = info.SeriesId;
+
if (!item.ParentId.Equals(channel.Id))
{
forceUpdate = true;
@@ -668,6 +591,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
item.EpisodeTitle = info.EpisodeTitle;
item.ExternalId = info.Id;
+ item.ExternalSeriesIdLegacy = seriesId;
+
+ if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal))
+ {
+ forceUpdate = true;
+ }
+ item.ExternalSeriesId = seriesId;
+
item.Genres = info.Genres;
item.IsHD = info.IsHD;
item.IsKids = info.IsKids;
@@ -698,7 +629,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
item.HomePageUrl = info.HomePageUrl;
item.ProductionYear = info.ProductionYear;
- item.PremiereDate = info.OriginalAirDate;
+
+ if (!info.IsSeries || info.IsRepeat)
+ {
+ item.PremiereDate = info.OriginalAirDate;
+ }
item.IndexNumber = info.EpisodeNumber;
item.ParentIndexNumber = info.SeasonNumber;
@@ -725,13 +660,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}
}
+ var isUpdated = false;
if (isNew)
{
- await _libraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false);
}
else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
{
- await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
+ isUpdated = true;
}
else
{
@@ -741,13 +676,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
if (!string.Equals(etag, item.ExternalEtag, StringComparison.OrdinalIgnoreCase))
{
item.ExternalEtag = etag;
- await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
+ isUpdated = true;
}
}
- _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem));
-
- return item;
+ return new Tuple<LiveTvProgram, bool, bool>(item, isNew, isUpdated);
}
private async Task<Guid> CreateRecordingRecord(RecordingInfo info, string serviceName, Guid parentFolderId, CancellationToken cancellationToken)
@@ -811,6 +744,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
recording.IsRepeat = info.IsRepeat;
recording.IsSports = info.IsSports;
recording.SeriesTimerId = info.SeriesTimerId;
+ recording.TimerId = info.TimerId;
recording.StartDate = info.StartDate;
if (!dataChanged)
@@ -903,8 +837,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user);
- var list = new List<Tuple<BaseItemDto, string, string>>();
- list.Add(new Tuple<BaseItemDto, string, string>(dto, program.ServiceName, program.ExternalId));
+ var list = new List<Tuple<BaseItemDto, string, string, string>>();
+ list.Add(new Tuple<BaseItemDto, string, string, string>(dto, program.ServiceName, program.ExternalId, program.ExternalSeriesIdLegacy));
await AddRecordingInfo(list, cancellationToken).ConfigureAwait(false);
@@ -932,17 +866,41 @@ namespace MediaBrowser.Server.Implementations.LiveTv
MaxStartDate = query.MaxStartDate,
ChannelIds = query.ChannelIds,
IsMovie = query.IsMovie,
+ IsSeries = query.IsSeries,
IsSports = query.IsSports,
IsKids = query.IsKids,
+ IsNews = query.IsNews,
Genres = query.Genres,
StartIndex = query.StartIndex,
Limit = query.Limit,
SortBy = query.SortBy,
SortOrder = query.SortOrder ?? SortOrder.Ascending,
EnableTotalRecordCount = query.EnableTotalRecordCount,
- TopParentIds = new[] { topFolder.Id.ToString("N") }
+ TopParentIds = new[] { topFolder.Id.ToString("N") },
+ DtoOptions = options
};
+ if (!string.IsNullOrWhiteSpace(query.SeriesTimerId))
+ {
+ var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery { }, cancellationToken).ConfigureAwait(false);
+ var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.ServiceName, i.Id).ToString("N"), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase));
+ if (seriesTimer != null)
+ {
+ internalQuery.ExternalSeriesId = seriesTimer.SeriesId;
+
+ if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId))
+ {
+ // Better to return nothing than every program in the database
+ return new QueryResult<BaseItemDto>();
+ }
+ }
+ else
+ {
+ // Better to return nothing than every program in the database
+ return new QueryResult<BaseItemDto>();
+ }
+ }
+
if (query.HasAired.HasValue)
{
if (query.HasAired.Value)
@@ -970,7 +928,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return result;
}
- public async Task<QueryResult<LiveTvProgram>> GetRecommendedProgramsInternal(RecommendedProgramQuery query, CancellationToken cancellationToken)
+ public async Task<QueryResult<LiveTvProgram>> GetRecommendedProgramsInternal(RecommendedProgramQuery query, DtoOptions options, CancellationToken cancellationToken)
{
var user = _userManager.GetUserById(query.UserId);
@@ -980,12 +938,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
IsAiring = query.IsAiring,
+ IsNews = query.IsNews,
IsMovie = query.IsMovie,
+ IsSeries = query.IsSeries,
IsSports = query.IsSports,
IsKids = query.IsKids,
EnableTotalRecordCount = query.EnableTotalRecordCount,
SortBy = new[] { ItemSortBy.StartDate },
- TopParentIds = new[] { topFolder.Id.ToString("N") }
+ TopParentIds = new[] { topFolder.Id.ToString("N") },
+ DtoOptions = options
};
if (query.Limit.HasValue)
@@ -1009,9 +970,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var programList = programs.ToList();
- var factorChannelWatchCount = (query.IsAiring ?? false) || (query.IsKids ?? false) || (query.IsSports ?? false) || (query.IsMovie ?? false);
+ var factorChannelWatchCount = (query.IsAiring ?? false) || (query.IsKids ?? false) || (query.IsSports ?? false) || (query.IsMovie ?? false) || (query.IsNews ?? false) || (query.IsSeries ?? false);
- programs = programList.OrderBy(i => i.HasImage(ImageType.Primary) ? 0 : 1)
+ programs = programList.OrderBy(i => i.StartDate.Date)
.ThenByDescending(i => GetRecommendationScore(i, user.Id, factorChannelWatchCount))
.ThenBy(i => i.StartDate);
@@ -1035,7 +996,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
public async Task<QueryResult<BaseItemDto>> GetRecommendedPrograms(RecommendedProgramQuery query, DtoOptions options, CancellationToken cancellationToken)
{
- var internalResult = await GetRecommendedProgramsInternal(query, cancellationToken).ConfigureAwait(false);
+ var internalResult = await GetRecommendedProgramsInternal(query, options, cancellationToken).ConfigureAwait(false);
var user = _userManager.GetUserById(query.UserId);
@@ -1092,15 +1053,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return score;
}
- private async Task AddRecordingInfo(IEnumerable<Tuple<BaseItemDto, string, string>> programs, CancellationToken cancellationToken)
+ private async Task AddRecordingInfo(IEnumerable<Tuple<BaseItemDto, string, string, string>> programs, CancellationToken cancellationToken)
{
var timers = new Dictionary<string, List<TimerInfo>>();
+ var seriesTimers = new Dictionary<string, List<SeriesTimerInfo>>();
foreach (var programTuple in programs)
{
var program = programTuple.Item1;
var serviceName = programTuple.Item2;
var externalProgramId = programTuple.Item3;
+ string externalSeriesId = programTuple.Item4;
if (string.IsNullOrWhiteSpace(serviceName))
{
@@ -1123,18 +1086,54 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}
var timer = timerList.FirstOrDefault(i => string.Equals(i.ProgramId, externalProgramId, StringComparison.OrdinalIgnoreCase));
+ var foundSeriesTimer = false;
if (timer != null)
{
- program.TimerId = _tvDtoService.GetInternalTimerId(serviceName, timer.Id)
- .ToString("N");
+ if (timer.Status != RecordingStatus.Cancelled && timer.Status != RecordingStatus.Error)
+ {
+ program.TimerId = _tvDtoService.GetInternalTimerId(serviceName, timer.Id)
+ .ToString("N");
+
+ program.Status = timer.Status.ToString();
+ }
if (!string.IsNullOrEmpty(timer.SeriesTimerId))
{
program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(serviceName, timer.SeriesTimerId)
.ToString("N");
+
+ foundSeriesTimer = true;
}
}
+
+ if (foundSeriesTimer || string.IsNullOrWhiteSpace(externalSeriesId))
+ {
+ continue;
+ }
+
+ List<SeriesTimerInfo> seriesTimerList;
+ if (!seriesTimers.TryGetValue(serviceName, out seriesTimerList))
+ {
+ try
+ {
+ var tempTimers = await GetService(serviceName).GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
+ seriesTimers[serviceName] = seriesTimerList = tempTimers.ToList();
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting series timer infos", ex);
+ seriesTimers[serviceName] = seriesTimerList = new List<SeriesTimerInfo>();
+ }
+ }
+
+ var seriesTimer = seriesTimerList.FirstOrDefault(i => string.Equals(i.SeriesId, externalSeriesId, StringComparison.OrdinalIgnoreCase));
+
+ if (seriesTimer != null)
+ {
+ program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(serviceName, seriesTimer.Id)
+ .ToString("N");
+ }
}
}
@@ -1267,14 +1266,96 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var start = DateTime.UtcNow.AddHours(-1);
var end = start.AddDays(guideDays);
+ var isMovie = false;
+ var isSports = false;
+ var isNews = false;
+ var isKids = false;
+ var iSSeries = false;
+
var channelPrograms = await service.GetProgramsAsync(currentChannel.ExternalId, start, end, cancellationToken).ConfigureAwait(false);
+ var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+
+ IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+ ChannelIds = new string[] { currentChannel.Id.ToString("N") }
+
+ }).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
+
+ var newPrograms = new List<LiveTvProgram>();
+ var updatedPrograms = new List<LiveTvProgram>();
+
foreach (var program in channelPrograms)
{
- var programItem = await GetProgram(program, currentChannel, currentChannel.ChannelType, service.Name, cancellationToken).ConfigureAwait(false);
+ var programTuple = GetProgram(program, existingPrograms, currentChannel, currentChannel.ChannelType, service.Name, cancellationToken);
+ var programItem = programTuple.Item1;
+
+ if (programTuple.Item2)
+ {
+ newPrograms.Add(programItem);
+ }
+ else if (programTuple.Item3)
+ {
+ updatedPrograms.Add(programItem);
+ }
programs.Add(programItem.Id);
+
+ if (program.IsMovie)
+ {
+ isMovie = true;
+ }
+
+ if (program.IsSeries)
+ {
+ iSSeries = true;
+ }
+
+ if (program.IsSports)
+ {
+ isSports = true;
+ }
+
+ if (program.IsNews)
+ {
+ isNews = true;
+ }
+
+ if (program.IsKids)
+ {
+ isKids = true;
+ }
+ }
+
+ _logger.Debug("Channel {0} has {1} new programs and {2} updated programs", currentChannel.Name, newPrograms.Count, updatedPrograms.Count);
+
+ if (newPrograms.Count > 0)
+ {
+ await _libraryManager.CreateItems(newPrograms, cancellationToken).ConfigureAwait(false);
}
+
+ // TODO: Do this in bulk
+ foreach (var program in updatedPrograms)
+ {
+ await _libraryManager.UpdateItem(program, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
+ }
+
+ foreach (var program in newPrograms)
+ {
+ _providerManager.QueueRefresh(program.Id, new MetadataRefreshOptions(_fileSystem));
+ }
+ foreach (var program in updatedPrograms)
+ {
+ _providerManager.QueueRefresh(program.Id, new MetadataRefreshOptions(_fileSystem));
+ }
+
+ currentChannel.IsMovie = isMovie;
+ currentChannel.IsNews = isNews;
+ currentChannel.IsSports = isSports;
+ currentChannel.IsKids = isKids;
+ currentChannel.IsSeries = iSSeries;
+
+ await currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -1361,7 +1442,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
private DateTime _lastRecordingRefreshTime;
private async Task RefreshRecordings(CancellationToken cancellationToken)
{
- const int cacheMinutes = 5;
+ const int cacheMinutes = 3;
if ((DateTime.UtcNow - _lastRecordingRefreshTime).TotalMinutes < cacheMinutes)
{
@@ -1409,9 +1490,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}
}
- private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, User user)
+ private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, DtoOptions dtoOptions, User user)
{
- if (user == null || (query.IsInProgress ?? false))
+ if (user == null)
+ {
+ return new QueryResult<BaseItem>();
+ }
+
+ if ((query.IsInProgress ?? false))
{
return new QueryResult<BaseItem>();
}
@@ -1485,7 +1571,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
EnableTotalRecordCount = query.EnableTotalRecordCount,
IncludeItemTypes = includeItemTypes.ToArray(),
ExcludeItemTypes = excludeItemTypes.ToArray(),
- Genres = genres.ToArray()
+ Genres = genres.ToArray(),
+ DtoOptions = dtoOptions
});
}
@@ -1556,9 +1643,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return new QueryResult<BaseItem>();
}
- if (_services.Count == 1)
+ if (_services.Count == 1 && !(query.IsInProgress ?? false))
{
- return GetEmbyRecordings(query, user);
+ return GetEmbyRecordings(query, new DtoOptions(), user);
}
await RefreshRecordings(cancellationToken).ConfigureAwait(false);
@@ -1609,6 +1696,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
recordings = recordings.Where(i => i.IsMovie == val);
}
+ if (query.IsNews.HasValue)
+ {
+ var val = query.IsNews.Value;
+ recordings = recordings.Where(i => i.IsNews == val);
+ }
+
if (query.IsSeries.HasValue)
{
var val = query.IsSeries.Value;
@@ -1659,7 +1752,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
public async Task AddInfoToProgramDto(List<Tuple<BaseItem, BaseItemDto>> tuples, List<ItemFields> fields, User user = null)
{
- var recordingTuples = new List<Tuple<BaseItemDto, string, string>>();
+ var recordingTuples = new List<Tuple<BaseItemDto, string, string, string>>();
foreach (var tuple in tuples)
{
@@ -1727,7 +1820,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
dto.ServiceName = serviceName;
}
- recordingTuples.Add(new Tuple<BaseItemDto, string, string>(dto, serviceName, program.ExternalId));
+ recordingTuples.Add(new Tuple<BaseItemDto, string, string, string>(dto, serviceName, program.ExternalId, program.ExternalSeriesIdLegacy));
}
await AddRecordingInfo(recordingTuples, CancellationToken.None).ConfigureAwait(false);
@@ -1746,6 +1839,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv
? null
: _tvDtoService.GetInternalSeriesTimerId(service.Name, info.SeriesTimerId).ToString("N");
+ dto.TimerId = string.IsNullOrEmpty(info.TimerId)
+ ? null
+ : _tvDtoService.GetInternalTimerId(service.Name, info.TimerId).ToString("N");
+
dto.StartDate = info.StartDate;
dto.RecordingStatus = info.Status;
dto.IsRepeat = info.IsRepeat;
@@ -1842,6 +1939,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}
}
+ if (query.IsScheduled.HasValue)
+ {
+ if (query.IsScheduled.Value)
+ {
+ timers = timers.Where(i => i.Item1.Status == RecordingStatus.New);
+ }
+ else
+ {
+ timers = timers.Where(i => !(i.Item1.Status == RecordingStatus.New));
+ }
+ }
+
if (!string.IsNullOrEmpty(query.ChannelId))
{
var guid = new Guid(query.ChannelId);
@@ -2002,6 +2111,56 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
}
+ private async Task<QueryResult<SeriesTimerInfo>> GetSeriesTimersInternal(SeriesTimerQuery query, CancellationToken cancellationToken)
+ {
+ var tasks = _services.Select(async i =>
+ {
+ try
+ {
+ var recs = await i.GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
+ return recs.Select(r =>
+ {
+ r.ServiceName = i.Name;
+ return new Tuple<SeriesTimerInfo, ILiveTvService>(r, i);
+ });
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting recordings", ex);
+ return new List<Tuple<SeriesTimerInfo, ILiveTvService>>();
+ }
+ });
+ var results = await Task.WhenAll(tasks).ConfigureAwait(false);
+ var timers = results.SelectMany(i => i.ToList());
+
+ if (string.Equals(query.SortBy, "Priority", StringComparison.OrdinalIgnoreCase))
+ {
+ timers = query.SortOrder == SortOrder.Descending ?
+ timers.OrderBy(i => i.Item1.Priority).ThenByStringDescending(i => i.Item1.Name) :
+ timers.OrderByDescending(i => i.Item1.Priority).ThenByString(i => i.Item1.Name);
+ }
+ else
+ {
+ timers = query.SortOrder == SortOrder.Descending ?
+ timers.OrderByStringDescending(i => i.Item1.Name) :
+ timers.OrderByString(i => i.Item1.Name);
+ }
+
+ var returnArray = timers
+ .Select(i =>
+ {
+ return i.Item1;
+
+ })
+ .ToArray();
+
+ return new QueryResult<SeriesTimerInfo>
+ {
+ Items = returnArray,
+ TotalRecordCount = returnArray.Length
+ };
+ }
+
public async Task<QueryResult<SeriesTimerInfoDto>> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken)
{
var tasks = _services.Select(async i =>
@@ -2146,6 +2305,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false);
+ info.RecordAnyChannel = true;
+ info.RecordAnyTime = true;
+ info.Days = new List<DayOfWeek>
+ {
+ DayOfWeek.Sunday,
+ DayOfWeek.Monday,
+ DayOfWeek.Tuesday,
+ DayOfWeek.Wednesday,
+ DayOfWeek.Thursday,
+ DayOfWeek.Friday,
+ DayOfWeek.Saturday
+ };
+
info.Id = null;
return new Tuple<SeriesTimerInfo, ILiveTvService>(info, service);
@@ -2399,47 +2571,22 @@ namespace MediaBrowser.Server.Implementations.LiveTv
};
}
- class LiveStreamData
+ public async Task CloseLiveStream(string id)
{
- internal MediaSourceInfo Info;
- internal string ItemId;
- internal bool IsChannel;
- }
+ var parts = id.Split(new[] { '_' }, 2);
- public async Task CloseLiveStream(string id, CancellationToken cancellationToken)
- {
- await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+ var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), parts[0], StringComparison.OrdinalIgnoreCase));
- try
+ if (service == null)
{
- var parts = id.Split(new[] { '_' }, 2);
-
- var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), parts[0], StringComparison.OrdinalIgnoreCase));
-
- if (service == null)
- {
- throw new ArgumentException("Service not found.");
- }
-
- id = parts[1];
-
- LiveStreamData data;
- _openStreams.TryRemove(id, out data);
+ throw new ArgumentException("Service not found.");
+ }
- _logger.Info("Closing live stream from {0}, stream Id: {1}", service.Name, id);
+ id = parts[1];
- await service.CloseLiveStream(id, cancellationToken).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.ErrorException("Error closing live stream", ex);
+ _logger.Info("Closing live stream from {0}, stream Id: {1}", service.Name, id);
- throw;
- }
- finally
- {
- _liveStreamSemaphore.Release();
- }
+ await service.CloseLiveStream(id, CancellationToken.None).ConfigureAwait(false);
}
public GuideInfo GetGuideInfo()
@@ -2462,7 +2609,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
Dispose(true);
}
- private readonly object _disposeLock = new object();
private bool _isDisposed = false;
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
@@ -2473,18 +2619,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
if (dispose)
{
_isDisposed = true;
-
- lock (_disposeLock)
- {
- foreach (var stream in _openStreams.Values.ToList())
- {
- var task = CloseLiveStream(stream.Info.Id, CancellationToken.None);
-
- Task.WaitAll(task);
- }
-
- _openStreams.Clear();
- }
}
}
@@ -2620,7 +2754,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
{
- info = (TunerHostInfo)_jsonSerializer.DeserializeFromString(_jsonSerializer.SerializeToString(info), typeof(TunerHostInfo));
+ info = _jsonSerializer.DeserializeFromString<TunerHostInfo>(_jsonSerializer.SerializeToString(info));
var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
@@ -2661,7 +2795,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings)
{
- info = (ListingsProviderInfo)_jsonSerializer.DeserializeFromString(_jsonSerializer.SerializeToString(info), typeof(ListingsProviderInfo));
+ info = _jsonSerializer.DeserializeFromString< ListingsProviderInfo>(_jsonSerializer.SerializeToString(info));
var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
@@ -2803,7 +2937,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
feature = "embytvseriesrecordings";
}
- if (string.Equals(feature, "dvr", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(feature, "dvr-l", StringComparison.OrdinalIgnoreCase))
{
var config = GetConfiguration();
if (config.TunerHosts.Count(i => i.IsEnabled) > 0 &&
diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
index cdba1873e..a62796036 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
@@ -9,9 +9,11 @@ using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Model.Dlna;
namespace MediaBrowser.Server.Implementations.LiveTv
{
@@ -63,12 +65,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
if (item is ILiveTvRecording)
{
- sources = await _liveTvManager.GetRecordingMediaSources(item.Id.ToString("N"), cancellationToken)
+ sources = await _liveTvManager.GetRecordingMediaSources(item, cancellationToken)
.ConfigureAwait(false);
}
else
{
- sources = await _liveTvManager.GetChannelMediaSources(item.Id.ToString("N"), cancellationToken)
+ sources = await _liveTvManager.GetChannelMediaSources(item, cancellationToken)
.ConfigureAwait(false);
}
}
@@ -116,17 +118,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return list;
}
- public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
+ public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken)
{
- MediaSourceInfo stream;
+ MediaSourceInfo stream = null;
const bool isAudio = false;
var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
+ IDirectStreamProvider directStreamProvider = null;
if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
{
- stream = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false);
+ var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false);
+ stream = info.Item1;
+ directStreamProvider = info.Item2;
}
else
{
@@ -135,14 +140,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv
try
{
- await AddMediaInfo(stream, isAudio, cancellationToken).ConfigureAwait(false);
+ if (stream.MediaStreams.Any(i => i.Index != -1))
+ {
+ await AddMediaInfo(stream, isAudio, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false);
+ }
}
catch (Exception ex)
{
_logger.ErrorException("Error probing live tv stream", ex);
}
- return stream;
+ return new Tuple<MediaSourceInfo, IDirectStreamProvider>(stream, directStreamProvider);
}
private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
@@ -204,9 +216,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}
}
- public Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken)
+ public Task CloseMediaSource(string liveStreamId)
{
- return _liveTvManager.CloseLiveStream(liveStreamId, cancellationToken);
+ return _liveTvManager.CloseLiveStream(liveStreamId);
}
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 9bb5b4fd7..0fe74798f 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -6,9 +6,11 @@ using MediaBrowser.Model.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+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;
@@ -17,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;
@@ -25,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;
@@ -71,7 +73,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
.ToList();
}
- public async Task<IEnumerable<ChannelInfo>> GetChannels(CancellationToken cancellationToken)
+ public async Task<IEnumerable<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken)
{
var list = new List<ChannelInfo>();
@@ -81,7 +83,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
{
try
{
- var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
+ var channels = await GetChannels(host, enableCache, cancellationToken).ConfigureAwait(false);
var newChannels = channels.Where(i => !list.Any(l => string.Equals(i.Id, l.Id, StringComparison.OrdinalIgnoreCase))).ToList();
list.AddRange(newChannels);
@@ -124,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
@@ -155,89 +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;
+ }
+ }
+
+ foreach (var host in hostsWithChannel)
+ {
+ 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();
@@ -263,117 +233,6 @@ 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)
- {
- 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;
-
- var info = await MediaEncoder.GetMediaInfo(new MediaInfoRequest
- {
- InputPath = mediaSource.Path,
- Protocol = mediaSource.Protocol,
- MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
- ExtractChapters = false
-
- }, cancellationToken).ConfigureAwait(false);
-
- mediaSource.Bitrate = info.Bitrate;
- mediaSource.Container = info.Container;
- mediaSource.Formats = info.Formats;
- mediaSource.MediaStreams = info.MediaStreams;
- mediaSource.RunTimeTicks = info.RunTimeTicks;
- mediaSource.Size = info.Size;
- mediaSource.Timestamp = info.Timestamp;
- mediaSource.Video3DFormat = info.Video3DFormat;
- mediaSource.VideoType = info.VideoType;
-
- mediaSource.DefaultSubtitleStreamIndex = null;
-
- // Null this out so that it will be treated like a live stream
- if (!originalRuntime.HasValue)
- {
- mediaSource.RunTimeTicks = null;
- }
-
- var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Audio);
-
- if (audioStream == null || audioStream.Index == -1)
- {
- mediaSource.DefaultAudioStreamIndex = null;
- }
- else
- {
- mediaSource.DefaultAudioStreamIndex = audioStream.Index;
- }
-
- var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Video);
- if (videoStream != null)
- {
- if (!videoStream.BitRate.HasValue)
- {
- var width = videoStream.Width ?? 1920;
-
- if (width >= 1900)
- {
- videoStream.BitRate = 8000000;
- }
-
- else if (width >= 1260)
- {
- videoStream.BitRate = 3000000;
- }
-
- else if (width >= 700)
- {
- videoStream.BitRate = 1000000;
- }
- }
- }
-
- // Try to estimate this
- if (!mediaSource.Bitrate.HasValue)
- {
- var total = mediaSource.MediaStreams.Select(i => i.BitRate ?? 0).Sum();
-
- if (total > 0)
- {
- mediaSource.Bitrate = total;
- }
- }
- }
-
protected abstract bool IsValidChannelId(string channelId);
protected LiveTvOptions GetConfiguration()
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs
index 9ba1c60cc..cd168ba58 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs
@@ -10,6 +10,7 @@ using System;
using System.Linq;
using System.Threading;
using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Events;
using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@@ -39,13 +40,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered;
}
- void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e)
+ void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
{
string server = null;
- if (e.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1)
+ var info = e.Argument;
+
+ if (info.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1)
{
string location;
- if (e.Headers.TryGetValue("Location", out location))
+ if (info.Headers.TryGetValue("Location", out location))
{
//_logger.Debug("HdHomerun found at {0}", location);
@@ -85,7 +88,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
using (var stream = await _httpClient.Get(new HttpRequestOptions
{
Url = string.Format("{0}/discover.json", url),
- CancellationToken = CancellationToken.None
+ CancellationToken = CancellationToken.None,
+ BufferContent = false
}))
{
var response = _json.DeserializeFromStream<HdHomerunHost.DiscoverResponse>(stream);
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index fd4775938..97d52836d 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
@@ -60,20 +67,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return id;
}
- public string ApplyDuration(string streamPath, TimeSpan duration)
- {
- streamPath += streamPath.IndexOf('?') == -1 ? "?" : "&";
- streamPath += "duration=" + Convert.ToInt32(duration.TotalSeconds).ToString(CultureInfo.InvariantCulture);
-
- return streamPath;
- }
-
private async Task<IEnumerable<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
{
var options = new HttpRequestOptions
{
Url = string.Format("{0}/lineup.json", GetApiUrl(info, false)),
- CancellationToken = cancellationToken
+ CancellationToken = cancellationToken,
+ BufferContent = false
};
using (var stream = await _httpClient.Get(options))
{
@@ -105,8 +105,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
});
}
+ private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
private async Task<string> GetModelInfo(TunerHostInfo info, CancellationToken cancellationToken)
{
+ lock (_modelCache)
+ {
+ DiscoverResponse response;
+ if (_modelCache.TryGetValue(info.Url, out response))
+ {
+ return response.ModelNumber;
+ }
+ }
+
try
{
using (var stream = await _httpClient.Get(new HttpRequestOptions()
@@ -115,11 +125,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
CancellationToken = cancellationToken,
CacheLength = TimeSpan.FromDays(1),
CacheMode = CacheMode.Unconditional,
- TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds)
+ TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
+ BufferContent = false
}))
{
var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
+ lock (_modelCache)
+ {
+ _modelCache[info.Id] = response;
+ }
+
return response.ModelNumber;
}
}
@@ -127,8 +143,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
{
+ var defaultValue = "HDHR";
// HDHR4 doesn't have this api
- return "HDHR";
+ lock (_modelCache)
+ {
+ _modelCache[info.Id] = new DiscoverResponse
+ {
+ ModelNumber = defaultValue
+ };
+ }
+ return defaultValue;
}
throw;
@@ -143,7 +167,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
Url = string.Format("{0}/tuners.html", GetApiUrl(info, false)),
CancellationToken = cancellationToken,
- TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds)
+ TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
+ BufferContent = false
}))
{
var tuners = new List<LiveTvTunerInfo>();
@@ -319,18 +344,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
videoBitrate = 1000000;
}
- if (string.IsNullOrWhiteSpace(videoCodec))
+ var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
+ var channel = channels.FirstOrDefault(i => string.Equals(i.Number, channelId, StringComparison.OrdinalIgnoreCase));
+ if (channel != null)
{
- var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
- var channel = channels.FirstOrDefault(i => string.Equals(i.Number, channelId, StringComparison.OrdinalIgnoreCase));
- if (channel != null)
+ if (string.IsNullOrWhiteSpace(videoCodec))
{
videoCodec = channel.VideoCodec;
- audioCodec = channel.AudioCodec;
+ }
+ audioCodec = channel.AudioCodec;
+ if (!videoBitrate.HasValue)
+ {
videoBitrate = (channel.IsHD ?? true) ? 15000000 : 2000000;
- audioBitrate = (channel.IsHD ?? true) ? 448000 : 192000;
}
+ audioBitrate = (channel.IsHD ?? true) ? 448000 : 192000;
}
// normalize
@@ -352,6 +380,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,
@@ -380,14 +415,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
BitRate = audioBitrate
}
},
- RequiresOpening = false,
+ RequiresOpening = true,
RequiresClosing = false,
BufferMs = 0,
Container = "ts",
- Id = profile,
- SupportsDirectPlay = true,
- SupportsDirectStream = false,
- SupportsTranscoding = true
+ Id = id,
+ SupportsDirectPlay = false,
+ SupportsDirectStream = true,
+ SupportsTranscoding = true,
+ IsInfiniteStream = true
};
return mediaSource;
@@ -417,18 +453,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
try
{
- string model = await GetModelInfo(info, cancellationToken).ConfigureAwait(false);
- model = model ?? string.Empty;
-
- if (info.AllowHWTranscoding && (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1))
+ if (info.AllowHWTranscoding)
{
- list.Add(await GetMediaSource(info, hdhrId, "heavy").ConfigureAwait(false));
+ string model = await GetModelInfo(info, cancellationToken).ConfigureAwait(false);
+ model = model ?? string.Empty;
- list.Add(await GetMediaSource(info, hdhrId, "internet540").ConfigureAwait(false));
- list.Add(await GetMediaSource(info, hdhrId, "internet480").ConfigureAwait(false));
- list.Add(await GetMediaSource(info, hdhrId, "internet360").ConfigureAwait(false));
- list.Add(await GetMediaSource(info, hdhrId, "internet240").ConfigureAwait(false));
- list.Add(await GetMediaSource(info, hdhrId, "mobile").ConfigureAwait(false));
+ if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1))
+ {
+ list.Add(await GetMediaSource(info, hdhrId, "heavy").ConfigureAwait(false));
+
+ list.Add(await GetMediaSource(info, hdhrId, "internet540").ConfigureAwait(false));
+ list.Add(await GetMediaSource(info, hdhrId, "internet480").ConfigureAwait(false));
+ list.Add(await GetMediaSource(info, hdhrId, "internet360").ConfigureAwait(false));
+ list.Add(await GetMediaSource(info, hdhrId, "internet240").ConfigureAwait(false));
+ list.Add(await GetMediaSource(info, hdhrId, "mobile").ConfigureAwait(false));
+ }
}
}
catch
@@ -449,9 +488,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))
{
@@ -459,7 +500,11 @@ 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, streamId, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost);
+ liveStream.EnableStreamSharing = true;
+ return liveStream;
}
public async Task Validate(TunerHostInfo info)
@@ -469,13 +514,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return;
}
+ lock (_modelCache)
+ {
+ _modelCache.Clear();
+ }
+
try
{
// Test it by pulling down the lineup
using (var stream = await _httpClient.Get(new HttpRequestOptions
{
Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
- CancellationToken = CancellationToken.None
+ CancellationToken = CancellationToken.None,
+ BufferContent = false
}))
{
var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
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..60222415c
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs
@@ -0,0 +1,145 @@
+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.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Common.Extensions;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ public class HdHomerunLiveStream : LiveStream, IDirectStreamProvider
+ {
+ 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();
+ private readonly TaskCompletionSource<bool> _liveStreamTaskCompletionSource = new TaskCompletionSource<bool>();
+ private readonly MulticastStream _multicastStream;
+
+
+ public HdHomerunLiveStream(MediaSourceInfo mediaSource, string originalStreamId, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost)
+ : base(mediaSource)
+ {
+ _fileSystem = fileSystem;
+ _httpClient = httpClient;
+ _logger = logger;
+ _appPaths = appPaths;
+ _appHost = appHost;
+ OriginalStreamId = originalStreamId;
+ _multicastStream = new MulticastStream(_logger);
+ }
+
+ protected override async Task OpenInternal(CancellationToken openCancellationToken)
+ {
+ _liveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested();
+
+ var mediaSource = OriginalMediaSource;
+
+ var url = mediaSource.Path;
+
+ _logger.Info("Opening HDHR Live stream from {0}", url);
+
+ var taskCompletionSource = new TaskCompletionSource<bool>();
+
+ StartStreaming(url, taskCompletionSource, _liveStreamCancellationTokenSource.Token);
+
+ //OpenedMediaSource.Protocol = MediaProtocol.File;
+ //OpenedMediaSource.Path = tempFile;
+ //OpenedMediaSource.ReadAtNativeFramerate = true;
+
+ OpenedMediaSource.Path = _appHost.GetLocalApiUrl("localhost") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
+ OpenedMediaSource.Protocol = MediaProtocol.Http;
+ OpenedMediaSource.SupportsDirectPlay = false;
+ OpenedMediaSource.SupportsDirectStream = true;
+ OpenedMediaSource.SupportsTranscoding = true;
+
+ await taskCompletionSource.Task.ConfigureAwait(false);
+
+ //await Task.Delay(5000).ConfigureAwait(false);
+ }
+
+ public override Task Close()
+ {
+ _logger.Info("Closing HDHR live stream");
+ _liveStreamCancellationTokenSource.Cancel();
+
+ return _liveStreamTaskCompletionSource.Task;
+ }
+
+ private async Task StartStreaming(string url, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
+ {
+ await Task.Run(async () =>
+ {
+ 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 multicastStream.CopyUntilCancelled");
+
+ Action onStarted = null;
+ if (isFirstAttempt)
+ {
+ onStarted = () => openTaskCompletionSource.TrySetResult(true);
+ }
+
+ await _multicastStream.CopyUntilCancelled(response.Content, 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;
+ }
+
+ _liveStreamTaskCompletionSource.TrySetResult(true);
+
+ }).ConfigureAwait(false);
+ }
+
+ public Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
+ {
+ return _multicastStream.CopyToAsync(stream);
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 5c508aacd..b03feefe4 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,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
RequiresOpening = false,
RequiresClosing = false,
- ReadAtNativeFramerate = false
+ ReadAtNativeFramerate = false,
+
+ Id = channel.Path.GetMD5().ToString("N"),
+ IsInfiniteStream = true
};
return new List<MediaSourceInfo> { mediaSource };
@@ -148,10 +154,5 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
{
return Task.FromResult(true);
}
-
- public string ApplyDuration(string streamPath, TimeSpan duration)
- {
- return streamPath;
- }
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs
new file mode 100644
index 000000000..8ff3fd6c1
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Logging;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
+{
+ public class MulticastStream
+ {
+ private readonly List<QueueStream> _outputStreams = new List<QueueStream>();
+ private const int BufferSize = 81920;
+ private CancellationToken _cancellationToken;
+ private readonly ILogger _logger;
+
+ public MulticastStream(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public async Task CopyUntilCancelled(Stream source, Action onStarted, CancellationToken cancellationToken)
+ {
+ _cancellationToken = cancellationToken;
+
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ byte[] buffer = new byte[BufferSize];
+
+ var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+
+ if (bytesRead > 0)
+ {
+ byte[] copy = new byte[bytesRead];
+ Buffer.BlockCopy(buffer, 0, copy, 0, bytesRead);
+
+ List<QueueStream> streams = null;
+
+ lock (_outputStreams)
+ {
+ streams = _outputStreams.ToList();
+ }
+
+ foreach (var stream in streams)
+ {
+ stream.Queue(copy);
+ }
+
+ if (onStarted != null)
+ {
+ var onStartedCopy = onStarted;
+ onStarted = null;
+ Task.Run(onStartedCopy);
+ }
+ }
+
+ else
+ {
+ await Task.Delay(100).ConfigureAwait(false);
+ }
+ }
+ }
+
+ public Task CopyToAsync(Stream stream)
+ {
+ var result = new QueueStream(stream, _logger)
+ {
+ OnFinished = OnFinished
+ };
+
+ lock (_outputStreams)
+ {
+ _outputStreams.Add(result);
+ }
+
+ result.Start(_cancellationToken);
+
+ return result.TaskCompletion.Task;
+ }
+
+ public void RemoveOutputStream(QueueStream stream)
+ {
+ lock (_outputStreams)
+ {
+ _outputStreams.Remove(stream);
+ }
+ }
+
+ private void OnFinished(QueueStream queueStream)
+ {
+ RemoveOutputStream(queueStream);
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs
new file mode 100644
index 000000000..c1566b900
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Logging;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
+{
+ public class QueueStream
+ {
+ private readonly Stream _outputStream;
+ private readonly ConcurrentQueue<byte[]> _queue = new ConcurrentQueue<byte[]>();
+ private CancellationToken _cancellationToken;
+ public TaskCompletionSource<bool> TaskCompletion { get; private set; }
+
+ public Action<QueueStream> OnFinished { get; set; }
+ private readonly ILogger _logger;
+
+ public QueueStream(Stream outputStream, ILogger logger)
+ {
+ _outputStream = outputStream;
+ _logger = logger;
+ TaskCompletion = new TaskCompletionSource<bool>();
+ }
+
+ public void Queue(byte[] bytes)
+ {
+ _queue.Enqueue(bytes);
+ }
+
+ public void Start(CancellationToken cancellationToken)
+ {
+ _cancellationToken = cancellationToken;
+ Task.Run(() => StartInternal());
+ }
+
+ private byte[] Dequeue()
+ {
+ byte[] bytes;
+ if (_queue.TryDequeue(out bytes))
+ {
+ return bytes;
+ }
+
+ return null;
+ }
+
+ private async Task StartInternal()
+ {
+ var cancellationToken = _cancellationToken;
+
+ try
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ var bytes = Dequeue();
+ if (bytes != null)
+ {
+ await _outputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ TaskCompletion.TrySetResult(true);
+ _logger.Debug("QueueStream complete");
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.Debug("QueueStream cancelled");
+ TaskCompletion.TrySetCanceled();
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in QueueStream", ex);
+ TaskCompletion.TrySetException(ex);
+ }
+ finally
+ {
+ if (OnFinished != null)
+ {
+ OnFinished(this);
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs
index cb0e573da..a0b8ef5f7 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs
@@ -14,6 +14,7 @@ using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Extensions;
using System.Xml.Linq;
+using MediaBrowser.Model.Events;
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
{
@@ -50,18 +51,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
_deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered;
}
- void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e)
+ void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
{
+ var info = e.Argument;
+
string st = null;
string nt = null;
- e.Headers.TryGetValue("ST", out st);
- e.Headers.TryGetValue("NT", out nt);
+ info.Headers.TryGetValue("ST", out st);
+ info.Headers.TryGetValue("NT", out nt);
if (string.Equals(st, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase) ||
string.Equals(nt, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase))
{
string location;
- if (e.Headers.TryGetValue("Location", out location) && !string.IsNullOrWhiteSpace(location))
+ if (info.Headers.TryGetValue("Location", out location) && !string.IsNullOrWhiteSpace(location))
{
_logger.Debug("SAT IP found at {0}", location);
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)