From fb251448c90ac8905a9f9ad6ec9a1b676aa51922 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 1 Sep 2016 21:54:16 -0400 Subject: Rtp Rtcp fix Discovery #2116 --- .../MediaBrowser.Server.Implementations.csproj | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj') diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 6879c3f40..9a92cf896 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -251,6 +251,18 @@ + + + + + + + + + + + + @@ -258,6 +270,8 @@ + + -- cgit v1.2.3 From d4324b7e893725c1fc42eb482d54184420b9a5d9 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 5 Sep 2016 16:07:36 -0400 Subject: add chapter image error handling --- MediaBrowser.Api/Playback/BaseStreamingService.cs | 3 +- .../Collections/ManualCollectionsFolder.cs | 36 +++++++++++++ MediaBrowser.Controller/Entities/UserView.cs | 3 +- .../MediaBrowser.Controller.csproj | 1 + .../Providers/BaseItemXmlParser.cs | 27 ++++++---- .../Folders/CollectionFolderMetadataService.cs | 14 +++++ .../Collections/CollectionsDynamicFolder.cs | 1 + .../Collections/ManualCollectionsFolder.cs | 36 ------------- .../LiveTv/EmbyTV/EncodedRecorder.cs | 22 +++++++- .../MediaBrowser.Server.Implementations.csproj | 1 - .../MediaEncoder/EncodingManager.cs | 16 +++--- .../UserViews/CollectionFolderImageProvider.cs | 62 ++++++++++++++++++++++ .../MediaBrowser.WebDashboard.csproj | 6 --- 13 files changed, 160 insertions(+), 68 deletions(-) create mode 100644 MediaBrowser.Controller/Collections/ManualCollectionsFolder.cs delete mode 100644 MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs (limited to 'MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj') diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index b419250f7..a979848e2 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -1791,8 +1791,7 @@ namespace MediaBrowser.Api.Playback if (!string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec)) { state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); - state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault(i => MediaEncoder.CanEncodeToAudioCodec(i)) - ?? state.SupportedVideoCodecs.FirstOrDefault(); + state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault(); } } diff --git a/MediaBrowser.Controller/Collections/ManualCollectionsFolder.cs b/MediaBrowser.Controller/Collections/ManualCollectionsFolder.cs new file mode 100644 index 000000000..d2d28e504 --- /dev/null +++ b/MediaBrowser.Controller/Collections/ManualCollectionsFolder.cs @@ -0,0 +1,36 @@ +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Collections +{ + public class ManualCollectionsFolder : BasePluginFolder, IHiddenFromDisplay + { + public ManualCollectionsFolder() + { + Name = "Collections"; + DisplayMediaType = "CollectionFolder"; + } + + public override bool IsHidden + { + get + { + return true; + } + } + + public bool IsHiddenFromUser(User user) + { + return !ConfigurationManager.Configuration.DisplayCollectionsView; + } + + public override string CollectionType + { + get { return Model.Entities.CollectionType.BoxSets; } + } + + public override string GetClientTypeName() + { + return typeof(CollectionFolder).Name; + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 194ba0ee4..35375e7e6 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -113,8 +113,7 @@ namespace MediaBrowser.Controller.Entities { var standaloneTypes = new List { - CollectionType.Playlists, - CollectionType.BoxSets + CollectionType.Playlists }; var collectionFolder = folder as ICollectionFolder; diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index e7eaa1dc0..5e74a3999 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -98,6 +98,7 @@ + diff --git a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs index 4484adb1d..fccbd9211 100644 --- a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs +++ b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs @@ -790,20 +790,25 @@ namespace MediaBrowser.Controller.Providers } default: - if (_validProviderIds.ContainsKey(reader.Name)) - { - var id = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(id)) + { + string readerName = reader.Name; + string providerIdValue; + if (_validProviderIds.TryGetValue(readerName, out providerIdValue)) { - item.SetProviderId(_validProviderIds[reader.Name], id); + var id = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(id)) + { + item.SetProviderId(providerIdValue, id); + } + } + else + { + reader.Skip(); } - } - else - { - reader.Skip(); - } - break; + break; + + } } } diff --git a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs index cdaa38366..2f534c12e 100644 --- a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs +++ b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using CommonIO; +using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -21,4 +22,17 @@ namespace MediaBrowser.Providers.Folders { } } + + public class ManualCollectionsFolderMetadataService : MetadataService + { + protected override void MergeData(MetadataResult source, MetadataResult target, List lockedFields, bool replaceData, bool mergeMetadataSettings) + { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); + } + + public ManualCollectionsFolderMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + { + } + } + } diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs b/MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs index 6cd9e9620..50bb6c559 100644 --- a/MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs +++ b/MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs @@ -2,6 +2,7 @@ using MediaBrowser.Controller.Entities; using System.IO; using CommonIO; +using MediaBrowser.Controller.Collections; namespace MediaBrowser.Server.Implementations.Collections { diff --git a/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs b/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs deleted file mode 100644 index 3e33066ae..000000000 --- a/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs +++ /dev/null @@ -1,36 +0,0 @@ -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Server.Implementations.Collections -{ - public class ManualCollectionsFolder : BasePluginFolder, IHiddenFromDisplay - { - public ManualCollectionsFolder() - { - Name = "Collections"; - DisplayMediaType = "CollectionFolder"; - } - - public override bool IsHidden - { - get - { - return true; - } - } - - public bool IsHiddenFromUser(User user) - { - return !ConfigurationManager.Configuration.DisplayCollectionsView; - } - - public override string CollectionType - { - get { return Model.Entities.CollectionType.BoxSets; } - } - - public override string GetClientTypeName() - { - return typeof(CollectionFolder).Name; - } - } -} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index fc3a507d1..5e7e3a94f 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -53,11 +53,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { + if (mediaSource.Path.IndexOf("m3u8", StringComparison.OrdinalIgnoreCase) != -1) + { + await RecordWithoutTempFile(mediaSource, targetFile, duration, onStarted, cancellationToken) + .ConfigureAwait(false); + + return; + } + var tempfile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts"); try { - await RecordInternal(mediaSource, tempfile, targetFile, duration, onStarted, cancellationToken) + await RecordWithTempFile(mediaSource, tempfile, targetFile, duration, onStarted, cancellationToken) .ConfigureAwait(false); } finally @@ -73,7 +81,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - public async Task RecordInternal(MediaSourceInfo mediaSource, string tempFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) + private async Task RecordWithoutTempFile(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) + { + var durationToken = new CancellationTokenSource(duration); + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + + await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationToken).ConfigureAwait(false); + + _logger.Info("Recording completed to file {0}", targetFile); + } + + private async Task RecordWithTempFile(MediaSourceInfo mediaSource, string tempFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { var httpRequestOptions = new HttpRequestOptions() { diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 9a92cf896..8850f3d35 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -124,7 +124,6 @@ - diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs index 11338df6d..7d0841fa6 100644 --- a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -149,16 +149,16 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder } } - // Add some time for the first chapter to make sure we don't end up with a black image - var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(FirstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); + try + { + // Add some time for the first chapter to make sure we don't end up with a black image + var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(FirstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); - var protocol = MediaProtocol.File; + var protocol = MediaProtocol.File; - var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, video.Path, protocol, null, video.PlayableStreamFileNames); + var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, video.Path, protocol, null, video.PlayableStreamFileNames); - try - { - _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); var tempFile = await _encoder.ExtractVideoImage(inputPath, protocol, video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); File.Copy(tempFile, path, true); @@ -178,7 +178,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder } catch (Exception ex) { - _logger.ErrorException("Error extracting chapter images for {0}", ex, string.Join(",", inputPath)); + _logger.ErrorException("Error extracting chapter images for {0}", ex, string.Join(",", video.Path)); success = false; break; } diff --git a/MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs b/MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs index 29716d33e..2cff4a14f 100644 --- a/MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs +++ b/MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs @@ -13,6 +13,10 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Querying; namespace MediaBrowser.Server.Implementations.UserViews { @@ -109,4 +113,62 @@ namespace MediaBrowser.Server.Implementations.UserViews return await base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex).ConfigureAwait(false); } } + + public class ManualCollectionFolderImageProvider : BaseDynamicImageProvider + { + private readonly ILibraryManager _libraryManager; + + public ManualCollectionFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor) + { + _libraryManager = libraryManager; + } + + public override IEnumerable GetSupportedImages(IHasImages item) + { + return new List + { + ImageType.Primary + }; + } + + protected override async Task> GetItemsWithImages(IHasImages item) + { + var view = (ManualCollectionsFolder)item; + + var recursive = !new[] { CollectionType.Playlists, CollectionType.Channels }.Contains(view.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + + var items = _libraryManager.GetItemList(new InternalItemsQuery + { + Recursive = recursive, + IncludeItemTypes = new[] { typeof(BoxSet).Name }, + Limit = 20, + SortBy = new[] { ItemSortBy.Random } + }); + + return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)).ToList(), 8); + } + + protected override bool Supports(IHasImages item) + { + return item is ManualCollectionsFolder; + } + + protected override async Task CreateImage(IHasImages item, List itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + { + var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png"); + + if (imageType == ImageType.Primary) + { + if (itemsWithImages.Count == 0) + { + return null; + } + + return await CreateThumbCollage(item, itemsWithImages, outputPath, 960, 540).ConfigureAwait(false); + } + + return await base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex).ConfigureAwait(false); + } + } + } diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index d1308a501..2b828b8fc 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -755,9 +755,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -944,9 +941,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest -- cgit v1.2.3 From 62d9eb1ec7da1b7017818e5620c2334ad336ac2f Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 11 Sep 2016 03:33:53 -0400 Subject: rework upnp discovery --- MediaBrowser.Controller/Dlna/IDeviceDiscovery.cs | 14 +- MediaBrowser.Controller/Dlna/ISsdpHandler.cs | 1 - MediaBrowser.Controller/LiveTv/ProgramInfo.cs | 2 + MediaBrowser.Dlna/Main/DlnaEntryPoint.cs | 86 ++- MediaBrowser.Dlna/MediaBrowser.Dlna.csproj | 12 +- MediaBrowser.Dlna/PlayTo/PlayToController.cs | 13 +- MediaBrowser.Dlna/PlayTo/PlayToManager.cs | 26 +- MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs | 213 ++----- MediaBrowser.Dlna/Ssdp/SsdpHandler.cs | 383 ------------ .../MediaBrowser.Model.Portable.csproj | 3 - .../MediaBrowser.Model.net35.csproj | 5 +- MediaBrowser.Model/Configuration/AutoOnOff.cs | 10 - .../Configuration/ServerConfiguration.cs | 3 - MediaBrowser.Model/MediaBrowser.Model.csproj | 1 - .../EntryPoints/ExternalPortForwarding.cs | 66 ++- .../IO/LibraryMonitor.cs | 20 +- .../LiveTv/Listings/SchedulesDirect.cs | 86 ++- .../TunerHosts/HdHomerun/HdHomerunDiscovery.cs | 9 +- .../LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs | 11 +- .../MediaBrowser.Server.Implementations.csproj | 7 +- .../packages.config | 1 - .../MediaBrowser.Server.Mono.csproj | 5 +- MediaBrowser.Server.Mono/app.config | 14 +- .../ApplicationHost.cs | 4 +- .../MediaBrowser.Server.Startup.Common.csproj | 3 +- MediaBrowser.sln | 32 + Mono.Nat/AbstractNatDevice.cs | 97 +++ Mono.Nat/AsyncResults/AsyncResult.cs | 71 +++ Mono.Nat/Enums/MapState.cs | 36 ++ Mono.Nat/Enums/ProtocolType.cs | 36 ++ Mono.Nat/EventArgs/DeviceEventArgs.cs | 45 ++ Mono.Nat/Exceptions/MappingException.cs | 87 +++ Mono.Nat/IMapper.cs | 50 ++ Mono.Nat/INatDevice.cs | 62 ++ Mono.Nat/ISearcher.cs | 51 ++ Mono.Nat/Mapping.cs | 123 ++++ Mono.Nat/Mono.Nat.csproj | 104 ++++ Mono.Nat/NatProtocol.cs | 9 + Mono.Nat/NatUtility.cs | 264 +++++++++ Mono.Nat/Pmp/AsyncResults/PortMapAsyncResult.cs | 52 ++ Mono.Nat/Pmp/Mappers/PmpMapper.cs | 83 +++ Mono.Nat/Pmp/Pmp.cs | 118 ++++ Mono.Nat/Pmp/PmpConstants.cs | 56 ++ Mono.Nat/Pmp/PmpNatDevice.cs | 347 +++++++++++ Mono.Nat/Pmp/Searchers/PmpSearcher.cs | 149 +++++ Mono.Nat/Properties/AssemblyInfo.cs | 31 + .../Upnp/AsyncResults/GetAllMappingsAsyncResult.cs | 56 ++ Mono.Nat/Upnp/AsyncResults/PortMapAsyncResult.cs | 75 +++ Mono.Nat/Upnp/Mappers/UpnpMapper.cs | 110 ++++ Mono.Nat/Upnp/Messages/DiscoverDeviceMessage.cs | 60 ++ Mono.Nat/Upnp/Messages/ErrorMessage.cs | 63 ++ Mono.Nat/Upnp/Messages/GetServicesMessage.cs | 62 ++ .../Messages/Requests/CreatePortMappingMessage.cs | 75 +++ .../Messages/Requests/DeletePortMappingMessage.cs | 57 ++ .../Requests/GetExternalIPAddressMessage.cs | 51 ++ .../Requests/GetGenericPortMappingEntry.cs | 55 ++ .../Requests/GetSpecificPortMappingEntryMessage.cs | 60 ++ .../Responses/CreatePortMappingResponseMessage.cs | 46 ++ .../Responses/DeletePortMappingResponseMessage.cs | 44 ++ .../GetExternalIPAddressResponseMessage.cs | 53 ++ .../GetGenericPortMappingEntryResponseMessage.cs | 108 ++++ Mono.Nat/Upnp/Messages/UpnpMessage.cs | 132 +++++ Mono.Nat/Upnp/Searchers/UpnpSearcher.cs | 287 +++++++++ Mono.Nat/Upnp/Upnp.cs | 83 +++ Mono.Nat/Upnp/UpnpNatDevice.cs | 651 +++++++++++++++++++++ 65 files changed, 4329 insertions(+), 700 deletions(-) delete mode 100644 MediaBrowser.Model/Configuration/AutoOnOff.cs create mode 100644 Mono.Nat/AbstractNatDevice.cs create mode 100644 Mono.Nat/AsyncResults/AsyncResult.cs create mode 100644 Mono.Nat/Enums/MapState.cs create mode 100644 Mono.Nat/Enums/ProtocolType.cs create mode 100644 Mono.Nat/EventArgs/DeviceEventArgs.cs create mode 100644 Mono.Nat/Exceptions/MappingException.cs create mode 100644 Mono.Nat/IMapper.cs create mode 100644 Mono.Nat/INatDevice.cs create mode 100644 Mono.Nat/ISearcher.cs create mode 100644 Mono.Nat/Mapping.cs create mode 100644 Mono.Nat/Mono.Nat.csproj create mode 100644 Mono.Nat/NatProtocol.cs create mode 100644 Mono.Nat/NatUtility.cs create mode 100644 Mono.Nat/Pmp/AsyncResults/PortMapAsyncResult.cs create mode 100644 Mono.Nat/Pmp/Mappers/PmpMapper.cs create mode 100644 Mono.Nat/Pmp/Pmp.cs create mode 100644 Mono.Nat/Pmp/PmpConstants.cs create mode 100644 Mono.Nat/Pmp/PmpNatDevice.cs create mode 100644 Mono.Nat/Pmp/Searchers/PmpSearcher.cs create mode 100644 Mono.Nat/Properties/AssemblyInfo.cs create mode 100644 Mono.Nat/Upnp/AsyncResults/GetAllMappingsAsyncResult.cs create mode 100644 Mono.Nat/Upnp/AsyncResults/PortMapAsyncResult.cs create mode 100644 Mono.Nat/Upnp/Mappers/UpnpMapper.cs create mode 100644 Mono.Nat/Upnp/Messages/DiscoverDeviceMessage.cs create mode 100644 Mono.Nat/Upnp/Messages/ErrorMessage.cs create mode 100644 Mono.Nat/Upnp/Messages/GetServicesMessage.cs create mode 100644 Mono.Nat/Upnp/Messages/Requests/CreatePortMappingMessage.cs create mode 100644 Mono.Nat/Upnp/Messages/Requests/DeletePortMappingMessage.cs create mode 100644 Mono.Nat/Upnp/Messages/Requests/GetExternalIPAddressMessage.cs create mode 100644 Mono.Nat/Upnp/Messages/Requests/GetGenericPortMappingEntry.cs create mode 100644 Mono.Nat/Upnp/Messages/Requests/GetSpecificPortMappingEntryMessage.cs create mode 100644 Mono.Nat/Upnp/Messages/Responses/CreatePortMappingResponseMessage.cs create mode 100644 Mono.Nat/Upnp/Messages/Responses/DeletePortMappingResponseMessage.cs create mode 100644 Mono.Nat/Upnp/Messages/Responses/GetExternalIPAddressResponseMessage.cs create mode 100644 Mono.Nat/Upnp/Messages/Responses/GetGenericPortMappingEntryResponseMessage.cs create mode 100644 Mono.Nat/Upnp/Messages/UpnpMessage.cs create mode 100644 Mono.Nat/Upnp/Searchers/UpnpSearcher.cs create mode 100644 Mono.Nat/Upnp/Upnp.cs create mode 100644 Mono.Nat/Upnp/UpnpNatDevice.cs (limited to 'MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj') diff --git a/MediaBrowser.Controller/Dlna/IDeviceDiscovery.cs b/MediaBrowser.Controller/Dlna/IDeviceDiscovery.cs index e8083b363..d2c5b9e4e 100644 --- a/MediaBrowser.Controller/Dlna/IDeviceDiscovery.cs +++ b/MediaBrowser.Controller/Dlna/IDeviceDiscovery.cs @@ -1,10 +1,20 @@ using System; +using System.Collections.Generic; +using System.Net; +using MediaBrowser.Model.Events; namespace MediaBrowser.Controller.Dlna { public interface IDeviceDiscovery { - event EventHandler DeviceDiscovered; - event EventHandler DeviceLeft; + event EventHandler> DeviceDiscovered; + event EventHandler> DeviceLeft; + } + + public class UpnpDeviceInfo + { + public Uri Location { get; set; } + public Dictionary Headers { get; set; } + public IPEndPoint LocalEndPoint { get; set; } } } diff --git a/MediaBrowser.Controller/Dlna/ISsdpHandler.cs b/MediaBrowser.Controller/Dlna/ISsdpHandler.cs index e4126ddcf..ec3a00aad 100644 --- a/MediaBrowser.Controller/Dlna/ISsdpHandler.cs +++ b/MediaBrowser.Controller/Dlna/ISsdpHandler.cs @@ -4,6 +4,5 @@ namespace MediaBrowser.Controller.Dlna { public interface ISsdpHandler { - event EventHandler MessageReceived; } } diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs index ea5e6dbc6..d0377fbfd 100644 --- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs @@ -107,6 +107,8 @@ namespace MediaBrowser.Controller.LiveTv /// The image URL. public string ImageUrl { get; set; } + public string LogoImageUrl { get; set; } + /// /// Gets or sets a value indicating whether this instance has image. /// diff --git a/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs b/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs index 9f2726b31..af03f325f 100644 --- a/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs +++ b/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs @@ -14,8 +14,10 @@ using MediaBrowser.Dlna.Ssdp; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using MediaBrowser.Controller.MediaEncoding; +using Rssdp; namespace MediaBrowser.Dlna.Main { @@ -38,12 +40,11 @@ namespace MediaBrowser.Dlna.Main private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaEncoder _mediaEncoder; - private readonly SsdpHandler _ssdpHandler; private readonly IDeviceDiscovery _deviceDiscovery; - private readonly List _registeredServerIds = new List(); private bool _ssdpHandlerStarted; private bool _dlnaServerStarted; + private SsdpDevicePublisher _Publisher; public DlnaEntryPoint(IServerConfigurationManager config, ILogManager logManager, @@ -58,7 +59,7 @@ namespace MediaBrowser.Dlna.Main IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, - ISsdpHandler ssdpHandler, IDeviceDiscovery deviceDiscovery, IMediaEncoder mediaEncoder) + IDeviceDiscovery deviceDiscovery, IMediaEncoder mediaEncoder) { _config = config; _appHost = appHost; @@ -74,7 +75,6 @@ namespace MediaBrowser.Dlna.Main _mediaSourceManager = mediaSourceManager; _deviceDiscovery = deviceDiscovery; _mediaEncoder = mediaEncoder; - _ssdpHandler = (SsdpHandler)ssdpHandler; _logger = logManager.GetLogger("Dlna"); } @@ -154,7 +154,7 @@ namespace MediaBrowser.Dlna.Main { try { - _ssdpHandler.Start(); + StartPublishing(); _ssdpHandlerStarted = true; StartDeviceDiscovery(); @@ -165,13 +165,16 @@ namespace MediaBrowser.Dlna.Main } } + private void StartPublishing() + { + _Publisher = new SsdpDevicePublisher(); + } + private void StartDeviceDiscovery() { try { - ((DeviceDiscovery)_deviceDiscovery).Start(_ssdpHandler); - - //DlnaChannel.Current.Start(() => _registeredServerIds.ToList()); + ((DeviceDiscovery)_deviceDiscovery).Start(); } catch (Exception ex) { @@ -199,8 +202,6 @@ namespace MediaBrowser.Dlna.Main { ((DeviceDiscovery)_deviceDiscovery).Dispose(); - _ssdpHandler.Dispose(); - _ssdpHandlerStarted = false; } catch (Exception ex) @@ -225,6 +226,14 @@ namespace MediaBrowser.Dlna.Main private async Task RegisterServerEndpoints() { + if (!_config.GetDlnaConfiguration().BlastAliveMessages) + { + return; + } + + var cacheLength = _config.GetDlnaConfiguration().BlastAliveMessageIntervalSeconds*2; + _Publisher.SupportPnpRootDevice = true; + foreach (var address in await _appHost.GetLocalIpAddresses().ConfigureAwait(false)) { //if (IPAddress.IsLoopback(address)) @@ -234,25 +243,41 @@ namespace MediaBrowser.Dlna.Main //} var addressString = address.ToString(); - var udn = addressString.GetMD5().ToString("N"); - - var descriptorURI = "/dlna/" + udn + "/description.xml"; - - var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorURI); var services = new List { - "upnp:rootdevice", "urn:schemas-upnp-org:device:MediaServer:1", "urn:schemas-upnp-org:service:ContentDirectory:1", "urn:schemas-upnp-org:service:ConnectionManager:1", - "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1", - "uuid:" + udn + "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" }; - _ssdpHandler.RegisterNotification(udn, uri, address, services); + var udn = (addressString).GetMD5().ToString("N"); + + foreach (var fullService in services) + { + var descriptorURI = "/dlna/" + udn + "/description.xml"; + var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorURI); + + var service = fullService.Replace("urn:", string.Empty).Replace(":1", string.Empty); - _registeredServerIds.Add(udn); + var serviceParts = service.Split(':'); + + var deviceTypeNamespace = serviceParts[0].Replace('.', '-'); + + _Publisher.AddDevice(new SsdpRootDevice + { + CacheLifetime = TimeSpan.FromSeconds(cacheLength), //How long SSDP clients can cache this info. + Location = uri, // Must point to the URL that serves your devices UPnP description document. + DeviceTypeNamespace = deviceTypeNamespace, + DeviceClass = serviceParts[1], + DeviceType = serviceParts[2], + FriendlyName = "Emby Server", + Manufacturer = "Emby", + ModelName = "Emby Server", + Uuid = udn // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. + }); + } } } @@ -315,20 +340,23 @@ namespace MediaBrowser.Dlna.Main public void DisposeDlnaServer() { - foreach (var id in _registeredServerIds) + if (_Publisher != null) { - try - { - _ssdpHandler.UnregisterNotification(id); - } - catch (Exception ex) + var devices = _Publisher.Devices.ToList(); + foreach (var device in devices) { - _logger.ErrorException("Error unregistering server", ex); + try + { + _Publisher.RemoveDevice(device); + } + catch (Exception ex) + { + _logger.ErrorException("Error sending bye bye", ex); + } } + _Publisher.Dispose(); } - _registeredServerIds.Clear(); - _dlnaServerStarted = false; } } diff --git a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj index d10a5f7b5..b25376d1b 100644 --- a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj +++ b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj @@ -1,5 +1,5 @@  - + Debug @@ -14,6 +14,7 @@ 2.0 v4.5 ..\ + true @@ -23,7 +24,7 @@ DEBUG;TRACE prompt 4 - v4.5 + v4.5.1 none @@ -50,8 +51,15 @@ ..\packages\Patterns.Logging.1.0.0.2\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.Logging.dll + + ..\ThirdParty\rssdp\Rssdp.NetFx40.dll + + + ..\ThirdParty\rssdp\Rssdp.Portable.dll + + diff --git a/MediaBrowser.Dlna/PlayTo/PlayToController.cs b/MediaBrowser.Dlna/PlayTo/PlayToController.cs index 5622885fc..d958d0e37 100644 --- a/MediaBrowser.Dlna/PlayTo/PlayToController.cs +++ b/MediaBrowser.Dlna/PlayTo/PlayToController.cs @@ -19,6 +19,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Events; namespace MediaBrowser.Dlna.PlayTo { @@ -122,16 +123,18 @@ namespace MediaBrowser.Dlna.PlayTo } } - void _deviceDiscovery_DeviceLeft(object sender, SsdpMessageEventArgs e) + void _deviceDiscovery_DeviceLeft(object sender, GenericEventArgs e) { + var info = e.Argument; + string nts; - e.Headers.TryGetValue("NTS", out nts); + info.Headers.TryGetValue("NTS", out nts); string usn; - if (!e.Headers.TryGetValue("USN", out usn)) usn = String.Empty; + if (!info.Headers.TryGetValue("USN", out usn)) usn = String.Empty; string nt; - if (!e.Headers.TryGetValue("NT", out nt)) nt = String.Empty; + if (!info.Headers.TryGetValue("NT", out nt)) nt = String.Empty; if (usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 && !_disposed) @@ -653,7 +656,7 @@ namespace MediaBrowser.Dlna.PlayTo _device.PlaybackProgress -= _device_PlaybackProgress; _device.PlaybackStopped -= _device_PlaybackStopped; _device.MediaChanged -= _device_MediaChanged; - _deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft; + //_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft; _device.OnDeviceUnavailable = null; _device.Dispose(); diff --git a/MediaBrowser.Dlna/PlayTo/PlayToManager.cs b/MediaBrowser.Dlna/PlayTo/PlayToManager.cs index cd9a7b1f0..6d6986f01 100644 --- a/MediaBrowser.Dlna/PlayTo/PlayToManager.cs +++ b/MediaBrowser.Dlna/PlayTo/PlayToManager.cs @@ -12,7 +12,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; +using System.Threading.Tasks; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Events; namespace MediaBrowser.Dlna.PlayTo { @@ -61,16 +63,17 @@ namespace MediaBrowser.Dlna.PlayTo _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; } - async void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e) + async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs e) { + var info = e.Argument; + string usn; - if (!e.Headers.TryGetValue("USN", out usn)) usn = string.Empty; + if (!info.Headers.TryGetValue("USN", out usn)) usn = string.Empty; string nt; - if (!e.Headers.TryGetValue("NT", out nt)) nt = string.Empty; + if (!info.Headers.TryGetValue("NT", out nt)) nt = string.Empty; - string location; - if (!e.Headers.TryGetValue("Location", out location)) location = string.Empty; + string location = info.Location.ToString(); // It has to report that it's a media renderer if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 && @@ -100,7 +103,7 @@ namespace MediaBrowser.Dlna.PlayTo } } - var uri = new Uri(location); + var uri = info.Location; _logger.Debug("Attempting to create PlayToController from location {0}", location); var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger).ConfigureAwait(false); @@ -121,7 +124,7 @@ namespace MediaBrowser.Dlna.PlayTo if (controller == null) { - var serverAddress = GetServerAddress(e.LocalEndPoint.Address); + var serverAddress = await GetServerAddress(info.LocalEndPoint == null ? null : info.LocalEndPoint.Address).ConfigureAwait(false); string accessToken = null; sessionInfo.SessionController = controller = new PlayToController(sessionInfo, @@ -173,9 +176,14 @@ namespace MediaBrowser.Dlna.PlayTo } } - private string GetServerAddress(IPAddress localIp) + private Task GetServerAddress(IPAddress localIp) { - return _appHost.GetLocalApiUrl(localIp); + if (localIp == null) + { + return _appHost.GetLocalApiUrl(); + } + + return Task.FromResult(_appHost.GetLocalApiUrl(localIp)); } public void Dispose() diff --git a/MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs b/MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs index 68768745e..91dbeb96e 100644 --- a/MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs +++ b/MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs @@ -11,6 +11,8 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; +using MediaBrowser.Model.Events; +using Rssdp; namespace MediaBrowser.Dlna.Ssdp { @@ -20,132 +22,43 @@ namespace MediaBrowser.Dlna.Ssdp private readonly ILogger _logger; private readonly IServerConfigurationManager _config; - private SsdpHandler _ssdpHandler; private readonly CancellationTokenSource _tokenSource; - private readonly IServerApplicationHost _appHost; - public event EventHandler DeviceDiscovered; - public event EventHandler DeviceLeft; - private readonly INetworkManager _networkManager; + public event EventHandler> DeviceDiscovered; + public event EventHandler> DeviceLeft; - public DeviceDiscovery(ILogger logger, IServerConfigurationManager config, IServerApplicationHost appHost, INetworkManager networkManager) + private SsdpDeviceLocator _DeviceLocator; + + public DeviceDiscovery(ILogger logger, IServerConfigurationManager config) { _tokenSource = new CancellationTokenSource(); _logger = logger; _config = config; - _appHost = appHost; - _networkManager = networkManager; - } - - private List GetLocalIpAddresses() - { - return _networkManager.GetLocalIpAddresses().ToList(); - } - - public void Start(SsdpHandler ssdpHandler) - { - _ssdpHandler = ssdpHandler; - _ssdpHandler.MessageReceived += _ssdpHandler_MessageReceived; - - foreach (var localIp in GetLocalIpAddresses()) - { - try - { - CreateListener(localIp); - } - catch (Exception e) - { - _logger.ErrorException("Failed to Initilize Socket", e); - } - } } - async void _ssdpHandler_MessageReceived(object sender, SsdpMessageEventArgs e) + // Call this method from somewhere in your code to start the search. + public void BeginSearch() { - string nts; - e.Headers.TryGetValue("NTS", out nts); - - if (String.Equals(e.Method, "NOTIFY", StringComparison.OrdinalIgnoreCase) && - String.Equals(nts, "ssdp:byebye", StringComparison.OrdinalIgnoreCase) && - !_disposed) - { - EventHelper.FireEventIfNotNull(DeviceLeft, this, e, _logger); - return; - } - - try - { - if (e.LocalEndPoint == null) - { - var ip = (await _appHost.GetLocalIpAddresses().ConfigureAwait(false)).FirstOrDefault(i => !IPAddress.IsLoopback(i)); - if (ip != null) - { - e.LocalEndPoint = new IPEndPoint(ip, 0); - } - } - - if (e.LocalEndPoint != null) - { - TryCreateDevice(e); - } - } - catch (OperationCanceledException) - { - } - catch (Exception ex) - { - _logger.ErrorException("Error creating play to controller", ex); - } - } - - private void CreateListener(IPAddress localIp) - { - Task.Factory.StartNew(async (o) => - { - try - { - _logger.Info("Creating SSDP listener on {0}", localIp); - - var endPoint = new IPEndPoint(localIp, 1900); - - using (var socket = GetMulticastSocket(localIp, endPoint)) - { - var receiveBuffer = new byte[64000]; - - CreateNotifier(localIp); - - while (!_tokenSource.IsCancellationRequested) - { - var receivedBytes = await socket.ReceiveAsync(receiveBuffer, 0, 64000); - - if (receivedBytes > 0) - { - var args = SsdpHelper.ParseSsdpResponse(receiveBuffer); - args.EndPoint = endPoint; - args.LocalEndPoint = new IPEndPoint(localIp, 0); - - _ssdpHandler.LogMessageReceived(args, true); - - TryCreateDevice(args); - } - } - } - - _logger.Info("SSDP listener - Task completed"); - } - catch (OperationCanceledException) - { - } - catch (Exception e) - { - _logger.ErrorException("Error in listener", e); - } - - }, _tokenSource.Token, TaskCreationOptions.LongRunning); + _DeviceLocator = new SsdpDeviceLocator(); + + // (Optional) Set the filter so we only see notifications for devices we care about + // (can be any search target value i.e device type, uuid value etc - any value that appears in the + // DiscoverdSsdpDevice.NotificationType property or that is used with the searchTarget parameter of the Search method). + //_DeviceLocator.NotificationFilter = "upnp:rootdevice"; + + // Connect our event handler so we process devices as they are found + _DeviceLocator.DeviceAvailable += deviceLocator_DeviceAvailable; + _DeviceLocator.DeviceUnavailable += _DeviceLocator_DeviceUnavailable; + // Enable listening for notifications (optional) + _DeviceLocator.StartListeningForNotifications(); + + // Perform a search so we don't have to wait for devices to broadcast notifications + // again to get any results right away (notifications are broadcast periodically). + StartAsyncSearch(); } - private void CreateNotifier(IPAddress localIp) + private void StartAsyncSearch() { Task.Factory.StartNew(async (o) => { @@ -153,7 +66,7 @@ namespace MediaBrowser.Dlna.Ssdp { while (true) { - _ssdpHandler.SendSearchMessage(new IPEndPoint(localIp, 1900)); + await _DeviceLocator.SearchAsync().ConfigureAwait(false); var delay = _config.GetDlnaConfiguration().ClientDiscoveryIntervalSeconds * 1000; @@ -165,66 +78,60 @@ namespace MediaBrowser.Dlna.Ssdp } catch (Exception ex) { - _logger.ErrorException("Error in notifier", ex); + _logger.ErrorException("Error searching for devices", ex); } - }, _tokenSource.Token, TaskCreationOptions.LongRunning); + }, CancellationToken.None, TaskCreationOptions.LongRunning); } - private Socket GetMulticastSocket(IPAddress localIpAddress, EndPoint localEndpoint) + // Process each found device in the event handler + void deviceLocator_DeviceAvailable(object sender, DeviceAvailableEventArgs e) { - var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), localIpAddress)); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4); + var originalHeaders = e.DiscoveredDevice.ResponseHeaders; - socket.Bind(localEndpoint); + var headerDict = originalHeaders == null ? new Dictionary>>() : originalHeaders.ToDictionary(i => i.Key, StringComparer.OrdinalIgnoreCase); - return socket; - } - - private void TryCreateDevice(SsdpMessageEventArgs args) - { - string nts; - args.Headers.TryGetValue("NTS", out nts); + var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); - if (String.Equals(nts, "ssdp:byebye", StringComparison.OrdinalIgnoreCase)) + var args = new GenericEventArgs { - if (String.Equals(args.Method, "NOTIFY", StringComparison.OrdinalIgnoreCase)) + Argument = new UpnpDeviceInfo { - if (!_disposed) - { - EventHelper.FireEventIfNotNull(DeviceLeft, this, args, _logger); - } + Location = e.DiscoveredDevice.DescriptionLocation, + Headers = headers } + }; - return; - } + EventHelper.FireEventIfNotNull(DeviceDiscovered, this, args, _logger); + } - string usn; - if (!args.Headers.TryGetValue("USN", out usn)) usn = string.Empty; + private void _DeviceLocator_DeviceUnavailable(object sender, DeviceUnavailableEventArgs e) + { + var originalHeaders = e.DiscoveredDevice.ResponseHeaders; + + var headerDict = originalHeaders == null ? new Dictionary>>() : originalHeaders.ToDictionary(i => i.Key, StringComparer.OrdinalIgnoreCase); - string nt; - if (!args.Headers.TryGetValue("NT", out nt)) nt = string.Empty; + var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); - // Need to be able to download device description - string location; - if (!args.Headers.TryGetValue("Location", out location) || - string.IsNullOrEmpty(location)) + var args = new GenericEventArgs { - return; - } + Argument = new UpnpDeviceInfo + { + Location = e.DiscoveredDevice.DescriptionLocation, + Headers = headers + } + }; - EventHelper.FireEventIfNotNull(DeviceDiscovered, this, args, _logger); + EventHelper.FireEventIfNotNull(DeviceLeft, this, args, _logger); } - public void Dispose() + public void Start() { - if (_ssdpHandler != null) - { - _ssdpHandler.MessageReceived -= _ssdpHandler_MessageReceived; - } + BeginSearch(); + } + public void Dispose() + { if (!_disposed) { _disposed = true; diff --git a/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs b/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs index 720ea71a0..0d0ca98a2 100644 --- a/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs +++ b/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs @@ -83,90 +83,6 @@ namespace MediaBrowser.Dlna.Ssdp } } - public event EventHandler MessageReceived; - - private async void OnMessageReceived(SsdpMessageEventArgs args, bool isMulticast) - { - if (IgnoreMessage(args, isMulticast)) - { - return; - } - - LogMessageReceived(args, isMulticast); - - var headers = args.Headers; - string st; - - if (string.Equals(args.Method, "M-SEARCH", StringComparison.OrdinalIgnoreCase) && headers.TryGetValue("st", out st)) - { - TimeSpan delay = GetSearchDelay(headers); - - if (_config.GetDlnaConfiguration().EnableDebugLog) - { - _logger.Debug("Delaying search response by {0} seconds", delay.TotalSeconds); - } - - await Task.Delay(delay).ConfigureAwait(false); - - RespondToSearch(args.EndPoint, st); - } - - EventHelper.FireEventIfNotNull(MessageReceived, this, args, _logger); - } - - internal void LogMessageReceived(SsdpMessageEventArgs args, bool isMulticast) - { - var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog; - - if (enableDebugLogging) - { - var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)); - var headerText = string.Join(",", headerTexts.ToArray()); - - var protocol = isMulticast ? "Multicast" : "Unicast"; - var localEndPointString = args.LocalEndPoint == null ? "null" : args.LocalEndPoint.ToString(); - _logger.Debug("{0} message received from {1} on {3}. Protocol: {4} Headers: {2}", args.Method, args.EndPoint, headerText, localEndPointString, protocol); - } - } - - internal bool IgnoreMessage(SsdpMessageEventArgs args, bool isMulticast) - { - string usn; - if (args.Headers.TryGetValue("USN", out usn)) - { - // USN=uuid:b67df29b5c379445fde78c3774ab518d::urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1 - if (RegisteredDevices.Any(i => string.Equals(i.USN, usn, StringComparison.OrdinalIgnoreCase))) - { - //var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)); - //var headerText = string.Join(",", headerTexts.ToArray()); - - //var protocol = isMulticast ? "Multicast" : "Unicast"; - //var localEndPointString = args.LocalEndPoint == null ? "null" : args.LocalEndPoint.ToString(); - //_logger.Debug("IGNORING {0} message received from {1} on {3}. Protocol: {4} Headers: {2}", args.Method, args.EndPoint, headerText, localEndPointString, protocol); - - return true; - } - } - - string serverId; - if (args.Headers.TryGetValue("X-EMBY-SERVERID", out serverId)) - { - if (string.Equals(serverId, _appHost.SystemId, StringComparison.OrdinalIgnoreCase)) - { - //var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)); - //var headerText = string.Join(",", headerTexts.ToArray()); - - //var protocol = isMulticast ? "Multicast" : "Unicast"; - //var localEndPointString = args.LocalEndPoint == null ? "null" : args.LocalEndPoint.ToString(); - //_logger.Debug("IGNORING {0} message received from {1} on {3}. Protocol: {4} Headers: {2}", args.Method, args.EndPoint, headerText, localEndPointString, protocol); - - return true; - } - } - - return false; - } - public IEnumerable RegisteredDevices { get @@ -188,8 +104,6 @@ namespace MediaBrowser.Dlna.Ssdp RestartSocketListener(); ReloadAliveNotifier(); - CreateUnicastClient(); - SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged; SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; } @@ -202,32 +116,6 @@ namespace MediaBrowser.Dlna.Ssdp } } - public void SendSearchMessage(EndPoint localIp) - { - var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - - values["HOST"] = "239.255.255.250:1900"; - values["USER-AGENT"] = "UPnP/1.0 DLNADOC/1.50 Platinum/1.0.4.2"; - values["X-EMBY-SERVERID"] = _appHost.SystemId; - - values["MAN"] = "\"ssdp:discover\""; - - // Search target - values["ST"] = "ssdp:all"; - - // Seconds to delay response - values["MX"] = "3"; - - var header = "M-SEARCH * HTTP/1.1"; - - var msg = new SsdpMessageBuilder().BuildMessage(header, values); - - // UDP is unreliable, so send 3 requests at a time (per Upnp spec, sec 1.1.2) - SendDatagram(msg, _ssdpEndp, localIp, true); - - SendUnicastRequest(msg); - } - public async void SendDatagram(string msg, EndPoint endpoint, EndPoint localAddress, @@ -248,75 +136,6 @@ namespace MediaBrowser.Dlna.Ssdp } } - /// - /// According to the spec: http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0-20080424.pdf - /// Device responses should be delayed a random duration between 0 and this many seconds to balance - /// load for the control point when it processes responses. In my testing kodi times out after mx - /// so we will generate from mx - 1 - /// - /// The mx headers - /// A timepsan for the amount to delay before returning search result. - private TimeSpan GetSearchDelay(Dictionary headers) - { - string mx; - headers.TryGetValue("mx", out mx); - int delaySeconds = 0; - if (!string.IsNullOrWhiteSpace(mx) - && int.TryParse(mx, NumberStyles.Any, CultureInfo.InvariantCulture, out delaySeconds) - && delaySeconds > 1) - { - delaySeconds = new Random().Next(delaySeconds - 1); - } - - return TimeSpan.FromSeconds(delaySeconds); - } - - private void RespondToSearch(EndPoint endpoint, string deviceType) - { - var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog; - - var isLogged = false; - - const string header = "HTTP/1.1 200 OK"; - - foreach (var d in RegisteredDevices) - { - if (string.Equals(deviceType, "ssdp:all", StringComparison.OrdinalIgnoreCase) || - string.Equals(deviceType, d.Type, StringComparison.OrdinalIgnoreCase)) - { - if (!isLogged) - { - if (enableDebugLogging) - { - _logger.Debug("Responding to search from {0} for {1}", endpoint, deviceType); - } - isLogged = true; - } - - var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - - values["CACHE-CONTROL"] = "max-age = 600"; - values["DATE"] = DateTime.Now.ToString("R"); - values["EXT"] = ""; - values["LOCATION"] = d.Descriptor.ToString(); - values["SERVER"] = _serverSignature; - values["ST"] = d.Type; - values["USN"] = d.USN; - - var msg = new SsdpMessageBuilder().BuildMessage(header, values); - - SendDatagram(msg, endpoint, null, false, 2); - SendDatagram(msg, endpoint, new IPEndPoint(d.Address, 0), false, 2); - //SendDatagram(header, values, endpoint, null, true); - - if (enableDebugLogging) - { - _logger.Debug("{1} - Responded to a {0} request to {2}", d.Type, endpoint, d.Address.ToString()); - } - } - } - } - private void RestartSocketListener() { if (_isDisposed) @@ -329,8 +148,6 @@ namespace MediaBrowser.Dlna.Ssdp _multicastSocket = CreateMulticastSocket(); _logger.Info("MultiCast socket created"); - - Receive(); } catch (Exception ex) { @@ -339,74 +156,6 @@ namespace MediaBrowser.Dlna.Ssdp } } - private void Receive() - { - try - { - var buffer = new byte[1024]; - - EndPoint endpoint = new IPEndPoint(IPAddress.Any, SSDPPort); - - _multicastSocket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref endpoint, ReceiveCallback, buffer); - } - catch (ObjectDisposedException) - { - if (!_isDisposed) - { - //StartSocketRetryTimer(); - } - } - catch (Exception ex) - { - _logger.Debug("Error in BeginReceiveFrom", ex); - } - } - - private void ReceiveCallback(IAsyncResult result) - { - if (_isDisposed) - { - return; - } - - try - { - EndPoint endpoint = new IPEndPoint(IPAddress.Any, SSDPPort); - - var length = _multicastSocket.EndReceiveFrom(result, ref endpoint); - - var received = (byte[])result.AsyncState; - - var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog; - - if (enableDebugLogging) - { - _logger.Debug(Encoding.ASCII.GetString(received)); - } - - var args = SsdpHelper.ParseSsdpResponse(received); - args.EndPoint = endpoint; - - OnMessageReceived(args, true); - } - catch (ObjectDisposedException) - { - if (!_isDisposed) - { - //StartSocketRetryTimer(); - } - } - catch (Exception ex) - { - _logger.ErrorException("Failed to read SSDP message", ex); - } - - if (_multicastSocket != null) - { - Receive(); - } - } - public void Dispose() { _config.NamedConfigurationUpdated -= _config_ConfigurationUpdated; @@ -414,7 +163,6 @@ namespace MediaBrowser.Dlna.Ssdp _isDisposed = true; - DisposeUnicastClient(); DisposeSocket(); StopAliveNotifier(); } @@ -523,137 +271,6 @@ namespace MediaBrowser.Dlna.Ssdp } } - private void CreateUnicastClient() - { - if (_unicastClient == null) - { - try - { - _unicastClient = new UdpClient(_unicastPort); - } - catch (Exception ex) - { - _logger.ErrorException("Error creating unicast client", ex); - } - - UnicastSetBeginReceive(); - } - } - - private void DisposeUnicastClient() - { - if (_unicastClient != null) - { - try - { - _unicastClient.Close(); - } - catch (Exception ex) - { - _logger.ErrorException("Error closing unicast client", ex); - } - - _unicastClient = null; - } - } - - /// - /// Listen for Unicast SSDP Responses - /// - private void UnicastSetBeginReceive() - { - try - { - var ipRxEnd = new IPEndPoint(IPAddress.Any, _unicastPort); - var udpListener = new UdpState { EndPoint = ipRxEnd }; - - udpListener.UdpClient = _unicastClient; - _unicastClient.BeginReceive(UnicastReceiveCallback, udpListener); - } - catch (Exception ex) - { - _logger.ErrorException("Error in UnicastSetBeginReceive", ex); - } - } - - /// - /// The UnicastReceiveCallback receives Http Responses - /// and Fired the SatIpDeviceFound Event for adding the SatIpDevice - /// - /// - private void UnicastReceiveCallback(IAsyncResult ar) - { - var udpClient = ((UdpState)(ar.AsyncState)).UdpClient; - var endpoint = ((UdpState)(ar.AsyncState)).EndPoint; - if (udpClient.Client != null) - { - try - { - var responseBytes = udpClient.EndReceive(ar, ref endpoint); - var args = SsdpHelper.ParseSsdpResponse(responseBytes); - - args.EndPoint = endpoint; - - OnMessageReceived(args, false); - - UnicastSetBeginReceive(); - } - catch (ObjectDisposedException) - { - - } - catch (SocketException) - { - - } - catch (Exception) - { - // If called while shutting down, seeing a NullReferenceException inside EndReceive - } - } - } - - private void SendUnicastRequest(string request, int sendCount = 3) - { - if (_unicastClient == null) - { - return; - } - - var ipSsdp = IPAddress.Parse(SSDPAddr); - var ipTxEnd = new IPEndPoint(ipSsdp, SSDPPort); - - SendUnicastRequest(request, ipTxEnd, sendCount); - } - - private async void SendUnicastRequest(string request, IPEndPoint toEndPoint, int sendCount = 3) - { - if (_unicastClient == null) - { - return; - } - - //_logger.Debug("Sending unicast request"); - - byte[] req = Encoding.ASCII.GetBytes(request); - - try - { - for (var i = 0; i < sendCount; i++) - { - if (i > 0) - { - await Task.Delay(50).ConfigureAwait(false); - } - _unicastClient.Send(req, req.Length, toEndPoint); - } - } - catch (Exception ex) - { - _logger.ErrorException("Error in SendUnicastRequest", ex); - } - } - private readonly object _notificationTimerSyncLock = new object(); private int _aliveNotifierIntervalMs; private void ReloadAliveNotifier() diff --git a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj index 351740e6e..ad7dea0a5 100644 --- a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj +++ b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj @@ -175,9 +175,6 @@ Configuration\AccessSchedule.cs - - Configuration\AutoOnOff.cs - Configuration\BaseApplicationConfiguration.cs diff --git a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj index ad3811646..61f2f3f13 100644 --- a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj +++ b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj @@ -147,9 +147,6 @@ Configuration\AccessSchedule.cs - - Configuration\AutoOnOff.cs - Configuration\BaseApplicationConfiguration.cs @@ -1193,4 +1190,4 @@ --> - + \ No newline at end of file diff --git a/MediaBrowser.Model/Configuration/AutoOnOff.cs b/MediaBrowser.Model/Configuration/AutoOnOff.cs deleted file mode 100644 index e911a0ff1..000000000 --- a/MediaBrowser.Model/Configuration/AutoOnOff.cs +++ /dev/null @@ -1,10 +0,0 @@ - -namespace MediaBrowser.Model.Configuration -{ - public enum AutoOnOff - { - Auto, - Enabled, - Disabled - } -} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 40ac4be8a..5cf266674 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -183,8 +183,6 @@ namespace MediaBrowser.Model.Configuration public int RemoteClientBitrateLimit { get; set; } - public AutoOnOff EnableLibraryMonitor { get; set; } - public int SharingExpirationDays { get; set; } public string[] Migrations { get; set; } @@ -244,7 +242,6 @@ namespace MediaBrowser.Model.Configuration // 5 minutes MinResumeDurationSeconds = 300; - EnableLibraryMonitor = AutoOnOff.Auto; LibraryMonitorDelay = 60; EnableInternetProviders = true; diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index db70b8606..c1a01680d 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -89,7 +89,6 @@ - diff --git a/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 280bec65b..1021d8823 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Globalization; using System.Net; using MediaBrowser.Common.Threading; +using MediaBrowser.Model.Events; namespace MediaBrowser.Server.Implementations.EntryPoints { @@ -17,17 +18,17 @@ namespace MediaBrowser.Server.Implementations.EntryPoints private readonly IServerApplicationHost _appHost; private readonly ILogger _logger; private readonly IServerConfigurationManager _config; - private readonly ISsdpHandler _ssdp; + private readonly IDeviceDiscovery _deviceDiscovery; private PeriodicTimer _timer; private bool _isStarted; - public ExternalPortForwarding(ILogManager logmanager, IServerApplicationHost appHost, IServerConfigurationManager config, ISsdpHandler ssdp) + public ExternalPortForwarding(ILogManager logmanager, IServerApplicationHost appHost, IServerConfigurationManager config, IDeviceDiscovery deviceDiscovery) { _logger = logmanager.GetLogger("PortMapper"); _appHost = appHost; _config = config; - _ssdp = ssdp; + _deviceDiscovery = deviceDiscovery; } private string _lastConfigIdentifier; @@ -61,7 +62,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints public void Run() { - //NatUtility.Logger = new LogWriter(_logger); + NatUtility.Logger = _logger; if (_config.Configuration.EnableUPnP) { @@ -93,33 +94,22 @@ namespace MediaBrowser.Server.Implementations.EntryPoints _timer = new PeriodicTimer(ClearCreatedRules, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); - _ssdp.MessageReceived += _ssdp_MessageReceived; + _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; _lastConfigIdentifier = GetConfigIdentifier(); _isStarted = true; } - private void ClearCreatedRules(object state) - { - _createdRules = new List(); - _usnsHandled = new List(); - } - - void _ssdp_MessageReceived(object sender, SsdpMessageEventArgs e) + private async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs e) { - var endpoint = e.EndPoint as IPEndPoint; - - if (endpoint == null || e.LocalEndPoint == null) - { - return; - } + var info = e.Argument; string usn; - if (!e.Headers.TryGetValue("USN", out usn)) usn = string.Empty; + if (!info.Headers.TryGetValue("USN", out usn)) usn = string.Empty; string nt; - if (!e.Headers.TryGetValue("NT", out nt)) nt = string.Empty; + if (!info.Headers.TryGetValue("NT", out nt)) nt = string.Empty; // Filter device type if (usn.IndexOf("WANIPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && @@ -132,15 +122,45 @@ namespace MediaBrowser.Server.Implementations.EntryPoints var identifier = string.IsNullOrWhiteSpace(usn) ? nt : usn; - if (!_usnsHandled.Contains(identifier)) + if (info.Location != null && !_usnsHandled.Contains(identifier)) { _usnsHandled.Add(identifier); _logger.Debug("Calling Nat.Handle on " + identifier); - NatUtility.Handle(e.LocalEndPoint.Address, e.Message, endpoint, NatProtocol.Upnp); + + IPAddress address; + if (IPAddress.TryParse(info.Location.Host, out address)) + { + // The Handle method doesn't need the port + var endpoint = new IPEndPoint(address, info.Location.Port); + + IPAddress localAddress = null; + + try + { + var localAddressString = await _appHost.GetLocalApiUrl().ConfigureAwait(false); + + if (!IPAddress.TryParse(localAddressString, out localAddress)) + { + return; + } + } + catch + { + return; + } + + NatUtility.Handle(localAddress, info, endpoint, NatProtocol.Upnp); + } } } + private void ClearCreatedRules(object state) + { + _createdRules = new List(); + _usnsHandled = new List(); + } + void NatUtility_UnhandledException(object sender, UnhandledExceptionEventArgs e) { var ex = e.ExceptionObject as Exception; @@ -228,7 +248,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints _timer = null; } - _ssdp.MessageReceived -= _ssdp_MessageReceived; + _deviceDiscovery.DeviceDiscovered -= _deviceDiscovery_DeviceDiscovered; try { diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs index c87d10ef4..80364bb55 100644 --- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -164,32 +164,16 @@ namespace MediaBrowser.Server.Implementations.IO Start(); } - private bool EnableLibraryMonitor - { - get - { - switch (ConfigurationManager.Configuration.EnableLibraryMonitor) - { - case AutoOnOff.Auto: - return Environment.OSVersion.Platform == PlatformID.Win32NT; - case AutoOnOff.Enabled: - return true; - default: - return false; - } - } - } - private bool IsLibraryMonitorEnabaled(BaseItem item) { var options = LibraryManager.GetLibraryOptions(item); - if (options != null && options.SchemaVersion >= 1) + if (options != null) { return options.EnableRealtimeMonitor; } - return EnableLibraryMonitor; + return false; } public void Start() diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 1a5ebedc2..6d2f79fa0 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -166,7 +166,27 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10)); if (imageIndex > -1) { - programDict[schedule.programID].images = GetProgramLogo(ApiUrl, images[imageIndex]); + var programEntry = programDict[schedule.programID]; + + var data = images[imageIndex].data ?? new List(); + data = data.OrderByDescending(GetSizeOrder).ToList(); + + programEntry.primaryImage = GetProgramImage(ApiUrl, data, "Logo", true); + //programEntry.thumbImage = GetProgramImage(ApiUrl, data, "Iconic", false); + //programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? + // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? + // GetProgramImage(ApiUrl, data, "Banner-LO", false) ?? + // GetProgramImage(ApiUrl, data, "Banner-LOT", false); + + if (!string.IsNullOrWhiteSpace(programEntry.thumbImage)) + { + var b = true; + } + + if (!string.IsNullOrWhiteSpace(programEntry.bannerImage)) + { + var b = true; + } } } @@ -179,6 +199,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings return programsInfo; } + private int GetSizeOrder(ScheduleDirect.ImageData image) + { + if (!string.IsNullOrWhiteSpace(image.size)) + { + int value; + if (int.TryParse(image.size, out value)) + { + return value; + } + } + + return 0; + } + private readonly object _channelCacheLock = new object(); private ScheduleDirect.Station GetStation(string listingsId, string channelNumber, string channelName) { @@ -384,13 +418,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings episodeTitle = details.episodeTitle150; } - string imageUrl = null; - - if (details.hasImageArtwork) - { - imageUrl = details.images; - } - var showType = details.showType ?? string.Empty; var info = new ProgramInfo @@ -406,7 +433,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings Audio = audioType, IsRepeat = repeat, IsSeries = showType.IndexOf("series", StringComparison.OrdinalIgnoreCase) != -1, - ImageUrl = imageUrl, + ImageUrl = details.primaryImage, IsKids = string.Equals(details.audience, "children", StringComparison.OrdinalIgnoreCase), IsSports = showType.IndexOf("sports", StringComparison.OrdinalIgnoreCase) != -1, IsMovie = showType.IndexOf("movie", StringComparison.OrdinalIgnoreCase) != -1 || showType.IndexOf("film", StringComparison.OrdinalIgnoreCase) != -1, @@ -485,36 +512,33 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings return date; } - private string GetProgramLogo(string apiUrl, ScheduleDirect.ShowImages images) + private string GetProgramImage(string apiUrl, List images, string category, bool returnDefaultImage) { string url = null; - if (images.data != null) + + var logoIndex = images.FindIndex(i => string.Equals(i.category, category, StringComparison.OrdinalIgnoreCase)); + if (logoIndex == -1) { - var smallImages = images.data.Where(i => i.size == "Sm").ToList(); - if (smallImages.Any()) + if (!returnDefaultImage) { - images.data = smallImages; + return null; } - var logoIndex = images.data.FindIndex(i => i.category == "Logo"); - if (logoIndex == -1) + logoIndex = 0; + } + var uri = images[logoIndex].uri; + + if (!string.IsNullOrWhiteSpace(uri)) + { + if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1) { - logoIndex = 0; + url = uri; } - var uri = images.data[logoIndex].uri; - - if (!string.IsNullOrWhiteSpace(uri)) + else { - if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1) - { - url = uri; - } - else - { - url = apiUrl + "/image/" + uri; - } + url = apiUrl + "/image/" + uri; } - //_logger.Debug("URL for image is : " + url); } + //_logger.Debug("URL for image is : " + url); return url; } @@ -1204,7 +1228,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings public List crew { get; set; } public string showType { get; set; } public bool hasImageArtwork { get; set; } - public string images { get; set; } + public string primaryImage { get; set; } + public string thumbImage { get; set; } + public string bannerImage { get; set; } public string imageID { get; set; } public string md5 { get; set; } public List contentAdvisory { get; set; } diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs index 9ba1c60cc..ef37e3b35 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs @@ -10,6 +10,7 @@ using System; using System.Linq; using System.Threading; using MediaBrowser.Common.Net; +using MediaBrowser.Model.Events; using MediaBrowser.Model.Serialization; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun @@ -39,13 +40,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; } - void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e) + void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs e) { string server = null; - if (e.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1) + var info = e.Argument; + + if (info.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1) { string location; - if (e.Headers.TryGetValue("Location", out location)) + if (info.Headers.TryGetValue("Location", out location)) { //_logger.Debug("HdHomerun found at {0}", location); diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs index cb0e573da..a0b8ef5f7 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs @@ -14,6 +14,7 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Extensions; using System.Xml.Linq; +using MediaBrowser.Model.Events; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp { @@ -50,18 +51,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; } - void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e) + void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs e) { + var info = e.Argument; + string st = null; string nt = null; - e.Headers.TryGetValue("ST", out st); - e.Headers.TryGetValue("NT", out nt); + info.Headers.TryGetValue("ST", out st); + info.Headers.TryGetValue("NT", out nt); if (string.Equals(st, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase) || string.Equals(nt, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase)) { string location; - if (e.Headers.TryGetValue("Location", out location) && !string.IsNullOrWhiteSpace(location)) + if (info.Headers.TryGetValue("Location", out location) && !string.IsNullOrWhiteSpace(location)) { _logger.Debug("SAT IP found at {0}", location); diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 8850f3d35..e182ad6a5 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -105,9 +105,6 @@ ..\ThirdParty\UniversalDetector\UniversalDetector.dll - - ..\packages\Mono.Nat.1.2.24.0\lib\net40\Mono.Nat.dll - @@ -390,6 +387,10 @@ {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B} MediaBrowser.Model + + {d7453b88-2266-4805-b39b-2b5a2a33e1ba} + Mono.Nat + diff --git a/MediaBrowser.Server.Implementations/packages.config b/MediaBrowser.Server.Implementations/packages.config index 746dc7f62..94522cd50 100644 --- a/MediaBrowser.Server.Implementations/packages.config +++ b/MediaBrowser.Server.Implementations/packages.config @@ -5,7 +5,6 @@ - diff --git a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj index bcbb10174..e7acb3f50 100644 --- a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj +++ b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj @@ -1,5 +1,5 @@  - + Debug x86 @@ -10,8 +10,9 @@ MediaBrowser.Server.Mono MediaBrowser.Server.Mono MediaBrowser.Server.Mono.MainClass - v4.5 + v4.5.1 ..\ + true diff --git a/MediaBrowser.Server.Mono/app.config b/MediaBrowser.Server.Mono/app.config index b0e8558fd..e14b908ad 100644 --- a/MediaBrowser.Server.Mono/app.config +++ b/MediaBrowser.Server.Mono/app.config @@ -1,21 +1,21 @@ - + -
+
- - + + - - + + - + diff --git a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs index 433855ea0..e6d9b482e 100644 --- a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs +++ b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs @@ -549,7 +549,7 @@ namespace MediaBrowser.Server.Startup.Common SubtitleManager = new SubtitleManager(LogManager.GetLogger("SubtitleManager"), FileSystemManager, LibraryMonitor, LibraryManager, MediaSourceManager); RegisterSingleInstance(SubtitleManager); - RegisterSingleInstance(new DeviceDiscovery(LogManager.GetLogger("IDeviceDiscovery"), ServerConfigurationManager, this, NetworkManager)); + RegisterSingleInstance(new DeviceDiscovery(LogManager.GetLogger("IDeviceDiscovery"), ServerConfigurationManager)); ChapterManager = new ChapterManager(LibraryManager, LogManager.GetLogger("ChapterManager"), ServerConfigurationManager, ItemRepository); RegisterSingleInstance(ChapterManager); @@ -566,8 +566,6 @@ namespace MediaBrowser.Server.Startup.Common await sharingRepo.Initialize().ConfigureAwait(false); RegisterSingleInstance(new SharingManager(sharingRepo, ServerConfigurationManager, LibraryManager, this)); - RegisterSingleInstance(new SsdpHandler(LogManager.GetLogger("SsdpHandler"), ServerConfigurationManager, this)); - var activityLogRepo = await GetActivityLogRepository().ConfigureAwait(false); RegisterSingleInstance(activityLogRepo); RegisterSingleInstance(new ActivityManager(LogManager.GetLogger("ActivityManager"), activityLogRepo, UserManager)); diff --git a/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj b/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj index 778002e50..7eba89650 100644 --- a/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj +++ b/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj @@ -9,9 +9,10 @@ Properties MediaBrowser.Server.Startup.Common MediaBrowser.Server.Startup.Common - v4.5 + v4.5.1 512 ..\ + true diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 0b9dd90cd..7e0d834fa 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -64,6 +64,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Server.Startup EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Emby.Drawing", "Emby.Drawing\Emby.Drawing.csproj", "{08FFF49B-F175-4807-A2B5-73B0EBD9F716}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mono.Nat", "Mono.Nat\Mono.Nat.csproj", "{D7453B88-2266-4805-B39B-2B5A2A33E1BA}" +EndProject Global GlobalSection(Performance) = preSolution HasPerformanceSessions = true @@ -522,6 +524,36 @@ Global {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|Win32.ActiveCfg = Release|Any CPU {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|x64.ActiveCfg = Release|Any CPU {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|x86.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|Win32.ActiveCfg = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|Win32.Build.0 = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|x64.ActiveCfg = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|x64.Build.0 = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|x86.ActiveCfg = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|x86.Build.0 = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|Any CPU.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|Any CPU.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|Mixed Platforms.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|Mixed Platforms.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|Win32.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|Win32.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|x64.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|x64.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|x86.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|x86.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|Any CPU.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|Win32.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|Win32.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|x64.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|x64.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|x86.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Mono.Nat/AbstractNatDevice.cs b/Mono.Nat/AbstractNatDevice.cs new file mode 100644 index 000000000..046cfc10f --- /dev/null +++ b/Mono.Nat/AbstractNatDevice.cs @@ -0,0 +1,97 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; + +namespace Mono.Nat +{ + public abstract class AbstractNatDevice : INatDevice + { + private DateTime lastSeen; + + protected AbstractNatDevice () + { + + } + + public abstract IPAddress LocalAddress { get; } + + public DateTime LastSeen + { + get { return lastSeen; } + set { lastSeen = value; } + } + + public virtual void CreatePortMap (Mapping mapping) + { + IAsyncResult result = BeginCreatePortMap (mapping, null, null); + EndCreatePortMap(result); + } + + public virtual void DeletePortMap (Mapping mapping) + { + IAsyncResult result = BeginDeletePortMap (mapping, null, mapping); + EndDeletePortMap(result); + } + + public virtual Mapping[] GetAllMappings () + { + IAsyncResult result = BeginGetAllMappings (null, null); + return EndGetAllMappings (result); + } + + public virtual IPAddress GetExternalIP () + { + IAsyncResult result = BeginGetExternalIP(null, null); + return EndGetExternalIP(result); + } + + public virtual Mapping GetSpecificMapping (Protocol protocol, int port) + { + IAsyncResult result = this.BeginGetSpecificMapping (protocol, port, null, null); + return this.EndGetSpecificMapping(result); + } + + public abstract IAsyncResult BeginCreatePortMap(Mapping mapping, AsyncCallback callback, object asyncState); + public abstract IAsyncResult BeginDeletePortMap (Mapping mapping, AsyncCallback callback, object asyncState); + + public abstract IAsyncResult BeginGetAllMappings (AsyncCallback callback, object asyncState); + public abstract IAsyncResult BeginGetExternalIP (AsyncCallback callback, object asyncState); + public abstract IAsyncResult BeginGetSpecificMapping(Protocol protocol, int externalPort, AsyncCallback callback, object asyncState); + + public abstract void EndCreatePortMap (IAsyncResult result); + public abstract void EndDeletePortMap (IAsyncResult result); + + public abstract Mapping[] EndGetAllMappings (IAsyncResult result); + public abstract IPAddress EndGetExternalIP (IAsyncResult result); + public abstract Mapping EndGetSpecificMapping (IAsyncResult result); + } +} diff --git a/Mono.Nat/AsyncResults/AsyncResult.cs b/Mono.Nat/AsyncResults/AsyncResult.cs new file mode 100644 index 000000000..e98e7d7ca --- /dev/null +++ b/Mono.Nat/AsyncResults/AsyncResult.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; + +namespace Mono.Nat +{ + internal class AsyncResult : IAsyncResult + { + private object asyncState; + private AsyncCallback callback; + private bool completedSynchronously; + private bool isCompleted; + private Exception storedException; + private ManualResetEvent waitHandle; + + public AsyncResult(AsyncCallback callback, object asyncState) + { + this.callback = callback; + this.asyncState = asyncState; + waitHandle = new ManualResetEvent(false); + } + + public object AsyncState + { + get { return asyncState; } + } + + public ManualResetEvent AsyncWaitHandle + { + get { return waitHandle; } + } + + WaitHandle IAsyncResult.AsyncWaitHandle + { + get { return waitHandle; } + } + + public bool CompletedSynchronously + { + get { return completedSynchronously; } + protected internal set { completedSynchronously = value; } + } + + public bool IsCompleted + { + get { return isCompleted; } + protected internal set { isCompleted = value; } + } + + public Exception StoredException + { + get { return storedException; } + } + + public void Complete() + { + Complete(storedException); + } + + public void Complete(Exception ex) + { + storedException = ex; + isCompleted = true; + waitHandle.Set(); + + if (callback != null) + callback(this); + } + } +} diff --git a/Mono.Nat/Enums/MapState.cs b/Mono.Nat/Enums/MapState.cs new file mode 100644 index 000000000..5ed2abd8f --- /dev/null +++ b/Mono.Nat/Enums/MapState.cs @@ -0,0 +1,36 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace Mono.Nat +{ + public enum MapState + { + AlreadyMapped, + Available + } +} \ No newline at end of file diff --git a/Mono.Nat/Enums/ProtocolType.cs b/Mono.Nat/Enums/ProtocolType.cs new file mode 100644 index 000000000..a1f5cbb0e --- /dev/null +++ b/Mono.Nat/Enums/ProtocolType.cs @@ -0,0 +1,36 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace Mono.Nat +{ + public enum Protocol + { + Tcp, + Udp + } +} \ No newline at end of file diff --git a/Mono.Nat/EventArgs/DeviceEventArgs.cs b/Mono.Nat/EventArgs/DeviceEventArgs.cs new file mode 100644 index 000000000..fbbbf63e3 --- /dev/null +++ b/Mono.Nat/EventArgs/DeviceEventArgs.cs @@ -0,0 +1,45 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace Mono.Nat +{ + public class DeviceEventArgs : EventArgs + { + private INatDevice device; + + public DeviceEventArgs(INatDevice device) + { + this.device = device; + } + + public INatDevice Device + { + get { return this.device; } + } + } +} \ No newline at end of file diff --git a/Mono.Nat/Exceptions/MappingException.cs b/Mono.Nat/Exceptions/MappingException.cs new file mode 100644 index 000000000..bb2e6a69d --- /dev/null +++ b/Mono.Nat/Exceptions/MappingException.cs @@ -0,0 +1,87 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Security.Permissions; + +namespace Mono.Nat +{ + [Serializable] + public class MappingException : Exception + { + private int errorCode; + private string errorText; + + public int ErrorCode + { + get { return this.errorCode; } + } + + public string ErrorText + { + get { return this.errorText; } + } + + #region Constructors + public MappingException() + : base() + { + } + + public MappingException(string message) + : base(message) + { + } + + public MappingException(int errorCode, string errorText) + : base (string.Format ("Error {0}: {1}", errorCode, errorText)) + { + this.errorCode = errorCode; + this.errorText = errorText; + } + + public MappingException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected MappingException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) + : base(info, context) + { + } + #endregion + + [SecurityPermission(SecurityAction.Demand, SerializationFormatter=true)] + public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) + { + if(info==null) throw new ArgumentNullException("info"); + + this.errorCode = info.GetInt32("errorCode"); + this.errorText = info.GetString("errorText"); + base.GetObjectData(info, context); + } + } +} diff --git a/Mono.Nat/IMapper.cs b/Mono.Nat/IMapper.cs new file mode 100644 index 000000000..b18e6cff2 --- /dev/null +++ b/Mono.Nat/IMapper.cs @@ -0,0 +1,50 @@ +// +// Authors: +// Nicholas Terry +// +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Mono.Nat +{ + public enum MapperType + { + Pmp, + Upnp + } + + internal interface IMapper + { + event EventHandler DeviceFound; + + void Map(IPAddress gatewayAddress); + + void Handle(IPAddress localAddres, byte[] response); + } +} diff --git a/Mono.Nat/INatDevice.cs b/Mono.Nat/INatDevice.cs new file mode 100644 index 000000000..c9f27055b --- /dev/null +++ b/Mono.Nat/INatDevice.cs @@ -0,0 +1,62 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; + +namespace Mono.Nat +{ + public interface INatDevice + { + void CreatePortMap (Mapping mapping); + void DeletePortMap (Mapping mapping); + + IPAddress LocalAddress { get; } + Mapping[] GetAllMappings (); + IPAddress GetExternalIP (); + Mapping GetSpecificMapping (Protocol protocol, int port); + + IAsyncResult BeginCreatePortMap (Mapping mapping, AsyncCallback callback, object asyncState); + IAsyncResult BeginDeletePortMap (Mapping mapping, AsyncCallback callback, object asyncState); + + IAsyncResult BeginGetAllMappings (AsyncCallback callback, object asyncState); + IAsyncResult BeginGetExternalIP (AsyncCallback callback, object asyncState); + IAsyncResult BeginGetSpecificMapping (Protocol protocol, int externalPort, AsyncCallback callback, object asyncState); + + void EndCreatePortMap (IAsyncResult result); + void EndDeletePortMap (IAsyncResult result); + + Mapping[] EndGetAllMappings (IAsyncResult result); + IPAddress EndGetExternalIP (IAsyncResult result); + Mapping EndGetSpecificMapping (IAsyncResult result); + + DateTime LastSeen { get; set; } + } +} diff --git a/Mono.Nat/ISearcher.cs b/Mono.Nat/ISearcher.cs new file mode 100644 index 000000000..56e438105 --- /dev/null +++ b/Mono.Nat/ISearcher.cs @@ -0,0 +1,51 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// Nicholas Terry +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net.Sockets; +using System.Net; + +namespace Mono.Nat +{ + public delegate void NatDeviceCallback(INatDevice device); + + internal interface ISearcher + { + event EventHandler DeviceFound; + event EventHandler DeviceLost; + + void Search(); + void Handle(IPAddress localAddress, byte[] response, IPEndPoint endpoint); + DateTime NextSearch { get; } + NatProtocol Protocol { get; } + } +} diff --git a/Mono.Nat/Mapping.cs b/Mono.Nat/Mapping.cs new file mode 100644 index 000000000..dd49404c6 --- /dev/null +++ b/Mono.Nat/Mapping.cs @@ -0,0 +1,123 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace Mono.Nat +{ + public class Mapping + { + private string description; + private DateTime expiration; + private int lifetime; + private int privatePort; + private Protocol protocol; + private int publicPort; + + + + public Mapping (Protocol protocol, int privatePort, int publicPort) + : this (protocol, privatePort, publicPort, 0) + { + } + + public Mapping (Protocol protocol, int privatePort, int publicPort, int lifetime) + { + this.protocol = protocol; + this.privatePort = privatePort; + this.publicPort = publicPort; + this.lifetime = lifetime; + + if (lifetime == int.MaxValue) + this.expiration = DateTime.MaxValue; + else if (lifetime == 0) + this.expiration = DateTime.Now; + else + this.expiration = DateTime.Now.AddSeconds (lifetime); + } + + public string Description + { + get { return description; } + set { description = value; } + } + + public Protocol Protocol + { + get { return protocol; } + internal set { protocol = value; } + } + + public int PrivatePort + { + get { return privatePort; } + internal set { privatePort = value; } + } + + public int PublicPort + { + get { return publicPort; } + internal set { publicPort = value; } + } + + public int Lifetime + { + get { return lifetime; } + internal set { lifetime = value; } + } + + public DateTime Expiration + { + get { return expiration; } + internal set { expiration = value; } + } + + public bool IsExpired () + { + return expiration < DateTime.Now; + } + + public override bool Equals (object obj) + { + Mapping other = obj as Mapping; + return other == null ? false : this.protocol == other.protocol && + this.privatePort == other.privatePort && this.publicPort == other.publicPort; + } + + public override int GetHashCode() + { + return this.protocol.GetHashCode() ^ this.privatePort.GetHashCode() ^ this.publicPort.GetHashCode(); + } + + public override string ToString( ) + { + return String.Format( "Protocol: {0}, Public Port: {1}, Private Port: {2}, Description: {3}, Expiration: {4}, Lifetime: {5}", + this.protocol, this.publicPort, this.privatePort, this.description, this.expiration, this.lifetime ); + } + } +} diff --git a/Mono.Nat/Mono.Nat.csproj b/Mono.Nat/Mono.Nat.csproj new file mode 100644 index 000000000..9c2781433 --- /dev/null +++ b/Mono.Nat/Mono.Nat.csproj @@ -0,0 +1,104 @@ + + + + + Debug + AnyCPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA} + Library + Properties + Mono.Nat + Mono.Nat + v4.5 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + Properties\SharedVersion.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + MediaBrowser.Controller + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + \ No newline at end of file diff --git a/Mono.Nat/NatProtocol.cs b/Mono.Nat/NatProtocol.cs new file mode 100644 index 000000000..ade8d921c --- /dev/null +++ b/Mono.Nat/NatProtocol.cs @@ -0,0 +1,9 @@ + +namespace Mono.Nat +{ + public enum NatProtocol + { + Upnp = 0, + Pmp = 1 + } +} diff --git a/Mono.Nat/NatUtility.cs b/Mono.Nat/NatUtility.cs new file mode 100644 index 000000000..6d91d2513 --- /dev/null +++ b/Mono.Nat/NatUtility.cs @@ -0,0 +1,264 @@ +// +// Authors: +// Ben Motmans +// Nicholas Terry +// +// Copyright (C) 2007 Ben Motmans +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Linq; +using System.Collections.Generic; +using System.IO; +using System.Net.NetworkInformation; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Model.Logging; +using Mono.Nat.Pmp.Mappers; +using Mono.Nat.Upnp.Mappers; + +namespace Mono.Nat +{ + public static class NatUtility + { + private static ManualResetEvent searching; + public static event EventHandler DeviceFound; + public static event EventHandler DeviceLost; + + public static event EventHandler UnhandledException; + + private static List controllers; + private static bool verbose; + + public static List EnabledProtocols { get; set; } + + public static ILogger Logger { get; set; } + + public static bool Verbose + { + get { return verbose; } + set { verbose = value; } + } + + static NatUtility() + { + EnabledProtocols = new List + { + NatProtocol.Upnp, + NatProtocol.Pmp + }; + + searching = new ManualResetEvent(false); + + controllers = new List(); + controllers.Add(UpnpSearcher.Instance); + controllers.Add(PmpSearcher.Instance); + + controllers.ForEach(searcher => + { + searcher.DeviceFound += (sender, args) => + { + if (DeviceFound != null) + DeviceFound(sender, args); + }; + searcher.DeviceLost += (sender, args) => + { + if (DeviceLost != null) + DeviceLost(sender, args); + }; + }); + Thread t = new Thread(SearchAndListen); + t.IsBackground = true; + t.Start(); + } + + internal static void Log(string format, params object[] args) + { + var logger = Logger; + if (logger != null) + logger.Debug(format, args); + } + + private static void SearchAndListen() + { + while (true) + { + searching.WaitOne(); + + try + { + var enabledProtocols = EnabledProtocols.ToList(); + + if (enabledProtocols.Contains(UpnpSearcher.Instance.Protocol)) + { + Receive(UpnpSearcher.Instance, UpnpSearcher.sockets); + } + if (enabledProtocols.Contains(PmpSearcher.Instance.Protocol)) + { + Receive(PmpSearcher.Instance, PmpSearcher.sockets); + } + + foreach (ISearcher s in controllers) + if (s.NextSearch < DateTime.Now && enabledProtocols.Contains(s.Protocol)) + { + Log("Searching for: {0}", s.GetType().Name); + s.Search(); + } + } + catch (Exception e) + { + if (UnhandledException != null) + UnhandledException(typeof(NatUtility), new UnhandledExceptionEventArgs(e, false)); + } + Thread.Sleep(10); + } + } + + static void Receive (ISearcher searcher, List clients) + { + IPEndPoint received = new IPEndPoint(IPAddress.Parse("192.168.0.1"), 5351); + foreach (UdpClient client in clients) + { + if (client.Available > 0) + { + IPAddress localAddress = ((IPEndPoint)client.Client.LocalEndPoint).Address; + byte[] data = client.Receive(ref received); + searcher.Handle(localAddress, data, received); + } + } + } + + static void Receive(IMapper mapper, List clients) + { + IPEndPoint received = new IPEndPoint(IPAddress.Parse("192.168.0.1"), 5351); + foreach (UdpClient client in clients) + { + if (client.Available > 0) + { + IPAddress localAddress = ((IPEndPoint)client.Client.LocalEndPoint).Address; + byte[] data = client.Receive(ref received); + mapper.Handle(localAddress, data); + } + } + } + + public static void StartDiscovery () + { + searching.Set(); + } + + public static void StopDiscovery () + { + searching.Reset(); + } + + //This is for when you know the Gateway IP and want to skip the costly search... + public static void DirectMap(IPAddress gatewayAddress, MapperType type) + { + IMapper mapper; + switch (type) + { + case MapperType.Pmp: + mapper = new PmpMapper(); + break; + case MapperType.Upnp: + mapper = new UpnpMapper(); + mapper.DeviceFound += (sender, args) => + { + if (DeviceFound != null) + DeviceFound(sender, args); + }; + mapper.Map(gatewayAddress); + break; + default: + throw new InvalidOperationException("Unsuported type given"); + + } + searching.Reset(); + + } + + //So then why is it here? -Nick + [Obsolete ("This method serves no purpose and shouldn't be used")] + public static IPAddress[] GetLocalAddresses (bool includeIPv6) + { + List addresses = new List (); + + IPHostEntry hostInfo = Dns.GetHostEntry (Dns.GetHostName ()); + foreach (IPAddress address in hostInfo.AddressList) { + if (address.AddressFamily == AddressFamily.InterNetwork || + (includeIPv6 && address.AddressFamily == AddressFamily.InterNetworkV6)) { + addresses.Add (address); + } + } + + return addresses.ToArray (); + } + + //checks if an IP address is a private address space as defined by RFC 1918 + public static bool IsPrivateAddressSpace (IPAddress address) + { + byte[] ba = address.GetAddressBytes (); + + switch ((int)ba[0]) { + case 10: + return true; //10.x.x.x + case 172: + return ((int)ba[1] & 16) != 0; //172.16-31.x.x + case 192: + return (int)ba[1] == 168; //192.168.x.x + default: + return false; + } + } + + public static void Handle(IPAddress localAddress, byte[] response, IPEndPoint endpoint, NatProtocol protocol) + { + switch (protocol) + { + case NatProtocol.Upnp: + UpnpSearcher.Instance.Handle(localAddress, response, endpoint); + break; + case NatProtocol.Pmp: + PmpSearcher.Instance.Handle(localAddress, response, endpoint); + break; + default: + throw new ArgumentException("Unexpected protocol: " + protocol); + } + } + + public static void Handle(IPAddress localAddress, UpnpDeviceInfo deviceInfo, IPEndPoint endpoint, NatProtocol protocol) + { + switch (protocol) + { + case NatProtocol.Upnp: + UpnpSearcher.Instance.Handle(localAddress, deviceInfo, endpoint); + break; + default: + throw new ArgumentException("Unexpected protocol: " + protocol); + } + } + } +} diff --git a/Mono.Nat/Pmp/AsyncResults/PortMapAsyncResult.cs b/Mono.Nat/Pmp/AsyncResults/PortMapAsyncResult.cs new file mode 100644 index 000000000..c8ccf5435 --- /dev/null +++ b/Mono.Nat/Pmp/AsyncResults/PortMapAsyncResult.cs @@ -0,0 +1,52 @@ +// +// Authors: +// Ben Motmans +// +// Copyright (C) 2007 Ben Motmans +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace Mono.Nat.Pmp +{ + internal class PortMapAsyncResult : AsyncResult + { + private Mapping mapping; + + internal PortMapAsyncResult (Mapping mapping, AsyncCallback callback, object asyncState) + : base (callback, asyncState) + { + this.mapping = mapping; + } + + internal PortMapAsyncResult (Protocol protocol, int port, int lifetime, AsyncCallback callback, object asyncState) + : base (callback, asyncState) + { + this.mapping = new Mapping (protocol, port, port, lifetime); + } + + internal Mapping Mapping + { + get { return mapping; } + } + } +} diff --git a/Mono.Nat/Pmp/Mappers/PmpMapper.cs b/Mono.Nat/Pmp/Mappers/PmpMapper.cs new file mode 100644 index 000000000..f33ca44c3 --- /dev/null +++ b/Mono.Nat/Pmp/Mappers/PmpMapper.cs @@ -0,0 +1,83 @@ +// +// Authors: +// Nicholas Terry +// +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using Mono.Nat.Pmp; + +namespace Mono.Nat.Pmp.Mappers +{ + internal class PmpMapper : Pmp, IMapper + { + public event EventHandler DeviceFound; + + static PmpMapper() + { + CreateSocketsAndAddGateways(); + } + + public void Map(IPAddress gatewayAddress) + { + sockets.ForEach(x => Map(x, gatewayAddress)); + } + + void Map(UdpClient client, IPAddress gatewayAddress) + { + // The nat-pmp search message. Must be sent to GatewayIP:53531 + byte[] buffer = new byte[] { PmpConstants.Version, PmpConstants.OperationCode }; + + client.Send(buffer, buffer.Length, new IPEndPoint(gatewayAddress, PmpConstants.ServerPort)); + } + + public void Handle(IPAddress localAddres, byte[] response) + { + //if (!IsSearchAddress(endpoint.Address)) + // return; + if (response.Length != 12) + return; + if (response[0] != PmpConstants.Version) + return; + if (response[1] != PmpConstants.ServerNoop) + return; + int errorcode = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(response, 2)); + if (errorcode != 0) + NatUtility.Log("Non zero error: {0}", errorcode); + + IPAddress publicIp = new IPAddress(new byte[] { response[8], response[9], response[10], response[11] }); + OnDeviceFound(new DeviceEventArgs(new PmpNatDevice(localAddres, publicIp))); + } + + private void OnDeviceFound(DeviceEventArgs args) + { + if (DeviceFound != null) + DeviceFound(this, args); + } + } +} diff --git a/Mono.Nat/Pmp/Pmp.cs b/Mono.Nat/Pmp/Pmp.cs new file mode 100644 index 000000000..6795561b1 --- /dev/null +++ b/Mono.Nat/Pmp/Pmp.cs @@ -0,0 +1,118 @@ +// +// Authors: +// Ben Motmans +// Nicholas Terry +// +// Copyright (C) 2007 Ben Motmans +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; + +namespace Mono.Nat.Pmp +{ + internal abstract class Pmp + { + public static List sockets; + protected static Dictionary> gatewayLists; + + internal static void CreateSocketsAndAddGateways() + { + sockets = new List(); + gatewayLists = new Dictionary>(); + + try + { + foreach (NetworkInterface n in NetworkInterface.GetAllNetworkInterfaces()) + { + if (n.OperationalStatus != OperationalStatus.Up && n.OperationalStatus != OperationalStatus.Unknown) + continue; + IPInterfaceProperties properties = n.GetIPProperties(); + List gatewayList = new List(); + + foreach (GatewayIPAddressInformation gateway in properties.GatewayAddresses) + { + if (gateway.Address.AddressFamily == AddressFamily.InterNetwork) + { + gatewayList.Add(new IPEndPoint(gateway.Address, PmpConstants.ServerPort)); + } + } + if (gatewayList.Count == 0) + { + /* Mono on OSX doesn't give any gateway addresses, so check DNS entries */ + foreach (var gw2 in properties.DnsAddresses) + { + if (gw2.AddressFamily == AddressFamily.InterNetwork) + { + gatewayList.Add(new IPEndPoint(gw2, PmpConstants.ServerPort)); + } + } + foreach (var unicast in properties.UnicastAddresses) + { + if (/*unicast.DuplicateAddressDetectionState == DuplicateAddressDetectionState.Preferred + && unicast.AddressPreferredLifetime != UInt32.MaxValue + && */unicast.Address.AddressFamily == AddressFamily.InterNetwork) + { + var bytes = unicast.Address.GetAddressBytes(); + bytes[3] = 1; + gatewayList.Add(new IPEndPoint(new IPAddress(bytes), PmpConstants.ServerPort)); + } + } + } + + if (gatewayList.Count > 0) + { + foreach (UnicastIPAddressInformation address in properties.UnicastAddresses) + { + if (address.Address.AddressFamily == AddressFamily.InterNetwork) + { + UdpClient client; + + try + { + client = new UdpClient(new IPEndPoint(address.Address, 0)); + } + catch (SocketException) + { + continue; // Move on to the next address. + } + + gatewayLists.Add(client, gatewayList); + sockets.Add(client); + } + } + } + } + } + catch (Exception) + { + // NAT-PMP does not use multicast, so there isn't really a good fallback. + } + } + } +} diff --git a/Mono.Nat/Pmp/PmpConstants.cs b/Mono.Nat/Pmp/PmpConstants.cs new file mode 100644 index 000000000..ff3eb6230 --- /dev/null +++ b/Mono.Nat/Pmp/PmpConstants.cs @@ -0,0 +1,56 @@ +// +// Authors: +// Ben Motmans +// +// Copyright (C) 2007 Ben Motmans +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace Mono.Nat.Pmp +{ + internal static class PmpConstants + { + public const byte Version = (byte)0; + + public const byte OperationCode = (byte)0; + public const byte OperationCodeUdp = (byte)1; + public const byte OperationCodeTcp = (byte)2; + public const byte ServerNoop = (byte)128; + + public const int ClientPort = 5350; + public const int ServerPort = 5351; + + public const int RetryDelay = 250; + public const int RetryAttempts = 9; + + public const int RecommendedLeaseTime = 60 * 60; + public const int DefaultLeaseTime = RecommendedLeaseTime; + + public const short ResultCodeSuccess = 0; + public const short ResultCodeUnsupportedVersion = 1; + public const short ResultCodeNotAuthorized = 2; + public const short ResultCodeNetworkFailure = 3; + public const short ResultCodeOutOfResources = 4; + public const short ResultCodeUnsupportedOperationCode = 5; + } +} \ No newline at end of file diff --git a/Mono.Nat/Pmp/PmpNatDevice.cs b/Mono.Nat/Pmp/PmpNatDevice.cs new file mode 100644 index 000000000..9a2962c4d --- /dev/null +++ b/Mono.Nat/Pmp/PmpNatDevice.cs @@ -0,0 +1,347 @@ +// +// Authors: +// Ben Motmans +// +// Copyright (C) 2007 Ben Motmans +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Collections.Generic; + +namespace Mono.Nat.Pmp +{ + internal sealed class PmpNatDevice : AbstractNatDevice, IEquatable + { + private AsyncResult externalIpResult; + private bool pendingOp; + private IPAddress localAddress; + private IPAddress publicAddress; + + internal PmpNatDevice (IPAddress localAddress, IPAddress publicAddress) + { + this.localAddress = localAddress; + this.publicAddress = publicAddress; + } + + public override IPAddress LocalAddress + { + get { return localAddress; } + } + + public override IPAddress GetExternalIP () + { + return publicAddress; + } + + public override IAsyncResult BeginCreatePortMap(Mapping mapping, AsyncCallback callback, object asyncState) + { + PortMapAsyncResult pmar = new PortMapAsyncResult (mapping.Protocol, mapping.PublicPort, PmpConstants.DefaultLeaseTime, callback, asyncState); + ThreadPool.QueueUserWorkItem (delegate + { + try + { + CreatePortMap(pmar.Mapping, true); + pmar.Complete(); + } + catch (Exception e) + { + pmar.Complete(e); + } + }); + return pmar; + } + + public override IAsyncResult BeginDeletePortMap (Mapping mapping, AsyncCallback callback, object asyncState) + { + PortMapAsyncResult pmar = new PortMapAsyncResult (mapping, callback, asyncState); + ThreadPool.QueueUserWorkItem (delegate { + try { + CreatePortMap(pmar.Mapping, false); + pmar.Complete(); + } catch (Exception e) { + pmar.Complete(e); + } + }); + return pmar; + } + + public override void EndCreatePortMap (IAsyncResult result) + { + PortMapAsyncResult pmar = result as PortMapAsyncResult; + pmar.AsyncWaitHandle.WaitOne (); + } + + public override void EndDeletePortMap (IAsyncResult result) + { + PortMapAsyncResult pmar = result as PortMapAsyncResult; + pmar.AsyncWaitHandle.WaitOne (); + } + + public override IAsyncResult BeginGetAllMappings (AsyncCallback callback, object asyncState) + { + //NAT-PMP does not specify a way to get all port mappings + throw new NotSupportedException (); + } + + public override IAsyncResult BeginGetExternalIP (AsyncCallback callback, object asyncState) + { + StartOp(ref externalIpResult, callback, asyncState); + AsyncResult result = externalIpResult; + result.Complete(); + return result; + } + + public override IAsyncResult BeginGetSpecificMapping (Protocol protocol, int port, AsyncCallback callback, object asyncState) + { + //NAT-PMP does not specify a way to get a specific port map + throw new NotSupportedException (); + } + + public override Mapping[] EndGetAllMappings (IAsyncResult result) + { + //NAT-PMP does not specify a way to get all port mappings + throw new NotSupportedException (); + } + + public override IPAddress EndGetExternalIP (IAsyncResult result) + { + EndOp(result, ref externalIpResult); + return publicAddress; + } + + private void StartOp(ref AsyncResult result, AsyncCallback callback, object asyncState) + { + if (pendingOp == true) + throw new InvalidOperationException("Can only have one simultaenous async operation"); + + pendingOp = true; + result = new AsyncResult(callback, asyncState); + } + + private void EndOp(IAsyncResult supplied, ref AsyncResult actual) + { + if (supplied == null) + throw new ArgumentNullException("result"); + + if (supplied != actual) + throw new ArgumentException("Supplied IAsyncResult does not match the stored result"); + + if (!supplied.IsCompleted) + supplied.AsyncWaitHandle.WaitOne(); + + if (actual.StoredException != null) + throw actual.StoredException; + + pendingOp = false; + actual = null; + } + + public override Mapping EndGetSpecificMapping (IAsyncResult result) + { + //NAT-PMP does not specify a way to get a specific port map + throw new NotSupportedException (); + } + + public override bool Equals(object obj) + { + PmpNatDevice device = obj as PmpNatDevice; + return (device == null) ? false : this.Equals(device); + } + + public override int GetHashCode () + { + return this.publicAddress.GetHashCode(); + } + + public bool Equals (PmpNatDevice other) + { + return (other == null) ? false : this.publicAddress.Equals(other.publicAddress); + } + + private Mapping CreatePortMap (Mapping mapping, bool create) + { + List package = new List (); + + package.Add (PmpConstants.Version); + package.Add (mapping.Protocol == Protocol.Tcp ? PmpConstants.OperationCodeTcp : PmpConstants.OperationCodeUdp); + package.Add ((byte)0); //reserved + package.Add ((byte)0); //reserved + package.AddRange (BitConverter.GetBytes (IPAddress.HostToNetworkOrder((short)mapping.PrivatePort))); + package.AddRange (BitConverter.GetBytes (create ? IPAddress.HostToNetworkOrder((short)mapping.PublicPort) : (short)0)); + package.AddRange (BitConverter.GetBytes (IPAddress.HostToNetworkOrder(mapping.Lifetime))); + + CreatePortMapAsyncState state = new CreatePortMapAsyncState (); + state.Buffer = package.ToArray (); + state.Mapping = mapping; + + ThreadPool.QueueUserWorkItem (new WaitCallback (CreatePortMapAsync), state); + WaitHandle.WaitAll (new WaitHandle[] {state.ResetEvent}); + + if (!state.Success) { + string type = create ? "create" : "delete"; + throw new MappingException (String.Format ("Failed to {0} portmap (protocol={1}, private port={2}", type, mapping.Protocol, mapping.PrivatePort)); + } + + return state.Mapping; + } + + private void CreatePortMapAsync (object obj) + { + CreatePortMapAsyncState state = obj as CreatePortMapAsyncState; + + UdpClient udpClient = new UdpClient (); + CreatePortMapListenState listenState = new CreatePortMapListenState (state, udpClient); + + int attempt = 0; + int delay = PmpConstants.RetryDelay; + + ThreadPool.QueueUserWorkItem (new WaitCallback (CreatePortMapListen), listenState); + + while (attempt < PmpConstants.RetryAttempts && !listenState.Success) { + udpClient.Send (state.Buffer, state.Buffer.Length, new IPEndPoint (localAddress, PmpConstants.ServerPort)); + listenState.UdpClientReady.Set(); + + attempt++; + delay *= 2; + Thread.Sleep (delay); + } + + state.Success = listenState.Success; + + udpClient.Close (); + state.ResetEvent.Set (); + } + + private void CreatePortMapListen (object obj) + { + CreatePortMapListenState state = obj as CreatePortMapListenState; + + UdpClient udpClient = state.UdpClient; + state.UdpClientReady.WaitOne(); // Evidently UdpClient has some lazy-init Send/Receive race? + IPEndPoint endPoint = new IPEndPoint (localAddress, PmpConstants.ServerPort); + + while (!state.Success) + { + byte[] data; + try + { + data = udpClient.Receive(ref endPoint); + } + catch (SocketException) + { + state.Success = false; + return; + } + + catch (ObjectDisposedException) + { + state.Success = false; + return; + } + + if (data.Length < 16) + continue; + + if (data[0] != PmpConstants.Version) + continue; + + byte opCode = (byte)(data[1] & (byte)127); + + Protocol protocol = Protocol.Tcp; + if (opCode == PmpConstants.OperationCodeUdp) + protocol = Protocol.Udp; + + short resultCode = IPAddress.NetworkToHostOrder (BitConverter.ToInt16 (data, 2)); + uint epoch = (uint)IPAddress.NetworkToHostOrder (BitConverter.ToInt32 (data, 4)); + + int privatePort = IPAddress.NetworkToHostOrder (BitConverter.ToInt16 (data, 8)); + int publicPort = IPAddress.NetworkToHostOrder (BitConverter.ToInt16 (data, 10)); + + uint lifetime = (uint)IPAddress.NetworkToHostOrder (BitConverter.ToInt32 (data, 12)); + + if (publicPort < 0 || privatePort < 0 || resultCode != PmpConstants.ResultCodeSuccess) + { + state.Success = false; + return; + } + + if (lifetime == 0) + { + //mapping was deleted + state.Success = true; + state.Mapping = null; + return; + } + else + { + //mapping was created + //TODO: verify that the private port+protocol are a match + Mapping mapping = state.Mapping; + mapping.PublicPort = publicPort; + mapping.Protocol = protocol; + mapping.Expiration = DateTime.Now.AddSeconds (lifetime); + + state.Success = true; + } + } + } + + + /// + /// Overridden. + /// + /// + public override string ToString( ) + { + return String.Format( "PmpNatDevice - Local Address: {0}, Public IP: {1}, Last Seen: {2}", + this.localAddress, this.publicAddress, this.LastSeen ); + } + + + private class CreatePortMapAsyncState + { + internal byte[] Buffer; + internal ManualResetEvent ResetEvent = new ManualResetEvent (false); + internal Mapping Mapping; + + internal bool Success; + } + + private class CreatePortMapListenState + { + internal volatile bool Success; + internal Mapping Mapping; + internal UdpClient UdpClient; + internal ManualResetEvent UdpClientReady; + + internal CreatePortMapListenState (CreatePortMapAsyncState state, UdpClient client) + { + Mapping = state.Mapping; + UdpClient = client; UdpClientReady = new ManualResetEvent(false); + } + } + } +} \ No newline at end of file diff --git a/Mono.Nat/Pmp/Searchers/PmpSearcher.cs b/Mono.Nat/Pmp/Searchers/PmpSearcher.cs new file mode 100644 index 000000000..df0273ccb --- /dev/null +++ b/Mono.Nat/Pmp/Searchers/PmpSearcher.cs @@ -0,0 +1,149 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// Nicholas Terry +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; +using Mono.Nat.Pmp; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Linq; + +namespace Mono.Nat +{ + internal class PmpSearcher : Pmp.Pmp, ISearcher + { + static PmpSearcher instance = new PmpSearcher(); + + + public static PmpSearcher Instance + { + get { return instance; } + } + + private int timeout; + private DateTime nextSearch; + public event EventHandler DeviceFound; + public event EventHandler DeviceLost; + + static PmpSearcher() + { + CreateSocketsAndAddGateways(); + } + + PmpSearcher() + { + timeout = 250; + } + + public void Search() + { + foreach (UdpClient s in sockets) + { + try + { + Search(s); + } + catch + { + // Ignore any search errors + } + } + } + + void Search (UdpClient client) + { + // Sort out the time for the next search first. The spec says the + // timeout should double after each attempt. Once it reaches 64 seconds + // (and that attempt fails), assume no devices available + nextSearch = DateTime.Now.AddMilliseconds(timeout); + timeout *= 2; + + // We've tried 9 times as per spec, try searching again in 5 minutes + if (timeout == 128 * 1000) + { + timeout = 250; + nextSearch = DateTime.Now.AddMinutes(10); + return; + } + + // The nat-pmp search message. Must be sent to GatewayIP:53531 + byte[] buffer = new byte[] { PmpConstants.Version, PmpConstants.OperationCode }; + foreach (IPEndPoint gatewayEndpoint in gatewayLists[client]) + client.Send(buffer, buffer.Length, gatewayEndpoint); + } + + bool IsSearchAddress(IPAddress address) + { + foreach (List gatewayList in gatewayLists.Values) + foreach (IPEndPoint gatewayEndpoint in gatewayList) + if (gatewayEndpoint.Address.Equals(address)) + return true; + return false; + } + + public void Handle(IPAddress localAddress, byte[] response, IPEndPoint endpoint) + { + if (!IsSearchAddress(endpoint.Address)) + return; + if (response.Length != 12) + return; + if (response[0] != PmpConstants.Version) + return; + if (response[1] != PmpConstants.ServerNoop) + return; + int errorcode = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(response, 2)); + if (errorcode != 0) + NatUtility.Log("Non zero error: {0}", errorcode); + + IPAddress publicIp = new IPAddress(new byte[] { response[8], response[9], response[10], response[11] }); + nextSearch = DateTime.Now.AddMinutes(5); + timeout = 250; + OnDeviceFound(new DeviceEventArgs(new PmpNatDevice(endpoint.Address, publicIp))); + } + + public DateTime NextSearch + { + get { return nextSearch; } + } + private void OnDeviceFound(DeviceEventArgs args) + { + if (DeviceFound != null) + DeviceFound(this, args); + } + + public NatProtocol Protocol + { + get { return NatProtocol.Pmp; } + } + } +} diff --git a/Mono.Nat/Properties/AssemblyInfo.cs b/Mono.Nat/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..c3c3101de --- /dev/null +++ b/Mono.Nat/Properties/AssemblyInfo.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Mono.Nat")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Mono.Nat")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d7453b88-2266-4805-b39b-2b5a2a33e1ba")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// \ No newline at end of file diff --git a/Mono.Nat/Upnp/AsyncResults/GetAllMappingsAsyncResult.cs b/Mono.Nat/Upnp/AsyncResults/GetAllMappingsAsyncResult.cs new file mode 100644 index 000000000..51ecfbaf0 --- /dev/null +++ b/Mono.Nat/Upnp/AsyncResults/GetAllMappingsAsyncResult.cs @@ -0,0 +1,56 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; + +namespace Mono.Nat.Upnp +{ + internal class GetAllMappingsAsyncResult : PortMapAsyncResult + { + private List mappings; + private Mapping specificMapping; + + public GetAllMappingsAsyncResult(WebRequest request, AsyncCallback callback, object asyncState) + : base(request, callback, asyncState) + { + mappings = new List(); + } + + public List Mappings + { + get { return this.mappings; } + } + + public Mapping SpecificMapping + { + get { return this.specificMapping; } + set { this.specificMapping = value; } + } + } +} diff --git a/Mono.Nat/Upnp/AsyncResults/PortMapAsyncResult.cs b/Mono.Nat/Upnp/AsyncResults/PortMapAsyncResult.cs new file mode 100644 index 000000000..d8ac3fe61 --- /dev/null +++ b/Mono.Nat/Upnp/AsyncResults/PortMapAsyncResult.cs @@ -0,0 +1,75 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + + +using System; +using System.Net; +using System.Threading; + +namespace Mono.Nat.Upnp +{ + internal class PortMapAsyncResult : AsyncResult + { + private WebRequest request; + private MessageBase savedMessage; + + protected PortMapAsyncResult(WebRequest request, AsyncCallback callback, object asyncState) + : base (callback, asyncState) + { + this.request = request; + } + + internal WebRequest Request + { + get { return this.request; } + set { this.request = value; } + } + + internal MessageBase SavedMessage + { + get { return this.savedMessage; } + set { this.savedMessage = value; } + } + + internal static PortMapAsyncResult Create (MessageBase message, WebRequest request, AsyncCallback storedCallback, object asyncState) + { + if (message is GetGenericPortMappingEntry) + return new GetAllMappingsAsyncResult(request, storedCallback, asyncState); + + if (message is GetSpecificPortMappingEntryMessage) + { + GetSpecificPortMappingEntryMessage mapMessage = (GetSpecificPortMappingEntryMessage)message; + GetAllMappingsAsyncResult result = new GetAllMappingsAsyncResult(request, storedCallback, asyncState); + + result.SpecificMapping = new Mapping(mapMessage.protocol, 0, mapMessage.externalPort, 0); + return result; + } + + return new PortMapAsyncResult(request, storedCallback, asyncState); + } + } +} diff --git a/Mono.Nat/Upnp/Mappers/UpnpMapper.cs b/Mono.Nat/Upnp/Mappers/UpnpMapper.cs new file mode 100644 index 000000000..6f2716805 --- /dev/null +++ b/Mono.Nat/Upnp/Mappers/UpnpMapper.cs @@ -0,0 +1,110 @@ +// +// Authors: +// Nicholas Terry +// +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +namespace Mono.Nat.Upnp.Mappers +{ + internal class UpnpMapper : Upnp, IMapper + { + + public event EventHandler DeviceFound; + + public UdpClient Client { get; set; } + + public UpnpMapper() + { + //Bind to local port 1900 for ssdp responses + Client = new UdpClient(1900); + } + + public void Map(IPAddress gatewayAddress) + { + //Get the httpu request payload + byte[] data = DiscoverDeviceMessage.EncodeUnicast(gatewayAddress); + + Client.Send(data, data.Length, new IPEndPoint(gatewayAddress, 1900)); + + new Thread(Receive).Start(); + } + + public void Receive() + { + while (true) + { + IPEndPoint received = new IPEndPoint(IPAddress.Parse("192.168.0.1"), 5351); + if (Client.Available > 0) + { + IPAddress localAddress = ((IPEndPoint)Client.Client.LocalEndPoint).Address; + byte[] data = Client.Receive(ref received); + Handle(localAddress, data, received); + } + } + } + + public void Handle(IPAddress localAddres, byte[] response) + { + Handle(localAddres, response, null); + } + + public void Handle(IPAddress localAddress, byte[] response, IPEndPoint endpoint) + { + // No matter what, this method should never throw an exception. If something goes wrong + // we should still be in a position to handle the next reply correctly. + try + { + UpnpNatDevice d = base.Handle(localAddress, response, endpoint); + d.GetServicesList(DeviceSetupComplete); + } + catch (Exception ex) + { + Trace.WriteLine("Unhandled exception when trying to decode a device's response Send me the following data: "); + Trace.WriteLine("ErrorMessage:"); + Trace.WriteLine(ex.Message); + Trace.WriteLine("Data string:"); + Trace.WriteLine(Encoding.UTF8.GetString(response)); + } + } + + private void DeviceSetupComplete(INatDevice device) + { + OnDeviceFound(new DeviceEventArgs(device)); + } + + private void OnDeviceFound(DeviceEventArgs args) + { + if (DeviceFound != null) + DeviceFound(this, args); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/DiscoverDeviceMessage.cs b/Mono.Nat/Upnp/Messages/DiscoverDeviceMessage.cs new file mode 100644 index 000000000..87f5835a6 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/DiscoverDeviceMessage.cs @@ -0,0 +1,60 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System.Net; +using System.Text; + +namespace Mono.Nat.Upnp +{ + internal static class DiscoverDeviceMessage + { + /// + /// The message sent to discover all uPnP devices on the network + /// + /// + public static byte[] EncodeSSDP() + { + string s = "M-SEARCH * HTTP/1.1\r\n" + + "HOST: 239.255.255.250:1900\r\n" + + "MAN: \"ssdp:discover\"\r\n" + + "MX: 3\r\n" + + "ST: ssdp:all\r\n\r\n"; + return UTF8Encoding.ASCII.GetBytes(s); + } + + public static byte[] EncodeUnicast(IPAddress gatewayAddress) + { + //Format obtained from http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf pg 31 + //This method only works with upnp 1.1 routers... unfortunately + string s = "M-SEARCH * HTTP/1.1\r\n" + + "HOST: " + gatewayAddress + ":1900\r\n" + + "MAN: \"ssdp:discover\"\r\n" + + "ST: ssdp:all\r\n\r\n"; + //+ "USER-AGENT: unix/5.1 UPnP/1.1 MyProduct/1.0\r\n\r\n"; + return UTF8Encoding.ASCII.GetBytes(s); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/ErrorMessage.cs b/Mono.Nat/Upnp/Messages/ErrorMessage.cs new file mode 100644 index 000000000..ce5270e9b --- /dev/null +++ b/Mono.Nat/Upnp/Messages/ErrorMessage.cs @@ -0,0 +1,63 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace Mono.Nat.Upnp +{ + internal class ErrorMessage : MessageBase + { + #region Member Variables + public string Description + { + get { return this.description; } + } + private string description; + + public int ErrorCode + { + get { return this.errorCode; } + } + private int errorCode; + #endregion + + + #region Constructors + public ErrorMessage(int errorCode, string description) + :base(null) + { + this.description = description; + this.errorCode = errorCode; + } + #endregion + + + public override System.Net.WebRequest Encode(out byte[] body) + { + throw new NotImplementedException(); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/GetServicesMessage.cs b/Mono.Nat/Upnp/Messages/GetServicesMessage.cs new file mode 100644 index 000000000..c5d7bce70 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/GetServicesMessage.cs @@ -0,0 +1,62 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Diagnostics; +using System.Net; + +namespace Mono.Nat.Upnp +{ + internal class GetServicesMessage : MessageBase + { + private string servicesDescriptionUrl; + private EndPoint hostAddress; + + public GetServicesMessage(string description, EndPoint hostAddress) + :base(null) + { + if (string.IsNullOrEmpty(description)) + Trace.WriteLine("Description is null"); + + if (hostAddress == null) + Trace.WriteLine("hostaddress is null"); + + this.servicesDescriptionUrl = description; + this.hostAddress = hostAddress; + } + + + public override WebRequest Encode(out byte[] body) + { + HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://" + this.hostAddress.ToString() + this.servicesDescriptionUrl); + req.Headers.Add("ACCEPT-LANGUAGE", "en"); + req.Method = "GET"; + + body = new byte[0]; + return req; + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Requests/CreatePortMappingMessage.cs b/Mono.Nat/Upnp/Messages/Requests/CreatePortMappingMessage.cs new file mode 100644 index 000000000..da650fb41 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Requests/CreatePortMappingMessage.cs @@ -0,0 +1,75 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System.Net; +using System.IO; +using System.Globalization; +using System.Text; +using System.Xml; + +namespace Mono.Nat.Upnp +{ + internal class CreatePortMappingMessage : MessageBase + { + #region Private Fields + + private IPAddress localIpAddress; + private Mapping mapping; + + #endregion + + + #region Constructors + public CreatePortMappingMessage(Mapping mapping, IPAddress localIpAddress, UpnpNatDevice device) + : base(device) + { + this.mapping = mapping; + this.localIpAddress = localIpAddress; + } + #endregion + + + public override WebRequest Encode(out byte[] body) + { + CultureInfo culture = CultureInfo.InvariantCulture; + + StringBuilder builder = new StringBuilder(256); + XmlWriter writer = CreateWriter(builder); + + WriteFullElement(writer, "NewRemoteHost", string.Empty); + WriteFullElement(writer, "NewExternalPort", this.mapping.PublicPort.ToString(culture)); + WriteFullElement(writer, "NewProtocol", this.mapping.Protocol == Protocol.Tcp ? "TCP" : "UDP"); + WriteFullElement(writer, "NewInternalPort", this.mapping.PrivatePort.ToString(culture)); + WriteFullElement(writer, "NewInternalClient", this.localIpAddress.ToString()); + WriteFullElement(writer, "NewEnabled", "1"); + WriteFullElement(writer, "NewPortMappingDescription", string.IsNullOrEmpty(mapping.Description) ? "Mono.Nat" : mapping.Description); + WriteFullElement(writer, "NewLeaseDuration", mapping.Lifetime.ToString()); + + writer.Flush(); + return CreateRequest("AddPortMapping", builder.ToString(), out body); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Requests/DeletePortMappingMessage.cs b/Mono.Nat/Upnp/Messages/Requests/DeletePortMappingMessage.cs new file mode 100644 index 000000000..d9be89a69 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Requests/DeletePortMappingMessage.cs @@ -0,0 +1,57 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System.Net; +using System.IO; +using System.Text; +using System.Xml; + +namespace Mono.Nat.Upnp +{ + internal class DeletePortMappingMessage : MessageBase + { + private Mapping mapping; + + public DeletePortMappingMessage(Mapping mapping, UpnpNatDevice device) + : base(device) + { + this.mapping = mapping; + } + + public override WebRequest Encode(out byte[] body) + { + StringBuilder builder = new StringBuilder(256); + XmlWriter writer = CreateWriter(builder); + + WriteFullElement(writer, "NewRemoteHost", string.Empty); + WriteFullElement(writer, "NewExternalPort", mapping.PublicPort.ToString(MessageBase.Culture)); + WriteFullElement(writer, "NewProtocol", mapping.Protocol == Protocol.Tcp ? "TCP" : "UDP"); + + writer.Flush(); + return CreateRequest("DeletePortMapping", builder.ToString(), out body); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Requests/GetExternalIPAddressMessage.cs b/Mono.Nat/Upnp/Messages/Requests/GetExternalIPAddressMessage.cs new file mode 100644 index 000000000..8f97002ea --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Requests/GetExternalIPAddressMessage.cs @@ -0,0 +1,51 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; +using System.IO; + +namespace Mono.Nat.Upnp +{ + internal class GetExternalIPAddressMessage : MessageBase + { + + #region Constructors + public GetExternalIPAddressMessage(UpnpNatDevice device) + :base(device) + { + } + #endregion + + + public override WebRequest Encode(out byte[] body) + { + return CreateRequest("GetExternalIPAddress", string.Empty, out body); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Requests/GetGenericPortMappingEntry.cs b/Mono.Nat/Upnp/Messages/Requests/GetGenericPortMappingEntry.cs new file mode 100644 index 000000000..c0c555881 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Requests/GetGenericPortMappingEntry.cs @@ -0,0 +1,55 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml; + +namespace Mono.Nat.Upnp +{ + internal class GetGenericPortMappingEntry : MessageBase + { + private int index; + + public GetGenericPortMappingEntry(int index, UpnpNatDevice device) + :base(device) + { + this.index = index; + } + + public override System.Net.WebRequest Encode(out byte[] body) + { + StringBuilder sb = new StringBuilder(128); + XmlWriter writer = CreateWriter(sb); + + WriteFullElement(writer, "NewPortMappingIndex", index.ToString()); + + writer.Flush(); + return CreateRequest("GetGenericPortMappingEntry", sb.ToString(), out body); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Requests/GetSpecificPortMappingEntryMessage.cs b/Mono.Nat/Upnp/Messages/Requests/GetSpecificPortMappingEntryMessage.cs new file mode 100644 index 000000000..314468ece --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Requests/GetSpecificPortMappingEntryMessage.cs @@ -0,0 +1,60 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml; +using System.Net; + +namespace Mono.Nat.Upnp +{ + internal class GetSpecificPortMappingEntryMessage : MessageBase + { + internal Protocol protocol; + internal int externalPort; + + public GetSpecificPortMappingEntryMessage(Protocol protocol, int externalPort, UpnpNatDevice device) + : base(device) + { + this.protocol = protocol; + this.externalPort = externalPort; + } + + public override WebRequest Encode(out byte[] body) + { + StringBuilder sb = new StringBuilder(64); + XmlWriter writer = CreateWriter(sb); + + WriteFullElement(writer, "NewRemoteHost", string.Empty); + WriteFullElement(writer, "NewExternalPort", externalPort.ToString()); + WriteFullElement(writer, "NewProtocol", protocol == Protocol.Tcp ? "TCP" : "UDP"); + writer.Flush(); + + return CreateRequest("GetSpecificPortMappingEntry", sb.ToString(), out body); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Responses/CreatePortMappingResponseMessage.cs b/Mono.Nat/Upnp/Messages/Responses/CreatePortMappingResponseMessage.cs new file mode 100644 index 000000000..e75926b09 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Responses/CreatePortMappingResponseMessage.cs @@ -0,0 +1,46 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + + +using System; +namespace Mono.Nat.Upnp +{ + internal class CreatePortMappingResponseMessage : MessageBase + { + #region Constructors + public CreatePortMappingResponseMessage() + :base(null) + { + } + #endregion + + public override System.Net.WebRequest Encode(out byte[] body) + { + throw new NotImplementedException(); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Responses/DeletePortMappingResponseMessage.cs b/Mono.Nat/Upnp/Messages/Responses/DeletePortMappingResponseMessage.cs new file mode 100644 index 000000000..1fce4eb04 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Responses/DeletePortMappingResponseMessage.cs @@ -0,0 +1,44 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + + +using System; +namespace Mono.Nat.Upnp +{ + internal class DeletePortMapResponseMessage : MessageBase + { + public DeletePortMapResponseMessage() + :base(null) + { + } + + public override System.Net.WebRequest Encode(out byte[] body) + { + throw new NotSupportedException(); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Responses/GetExternalIPAddressResponseMessage.cs b/Mono.Nat/Upnp/Messages/Responses/GetExternalIPAddressResponseMessage.cs new file mode 100644 index 000000000..ee4b18cd1 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Responses/GetExternalIPAddressResponseMessage.cs @@ -0,0 +1,53 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; + +namespace Mono.Nat.Upnp +{ + internal class GetExternalIPAddressResponseMessage : MessageBase + { + public IPAddress ExternalIPAddress + { + get { return this.externalIPAddress; } + } + private IPAddress externalIPAddress; + + public GetExternalIPAddressResponseMessage(string ip) + :base(null) + { + this.externalIPAddress = IPAddress.Parse(ip); + } + + public override WebRequest Encode(out byte[] body) + { + throw new NotImplementedException(); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Responses/GetGenericPortMappingEntryResponseMessage.cs b/Mono.Nat/Upnp/Messages/Responses/GetGenericPortMappingEntryResponseMessage.cs new file mode 100644 index 000000000..b11bfa027 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Responses/GetGenericPortMappingEntryResponseMessage.cs @@ -0,0 +1,108 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml; + +namespace Mono.Nat.Upnp +{ + internal class GetGenericPortMappingEntryResponseMessage : MessageBase + { + private string remoteHost; + private int externalPort; + private Protocol protocol; + private int internalPort; + private string internalClient; + private bool enabled; + private string portMappingDescription; + private int leaseDuration; + + public string RemoteHost + { + get { return this.remoteHost; } + } + + public int ExternalPort + { + get { return this.externalPort; } + } + + public Protocol Protocol + { + get { return this.protocol; } + } + + public int InternalPort + { + get { return this.internalPort; } + } + + public string InternalClient + { + get { return this.internalClient; } + } + + public bool Enabled + { + get { return this.enabled; } + } + + public string PortMappingDescription + { + get { return this.portMappingDescription; } + } + + public int LeaseDuration + { + get { return this.leaseDuration; } + } + + + public GetGenericPortMappingEntryResponseMessage(XmlNode data, bool genericMapping) + : base(null) + { + remoteHost = (genericMapping) ? data["NewRemoteHost"].InnerText : string.Empty; + externalPort = (genericMapping) ? Convert.ToInt32(data["NewExternalPort"].InnerText) : -1; + if (genericMapping) + protocol = data["NewProtocol"].InnerText.Equals("TCP", StringComparison.InvariantCultureIgnoreCase) ? Protocol.Tcp : Protocol.Udp; + else + protocol = Protocol.Udp; + + internalPort = Convert.ToInt32(data["NewInternalPort"].InnerText); + internalClient = data["NewInternalClient"].InnerText; + enabled = data["NewEnabled"].InnerText == "1" ? true : false; + portMappingDescription = data["NewPortMappingDescription"].InnerText; + leaseDuration = Convert.ToInt32(data["NewLeaseDuration"].InnerText); + } + + public override System.Net.WebRequest Encode(out byte[] body) + { + throw new NotImplementedException(); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/UpnpMessage.cs b/Mono.Nat/Upnp/Messages/UpnpMessage.cs new file mode 100644 index 000000000..44c16eec6 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/UpnpMessage.cs @@ -0,0 +1,132 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Diagnostics; +using System.Xml; +using System.Net; +using System.IO; +using System.Text; +using System.Globalization; + +namespace Mono.Nat.Upnp +{ + internal abstract class MessageBase + { + internal static readonly CultureInfo Culture = CultureInfo.InvariantCulture; + protected UpnpNatDevice device; + + protected MessageBase(UpnpNatDevice device) + { + this.device = device; + } + + protected WebRequest CreateRequest(string upnpMethod, string methodParameters, out byte[] body) + { + string ss = "http://" + this.device.HostEndPoint.ToString() + this.device.ControlUrl; + NatUtility.Log("Initiating request to: {0}", ss); + Uri location = new Uri(ss); + + HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(location); + req.KeepAlive = false; + req.Method = "POST"; + req.ContentType = "text/xml; charset=\"utf-8\""; + req.Headers.Add("SOAPACTION", "\"" + device.ServiceType + "#" + upnpMethod + "\""); + + string bodyString = "" + + "" + + "" + + methodParameters + + "" + + "" + + "\r\n\r\n"; + + body = System.Text.Encoding.UTF8.GetBytes(bodyString); + return req; + } + + public static MessageBase Decode(UpnpNatDevice device, string message) + { + XmlNode node; + XmlDocument doc = new XmlDocument(); + doc.LoadXml(message); + + XmlNamespaceManager nsm = new XmlNamespaceManager(doc.NameTable); + + // Error messages should be found under this namespace + nsm.AddNamespace("errorNs", "urn:schemas-upnp-org:control-1-0"); + nsm.AddNamespace("responseNs", device.ServiceType); + + // Check to see if we have a fault code message. + if ((node = doc.SelectSingleNode("//errorNs:UPnPError", nsm)) != null) { + string errorCode = node["errorCode"] != null ? node["errorCode"].InnerText : ""; + string errorDescription = node["errorDescription"] != null ? node["errorDescription"].InnerText : ""; + + return new ErrorMessage(Convert.ToInt32(errorCode, CultureInfo.InvariantCulture), errorDescription); + } + + if ((doc.SelectSingleNode("//responseNs:AddPortMappingResponse", nsm)) != null) + return new CreatePortMappingResponseMessage(); + + if ((doc.SelectSingleNode("//responseNs:DeletePortMappingResponse", nsm)) != null) + return new DeletePortMapResponseMessage(); + + if ((node = doc.SelectSingleNode("//responseNs:GetExternalIPAddressResponse", nsm)) != null) { + string newExternalIPAddress = node["NewExternalIPAddress"] != null ? node["NewExternalIPAddress"].InnerText : ""; + return new GetExternalIPAddressResponseMessage(newExternalIPAddress); + } + + if ((node = doc.SelectSingleNode("//responseNs:GetGenericPortMappingEntryResponse", nsm)) != null) + return new GetGenericPortMappingEntryResponseMessage(node, true); + + if ((node = doc.SelectSingleNode("//responseNs:GetSpecificPortMappingEntryResponse", nsm)) != null) + return new GetGenericPortMappingEntryResponseMessage(node, false); + + NatUtility.Log("Unknown message returned. Please send me back the following XML:"); + NatUtility.Log(message); + return null; + } + + public abstract WebRequest Encode(out byte[] body); + + internal static void WriteFullElement(XmlWriter writer, string element, string value) + { + writer.WriteStartElement(element); + writer.WriteString(value); + writer.WriteEndElement(); + } + + internal static XmlWriter CreateWriter(StringBuilder sb) + { + XmlWriterSettings settings = new XmlWriterSettings(); + settings.ConformanceLevel = ConformanceLevel.Fragment; + return XmlWriter.Create(sb, settings); + } + } +} diff --git a/Mono.Nat/Upnp/Searchers/UpnpSearcher.cs b/Mono.Nat/Upnp/Searchers/UpnpSearcher.cs new file mode 100644 index 000000000..edc5a5d76 --- /dev/null +++ b/Mono.Nat/Upnp/Searchers/UpnpSearcher.cs @@ -0,0 +1,287 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// Nicholas Terry +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; +using Mono.Nat.Upnp; +using System.Diagnostics; +using System.Net.Sockets; +using System.Net.NetworkInformation; +using MediaBrowser.Controller.Dlna; + +namespace Mono.Nat +{ + internal class UpnpSearcher : ISearcher + { + private const int SearchPeriod = 5 * 60; // The time in seconds between each search + static UpnpSearcher instance = new UpnpSearcher(); + public static List sockets = CreateSockets(); + + public static UpnpSearcher Instance + { + get { return instance; } + } + + public event EventHandler DeviceFound; + public event EventHandler DeviceLost; + + private List devices; + private Dictionary lastFetched; + private DateTime nextSearch; + private IPEndPoint searchEndpoint; + + UpnpSearcher() + { + devices = new List(); + lastFetched = new Dictionary(); + //searchEndpoint = new IPEndPoint(IPAddress.Parse("239.255.255.250"), 1900); + searchEndpoint = new IPEndPoint(IPAddress.Parse("239.255.255.250"), 1900); + } + + static List CreateSockets() + { + List clients = new List(); + try + { + foreach (NetworkInterface n in NetworkInterface.GetAllNetworkInterfaces()) + { + foreach (UnicastIPAddressInformation address in n.GetIPProperties().UnicastAddresses) + { + if (address.Address.AddressFamily == AddressFamily.InterNetwork) + { + try + { + clients.Add(new UdpClient(new IPEndPoint(address.Address, 0))); + } + catch + { + continue; // Move on to the next address. + } + } + } + } + } + catch (Exception) + { + clients.Add(new UdpClient(0)); + } + return clients; + } + + public void Search() + { + foreach (UdpClient s in sockets) + { + try + { + Search(s); + } + catch + { + // Ignore any search errors + } + } + } + + void Search(UdpClient client) + { + nextSearch = DateTime.Now.AddSeconds(SearchPeriod); + byte[] data = DiscoverDeviceMessage.EncodeSSDP(); + + // UDP is unreliable, so send 3 requests at a time (per Upnp spec, sec 1.1.2) + for (int i = 0; i < 3; i++) + client.Send(data, data.Length, searchEndpoint); + } + + public IPEndPoint SearchEndpoint + { + get { return searchEndpoint; } + } + + public void Handle(IPAddress localAddress, UpnpDeviceInfo deviceInfo, IPEndPoint endpoint) + { + // No matter what, this method should never throw an exception. If something goes wrong + // we should still be in a position to handle the next reply correctly. + try + { + /* For UPnP Port Mapping we need ot find either WANPPPConnection or WANIPConnection. + Any other device type is no good to us for this purpose. See the IGP overview paper + page 5 for an overview of device types and their hierarchy. + http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf */ + + /* TODO: Currently we are assuming version 1 of the protocol. We should figure out which + version it is and apply the correct URN. */ + + /* Some routers don't correctly implement the version ID on the URN, so we only search for the type + prefix. */ + + // We have an internet gateway device now + UpnpNatDevice d = new UpnpNatDevice(localAddress, deviceInfo, endpoint, string.Empty); + + if (devices.Contains(d)) + { + // We already have found this device, so we just refresh it to let people know it's + // Still alive. If a device doesn't respond to a search, we dump it. + devices[devices.IndexOf(d)].LastSeen = DateTime.Now; + } + else + { + + // If we send 3 requests at a time, ensure we only fetch the services list once + // even if three responses are received + if (lastFetched.ContainsKey(endpoint.Address)) + { + DateTime last = lastFetched[endpoint.Address]; + if ((DateTime.Now - last) < TimeSpan.FromSeconds(20)) + return; + } + lastFetched[endpoint.Address] = DateTime.Now; + + // Once we've parsed the information we need, we tell the device to retrieve it's service list + // Once we successfully receive the service list, the callback provided will be invoked. + NatUtility.Log("Fetching service list: {0}", d.HostEndPoint); + d.GetServicesList(DeviceSetupComplete); + } + } + catch (Exception ex) + { + NatUtility.Log("Unhandled exception when trying to decode a device's response Send me the following data: "); + NatUtility.Log("ErrorMessage:"); + NatUtility.Log(ex.Message); + } + } + + public void Handle(IPAddress localAddress, byte[] response, IPEndPoint endpoint) + { + // Convert it to a string for easy parsing + string dataString = null; + + // No matter what, this method should never throw an exception. If something goes wrong + // we should still be in a position to handle the next reply correctly. + try { + string urn; + dataString = Encoding.UTF8.GetString(response); + + if (NatUtility.Verbose) + NatUtility.Log("UPnP Response: {0}", dataString); + + /* For UPnP Port Mapping we need ot find either WANPPPConnection or WANIPConnection. + Any other device type is no good to us for this purpose. See the IGP overview paper + page 5 for an overview of device types and their hierarchy. + http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf */ + + /* TODO: Currently we are assuming version 1 of the protocol. We should figure out which + version it is and apply the correct URN. */ + + /* Some routers don't correctly implement the version ID on the URN, so we only search for the type + prefix. */ + + string log = "UPnP Response: Router advertised a '{0}' service"; + StringComparison c = StringComparison.OrdinalIgnoreCase; + if (dataString.IndexOf("urn:schemas-upnp-org:service:WANIPConnection:", c) != -1) { + urn = "urn:schemas-upnp-org:service:WANIPConnection:1"; + NatUtility.Log(log, "urn:schemas-upnp-org:service:WANIPConnection:1"); + } else if (dataString.IndexOf("urn:schemas-upnp-org:service:WANPPPConnection:", c) != -1) { + urn = "urn:schemas-upnp-org:service:WANPPPConnection:1"; + NatUtility.Log(log, "urn:schemas-upnp-org:service:WANPPPConnection:"); + } else + return; + + // We have an internet gateway device now + UpnpNatDevice d = new UpnpNatDevice(localAddress, dataString, urn); + + if (devices.Contains(d)) + { + // We already have found this device, so we just refresh it to let people know it's + // Still alive. If a device doesn't respond to a search, we dump it. + devices[devices.IndexOf(d)].LastSeen = DateTime.Now; + } + else + { + + // If we send 3 requests at a time, ensure we only fetch the services list once + // even if three responses are received + if (lastFetched.ContainsKey(endpoint.Address)) + { + DateTime last = lastFetched[endpoint.Address]; + if ((DateTime.Now - last) < TimeSpan.FromSeconds(20)) + return; + } + lastFetched[endpoint.Address] = DateTime.Now; + + // Once we've parsed the information we need, we tell the device to retrieve it's service list + // Once we successfully receive the service list, the callback provided will be invoked. + NatUtility.Log("Fetching service list: {0}", d.HostEndPoint); + d.GetServicesList(DeviceSetupComplete); + } + } + catch (Exception ex) + { + Trace.WriteLine("Unhandled exception when trying to decode a device's response Send me the following data: "); + Trace.WriteLine("ErrorMessage:"); + Trace.WriteLine(ex.Message); + Trace.WriteLine("Data string:"); + Trace.WriteLine(dataString); + } + } + + public DateTime NextSearch + { + get { return nextSearch; } + } + + private void DeviceSetupComplete(INatDevice device) + { + lock (this.devices) + { + // We don't want the same device in there twice + if (devices.Contains(device)) + return; + + devices.Add(device); + } + + OnDeviceFound(new DeviceEventArgs(device)); + } + + private void OnDeviceFound(DeviceEventArgs args) + { + if (DeviceFound != null) + DeviceFound(this, args); + } + + public NatProtocol Protocol + { + get { return NatProtocol.Upnp; } + } + } +} diff --git a/Mono.Nat/Upnp/Upnp.cs b/Mono.Nat/Upnp/Upnp.cs new file mode 100644 index 000000000..e44a51c24 --- /dev/null +++ b/Mono.Nat/Upnp/Upnp.cs @@ -0,0 +1,83 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// Nicholas Terry +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; + +namespace Mono.Nat.Upnp +{ + internal class Upnp + { + public UpnpNatDevice Handle(IPAddress localAddress, byte[] response, IPEndPoint endpoint) + { + // Convert it to a string for easy parsing + string dataString = null; + + + string urn; + dataString = Encoding.UTF8.GetString(response); + + if (NatUtility.Verbose) + NatUtility.Log("UPnP Response: {0}", dataString); + + /* For UPnP Port Mapping we need ot find either WANPPPConnection or WANIPConnection. + Any other device type is no good to us for this purpose. See the IGP overview paper + page 5 for an overview of device types and their hierarchy. + http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf */ + + /* TODO: Currently we are assuming version 1 of the protocol. We should figure out which + version it is and apply the correct URN. */ + + /* Some routers don't correctly implement the version ID on the URN, so we only search for the type + prefix. */ + + string log = "UPnP Response: Router advertised a '{0}' service"; + StringComparison c = StringComparison.OrdinalIgnoreCase; + if (dataString.IndexOf("urn:schemas-upnp-org:service:WANIPConnection:", c) != -1) + { + urn = "urn:schemas-upnp-org:service:WANIPConnection:1"; + NatUtility.Log(log, "urn:schemas-upnp-org:service:WANIPConnection:1"); + } + else if (dataString.IndexOf("urn:schemas-upnp-org:service:WANPPPConnection:", c) != -1) + { + urn = "urn:schemas-upnp-org:service:WANPPPConnection:1"; + NatUtility.Log(log, "urn:schemas-upnp-org:service:WANPPPConnection:"); + } + else + throw new NotSupportedException("Received non-supported device type"); + + // We have an internet gateway device now + return new UpnpNatDevice(localAddress, dataString, urn); + } + } +} diff --git a/Mono.Nat/Upnp/UpnpNatDevice.cs b/Mono.Nat/Upnp/UpnpNatDevice.cs new file mode 100644 index 000000000..1160d3ac2 --- /dev/null +++ b/Mono.Nat/Upnp/UpnpNatDevice.cs @@ -0,0 +1,651 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Xml; +using System.Text; +using System.Diagnostics; +using MediaBrowser.Controller.Dlna; + +namespace Mono.Nat.Upnp +{ + public sealed class UpnpNatDevice : AbstractNatDevice, IEquatable + { + private EndPoint hostEndPoint; + private IPAddress localAddress; + private string serviceDescriptionUrl; + private string controlUrl; + private string serviceType; + + public override IPAddress LocalAddress + { + get { return localAddress; } + } + + /// + /// The callback to invoke when we are finished setting up the device + /// + private NatDeviceCallback callback; + + internal UpnpNatDevice(IPAddress localAddress, UpnpDeviceInfo deviceInfo, IPEndPoint hostEndPoint, string serviceType) + { + this.LastSeen = DateTime.Now; + this.localAddress = localAddress; + + // Split the string at the "location" section so i can extract the ipaddress and service description url + string locationDetails = deviceInfo.Location.ToString(); + this.serviceType = serviceType; + + // Make sure we have no excess whitespace + locationDetails = locationDetails.Trim(); + + // FIXME: Is this reliable enough. What if we get a hostname as opposed to a proper http address + // Are we going to get addresses with the "http://" attached? + if (locationDetails.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase)) + { + NatUtility.Log("Found device at: {0}", locationDetails); + // This bit strings out the "http://" from the string + locationDetails = locationDetails.Substring(7); + + this.hostEndPoint = hostEndPoint; + + NatUtility.Log("Parsed device as: {0}", this.hostEndPoint.ToString()); + + // The service description URL is the remainder of the "locationDetails" string. The bit that was originally after the ip + // and port information + this.serviceDescriptionUrl = locationDetails.Substring(locationDetails.IndexOf('/')); + } + else + { + NatUtility.Log("Couldn't decode address. Please send following string to the developer: "); + } + } + + internal UpnpNatDevice (IPAddress localAddress, string deviceDetails, string serviceType) + { + this.LastSeen = DateTime.Now; + this.localAddress = localAddress; + + // Split the string at the "location" section so i can extract the ipaddress and service description url + string locationDetails = deviceDetails.Substring(deviceDetails.IndexOf("Location", StringComparison.InvariantCultureIgnoreCase) + 9).Split('\r')[0]; + this.serviceType = serviceType; + + // Make sure we have no excess whitespace + locationDetails = locationDetails.Trim(); + + // FIXME: Is this reliable enough. What if we get a hostname as opposed to a proper http address + // Are we going to get addresses with the "http://" attached? + if (locationDetails.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase)) + { + NatUtility.Log("Found device at: {0}", locationDetails); + // This bit strings out the "http://" from the string + locationDetails = locationDetails.Substring(7); + + // We then split off the end of the string to get something like: 192.168.0.3:241 in our string + string hostAddressAndPort = locationDetails.Remove(locationDetails.IndexOf('/')); + + // From this we parse out the IP address and Port + if (hostAddressAndPort.IndexOf(':') > 0) + { + this.hostEndPoint = new IPEndPoint(IPAddress.Parse(hostAddressAndPort.Remove(hostAddressAndPort.IndexOf(':'))), + Convert.ToUInt16(hostAddressAndPort.Substring(hostAddressAndPort.IndexOf(':') + 1), System.Globalization.CultureInfo.InvariantCulture)); + } + else + { + // there is no port specified, use default port (80) + this.hostEndPoint = new IPEndPoint(IPAddress.Parse(hostAddressAndPort), 80); + } + + NatUtility.Log("Parsed device as: {0}", this.hostEndPoint.ToString()); + + // The service description URL is the remainder of the "locationDetails" string. The bit that was originally after the ip + // and port information + this.serviceDescriptionUrl = locationDetails.Substring(locationDetails.IndexOf('/')); + } + else + { + Trace.WriteLine("Couldn't decode address. Please send following string to the developer: "); + Trace.WriteLine(deviceDetails); + } + } + + /// + /// The EndPoint that the device is at + /// + internal EndPoint HostEndPoint + { + get { return this.hostEndPoint; } + } + + /// + /// The relative url of the xml file that describes the list of services is at + /// + internal string ServiceDescriptionUrl + { + get { return this.serviceDescriptionUrl; } + } + + /// + /// The relative url that we can use to control the port forwarding + /// + internal string ControlUrl + { + get { return this.controlUrl; } + } + + /// + /// The service type we're using on the device + /// + public string ServiceType + { + get { return serviceType; } + } + + /// + /// Begins an async call to get the external ip address of the router + /// + public override IAsyncResult BeginGetExternalIP(AsyncCallback callback, object asyncState) + { + // Create the port map message + GetExternalIPAddressMessage message = new GetExternalIPAddressMessage(this); + return BeginMessageInternal(message, callback, asyncState, EndGetExternalIPInternal); + } + + /// + /// Maps the specified port to this computer + /// + public override IAsyncResult BeginCreatePortMap(Mapping mapping, AsyncCallback callback, object asyncState) + { + CreatePortMappingMessage message = new CreatePortMappingMessage(mapping, localAddress, this); + return BeginMessageInternal(message, callback, asyncState, EndCreatePortMapInternal); + } + + /// + /// Removes a port mapping from this computer + /// + public override IAsyncResult BeginDeletePortMap(Mapping mapping, AsyncCallback callback, object asyncState) + { + DeletePortMappingMessage message = new DeletePortMappingMessage(mapping, this); + return BeginMessageInternal(message, callback, asyncState, EndDeletePortMapInternal); + } + + + public override IAsyncResult BeginGetAllMappings(AsyncCallback callback, object asyncState) + { + GetGenericPortMappingEntry message = new GetGenericPortMappingEntry(0, this); + return BeginMessageInternal(message, callback, asyncState, EndGetAllMappingsInternal); + } + + + public override IAsyncResult BeginGetSpecificMapping (Protocol protocol, int port, AsyncCallback callback, object asyncState) + { + GetSpecificPortMappingEntryMessage message = new GetSpecificPortMappingEntryMessage(protocol, port, this); + return this.BeginMessageInternal(message, callback, asyncState, new AsyncCallback(this.EndGetSpecificMappingInternal)); + } + + /// + /// + /// + /// + public override void EndCreatePortMap(IAsyncResult result) + { + if (result == null) throw new ArgumentNullException("result"); + + PortMapAsyncResult mappingResult = result as PortMapAsyncResult; + if (mappingResult == null) + throw new ArgumentException("Invalid AsyncResult", "result"); + + // Check if we need to wait for the operation to finish + if (!result.IsCompleted) + result.AsyncWaitHandle.WaitOne(); + + // If we have a saved exception, it means something went wrong during the mapping + // so we just rethrow the exception and let the user figure out what they should do. + if (mappingResult.SavedMessage is ErrorMessage) + { + ErrorMessage msg = mappingResult.SavedMessage as ErrorMessage; + throw new MappingException(msg.ErrorCode, msg.Description); + } + + //return result.AsyncState as Mapping; + } + + + /// + /// + /// + /// + public override void EndDeletePortMap(IAsyncResult result) + { + if (result == null) + throw new ArgumentNullException("result"); + + PortMapAsyncResult mappingResult = result as PortMapAsyncResult; + if (mappingResult == null) + throw new ArgumentException("Invalid AsyncResult", "result"); + + // Check if we need to wait for the operation to finish + if (!mappingResult.IsCompleted) + mappingResult.AsyncWaitHandle.WaitOne(); + + // If we have a saved exception, it means something went wrong during the mapping + // so we just rethrow the exception and let the user figure out what they should do. + if (mappingResult.SavedMessage is ErrorMessage) + { + ErrorMessage msg = mappingResult.SavedMessage as ErrorMessage; + throw new MappingException(msg.ErrorCode, msg.Description); + } + + // If all goes well, we just return + //return true; + } + + + public override Mapping[] EndGetAllMappings(IAsyncResult result) + { + if (result == null) + throw new ArgumentNullException("result"); + + GetAllMappingsAsyncResult mappingResult = result as GetAllMappingsAsyncResult; + if (mappingResult == null) + throw new ArgumentException("Invalid AsyncResult", "result"); + + if (!mappingResult.IsCompleted) + mappingResult.AsyncWaitHandle.WaitOne(); + + if (mappingResult.SavedMessage is ErrorMessage) + { + ErrorMessage msg = mappingResult.SavedMessage as ErrorMessage; + if (msg.ErrorCode != 713) + throw new MappingException(msg.ErrorCode, msg.Description); + } + + return mappingResult.Mappings.ToArray(); + } + + + /// + /// Ends an async request to get the external ip address of the router + /// + public override IPAddress EndGetExternalIP(IAsyncResult result) + { + if (result == null) throw new ArgumentNullException("result"); + + PortMapAsyncResult mappingResult = result as PortMapAsyncResult; + if (mappingResult == null) + throw new ArgumentException("Invalid AsyncResult", "result"); + + if (!result.IsCompleted) + result.AsyncWaitHandle.WaitOne(); + + if (mappingResult.SavedMessage is ErrorMessage) + { + ErrorMessage msg = mappingResult.SavedMessage as ErrorMessage; + throw new MappingException(msg.ErrorCode, msg.Description); + } + + if (mappingResult.SavedMessage == null) + return null; + else + return ((GetExternalIPAddressResponseMessage)mappingResult.SavedMessage).ExternalIPAddress; + } + + + public override Mapping EndGetSpecificMapping(IAsyncResult result) + { + if (result == null) + throw new ArgumentNullException("result"); + + GetAllMappingsAsyncResult mappingResult = result as GetAllMappingsAsyncResult; + if (mappingResult == null) + throw new ArgumentException("Invalid AsyncResult", "result"); + + if (!mappingResult.IsCompleted) + mappingResult.AsyncWaitHandle.WaitOne(); + + if (mappingResult.SavedMessage is ErrorMessage) + { + ErrorMessage message = mappingResult.SavedMessage as ErrorMessage; + if (message.ErrorCode != 0x2ca) + { + throw new MappingException(message.ErrorCode, message.Description); + } + } + if (mappingResult.Mappings.Count == 0) + return new Mapping (Protocol.Tcp, -1, -1); + + return mappingResult.Mappings[0]; + } + + + public override bool Equals(object obj) + { + UpnpNatDevice device = obj as UpnpNatDevice; + return (device == null) ? false : this.Equals((device)); + } + + + public bool Equals(UpnpNatDevice other) + { + return (other == null) ? false : (this.hostEndPoint.Equals(other.hostEndPoint) + //&& this.controlUrl == other.controlUrl + && this.serviceDescriptionUrl == other.serviceDescriptionUrl); + } + + public override int GetHashCode() + { + return (this.hostEndPoint.GetHashCode() ^ this.controlUrl.GetHashCode() ^ this.serviceDescriptionUrl.GetHashCode()); + } + + private IAsyncResult BeginMessageInternal(MessageBase message, AsyncCallback storedCallback, object asyncState, AsyncCallback callback) + { + byte[] body; + WebRequest request = message.Encode(out body); + PortMapAsyncResult mappingResult = PortMapAsyncResult.Create(message, request, storedCallback, asyncState); + + if (body.Length > 0) + { + request.ContentLength = body.Length; + request.BeginGetRequestStream(delegate(IAsyncResult result) { + try + { + Stream s = request.EndGetRequestStream(result); + s.Write(body, 0, body.Length); + request.BeginGetResponse(callback, mappingResult); + } + catch (Exception ex) + { + mappingResult.Complete(ex); + } + }, null); + } + else + { + request.BeginGetResponse(callback, mappingResult); + } + return mappingResult; + } + + private void CompleteMessage(IAsyncResult result) + { + PortMapAsyncResult mappingResult = result.AsyncState as PortMapAsyncResult; + mappingResult.CompletedSynchronously = result.CompletedSynchronously; + mappingResult.Complete(); + } + + private MessageBase DecodeMessageFromResponse(Stream s, long length) + { + StringBuilder data = new StringBuilder(); + int bytesRead = 0; + int totalBytesRead = 0; + byte[] buffer = new byte[10240]; + + // Read out the content of the message, hopefully picking everything up in the case where we have no contentlength + if (length != -1) + { + while (totalBytesRead < length) + { + bytesRead = s.Read(buffer, 0, buffer.Length); + data.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead)); + totalBytesRead += bytesRead; + } + } + else + { + while ((bytesRead = s.Read(buffer, 0, buffer.Length)) != 0) + data.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead)); + } + + // Once we have our content, we need to see what kind of message it is. It'll either a an error + // or a response based on the action we performed. + return MessageBase.Decode(this, data.ToString()); + } + + private void EndCreatePortMapInternal(IAsyncResult result) + { + EndMessageInternal(result); + CompleteMessage(result); + } + + private void EndMessageInternal(IAsyncResult result) + { + HttpWebResponse response = null; + PortMapAsyncResult mappingResult = result.AsyncState as PortMapAsyncResult; + + try + { + try + { + response = (HttpWebResponse)mappingResult.Request.EndGetResponse(result); + } + catch (WebException ex) + { + // Even if the request "failed" i want to continue on to read out the response from the router + response = ex.Response as HttpWebResponse; + if (response == null) + mappingResult.SavedMessage = new ErrorMessage((int)ex.Status, ex.Message); + } + if (response != null) + mappingResult.SavedMessage = DecodeMessageFromResponse(response.GetResponseStream(), response.ContentLength); + } + + finally + { + if (response != null) + response.Close(); + } + } + + private void EndDeletePortMapInternal(IAsyncResult result) + { + EndMessageInternal(result); + CompleteMessage(result); + } + + private void EndGetAllMappingsInternal(IAsyncResult result) + { + EndMessageInternal(result); + + GetAllMappingsAsyncResult mappingResult = result.AsyncState as GetAllMappingsAsyncResult; + GetGenericPortMappingEntryResponseMessage message = mappingResult.SavedMessage as GetGenericPortMappingEntryResponseMessage; + if (message != null) + { + Mapping mapping = new Mapping (message.Protocol, message.InternalPort, message.ExternalPort, message.LeaseDuration); + mapping.Description = message.PortMappingDescription; + mappingResult.Mappings.Add(mapping); + GetGenericPortMappingEntry next = new GetGenericPortMappingEntry(mappingResult.Mappings.Count, this); + + // It's ok to do this synchronously because we should already be on anther thread + // and this won't block the user. + byte[] body; + WebRequest request = next.Encode(out body); + if (body.Length > 0) + { + request.ContentLength = body.Length; + request.GetRequestStream().Write(body, 0, body.Length); + } + mappingResult.Request = request; + request.BeginGetResponse(EndGetAllMappingsInternal, mappingResult); + return; + } + + CompleteMessage(result); + } + + private void EndGetExternalIPInternal(IAsyncResult result) + { + EndMessageInternal(result); + CompleteMessage(result); + } + + private void EndGetSpecificMappingInternal(IAsyncResult result) + { + EndMessageInternal(result); + + GetAllMappingsAsyncResult mappingResult = result.AsyncState as GetAllMappingsAsyncResult; + GetGenericPortMappingEntryResponseMessage message = mappingResult.SavedMessage as GetGenericPortMappingEntryResponseMessage; + if (message != null) { + Mapping mapping = new Mapping(mappingResult.SpecificMapping.Protocol, message.InternalPort, mappingResult.SpecificMapping.PublicPort, message.LeaseDuration); + mapping.Description = mappingResult.SpecificMapping.Description; + mappingResult.Mappings.Add(mapping); + } + + CompleteMessage(result); + } + + internal void GetServicesList(NatDeviceCallback callback) + { + // Save the callback so i can use it again later when i've finished parsing the services available + this.callback = callback; + + // Create a HTTPWebRequest to download the list of services the device offers + byte[] body; + WebRequest request = new GetServicesMessage(this.serviceDescriptionUrl, this.hostEndPoint).Encode(out body); + if (body.Length > 0) + NatUtility.Log("Error: Services Message contained a body"); + request.BeginGetResponse(this.ServicesReceived, request); + } + + private void ServicesReceived(IAsyncResult result) + { + HttpWebResponse response = null; + try + { + int abortCount = 0; + int bytesRead = 0; + byte[] buffer = new byte[10240]; + StringBuilder servicesXml = new StringBuilder(); + XmlDocument xmldoc = new XmlDocument(); + HttpWebRequest request = result.AsyncState as HttpWebRequest; + response = request.EndGetResponse(result) as HttpWebResponse; + Stream s = response.GetResponseStream(); + + if (response.StatusCode != HttpStatusCode.OK) { + NatUtility.Log("{0}: Couldn't get services list: {1}", HostEndPoint, response.StatusCode); + return; // FIXME: This the best thing to do?? + } + + while (true) + { + bytesRead = s.Read(buffer, 0, buffer.Length); + servicesXml.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead)); + try + { + xmldoc.LoadXml(servicesXml.ToString()); + response.Close(); + break; + } + catch (XmlException) + { + // If we can't receive the entire XML within 500ms, then drop the connection + // Unfortunately not all routers supply a valid ContentLength (mine doesn't) + // so this hack is needed to keep testing our recieved data until it gets successfully + // parsed by the xmldoc. Without this, the code will never pick up my router. + if (abortCount++ > 50) + { + response.Close(); + return; + } + NatUtility.Log("{0}: Couldn't parse services list", HostEndPoint); + System.Threading.Thread.Sleep(10); + } + } + + NatUtility.Log("{0}: Parsed services list", HostEndPoint); + XmlNamespaceManager ns = new XmlNamespaceManager(xmldoc.NameTable); + ns.AddNamespace("ns", "urn:schemas-upnp-org:device-1-0"); + XmlNodeList nodes = xmldoc.SelectNodes("//*/ns:serviceList", ns); + + foreach (XmlNode node in nodes) + { + //Go through each service there + foreach (XmlNode service in node.ChildNodes) + { + //If the service is a WANIPConnection, then we have what we want + string type = service["serviceType"].InnerText; + NatUtility.Log("{0}: Found service: {1}", HostEndPoint, type); + StringComparison c = StringComparison.OrdinalIgnoreCase; + // TODO: Add support for version 2 of UPnP. + if (type.Equals("urn:schemas-upnp-org:service:WANPPPConnection:1", c) || + type.Equals("urn:schemas-upnp-org:service:WANIPConnection:1", c)) + { + this.controlUrl = service["controlURL"].InnerText; + NatUtility.Log("{0}: Found upnp service at: {1}", HostEndPoint, controlUrl); + try + { + Uri u = new Uri(controlUrl); + if (u.IsAbsoluteUri) + { + EndPoint old = hostEndPoint; + this.hostEndPoint = new IPEndPoint(IPAddress.Parse(u.Host), u.Port); + NatUtility.Log("{0}: Absolute URI detected. Host address is now: {1}", old, HostEndPoint); + this.controlUrl = controlUrl.Substring(u.GetLeftPart(UriPartial.Authority).Length); + NatUtility.Log("{0}: New control url: {1}", HostEndPoint, controlUrl); + } + } + catch + { + NatUtility.Log("{0}: Assuming control Uri is relative: {1}", HostEndPoint, controlUrl); + } + NatUtility.Log("{0}: Handshake Complete", HostEndPoint); + this.callback(this); + return; + } + } + } + + //If we get here, it means that we didn't get WANIPConnection service, which means no uPnP forwarding + //So we don't invoke the callback, so this device is never added to our lists + } + catch (WebException ex) + { + // Just drop the connection, FIXME: Should i retry? + NatUtility.Log("{0}: Device denied the connection attempt: {1}", HostEndPoint, ex); + } + finally + { + if (response != null) + response.Close(); + } + } + + /// + /// Overridden. + /// + /// + public override string ToString( ) + { + //GetExternalIP is blocking and can throw exceptions, can't use it here. + return String.Format( + "UpnpNatDevice - EndPoint: {0}, External IP: {1}, Control Url: {2}, Service Description Url: {3}, Service Type: {4}, Last Seen: {5}", + this.hostEndPoint, "Manually Check" /*this.GetExternalIP()*/, this.controlUrl, this.serviceDescriptionUrl, this.serviceType, this.LastSeen); + } + } +} \ No newline at end of file -- cgit v1.2.3 From c6419babaa75cedfbd021369f825d3a75e522c9b Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sat, 17 Sep 2016 13:01:58 -0400 Subject: update nlog, simpleinjector --- .../MediaBrowser.Common.Implementations.csproj | 6 +++--- MediaBrowser.Common.Implementations/packages.config | 4 ++-- .../MediaBrowser.Server.Implementations.csproj | 4 ++-- MediaBrowser.Server.Implementations/packages.config | 2 +- Nuget/MediaBrowser.Common.Internal.nuspec | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) (limited to 'MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj') diff --git a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj index ced2dd5a3..7e8823bc0 100644 --- a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj +++ b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj @@ -55,7 +55,7 @@ ..\packages\morelinq.1.4.0\lib\net35\MoreLinq.dll - ..\packages\NLog.4.3.6\lib\net45\NLog.dll + ..\packages\NLog.4.3.8\lib\net45\NLog.dll True @@ -65,8 +65,8 @@ False ..\ThirdParty\SharpCompress\SharpCompress.dll - - ..\packages\SimpleInjector.3.2.0\lib\net45\SimpleInjector.dll + + ..\packages\SimpleInjector.3.2.2\lib\net45\SimpleInjector.dll True diff --git a/MediaBrowser.Common.Implementations/packages.config b/MediaBrowser.Common.Implementations/packages.config index 594b4c7c5..f444a3a05 100644 --- a/MediaBrowser.Common.Implementations/packages.config +++ b/MediaBrowser.Common.Implementations/packages.config @@ -2,7 +2,7 @@ - + - + \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index e182ad6a5..b25c07fe3 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -69,8 +69,8 @@ ..\ThirdParty\ServiceStack\ServiceStack.Api.Swagger.dll - - ..\packages\SimpleInjector.3.2.0\lib\net45\SimpleInjector.dll + + ..\packages\SimpleInjector.3.2.2\lib\net45\SimpleInjector.dll True diff --git a/MediaBrowser.Server.Implementations/packages.config b/MediaBrowser.Server.Implementations/packages.config index 94522cd50..5d58aea19 100644 --- a/MediaBrowser.Server.Implementations/packages.config +++ b/MediaBrowser.Server.Implementations/packages.config @@ -7,6 +7,6 @@ - + \ No newline at end of file diff --git a/Nuget/MediaBrowser.Common.Internal.nuspec b/Nuget/MediaBrowser.Common.Internal.nuspec index ff52aebce..d4f3df3a5 100644 --- a/Nuget/MediaBrowser.Common.Internal.nuspec +++ b/Nuget/MediaBrowser.Common.Internal.nuspec @@ -13,8 +13,8 @@ Copyright © Emby 2013 - - + + -- cgit v1.2.3 From bb117d6b9894c9d6572af1dc348bed6574ddc8ea Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 23 Sep 2016 01:45:39 -0400 Subject: update SocketHttpListener --- .../MediaBrowser.Server.Implementations.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj') diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index b25c07fe3..eb3da1a12 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -73,8 +73,8 @@ ..\packages\SimpleInjector.3.2.2\lib\net45\SimpleInjector.dll True - - ..\packages\SocketHttpListener.1.0.0.39\lib\net45\SocketHttpListener.dll + + ..\packages\SocketHttpListener.1.0.0.40\lib\net45\SocketHttpListener.dll True -- cgit v1.2.3 From d596053ec7830d89a83723b0ae2f8439c6319f6f Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 25 Sep 2016 14:39:13 -0400 Subject: rework live stream handling --- MediaBrowser.Api/ApiEntryPoint.cs | 39 +++++- MediaBrowser.Api/LiveTv/LiveTvService.cs | 33 ++++- MediaBrowser.Api/Playback/Hls/BaseHlsService.cs | 44 +++--- MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs | 7 +- .../Progressive/BaseProgressiveStreamingService.cs | 23 ++- .../Progressive/ProgressiveStreamWriter.cs | 4 +- MediaBrowser.Api/Playback/StreamState.cs | 10 +- MediaBrowser.Controller/LiveTv/ILiveTvManager.cs | 18 +-- MediaBrowser.Controller/LiveTv/ITunerHost.cs | 2 +- MediaBrowser.Controller/LiveTv/LiveStream.cs | 30 ++++ .../MediaBrowser.Controller.csproj | 1 + .../IO/LibraryMonitor.cs | 8 +- .../Library/LibraryManager.cs | 20 ++- .../LiveTv/EmbyTV/DirectRecorder.cs | 18 ++- .../LiveTv/EmbyTV/EmbyTV.cs | 100 +++++++------ .../LiveTv/EmbyTV/EncodedRecorder.cs | 84 +---------- .../LiveTv/TunerHosts/BaseTunerHost.cs | 145 +++++++------------ .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 33 ++++- .../TunerHosts/HdHomerun/HdHomerunLiveStream.cs | 156 +++++++++++++++++++++ .../LiveTv/TunerHosts/M3UTunerHost.cs | 13 +- .../LiveTv/TunerHosts/SatIp/SatIpHost.cs | 10 +- .../MediaBrowser.Server.Implementations.csproj | 1 + .../MediaBrowser.WebDashboard.csproj | 18 +-- MediaBrowser.XbmcMetadata/EntryPoint.cs | 10 ++ 24 files changed, 520 insertions(+), 307 deletions(-) create mode 100644 MediaBrowser.Controller/LiveTv/LiveStream.cs create mode 100644 MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs (limited to 'MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj') diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 214fb7488..2f5b9e1e0 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -8,6 +8,7 @@ using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Session; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -44,7 +45,13 @@ namespace MediaBrowser.Api private readonly IFileSystem _fileSystem; private readonly IMediaSourceManager _mediaSourceManager; - public readonly SemaphoreSlim TranscodingStartLock = new SemaphoreSlim(1, 1); + /// + /// The active transcoding jobs + /// + private readonly List _activeTranscodingJobs = new List(); + + private readonly Dictionary _transcodingLocks = + new Dictionary(); /// /// Initializes a new instance of the class. @@ -67,6 +74,21 @@ namespace MediaBrowser.Api _sessionManager.PlaybackStart += _sessionManager_PlaybackStart; } + public SemaphoreSlim GetTranscodingLock(string outputPath) + { + lock (_transcodingLocks) + { + SemaphoreSlim result; + if (!_transcodingLocks.TryGetValue(outputPath, out result)) + { + result = new SemaphoreSlim(1, 1); + _transcodingLocks[outputPath] = result; + } + + return result; + } + } + private void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e) { if (!string.IsNullOrWhiteSpace(e.PlaySessionId)) @@ -148,11 +170,6 @@ namespace MediaBrowser.Api } } - /// - /// The active transcoding jobs - /// - private readonly List _activeTranscodingJobs = new List(); - /// /// Called when [transcode beginning]. /// @@ -258,6 +275,11 @@ namespace MediaBrowser.Api } } + lock (_transcodingLocks) + { + _transcodingLocks.Remove(path); + } + if (!string.IsNullOrWhiteSpace(state.Request.DeviceId)) { _sessionManager.ClearTranscodingInfo(state.Request.DeviceId); @@ -497,6 +519,11 @@ namespace MediaBrowser.Api } } + lock (_transcodingLocks) + { + _transcodingLocks.Remove(job.Path); + } + lock (job.ProcessLock) { if (job.TranscodingThrottler != null) diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 3ad0ec1ba..a5f8fce6e 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -12,9 +12,13 @@ using ServiceStack; using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Api.Playback.Progressive; +using MediaBrowser.Controller.Configuration; namespace MediaBrowser.Api.LiveTv { @@ -613,16 +617,24 @@ namespace MediaBrowser.Api.LiveTv } + [Route("/LiveTv/LiveStreamFiles/{Id}/stream.{Container}", "GET", Summary = "Gets a live tv channel")] + public class GetLiveStreamFile + { + public string Id { get; set; } + public string Container { get; set; } + } + public class LiveTvService : BaseApiService { private readonly ILiveTvManager _liveTvManager; private readonly IUserManager _userManager; - private readonly IConfigurationManager _config; + private readonly IServerConfigurationManager _config; private readonly IHttpClient _httpClient; private readonly ILibraryManager _libraryManager; private readonly IDtoService _dtoService; + private readonly IFileSystem _fileSystem; - public LiveTvService(ILiveTvManager liveTvManager, IUserManager userManager, IConfigurationManager config, IHttpClient httpClient, ILibraryManager libraryManager, IDtoService dtoService) + public LiveTvService(ILiveTvManager liveTvManager, IUserManager userManager, IServerConfigurationManager config, IHttpClient httpClient, ILibraryManager libraryManager, IDtoService dtoService, IFileSystem fileSystem) { _liveTvManager = liveTvManager; _userManager = userManager; @@ -630,6 +642,23 @@ namespace MediaBrowser.Api.LiveTv _httpClient = httpClient; _libraryManager = libraryManager; _dtoService = dtoService; + _fileSystem = fileSystem; + } + + public object Get(GetLiveStreamFile request) + { + var filePath = Path.Combine(_config.ApplicationPaths.TranscodingTempPath, request.Id + ".ts"); + + var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + + outputHeaders["Content-Type"] = MimeTypes.GetMimeType(filePath); + + var streamSource = new ProgressiveFileCopier(_fileSystem, filePath, outputHeaders, null, Logger, CancellationToken.None) + { + AllowEndOfFile = false + }; + + return ResultFactory.GetAsyncStreamWriter(streamSource); } public object Get(GetDefaultListingProvider request) diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 761b1eb4e..319e4bbb6 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -87,7 +87,8 @@ namespace MediaBrowser.Api.Playback.Hls if (!FileSystem.FileExists(playlist)) { - await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); + var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlist); + await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); try { if (!FileSystem.FileExists(playlist)) @@ -104,13 +105,13 @@ namespace MediaBrowser.Api.Playback.Hls throw; } - var waitForSegments = state.SegmentLength >= 10 ? 2 : (state.SegmentLength > 3 || !isLive ? 3 : 4); + var waitForSegments = state.SegmentLength >= 10 ? 2 : (state.SegmentLength > 3 || !isLive ? 3 : 3); await WaitForMinimumSegmentCount(playlist, waitForSegments, cancellationTokenSource.Token).ConfigureAwait(false); } } finally { - ApiEntryPoint.Instance.TranscodingStartLock.Release(); + transcodingLock.Release(); } } @@ -182,32 +183,41 @@ namespace MediaBrowser.Api.Playback.Hls { Logger.Debug("Waiting for {0} segments in {1}", segmentCount, playlist); - while (true) + while (!cancellationToken.IsCancellationRequested) { - // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written - using (var fileStream = GetPlaylistFileStream(playlist)) + try { - using (var reader = new StreamReader(fileStream)) + // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written + using (var fileStream = GetPlaylistFileStream(playlist)) { - var count = 0; - - while (!reader.EndOfStream) + using (var reader = new StreamReader(fileStream)) { - var line = await reader.ReadLineAsync().ConfigureAwait(false); + var count = 0; - if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1) + while (!reader.EndOfStream) { - count++; - if (count >= segmentCount) + var line = await reader.ReadLineAsync().ConfigureAwait(false); + + if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1) { - Logger.Debug("Finished waiting for {0} segments in {1}", segmentCount, playlist); - return; + count++; + if (count >= segmentCount) + { + Logger.Debug("Finished waiting for {0} segments in {1}", segmentCount, playlist); + return; + } } } + await Task.Delay(100, cancellationToken).ConfigureAwait(false); } - await Task.Delay(100, cancellationToken).ConfigureAwait(false); } } + catch (IOException) + { + // May get an error if the file is locked + } + + await Task.Delay(50, cancellationToken).ConfigureAwait(false); } } diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 9cd55528d..d4ddbd7c5 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -171,14 +171,15 @@ namespace MediaBrowser.Api.Playback.Hls return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false); } - await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); + var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlistPath); + await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); var released = false; try { if (FileSystem.FileExists(segmentPath)) { job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); - ApiEntryPoint.Instance.TranscodingStartLock.Release(); + transcodingLock.Release(); released = true; return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false); } @@ -242,7 +243,7 @@ namespace MediaBrowser.Api.Playback.Hls { if (!released) { - ApiEntryPoint.Instance.TranscodingStartLock.Release(); + transcodingLock.Release(); } } diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index b8cb6b14f..a68319109 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -17,6 +17,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using CommonIO; +using ServiceStack; namespace MediaBrowser.Api.Playback.Progressive { @@ -129,6 +130,23 @@ namespace MediaBrowser.Api.Playback.Progressive using (state) { + if (state.MediaPath.IndexOf("/livestreamfiles/", StringComparison.OrdinalIgnoreCase) != -1) + { + var parts = state.MediaPath.Split('/'); + var filename = parts[parts.Length - 2] + Path.GetExtension(parts[parts.Length - 1]); + var filePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, filename); + + var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + + outputHeaders["Content-Type"] = MimeTypes.GetMimeType(filePath); + + var streamSource = new ProgressiveFileCopier(FileSystem, filePath, outputHeaders, null, Logger, CancellationToken.None) + { + AllowEndOfFile = false + }; + return ResultFactory.GetAsyncStreamWriter(streamSource); + } + return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource) .ConfigureAwait(false); } @@ -345,7 +363,8 @@ namespace MediaBrowser.Api.Playback.Progressive return streamResult; } - await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); + var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath); + await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); try { TranscodingJob job; @@ -376,7 +395,7 @@ namespace MediaBrowser.Api.Playback.Progressive } finally { - ApiEntryPoint.Instance.TranscodingStartLock.Release(); + transcodingLock.Release(); } } diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs index 0a9a44641..80b5e357d 100644 --- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs +++ b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs @@ -24,6 +24,8 @@ namespace MediaBrowser.Api.Playback.Progressive private long _bytesWritten = 0; + public bool AllowEndOfFile = true; + public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken) { _fileSystem = fileSystem; @@ -50,7 +52,7 @@ namespace MediaBrowser.Api.Playback.Progressive using (var fs = _fileSystem.GetFileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) { - while (eofCount < 15) + while (eofCount < 15 || !AllowEndOfFile) { var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, _cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index 109aa85de..ef0282abc 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -73,10 +73,6 @@ namespace MediaBrowser.Api.Playback { get { - if (!RunTimeTicks.HasValue) - { - return 6; - } if (string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { var userAgent = UserAgent ?? string.Empty; @@ -92,12 +88,16 @@ namespace MediaBrowser.Api.Playback return 10; } + if (!RunTimeTicks.HasValue) + { + return 3; + } return 6; } if (!RunTimeTicks.HasValue) { - return 6; + return 3; } return 3; } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index a8e42749b..41c5dbdbb 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -37,7 +37,7 @@ namespace MediaBrowser.Controller.LiveTv /// The cancellation token. /// Task{SeriesTimerInfoDto}. Task GetNewTimerDefaults(string programId, CancellationToken cancellationToken); - + /// /// Deletes the recording. /// @@ -51,7 +51,7 @@ namespace MediaBrowser.Controller.LiveTv /// The recording. /// Task. Task DeleteRecording(BaseItem recording); - + /// /// Cancels the timer. /// @@ -83,7 +83,7 @@ namespace MediaBrowser.Controller.LiveTv /// The user. /// Task{RecordingInfoDto}. Task GetRecording(string id, DtoOptions options, CancellationToken cancellationToken, User user = null); - + /// /// Gets the timer. /// @@ -125,14 +125,14 @@ namespace MediaBrowser.Controller.LiveTv /// The cancellation token. /// Task{QueryResult{SeriesTimerInfoDto}}. Task> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken); - + /// /// Gets the channel. /// /// The identifier. /// Channel. LiveTvChannel GetInternalChannel(string id); - + /// /// Gets the recording. /// @@ -157,7 +157,7 @@ namespace MediaBrowser.Controller.LiveTv /// The cancellation token. /// Task{StreamResponseInfo}. Task GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken); - + /// /// Gets the program. /// @@ -331,8 +331,8 @@ namespace MediaBrowser.Controller.LiveTv /// The fields. /// The user. /// Task. - Task AddInfoToProgramDto(List> programs, List fields, User user = null); - + Task AddInfoToProgramDto(List> programs, List fields, User user = null); + /// /// Saves the tuner host. /// @@ -395,7 +395,7 @@ namespace MediaBrowser.Controller.LiveTv Task> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken); Task> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken); - List ListingProviders { get;} + List ListingProviders { get; } event EventHandler> SeriesTimerCancelled; event EventHandler> TimerCancelled; diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs index 1e7aa3de5..3c8b964a2 100644 --- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs +++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs @@ -38,7 +38,7 @@ namespace MediaBrowser.Controller.LiveTv /// The stream identifier. /// The cancellation token. /// Task<MediaSourceInfo>. - Task> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken); + Task GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken); /// /// Gets the channel stream media sources. /// diff --git a/MediaBrowser.Controller/LiveTv/LiveStream.cs b/MediaBrowser.Controller/LiveTv/LiveStream.cs new file mode 100644 index 000000000..15d09d857 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/LiveStream.cs @@ -0,0 +1,30 @@ +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Dto; + +namespace MediaBrowser.Controller.LiveTv +{ + public class LiveStream + { + public MediaSourceInfo OriginalMediaSource { get; set; } + public MediaSourceInfo PublicMediaSource { get; set; } + public string Id { get; set; } + + public LiveStream(MediaSourceInfo mediaSource) + { + OriginalMediaSource = mediaSource; + PublicMediaSource = mediaSource; + Id = mediaSource.Id; + } + + public virtual Task Open(CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public virtual Task Close() + { + return Task.FromResult(true); + } + } +} diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index cb36afa5f..d70fba742 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -201,6 +201,7 @@ + diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs index bcc4e5dcf..76f0e6a1d 100644 --- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -43,16 +43,14 @@ namespace MediaBrowser.Server.Implementations.IO // WMC temp recording directories that will constantly be written to "TempRec", - "TempSBE", - "@eaDir", - "eaDir", - "#recycle" + "TempSBE" }; private readonly IReadOnlyList _alwaysIgnoreSubstrings = new List { // Synology - "@eaDir", + "eaDir", + "#recycle", ".wd_tv", ".actors" }; diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index 7c3196065..b076996df 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -2803,6 +2803,17 @@ namespace MediaBrowser.Server.Implementations.Library } } + private bool ValidateNetworkPath(string path) + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT || !path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase)) + { + return Directory.Exists(path); + } + + // Without native support for unc, we cannot validate this when running under mono + return true; + } + private const string ShortcutFileExtension = ".mblink"; private const string ShortcutFileSearch = "*" + ShortcutFileExtension; public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo) @@ -2829,12 +2840,7 @@ namespace MediaBrowser.Server.Implementations.Library throw new DirectoryNotFoundException("The path does not exist."); } - if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !_fileSystem.DirectoryExists(pathInfo.NetworkPath)) - { - throw new DirectoryNotFoundException("The network path does not exist."); - } - - if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !_fileSystem.DirectoryExists(pathInfo.NetworkPath)) + if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath)) { throw new DirectoryNotFoundException("The network path does not exist."); } @@ -2877,7 +2883,7 @@ namespace MediaBrowser.Server.Implementations.Library throw new ArgumentNullException("path"); } - if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !_fileSystem.DirectoryExists(pathInfo.NetworkPath)) + if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath)) { throw new DirectoryNotFoundException("The network path does not exist."); } diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index 2e3edf3e9..0d043669a 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -69,11 +69,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } private const int BufferSize = 81920; - public static async Task CopyUntilCancelled(Stream source, Stream target, CancellationToken cancellationToken) + public static Task CopyUntilCancelled(Stream source, Stream target, CancellationToken cancellationToken) + { + return CopyUntilCancelled(source, target, null, cancellationToken); + } + public static async Task CopyUntilCancelled(Stream source, Stream target, Action onStarted, CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { - var bytesRead = await CopyToAsyncInternal(source, target, BufferSize, cancellationToken).ConfigureAwait(false); + var bytesRead = await CopyToAsyncInternal(source, target, BufferSize, onStarted, cancellationToken).ConfigureAwait(false); + + onStarted = null; //var position = fs.Position; //_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path); @@ -85,7 +91,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - private static async Task CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, CancellationToken cancellationToken) + private static async Task CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, Action onStarted, CancellationToken cancellationToken) { byte[] buffer = new byte[bufferSize]; int bytesRead; @@ -96,6 +102,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); totalBytesRead += bytesRead; + + if (onStarted != null) + { + onStarted(); + } + onStarted = null; } return totalBytesRead; diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index e358f9d25..6585e92be 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -746,33 +746,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV throw new NotImplementedException(); } + private readonly SemaphoreSlim _liveStreamsSemaphore = new SemaphoreSlim(1, 1); + private readonly Dictionary _liveStreams = new Dictionary(); + public async Task GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) { - _logger.Info("Streaming Channel " + channelId); + var result = await GetChannelStreamInternal(channelId, streamId, cancellationToken).ConfigureAwait(false); - foreach (var hostInstance in _liveTvManager.TunerHosts) - { - try - { - var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); - - result.Item2.Release(); - - return result.Item1; - } - catch (FileNotFoundException) - { - } - catch (Exception e) - { - _logger.ErrorException("Error getting channel stream", e); - } - } - - throw new ApplicationException("Tuner not found."); + return result.Item1.PublicMediaSource; } - private async Task> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken) + private async Task> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken) { _logger.Info("Streaming Channel " + channelId); @@ -782,7 +766,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); - return new Tuple(result.Item1, hostInstance, result.Item2); + await _liveStreamsSemaphore.WaitAsync().ConfigureAwait(false); + _liveStreams[result.Id] = result; + _liveStreamsSemaphore.Release(); + + return new Tuple(result, hostInstance); } catch (FileNotFoundException) { @@ -823,9 +811,31 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV throw new NotImplementedException(); } - public Task CloseLiveStream(string id, CancellationToken cancellationToken) + public async Task CloseLiveStream(string id, CancellationToken cancellationToken) { - return Task.FromResult(0); + await _liveStreamsSemaphore.WaitAsync().ConfigureAwait(false); + + try + { + LiveStream stream; + if (_liveStreams.TryGetValue(id, out stream)) + { + _liveStreams.Remove(id); + + try + { + await stream.Close().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error closing live stream", ex); + } + } + } + finally + { + _liveStreamsSemaphore.Release(); + } } public Task RecordLiveStream(string id, CancellationToken cancellationToken) @@ -999,15 +1009,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV string seriesPath = null; var recordPath = GetRecordingPath(timer, out seriesPath); var recordingStatus = RecordingStatus.New; - var isResourceOpen = false; - SemaphoreSlim semaphore = null; + + LiveStream liveStream = null; try { - var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false); - isResourceOpen = true; - semaphore = result.Item3; - var mediaStreamInfo = result.Item1; + var allMediaSources = await GetChannelStreamMediaSources(timer.ChannelId, CancellationToken.None).ConfigureAwait(false); + + var liveStreamInfo = await GetChannelStreamInternal(timer.ChannelId, allMediaSources[0].Id, CancellationToken.None).ConfigureAwait(false); + liveStream = liveStreamInfo.Item1; + var mediaStreamInfo = liveStreamInfo.Item1.PublicMediaSource; + var tunerHost = liveStreamInfo.Item2; // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg //await Task.Delay(3000, cancellationToken).ConfigureAwait(false); @@ -1034,13 +1046,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV timer.Status = RecordingStatus.InProgress; _timerProvider.AddOrUpdate(timer, false); - result.Item3.Release(); - isResourceOpen = false; - SaveNfo(timer, recordPath, seriesPath); }; - var pathWithDuration = result.Item2.ApplyDuration(mediaStreamInfo.Path, duration); + var pathWithDuration = tunerHost.ApplyDuration(mediaStreamInfo.Path, duration); // If it supports supplying duration via url if (!string.Equals(pathWithDuration, mediaStreamInfo.Path, StringComparison.OrdinalIgnoreCase)) @@ -1064,19 +1073,24 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _logger.ErrorException("Error recording to {0}", ex, recordPath); recordingStatus = RecordingStatus.Error; } - finally + + if (liveStream != null) { - if (isResourceOpen && semaphore != null) + try + { + await CloseLiveStream(liveStream.Id, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) { - semaphore.Release(); + _logger.ErrorException("Error closing live stream", ex); } + } - _libraryManager.UnRegisterIgnoredPath(recordPath); - _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true); + _libraryManager.UnRegisterIgnoredPath(recordPath); + _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true); - ActiveRecordingInfo removed; - _activeRecordings.TryRemove(timer.Id, out removed); - } + ActiveRecordingInfo removed; + _activeRecordings.TryRemove(timer.Id, out removed); if (recordingStatus == RecordingStatus.Completed) { diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 4e7f637b1..f74a76e3f 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -68,18 +68,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { - if (mediaSource.Path.IndexOf("m3u8", StringComparison.OrdinalIgnoreCase) != -1) - { - await RecordWithoutTempFile(mediaSource, targetFile, duration, onStarted, cancellationToken) - .ConfigureAwait(false); - - return; - } + var durationToken = new CancellationTokenSource(duration); + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - var tempfile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts"); + await RecordFromFile(mediaSource, mediaSource.Path, targetFile, false, duration, onStarted, cancellationToken).ConfigureAwait(false); - await RecordWithTempFile(mediaSource, tempfile, targetFile, duration, onStarted, cancellationToken) - .ConfigureAwait(false); + _logger.Info("Recording completed to file {0}", targetFile); } private async void DeleteTempFile(string path) @@ -108,76 +102,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - private async Task RecordWithoutTempFile(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) - { - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - - await RecordFromFile(mediaSource, mediaSource.Path, targetFile, false, duration, onStarted, cancellationToken).ConfigureAwait(false); - - _logger.Info("Recording completed to file {0}", targetFile); - } - - private async Task RecordWithTempFile(MediaSourceInfo mediaSource, string tempFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) - { - var httpRequestOptions = new HttpRequestOptions() - { - Url = mediaSource.Path - }; - - httpRequestOptions.BufferContent = false; - - using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false)) - { - _logger.Info("Opened recording stream from tuner provider"); - - Directory.CreateDirectory(Path.GetDirectoryName(tempFile)); - - using (var output = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - //onStarted(); - - _logger.Info("Copying recording stream to file {0}", tempFile); - - var bufferMs = 5000; - - if (mediaSource.RunTimeTicks.HasValue) - { - // The media source already has a fixed duration - // But add another stop 1 minute later just in case the recording gets stuck for any reason - var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMinutes(1))); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - } - else - { - // The media source if infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMilliseconds(bufferMs))); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - } - - var tempFileTask = DirectRecorder.CopyUntilCancelled(response.Content, output, cancellationToken); - - // Give the temp file a little time to build up - await Task.Delay(bufferMs, cancellationToken).ConfigureAwait(false); - - var recordTask = Task.Run(() => RecordFromFile(mediaSource, tempFile, targetFile, true, duration, onStarted, cancellationToken), CancellationToken.None); - - try - { - await tempFileTask.ConfigureAwait(false); - } - catch (OperationCanceledException) - { - - } - - await recordTask.ConfigureAwait(false); - } - } - - _logger.Info("Recording completed to file {0}", targetFile); - } - private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, bool deleteInputFileAfterCompletion, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { _targetPath = targetFile; diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 3f6bb140b..6beea352a 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -10,6 +10,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Serialization; @@ -18,7 +19,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { public abstract class BaseTunerHost { - protected readonly IConfigurationManager Config; + protected readonly IServerConfigurationManager Config; protected readonly ILogger Logger; protected IJsonSerializer JsonSerializer; protected readonly IMediaEncoder MediaEncoder; @@ -26,7 +27,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts private readonly ConcurrentDictionary _channelCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - protected BaseTunerHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder) + protected BaseTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder) { Config = config; Logger = logger; @@ -125,12 +126,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts foreach (var host in hostsWithChannel) { - var resourcePool = GetLock(host.Url); - Logger.Debug("GetChannelStreamMediaSources - Waiting on tuner resource pool"); - - await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - Logger.Debug("GetChannelStreamMediaSources - Unlocked resource pool"); - try { // Check to make sure the tuner is available @@ -156,93 +151,63 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { Logger.Error("Error opening tuner", ex); } - finally - { - resourcePool.Release(); - } } } return new List(); } - protected abstract Task GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken); + protected abstract Task GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken); - public async Task> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) + public async Task GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) { - if (IsValidChannelId(channelId)) + if (!IsValidChannelId(channelId)) { - var hosts = GetTunerHosts(); - - var hostsWithChannel = new List(); + throw new FileNotFoundException(); + } - foreach (var host in hosts) - { - if (string.IsNullOrWhiteSpace(streamId)) - { - try - { - var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false); + var hosts = GetTunerHosts(); - if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase))) - { - hostsWithChannel.Add(host); - } - } - catch (Exception ex) - { - Logger.Error("Error getting channels", ex); - } - } - else if (streamId.StartsWith(host.Id, StringComparison.OrdinalIgnoreCase)) - { - hostsWithChannel = new List {host}; - streamId = streamId.Substring(host.Id.Length); - break; - } - } + var hostsWithChannel = new List(); - foreach (var host in hostsWithChannel) + foreach (var host in hosts) + { + if (string.IsNullOrWhiteSpace(streamId)) { - var resourcePool = GetLock(host.Url); - Logger.Debug("GetChannelStream - Waiting on tuner resource pool"); - await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - Logger.Debug("GetChannelStream - Unlocked resource pool"); try { - // Check to make sure the tuner is available - // If there's only one tuner, don't bother with the check and just let the tuner be the one to throw an error - // If a streamId is specified then availibility has already been checked in GetChannelStreamMediaSources - if (string.IsNullOrWhiteSpace(streamId) && hostsWithChannel.Count > 1) - { - if (!await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false)) - { - Logger.Error("Tuner is not currently available"); - resourcePool.Release(); - continue; - } - } - - var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false); + var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false); - if (EnableMediaProbing) + if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase))) { - await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false); + hostsWithChannel.Add(host); } - - return new Tuple(stream, resourcePool); } catch (Exception ex) { - Logger.Error("Error opening tuner", ex); - - resourcePool.Release(); + Logger.Error("Error getting channels", ex); } } + else if (streamId.StartsWith(host.Id, StringComparison.OrdinalIgnoreCase)) + { + hostsWithChannel = new List { host }; + streamId = streamId.Substring(host.Id.Length); + break; + } } - else + + foreach (var host in hostsWithChannel) { - throw new FileNotFoundException(); + try + { + var liveStream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false); + await liveStream.Open(cancellationToken).ConfigureAwait(false); + return liveStream; + } + catch (Exception ex) + { + Logger.Error("Error opening tuner", ex); + } } throw new LiveTvConflictException(); @@ -268,37 +233,23 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts protected abstract Task IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken); - /// - /// The _semaphoreLocks - /// - private readonly ConcurrentDictionary _semaphoreLocks = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - /// - /// Gets the lock. - /// - /// The filename. - /// System.Object. - private SemaphoreSlim GetLock(string url) - { - return _semaphoreLocks.GetOrAdd(url, key => new SemaphoreSlim(1, 1)); - } - - private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, SemaphoreSlim resourcePool, CancellationToken cancellationToken) + private async Task AddMediaInfo(LiveStream stream, bool isAudio, CancellationToken cancellationToken) { - await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + //await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - await AddMediaInfoInternal(mediaSource, isAudio, cancellationToken).ConfigureAwait(false); + //try + //{ + // await AddMediaInfoInternal(mediaSource, isAudio, cancellationToken).ConfigureAwait(false); - // Leave the resource locked. it will be released upstream - } - catch (Exception) - { - // Release the resource if there's some kind of failure. - resourcePool.Release(); + // // Leave the resource locked. it will be released upstream + //} + //catch (Exception) + //{ + // // Release the resource if there's some kind of failure. + // resourcePool.Release(); - throw; - } + // throw; + //} } private async Task AddMediaInfoInternal(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken) diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index c5bd648cf..b40b74436 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -14,7 +14,10 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Net; @@ -24,11 +27,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IServerApplicationHost _appHost; - public HdHomerunHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient) + public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost) : base(config, logger, jsonSerializer, mediaEncoder) { _httpClient = httpClient; + _fileSystem = fileSystem; + _appHost = appHost; } public string Name @@ -355,6 +362,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun url += "?transcode=" + profile; } + var id = profile; + if (string.IsNullOrWhiteSpace(id)) + { + id = "native"; + } + id += "_" + url.GetMD5().ToString("N"); + var mediaSource = new MediaSourceInfo { Path = url, @@ -387,9 +401,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun RequiresClosing = false, BufferMs = 0, Container = "ts", - Id = profile, - SupportsDirectPlay = true, - SupportsDirectStream = false, + Id = id, + SupportsDirectPlay = false, + SupportsDirectStream = true, SupportsTranscoding = true }; @@ -452,9 +466,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase); } - protected override async Task GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) + protected override async Task GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) { - Logger.Info("GetChannelStream: channel id: {0}. stream id: {1}", channelId, streamId ?? string.Empty); + var profile = streamId.Split('_')[0]; + + Logger.Info("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelId, streamId, profile); if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase)) { @@ -462,7 +478,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun } var hdhrId = GetHdHrIdFromChannelId(channelId); - return await GetMediaSource(info, hdhrId, streamId).ConfigureAwait(false); + var mediaSource = await GetMediaSource(info, hdhrId, profile).ConfigureAwait(false); + + var liveStream = new HdHomerunLiveStream(mediaSource, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost); + return liveStream; } public async Task Validate(TunerHostInfo info) diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs new file mode 100644 index 000000000..6078c4a70 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs @@ -0,0 +1,156 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Server.Implementations.LiveTv.EmbyTV; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun +{ + public class HdHomerunLiveStream : LiveStream + { + private readonly ILogger _logger; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IServerApplicationPaths _appPaths; + private readonly IServerApplicationHost _appHost; + + private readonly CancellationTokenSource _liveStreamCancellationTokenSource = new CancellationTokenSource(); + + public HdHomerunLiveStream(MediaSourceInfo mediaSource, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost) + : base(mediaSource) + { + _fileSystem = fileSystem; + _httpClient = httpClient; + _logger = logger; + _appPaths = appPaths; + _appHost = appHost; + } + + public override async Task Open(CancellationToken openCancellationToken) + { + _liveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested(); + + var mediaSource = OriginalMediaSource; + + var url = mediaSource.Path; + var tempFile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts"); + Directory.CreateDirectory(Path.GetDirectoryName(tempFile)); + + _logger.Info("Opening HDHR Live stream from {0} to {1}", url, tempFile); + + var output = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read); + + var taskCompletionSource = new TaskCompletionSource(); + + StartStreamingToTempFile(output, tempFile, url, taskCompletionSource, _liveStreamCancellationTokenSource.Token); + + await taskCompletionSource.Task.ConfigureAwait(false); + + PublicMediaSource.Path = _appHost.GetLocalApiUrl("localhost") + "/LiveTv/LiveStreamFiles/" + Path.GetFileNameWithoutExtension(tempFile) + "/stream.ts"; + + PublicMediaSource.Protocol = MediaProtocol.Http; + } + + public override Task Close() + { + _liveStreamCancellationTokenSource.Cancel(); + + return base.Close(); + } + + private async Task StartStreamingToTempFile(Stream outputStream, string tempFilePath, string url, TaskCompletionSource openTaskCompletionSource, CancellationToken cancellationToken) + { + await Task.Run(async () => + { + using (outputStream) + { + var isFirstAttempt = true; + + while (!cancellationToken.IsCancellationRequested) + { + try + { + using (var response = await _httpClient.SendAsync(new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + BufferContent = false + + }, "GET").ConfigureAwait(false)) + { + _logger.Info("Opened HDHR stream from {0}", url); + + if (!cancellationToken.IsCancellationRequested) + { + _logger.Info("Beginning DirectRecorder.CopyUntilCancelled"); + + Action onStarted = null; + if (isFirstAttempt) + { + onStarted = () => openTaskCompletionSource.TrySetResult(true); + } + await DirectRecorder.CopyUntilCancelled(response.Content, outputStream, onStarted, cancellationToken).ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + if (isFirstAttempt) + { + _logger.ErrorException("Error opening live stream:", ex); + openTaskCompletionSource.TrySetException(ex); + break; + } + + _logger.ErrorException("Error copying live stream, will reopen", ex); + } + + isFirstAttempt = false; + } + } + + await Task.Delay(5000).ConfigureAwait(false); + + DeleteTempFile(tempFilePath); + + }).ConfigureAwait(false); + } + + private async void DeleteTempFile(string path) + { + for (var i = 0; i < 10; i++) + { + try + { + File.Delete(path); + return; + } + catch (FileNotFoundException) + { + return; + } + catch (DirectoryNotFoundException) + { + return; + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting temp file {0}", ex, path); + } + + await Task.Delay(1000).ConfigureAwait(false); + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 5c508aacd..d9c0bb8bf 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -13,8 +13,10 @@ using System.Threading; using System.Threading.Tasks; using CommonIO; using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Serialization; +using MediaBrowser.Server.Implementations.LiveTv.EmbyTV; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { @@ -23,7 +25,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts private readonly IFileSystem _fileSystem; private readonly IHttpClient _httpClient; - public M3UTunerHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) + public M3UTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) : base(config, logger, jsonSerializer, mediaEncoder) { _fileSystem = fileSystem; @@ -63,11 +65,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts return Task.FromResult(list); } - protected override async Task GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) + protected override async Task GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) { var sources = await GetChannelStreamMediaSources(info, channelId, cancellationToken).ConfigureAwait(false); - return sources.First(); + var liveStream = new LiveStream(sources.First()); + return liveStream; } public async Task Validate(TunerHostInfo info) @@ -136,7 +139,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts RequiresOpening = false, RequiresClosing = false, - ReadAtNativeFramerate = false + ReadAtNativeFramerate = false, + + Id = channel.Path.GetMD5().ToString("N") }; return new List { mediaSource }; diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs index b1e349a86..81deb2995 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs @@ -8,6 +8,7 @@ using CommonIO; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dto; @@ -16,6 +17,7 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Logging; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; +using MediaBrowser.Server.Implementations.LiveTv.EmbyTV; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp { @@ -24,7 +26,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp private readonly IFileSystem _fileSystem; private readonly IHttpClient _httpClient; - public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) + public SatIpHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) : base(config, logger, jsonSerializer, mediaEncoder) { _fileSystem = fileSystem; @@ -113,11 +115,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp return new List(); } - protected override async Task GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken) + protected override async Task GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken) { var sources = await GetChannelStreamMediaSources(tuner, channelId, cancellationToken).ConfigureAwait(false); - return sources.First(); + var liveStream = new LiveStream(sources.First()); + + return liveStream; } protected override async Task IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index eb3da1a12..12691a69b 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -241,6 +241,7 @@ + diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 00279fb05..b57416fab 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -104,6 +104,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -437,15 +443,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - PreserveNewest @@ -470,9 +467,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest diff --git a/MediaBrowser.XbmcMetadata/EntryPoint.cs b/MediaBrowser.XbmcMetadata/EntryPoint.cs index 0844f1f74..bf3d3c303 100644 --- a/MediaBrowser.XbmcMetadata/EntryPoint.cs +++ b/MediaBrowser.XbmcMetadata/EntryPoint.cs @@ -91,6 +91,16 @@ namespace MediaBrowser.XbmcMetadata return; } + if (!item.SupportsLocalMetadata) + { + return; + } + + if (!item.IsSaveLocalMetadataEnabled()) + { + return; + } + try { await _providerManager.SaveMetadata(item, updateReason, new[] { BaseNfoSaver.SaverName }).ConfigureAwait(false); -- cgit v1.2.3 From 50e66869872579d2cbd8337c4b114cf68dff814a Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 7 Oct 2016 11:08:13 -0400 Subject: update live stream management --- Emby.Drawing/ImageProcessor.cs | 13 +- MediaBrowser.Api/MediaBrowser.Api.csproj | 4 + MediaBrowser.Api/StartupWizardService.cs | 1 + MediaBrowser.Controller/Entities/BaseItem.cs | 28 ++- MediaBrowser.Controller/Entities/Game.cs | 2 +- MediaBrowser.Controller/Entities/IHasImages.cs | 6 +- MediaBrowser.Controller/Entities/Movies/Movie.cs | 6 +- MediaBrowser.Controller/Entities/MusicVideo.cs | 11 +- MediaBrowser.Controller/Entities/Trailer.cs | 4 +- MediaBrowser.Controller/Entities/Video.cs | 2 +- MediaBrowser.Controller/LiveTv/LiveStream.cs | 1 + .../MediaBrowser.Controller.csproj | 2 - MediaBrowser.Controller/Net/IHttpResultFactory.cs | 8 - .../Providers/IImageFileSaver.cs | 20 -- MediaBrowser.Controller/Providers/IImageSaver.cs | 11 - .../Providers/IProviderManager.cs | 7 - MediaBrowser.Controller/Providers/ItemInfo.cs | 2 +- MediaBrowser.Dlna/Eventing/EventManager.cs | 1 - .../Images/LocalImageProvider.cs | 2 +- MediaBrowser.LocalMetadata/Savers/GameXmlSaver.cs | 2 +- .../Encoder/EncodingUtils.cs | 2 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 4 +- .../Configuration/ServerConfiguration.cs | 1 + .../Notifications/NotificationType.cs | 1 - MediaBrowser.Providers/Manager/ImageSaver.cs | 10 +- MediaBrowser.Providers/Manager/ProviderManager.cs | 10 +- .../TV/TheTVDB/TvdbSeriesProvider.cs | 3 +- .../EntryPoints/Notifications/Notifications.cs | 17 +- .../HttpServer/HttpListenerHost.cs | 10 +- .../HttpServer/HttpResultFactory.cs | 23 -- .../HttpServer/NativeWebSocket.cs | 240 ------------------ .../HttpServer/RangeRequestWriter.cs | 18 -- .../SocketSharp/WebSocketSharpResponse.cs | 18 +- .../Library/LibraryManager.cs | 2 +- .../Library/Resolvers/Audio/MusicArtistResolver.cs | 18 +- .../Library/Resolvers/Movies/MovieResolver.cs | 30 +-- .../Library/UserDataManager.cs | 24 +- .../LiveTv/EmbyTV/EmbyTV.cs | 33 ++- .../LiveTv/LiveTvManager.cs | 132 ++++------ .../LiveTv/LiveTvMediaSourceProvider.cs | 19 +- .../LiveTv/TunerHosts/BaseTunerHost.cs | 19 -- .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 38 ++- .../TunerHosts/HdHomerun/HdHomerunLiveStream.cs | 196 ++++----------- .../LiveTv/TunerHosts/MulticastStream.cs | 96 ++++++++ .../LiveTv/TunerHosts/QueueStream.cs | 93 +++++++ .../Localization/Core/en-US.json | 1 - .../MediaBrowser.Server.Implementations.csproj | 3 +- .../Notifications/CoreNotificationTypes.cs | 7 - .../Persistence/SqliteItemRepository.cs | 7 +- .../ServerManager/WebSocketConnection.cs | 63 +---- .../ApplicationHost.cs | 15 +- MediaBrowser.XbmcMetadata/Images/XbmcImageSaver.cs | 272 --------------------- .../MediaBrowser.XbmcMetadata.csproj | 1 - 53 files changed, 484 insertions(+), 1075 deletions(-) delete mode 100644 MediaBrowser.Controller/Providers/IImageFileSaver.cs delete mode 100644 MediaBrowser.Controller/Providers/IImageSaver.cs delete mode 100644 MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs create mode 100644 MediaBrowser.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs create mode 100644 MediaBrowser.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs delete mode 100644 MediaBrowser.XbmcMetadata/Images/XbmcImageSaver.cs (limited to 'MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj') diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index 80ebbb719..e9f8f81f3 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -829,18 +829,7 @@ namespace Emby.Drawing // Run the enhancers sequentially in order of priority foreach (var enhancer in imageEnhancers) { - var typeName = enhancer.GetType().Name; - - try - { - await enhancer.EnhanceImageAsync(item, inputPath, outputPath, imageType, imageIndex).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("{0} failed enhancing {1}", ex, typeName, item.Name); - - throw; - } + await enhancer.EnhanceImageAsync(item, inputPath, outputPath, imageType, imageIndex).ConfigureAwait(false); // Feed the output into the next enhancer as input inputPath = outputPath; diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index a98637650..96d7874f0 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -197,6 +197,10 @@ {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B} MediaBrowser.Model + + {2e781478-814d-4a48-9d80-bff206441a65} + MediaBrowser.Server.Implementations + diff --git a/MediaBrowser.Api/StartupWizardService.cs b/MediaBrowser.Api/StartupWizardService.cs index ebb3204a4..4c5abc996 100644 --- a/MediaBrowser.Api/StartupWizardService.cs +++ b/MediaBrowser.Api/StartupWizardService.cs @@ -116,6 +116,7 @@ namespace MediaBrowser.Api config.EnableCaseSensitiveItemIds = true; //config.EnableFolderView = true; config.SchemaVersion = 109; + config.EnableSimpleArtistDetection = true; } public void Post(UpdateStartupConfiguration request) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index be88c535e..90a22b217 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -115,6 +115,22 @@ namespace MediaBrowser.Controller.Entities [IgnoreDataMember] public bool IsInMixedFolder { get; set; } + [IgnoreDataMember] + protected virtual bool SupportsIsInMixedFolderDetection + { + get { return false; } + } + + public bool DetectIsInMixedFolder() + { + if (SupportsIsInMixedFolderDetection) + { + + } + + return IsInMixedFolder; + } + [IgnoreDataMember] public virtual bool SupportsRemoteImageDownloading { @@ -1116,7 +1132,7 @@ namespace MediaBrowser.Controller.Entities var hasThemeMedia = this as IHasThemeMedia; if (hasThemeMedia != null) { - if (!IsInMixedFolder) + if (!DetectIsInMixedFolder()) { themeSongsChanged = await RefreshThemeSongs(hasThemeMedia, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); @@ -1266,7 +1282,15 @@ namespace MediaBrowser.Controller.Entities { var current = this; - return current.IsInMixedFolder == newItem.IsInMixedFolder; + if (!SupportsIsInMixedFolderDetection) + { + if (current.IsInMixedFolder != newItem.IsInMixedFolder) + { + return false; + } + } + + return true; } public void AfterMetadataRefresh() diff --git a/MediaBrowser.Controller/Entities/Game.cs b/MediaBrowser.Controller/Entities/Game.cs index 24910498f..a48b9f564 100644 --- a/MediaBrowser.Controller/Entities/Game.cs +++ b/MediaBrowser.Controller/Entities/Game.cs @@ -98,7 +98,7 @@ namespace MediaBrowser.Controller.Entities public override IEnumerable GetDeletePaths() { - if (!IsInMixedFolder) + if (!DetectIsInMixedFolder()) { return new[] { System.IO.Path.GetDirectoryName(Path) }; } diff --git a/MediaBrowser.Controller/Entities/IHasImages.cs b/MediaBrowser.Controller/Entities/IHasImages.cs index a38b7394d..1ab0566e0 100644 --- a/MediaBrowser.Controller/Entities/IHasImages.cs +++ b/MediaBrowser.Controller/Entities/IHasImages.cs @@ -150,11 +150,7 @@ namespace MediaBrowser.Controller.Entities /// true if [supports local metadata]; otherwise, false. bool SupportsLocalMetadata { get; } - /// - /// Gets a value indicating whether this instance is in mixed folder. - /// - /// true if this instance is in mixed folder; otherwise, false. - bool IsInMixedFolder { get; } + bool DetectIsInMixedFolder(); /// /// Gets a value indicating whether this instance is locked. diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index f0270497c..e1e336147 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -81,7 +81,7 @@ namespace MediaBrowser.Controller.Entities.Movies // Must have a parent to have special features // In other words, it must be part of the Parent/Child tree - if (LocationType == LocationType.FileSystem && GetParent() != null && !IsInMixedFolder) + if (LocationType == LocationType.FileSystem && GetParent() != null && !DetectIsInMixedFolder()) { var specialFeaturesChanged = await RefreshSpecialFeatures(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); @@ -119,7 +119,7 @@ namespace MediaBrowser.Controller.Entities.Movies { var info = GetItemLookupInfo(); - if (!IsInMixedFolder) + if (!DetectIsInMixedFolder()) { info.Name = System.IO.Path.GetFileName(ContainingFolderPath); } @@ -145,7 +145,7 @@ namespace MediaBrowser.Controller.Entities.Movies else { // Try to get the year from the folder name - if (!IsInMixedFolder) + if (!DetectIsInMixedFolder()) { info = LibraryManager.ParseName(System.IO.Path.GetFileName(ContainingFolderPath)); diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs index 8b749b7a5..9254802dd 100644 --- a/MediaBrowser.Controller/Entities/MusicVideo.cs +++ b/MediaBrowser.Controller/Entities/MusicVideo.cs @@ -37,6 +37,15 @@ namespace MediaBrowser.Controller.Entities } } + [IgnoreDataMember] + protected override bool SupportsIsInMixedFolderDetection + { + get + { + return true; + } + } + public override UnratedItem GetBlockUnratedType() { return UnratedItem.Music; @@ -65,7 +74,7 @@ namespace MediaBrowser.Controller.Entities else { // Try to get the year from the folder name - if (!IsInMixedFolder) + if (!DetectIsInMixedFolder()) { info = LibraryManager.ParseName(System.IO.Path.GetFileName(ContainingFolderPath)); diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index 7a987a68e..f68cd2c85 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Entities info.IsLocalTrailer = TrailerTypes.Contains(TrailerType.LocalTrailer); - if (!IsInMixedFolder && LocationType == LocationType.FileSystem) + if (!DetectIsInMixedFolder() && LocationType == LocationType.FileSystem) { info.Name = System.IO.Path.GetFileName(ContainingFolderPath); } @@ -90,7 +90,7 @@ namespace MediaBrowser.Controller.Entities else { // Try to get the year from the folder name - if (!IsInMixedFolder) + if (!DetectIsInMixedFolder()) { info = LibraryManager.ParseName(System.IO.Path.GetFileName(ContainingFolderPath)); diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 1406a05ce..c64cdf57d 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -480,7 +480,7 @@ namespace MediaBrowser.Controller.Entities public override IEnumerable GetDeletePaths() { - if (!IsInMixedFolder) + if (!DetectIsInMixedFolder()) { return new[] { ContainingFolderPath }; } diff --git a/MediaBrowser.Controller/LiveTv/LiveStream.cs b/MediaBrowser.Controller/LiveTv/LiveStream.cs index 7d44fbd90..a5d432a54 100644 --- a/MediaBrowser.Controller/LiveTv/LiveStream.cs +++ b/MediaBrowser.Controller/LiveTv/LiveStream.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Controller.LiveTv public ITunerHost TunerHost { get; set; } public string OriginalStreamId { get; set; } public bool EnableStreamSharing { get; set; } + public string UniqueId = Guid.NewGuid().ToString("N"); public LiveStream(MediaSourceInfo mediaSource) { diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index e9d2054da..7c1114e22 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -287,9 +287,7 @@ - - diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs index 8fdb1ce37..ca453840f 100644 --- a/MediaBrowser.Controller/Net/IHttpResultFactory.cs +++ b/MediaBrowser.Controller/Net/IHttpResultFactory.cs @@ -11,14 +11,6 @@ namespace MediaBrowser.Controller.Net /// public interface IHttpResultFactory { - /// - /// Throws the error. - /// - /// The status code. - /// The error message. - /// The response headers. - void ThrowError(int statusCode, string errorMessage, IDictionary responseHeaders = null); - /// /// Gets the result. /// diff --git a/MediaBrowser.Controller/Providers/IImageFileSaver.cs b/MediaBrowser.Controller/Providers/IImageFileSaver.cs deleted file mode 100644 index 3e11d8bf8..000000000 --- a/MediaBrowser.Controller/Providers/IImageFileSaver.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public interface IImageFileSaver : IImageSaver - { - /// - /// Gets the save paths. - /// - /// The item. - /// The type. - /// The format. - /// The index. - /// IEnumerable{System.String}. - IEnumerable GetSavePaths(IHasImages item, ImageType type, ImageFormat format, int index); - } -} \ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/IImageSaver.cs b/MediaBrowser.Controller/Providers/IImageSaver.cs deleted file mode 100644 index 62017160f..000000000 --- a/MediaBrowser.Controller/Providers/IImageSaver.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MediaBrowser.Controller.Providers -{ - public interface IImageSaver - { - /// - /// Gets the name. - /// - /// The name. - string Name { get; } - } -} diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index 3eefa9647..d3e5685bb 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -95,15 +95,8 @@ namespace MediaBrowser.Controller.Providers /// /// Adds the metadata providers. /// - /// The image providers. - /// The metadata services. - /// The metadata providers. - /// The savers. - /// The image savers. - /// The external ids. void AddParts(IEnumerable imageProviders, IEnumerable metadataServices, IEnumerable metadataProviders, IEnumerable savers, - IEnumerable imageSavers, IEnumerable externalIds); /// diff --git a/MediaBrowser.Controller/Providers/ItemInfo.cs b/MediaBrowser.Controller/Providers/ItemInfo.cs index 63cc48058..8de11b743 100644 --- a/MediaBrowser.Controller/Providers/ItemInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemInfo.cs @@ -10,7 +10,7 @@ namespace MediaBrowser.Controller.Providers { Path = item.Path; ContainingFolderPath = item.ContainingFolderPath; - IsInMixedFolder = item.IsInMixedFolder; + IsInMixedFolder = item.DetectIsInMixedFolder(); var video = item as Video; if (video != null) diff --git a/MediaBrowser.Dlna/Eventing/EventManager.cs b/MediaBrowser.Dlna/Eventing/EventManager.cs index 68f012c3a..51c8d2d91 100644 --- a/MediaBrowser.Dlna/Eventing/EventManager.cs +++ b/MediaBrowser.Dlna/Eventing/EventManager.cs @@ -156,7 +156,6 @@ namespace MediaBrowser.Dlna.Eventing } catch (OperationCanceledException) { - throw; } catch { diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs index fe61a7a46..ef9160b70 100644 --- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -132,7 +132,7 @@ namespace MediaBrowser.LocalMetadata.Images } var imagePrefix = item.FileNameWithoutExtension + "-"; - var isInMixedFolder = item.IsInMixedFolder; + var isInMixedFolder = item.DetectIsInMixedFolder(); PopulatePrimaryImages(item, images, files, imagePrefix, isInMixedFolder); diff --git a/MediaBrowser.LocalMetadata/Savers/GameXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/GameXmlSaver.cs index a90789a3e..5592c068c 100644 --- a/MediaBrowser.LocalMetadata/Savers/GameXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/GameXmlSaver.cs @@ -99,7 +99,7 @@ namespace MediaBrowser.LocalMetadata.Savers public static string GetGameSavePath(Game item) { - if (item.IsInMixedFolder) + if (item.DetectIsInMixedFolder()) { return Path.ChangeExtension(item.Path, ".xml"); } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs index 33e90743a..5d0f1f075 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs @@ -76,7 +76,7 @@ namespace MediaBrowser.MediaEncoding.Encoder public static string GetProbeSizeArgument(bool isDvd) { - return isDvd ? "-probesize 1G -analyzeduration 200M" : " -analyzeduration 2M"; + return isDvd ? "-probesize 1G -analyzeduration 200M" : ""; } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 25ad14fe8..5c3345008 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -426,8 +426,10 @@ namespace MediaBrowser.MediaEncoding.Encoder var inputFiles = MediaEncoderHelpers.GetInputArgument(FileSystem, request.InputPath, request.Protocol, request.MountedIso, request.PlayableStreamFileNames); + var probeSizeArgument = GetProbeSizeArgument(inputFiles, request.Protocol); + return GetMediaInfoInternal(GetInputArgument(inputFiles, request.Protocol), request.InputPath, request.Protocol, extractChapters, - GetProbeSizeArgument(inputFiles, request.Protocol), request.MediaType == DlnaProfileType.Audio, request.VideoType, cancellationToken); + probeSizeArgument, request.MediaType == DlnaProfileType.Audio, request.VideoType, cancellationToken); } /// diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 1d2928f67..e7f8e6548 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -203,6 +203,7 @@ namespace MediaBrowser.Model.Configuration public string[] CodecsUsed { get; set; } public bool EnableChannelView { get; set; } public bool EnableExternalContentInSuggestions { get; set; } + public bool EnableSimpleArtistDetection { get; set; } public int ImageExtractionTimeoutMs { get; set; } /// diff --git a/MediaBrowser.Model/Notifications/NotificationType.cs b/MediaBrowser.Model/Notifications/NotificationType.cs index f5e3624f0..eefd15808 100644 --- a/MediaBrowser.Model/Notifications/NotificationType.cs +++ b/MediaBrowser.Model/Notifications/NotificationType.cs @@ -16,7 +16,6 @@ namespace MediaBrowser.Model.Notifications PluginUpdateInstalled, PluginUninstalled, NewLibraryContent, - NewLibraryContentMultiple, ServerRestartRequired, TaskFailed, CameraImageUploaded, diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index c9b3f22c5..5203adc9d 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -371,7 +371,7 @@ namespace MediaBrowser.Providers.Manager return Path.Combine(seriesFolder, imageFilename); } - if (item.IsInMixedFolder) + if (item.DetectIsInMixedFolder()) { return GetSavePathForItemInMixedFolder(item, type, "landscape", extension); } @@ -447,7 +447,7 @@ namespace MediaBrowser.Providers.Manager path = Path.Combine(Path.GetDirectoryName(item.Path), "metadata", filename + extension); } - else if (item.IsInMixedFolder) + else if (item.DetectIsInMixedFolder()) { path = GetSavePathForItemInMixedFolder(item, type, filename, extension); } @@ -514,7 +514,7 @@ namespace MediaBrowser.Providers.Manager if (imageIndex.Value == 0) { - if (item.IsInMixedFolder) + if (item.DetectIsInMixedFolder()) { return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart", extension) }; } @@ -540,7 +540,7 @@ namespace MediaBrowser.Providers.Manager var outputIndex = imageIndex.Value; - if (item.IsInMixedFolder) + if (item.DetectIsInMixedFolder()) { return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart" + outputIndex.ToString(UsCulture), extension) }; } @@ -583,7 +583,7 @@ namespace MediaBrowser.Providers.Manager return new[] { Path.Combine(seasonFolder, imageFilename) }; } - if (item.IsInMixedFolder || item is MusicVideo) + if (item.DetectIsInMixedFolder() || item is MusicVideo) { return new[] { GetSavePathForItemInMixedFolder(item, type, string.Empty, extension) }; } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 7e28254b0..ae1d60eb9 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -58,7 +58,6 @@ namespace MediaBrowser.Providers.Manager private IMetadataService[] _metadataServices = { }; private IMetadataProvider[] _metadataProviders = { }; private IEnumerable _savers; - private IImageSaver[] _imageSavers; private readonly IServerApplicationPaths _appPaths; private readonly IJsonSerializer _json; @@ -91,21 +90,14 @@ namespace MediaBrowser.Providers.Manager /// /// Adds the metadata providers. /// - /// The image providers. - /// The metadata services. - /// The metadata providers. - /// The metadata savers. - /// The image savers. - /// The external ids. public void AddParts(IEnumerable imageProviders, IEnumerable metadataServices, IEnumerable metadataProviders, IEnumerable metadataSavers, - IEnumerable imageSavers, IEnumerable externalIds) + IEnumerable externalIds) { ImageProviders = imageProviders.ToArray(); _metadataServices = metadataServices.OrderBy(i => i.Order).ToArray(); _metadataProviders = metadataProviders.ToArray(); - _imageSavers = imageSavers.ToArray(); _externalIds = externalIds.OrderBy(i => i.Name).ToArray(); _savers = metadataSavers.Where(i => diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs index ca4f1b956..2572a4f58 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs @@ -41,7 +41,7 @@ namespace MediaBrowser.Providers.TV private readonly ILibraryManager _libraryManager; private readonly IMemoryStreamProvider _memoryStreamProvider; - public TvdbSeriesProvider(IZipClient zipClient, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config, ILogger logger, ILibraryManager libraryManager) + public TvdbSeriesProvider(IZipClient zipClient, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config, ILogger logger, ILibraryManager libraryManager, IMemoryStreamProvider memoryStreamProvider) { _zipClient = zipClient; _httpClient = httpClient; @@ -49,6 +49,7 @@ namespace MediaBrowser.Providers.TV _config = config; _logger = logger; _libraryManager = libraryManager; + _memoryStreamProvider = memoryStreamProvider; Current = this; } diff --git a/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs b/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs index e84b66c5a..f7fe707da 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs @@ -377,10 +377,10 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications DisposeLibraryUpdateTimer(); } - if (items.Count == 1) - { - var item = items.First(); + items = items.Take(10).ToList(); + foreach (var item in items) + { var notification = new NotificationRequest { NotificationType = NotificationType.NewLibraryContent.ToString() @@ -388,17 +388,6 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications notification.Variables["Name"] = GetItemName(item); - await SendNotification(notification).ConfigureAwait(false); - } - else - { - var notification = new NotificationRequest - { - NotificationType = NotificationType.NewLibraryContentMultiple.ToString() - }; - - notification.Variables["ItemCount"] = items.Count.ToString(CultureInfo.InvariantCulture); - await SendNotification(notification).ConfigureAwait(false); } } diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs index 7dc6fbb25..2ebeb0d44 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -94,12 +94,12 @@ namespace MediaBrowser.Server.Implementations.HttpServer // The Markdown feature causes slow startup times (5 mins+) on cold boots for some users // Custom format allows images - HostConfig.Instance.EnableFeatures = Feature.Csv | Feature.Html | Feature.Json | Feature.Jsv | Feature.Metadata | Feature.Xml | Feature.CustomFormat; + HostConfig.Instance.EnableFeatures = Feature.Html | Feature.Json | Feature.CustomFormat; container.Adapter = _containerAdapter; Plugins.RemoveAll(x => x is NativeTypesFeature); - Plugins.Add(new SwaggerFeature()); + //Plugins.Add(new SwaggerFeature()); Plugins.Add(new CorsFeature(allowedHeaders: "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization")); //Plugins.Add(new AuthFeature(() => new AuthUserSession(), new IAuthProvider[] { @@ -546,8 +546,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer } } } - - throw new NotImplementedException("Cannot execute handler: " + handler + " at PathInfo: " + httpReq.PathInfo); + else + { + httpRes.Close(); + } } /// diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs index 04085d3c7..10d6f7493 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -683,29 +683,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer } } - /// - /// Gets the error result. - /// - /// The status code. - /// The error message. - /// The response headers. - /// System.Object. - public void ThrowError(int statusCode, string errorMessage, IDictionary responseHeaders = null) - { - var error = new HttpError - { - Status = statusCode, - ErrorCode = errorMessage - }; - - if (responseHeaders != null) - { - AddResponseHeaders(error, responseHeaders); - } - - throw error; - } - public object GetAsyncStreamWriter(IAsyncStreamSource streamSource) { return new AsyncStreamWriter(streamSource); diff --git a/MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs b/MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs deleted file mode 100644 index cac2f8e09..000000000 --- a/MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs +++ /dev/null @@ -1,240 +0,0 @@ -using MediaBrowser.Common.Events; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Logging; -using System; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using WebSocketMessageType = MediaBrowser.Model.Net.WebSocketMessageType; -using WebSocketState = MediaBrowser.Model.Net.WebSocketState; - -namespace MediaBrowser.Server.Implementations.HttpServer -{ - /// - /// Class NativeWebSocket - /// - public class NativeWebSocket : IWebSocket - { - /// - /// The logger - /// - private readonly ILogger _logger; - - public event EventHandler Closed; - - /// - /// Gets or sets the web socket. - /// - /// The web socket. - private System.Net.WebSockets.WebSocket WebSocket { get; set; } - - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - /// - /// Initializes a new instance of the class. - /// - /// The socket. - /// The logger. - /// socket - public NativeWebSocket(WebSocket socket, ILogger logger) - { - if (socket == null) - { - throw new ArgumentNullException("socket"); - } - - if (logger == null) - { - throw new ArgumentNullException("logger"); - } - - _logger = logger; - WebSocket = socket; - - Receive(); - } - - /// - /// Gets or sets the state. - /// - /// The state. - public WebSocketState State - { - get - { - WebSocketState commonState; - - if (!Enum.TryParse(WebSocket.State.ToString(), true, out commonState)) - { - _logger.Warn("Unrecognized WebSocketState: {0}", WebSocket.State.ToString()); - } - - return commonState; - } - } - - /// - /// Receives this instance. - /// - private async void Receive() - { - while (true) - { - byte[] bytes; - - try - { - bytes = await ReceiveBytesAsync(_cancellationTokenSource.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - break; - } - catch (WebSocketException ex) - { - _logger.ErrorException("Error receiving web socket message", ex); - - break; - } - - if (bytes == null) - { - // Connection closed - EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger); - break; - } - - if (OnReceiveBytes != null) - { - OnReceiveBytes(bytes); - } - } - } - - /// - /// Receives the async. - /// - /// The cancellation token. - /// Task{WebSocketMessageInfo}. - /// Connection closed - private async Task ReceiveBytesAsync(CancellationToken cancellationToken) - { - var bytes = new byte[4096]; - var buffer = new ArraySegment(bytes); - - var result = await WebSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); - - if (result.CloseStatus.HasValue) - { - _logger.Info("Web socket connection closed by client. Reason: {0}", result.CloseStatus.Value); - return null; - } - - return buffer.Array; - } - - /// - /// Sends the async. - /// - /// The bytes. - /// The type. - /// if set to true [end of message]. - /// The cancellation token. - /// Task. - public Task SendAsync(byte[] bytes, WebSocketMessageType type, bool endOfMessage, CancellationToken cancellationToken) - { - System.Net.WebSockets.WebSocketMessageType nativeType; - - if (!Enum.TryParse(type.ToString(), true, out nativeType)) - { - _logger.Warn("Unrecognized WebSocketMessageType: {0}", type.ToString()); - } - - var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token); - - return WebSocket.SendAsync(new ArraySegment(bytes), nativeType, true, linkedTokenSource.Token); - } - - public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken) - { - var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token); - - return WebSocket.SendAsync(new ArraySegment(bytes), System.Net.WebSockets.WebSocketMessageType.Binary, true, linkedTokenSource.Token); - } - - public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken) - { - var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token); - - var bytes = Encoding.UTF8.GetBytes(text); - - return WebSocket.SendAsync(new ArraySegment(bytes), System.Net.WebSockets.WebSocketMessageType.Text, true, linkedTokenSource.Token); - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - Dispose(true); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool dispose) - { - if (dispose) - { - _cancellationTokenSource.Cancel(); - - WebSocket.Dispose(); - } - } - - /// - /// Gets or sets the receive action. - /// - /// The receive action. - public Action OnReceiveBytes { get; set; } - - /// - /// Gets or sets the on receive. - /// - /// The on receive. - public Action OnReceive { get; set; } - - /// - /// The _supports native web socket - /// - private static bool? _supportsNativeWebSocket; - - /// - /// Gets a value indicating whether [supports web sockets]. - /// - /// true if [supports web sockets]; otherwise, false. - public static bool IsSupported - { - get - { - if (!_supportsNativeWebSocket.HasValue) - { - try - { - new ClientWebSocket(); - - _supportsNativeWebSocket = true; - } - catch (PlatformNotSupportedException) - { - _supportsNativeWebSocket = false; - } - } - - return _supportsNativeWebSocket.Value; - } - } - } -} diff --git a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs index 488c630fe..4b94095f5 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -191,15 +191,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer } } } - catch (IOException) - { - throw; - } - catch (Exception ex) - { - _logger.ErrorException("Error in range request writer", ex); - throw; - } finally { if (OnComplete != null) @@ -251,15 +242,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer } } } - catch (IOException ex) - { - throw; - } - catch (Exception ex) - { - _logger.ErrorException("Error in range request writer", ex); - throw; - } finally { if (OnComplete != null) diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs index e08be8bd1..a58645ec5 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs @@ -81,20 +81,12 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp public void Write(string text) { - try - { - var bOutput = System.Text.Encoding.UTF8.GetBytes(text); - response.ContentLength64 = bOutput.Length; + var bOutput = System.Text.Encoding.UTF8.GetBytes(text); + response.ContentLength64 = bOutput.Length; - var outputStream = response.OutputStream; - outputStream.Write(bOutput, 0, bOutput.Length); - Close(); - } - catch (Exception ex) - { - _logger.ErrorException("Could not WriteTextToResponse: " + ex.Message, ex); - throw; - } + var outputStream = response.OutputStream; + outputStream.Write(bOutput, 0, bOutput.Length); + Close(); } public void Close() diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index b2bddc70d..f7661f55b 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -2463,7 +2463,7 @@ namespace MediaBrowser.Server.Implementations.Library public IEnumerable