diff options
Diffstat (limited to 'MediaBrowser.Server.Implementations/LiveTv')
25 files changed, 2997 insertions, 580 deletions
diff --git a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs index 24d38a63e..23560b1aa 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs @@ -41,7 +41,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, liveTvItem.ServiceName, StringComparison.OrdinalIgnoreCase)); - if (service != null) + if (service != null && !item.HasImage(ImageType.Primary)) { try { @@ -77,7 +77,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv get { return 0; } } - public bool HasChanged(IHasMetadata item, MetadataStatus status, IDirectoryService directoryService) + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) { return GetSupportedImages(item).Any(i => !item.HasImage(i)); } diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index d33b2c51d..b21aa904b 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -23,6 +23,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _fileSystem = fileSystem; } + public string GetOutputPath(MediaSourceInfo mediaSource, string targetFile) + { + return targetFile; + } + public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { var httpRequestOptions = new HttpRequestOptions() @@ -40,14 +45,27 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { onStarted(); - _logger.Info("Copying recording stream to file stream"); + _logger.Info("Copying recording stream to file {0}", targetFile); - var durationToken = new CancellationTokenSource(duration); - var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + 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, linkedToken).ConfigureAwait(false); + await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); } } + + _logger.Info("Recording completed to file {0}", targetFile); } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 60ff23b04..8f56554f1 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -26,13 +26,16 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Power; using Microsoft.Win32; namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { - public class EmbyTV : ILiveTvService, IHasRegistrationInfo, IDisposable + public class EmbyTV : ILiveTvService, ISupportsNewTimerIds, IHasRegistrationInfo, IDisposable { private readonly IApplicationHost _appHpst; private readonly ILogger _logger; @@ -40,7 +43,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private readonly IServerConfigurationManager _config; private readonly IJsonSerializer _jsonSerializer; - private readonly ItemDataProvider<RecordingInfo> _recordingProvider; private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider; private readonly TimerManager _timerProvider; @@ -56,6 +58,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV public static EmbyTV Current; + 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, ISecurityManager security, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder, IPowerManagement powerManagement) { Current = this; @@ -74,10 +82,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _liveTvManager = (LiveTvManager)liveTvManager; _jsonSerializer = jsonSerializer; - _recordingProvider = new ItemDataProvider<RecordingInfo>(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "recordings"), (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)); _seriesTimerProvider = new SeriesTimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers")); _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), powerManagement, _logger); _timerProvider.TimerFired += _timerProvider_TimerFired; + + _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; + } + + private void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) + { + if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase)) + { + OnRecordingFoldersChanged(); + } } public void Start() @@ -85,6 +102,124 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _timerProvider.RestartTimers(); SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; + CreateRecordingFolders(); + } + + private void OnRecordingFoldersChanged() + { + CreateRecordingFolders(); + } + + internal void CreateRecordingFolders() + { + try + { + CreateRecordingFoldersInternal(); + } + catch (Exception ex) + { + _logger.ErrorException("Error creating recording folders", ex); + } + } + + internal void CreateRecordingFoldersInternal() + { + var recordingFolders = GetRecordingFolders(); + + var virtualFolders = _libraryManager.GetVirtualFolders() + .ToList(); + + var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList(); + + var pathsAdded = new List<string>(); + + foreach (var recordingFolder in recordingFolders) + { + var pathsToCreate = recordingFolder.Locations + .Where(i => !allExistingPaths.Contains(i, StringComparer.OrdinalIgnoreCase)) + .ToList(); + + if (pathsToCreate.Count == 0) + { + continue; + } + + try + { + _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, pathsToCreate.ToArray(), true); + } + catch (Exception ex) + { + _logger.ErrorException("Error creating virtual folder", ex); + } + + pathsAdded.AddRange(pathsToCreate); + } + + var config = GetConfiguration(); + + var pathsToRemove = config.MediaLocationsCreated + .Except(recordingFolders.SelectMany(i => i.Locations)) + .ToList(); + + if (pathsAdded.Count > 0 || pathsToRemove.Count > 0) + { + pathsAdded.InsertRange(0, config.MediaLocationsCreated); + config.MediaLocationsCreated = pathsAdded.Except(pathsToRemove).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + _config.SaveConfiguration("livetv", config); + } + + foreach (var path in pathsToRemove) + { + RemovePathFromLibrary(path); + } + } + + private void RemovePathFromLibrary(string path) + { + _logger.Debug("Removing path from library: {0}", path); + + var requiresRefresh = false; + var virtualFolders = _libraryManager.GetVirtualFolders() + .ToList(); + + foreach (var virtualFolder in virtualFolders) + { + if (!virtualFolder.Locations.Contains(path, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + if (virtualFolder.Locations.Count == 1) + { + // remove entire virtual folder + try + { + _libraryManager.RemoveVirtualFolder(virtualFolder.Name, true); + } + catch (Exception ex) + { + _logger.ErrorException("Error removing virtual folder", ex); + } + } + else + { + try + { + _libraryManager.RemoveMediaPath(virtualFolder.Name, path); + requiresRefresh = true; + } + catch (Exception ex) + { + _logger.ErrorException("Error removing media path", ex); + } + } + } + + if (requiresRefresh) + { + _libraryManager.ValidateMediaLibrary(new Progress<Double>(), CancellationToken.None); + } } void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e) @@ -97,13 +232,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - public event EventHandler DataSourceChanged; - - public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged; - - private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings = - new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase); - public string Name { get { return "Emby"; } @@ -114,6 +242,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv"); } } + private string DefaultRecordingPath + { + get + { + return Path.Combine(DataPath, "recordings"); + } + } + + private string RecordingPath + { + get + { + var path = GetConfiguration().RecordingPath; + + return string.IsNullOrWhiteSpace(path) + ? DefaultRecordingPath + : path; + } + } + public string HomePageUrl { get { return "http://emby.media"; } @@ -234,6 +382,29 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return list; } + public async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo listingsProvider, CancellationToken cancellationToken) + { + var list = new List<ChannelInfo>(); + + foreach (var hostInstance in _liveTvManager.TunerHosts) + { + try + { + var channels = await hostInstance.GetChannels(cancellationToken).ConfigureAwait(false); + + list.AddRange(channels); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting channels", ex); + } + } + + return list + .Where(i => IsListingProviderEnabledForTuner(listingsProvider, i.TunerHostId)) + .ToList(); + } + public Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken) { return GetChannelsAsync(false, cancellationToken); @@ -280,59 +451,29 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return Task.FromResult(true); } - public async Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken) + public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken) { - var remove = _recordingProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, recordingId, StringComparison.OrdinalIgnoreCase)); - if (remove != null) - { - if (!string.IsNullOrWhiteSpace(remove.TimerId)) - { - var enableDelay = _activeRecordings.ContainsKey(remove.TimerId); - - CancelTimerInternal(remove.TimerId); - - if (enableDelay) - { - // A hack yes, but need to make sure the file is closed before attempting to delete it - await Task.Delay(3000, cancellationToken).ConfigureAwait(false); - } - } - - if (!string.IsNullOrWhiteSpace(remove.Path)) - { - try - { - _fileSystem.DeleteFile(remove.Path); - } - catch (DirectoryNotFoundException) - { + return Task.FromResult(true); + } - } - catch (FileNotFoundException) - { + public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken) + { + return CreateTimer(info, cancellationToken); + } - } - catch (Exception ex) - { - _logger.ErrorException("Error deleting recording file {0}", ex, remove.Path); - } - } - _recordingProvider.Delete(remove); - } - else - { - throw new ResourceNotFoundException("Recording not found: " + recordingId); - } + public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) + { + return CreateSeriesTimer(info, cancellationToken); } - public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken) + public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken) { info.Id = Guid.NewGuid().ToString("N"); _timerProvider.Add(info); - return Task.FromResult(0); + return Task.FromResult(info.Id); } - public async Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) + public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken) { info.Id = Guid.NewGuid().ToString("N"); @@ -362,6 +503,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _seriesTimerProvider.Add(info); await UpdateTimersForSeriesTimer(epgData, info, false).ConfigureAwait(false); + + return info.Id; } public async Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) @@ -424,29 +567,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV public async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken) { - var recordings = _recordingProvider.GetAll().ToList(); - var updated = false; - - foreach (var recording in recordings) - { - if (recording.Status == RecordingStatus.InProgress) - { - if (string.IsNullOrWhiteSpace(recording.TimerId) || !_activeRecordings.ContainsKey(recording.TimerId)) - { - recording.Status = RecordingStatus.Cancelled; - recording.DateLastUpdated = DateTime.UtcNow; - _recordingProvider.Update(recording); - updated = true; - } - } - } - - if (updated) - { - recordings = _recordingProvider.GetAll().ToList(); - } - - return recordings; + return new List<RecordingInfo>(); } public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken) @@ -462,9 +583,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { PostPaddingSeconds = Math.Max(config.PostPaddingSeconds, 0), PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0), - RecordAnyChannel = false, - RecordAnyTime = false, - RecordNewOnly = false + RecordAnyChannel = true, + RecordAnyTime = true, + RecordNewOnly = false, + + Days = new List<DayOfWeek> + { + DayOfWeek.Sunday, + DayOfWeek.Monday, + DayOfWeek.Tuesday, + DayOfWeek.Wednesday, + DayOfWeek.Thursday, + DayOfWeek.Friday, + DayOfWeek.Saturday + } }; if (program != null) @@ -528,7 +660,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _logger.Debug("Getting programs for channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty); - var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channel.Number, channel.Name, startDateUtc, endDateUtc, cancellationToken) + var channelMappings = GetChannelMappings(provider.Item2); + var channelNumber = channel.Number; + string mappedChannelNumber; + if (channelMappings.TryGetValue(channelNumber, out mappedChannelNumber)) + { + _logger.Debug("Found mapped channel on provider {0}. Tuner channel number: {1}, Mapped channel number: {2}", provider.Item1.Name, channelNumber, mappedChannelNumber); + channelNumber = mappedChannelNumber; + } + + var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channelNumber, channel.Name, startDateUtc, endDateUtc, cancellationToken) .ConfigureAwait(false); var list = programs.ToList(); @@ -550,6 +691,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return new List<ProgramInfo>(); } + private Dictionary<string, string> GetChannelMappings(ListingsProviderInfo info) + { + var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + foreach (var mapping in info.ChannelMappings) + { + dict[mapping.Name] = mapping.Value; + } + + return dict; + } + private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders() { return GetConfiguration().ListingProviders @@ -591,7 +744,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV throw new ApplicationException("Tuner not found."); } - private async Task<Tuple<MediaSourceInfo, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken) + private async Task<Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken) { _logger.Info("Streaming Channel " + channelId); @@ -599,7 +752,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { try { - return await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); + var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); + + return new Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>(result.Item1, hostInstance, result.Item2); } catch (Exception e) { @@ -693,6 +848,106 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } + private string GetRecordingPath(TimerInfo timer, ProgramInfo info) + { + var recordPath = RecordingPath; + var config = GetConfiguration(); + + if (info.IsMovie) + { + var customRecordingPath = config.MovieRecordingPath; + var allowSubfolder = true; + if (!string.IsNullOrWhiteSpace(customRecordingPath)) + { + allowSubfolder = string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase); + recordPath = customRecordingPath; + } + + if (allowSubfolder && config.EnableRecordingSubfolders) + { + recordPath = Path.Combine(recordPath, "Movies"); + } + + var folderName = _fileSystem.GetValidFilename(info.Name).Trim(); + if (info.ProductionYear.HasValue) + { + folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; + } + recordPath = Path.Combine(recordPath, folderName); + } + else if (info.IsSeries) + { + var customRecordingPath = config.SeriesRecordingPath; + var allowSubfolder = true; + if (!string.IsNullOrWhiteSpace(customRecordingPath)) + { + allowSubfolder = string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase); + recordPath = customRecordingPath; + } + + if (allowSubfolder && config.EnableRecordingSubfolders) + { + 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) + ")"; + } + + if (Directory.Exists(Path.Combine(recordPath, folderName))) + { + recordPath = Path.Combine(recordPath, folderName); + } + else + { + recordPath = Path.Combine(recordPath, folderNameWithYear); + } + + if (info.SeasonNumber.HasValue) + { + folderName = string.Format("Season {0}", info.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture)); + recordPath = Path.Combine(recordPath, folderName); + } + } + else if (info.IsKids) + { + if (config.EnableRecordingSubfolders) + { + recordPath = Path.Combine(recordPath, "Kids"); + } + + var folderName = _fileSystem.GetValidFilename(info.Name).Trim(); + if (info.ProductionYear.HasValue) + { + folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; + } + recordPath = Path.Combine(recordPath, folderName); + } + else if (info.IsSports) + { + if (config.EnableRecordingSubfolders) + { + recordPath = Path.Combine(recordPath, "Sports"); + } + recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim()); + } + else + { + if (config.EnableRecordingSubfolders) + { + recordPath = Path.Combine(recordPath, "Other"); + } + recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim()); + } + + var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts"; + + return Path.Combine(recordPath, recordingFileName); + } + private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken) { if (timer == null) @@ -722,161 +977,102 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV throw new InvalidOperationException(string.Format("Program with Id {0} not found", timer.ProgramId)); } - var recordPath = RecordingPath; - - if (info.IsMovie) - { - recordPath = Path.Combine(recordPath, "Movies", _fileSystem.GetValidFilename(info.Name).Trim()); - } - else if (info.IsSeries) - { - recordPath = Path.Combine(recordPath, "Series", _fileSystem.GetValidFilename(info.Name).Trim()); - } - else if (info.IsKids) - { - recordPath = Path.Combine(recordPath, "Kids", _fileSystem.GetValidFilename(info.Name).Trim()); - } - else if (info.IsSports) - { - recordPath = Path.Combine(recordPath, "Sports", _fileSystem.GetValidFilename(info.Name).Trim()); - } - else - { - recordPath = Path.Combine(recordPath, "Other", _fileSystem.GetValidFilename(info.Name).Trim()); - } - - var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts"; - - recordPath = Path.Combine(recordPath, recordingFileName); - - var recordingId = info.Id.GetMD5().ToString("N"); - var recording = _recordingProvider.GetAll().FirstOrDefault(x => string.Equals(x.Id, recordingId, StringComparison.OrdinalIgnoreCase)); - - if (recording == null) - { - recording = new RecordingInfo - { - ChannelId = info.ChannelId, - Id = recordingId, - StartDate = info.StartDate, - EndDate = info.EndDate, - Genres = info.Genres, - IsKids = info.IsKids, - IsLive = info.IsLive, - IsMovie = info.IsMovie, - IsHD = info.IsHD, - IsNews = info.IsNews, - IsPremiere = info.IsPremiere, - IsSeries = info.IsSeries, - IsSports = info.IsSports, - IsRepeat = !info.IsPremiere, - Name = info.Name, - EpisodeTitle = info.EpisodeTitle, - ProgramId = info.Id, - ImagePath = info.ImagePath, - ImageUrl = info.ImageUrl, - OriginalAirDate = info.OriginalAirDate, - Status = RecordingStatus.Scheduled, - Overview = info.Overview, - SeriesTimerId = timer.SeriesTimerId, - TimerId = timer.Id, - ShowId = info.ShowId - }; - _recordingProvider.AddOrUpdate(recording); - } + var recordPath = GetRecordingPath(timer, info); + var recordingStatus = RecordingStatus.New; + var isResourceOpen = false; + SemaphoreSlim semaphore = null; try { var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false); + isResourceOpen = true; + semaphore = result.Item3; var mediaStreamInfo = result.Item1; - var isResourceOpen = true; - // Unfortunately due to the semaphore we have to have a nested try/finally - try - { - // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg - //await Task.Delay(3000, cancellationToken).ConfigureAwait(false); + // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg + //await Task.Delay(3000, cancellationToken).ConfigureAwait(false); - var duration = recordingEndDate - DateTime.UtcNow; + var recorder = await GetRecorder().ConfigureAwait(false); - var recorder = await GetRecorder().ConfigureAwait(false); + recordPath = recorder.GetOutputPath(mediaStreamInfo, recordPath); + recordPath = EnsureFileUnique(recordPath, timer.Id); - if (recorder is EncodedRecorder) - { - recordPath = Path.ChangeExtension(recordPath, ".mp4"); - } - recordPath = EnsureFileUnique(recordPath, timer.Id); - _fileSystem.CreateDirectory(Path.GetDirectoryName(recordPath)); - activeRecordingInfo.Path = recordPath; + _libraryMonitor.ReportFileSystemChangeBeginning(recordPath); + _fileSystem.CreateDirectory(Path.GetDirectoryName(recordPath)); + activeRecordingInfo.Path = recordPath; - _libraryMonitor.ReportFileSystemChangeBeginning(recordPath); + var duration = recordingEndDate - DateTime.UtcNow; - recording.Path = recordPath; - recording.Status = RecordingStatus.InProgress; - recording.DateLastUpdated = DateTime.UtcNow; - _recordingProvider.AddOrUpdate(recording); + _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"); - _logger.Info("Writing file to path: " + recordPath); - _logger.Info("Opening recording stream from tuner provider"); + Action onStarted = () => + { + timer.Status = RecordingStatus.InProgress; + _timerProvider.AddOrUpdate(timer, false); - Action onStarted = () => - { - result.Item2.Release(); - isResourceOpen = false; - }; + result.Item3.Release(); + isResourceOpen = false; + }; - await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken).ConfigureAwait(false); + var pathWithDuration = result.Item2.ApplyDuration(mediaStreamInfo.Path, duration); - recording.Status = RecordingStatus.Completed; - _logger.Info("Recording completed: {0}", recordPath); - } - finally + // If it supports supplying duration via url + if (!string.Equals(pathWithDuration, mediaStreamInfo.Path, StringComparison.OrdinalIgnoreCase)) { - if (isResourceOpen) - { - result.Item2.Release(); - } - - _libraryMonitor.ReportFileSystemChangeComplete(recordPath, false); + mediaStreamInfo.Path = pathWithDuration; + mediaStreamInfo.RunTimeTicks = duration.Ticks; } + + await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken).ConfigureAwait(false); + + recordingStatus = RecordingStatus.Completed; + _logger.Info("Recording completed: {0}", recordPath); } catch (OperationCanceledException) { _logger.Info("Recording stopped: {0}", recordPath); - recording.Status = RecordingStatus.Completed; + recordingStatus = RecordingStatus.Completed; } catch (Exception ex) { _logger.ErrorException("Error recording to {0}", ex, recordPath); - recording.Status = RecordingStatus.Error; + recordingStatus = RecordingStatus.Error; } finally { + if (isResourceOpen && semaphore != null) + { + semaphore.Release(); + } + + _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true); + ActiveRecordingInfo removed; _activeRecordings.TryRemove(timer.Id, out removed); } - recording.DateLastUpdated = DateTime.UtcNow; - _recordingProvider.AddOrUpdate(recording); - - if (recording.Status == RecordingStatus.Completed) + if (recordingStatus == RecordingStatus.Completed) { - OnSuccessfulRecording(recording); + timer.Status = RecordingStatus.Completed; _timerProvider.Delete(timer); + + OnSuccessfulRecording(info.IsSeries, recordPath); } else if (DateTime.UtcNow < timer.EndDate) { const int retryIntervalSeconds = 60; _logger.Info("Retrying recording in {0} seconds.", retryIntervalSeconds); - _timerProvider.StartTimer(timer, TimeSpan.FromSeconds(retryIntervalSeconds)); + timer.Status = RecordingStatus.New; + timer.StartDate = DateTime.UtcNow.AddSeconds(retryIntervalSeconds); + _timerProvider.AddOrUpdate(timer); } else { _timerProvider.Delete(timer); - _recordingProvider.Delete(recording); } } @@ -916,24 +1112,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private async Task<IRecorder> GetRecorder() { - if (GetConfiguration().EnableRecordingEncoding) + var config = GetConfiguration(); + + if (config.EnableRecordingEncoding) { var regInfo = await _security.GetRegistrationStatus("embytvrecordingconversion").ConfigureAwait(false); if (regInfo.IsValid) { - return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer); + return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, config, _httpClient); } } return new DirectRecorder(_logger, _httpClient, _fileSystem); } - private async void OnSuccessfulRecording(RecordingInfo recording) + private async void OnSuccessfulRecording(bool isSeries, string path) { if (GetConfiguration().EnableAutoOrganize) { - if (recording.IsSeries) + if (isSeries) { try { @@ -943,12 +1141,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager); - var result = await organize.OrganizeEpisodeFile(recording.Path, CancellationToken.None).ConfigureAwait(false); - - if (result.Status == FileSortingStatus.Success) - { - _recordingProvider.Delete(recording); - } + var result = await organize.OrganizeEpisodeFile(path, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { @@ -972,18 +1165,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return epgData.FirstOrDefault(p => Math.Abs(startDateTicks - p.StartDate.Ticks) <= TimeSpan.FromMinutes(3).Ticks); } - private string RecordingPath - { - get - { - var path = GetConfiguration().RecordingPath; - - return string.IsNullOrWhiteSpace(path) - ? Path.Combine(DataPath, "recordings") - : path; - } - } - private LiveTvOptions GetConfiguration() { return _config.GetConfiguration<LiveTvOptions>("livetv"); @@ -991,7 +1172,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private async Task UpdateTimersForSeriesTimer(List<ProgramInfo> epgData, SeriesTimerInfo seriesTimer, bool deleteInvalidTimers) { - var newTimers = GetTimersForSeries(seriesTimer, epgData, _recordingProvider.GetAll()).ToList(); + var newTimers = GetTimersForSeries(seriesTimer, epgData, true).ToList(); var registration = await GetRegistrationInfo("seriesrecordings").ConfigureAwait(false); @@ -1005,7 +1186,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV if (deleteInvalidTimers) { - var allTimers = GetTimersForSeries(seriesTimer, epgData, new List<RecordingInfo>()) + var allTimers = GetTimersForSeries(seriesTimer, epgData, false) .Select(i => i.Id) .ToList(); @@ -1021,7 +1202,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms, IReadOnlyList<RecordingInfo> currentRecordings) + private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer, + IEnumerable<ProgramInfo> allPrograms, + bool filterByCurrentRecordings) { if (seriesTimer == null) { @@ -1031,28 +1214,80 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { throw new ArgumentNullException("allPrograms"); } - if (currentRecordings == null) - { - throw new ArgumentNullException("currentRecordings"); - } // Exclude programs that have already ended allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow && i.StartDate > DateTime.UtcNow); allPrograms = GetProgramsForSeries(seriesTimer, allPrograms); - var recordingShowIds = currentRecordings.Select(i => i.ProgramId).Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); - - allPrograms = allPrograms.Where(i => !recordingShowIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase)); + if (filterByCurrentRecordings) + { + allPrograms = allPrograms.Where(i => !IsProgramAlreadyInLibrary(i)); + } return allPrograms.Select(i => RecordingHelper.CreateTimer(i, seriesTimer)); } + private bool IsProgramAlreadyInLibrary(ProgramInfo program) + { + if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle)) + { + var seriesIds = _libraryManager.GetItemIds(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Series).Name }, + Name = program.Name + + }).Select(i => i.ToString("N")).ToArray(); + + if (seriesIds.Length == 0) + { + return false; + } + + if (program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) + { + var result = _libraryManager.GetItemsResult(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Episode).Name }, + ParentIndexNumber = program.SeasonNumber.Value, + IndexNumber = program.EpisodeNumber.Value, + AncestorIds = seriesIds, + ExcludeLocationTypes = new[] { LocationType.Virtual } + }); + + if (result.TotalRecordCount > 0) + { + return true; + } + } + + if (!string.IsNullOrWhiteSpace(program.EpisodeTitle)) + { + var result = _libraryManager.GetItemsResult(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Episode).Name }, + Name = program.EpisodeTitle, + AncestorIds = seriesIds, + ExcludeLocationTypes = new[] { LocationType.Virtual } + }); + + if (result.TotalRecordCount > 0) + { + return true; + } + } + } + + return false; + } + 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) @@ -1065,8 +1300,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV allPrograms = allPrograms.Where(epg => string.Equals(epg.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase)); } - allPrograms = allPrograms.Where(i => seriesTimer.Days.Contains(i.StartDate.ToLocalTime().DayOfWeek)); - if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId)) { _logger.Error("seriesTimer.SeriesId is null. Cannot find programs for series"); @@ -1132,6 +1365,47 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV }); } + public List<VirtualFolderInfo> GetRecordingFolders() + { + var list = new List<VirtualFolderInfo>(); + + var defaultFolder = RecordingPath; + var defaultName = "Recordings"; + + if (Directory.Exists(defaultFolder)) + { + list.Add(new VirtualFolderInfo + { + Locations = new List<string> { defaultFolder }, + Name = defaultName + }); + } + + var customPath = GetConfiguration().MovieRecordingPath; + if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath)) + { + list.Add(new VirtualFolderInfo + { + Locations = new List<string> { customPath }, + Name = "Recorded Movies", + CollectionType = CollectionType.Movies + }); + } + + customPath = GetConfiguration().SeriesRecordingPath; + if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath)) + { + list.Add(new VirtualFolderInfo + { + Locations = new List<string> { customPath }, + Name = "Recorded Series", + CollectionType = CollectionType.TvShows + }); + } + + return list; + } + class ActiveRecordingInfo { public string Path { get; set; } diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTVRegistration.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTVRegistration.cs new file mode 100644 index 000000000..675fca325 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTVRegistration.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using MediaBrowser.Common.Security; + +namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV +{ + public class EmbyTVRegistration : IRequiresRegistration + { + private readonly ISecurityManager _securityManager; + + public static EmbyTVRegistration Instance; + + public EmbyTVRegistration(ISecurityManager securityManager) + { + _securityManager = securityManager; + Instance = this; + } + + private bool? _isXmlTvEnabled; + + public Task LoadRegistrationInfoAsync() + { + _isXmlTvEnabled = null; + return Task.FromResult(true); + } + + public async Task<bool> EnableXmlTv() + { + if (!_isXmlTvEnabled.HasValue) + { + var info = await _securityManager.GetRegistrationStatus("xmltv").ConfigureAwait(false); + _isXmlTvEnabled = info.IsValid; + } + return _isXmlTvEnabled.Value; + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 69cc8ebf7..5e428e6f0 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -8,10 +8,13 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using CommonIO; -using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; @@ -21,8 +24,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { private readonly ILogger _logger; private readonly IFileSystem _fileSystem; + private readonly IHttpClient _httpClient; private readonly IMediaEncoder _mediaEncoder; - private readonly IApplicationPaths _appPaths; + private readonly IServerApplicationPaths _appPaths; + private readonly LiveTvOptions _liveTvOptions; private bool _hasExited; private Stream _logFileStream; private string _targetPath; @@ -30,17 +35,99 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private readonly IJsonSerializer _json; private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(); - public EncodedRecorder(ILogger logger, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IApplicationPaths appPaths, IJsonSerializer json) + public EncodedRecorder(ILogger logger, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IServerApplicationPaths appPaths, IJsonSerializer json, LiveTvOptions liveTvOptions, IHttpClient httpClient) { _logger = logger; _fileSystem = fileSystem; _mediaEncoder = mediaEncoder; _appPaths = appPaths; _json = json; + _liveTvOptions = liveTvOptions; + _httpClient = httpClient; + } + + public string GetOutputPath(MediaSourceInfo mediaSource, string targetFile) + { + return Path.ChangeExtension(targetFile, ".mp4"); } public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { + var tempfile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts"); + + try + { + await RecordInternal(mediaSource, tempfile, targetFile, duration, onStarted, cancellationToken) + .ConfigureAwait(false); + } + finally + { + try + { + File.Delete(tempfile); + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting recording temp file", ex); + } + } + } + + public async Task RecordInternal(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; _fileSystem.CreateDirectory(Path.GetDirectoryName(targetFile)); @@ -52,12 +139,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV UseShellExecute = false, // Must consume both stdout and stderr or deadlocks may occur - RedirectStandardOutput = true, + //RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true, FileName = _mediaEncoder.EncoderPath, - Arguments = GetCommandLineArgs(mediaSource, targetFile, duration), + Arguments = GetCommandLineArgs(mediaSource, inputFile, targetFile, duration), WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false @@ -78,7 +165,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true); var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); - await _logFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationToken).ConfigureAwait(false); + _logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length); process.Exited += (sender, args) => OnFfMpegProcessExited(process); @@ -87,24 +174,24 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV cancellationToken.Register(Stop); // MUST read both stdout and stderr asynchronously or a deadlock may occurr - process.BeginOutputReadLine(); + //process.BeginOutputReadLine(); onStarted(); // 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); - await _taskCompletionSource.Task.ConfigureAwait(false); + return _taskCompletionSource.Task; } - private string GetCommandLineArgs(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration) + private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile, TimeSpan duration) { string videoArgs; if (EncodeVideo(mediaSource)) { var maxBitrate = 25000000; videoArgs = string.Format( - "-codec:v:0 libx264 -force_key_frames expr:gte(t,n_forced*5) {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -vsync vfr -profile:v high -level 41", + "-codec:v:0 libx264 -force_key_frames expr:gte(t,n_forced*5) {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -vsync -1 -profile:v high -level 41", GetOutputSizeParam(), maxBitrate.ToString(CultureInfo.InvariantCulture)); } @@ -113,23 +200,31 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV videoArgs = "-codec:v:0 copy"; } - var commandLineArgs = "-fflags +genpts -async 1 -vsync -1 -i \"{0}\" -t {4} -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\""; + 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}\""; if (mediaSource.ReadAtNativeFramerate) { commandLineArgs = "-re " + commandLineArgs; } - commandLineArgs = string.Format(commandLineArgs, mediaSource.Path, targetFile, videoArgs, GetAudioArgs(mediaSource), _mediaEncoder.GetTimeParameter(duration.Ticks)); + commandLineArgs = string.Format(commandLineArgs, inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), durationParam); return commandLineArgs; } private string GetAudioArgs(MediaSourceInfo mediaSource) { - var copyAudio = new[] { "aac", "mp3" }; + // do not copy aac because many players have difficulty with aac_latm + var copyAudio = new[] { "mp3" }; var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>(); - if (mediaStreams.Any(i => i.Type == MediaStreamType.Audio && copyAudio.Contains(i.Codec, StringComparer.OrdinalIgnoreCase))) + var inputAudioCodec = mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Select(i => i.Codec).FirstOrDefault() ?? string.Empty; + + if (copyAudio.Contains(inputAudioCodec, StringComparer.OrdinalIgnoreCase)) + { + return "-codec:a:0 copy"; + } + if (_liveTvOptions.EnableOriginalAudioWithEncodedRecordings && !string.Equals(inputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase)) { return "-codec:a:0 copy"; } @@ -175,9 +270,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV //process.Kill(); _process.StandardInput.WriteLine("q"); - - // Need to wait because killing is asynchronous - _process.WaitForExit(5000); } catch (Exception ex) { diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs index 268a4f751..5706b6ae9 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs @@ -17,5 +17,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken); + + string GetOutputPath(MediaSourceInfo mediaSource, string targetFile); } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index 5d462f106..423358906 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading; using CommonIO; using MediaBrowser.Controller.Power; +using MediaBrowser.Model.LiveTv; namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { @@ -71,6 +72,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } + public void AddOrUpdate(TimerInfo item, bool resetTimer) + { + if (resetTimer) + { + AddOrUpdate(item); + return; + } + + var list = GetAll().ToList(); + + if (!list.Any(i => EqualityComparer(i, item))) + { + base.Add(item); + } + else + { + base.Update(item); + } + } + public override void Add(TimerInfo item) { if (string.IsNullOrWhiteSpace(item.Id)) @@ -85,6 +106,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private void AddTimer(TimerInfo item) { + if (item.Status == RecordingStatus.Completed) + { + return; + } + var startDate = RecordingHelper.GetStartTime(item); var now = DateTime.UtcNow; @@ -117,15 +143,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - public void StartTimer(TimerInfo item, TimeSpan length) + public void StartTimer(TimerInfo item, TimeSpan dueTime) { StopTimer(item); - var timer = new Timer(TimerCallback, item.Id, length, TimeSpan.Zero); + var timer = new Timer(TimerCallback, item.Id, dueTime, TimeSpan.Zero); if (_timers.TryAdd(item.Id, timer)) { - _logger.Info("Creating recording timer for {0}, {1}. Timer will fire in {2} minutes", item.Id, item.Name, length.TotalMinutes.ToString(CultureInfo.InvariantCulture)); + _logger.Info("Creating recording timer for {0}, {1}. Timer will fire in {2} minutes", item.Id, item.Name, dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture)); } else { diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index ae2a85090..e37109c14 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -506,8 +506,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings } private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms( - ListingsProviderInfo info, - List<string> programIds, + ListingsProviderInfo info, + List<string> programIds, CancellationToken cancellationToken) { var imageIdString = "["; @@ -564,7 +564,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings try { - using (Stream responce = await Get(options, false, info).ConfigureAwait(false)) + using (Stream responce = await Get(options, false, info).ConfigureAwait(false)) { var root = _jsonSerializer.DeserializeFromStream<List<ScheduleDirect.Headends>>(responce); @@ -666,58 +666,60 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings } } - private async Task<HttpResponseInfo> Post(HttpRequestOptions options, - bool enableRetry, - ListingsProviderInfo providerInfo) + private async Task<HttpResponseInfo> Post(HttpRequestOptions options, + bool enableRetry, + ListingsProviderInfo providerInfo) { try { return await _httpClient.Post(options).ConfigureAwait(false); - } - catch (HttpException ex) - { - _tokens.Clear(); - - if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500) - { - enableRetry = false; - } - - if (!enableRetry) { - throw; - } - } - - var newToken = await GetToken (providerInfo, options.CancellationToken).ConfigureAwait (false); - options.RequestHeaders ["token"] = newToken; - return await Post (options, false, providerInfo).ConfigureAwait (false); + } + catch (HttpException ex) + { + _tokens.Clear(); + + if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500) + { + enableRetry = false; + } + + if (!enableRetry) + { + throw; + } + } + + var newToken = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false); + options.RequestHeaders["token"] = newToken; + return await Post(options, false, providerInfo).ConfigureAwait(false); } - private async Task<Stream> Get(HttpRequestOptions options, - bool enableRetry, - ListingsProviderInfo providerInfo) + private async Task<Stream> Get(HttpRequestOptions options, + bool enableRetry, + ListingsProviderInfo providerInfo) { try { return await _httpClient.Get(options).ConfigureAwait(false); - } - catch (HttpException ex) - { - _tokens.Clear(); - - if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500) - { - enableRetry = false; - } - - if (!enableRetry) { - throw; - } - } - - var newToken = await GetToken (providerInfo, options.CancellationToken).ConfigureAwait (false); - options.RequestHeaders ["token"] = newToken; - return await Get (options, false, providerInfo).ConfigureAwait (false); + } + catch (HttpException ex) + { + _tokens.Clear(); + + if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500) + { + enableRetry = false; + } + + if (!enableRetry) + { + throw; + } + } + + var newToken = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false); + options.RequestHeaders["token"] = newToken; + return await Get(options, false, providerInfo).ConfigureAwait(false); } private async Task<string> GetTokenInternal(string username, string password, @@ -734,7 +736,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings //_logger.Info("Obtaining token from Schedules Direct from addres: " + httpOptions.Url + " with body " + // httpOptions.RequestContent); - using (var responce = await Post(httpOptions, false, null).ConfigureAwait(false)) + using (var responce = await Post(httpOptions, false, null).ConfigureAwait(false)) { var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Token>(responce.Content); if (root.message == "OK") @@ -816,7 +818,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings try { - using (var response = await Get(options, false, null).ConfigureAwait(false)) + using (var response = await Get(options, false, null).ConfigureAwait(false)) { var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Lineups>(response); @@ -869,6 +871,75 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings return GetHeadends(info, country, location, CancellationToken.None); } + public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken) + { + var listingsId = info.ListingsId; + if (string.IsNullOrWhiteSpace(listingsId)) + { + throw new Exception("ListingsId required"); + } + + await AddMetadata(info, new List<ChannelInfo>(), cancellationToken).ConfigureAwait(false); + + var token = await GetToken(info, cancellationToken); + + if (string.IsNullOrWhiteSpace(token)) + { + throw new Exception("token required"); + } + + var httpOptions = new HttpRequestOptions() + { + Url = ApiUrl + "/lineups/" + listingsId, + UserAgent = UserAgent, + CancellationToken = cancellationToken, + LogErrorResponseBody = true, + // The data can be large so give it some extra time + TimeoutMs = 60000 + }; + + httpOptions.RequestHeaders["token"] = token; + + var list = new List<ChannelInfo>(); + + using (var response = await Get(httpOptions, true, info).ConfigureAwait(false)) + { + var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Channel>(response); + _logger.Info("Found " + root.map.Count + " channels on the lineup on ScheduleDirect"); + _logger.Info("Mapping Stations to Channel"); + foreach (ScheduleDirect.Map map in root.map) + { + var channelNumber = map.logicalChannelNumber; + + if (string.IsNullOrWhiteSpace(channelNumber)) + { + channelNumber = map.channel; + } + if (string.IsNullOrWhiteSpace(channelNumber)) + { + channelNumber = map.atscMajor + "." + map.atscMinor; + } + channelNumber = channelNumber.TrimStart('0'); + + var name = channelNumber; + var station = GetStation(listingsId, channelNumber, null); + + if (station != null) + { + name = station.name; + } + + list.Add(new ChannelInfo + { + Number = channelNumber, + Name = name + }); + } + } + + return list; + } + public class ScheduleDirect { public class Token diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs deleted file mode 100644 index ac316f9a1..000000000 --- a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs +++ /dev/null @@ -1,44 +0,0 @@ -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.LiveTv; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.LiveTv.Listings -{ - public class XmlTv : IListingsProvider - { - public string Name - { - get { return "XmlTV"; } - } - - public string Type - { - get { return "xmltv"; } - } - - public Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, string channelName, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public async Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels, CancellationToken cancellationToken) - { - // Might not be needed - } - - public async Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings) - { - // Check that the path or url is valid. If not, throw a file not found exception - } - - public Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location) - { - // In theory this should never be called because there is always only one lineup - throw new NotImplementedException(); - } - } -} diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs new file mode 100644 index 000000000..1e82e3fce --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -0,0 +1,219 @@ +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.LiveTv; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +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; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Server.Implementations.LiveTv.Listings +{ + public class XmlTvListingsProvider : IListingsProvider + { + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly ILogger _logger; + + public XmlTvListingsProvider(IServerConfigurationManager config, IHttpClient httpClient, ILogger logger) + { + _config = config; + _httpClient = httpClient; + _logger = logger; + } + + public string Name + { + get { return "XmlTV"; } + } + + public string Type + { + get { return "xmltv"; } + } + + private string GetLanguage() + { + return _config.Configuration.PreferredMetadataLanguage; + } + + private async Task<string> GetXml(string path, CancellationToken cancellationToken) + { + _logger.Info("xmltv path: {0}", path); + + if (!path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return path; + } + + var cacheFilename = DateTime.UtcNow.DayOfYear.ToString(CultureInfo.InvariantCulture) + "-" + DateTime.UtcNow.Hour.ToString(CultureInfo.InvariantCulture) + ".xml"; + var cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename); + if (File.Exists(cacheFile)) + { + return cacheFile; + } + + _logger.Info("Downloading xmltv listings from {0}", path); + + var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = path, + Progress = new Progress<Double>(), + DecompressionMethod = DecompressionMethods.GZip, + + // It's going to come back gzipped regardless of this value + // So we need to make sure the decompression method is set to gzip + EnableHttpCompression = true + + }).ConfigureAwait(false); + + Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)); + + using (var stream = File.OpenRead(tempFile)) + { + using (var reader = new StreamReader(stream, Encoding.UTF8)) + { + using (var fileStream = File.OpenWrite(cacheFile)) + { + using (var writer = new StreamWriter(fileStream)) + { + while (!reader.EndOfStream) + { + writer.WriteLine(reader.ReadLine()); + } + } + } + } + } + + _logger.Debug("Returning xmltv path {0}", cacheFile); + return cacheFile; + } + + public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, string channelName, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) + { + if (!await EmbyTV.EmbyTVRegistration.Instance.EnableXmlTv().ConfigureAwait(false)) + { + var length = endDateUtc - startDateUtc; + if (length.TotalDays > 1) + { + endDateUtc = startDateUtc.AddDays(1); + } + } + + var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false); + var reader = new XmlTvReader(path, GetLanguage(), null); + + var results = reader.GetProgrammes(channelNumber, startDateUtc, endDateUtc, cancellationToken); + return results.Select(p => new ProgramInfo() + { + ChannelId = p.ChannelId, + EndDate = GetDate(p.EndDate), + EpisodeNumber = p.Episode == null ? null : p.Episode.Episode, + EpisodeTitle = p.Episode == null ? null : p.Episode.Title, + Genres = p.Categories, + Id = String.Format("{0}_{1:O}", p.ChannelId, p.StartDate), // Construct an id from the channel and start date, + StartDate = GetDate(p.StartDate), + Name = p.Title, + Overview = p.Description, + ShortOverview = p.Description, + ProductionYear = !p.CopyrightDate.HasValue ? (int?)null : p.CopyrightDate.Value.Year, + SeasonNumber = p.Episode == null ? null : p.Episode.Series, + IsSeries = p.Episode != null, + IsRepeat = p.IsRepeat, + IsPremiere = p.Premiere != null, + IsKids = p.Categories.Any(c => info.KidsCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)), + IsMovie = p.Categories.Any(c => info.MovieCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)), + IsNews = p.Categories.Any(c => info.NewsCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)), + IsSports = p.Categories.Any(c => info.SportsCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)), + ImageUrl = p.Icon != null && !String.IsNullOrEmpty(p.Icon.Source) ? p.Icon.Source : null, + HasImage = p.Icon != null && !String.IsNullOrEmpty(p.Icon.Source), + 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 + }); + } + + private DateTime GetDate(DateTime date) + { + if (date.Kind != DateTimeKind.Utc) + { + date = DateTime.SpecifyKind(date, DateTimeKind.Utc); + } + return date; + } + + public async Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels, CancellationToken cancellationToken) + { + // Add the channel image url + var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false); + var reader = new XmlTvReader(path, GetLanguage(), null); + var results = reader.GetChannels().ToList(); + + if (channels != null) + { + channels.ForEach(c => + { + var channelNumber = info.GetMappedChannel(c.Number); + var match = results.FirstOrDefault(r => string.Equals(r.Id, channelNumber, StringComparison.OrdinalIgnoreCase)); + + if (match != null && match.Icon != null && !String.IsNullOrEmpty(match.Icon.Source)) + { + c.ImageUrl = match.Icon.Source; + } + }); + } + } + + public Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings) + { + // Assume all urls are valid. check files for existence + if (!info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase) && !File.Exists(info.Path)) + { + throw new FileNotFoundException("Could not find the XmlTv file specified:", info.Path); + } + + return Task.FromResult(true); + } + + public async Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location) + { + // In theory this should never be called because there is always only one lineup + var path = await GetXml(info.Path, CancellationToken.None).ConfigureAwait(false); + var reader = new XmlTvReader(path, GetLanguage(), null); + var results = reader.GetChannels(); + + // Should this method be async? + return results.Select(c => new NameIdPair() { Id = c.Id, Name = c.DisplayName }).ToList(); + } + + public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken) + { + // In theory this should never be called because there is always only one lineup + var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false); + var reader = new XmlTvReader(path, GetLanguage(), null); + var results = reader.GetChannels(); + + // Should this method be async? + return results.Select(c => new ChannelInfo() + { + Id = c.Id, + Name = c.DisplayName, + ImageUrl = c.Icon != null && !String.IsNullOrEmpty(c.Icon.Source) ? c.Icon.Source : null, + Number = c.Id + + }).ToList(); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index 3849f44ab..64af35a9a 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -30,6 +30,8 @@ using System.Threading.Tasks; using CommonIO; using IniParser; using IniParser.Model; +using MediaBrowser.Common.Events; +using MediaBrowser.Model.Events; namespace MediaBrowser.Server.Implementations.LiveTv { @@ -64,6 +66,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv private readonly List<IListingsProvider> _listingProviders = new List<IListingsProvider>(); private readonly IFileSystem _fileSystem; + public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled; + public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled; + public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated; + public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated; + public LiveTvManager(IApplicationHost appHost, IServerConfigurationManager config, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager, ILibraryManager libraryManager, ITaskManager taskManager, ILocalizationManager localization, IJsonSerializer jsonSerializer, IProviderManager providerManager, IFileSystem fileSystem) { _config = config; @@ -133,9 +140,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); + var topFolder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false); + var channels = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(LiveTvChannel).Name } + IncludeItemTypes = new[] { typeof(LiveTvChannel).Name }, + SortBy = new[] { ItemSortBy.SortName }, + TopParentIds = new[] { topFolder.Id.ToString("N") } }).Cast<LiveTvChannel>(); @@ -164,7 +175,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var val = query.IsFavorite.Value; channels = channels - .Where(i => _userDataManager.GetUserData(user.Id, i.GetUserDataKey()).IsFavorite == val); + .Where(i => _userDataManager.GetUserData(user, i).IsFavorite == val); } if (query.IsLiked.HasValue) @@ -174,7 +185,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv channels = channels .Where(i => { - var likes = _userDataManager.GetUserData(user.Id, i.GetUserDataKey()).Likes; + var likes = _userDataManager.GetUserData(user, i).Likes; return likes.HasValue && likes.Value == val; }); @@ -187,7 +198,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv channels = channels .Where(i => { - var likes = _userDataManager.GetUserData(user.Id, i.GetUserDataKey()).Likes; + var likes = _userDataManager.GetUserData(user, i).Likes; return likes.HasValue && likes.Value != val; }); @@ -200,7 +211,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv { if (enableFavoriteSorting) { - var userData = _userDataManager.GetUserData(user.Id, i.GetUserDataKey()); + var userData = _userDataManager.GetUserData(user, i); if (userData.IsFavorite) { @@ -511,14 +522,22 @@ namespace MediaBrowser.Server.Implementations.LiveTv if (!(service is EmbyTV.EmbyTV)) { // We can't trust that we'll be able to direct stream it through emby server, no matter what the provider says - mediaSource.SupportsDirectStream = true; + mediaSource.SupportsDirectStream = false; mediaSource.SupportsTranscoding = true; + foreach (var stream in mediaSource.MediaStreams) + { + if (stream.Type == MediaStreamType.Video && string.IsNullOrWhiteSpace(stream.NalLengthSize)) + { + stream.NalLengthSize = "0"; + } + } } } private async Task<LiveTvChannel> GetChannel(ChannelInfo channelInfo, string serviceName, Guid parentFolderId, CancellationToken cancellationToken) { var isNew = false; + var forceUpdate = false; var id = _tvDtoService.GetInternalChannelId(serviceName, channelInfo.Id); @@ -568,10 +587,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv if (!string.IsNullOrWhiteSpace(channelInfo.ImagePath)) { item.SetImagePath(ImageType.Primary, channelInfo.ImagePath); + forceUpdate = true; } else if (!string.IsNullOrWhiteSpace(channelInfo.ImageUrl)) { item.SetImagePath(ImageType.Primary, channelInfo.ImageUrl); + forceUpdate = true; } } @@ -580,9 +601,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv item.Name = channelInfo.Name; } + if (isNew) + { + await _libraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false); + } + else if (forceUpdate) + { + await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + } + await item.RefreshMetadata(new MetadataRefreshOptions(_fileSystem) { - ForceSave = isNew + ForceSave = isNew || forceUpdate }, cancellationToken); @@ -626,7 +656,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv item.Audio = info.Audio; item.ChannelId = channel.Id.ToString("N"); item.CommunityRating = item.CommunityRating ?? info.CommunityRating; - item.EndDate = info.EndDate; + item.EpisodeTitle = info.EpisodeTitle; item.ExternalId = info.Id; item.Genres = info.Genres; @@ -643,7 +673,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv item.OfficialRating = item.OfficialRating ?? info.OfficialRating; item.Overview = item.Overview ?? info.Overview; item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks; + + if (item.StartDate != info.StartDate) + { + forceUpdate = true; + } item.StartDate = info.StartDate; + + if (item.EndDate != info.EndDate) + { + forceUpdate = true; + } + item.EndDate = info.EndDate; + item.HomePageUrl = info.HomePageUrl; item.ProductionYear = info.ProductionYear; @@ -864,6 +906,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); + var topFolder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false); + + if (query.SortBy.Length == 0) + { + // Unless something else was specified, order by start date to take advantage of a specialized index + query.SortBy = new[] { ItemSortBy.StartDate }; + } + var internalQuery = new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, @@ -879,7 +929,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv StartIndex = query.StartIndex, Limit = query.Limit, SortBy = query.SortBy, - SortOrder = query.SortOrder ?? SortOrder.Ascending + SortOrder = query.SortOrder ?? SortOrder.Ascending, + EnableTotalRecordCount = query.EnableTotalRecordCount, + TopParentIds = new[] { topFolder.Id.ToString("N") } }; if (query.HasAired.HasValue) @@ -896,7 +948,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var queryResult = _libraryManager.QueryItems(internalQuery); - var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user).ToArray(); + var returnArray = (await _dtoService.GetBaseItemDtos(queryResult.Items, options, user).ConfigureAwait(false)).ToArray(); var result = new QueryResult<BaseItemDto> { @@ -911,15 +963,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var user = _userManager.GetUserById(query.UserId); + var topFolder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false); + var internalQuery = new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, IsAiring = query.IsAiring, IsMovie = query.IsMovie, IsSports = query.IsSports, - IsKids = query.IsKids + IsKids = query.IsKids, + EnableTotalRecordCount = query.EnableTotalRecordCount, + SortBy = new[] { ItemSortBy.StartDate }, + TopParentIds = new[] { topFolder.Id.ToString("N") } }; + if (query.Limit.HasValue) + { + internalQuery.Limit = Math.Max(query.Limit.Value * 4, 200); + } + if (query.HasAired.HasValue) { if (query.HasAired.Value) @@ -936,15 +998,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv var programList = programs.ToList(); - var genres = programList.SelectMany(i => i.Genres) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .DistinctNames() - .Select(i => _libraryManager.GetGenre(i)) - .DistinctBy(i => i.Id) - .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); + var factorChannelWatchCount = (query.IsAiring ?? false) || (query.IsKids ?? false) || (query.IsSports ?? false) || (query.IsMovie ?? false); programs = programList.OrderBy(i => i.HasImage(ImageType.Primary) ? 0 : 1) - .ThenByDescending(i => GetRecommendationScore(i, user.Id, genres)) + .ThenByDescending(i => GetRecommendationScore(i, user.Id, factorChannelWatchCount)) .ThenBy(i => i.StartDate); if (query.Limit.HasValue) @@ -971,7 +1028,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var user = _userManager.GetUserById(query.UserId); - var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user).ToArray(); + var returnArray = (await _dtoService.GetBaseItemDtos(internalResult.Items, options, user).ConfigureAwait(false)).ToArray(); var result = new QueryResult<BaseItemDto> { @@ -982,7 +1039,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv return result; } - private int GetRecommendationScore(LiveTvProgram program, Guid userId, Dictionary<string, Genre> genres) + private int GetRecommendationScore(LiveTvProgram program, Guid userId, bool factorChannelWatchCount) { var score = 0; @@ -998,7 +1055,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var channel = GetInternalChannel(program.ChannelId); - var channelUserdata = _userDataManager.GetUserData(userId, channel.GetUserDataKey()); + var channelUserdata = _userDataManager.GetUserData(userId, channel); if (channelUserdata.Likes ?? false) { @@ -1014,41 +1071,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv score += 3; } - score += GetGenreScore(program.Genres, userId, genres); - - return score; - } - - private int GetGenreScore(IEnumerable<string> programGenres, Guid userId, Dictionary<string, Genre> genres) - { - return programGenres.Select(i => + if (factorChannelWatchCount) { - var score = 0; - - Genre genre; - - if (genres.TryGetValue(i, out genre)) - { - var genreUserdata = _userDataManager.GetUserData(userId, genre.GetUserDataKey()); - - if (genreUserdata.Likes ?? false) - { - score++; - } - else if (!(genreUserdata.Likes ?? true)) - { - score--; - } - - if (genreUserdata.IsFavorite) - { - score += 2; - } - } - - return score; + score += channelUserdata.PlayCount; + } - }).Sum(); + return score; } private async Task AddRecordingInfo(IEnumerable<Tuple<BaseItemDto, string, string>> programs, CancellationToken cancellationToken) @@ -1104,6 +1132,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv private async Task RefreshChannelsInternal(IProgress<double> progress, CancellationToken cancellationToken) { + EmbyTV.EmbyTV.Current.CreateRecordingFolders(); + var numComplete = 0; double progressPerService = _services.Count == 0 ? 0 @@ -1184,8 +1214,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv var item = await GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolderId, cancellationToken).ConfigureAwait(false); list.Add(item); - - _libraryManager.RegisterItem(item); } catch (OperationCanceledException) { @@ -1256,11 +1284,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv private async Task CleanDatabaseInternal(List<Guid> currentIdList, string[] validTypes, IProgress<double> progress, CancellationToken cancellationToken) { - var list = _itemRepo.GetItemIds(new InternalItemsQuery + var list = _itemRepo.GetItemIdsList(new InternalItemsQuery { IncludeItemTypes = validTypes - }).Items.ToList(); + }).ToList(); var numComplete = 0; @@ -1373,6 +1401,40 @@ namespace MediaBrowser.Server.Implementations.LiveTv } } + private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, User user) + { + if (user == null || (query.IsInProgress ?? false)) + { + return new QueryResult<BaseItem>(); + } + + var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders() + .SelectMany(i => i.Locations) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Select(i => _libraryManager.FindByPath(i, true)) + .Where(i => i != null) + .Where(i => i.IsVisibleStandalone(user)) + .ToList(); + + if (folders.Count == 0) + { + return new QueryResult<BaseItem>(); + } + + return _libraryManager.GetItemsResult(new InternalItemsQuery(user) + { + MediaTypes = new[] { MediaType.Video }, + Recursive = true, + AncestorIds = folders.Select(i => i.Id.ToString("N")).ToArray(), + IsFolder = false, + ExcludeLocationTypes = new[] { LocationType.Virtual }, + Limit = Math.Min(200, query.Limit ?? int.MaxValue), + SortBy = new[] { ItemSortBy.DateCreated }, + SortOrder = SortOrder.Descending, + EnableTotalRecordCount = query.EnableTotalRecordCount + }); + } + public async Task<QueryResult<BaseItem>> GetInternalRecordings(RecordingQuery query, CancellationToken cancellationToken) { var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); @@ -1381,6 +1443,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv return new QueryResult<BaseItem>(); } + if (_services.Count == 1) + { + return GetEmbyRecordings(query, user); + } + await RefreshRecordings(cancellationToken).ConfigureAwait(false); var internalQuery = new InternalItemsQuery(user) @@ -1393,7 +1460,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv internalQuery.ChannelIds = new[] { query.ChannelId }; } - var queryResult = _libraryManager.GetItemList(internalQuery, new string[] { }); + var queryResult = _libraryManager.GetItemList(internalQuery); IEnumerable<ILiveTvRecording> recordings = queryResult.Cast<ILiveTvRecording>(); if (!string.IsNullOrWhiteSpace(query.Id)) @@ -1506,6 +1573,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv { dto.ChannelName = channel.Name; dto.MediaType = channel.MediaType; + dto.ChannelNumber = channel.Number; if (channel.HasImage(ImageType.Primary)) { @@ -1596,7 +1664,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var internalResult = await GetInternalRecordings(query, cancellationToken).ConfigureAwait(false); - var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user).ToArray(); + var returnArray = (await _dtoService.GetBaseItemDtos(internalResult.Items, options, user).ConfigureAwait(false)).ToArray(); return new QueryResult<BaseItemDto> { @@ -1623,6 +1691,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv var results = await Task.WhenAll(tasks).ConfigureAwait(false); var timers = results.SelectMany(i => i.ToList()); + if (query.IsActive.HasValue) + { + if (query.IsActive.Value) + { + timers = timers.Where(i => i.Item1.Status == RecordingStatus.InProgress); + } + else + { + timers = timers.Where(i => i.Item1.Status != RecordingStatus.InProgress); + } + } + if (!string.IsNullOrEmpty(query.ChannelId)) { var guid = new Guid(query.ChannelId); @@ -1724,6 +1804,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); _lastRecordingRefreshTime = DateTime.MinValue; + + EventHelper.QueueEventIfNotNull(TimerCancelled, this, new GenericEventArgs<TimerEventInfo> + { + Argument = new TimerEventInfo + { + Id = id + } + }, _logger); } public async Task CancelSeriesTimer(string id) @@ -1739,6 +1827,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv await service.CancelSeriesTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); _lastRecordingRefreshTime = DateTime.MinValue; + + EventHelper.QueueEventIfNotNull(SeriesTimerCancelled, this, new GenericEventArgs<TimerEventInfo> + { + Argument = new TimerEventInfo + { + Id = id + } + }, _logger); } public async Task<BaseItemDto> GetRecording(string id, DtoOptions options, CancellationToken cancellationToken, User user = null) @@ -1835,9 +1931,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv MaxStartDate = now, MinEndDate = now, Limit = channelIds.Length, - SortBy = new[] { "StartDate" } + SortBy = new[] { "StartDate" }, + TopParentIds = new[] { GetInternalLiveTvFolder(CancellationToken.None).Result.Id.ToString("N") } - }, new string[] { }).ToList(); + }).ToList(); foreach (var tuple in tuples) { @@ -1845,6 +1942,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var channel = tuple.Item2; dto.Number = channel.Number; + dto.ChannelNumber = channel.Number; dto.ChannelType = channel.ChannelType; dto.ServiceName = GetService(channel).Name; @@ -1923,10 +2021,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var defaults = await GetNewTimerDefaultsInternal(cancellationToken, program).ConfigureAwait(false); var info = _tvDtoService.GetSeriesTimerInfoDto(defaults.Item1, defaults.Item2, null); - info.Days = new List<DayOfWeek> - { - program.StartDate.ToLocalTime().DayOfWeek - }; + info.Days = defaults.Item1.Days; info.DayPattern = _tvDtoService.GetDayPattern(info.Days); @@ -1957,9 +2052,29 @@ namespace MediaBrowser.Server.Implementations.LiveTv var defaultValues = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false); info.Priority = defaultValues.Priority; - await service.CreateTimerAsync(info, cancellationToken).ConfigureAwait(false); + string newTimerId = null; + var supportsNewTimerIds = service as ISupportsNewTimerIds; + if (supportsNewTimerIds != null) + { + newTimerId = await supportsNewTimerIds.CreateTimer(info, cancellationToken).ConfigureAwait(false); + newTimerId = _tvDtoService.GetInternalTimerId(timer.ServiceName, newTimerId).ToString("N"); + } + else + { + await service.CreateTimerAsync(info, cancellationToken).ConfigureAwait(false); + } + _lastRecordingRefreshTime = DateTime.MinValue; _logger.Info("New recording scheduled"); + + EventHelper.QueueEventIfNotNull(TimerCreated, this, new GenericEventArgs<TimerEventInfo> + { + Argument = new TimerEventInfo + { + ProgramId = _tvDtoService.GetInternalProgramId(timer.ServiceName, info.ProgramId).ToString("N"), + Id = newTimerId + } + }, _logger); } public async Task CreateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken) @@ -1972,8 +2087,28 @@ namespace MediaBrowser.Server.Implementations.LiveTv var defaultValues = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false); info.Priority = defaultValues.Priority; - await service.CreateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false); + string newTimerId = null; + var supportsNewTimerIds = service as ISupportsNewTimerIds; + if (supportsNewTimerIds != null) + { + newTimerId = await supportsNewTimerIds.CreateSeriesTimer(info, cancellationToken).ConfigureAwait(false); + newTimerId = _tvDtoService.GetInternalSeriesTimerId(timer.ServiceName, newTimerId).ToString("N"); + } + else + { + await service.CreateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false); + } + _lastRecordingRefreshTime = DateTime.MinValue; + + EventHelper.QueueEventIfNotNull(SeriesTimerCreated, this, new GenericEventArgs<TimerEventInfo> + { + Argument = new TimerEventInfo + { + ProgramId = _tvDtoService.GetInternalProgramId(timer.ServiceName, info.ProgramId).ToString("N"), + Id = newTimerId + } + }, _logger); } public async Task UpdateTimer(TimerInfoDto timer, CancellationToken cancellationToken) @@ -2048,7 +2183,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv }, cancellationToken).ConfigureAwait(false); - var recordings = recordingResult.Items.Cast<ILiveTvRecording>().ToList(); + var recordings = recordingResult.Items.OfType<ILiveTvRecording>().ToList(); var groups = new List<BaseItemDto>(); @@ -2389,6 +2524,79 @@ namespace MediaBrowser.Server.Implementations.LiveTv return info; } + public void DeleteListingsProvider(string id) + { + var config = GetConfiguration(); + + config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToList(); + + _config.SaveConfiguration("livetv", config); + _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>(); + } + + public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber) + { + var config = GetConfiguration(); + + var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase)); + listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray(); + + if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase)) + { + var list = listingsProviderInfo.ChannelMappings.ToList(); + list.Add(new NameValuePair + { + Name = tunerChannelNumber, + Value = providerChannelNumber + }); + listingsProviderInfo.ChannelMappings = list.ToArray(); + } + + _config.SaveConfiguration("livetv", config); + + var tunerChannels = await GetChannelsForListingsProvider(providerId, CancellationToken.None) + .ConfigureAwait(false); + + var providerChannels = await GetChannelsFromListingsProviderData(providerId, CancellationToken.None) + .ConfigureAwait(false); + + var mappings = listingsProviderInfo.ChannelMappings.ToList(); + + var tunerChannelMappings = + tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList(); + + _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>(); + + return tunerChannelMappings.First(i => string.Equals(i.Number, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)); + } + + public TunerChannelMapping GetTunerChannelMapping(ChannelInfo channel, List<NameValuePair> mappings, List<ChannelInfo> providerChannels) + { + var result = new TunerChannelMapping + { + Name = channel.Number + " " + channel.Name, + Number = channel.Number + }; + + var mapping = mappings.FirstOrDefault(i => string.Equals(i.Name, channel.Number, StringComparison.OrdinalIgnoreCase)); + var providerChannelNumber = channel.Number; + + if (mapping != null) + { + providerChannelNumber = mapping.Value; + } + + var providerChannel = providerChannels.FirstOrDefault(i => string.Equals(i.Number, providerChannelNumber, StringComparison.OrdinalIgnoreCase)); + + if (providerChannel != null) + { + result.ProviderChannelNumber = providerChannel.Number; + result.ProviderChannelName = providerChannel.Name; + } + + return result; + } + public Task<List<NameIdPair>> GetLineups(string providerType, string providerId, string country, string location) { var config = GetConfiguration(); @@ -2450,7 +2658,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv public List<NameValuePair> GetSatIniMappings() { - var names = GetType().Assembly.GetManifestResourceNames().Where(i => i.IndexOf("SatIp.ini.satellite", StringComparison.OrdinalIgnoreCase) != -1).ToList(); + var names = GetType().Assembly.GetManifestResourceNames().Where(i => i.IndexOf("SatIp.ini", StringComparison.OrdinalIgnoreCase) != -1).ToList(); return names.Select(GetSatIniMappings).Where(i => i != null).DistinctBy(i => i.Value.Split('|')[0]).ToList(); } @@ -2472,13 +2680,34 @@ namespace MediaBrowser.Server.Implementations.LiveTv return null; } + var srch = "SatIp.ini."; + var filename = Path.GetFileName(resource); + return new NameValuePair { Name = satType1 + " " + satType2, - Value = satType2 + "|" + Path.GetFileName(resource) + Value = satType2 + "|" + filename.Substring(filename.IndexOf(srch) + srch.Length) }; } } } + + public Task<List<ChannelInfo>> GetSatChannelScanResult(TunerHostInfo info, CancellationToken cancellationToken) + { + return new TunerHosts.SatIp.ChannelScan(_logger).Scan(info, cancellationToken); + } + + public Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken) + { + var info = GetConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); + return EmbyTV.EmbyTV.Current.GetChannelsForListingsProvider(info, cancellationToken); + } + + public Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken) + { + var info = GetConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); + var provider = _listingProviders.First(i => string.Equals(i.Type, info.Type, StringComparison.OrdinalIgnoreCase)); + return provider.GetChannels(info, cancellationToken); + } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index d3bb87bc7..cdba1873e 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -83,7 +83,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv } var list = sources.ToList(); - var serverUrl = _appHost.LocalApiUrl; + var serverUrl = await _appHost.GetLocalApiUrl().ConfigureAwait(false); foreach (var source in list) { diff --git a/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs index ab8ec720b..3f0538bd0 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs @@ -74,7 +74,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv } } - public bool HasChanged(IHasMetadata item, MetadataStatus status, IDirectoryService directoryService) + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) { var liveTvItem = item as LiveTvProgram; diff --git a/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs index fce3223ea..25678c29d 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs @@ -68,7 +68,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv get { return 0; } } - public bool HasChanged(IHasMetadata item, MetadataStatus status, IDirectoryService directoryService) + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) { var liveTvItem = item as ILiveTvRecording; diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 02a8d6938..9bb5b4fd7 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -224,7 +224,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false); - //await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false); + if (EnableMediaProbing) + { + await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false); + } + return new Tuple<MediaSourceInfo, SemaphoreSlim>(stream, resourcePool); } catch (Exception ex) @@ -239,6 +243,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts throw new LiveTvConflictException(); } + protected virtual bool EnableMediaProbing + { + get { return false; } + } + protected async Task<bool> IsAvailable(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) { try @@ -268,6 +277,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts return _semaphoreLocks.GetOrAdd(url, key => new SemaphoreSlim(1, 1)); } + private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, SemaphoreSlim resourcePool, CancellationToken cancellationToken) + { + await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + await AddMediaInfoInternal(mediaSource, isAudio, cancellationToken).ConfigureAwait(false); + + // Leave the resource locked. it will be released upstream + } + catch (Exception) + { + // Release the resource if there's some kind of failure. + resourcePool.Release(); + + throw; + } + } + private async Task AddMediaInfoInternal(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken) { var originalRuntime = mediaSource.RunTimeTicks; diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index db7f6f86c..69b6fb5a9 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -17,6 +17,7 @@ using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Net; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun { @@ -59,7 +60,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun return id; } - protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken) + 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 { @@ -68,45 +77,61 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun }; using (var stream = await _httpClient.Get(options)) { - var root = JsonSerializer.DeserializeFromStream<List<Channels>>(stream); + var lineup = JsonSerializer.DeserializeFromStream<List<Channels>>(stream) ?? new List<Channels>(); - if (root != null) + if (info.ImportFavoritesOnly) { - var result = root.Select(i => new ChannelInfo - { - Name = i.GuideName, - Number = i.GuideNumber.ToString(CultureInfo.InvariantCulture), - Id = GetChannelId(info, i), - IsFavorite = i.Favorite, - TunerHostId = info.Id + lineup = lineup.Where(i => i.Favorite).ToList(); + } - }); + return lineup.Where(i => !i.DRM).ToList(); + } + } - if (info.ImportFavoritesOnly) - { - result = result.Where(i => i.IsFavorite ?? true).ToList(); - } + protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken) + { + var lineup = await GetLineup(info, cancellationToken).ConfigureAwait(false); - return result; - } - return new List<ChannelInfo>(); - } + return lineup.Select(i => new ChannelInfo + { + Name = i.GuideName, + Number = i.GuideNumber.ToString(CultureInfo.InvariantCulture), + Id = GetChannelId(info, i), + IsFavorite = i.Favorite, + TunerHostId = info.Id, + IsHD = i.HD == 1, + AudioCodec = i.AudioCodec, + VideoCodec = i.VideoCodec + }); } private async Task<string> GetModelInfo(TunerHostInfo info, CancellationToken cancellationToken) { - using (var stream = await _httpClient.Get(new HttpRequestOptions() + try { - Url = string.Format("{0}/discover.json", GetApiUrl(info, false)), - CancellationToken = cancellationToken, - CacheLength = TimeSpan.FromDays(1), - CacheMode = CacheMode.Unconditional, - TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds) - })) + using (var stream = await _httpClient.Get(new HttpRequestOptions() + { + Url = string.Format("{0}/discover.json", GetApiUrl(info, false)), + CancellationToken = cancellationToken, + CacheLength = TimeSpan.FromDays(1), + CacheMode = CacheMode.Unconditional, + TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds) + })) + { + var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream); + + return response.ModelNumber; + } + } + catch (HttpException ex) { - var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream); + if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound) + { + // HDHR4 doesn't have this api + return "HDHR"; + } - return response.ModelNumber; + throw; } } @@ -226,19 +251,24 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun { public string GuideNumber { get; set; } public string GuideName { get; set; } + public string VideoCodec { get; set; } + public string AudioCodec { get; set; } public string URL { get; set; } public bool Favorite { get; set; } public bool DRM { get; set; } + public int HD { get; set; } } - private MediaSourceInfo GetMediaSource(TunerHostInfo info, string channelId, string profile) + private async Task<MediaSourceInfo> GetMediaSource(TunerHostInfo info, string channelId, string profile) { int? width = null; int? height = null; bool isInterlaced = true; - var videoCodec = !string.IsNullOrWhiteSpace(GetEncodingOptions().HardwareAccelerationType) ? null : "mpeg2video"; + string videoCodec = null; + string audioCodec = "ac3"; int? videoBitrate = null; + int? audioBitrate = null; if (string.Equals(profile, "mobile", StringComparison.OrdinalIgnoreCase)) { @@ -256,18 +286,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun videoCodec = "h264"; videoBitrate = 15000000; } - else if (string.Equals(profile, "internet720", StringComparison.OrdinalIgnoreCase)) - { - width = 1280; - height = 720; - isInterlaced = false; - videoCodec = "h264"; - videoBitrate = 8000000; - } else if (string.Equals(profile, "internet540", StringComparison.OrdinalIgnoreCase)) { - width = 1280; - height = 720; + width = 960; + height = 546; isInterlaced = false; videoCodec = "h264"; videoBitrate = 2500000; @@ -297,6 +319,32 @@ 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) + { + videoCodec = channel.VideoCodec; + audioCodec = channel.AudioCodec; + + videoBitrate = (channel.IsHD ?? true) ? 15000000 : 2000000; + audioBitrate = (channel.IsHD ?? true) ? 448000 : 192000; + } + } + + // normalize + if (string.Equals(videoCodec, "mpeg2", StringComparison.OrdinalIgnoreCase)) + { + videoCodec = "mpeg2video"; + } + + string nal = null; + if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) + { + nal = "0"; + } + var url = GetApiUrl(info, true) + "/auto/v" + channelId; if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase)) @@ -319,16 +367,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun Codec = videoCodec, Width = width, Height = height, - BitRate = videoBitrate - + BitRate = videoBitrate, + NalLengthSize = nal + }, new MediaStream { Type = MediaStreamType.Audio, // Set the index to -1 because we don't know the exact index of the audio stream within the container Index = -1, - Codec = "ac3", - BitRate = 192000 + Codec = audioCodec, + BitRate = audioBitrate } }, RequiresOpening = false, @@ -337,7 +386,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun Container = "ts", Id = profile, SupportsDirectPlay = true, - SupportsDirectStream = true, + SupportsDirectStream = false, SupportsTranscoding = true }; @@ -364,21 +413,22 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun } var hdhrId = GetHdHrIdFromChannelId(channelId); - list.Add(GetMediaSource(info, hdhrId, "native")); + list.Add(await GetMediaSource(info, hdhrId, "native").ConfigureAwait(false)); try { string model = await GetModelInfo(info, cancellationToken).ConfigureAwait(false); model = model ?? string.Empty; - if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1) + if (info.AllowHWTranscoding && (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)) { - list.Insert(0, GetMediaSource(info, hdhrId, "heavy")); + list.Add(await GetMediaSource(info, hdhrId, "heavy").ConfigureAwait(false)); - list.Add(GetMediaSource(info, hdhrId, "internet480")); - list.Add(GetMediaSource(info, hdhrId, "internet360")); - list.Add(GetMediaSource(info, hdhrId, "internet240")); - list.Add(GetMediaSource(info, hdhrId, "mobile")); + 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 (Exception ex) @@ -409,7 +459,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun } var hdhrId = GetHdHrIdFromChannelId(channelId); - return GetMediaSource(info, hdhrId, streamId); + return await GetMediaSource(info, hdhrId, streamId).ConfigureAwait(false); } public async Task Validate(TunerHostInfo info) @@ -419,16 +469,29 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun return; } - // Test it by pulling down the lineup - using (var stream = await _httpClient.Get(new HttpRequestOptions + try { - Url = string.Format("{0}/discover.json", GetApiUrl(info, false)), - CancellationToken = CancellationToken.None - })) + // 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 + })) + { + var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream); + + info.DeviceId = response.DeviceID; + } + } + catch (HttpException ex) { - var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream); + if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound) + { + // HDHR4 doesn't have this api + return; + } - info.DeviceId = response.DeviceID; + throw; } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 523f14dfc..2a974b545 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -134,7 +134,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts } }, RequiresOpening = false, - RequiresClosing = false + RequiresClosing = false, + + ReadAtNativeFramerate = true }; return new List<MediaSourceInfo> { mediaSource }; @@ -146,5 +148,10 @@ 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/SatIp/ChannelScan.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/ChannelScan.cs new file mode 100644 index 000000000..fdeae25b0 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/ChannelScan.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using IniParser; +using IniParser.Model; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Logging; +using MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp +{ + public class ChannelScan + { + private readonly ILogger _logger; + + public ChannelScan(ILogger logger) + { + _logger = logger; + } + + public async Task<List<ChannelInfo>> Scan(TunerHostInfo info, CancellationToken cancellationToken) + { + var ini = info.SourceA.Split('|')[1]; + var resource = GetType().Assembly.GetManifestResourceNames().FirstOrDefault(i => i.EndsWith(ini, StringComparison.OrdinalIgnoreCase)); + + _logger.Info("Opening ini file {0}", resource); + var list = new List<ChannelInfo>(); + + using (var stream = GetType().Assembly.GetManifestResourceStream(resource)) + { + using (var reader = new StreamReader(stream)) + { + var parser = new StreamIniDataParser(); + var data = parser.ReadData(reader); + + var count = GetInt(data, "DVB", "0", 0); + + _logger.Info("DVB Count: {0}", count); + + var index = 1; + var source = "1"; + + while (index <= count) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var rtspSession = new RtspSession(info.Url, _logger)) + { + float percent = count == 0 ? 0 : (float)(index) / count; + percent = Math.Max(percent * 100, 100); + + //SetControlPropertyThreadSafe(pgbSearchResult, "Value", (int)percent); + var strArray = data["DVB"][index.ToString(CultureInfo.InvariantCulture)].Split(','); + + string tuning; + if (strArray[4] == "S2") + { + tuning = string.Format("src={0}&freq={1}&pol={2}&sr={3}&fec={4}&msys=dvbs2&mtype={5}&plts=on&ro=0.35&pids=0,16,17,18,20", source, strArray[0], strArray[1].ToLower(), strArray[2].ToLower(), strArray[3], strArray[5].ToLower()); + } + else + { + tuning = string.Format("src={0}&freq={1}&pol={2}&sr={3}&fec={4}&msys=dvbs&mtype={5}&pids=0,16,17,18,20", source, strArray[0], strArray[1].ToLower(), strArray[2], strArray[3], strArray[5].ToLower()); + } + + rtspSession.Setup(tuning, "unicast"); + + rtspSession.Play(string.Empty); + + int signallevel; + int signalQuality; + rtspSession.Describe(out signallevel, out signalQuality); + + await Task.Delay(500).ConfigureAwait(false); + index++; + } + } + } + } + + return list; + } + + private int GetInt(IniData data, string s1, string s2, int defaultValue) + { + var value = data[s1][s2]; + int numericValue; + if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out numericValue)) + { + return numericValue; + } + + return defaultValue; + } + } + + public class SatChannel + { + // TODO: Add properties + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspMethod.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspMethod.cs new file mode 100644 index 000000000..5f286f1db --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspMethod.cs @@ -0,0 +1,88 @@ +/* + Copyright (C) <2007-2016> <Kay Diefenthal> + + SatIp.RtspSample is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + SatIp.RtspSample is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SatIp.RtspSample. If not, see <http://www.gnu.org/licenses/>. +*/ + +using System.Collections.Generic; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp +{ + /// <summary> + /// Standard RTSP request methods. + /// </summary> + public sealed class RtspMethod + { + public override int GetHashCode() + { + return (_name != null ? _name.GetHashCode() : 0); + } + + private readonly string _name; + private static readonly IDictionary<string, RtspMethod> _values = new Dictionary<string, RtspMethod>(); + + public static readonly RtspMethod Describe = new RtspMethod("DESCRIBE"); + public static readonly RtspMethod Announce = new RtspMethod("ANNOUNCE"); + public static readonly RtspMethod GetParameter = new RtspMethod("GET_PARAMETER"); + public static readonly RtspMethod Options = new RtspMethod("OPTIONS"); + public static readonly RtspMethod Pause = new RtspMethod("PAUSE"); + public static readonly RtspMethod Play = new RtspMethod("PLAY"); + public static readonly RtspMethod Record = new RtspMethod("RECORD"); + public static readonly RtspMethod Redirect = new RtspMethod("REDIRECT"); + public static readonly RtspMethod Setup = new RtspMethod("SETUP"); + public static readonly RtspMethod SetParameter = new RtspMethod("SET_PARAMETER"); + public static readonly RtspMethod Teardown = new RtspMethod("TEARDOWN"); + + private RtspMethod(string name) + { + _name = name; + _values.Add(name, this); + } + + public override string ToString() + { + return _name; + } + + public override bool Equals(object obj) + { + var method = obj as RtspMethod; + if (method != null && this == method) + { + return true; + } + return false; + } + + public static ICollection<RtspMethod> Values + { + get { return _values.Values; } + } + + public static explicit operator RtspMethod(string name) + { + RtspMethod value; + if (!_values.TryGetValue(name, out value)) + { + return null; + } + return value; + } + + public static implicit operator string(RtspMethod method) + { + return method._name; + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspRequest.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspRequest.cs new file mode 100644 index 000000000..600eda02d --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspRequest.cs @@ -0,0 +1,140 @@ +/* + Copyright (C) <2007-2016> <Kay Diefenthal> + + SatIp.RtspSample is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + SatIp.RtspSample is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SatIp.RtspSample. If not, see <http://www.gnu.org/licenses/>. +*/ + +using System.Collections.Generic; +using System.Text; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp +{ + /// <summary> + /// A simple class that can be used to serialise RTSP requests. + /// </summary> + public class RtspRequest + { + private readonly RtspMethod _method; + private readonly string _uri; + private readonly int _majorVersion; + private readonly int _minorVersion; + private IDictionary<string, string> _headers = new Dictionary<string, string>(); + private string _body = string.Empty; + + /// <summary> + /// Initialise a new instance of the <see cref="RtspRequest"/> class. + /// </summary> + /// <param name="method">The request method.</param> + /// <param name="uri">The request URI</param> + /// <param name="majorVersion">The major version number.</param> + /// <param name="minorVersion">The minor version number.</param> + public RtspRequest(RtspMethod method, string uri, int majorVersion, int minorVersion) + { + _method = method; + _uri = uri; + _majorVersion = majorVersion; + _minorVersion = minorVersion; + } + + /// <summary> + /// Get the request method. + /// </summary> + public RtspMethod Method + { + get + { + return _method; + } + } + + /// <summary> + /// Get the request URI. + /// </summary> + public string Uri + { + get + { + return _uri; + } + } + + /// <summary> + /// Get the request major version number. + /// </summary> + public int MajorVersion + { + get + { + return _majorVersion; + } + } + + /// <summary> + /// Get the request minor version number. + /// </summary> + public int MinorVersion + { + get + { + return _minorVersion; + } + } + + /// <summary> + /// Get or set the request headers. + /// </summary> + public IDictionary<string, string> Headers + { + get + { + return _headers; + } + set + { + _headers = value; + } + } + + /// <summary> + /// Get or set the request body. + /// </summary> + public string Body + { + get + { + return _body; + } + set + { + _body = value; + } + } + + /// <summary> + /// Serialise this request. + /// </summary> + /// <returns>raw request bytes</returns> + public byte[] Serialise() + { + var request = new StringBuilder(); + request.AppendFormat("{0} {1} RTSP/{2}.{3}\r\n", _method, _uri, _majorVersion, _minorVersion); + foreach (var header in _headers) + { + request.AppendFormat("{0}: {1}\r\n", header.Key, header.Value); + } + request.AppendFormat("\r\n{0}", _body); + return Encoding.UTF8.GetBytes(request.ToString()); + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspResponse.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspResponse.cs new file mode 100644 index 000000000..97290623b --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspResponse.cs @@ -0,0 +1,149 @@ +/* + Copyright (C) <2007-2016> <Kay Diefenthal> + + SatIp.RtspSample is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + SatIp.RtspSample is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SatIp.RtspSample. If not, see <http://www.gnu.org/licenses/>. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp +{ + /// <summary> + /// A simple class that can be used to deserialise RTSP responses. + /// </summary> + public class RtspResponse + { + private static readonly Regex RegexStatusLine = new Regex(@"RTSP/(\d+)\.(\d+)\s+(\d+)\s+([^.]+?)\r\n(.*)", RegexOptions.Singleline); + + private int _majorVersion = 1; + private int _minorVersion; + private RtspStatusCode _statusCode; + private string _reasonPhrase; + private IDictionary<string, string> _headers; + private string _body; + + /// <summary> + /// Initialise a new instance of the <see cref="RtspResponse"/> class. + /// </summary> + private RtspResponse() + { + } + + /// <summary> + /// Get the response major version number. + /// </summary> + public int MajorVersion + { + get + { + return _majorVersion; + } + } + + /// <summary> + /// Get the response minor version number. + /// </summary> + public int MinorVersion + { + get + { + return _minorVersion; + } + } + + /// <summary> + /// Get the response status code. + /// </summary> + public RtspStatusCode StatusCode + { + get + { + return _statusCode; + } + } + + /// <summary> + /// Get the response reason phrase. + /// </summary> + public string ReasonPhrase + { + get + { + return _reasonPhrase; + } + } + + /// <summary> + /// Get the response headers. + /// </summary> + public IDictionary<string, string> Headers + { + get + { + return _headers; + } + } + + /// <summary> + /// Get the response body. + /// </summary> + public string Body + { + get + { + return _body; + } + set + { + _body = value; + } + } + + /// <summary> + /// Deserialise/parse an RTSP response. + /// </summary> + /// <param name="responseBytes">The raw response bytes.</param> + /// <param name="responseByteCount">The number of valid bytes in the response.</param> + /// <returns>a response object</returns> + public static RtspResponse Deserialise(byte[] responseBytes, int responseByteCount) + { + var response = new RtspResponse(); + var responseString = Encoding.UTF8.GetString(responseBytes, 0, responseByteCount); + + var m = RegexStatusLine.Match(responseString); + if (m.Success) + { + response._majorVersion = int.Parse(m.Groups[1].Captures[0].Value); + response._minorVersion = int.Parse(m.Groups[2].Captures[0].Value); + response._statusCode = (RtspStatusCode)int.Parse(m.Groups[3].Captures[0].Value); + response._reasonPhrase = m.Groups[4].Captures[0].Value; + responseString = m.Groups[5].Captures[0].Value; + } + + var sections = responseString.Split(new[] { "\r\n\r\n" }, StringSplitOptions.None); + response._body = sections[1]; + var headers = sections[0].Split(new[] { "\r\n" }, StringSplitOptions.None); + response._headers = new Dictionary<string, string>(); + foreach (var headerInfo in headers.Select(header => header.Split(':'))) + { + response._headers.Add(headerInfo[0], headerInfo[1].Trim()); + } + return response; + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspSession.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspSession.cs new file mode 100644 index 000000000..71b3f8a18 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspSession.cs @@ -0,0 +1,688 @@ +/* + Copyright (C) <2007-2016> <Kay Diefenthal> + + SatIp.RtspSample is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + SatIp.RtspSample is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SatIp.RtspSample. If not, see <http://www.gnu.org/licenses/>. +*/ + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text.RegularExpressions; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp +{ + public class RtspSession : IDisposable + { + #region Private Fields + private static readonly Regex RegexRtspSessionHeader = new Regex(@"\s*([^\s;]+)(;timeout=(\d+))?"); + private const int DefaultRtspSessionTimeout = 30; // unit = s + private static readonly Regex RegexDescribeResponseSignalInfo = new Regex(@";tuner=\d+,(\d+),(\d+),(\d+),", RegexOptions.Singleline | RegexOptions.IgnoreCase); + private string _address; + private string _rtspSessionId; + + public string RtspSessionId + { + get { return _rtspSessionId; } + set { _rtspSessionId = value; } + } + private int _rtspSessionTimeToLive = 0; + private string _rtspStreamId; + private int _clientRtpPort; + private int _clientRtcpPort; + private int _serverRtpPort; + private int _serverRtcpPort; + private int _rtpPort; + private int _rtcpPort; + private string _rtspStreamUrl; + private string _destination; + private string _source; + private string _transport; + private int _signalLevel; + private int _signalQuality; + private Socket _rtspSocket; + private int _rtspSequenceNum = 1; + private bool _disposed = false; + private readonly ILogger _logger; + #endregion + + #region Constructor + + public RtspSession(string address, ILogger logger) + { + if (string.IsNullOrWhiteSpace(address)) + { + throw new ArgumentNullException("address"); + } + + _address = address; + _logger = logger; + + _logger.Info("Creating RtspSession with url {0}", address); + } + ~RtspSession() + { + Dispose(false); + } + #endregion + + #region Properties + + #region Rtsp + + public string RtspStreamId + { + get { return _rtspStreamId; } + set { if (_rtspStreamId != value) { _rtspStreamId = value; OnPropertyChanged("RtspStreamId"); } } + } + public string RtspStreamUrl + { + get { return _rtspStreamUrl; } + set { if (_rtspStreamUrl != value) { _rtspStreamUrl = value; OnPropertyChanged("RtspStreamUrl"); } } + } + + public int RtspSessionTimeToLive + { + get + { + if (_rtspSessionTimeToLive == 0) + _rtspSessionTimeToLive = DefaultRtspSessionTimeout; + return _rtspSessionTimeToLive * 1000 - 20; + } + set { if (_rtspSessionTimeToLive != value) { _rtspSessionTimeToLive = value; OnPropertyChanged("RtspSessionTimeToLive"); } } + } + + #endregion + + #region Rtp Rtcp + + /// <summary> + /// The LocalEndPoint Address + /// </summary> + public string Destination + { + get + { + if (string.IsNullOrEmpty(_destination)) + { + var result = ""; + var host = Dns.GetHostName(); + var hostentry = Dns.GetHostEntry(host); + foreach (var ip in hostentry.AddressList.Where(ip => ip.AddressFamily == AddressFamily.InterNetwork)) + { + result = ip.ToString(); + } + + _destination = result; + } + return _destination; + } + set + { + if (_destination != value) + { + _destination = value; + OnPropertyChanged("Destination"); + } + } + } + + /// <summary> + /// The RemoteEndPoint Address + /// </summary> + public string Source + { + get { return _source; } + set + { + if (_source != value) + { + _source = value; + OnPropertyChanged("Source"); + } + } + } + + /// <summary> + /// The Media Data Delivery RemoteEndPoint Port if we use Unicast + /// </summary> + public int ServerRtpPort + { + get + { + return _serverRtpPort; + } + set { if (_serverRtpPort != value) { _serverRtpPort = value; OnPropertyChanged("ServerRtpPort"); } } + } + + /// <summary> + /// The Media Metadata Delivery RemoteEndPoint Port if we use Unicast + /// </summary> + public int ServerRtcpPort + { + get { return _serverRtcpPort; } + set { if (_serverRtcpPort != value) { _serverRtcpPort = value; OnPropertyChanged("ServerRtcpPort"); } } + } + + /// <summary> + /// The Media Data Delivery LocalEndPoint Port if we use Unicast + /// </summary> + public int ClientRtpPort + { + get { return _clientRtpPort; } + set { if (_clientRtpPort != value) { _clientRtpPort = value; OnPropertyChanged("ClientRtpPort"); } } + } + + /// <summary> + /// The Media Metadata Delivery LocalEndPoint Port if we use Unicast + /// </summary> + public int ClientRtcpPort + { + get { return _clientRtcpPort; } + set { if (_clientRtcpPort != value) { _clientRtcpPort = value; OnPropertyChanged("ClientRtcpPort"); } } + } + + /// <summary> + /// The Media Data Delivery RemoteEndPoint Port if we use Multicast + /// </summary> + public int RtpPort + { + get { return _rtpPort; } + set { if (_rtpPort != value) { _rtpPort = value; OnPropertyChanged("RtpPort"); } } + } + + /// <summary> + /// The Media Meta Delivery RemoteEndPoint Port if we use Multicast + /// </summary> + public int RtcpPort + { + get { return _rtcpPort; } + set { if (_rtcpPort != value) { _rtcpPort = value; OnPropertyChanged("RtcpPort"); } } + } + + #endregion + + public string Transport + { + get + { + if (string.IsNullOrEmpty(_transport)) + { + _transport = "unicast"; + } + return _transport; + } + set + { + if (_transport != value) + { + _transport = value; + OnPropertyChanged("Transport"); + } + } + } + public int SignalLevel + { + get { return _signalLevel; } + set { if (_signalLevel != value) { _signalLevel = value; OnPropertyChanged("SignalLevel"); } } + } + public int SignalQuality + { + get { return _signalQuality; } + set { if (_signalQuality != value) { _signalQuality = value; OnPropertyChanged("SignalQuality"); } } + } + + #endregion + + #region Private Methods + + private void ProcessSessionHeader(string sessionHeader, string response) + { + if (!string.IsNullOrEmpty(sessionHeader)) + { + var m = RegexRtspSessionHeader.Match(sessionHeader); + if (!m.Success) + { + _logger.Error("Failed to tune, RTSP {0} response session header {1} format not recognised", response, sessionHeader); + } + _rtspSessionId = m.Groups[1].Captures[0].Value; + _rtspSessionTimeToLive = m.Groups[3].Captures.Count == 1 ? int.Parse(m.Groups[3].Captures[0].Value) : DefaultRtspSessionTimeout; + } + } + private void ProcessTransportHeader(string transportHeader) + { + if (!string.IsNullOrEmpty(transportHeader)) + { + var transports = transportHeader.Split(','); + foreach (var transport in transports) + { + if (transport.Trim().StartsWith("RTP/AVP")) + { + var sections = transport.Split(';'); + foreach (var section in sections) + { + var parts = section.Split('='); + if (parts[0].Equals("server_port")) + { + var ports = parts[1].Split('-'); + _serverRtpPort = int.Parse(ports[0]); + _serverRtcpPort = int.Parse(ports[1]); + } + else if (parts[0].Equals("destination")) + { + _destination = parts[1]; + } + else if (parts[0].Equals("port")) + { + var ports = parts[1].Split('-'); + _rtpPort = int.Parse(ports[0]); + _rtcpPort = int.Parse(ports[1]); + } + else if (parts[0].Equals("ttl")) + { + _rtspSessionTimeToLive = int.Parse(parts[1]); + } + else if (parts[0].Equals("source")) + { + _source = parts[1]; + } + else if (parts[0].Equals("client_port")) + { + var ports = parts[1].Split('-'); + var rtp = int.Parse(ports[0]); + var rtcp = int.Parse(ports[1]); + //if (!rtp.Equals(_rtpPort)) + //{ + // Logger.Error("SAT>IP base: server specified RTP client port {0} instead of {1}", rtp, _rtpPort); + //} + //if (!rtcp.Equals(_rtcpPort)) + //{ + // Logger.Error("SAT>IP base: server specified RTCP client port {0} instead of {1}", rtcp, _rtcpPort); + //} + _rtpPort = rtp; + _rtcpPort = rtcp; + } + } + } + } + } + } + private void Connect() + { + _rtspSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + var ip = IPAddress.Parse(_address); + var rtspEndpoint = new IPEndPoint(ip, 554); + _rtspSocket.Connect(rtspEndpoint); + } + private void Disconnect() + { + if (_rtspSocket != null && _rtspSocket.Connected) + { + _rtspSocket.Shutdown(SocketShutdown.Both); + _rtspSocket.Close(); + } + } + private void SendRequest(RtspRequest request) + { + if (_rtspSocket == null) + { + Connect(); + } + try + { + request.Headers.Add("CSeq", _rtspSequenceNum.ToString()); + _rtspSequenceNum++; + byte[] requestBytes = request.Serialise(); + if (_rtspSocket != null) + { + var requestBytesCount = _rtspSocket.Send(requestBytes, requestBytes.Length, SocketFlags.None); + if (requestBytesCount < 1) + { + + } + } + } + catch (Exception e) + { + _logger.Error(e.Message); + } + } + private void ReceiveResponse(out RtspResponse response) + { + response = null; + var responseBytesCount = 0; + byte[] responseBytes = new byte[1024]; + try + { + responseBytesCount = _rtspSocket.Receive(responseBytes, responseBytes.Length, SocketFlags.None); + response = RtspResponse.Deserialise(responseBytes, responseBytesCount); + string contentLengthString; + int contentLength = 0; + if (response.Headers.TryGetValue("Content-Length", out contentLengthString)) + { + contentLength = int.Parse(contentLengthString); + if ((string.IsNullOrEmpty(response.Body) && contentLength > 0) || response.Body.Length < contentLength) + { + if (response.Body == null) + { + response.Body = string.Empty; + } + while (responseBytesCount > 0 && response.Body.Length < contentLength) + { + responseBytesCount = _rtspSocket.Receive(responseBytes, responseBytes.Length, SocketFlags.None); + response.Body += System.Text.Encoding.UTF8.GetString(responseBytes, 0, responseBytesCount); + } + } + } + } + catch (SocketException) + { + } + } + + #endregion + + #region Public Methods + + public RtspStatusCode Setup(string query, string transporttype) + { + + RtspRequest request; + RtspResponse response; + //_rtspClient = new RtspClient(_rtspDevice.ServerAddress); + if ((_rtspSocket == null)) + { + Connect(); + } + if (string.IsNullOrEmpty(_rtspSessionId)) + { + request = new RtspRequest(RtspMethod.Setup, string.Format("rtsp://{0}:{1}/?{2}", _address, 554, query), 1, 0); + switch (transporttype) + { + case "multicast": + request.Headers.Add("Transport", string.Format("RTP/AVP;multicast")); + break; + case "unicast": + var activeTcpConnections = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections(); + var usedPorts = new HashSet<int>(); + foreach (var connection in activeTcpConnections) + { + usedPorts.Add(connection.LocalEndPoint.Port); + } + for (var port = 40000; port <= 65534; port += 2) + { + if (!usedPorts.Contains(port) && !usedPorts.Contains(port + 1)) + { + + _clientRtpPort = port; + _clientRtcpPort = port + 1; + break; + } + } + request.Headers.Add("Transport", string.Format("RTP/AVP;unicast;client_port={0}-{1}", _clientRtpPort, _clientRtcpPort)); + break; + } + } + else + { + request = new RtspRequest(RtspMethod.Setup, string.Format("rtsp://{0}:{1}/?{2}", _address, 554, query), 1, 0); + switch (transporttype) + { + case "multicast": + request.Headers.Add("Transport", string.Format("RTP/AVP;multicast")); + break; + case "unicast": + request.Headers.Add("Transport", string.Format("RTP/AVP;unicast;client_port={0}-{1}", _clientRtpPort, _clientRtcpPort)); + break; + } + + } + SendRequest(request); + ReceiveResponse(out response); + + //if (_rtspClient.SendRequest(request, out response) != RtspStatusCode.Ok) + //{ + // Logger.Error("Failed to tune, non-OK RTSP SETUP status code {0} {1}", response.StatusCode, response.ReasonPhrase); + //} + if (!response.Headers.TryGetValue("com.ses.streamID", out _rtspStreamId)) + { + _logger.Error(string.Format("Failed to tune, not able to locate Stream ID header in RTSP SETUP response")); + } + string sessionHeader; + if (!response.Headers.TryGetValue("Session", out sessionHeader)) + { + _logger.Error(string.Format("Failed to tune, not able to locate Session header in RTSP SETUP response")); + } + ProcessSessionHeader(sessionHeader, "Setup"); + string transportHeader; + if (!response.Headers.TryGetValue("Transport", out transportHeader)) + { + _logger.Error(string.Format("Failed to tune, not able to locate Transport header in RTSP SETUP response")); + } + ProcessTransportHeader(transportHeader); + return response.StatusCode; + } + + public RtspStatusCode Play(string query) + { + if ((_rtspSocket == null)) + { + Connect(); + } + //_rtspClient = new RtspClient(_rtspDevice.ServerAddress); + RtspResponse response; + string data; + if (string.IsNullOrEmpty(query)) + { + data = string.Format("rtsp://{0}:{1}/stream={2}", _address, + 554, _rtspStreamId); + } + else + { + data = string.Format("rtsp://{0}:{1}/stream={2}?{3}", _address, + 554, _rtspStreamId, query); + } + var request = new RtspRequest(RtspMethod.Play, data, 1, 0); + request.Headers.Add("Session", _rtspSessionId); + SendRequest(request); + ReceiveResponse(out response); + //if (_rtspClient.SendRequest(request, out response) != RtspStatusCode.Ok) + //{ + // Logger.Error("Failed to tune, non-OK RTSP SETUP status code {0} {1}", response.StatusCode, response.ReasonPhrase); + //} + //Logger.Info("RtspSession-Play : \r\n {0}", response); + string sessionHeader; + if (!response.Headers.TryGetValue("Session", out sessionHeader)) + { + _logger.Error(string.Format("Failed to tune, not able to locate Session header in RTSP Play response")); + } + ProcessSessionHeader(sessionHeader, "Play"); + string rtpinfoHeader; + if (!response.Headers.TryGetValue("RTP-Info", out rtpinfoHeader)) + { + _logger.Error(string.Format("Failed to tune, not able to locate Rtp-Info header in RTSP Play response")); + } + return response.StatusCode; + } + + public RtspStatusCode Options() + { + if ((_rtspSocket == null)) + { + Connect(); + } + //_rtspClient = new RtspClient(_rtspDevice.ServerAddress); + RtspRequest request; + RtspResponse response; + + + if (string.IsNullOrEmpty(_rtspSessionId)) + { + request = new RtspRequest(RtspMethod.Options, string.Format("rtsp://{0}:{1}/", _address, 554), 1, 0); + } + else + { + request = new RtspRequest(RtspMethod.Options, string.Format("rtsp://{0}:{1}/", _address, 554), 1, 0); + request.Headers.Add("Session", _rtspSessionId); + } + SendRequest(request); + ReceiveResponse(out response); + //if (_rtspClient.SendRequest(request, out response) != RtspStatusCode.Ok) + //{ + // Logger.Error("Failed to tune, non-OK RTSP SETUP status code {0} {1}", response.StatusCode, response.ReasonPhrase); + //} + //Logger.Info("RtspSession-Options : \r\n {0}", response); + string sessionHeader; + if (!response.Headers.TryGetValue("Session", out sessionHeader)) + { + _logger.Error(string.Format("Failed to tune, not able to locate session header in RTSP Options response")); + } + ProcessSessionHeader(sessionHeader, "Options"); + string optionsHeader; + if (!response.Headers.TryGetValue("Public", out optionsHeader)) + { + _logger.Error(string.Format("Failed to tune, not able to Options header in RTSP Options response")); + } + return response.StatusCode; + } + + public RtspStatusCode Describe(out int level, out int quality) + { + if ((_rtspSocket == null)) + { + Connect(); + } + //_rtspClient = new RtspClient(_rtspDevice.ServerAddress); + RtspRequest request; + RtspResponse response; + level = 0; + quality = 0; + + if (string.IsNullOrEmpty(_rtspSessionId)) + { + request = new RtspRequest(RtspMethod.Describe, string.Format("rtsp://{0}:{1}/", _address, 554), 1, 0); + request.Headers.Add("Accept", "application/sdp"); + + } + else + { + request = new RtspRequest(RtspMethod.Describe, string.Format("rtsp://{0}:{1}/stream={2}", _address, 554, _rtspStreamId), 1, 0); + request.Headers.Add("Accept", "application/sdp"); + request.Headers.Add("Session", _rtspSessionId); + } + SendRequest(request); + ReceiveResponse(out response); + //if (_rtspClient.SendRequest(request, out response) != RtspStatusCode.Ok) + //{ + // Logger.Error("Failed to tune, non-OK RTSP Describe status code {0} {1}", response.StatusCode, response.ReasonPhrase); + //} + //Logger.Info("RtspSession-Describe : \r\n {0}", response); + string sessionHeader; + if (!response.Headers.TryGetValue("Session", out sessionHeader)) + { + _logger.Error(string.Format("Failed to tune, not able to locate session header in RTSP Describe response")); + } + ProcessSessionHeader(sessionHeader, "Describe"); + var m = RegexDescribeResponseSignalInfo.Match(response.Body); + if (m.Success) + { + + //isSignalLocked = m.Groups[2].Captures[0].Value.Equals("1"); + level = int.Parse(m.Groups[1].Captures[0].Value) * 100 / 255; // level: 0..255 => 0..100 + quality = int.Parse(m.Groups[3].Captures[0].Value) * 100 / 15; // quality: 0..15 => 0..100 + + } + /* + v=0 + o=- 1378633020884883 1 IN IP4 192.168.2.108 + s=SatIPServer:1 4 + t=0 0 + a=tool:idl4k + m=video 52780 RTP/AVP 33 + c=IN IP4 0.0.0.0 + b=AS:5000 + a=control:stream=4 + a=fmtp:33 ver=1.0;tuner=1,0,0,0,12344,h,dvbs2,,off,,22000,34;pids=0,100,101,102,103,106 + =sendonly + */ + + + return response.StatusCode; + } + + public RtspStatusCode TearDown() + { + if ((_rtspSocket == null)) + { + Connect(); + } + //_rtspClient = new RtspClient(_rtspDevice.ServerAddress); + RtspResponse response; + + var request = new RtspRequest(RtspMethod.Teardown, string.Format("rtsp://{0}:{1}/stream={2}", _address, 554, _rtspStreamId), 1, 0); + request.Headers.Add("Session", _rtspSessionId); + SendRequest(request); + ReceiveResponse(out response); + //if (_rtspClient.SendRequest(request, out response) != RtspStatusCode.Ok) + //{ + // Logger.Error("Failed to tune, non-OK RTSP Teardown status code {0} {1}", response.StatusCode, response.ReasonPhrase); + //} + return response.StatusCode; + } + + #endregion + + #region Public Events + + public event PropertyChangedEventHandler PropertyChanged; + + #endregion + + #region Protected Methods + + protected void OnPropertyChanged(string name) + { + //var handler = PropertyChanged; + //if (handler != null) + //{ + // handler(this, new PropertyChangedEventArgs(name)); + //} + } + + #endregion + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this);//Disconnect(); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + TearDown(); + Disconnect(); + } + } + _disposed = true; + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspStatusCode.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspStatusCode.cs new file mode 100644 index 000000000..6d6d50623 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspStatusCode.cs @@ -0,0 +1,251 @@ +/* + Copyright (C) <2007-2016> <Kay Diefenthal> + + SatIp.RtspSample is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + SatIp.RtspSample is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SatIp.RtspSample. If not, see <http://www.gnu.org/licenses/>. +*/ + +using System.ComponentModel; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp +{ + /// <summary> + /// Standard RTSP status codes. + /// </summary> + public enum RtspStatusCode + { + /// <summary> + /// 100 continue + /// </summary> + Continue = 100, + + /// <summary> + /// 200 OK + /// </summary> + [Description("Okay")] + Ok = 200, + /// <summary> + /// 201 created + /// </summary> + Created = 201, + + /// <summary> + /// 250 low on storage space + /// </summary> + [Description("Low On Storage Space")] + LowOnStorageSpace = 250, + + /// <summary> + /// 300 multiple choices + /// </summary> + [Description("Multiple Choices")] + MultipleChoices = 300, + /// <summary> + /// 301 moved permanently + /// </summary> + [Description("Moved Permanently")] + MovedPermanently = 301, + /// <summary> + /// 302 moved temporarily + /// </summary> + [Description("Moved Temporarily")] + MovedTemporarily = 302, + /// <summary> + /// 303 see other + /// </summary> + [Description("See Other")] + SeeOther = 303, + /// <summary> + /// 304 not modified + /// </summary> + [Description("Not Modified")] + NotModified = 304, + /// <summary> + /// 305 use proxy + /// </summary> + [Description("Use Proxy")] + UseProxy = 305, + + /// <summary> + /// 400 bad request + /// </summary> + [Description("Bad Request")] + BadRequest = 400, + /// <summary> + /// 401 unauthorised + /// </summary> + Unauthorised = 401, + /// <summary> + /// 402 payment required + /// </summary> + [Description("Payment Required")] + PaymentRequired = 402, + /// <summary> + /// 403 forbidden + /// </summary> + Forbidden = 403, + /// <summary> + /// 404 not found + /// </summary> + [Description("Not Found")] + NotFound = 404, + /// <summary> + /// 405 method not allowed + /// </summary> + [Description("Method Not Allowed")] + MethodNotAllowed = 405, + /// <summary> + /// 406 not acceptable + /// </summary> + [Description("Not Acceptable")] + NotAcceptable = 406, + /// <summary> + /// 407 proxy authentication required + /// </summary> + [Description("Proxy Authentication Required")] + ProxyAuthenticationRequred = 407, + /// <summary> + /// 408 request time-out + /// </summary> + [Description("Request Time-Out")] + RequestTimeOut = 408, + + /// <summary> + /// 410 gone + /// </summary> + Gone = 410, + /// <summary> + /// 411 length required + /// </summary> + [Description("Length Required")] + LengthRequired = 411, + /// <summary> + /// 412 precondition failed + /// </summary> + [Description("Precondition Failed")] + PreconditionFailed = 412, + /// <summary> + /// 413 request entity too large + /// </summary> + [Description("Request Entity Too Large")] + RequestEntityTooLarge = 413, + /// <summary> + /// 414 request URI too large + /// </summary> + [Description("Request URI Too Large")] + RequestUriTooLarge = 414, + /// <summary> + /// 415 unsupported media type + /// </summary> + [Description("Unsupported Media Type")] + UnsupportedMediaType = 415, + + /// <summary> + /// 451 parameter not understood + /// </summary> + [Description("Parameter Not Understood")] + ParameterNotUnderstood = 451, + /// <summary> + /// 452 conference not found + /// </summary> + [Description("Conference Not Found")] + ConferenceNotFound = 452, + /// <summary> + /// 453 not enough bandwidth + /// </summary> + [Description("Not Enough Bandwidth")] + NotEnoughBandwidth = 453, + /// <summary> + /// 454 session not found + /// </summary> + [Description("Session Not Found")] + SessionNotFound = 454, + /// <summary> + /// 455 method not valid in this state + /// </summary> + [Description("Method Not Valid In This State")] + MethodNotValidInThisState = 455, + /// <summary> + /// 456 header field not valid for this resource + /// </summary> + [Description("Header Field Not Valid For This Resource")] + HeaderFieldNotValidForThisResource = 456, + /// <summary> + /// 457 invalid range + /// </summary> + [Description("Invalid Range")] + InvalidRange = 457, + /// <summary> + /// 458 parameter is read-only + /// </summary> + [Description("Parameter Is Read-Only")] + ParameterIsReadOnly = 458, + /// <summary> + /// 459 aggregate operation not allowed + /// </summary> + [Description("Aggregate Operation Not Allowed")] + AggregateOperationNotAllowed = 459, + /// <summary> + /// 460 only aggregate operation allowed + /// </summary> + [Description("Only Aggregate Operation Allowed")] + OnlyAggregateOperationAllowed = 460, + /// <summary> + /// 461 unsupported transport + /// </summary> + [Description("Unsupported Transport")] + UnsupportedTransport = 461, + /// <summary> + /// 462 destination unreachable + /// </summary> + [Description("Destination Unreachable")] + DestinationUnreachable = 462, + + /// <summary> + /// 500 internal server error + /// </summary> + [Description("Internal Server Error")] + InternalServerError = 500, + /// <summary> + /// 501 not implemented + /// </summary> + [Description("Not Implemented")] + NotImplemented = 501, + /// <summary> + /// 502 bad gateway + /// </summary> + [Description("Bad Gateway")] + BadGateway = 502, + /// <summary> + /// 503 service unavailable + /// </summary> + [Description("Service Unavailable")] + ServiceUnavailable = 503, + /// <summary> + /// 504 gateway time-out + /// </summary> + [Description("Gateway Time-Out")] + GatewayTimeOut = 504, + /// <summary> + /// 505 RTSP version not supported + /// </summary> + [Description("RTSP Version Not Supported")] + RtspVersionNotSupported = 505, + + /// <summary> + /// 551 option not supported + /// </summary> + [Description("Option Not Supported")] + OptionNotSupported = 551 + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs index da1894bb7..d0a55966f 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs @@ -15,6 +15,7 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Extensions; +using System.Xml.Linq; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp { @@ -171,58 +172,86 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp public async Task<SatIpTunerHostInfo> GetInfo(string url, CancellationToken cancellationToken) { + Uri locationUri = new Uri(url); + string devicetype = ""; + string friendlyname = ""; + string uniquedevicename = ""; + string manufacturer = ""; + string manufacturerurl = ""; + string modelname = ""; + string modeldescription = ""; + string modelnumber = ""; + string modelurl = ""; + string serialnumber = ""; + string presentationurl = ""; + string capabilities = ""; + string m3u = ""; + var document = XDocument.Load(locationUri.AbsoluteUri); + var xnm = new XmlNamespaceManager(new NameTable()); + XNamespace n1 = "urn:ses-com:satip"; + XNamespace n0 = "urn:schemas-upnp-org:device-1-0"; + xnm.AddNamespace("root", n0.NamespaceName); + xnm.AddNamespace("satip:", n1.NamespaceName); + if (document.Root != null) + { + var deviceElement = document.Root.Element(n0 + "device"); + if (deviceElement != null) + { + var devicetypeElement = deviceElement.Element(n0 + "deviceType"); + if (devicetypeElement != null) + devicetype = devicetypeElement.Value; + var friendlynameElement = deviceElement.Element(n0 + "friendlyName"); + if (friendlynameElement != null) + friendlyname = friendlynameElement.Value; + var manufactureElement = deviceElement.Element(n0 + "manufacturer"); + if (manufactureElement != null) + manufacturer = manufactureElement.Value; + var manufactureurlElement = deviceElement.Element(n0 + "manufacturerURL"); + if (manufactureurlElement != null) + manufacturerurl = manufactureurlElement.Value; + var modeldescriptionElement = deviceElement.Element(n0 + "modelDescription"); + if (modeldescriptionElement != null) + modeldescription = modeldescriptionElement.Value; + var modelnameElement = deviceElement.Element(n0 + "modelName"); + if (modelnameElement != null) + modelname = modelnameElement.Value; + var modelnumberElement = deviceElement.Element(n0 + "modelNumber"); + if (modelnumberElement != null) + modelnumber = modelnumberElement.Value; + var modelurlElement = deviceElement.Element(n0 + "modelURL"); + if (modelurlElement != null) + modelurl = modelurlElement.Value; + var serialnumberElement = deviceElement.Element(n0 + "serialNumber"); + if (serialnumberElement != null) + serialnumber = serialnumberElement.Value; + var uniquedevicenameElement = deviceElement.Element(n0 + "UDN"); + if (uniquedevicenameElement != null) uniquedevicename = uniquedevicenameElement.Value; + var presentationUrlElement = deviceElement.Element(n0 + "presentationURL"); + if (presentationUrlElement != null) presentationurl = presentationUrlElement.Value; + var capabilitiesElement = deviceElement.Element(n1 + "X_SATIPCAP"); + if (capabilitiesElement != null) capabilities = capabilitiesElement.Value; + var m3uElement = deviceElement.Element(n1 + "X_SATIPM3U"); + if (m3uElement != null) m3u = m3uElement.Value; + } + } + var result = new SatIpTunerHostInfo { Url = url, + Id = uniquedevicename, IsEnabled = true, Type = SatIpHost.DeviceType, Tuners = 1, - TunersAvailable = 1 + TunersAvailable = 1, + M3UUrl = m3u }; - using (var stream = await _httpClient.Get(url, cancellationToken).ConfigureAwait(false)) - { - using (var streamReader = new StreamReader(stream)) - { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader)) - { - reader.MoveToContent(); - - // Loop through each element - while (reader.Read()) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "device": - using (var subtree = reader.ReadSubtree()) - { - FillFromDeviceNode(result, subtree); - } - break; - default: - reader.Skip(); - break; - } - } - } - } - } - } - - if (string.IsNullOrWhiteSpace(result.DeviceId)) + result.FriendlyName = friendlyname; + if (string.IsNullOrWhiteSpace(result.Id)) { throw new NotImplementedException(); } - // Device hasn't implemented an m3u list - if (string.IsNullOrWhiteSpace(result.M3UUrl)) - { - result.IsEnabled = false; - } - else if (!result.M3UUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { var fullM3uUrl = url.Substring(0, url.LastIndexOf('/')); @@ -233,66 +262,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp return result; } - - private void FillFromDeviceNode(SatIpTunerHostInfo info, XmlReader reader) - { - reader.MoveToContent(); - - while (reader.Read()) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.LocalName) - { - case "UDN": - { - info.DeviceId = reader.ReadElementContentAsString(); - break; - } - - case "friendlyName": - { - info.FriendlyName = reader.ReadElementContentAsString(); - break; - } - - case "satip:X_SATIPCAP": - case "X_SATIPCAP": - { - // <satip:X_SATIPCAP xmlns:satip="urn:ses-com:satip">DVBS2-2</satip:X_SATIPCAP> - var value = reader.ReadElementContentAsString() ?? string.Empty; - var parts = value.Split(new[] { '-' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 2) - { - int intValue; - if (int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out intValue)) - { - info.TunersAvailable = intValue; - } - - if (int.TryParse(parts[0].Substring(parts[0].Length - 1), NumberStyles.Any, CultureInfo.InvariantCulture, out intValue)) - { - info.Tuners = intValue; - } - } - break; - } - - case "satip:X_SATIPM3U": - case "X_SATIPM3U": - { - // <satip:X_SATIPM3U xmlns:satip="urn:ses-com:satip">/channellist.lua?select=m3u</satip:X_SATIPM3U> - info.M3UUrl = reader.ReadElementContentAsString(); - break; - } - - default: - reader.Skip(); - break; - } - } - } - } } public class SatIpTunerHostInfo : TunerHostInfo diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs index 46a2a8524..1e571c84f 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs @@ -40,7 +40,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp return await new M3uParser(Logger, _fileSystem, _httpClient).Parse(tuner.M3UUrl, ChannelIdPrefix, tuner.Id, cancellationToken).ConfigureAwait(false); } - return new List<ChannelInfo>(); + var channels = await new ChannelScan(Logger).Scan(tuner, cancellationToken).ConfigureAwait(false); + return channels; } public static string DeviceType @@ -163,5 +164,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp return list; } + + public string ApplyDuration(string streamPath, TimeSpan duration) + { + return streamPath; + } } } |
