From dfb491fcc54cf6f32fb212d8493e83f541d05c89 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 26 Feb 2014 21:44:00 -0500 Subject: [PATCH] import remaining dlna classes --- MediaBrowser.Controller/Entities/User.cs | 6 +- MediaBrowser.Dlna/MediaBrowser.Dlna.csproj | 16 + .../PlayTo/Configuration/DlnaProfile.cs | 53 ++ .../Configuration/PluginConfiguration.cs | 119 +++++ .../PlayTo/Configuration/TranscodeSetting.cs | 76 +++ MediaBrowser.Dlna/PlayTo/Device.cs | 384 ++++++++------ MediaBrowser.Dlna/PlayTo/DidlBuilder.cs | 154 ++++++ MediaBrowser.Dlna/PlayTo/DlnaController.cs | 481 ++++++++++++++++++ .../PlayTo/DlnaControllerFactory.cs | 31 ++ MediaBrowser.Dlna/PlayTo/PlayToManager.cs | 271 ++++++++++ .../PlayTo/PlayToServerEntryPoint.cs | 69 +++ MediaBrowser.Dlna/PlayTo/PlaylistItem.cs | 125 ++--- MediaBrowser.Dlna/PlayTo/StreamHelper.cs | 188 +++++++ MediaBrowser.Dlna/PlayTo/TransportCommands.cs | 4 +- .../ApplicationHost.cs | 4 + 15 files changed, 1766 insertions(+), 215 deletions(-) create mode 100644 MediaBrowser.Dlna/PlayTo/Configuration/DlnaProfile.cs create mode 100644 MediaBrowser.Dlna/PlayTo/Configuration/PluginConfiguration.cs create mode 100644 MediaBrowser.Dlna/PlayTo/Configuration/TranscodeSetting.cs create mode 100644 MediaBrowser.Dlna/PlayTo/DidlBuilder.cs create mode 100644 MediaBrowser.Dlna/PlayTo/DlnaController.cs create mode 100644 MediaBrowser.Dlna/PlayTo/DlnaControllerFactory.cs create mode 100644 MediaBrowser.Dlna/PlayTo/PlayToManager.cs create mode 100644 MediaBrowser.Dlna/PlayTo/PlayToServerEntryPoint.cs create mode 100644 MediaBrowser.Dlna/PlayTo/StreamHelper.cs diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs index d5fc3172cf..e6a62c1812 100644 --- a/MediaBrowser.Controller/Entities/User.cs +++ b/MediaBrowser.Controller/Entities/User.cs @@ -226,11 +226,11 @@ namespace MediaBrowser.Controller.Entities /// /// Saves the current configuration to the file system /// - public void SaveConfiguration(IXmlSerializer serializer) + public void SaveConfiguration() { var xmlPath = ConfigurationFilePath; Directory.CreateDirectory(System.IO.Path.GetDirectoryName(xmlPath)); - serializer.SerializeToFile(Configuration, xmlPath); + XmlSerializer.SerializeToFile(Configuration, xmlPath); } /// @@ -247,7 +247,7 @@ namespace MediaBrowser.Controller.Entities } Configuration = config; - SaveConfiguration(serializer); + SaveConfiguration(); } } } diff --git a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj index 937c9b4c91..59420c5a1d 100644 --- a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj +++ b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj @@ -52,16 +52,28 @@ Properties\SharedVersion.cs + + + + + Code + + + + Code + + + @@ -77,6 +89,10 @@ {9142eefa-7570-41e1-bfcc-468bb571af2f} MediaBrowser.Common + + {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + MediaBrowser.Controller + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} MediaBrowser.Model diff --git a/MediaBrowser.Dlna/PlayTo/Configuration/DlnaProfile.cs b/MediaBrowser.Dlna/PlayTo/Configuration/DlnaProfile.cs new file mode 100644 index 0000000000..e75cea5a9d --- /dev/null +++ b/MediaBrowser.Dlna/PlayTo/Configuration/DlnaProfile.cs @@ -0,0 +1,53 @@ +namespace MediaBrowser.Dlna.PlayTo.Configuration +{ + public class DlnaProfile + { + /// + /// Gets or sets the name to be displayed. + /// + /// + /// The name. + /// + public string Name { get; set; } + + /// + /// Gets or sets the type of the client. + /// + /// + /// The type of the client. + /// + public string ClientType { get; set; } + + /// + /// Gets or sets the name of the friendly. + /// + /// + /// The name of the friendly. + /// + public string FriendlyName { get; set; } + + /// + /// Gets or sets the model number. + /// + /// + /// The model number. + /// + public string ModelNumber { get; set; } + + /// + /// Gets or sets the name of the model. + /// + /// + /// The name of the model. + /// + public string ModelName { get; set; } + + /// + /// Gets or sets the transcode settings. + /// + /// + /// The transcode settings. + /// + public TranscodeSettings[] TranscodeSettings { get; set; } + } +} diff --git a/MediaBrowser.Dlna/PlayTo/Configuration/PluginConfiguration.cs b/MediaBrowser.Dlna/PlayTo/Configuration/PluginConfiguration.cs new file mode 100644 index 0000000000..1bd8781bb2 --- /dev/null +++ b/MediaBrowser.Dlna/PlayTo/Configuration/PluginConfiguration.cs @@ -0,0 +1,119 @@ +namespace MediaBrowser.Dlna.PlayTo.Configuration +{ + public class PlayToConfiguration + { + private static readonly string[] _supportedStaticFormats = { "mp3", "flac", "m4a", "wma", "avi", "mp4", "mkv", "ts" }; + public static string[] SupportedStaticFormats + { + get + { + return _supportedStaticFormats; + } + } + + private static readonly DlnaProfile[] _profiles = GetDefaultProfiles(); + public static DlnaProfile[] Profiles + { + get + { + return _profiles; + } + } + + private static DlnaProfile[] GetDefaultProfiles() + { + var profile0 = new DlnaProfile + { + Name = "Samsung TV (B Series) [Profile]", + ClientType = "DLNA", + FriendlyName = "^TV$", + ModelNumber = @"1\.0", + ModelName = "Samsung DTV DMR", + TranscodeSettings = TranscodeSettings.GetDefaultTranscodingSettings() + }; + + var profile1 = new DlnaProfile + { + Name = "Samsung TV (E/F-series) [Profile]", + ClientType = "DLNA", + FriendlyName = @"(^\[TV\][A-Z]{2}\d{2}(E|F)[A-Z]?\d{3,4}.*)|^\[TV\] Samsung", + ModelNumber = @"(1\.0)|(AllShare1\.0)", + TranscodeSettings = TranscodeSettings.GetDefaultTranscodingSettings() + }; + + var profile2 = new DlnaProfile + { + Name = "Samsung TV (C/D-series) [Profile]", + ClientType = "DLNA", + FriendlyName = @"(^TV-\d{2}C\d{3}.*)|(^\[TV\][A-Z]{2}\d{2}(D)[A-Z]?\d{3,4}.*)|^\[TV\] Samsung", + ModelNumber = @"(1\.0)|(AllShare1\.0)", + TranscodeSettings = TranscodeSettings.GetDefaultTranscodingSettings() + }; + + var profile3 = new DlnaProfile + { + Name = "Xbox 360 [Profile]", + ClientType = "DLNA", + ModelName = "Xbox 360", + TranscodeSettings = new[] + { + new TranscodeSettings {Container = "mkv", TargetContainer = "ts"}, + new TranscodeSettings {Container = "flac", TargetContainer = "mp3"}, + new TranscodeSettings {Container = "m4a", TargetContainer = "mp3"} + } + }; + + var profile4 = new DlnaProfile + { + Name = "Xbox One [Profile]", + ModelName = "Xbox One", + ClientType = "DLNA", + FriendlyName = "Xbox-SystemOS", + TranscodeSettings = new[] + { + new TranscodeSettings {Container = "mkv", TargetContainer = "ts"}, + new TranscodeSettings {Container = "flac", TargetContainer = "mp3"}, + new TranscodeSettings {Container = "m4a", TargetContainer = "mp3"} + } + }; + + var profile5 = new DlnaProfile + { + Name = "Sony Bravia TV (2012)", + ClientType = "TV", + FriendlyName = @"BRAVIA KDL-\d{2}[A-Z]X\d5(\d|G).*", + TranscodeSettings = TranscodeSettings.GetDefaultTranscodingSettings() + }; + + //WDTV does not need any transcoding of the formats we support statically + var profile6 = new DlnaProfile + { + Name = "WDTV Live [Profile]", + ClientType = "DLNA", + ModelName = "WD TV HD Live", + TranscodeSettings = new TranscodeSettings[] { } + }; + + var profile7 = new DlnaProfile + { + //Linksys DMA2100us does not need any transcoding of the formats we support statically + Name = "Linksys DMA2100 [Profile]", + ClientType = "DLNA", + ModelName = "DMA2100us", + TranscodeSettings = new TranscodeSettings[] { } + }; + + return new[] + { + profile0, + profile1, + profile2, + profile3, + profile4, + profile5, + profile6, + profile7 + }; + } + } +} diff --git a/MediaBrowser.Dlna/PlayTo/Configuration/TranscodeSetting.cs b/MediaBrowser.Dlna/PlayTo/Configuration/TranscodeSetting.cs new file mode 100644 index 0000000000..f5cceaaaaa --- /dev/null +++ b/MediaBrowser.Dlna/PlayTo/Configuration/TranscodeSetting.cs @@ -0,0 +1,76 @@ +using System; + +namespace MediaBrowser.Dlna.PlayTo.Configuration +{ + public class TranscodeSettings + { + /// + /// Gets or sets the container. + /// + /// + /// The container. + /// + public string Container { get; set; } + + /// + /// Gets or sets the target container. + /// + /// + /// The target container. + /// + public string TargetContainer { get; set; } + + /// + /// The default transcoding settings + /// + private static readonly TranscodeSettings[] DefaultTranscodingSettings = + { + new TranscodeSettings { Container = "mkv", TargetContainer = "ts" }, + new TranscodeSettings { Container = "flac", TargetContainer = "mp3" }, + new TranscodeSettings { Container = "m4a", TargetContainer = "mp3" } + }; + + public static TranscodeSettings[] GetDefaultTranscodingSettings() + { + return DefaultTranscodingSettings; + } + + /// + /// Gets the profile settings. + /// + /// The device properties. + /// The TranscodeSettings for the device + public static TranscodeSettings[] GetProfileSettings(DeviceProperties deviceProperties) + { + foreach (var profile in PlayToConfiguration.Profiles) + { + if (!string.IsNullOrEmpty(profile.FriendlyName)) + { + if (!string.Equals(deviceProperties.Name, profile.FriendlyName, StringComparison.OrdinalIgnoreCase)) + continue; + } + + if (!string.IsNullOrEmpty(profile.ModelNumber)) + { + if (!string.Equals(deviceProperties.ModelNumber, profile.ModelNumber, StringComparison.OrdinalIgnoreCase)) + continue; + } + + if (!string.IsNullOrEmpty(profile.ModelName)) + { + if (!string.Equals(deviceProperties.ModelName, profile.ModelName, StringComparison.OrdinalIgnoreCase)) + continue; + } + + deviceProperties.DisplayName = profile.Name; + deviceProperties.ClientType = profile.ClientType; + return profile.TranscodeSettings; + + } + + // Since we don't have alot of info about different devices we go down the safe + // route abd use the default transcoding settings if no profile exist + return GetDefaultTranscodingSettings(); + } + } +} diff --git a/MediaBrowser.Dlna/PlayTo/Device.cs b/MediaBrowser.Dlna/PlayTo/Device.cs index 36e631c132..b6de29af69 100644 --- a/MediaBrowser.Dlna/PlayTo/Device.cs +++ b/MediaBrowser.Dlna/PlayTo/Device.cs @@ -1,12 +1,11 @@ using MediaBrowser.Common.Net; using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; using System.Timers; using System.Xml.Linq; +using MediaBrowser.Model.Logging; namespace MediaBrowser.Dlna.PlayTo { @@ -122,12 +121,15 @@ namespace MediaBrowser.Dlna.PlayTo #endregion private readonly IHttpClient _httpClient; - + private readonly ILogger _logger; + #region Constructor & Initializer - public Device(DeviceProperties deviceProperties) + public Device(DeviceProperties deviceProperties, IHttpClient httpClient, ILogger logger) { Properties = deviceProperties; + _httpClient = httpClient; + _logger = logger; } internal void Start() @@ -182,9 +184,15 @@ namespace MediaBrowser.Dlna.PlayTo if (command == null) return true; - var service = this.Properties.Services.FirstOrDefault(s => s.ServiceId == ServiceRenderingId); + var service = Properties.Services.FirstOrDefault(s => s.ServiceId == ServiceRenderingId); - var result = await SsdpHttpClient.SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, value)); + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, value)) + .ConfigureAwait(false); Volume = value; return true; } @@ -197,7 +205,14 @@ namespace MediaBrowser.Dlna.PlayTo var service = Properties.Services.FirstOrDefault(s => s.ServiceId == ServiceAvtransportId); - var result = await SsdpHttpClient.SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, String.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME")); + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, String.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME")) + .ConfigureAwait(false); + return value; } @@ -206,7 +221,9 @@ namespace MediaBrowser.Dlna.PlayTo _dt.Stop(); TransportState = "STOPPED"; CurrentId = "0"; - await Task.Delay(50); + + await Task.Delay(50).ConfigureAwait(false); + var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI"); if (command == null) return false; @@ -218,12 +235,21 @@ namespace MediaBrowser.Dlna.PlayTo }; var service = Properties.Services.FirstOrDefault(s => s.ServiceId == ServiceAvtransportId); - var result = await SsdpHttpClient.SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, url, dictionary), header); + + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, url, dictionary), header) + .ConfigureAwait(false); + if (!IsPlaying) { - await Task.Delay(50); - await SetPlay(); + await Task.Delay(50).ConfigureAwait(false); + await SetPlay().ConfigureAwait(false); } + _count = 5; _dt.Start(); return true; @@ -252,8 +278,17 @@ namespace MediaBrowser.Dlna.PlayTo dictionary.Add("NextURIMetaData", CreateDidlMeta(metaData)); var service = Properties.Services.FirstOrDefault(s => s.ServiceId == ServiceAvtransportId); - var result = await SsdpHttpClient.SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, value, dictionary), header); - await Task.Delay(100); + + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, value, dictionary), header) + .ConfigureAwait(false); + + await Task.Delay(100).ConfigureAwait(false); + return true; } @@ -265,7 +300,14 @@ namespace MediaBrowser.Dlna.PlayTo var service = Properties.Services.FirstOrDefault(s => s.ServiceId == ServiceAvtransportId); - var result = await SsdpHttpClient.SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, 1)); + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, 1)) + .ConfigureAwait(false); + _count = 5; return true; } @@ -278,8 +320,10 @@ namespace MediaBrowser.Dlna.PlayTo var service = Properties.Services.FirstOrDefault(s => s.ServiceId == ServiceAvtransportId); - var result = await SsdpHttpClient.SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, 1)); - await Task.Delay(50); + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, 1)) + .ConfigureAwait(false); + + await Task.Delay(50).ConfigureAwait(false); _count = 4; return true; } @@ -292,8 +336,10 @@ namespace MediaBrowser.Dlna.PlayTo var service = Properties.Services.FirstOrDefault(s => s.ServiceId == ServiceAvtransportId); - var result = await SsdpHttpClient.SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, 0)); - await Task.Delay(50); + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, 0)) + .ConfigureAwait(false); + + await Task.Delay(50).ConfigureAwait(false); TransportState = "PAUSED_PLAYBACK"; return true; } @@ -302,23 +348,26 @@ namespace MediaBrowser.Dlna.PlayTo #region Get data + // TODO: What is going on here int _count = 5; + async void dt_Elapsed(object sender, ElapsedEventArgs e) { if (_disposed) return; ((Timer)sender).Stop(); - var hasTrack = await GetPositionInfo(); + var hasTrack = await GetPositionInfo().ConfigureAwait(false); + + // TODO: Why make these requests if hasTrack==false? if (_count > 4) { - - await GetTransportInfo(); + await GetTransportInfo().ConfigureAwait(false); if (!hasTrack) { - await GetMediaInfo(); + await GetMediaInfo().ConfigureAwait(false); } - await GetVolume(); + await GetVolume().ConfigureAwait(false); _count = 0; } @@ -335,23 +384,41 @@ namespace MediaBrowser.Dlna.PlayTo return; var service = Properties.Services.FirstOrDefault(s => s.ServiceId == ServiceRenderingId); + + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + XDocument result; + try { - var result = await SsdpHttpClient.SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType)); - if (result == null) - return; - var volume = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetVolumeResponse").FirstOrDefault().Element("CurrentVolume").Value; - if (volume == null) - return; - Volume = Int32.Parse(volume); - - //Reset the Mute value if Volume is bigger than zero - if (Volume > 0 && _muteVol > 0) - { - _muteVol = 0; - } + result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType)) + .ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting volume info", ex); + return; + } + + if (result == null || result.Document == null) + return; + + var volume = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null); + var volumeValue = volume == null ? null : volume.Value; + + if (volumeValue == null) + return; + + Volume = Int32.Parse(volumeValue); + + //Reset the Mute value if Volume is bigger than zero + if (Volume > 0 && _muteVol > 0) + { + _muteVol = 0; } - catch { } } private async Task GetTransportInfo() @@ -360,21 +427,35 @@ namespace MediaBrowser.Dlna.PlayTo if (command == null) return; - var service = this.Properties.Services.FirstOrDefault(s => s.ServiceId == ServiceAvtransportId); + var service = Properties.Services.FirstOrDefault(s => s.ServiceId == ServiceAvtransportId); if (service == null) return; - var result = await SsdpHttpClient.SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType)); + XDocument result; + try { - var transportState = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetTransportInfoResponse").FirstOrDefault().Element("CurrentTransportState").Value; - if (transportState != null) - TransportState = transportState; + result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType)) + .ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting transport info", ex); + return; } - catch { } - if (result != null) - UpdateTime = DateTime.UtcNow; + if (result == null || result.Document == null) + return; + + var transportState = + result.Document.Descendants(uPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null); + + var transportStateValue = transportState == null ? null : transportState.Value; + + if (transportStateValue != null) + TransportState = transportStateValue; + + UpdateTime = DateTime.UtcNow; } private async Task GetMediaInfo() @@ -385,28 +466,47 @@ namespace MediaBrowser.Dlna.PlayTo var service = Properties.Services.FirstOrDefault(s => s.ServiceId == ServiceAvtransportId); - var result = await SsdpHttpClient.SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType)); + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + XDocument result; + try { - var track = result.Document.Descendants("CurrentURIMetaData").FirstOrDefault().Value; - if (String.IsNullOrEmpty(track)) - { - CurrentId = "0"; - return; - } - XElement uPnpResponse = XElement.Parse((String)track); - - var e = uPnpResponse.Element(uPnpNamespaces.items); - - if (e == null) - e = uPnpResponse; - - var uTrack = uParser.CreateObjectFromXML(new uParserObject { Type = e.Element(uPnpNamespaces.uClass).Value, Element = e }); - if (uTrack != null) - CurrentId = uTrack.Id; - + result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType)) + .ConfigureAwait(false); } - catch { } + catch (Exception ex) + { + _logger.ErrorException("Error getting media info", ex); + return; + } + + if (result == null || result.Document == null) + return; + + var track = result.Document.Descendants("CurrentURIMetaData").Select(i => i.Value).FirstOrDefault(); + + if (String.IsNullOrEmpty(track)) + { + CurrentId = "0"; + return; + } + + var uPnpResponse = XElement.Parse(track); + + var e = uPnpResponse.Element(uPnpNamespaces.items) ?? uPnpResponse; + + var uTrack = uParser.CreateObjectFromXML(new uParserObject + { + Type = e.GetValue(uPnpNamespaces.uClass), + Element = e + }); + + if (uTrack != null) + CurrentId = uTrack.Id; } private async Task GetPositionInfo() @@ -417,78 +517,89 @@ namespace MediaBrowser.Dlna.PlayTo var service = Properties.Services.FirstOrDefault(s => s.ServiceId == ServiceAvtransportId); - var result = await SsdpHttpClient.SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType)); - + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + XDocument result; + try { - var duration = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").FirstOrDefault().Element("TrackDuration").Value; - - if (duration != null) - { - Duration = TimeSpan.Parse(duration); - } - - var position = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").FirstOrDefault().Element("RelTime").Value; - - if (position != null) - { - Position = TimeSpan.Parse(position); - } - - var track = result.Document.Descendants("TrackMetaData").Select(i => i.Value) - .FirstOrDefault(); - - if (String.IsNullOrEmpty(track)) - { - //If track is null, some vendors do this, use GetMediaInfo instead - return false; - } - - var uPnpResponse = XElement.Parse(track); - - var e = uPnpResponse.Element(uPnpNamespaces.items) ?? uPnpResponse; - - var uTrack = uBaseObject.Create(e); - - if (uTrack == null) - return true; - - CurrentId = uTrack.Id; - - return true; + result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType)) + .ConfigureAwait(false); } - catch { return false; } + catch (Exception ex) + { + _logger.ErrorException("Error getting position info", ex); + return false; + } + + if (result == null || result.Document == null) + return true; + + var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null); + var duration = durationElem == null ? null : durationElem.Value; + + if (duration != null) + { + Duration = TimeSpan.Parse(duration); + } + + var positionElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null); + var position = positionElem == null ? null : positionElem.Value; + + if (position != null) + { + Position = TimeSpan.Parse(position); + } + + var track = result.Document.Descendants("TrackMetaData").Select(i => i.Value) + .FirstOrDefault(); + + if (String.IsNullOrEmpty(track)) + { + //If track is null, some vendors do this, use GetMediaInfo instead + return false; + } + + var uPnpResponse = XElement.Parse(track); + + var e = uPnpResponse.Element(uPnpNamespaces.items) ?? uPnpResponse; + + var uTrack = uBaseObject.Create(e); + + if (uTrack == null) + return true; + + CurrentId = uTrack.Id; + + return true; } #endregion #region From XML - internal async Task GetAVProtocolAsync() + private async Task GetAVProtocolAsync() { var avService = Properties.Services.FirstOrDefault(s => s.ServiceId == ServiceAvtransportId); if (avService == null) return; - string url = avService.SCPDURL; + var url = avService.SCPDURL; if (!url.Contains("/")) url = "/dmr/" + url; if (!url.StartsWith("/")) url = "/" + url; - var httpClient = new SsdpHttpClient(); - var stream = await httpClient.GetDataAsync(new Uri(Properties.BaseUrl + url)); - - if (stream == null) - return; - - XDocument document = httpClient.ParseStream(stream); - stream.Dispose(); + var httpClient = new SsdpHttpClient(_httpClient); + var document = await httpClient.GetDataAsync(new Uri(Properties.BaseUrl + url)); AvCommands = TransportCommands.Create(document); } - internal async Task GetRenderingProtocolAsync() + private async Task GetRenderingProtocolAsync() { var avService = Properties.Services.FirstOrDefault(s => s.ServiceId == ServiceRenderingId); @@ -500,14 +611,8 @@ namespace MediaBrowser.Dlna.PlayTo if (!url.StartsWith("/")) url = "/" + url; - var httpClient = new SsdpHttpClient(); - var stream = await httpClient.GetDataAsync(new Uri(Properties.BaseUrl + url)); - - if (stream == null) - return; - - XDocument document = httpClient.ParseStream(stream); - stream.Dispose(); + var httpClient = new SsdpHttpClient(_httpClient); + var document = await httpClient.GetDataAsync(new Uri(Properties.BaseUrl + url)); RendererCommands = TransportCommands.Create(document); } @@ -524,16 +629,11 @@ namespace MediaBrowser.Dlna.PlayTo set; } - public static async Task CreateuPnpDeviceAsync(Uri url) + public static async Task CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, ILogger logger) { - var httpClient = new SsdpHttpClient(); - var stream = await httpClient.GetDataAsync(url); + var ssdpHttpClient = new SsdpHttpClient(httpClient); - if (stream == null) - return null; - - var document = httpClient.ParseStream(stream); - stream.Dispose(); + var document = await ssdpHttpClient.GetDataAsync(url).ConfigureAwait(false); var deviceProperties = new DeviceProperties(); @@ -587,14 +687,14 @@ namespace MediaBrowser.Dlna.PlayTo return null; var servicesList = services.Descendants(uPnpNamespaces.ud.GetName("service")); - + if (servicesList == null) return null; foreach (var element in servicesList) { var service = uService.Create(element); - + if (service != null) { deviceProperties.Services.Add(service); @@ -609,10 +709,11 @@ namespace MediaBrowser.Dlna.PlayTo if (isRenderer) { - var device = new Device(deviceProperties); + var device = new Device(deviceProperties, httpClient, logger); + + await device.GetRenderingProtocolAsync().ConfigureAwait(false); + await device.GetAVProtocolAsync().ConfigureAwait(false); - await device.GetRenderingProtocolAsync(); - await device.GetAVProtocolAsync(); return device; } @@ -663,20 +764,5 @@ namespace MediaBrowser.Dlna.PlayTo { return String.Format("{0} - {1}", Properties.Name, Properties.BaseUrl); } - - private XDocument ParseStream(Stream stream) - { - var reader = new StreamReader(stream, Encoding.UTF8); - try - { - var doc = XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace); - stream.Dispose(); - return doc; - } - catch - { - } - return null; - } } } diff --git a/MediaBrowser.Dlna/PlayTo/DidlBuilder.cs b/MediaBrowser.Dlna/PlayTo/DidlBuilder.cs new file mode 100644 index 0000000000..04f9a4644c --- /dev/null +++ b/MediaBrowser.Dlna/PlayTo/DidlBuilder.cs @@ -0,0 +1,154 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Dlna.PlayTo +{ + internal class DidlBuilder + { + #region Constants + + internal const string CRLF = "\r\n"; + internal const string UNKNOWN = "Unknown"; + + internal const string DIDL_START = @"" + CRLF; + internal const string DIDL_TITLE = @" {0}" + CRLF; + internal const string DIDL_ARTIST = @"{0}" + CRLF; + internal const string DIDL_ALBUM = @"{0}" + CRLF; + internal const string DIDL_TRACKNUM = @"0" + CRLF; + internal const string DIDL_VIDEOCLASS = @" object.item.videoItem" + CRLF; + internal const string DIDL_AUDIOCLASS = @" object.item.audioItem.musicTrack" + CRLF; + internal const string DIDL_IMAGE = @" {0}" + CRLF + + @" {0}" + CRLF; + internal const string DIDL_RELEASEDATE = @" {0}" + CRLF; + internal const string DIDL_GENRE = @" {0}" + CRLF; + internal const string DESCRIPTION = @" {0}" + CRLF; + internal const string DIDL_VIDEO_RES = @" {4}" + CRLF; + internal const string DIDL_AUDIO_RES = @" {3}" + CRLF; + internal const string DIDL_IMAGE_RES = @" {0}" + CRLF; + internal const string DIDL_ALBUMIMAGE_RES = @" {0}" + CRLF; + internal const string DIDL_RATING = @" {0}" + CRLF; + internal const string DIDL_END = ""; + + #endregion + + /// + /// Builds a Didl MetaData object for the specified dto. + /// + /// The dto. + /// The user identifier. + /// The server address. + /// The stream URL. + /// The streams. + /// System.String. + internal static string Build(BaseItem dto, string userId, string serverAddress, string streamUrl, IEnumerable streams) + { + string response = string.Format(DIDL_START, dto.Id, userId); + response += string.Format(DIDL_TITLE, dto.Name.Replace("&", "and")); + if (IsVideo(dto)) + response += DIDL_VIDEOCLASS; + else + response += DIDL_AUDIOCLASS; + + response += string.Format(DIDL_IMAGE, GetImageUrl(dto, serverAddress)); + response += string.Format(DIDL_RELEASEDATE, GetDateString(dto.PremiereDate)); + + //TODO Add genres to didl; + response += string.Format(DIDL_GENRE, UNKNOWN); + + if (IsVideo(dto)) + { + response += string.Format(DESCRIPTION, UNKNOWN); + response += GetVideoDIDL(dto, streamUrl, streams); + response += string.Format(DIDL_IMAGE_RES, GetImageUrl(dto, serverAddress)); + } + else + { + var audio = dto as Audio; + + if (audio != null) + { + response += string.Format(DIDL_ARTIST, audio.Artists.FirstOrDefault() ?? UNKNOWN); + response += string.Format(DIDL_ALBUM, audio.Album); + + // TODO: Bad format string? + response += string.Format(DIDL_TRACKNUM, audio.IndexNumber ?? 0); + } + + response += GetAudioDIDL(dto, streamUrl, streams); + response += string.Format(DIDL_ALBUMIMAGE_RES, GetImageUrl(dto, serverAddress)); + } + + response += DIDL_END; + + return response; + + } + + #region Private methods + + private static string GetVideoDIDL(BaseItem dto, string streamUrl, IEnumerable streams) + { + var videostream = streams.Where(stream => stream.Type == Model.Entities.MediaStreamType.Video).OrderBy(s => s.IsDefault).FirstOrDefault(); + + if (videostream == null) + { + // TOOD: ??? + return string.Empty; + } + + return string.Format(DIDL_VIDEO_RES, videostream.BitRate.HasValue ? videostream.BitRate.Value / 10 : 0, GetDurationString(dto), videostream.Width ?? 0, videostream.Height ?? 0, streamUrl); + } + + private static string GetAudioDIDL(BaseItem dto, string streamUrl, IEnumerable streams) + { + var audiostream = streams.Where(stream => stream.Type == MediaStreamType.Audio).OrderBy(s => s.IsDefault).FirstOrDefault(); + + if (audiostream == null) + { + // TOOD: ??? + return string.Empty; + } + + return string.Format(DIDL_AUDIO_RES, audiostream.BitRate.HasValue ? audiostream.BitRate.Value / 10 : 16000, GetDurationString(dto), audiostream.SampleRate ?? 0, streamUrl); + } + + private static string GetImageUrl(BaseItem dto, string serverAddress) + { + var imageType = ImageType.Primary; + + if (!dto.HasImage(ImageType.Primary)) + { + dto = dto.Parents.FirstOrDefault(i => i.HasImage(ImageType.Primary)); + } + + return string.Format("{0}/Items/{1}/Images/{2}", serverAddress, dto.Id, imageType); + } + + private static string GetDurationString(BaseItem dto) + { + var duration = TimeSpan.FromTicks(dto.RunTimeTicks.HasValue ? dto.RunTimeTicks.Value : 0); + + // TODO: Bad format string? + return string.Format("{0}:{1:00}:2{00}.000", duration.Hours, duration.Minutes, duration.Seconds); + } + + private static string GetDateString(DateTime? date) + { + if (!date.HasValue) + return UNKNOWN; + + return string.Format("{0}-{1:00}-{2:00}", date.Value.Year, date.Value.Month, date.Value.Day); + } + + private static bool IsVideo(BaseItem item) + { + return string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase); + } + + #endregion + } +} diff --git a/MediaBrowser.Dlna/PlayTo/DlnaController.cs b/MediaBrowser.Dlna/PlayTo/DlnaController.cs new file mode 100644 index 0000000000..894e32599d --- /dev/null +++ b/MediaBrowser.Dlna/PlayTo/DlnaController.cs @@ -0,0 +1,481 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Session; +using MediaBrowser.Dlna.PlayTo.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Session; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Timers; +using Timer = System.Timers.Timer; + +namespace MediaBrowser.Dlna.PlayTo +{ + public class PlayToController : ISessionController + { + private Device _device; + private BaseItem _currentItem = null; + private TranscodeSettings[] _transcodeSettings; + private readonly SessionInfo _session; + private readonly ISessionManager _sessionManager; + private readonly IItemRepository _itemRepository; + private readonly ILibraryManager _libraryManager; + private readonly INetworkManager _networkManager; + private readonly ILogger _logger; + private bool _playbackStarted = false; + + public bool SupportsMediaRemoteControl + { + get { return true; } + } + + public bool IsSessionActive + { + get + { + if (_device == null || _device.UpdateTime == default(DateTime)) + return false; + + return DateTime.UtcNow <= _device.UpdateTime.AddSeconds(30); + } + } + + public PlayToController(SessionInfo session, ISessionManager sessionManager, IItemRepository itemRepository, ILibraryManager libraryManager, ILogger logger, INetworkManager networkManager) + { + _session = session; + _itemRepository = itemRepository; + _sessionManager = sessionManager; + _libraryManager = libraryManager; + _networkManager = networkManager; + _logger = logger; + } + + public void Init(Device device, TranscodeSettings[] transcodeSettings) + { + _transcodeSettings = transcodeSettings; + _device = device; + _device.PlaybackChanged += Device_PlaybackChanged; + _device.CurrentIdChanged += Device_CurrentIdChanged; + _device.Start(); + + _updateTimer = new Timer(1000); + _updateTimer.Elapsed += updateTimer_Elapsed; + _updateTimer.Start(); + } + + #region Device EventHandlers & Update Timer + + Timer _updateTimer; + + async void Device_PlaybackChanged(object sender, TransportStateEventArgs e) + { + if (_currentItem == null) + return; + + if (e.Stopped == false) + await ReportProgress().ConfigureAwait(false); + + else if (e.Stopped && _playbackStarted) + { + _playbackStarted = false; + + await _sessionManager.OnPlaybackStopped(new PlaybackStopInfo + { + Item = _currentItem, + SessionId = _session.Id, + PositionTicks = _device.Position.Ticks + + }).ConfigureAwait(false); + + await SetNext().ConfigureAwait(false); + } + } + + async void Device_CurrentIdChanged(object sender, CurrentIdEventArgs e) + { + if (e.Id != Guid.Empty) + { + if (_currentItem != null && _currentItem.Id == e.Id) + { + return; + } + + var item = _libraryManager.GetItemById(e.Id); + + if (item != null) + { + _logger.Debug("{0} - CurrentId {1}", _session.DeviceName, item.Id); + _currentItem = item; + _playbackStarted = false; + + await ReportProgress().ConfigureAwait(false); + } + } + } + + /// + /// Handles the Elapsed event of the updateTimer control. + /// + /// The source of the event. + /// The instance containing the event data. + async void updateTimer_Elapsed(object sender, ElapsedEventArgs e) + { + if (_disposed) + return; + + ((Timer)sender).Stop(); + + await ReportProgress().ConfigureAwait(false); + + if (!_disposed && IsSessionActive) + ((Timer)sender).Start(); + } + + /// + /// Reports the playback progress. + /// + /// + private async Task ReportProgress() + { + if (_currentItem == null || _device.IsStopped) + return; + + if (!_playbackStarted) + { + await _sessionManager.OnPlaybackStart(new PlaybackInfo { Item = _currentItem, SessionId = _session.Id, CanSeek = true, QueueableMediaTypes = new List { "Audio", "Video" } }).ConfigureAwait(false); + _playbackStarted = true; + } + + if ((_device.IsPlaying || _device.IsPaused)) + { + var playlistItem = Playlist.FirstOrDefault(p => p.PlayState == 1); + if (playlistItem != null && playlistItem.Transcode) + { + await _sessionManager.OnPlaybackProgress(new PlaybackProgressInfo + { + Item = _currentItem, + SessionId = _session.Id, + PositionTicks = _device.Position.Ticks + playlistItem.StartPositionTicks, + IsMuted = _device.IsMuted, + IsPaused = _device.IsPaused + + }).ConfigureAwait(false); + } + else if (_currentItem != null) + { + await _sessionManager.OnPlaybackProgress(new PlaybackProgressInfo + { + Item = _currentItem, + SessionId = _session.Id, + PositionTicks = _device.Position.Ticks, + IsMuted = _device.IsMuted, + IsPaused = _device.IsPaused + + }).ConfigureAwait(false); + } + } + } + + #endregion + + #region SendCommands + + public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken) + { + _logger.Debug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand); + + var items = new List(); + foreach (string id in command.ItemIds) + { + AddItemFromId(Guid.Parse(id), items); + } + + var playlist = new List(); + var isFirst = true; + + var serverAddress = GetServerAddress(); + + foreach (var item in items) + { + if (isFirst && command.StartPositionTicks.HasValue) + { + playlist.Add(CreatePlaylistItem(item, command.StartPositionTicks.Value, serverAddress)); + isFirst = false; + } + else + { + playlist.Add(CreatePlaylistItem(item, 0, serverAddress)); + } + } + + _logger.Debug("{0} - Playlist created", _session.DeviceName); + + if (command.PlayCommand == PlayCommand.PlayLast) + { + AddItemsToPlaylist(playlist); + return Task.FromResult(true); + } + if (command.PlayCommand == PlayCommand.PlayNext) + { + AddItemsToPlaylist(playlist); + return Task.FromResult(true); + } + + _logger.Debug("{0} - Playing {1} items", _session.DeviceName, playlist.Count); + return PlayItems(playlist); + } + + public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken) + { + switch (command.Command) + { + case PlaystateCommand.Stop: + Playlist.Clear(); + return _device.SetStop(); + + case PlaystateCommand.Pause: + return _device.SetPause(); + + case PlaystateCommand.Unpause: + return _device.SetPlay(); + + case PlaystateCommand.Seek: + var playlistItem = Playlist.FirstOrDefault(p => p.PlayState == 1); + if (playlistItem != null && playlistItem.Transcode && playlistItem.IsVideo && _currentItem != null) + { + var newItem = CreatePlaylistItem(_currentItem, command.SeekPositionTicks ?? 0, GetServerAddress()); + playlistItem.StartPositionTicks = newItem.StartPositionTicks; + playlistItem.StreamUrl = newItem.StreamUrl; + playlistItem.Didl = newItem.Didl; + return _device.SetAvTransport(playlistItem.StreamUrl, playlistItem.DlnaHeaders, playlistItem.Didl); + + } + return _device.Seek(TimeSpan.FromTicks(command.SeekPositionTicks ?? 0)); + + + case PlaystateCommand.NextTrack: + _currentItem = null; + return SetNext(); + + case PlaystateCommand.PreviousTrack: + _currentItem = null; + return SetPrevious(); + } + + return Task.FromResult(true); + } + + public Task SendSystemCommand(SystemCommand command, CancellationToken cancellationToken) + { + switch (command) + { + case SystemCommand.VolumeDown: + return _device.VolumeDown(); + case SystemCommand.VolumeUp: + return _device.VolumeUp(); + case SystemCommand.Mute: + return _device.VolumeDown(true); + case SystemCommand.Unmute: + return _device.VolumeUp(true); + case SystemCommand.ToggleMute: + return _device.ToggleMute(); + default: + return Task.FromResult(true); + } + } + + public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendRestartRequiredNotification(CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendServerRestartNotification(CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendServerShutdownNotification(CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendBrowseCommand(BrowseRequest command, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendLibraryUpdateInfo(LibraryUpdateInfo info, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendMessageCommand(MessageCommand command, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + #endregion + + #region Playlist + + private List _playlist = new List(); + + private List Playlist + { + get + { + return _playlist; + } + set + { + _playlist = value; + } + } + + private void AddItemFromId(Guid id, List list) + { + var item = _libraryManager.GetItemById(id); + if (item.IsFolder) + { + foreach (var childId in _itemRepository.GetChildren(item.Id)) + { + AddItemFromId(childId, list); + } + } + else + { + if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video) + { + list.Add(item); + } + } + } + + private string GetServerAddress() + { + return string.Format("{0}://{1}:{2}/mediabrowser", + + "http", + _networkManager.GetLocalIpAddresses().FirstOrDefault() ?? "localhost", + "8096" + ); + } + + private PlaylistItem CreatePlaylistItem(BaseItem item, long startPostionTicks, string serverAddress) + { + var streams = _itemRepository.GetMediaStreams(new MediaStreamQuery { ItemId = item.Id }).ToList(); + + var playlistItem = PlaylistItem.GetBasicConfig(item, _transcodeSettings); + playlistItem.StartPositionTicks = startPostionTicks; + + if (playlistItem.IsAudio) + playlistItem.StreamUrl = StreamHelper.GetAudioUrl(playlistItem, serverAddress); + else + { + playlistItem.StreamUrl = StreamHelper.GetVideoUrl(_device.Properties, playlistItem, streams, serverAddress); + } + + var didl = DidlBuilder.Build(item, _session.UserId.ToString(), serverAddress, playlistItem.StreamUrl, streams); + playlistItem.Didl = didl; + + var header = StreamHelper.GetDlnaHeaders(playlistItem); + playlistItem.DlnaHeaders = header; + return playlistItem; + } + + /// + /// Plays the items. + /// + /// The items. + /// + private async Task PlayItems(IEnumerable items) + { + Playlist.Clear(); + Playlist.AddRange(items); + await SetNext(); + return true; + } + + /// + /// Adds the items to playlist. + /// + /// The items. + private void AddItemsToPlaylist(IEnumerable items) + { + Playlist.AddRange(items); + } + + private async Task SetNext() + { + if (!Playlist.Any() || Playlist.All(i => i.PlayState != 0)) + { + return true; + } + var currentitem = Playlist.FirstOrDefault(i => i.PlayState == 1); + + if (currentitem != null) + { + currentitem.PlayState = 2; + } + + var nextTrack = Playlist.FirstOrDefault(i => i.PlayState == 0); + if (nextTrack == null) + { + await _device.SetStop(); + return true; + } + nextTrack.PlayState = 1; + await _device.SetAvTransport(nextTrack.StreamUrl, nextTrack.DlnaHeaders, nextTrack.Didl); + if (nextTrack.StartPositionTicks > 0 && !nextTrack.Transcode) + await _device.Seek(TimeSpan.FromTicks(nextTrack.StartPositionTicks)); + return true; + } + + public Task SetPrevious() + { + if (!Playlist.Any() || Playlist.All(i => i.PlayState != 2)) + return Task.FromResult(false); + + var currentitem = Playlist.FirstOrDefault(i => i.PlayState == 1); + + var prevTrack = Playlist.LastOrDefault(i => i.PlayState == 2); + + if (currentitem != null) + { + currentitem.PlayState = 0; + } + + if (prevTrack == null) + return Task.FromResult(false); + + prevTrack.PlayState = 1; + return _device.SetAvTransport(prevTrack.StreamUrl, prevTrack.DlnaHeaders, prevTrack.Didl); + } + + #endregion + + private bool _disposed; + + public void Dispose() + { + if (!_disposed) + { + _updateTimer.Stop(); + _disposed = true; + _device.Dispose(); + _logger.Log(LogSeverity.Debug, "PlayTo - Controller disposed"); + } + } + } +} diff --git a/MediaBrowser.Dlna/PlayTo/DlnaControllerFactory.cs b/MediaBrowser.Dlna/PlayTo/DlnaControllerFactory.cs new file mode 100644 index 0000000000..720dc200b4 --- /dev/null +++ b/MediaBrowser.Dlna/PlayTo/DlnaControllerFactory.cs @@ -0,0 +1,31 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Dlna.PlayTo +{ + public class PlayToControllerFactory : ISessionControllerFactory + { + private readonly ISessionManager _sessionManager; + private readonly IItemRepository _itemRepository; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly INetworkManager _networkManager; + + public PlayToControllerFactory(ISessionManager sessionManager, IItemRepository itemRepository, ILibraryManager libraryManager, ILogManager logManager, INetworkManager networkManager) + { + _itemRepository = itemRepository; + _sessionManager = sessionManager; + _libraryManager = libraryManager; + _networkManager = networkManager; + _logger = logManager.GetLogger("PlayTo"); + } + + public ISessionController GetSessionController(SessionInfo session) + { + return null; + } + } +} diff --git a/MediaBrowser.Dlna/PlayTo/PlayToManager.cs b/MediaBrowser.Dlna/PlayTo/PlayToManager.cs new file mode 100644 index 0000000000..1e81f32f80 --- /dev/null +++ b/MediaBrowser.Dlna/PlayTo/PlayToManager.cs @@ -0,0 +1,271 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Session; +using MediaBrowser.Dlna.PlayTo.Configuration; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Dlna.PlayTo +{ + class PlayToManager : IDisposable + { + private bool _disposed = false; + private readonly ILogger _logger; + private readonly ISessionManager _sessionManager; + private readonly IHttpClient _httpClient; + private User _defualtUser; + private readonly CancellationTokenSource _tokenSource; + private ConcurrentDictionary _locations; + + private readonly IItemRepository _itemRepository; + private readonly ILibraryManager _libraryManager; + private readonly INetworkManager _networkManager; + + public PlayToManager(ILogger logger, ISessionManager sessionManager, IHttpClient httpClient, IItemRepository itemRepository, ILibraryManager libraryManager, INetworkManager networkManager) + { + _locations = new ConcurrentDictionary(); + _tokenSource = new CancellationTokenSource(); + + _logger = logger; + _sessionManager = sessionManager; + _httpClient = httpClient; + _itemRepository = itemRepository; + _libraryManager = libraryManager; + _networkManager = networkManager; + } + + public async void Start(User defaultUser) + { + _defualtUser = defaultUser; + _logger.Log(LogSeverity.Info, "PlayTo-Manager starting"); + + _locations = new ConcurrentDictionary(); + + foreach (var network in NetworkInterface.GetAllNetworkInterfaces()) + { + _logger.Debug("Found interface: {0}. Type: {1}. Status: {2}", network.Name, network.NetworkInterfaceType, network.OperationalStatus); + + if (!network.SupportsMulticast || OperationalStatus.Up != network.OperationalStatus || !network.GetIPProperties().MulticastAddresses.Any()) + continue; + + var ipV4 = network.GetIPProperties().GetIPv4Properties(); + if (null == ipV4) + continue; + + IPAddress localIp = null; + + foreach (UnicastIPAddressInformation ipInfo in network.GetIPProperties().UnicastAddresses) + { + if (ipInfo.Address.AddressFamily == AddressFamily.InterNetwork) + { + localIp = ipInfo.Address; + break; + } + } + + if (localIp == null) + { + continue; + } + + try + { + CreateListener(localIp); + } + catch (Exception e) + { + _logger.ErrorException("Failed to Initilize Socket", e); + } + + await Task.Delay(100).ConfigureAwait(false); + } + } + + public void Stop() + { + } + + /// + /// Creates a socket for the interface and listends for data. + /// + /// The local ip. + private void CreateListener(IPAddress localIp) + { + Task.Factory.StartNew(async (o) => + { + try + { + var socket = GetMulticastSocket(); + + socket.Bind(new IPEndPoint(localIp, 0)); + + _logger.Info("Creating SSDP listener"); + + var receiveBuffer = new byte[64000]; + + CreateNotifier(socket); + + while (!_tokenSource.IsCancellationRequested) + { + var receivedBytes = await socket.ReceiveAsync(receiveBuffer, 0, 64000); + + if (receivedBytes > 0) + { + var rawData = Encoding.UTF8.GetString(receiveBuffer, 0, receivedBytes); + var uri = SsdpHelper.ParseSsdpResponse(rawData); + + TryCreateController(uri); + } + } + + _logger.Info("SSDP listener - Task completed"); + } + catch (OperationCanceledException c) + { + } + catch (Exception e) + { + _logger.ErrorException("Error in listener", e); + } + + }, _tokenSource.Token, TaskCreationOptions.LongRunning); + } + + private void TryCreateController(Uri uri) + { + Task.Run(async () => + { + try + { + await CreateController(uri).ConfigureAwait(false); + } + catch (OperationCanceledException c) + { + } + catch (Exception ex) + { + _logger.ErrorException("Error creating play to controller", ex); + } + }); + } + + private void CreateNotifier(Socket socket) + { + Task.Factory.StartNew(async (o) => + { + try + { + var request = SsdpHelper.CreateRendererSSDP(3); + + while (true) + { + socket.SendTo(request, new IPEndPoint(IPAddress.Parse("239.255.255.250"), 1900)); + + await Task.Delay(10000).ConfigureAwait(false); + } + } + catch (OperationCanceledException c) + { + } + catch (Exception ex) + { + _logger.ErrorException("Error in notifier", ex); + } + + }, _tokenSource.Token, TaskCreationOptions.LongRunning); + + } + + /// + /// Gets a socket configured for SDDP multicasting. + /// + /// + private Socket GetMulticastSocket() + { + 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"))); + //socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 3); + return socket; + } + + /// + /// Creates a new DlnaSessionController. + /// and logs the session in SessionManager + /// + /// The URI. + /// + private async Task CreateController(Uri uri) + { + if (!IsUriValid(uri)) + return; + + var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _logger).ConfigureAwait(false); + + if (device != null && device.RendererCommands != null && !_sessionManager.Sessions.Any(s => string.Equals(s.DeviceId, device.Properties.UUID) && s.IsActive)) + { + var transcodeProfiles = TranscodeSettings.GetProfileSettings(device.Properties); + + var sessionInfo = await _sessionManager.LogSessionActivity(device.Properties.ClientType, device.Properties.Name, device.Properties.UUID, device.Properties.DisplayName, uri.OriginalString, _defualtUser) + .ConfigureAwait(false); + + var controller = sessionInfo.SessionController as PlayToController; + + if (controller == null) + { + sessionInfo.SessionController = controller = new PlayToController(sessionInfo, _sessionManager, _itemRepository, _libraryManager, _logger, _networkManager); + } + + controller.Init(device, transcodeProfiles); + + _logger.Info("DLNA Session created for {0} - {1}", device.Properties.Name, device.Properties.ModelName); + } + } + + /// + /// Determines if the Uri is valid for further inspection or not. + /// (the limit for reinspection is 5 minutes) + /// + /// The URI. + /// Returns True if the Uri is valid for further inspection + private bool IsUriValid(Uri uri) + { + if (uri == null) + return false; + + if (!_locations.ContainsKey(uri.OriginalString)) + { + _locations.AddOrUpdate(uri.OriginalString, DateTime.UtcNow, (key, existingVal) => existingVal); + + return true; + } + + var time = _locations[uri.OriginalString]; + + if ((DateTime.UtcNow - time).TotalMinutes <= 5) + { + return false; + } + return _locations.TryUpdate(uri.OriginalString, DateTime.UtcNow, time); + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + _tokenSource.Cancel(); + } + } + } +} diff --git a/MediaBrowser.Dlna/PlayTo/PlayToServerEntryPoint.cs b/MediaBrowser.Dlna/PlayTo/PlayToServerEntryPoint.cs new file mode 100644 index 0000000000..e998d13c1e --- /dev/null +++ b/MediaBrowser.Dlna/PlayTo/PlayToServerEntryPoint.cs @@ -0,0 +1,69 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Logging; +using System.Linq; +using System.Threading.Tasks; + +namespace MediaBrowser.Dlna.PlayTo +{ + public class PlayToServerEntryPoint : IServerEntryPoint + { + const string DefaultUser = "Play To"; + + private bool _disposed; + + private readonly IUserManager _userManager; + private readonly PlayToManager _manager; + + public PlayToServerEntryPoint(ILogManager logManager, ISessionManager sessionManager, IUserManager userManager, IHttpClient httpClient, INetworkManager networkManager, IItemRepository itemRepository, ILibraryManager libraryManager) + { + _userManager = userManager; + + _manager = new PlayToManager(logManager.GetLogger("PlayTo"), sessionManager, httpClient, itemRepository, libraryManager, networkManager); + } + + /// + /// Creates the defaultuser if needed. + /// + private async Task CreateUserIfNeeded() + { + var user = _userManager.Users.FirstOrDefault(u => u.Name == DefaultUser); + + if (user == null) + { + user = await _userManager.CreateUser(DefaultUser); + + user.Configuration.IsHidden = true; + user.Configuration.IsAdministrator = false; + user.SaveConfiguration(); + } + + return user; + } + + public async void Run() + { + //var defaultUser = await CreateUserIfNeeded().ConfigureAwait(false); + + //_manager.Start(defaultUser); + } + + #region Dispose + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + _manager.Stop(); + _manager.Dispose(); + } + } + + #endregion + } +} diff --git a/MediaBrowser.Dlna/PlayTo/PlaylistItem.cs b/MediaBrowser.Dlna/PlayTo/PlaylistItem.cs index 591e39bef9..d2d04c4a8e 100644 --- a/MediaBrowser.Dlna/PlayTo/PlaylistItem.cs +++ b/MediaBrowser.Dlna/PlayTo/PlaylistItem.cs @@ -1,4 +1,7 @@ - +using MediaBrowser.Controller.Entities; +using MediaBrowser.Dlna.PlayTo.Configuration; +using System; + namespace MediaBrowser.Dlna.PlayTo { public class PlaylistItem @@ -23,73 +26,73 @@ namespace MediaBrowser.Dlna.PlayTo public long StartPositionTicks { get; set; } - //internal static PlaylistItem GetBasicConfig(BaseItem item, TranscodeSettings[] profileTranscodings) - //{ + public static PlaylistItem GetBasicConfig(BaseItem item, TranscodeSettings[] profileTranscodings) + { - // var playlistItem = new PlaylistItem(); - // playlistItem.ItemId = item.Id.ToString(); + var playlistItem = new PlaylistItem(); + playlistItem.ItemId = item.Id.ToString(); - // if (string.Equals(item.MediaType, MediaBrowser.Model.Entities.MediaType.Video, StringComparison.OrdinalIgnoreCase)) - // { - // playlistItem.IsVideo = true; - // } - // else - // { - // playlistItem.IsAudio = true; - // } + if (string.Equals(item.MediaType, Model.Entities.MediaType.Video, StringComparison.OrdinalIgnoreCase)) + { + playlistItem.IsVideo = true; + } + else + { + playlistItem.IsAudio = true; + } - - // var path = item.Path.ToLower(); - // //Check the DlnaProfile associated with the renderer - // if (profileTranscodings != null) - // { - // foreach (TranscodeSettings transcodeSetting in profileTranscodings) - // { - // if (string.IsNullOrWhiteSpace(transcodeSetting.Container)) - // continue; - // if (path.EndsWith(transcodeSetting.Container)) - // { - // playlistItem.Transcode = true; - // playlistItem.FileFormat = transcodeSetting.TargetContainer; - // return playlistItem; - // } - // } - // } - // if (playlistItem.IsVideo) - // { + var path = item.Path.ToLower(); - // //Check to see if we support serving the format statically - // foreach (string supported in PlayToConfiguration.SupportedStaticFormats) - // { - // if (path.EndsWith(supported)) - // { - // playlistItem.Transcode = false; - // playlistItem.FileFormat = supported; - // return playlistItem; - // } - // } + //Check the DlnaProfile associated with the renderer + if (profileTranscodings != null) + { + foreach (TranscodeSettings transcodeSetting in profileTranscodings) + { + if (string.IsNullOrWhiteSpace(transcodeSetting.Container)) + continue; + if (path.EndsWith(transcodeSetting.Container)) + { + playlistItem.Transcode = true; + playlistItem.FileFormat = transcodeSetting.TargetContainer; + return playlistItem; + } + } + } + if (playlistItem.IsVideo) + { - // playlistItem.Transcode = true; - // playlistItem.FileFormat = "ts"; - // } - // else - // { - // foreach (string supported in PlayToConfiguration.SupportedStaticFormats) - // { - // if (path.EndsWith(supported)) - // { - // playlistItem.Transcode = false; - // playlistItem.FileFormat = supported; - // return playlistItem; - // } - // } + //Check to see if we support serving the format statically + foreach (string supported in PlayToConfiguration.SupportedStaticFormats) + { + if (path.EndsWith(supported)) + { + playlistItem.Transcode = false; + playlistItem.FileFormat = supported; + return playlistItem; + } + } - // playlistItem.Transcode = true; - // playlistItem.FileFormat = "mp3"; - // } + playlistItem.Transcode = true; + playlistItem.FileFormat = "ts"; + } + else + { + foreach (string supported in PlayToConfiguration.SupportedStaticFormats) + { + if (path.EndsWith(supported)) + { + playlistItem.Transcode = false; + playlistItem.FileFormat = supported; + return playlistItem; + } + } - // return playlistItem; - //} + playlistItem.Transcode = true; + playlistItem.FileFormat = "mp3"; + } + + return playlistItem; + } } } diff --git a/MediaBrowser.Dlna/PlayTo/StreamHelper.cs b/MediaBrowser.Dlna/PlayTo/StreamHelper.cs new file mode 100644 index 0000000000..eed0bb7d77 --- /dev/null +++ b/MediaBrowser.Dlna/PlayTo/StreamHelper.cs @@ -0,0 +1,188 @@ +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace MediaBrowser.Dlna.PlayTo +{ + class StreamHelper + { + /// + /// Gets the dlna headers. + /// + /// The item. + /// + internal static string GetDlnaHeaders(PlaylistItem item) + { + var orgOp = item.Transcode ? ";DLNA.ORG_OP=00" : ";DLNA.ORG_OP=01"; + + var orgCi = item.Transcode ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1"; + + const string dlnaflags = ";DLNA.ORG_FLAGS=01500000000000000000000000000000"; + + var contentFeatures = string.Empty; + + if (string.Equals(item.FileFormat, "mp3", StringComparison.OrdinalIgnoreCase)) + { + contentFeatures = "DLNA.ORG_PN=MP3"; + } + else if (string.Equals(item.FileFormat, "wma", StringComparison.OrdinalIgnoreCase)) + { + contentFeatures = "DLNA.ORG_PN=WMABASE"; + } + else if (string.Equals(item.FileFormat, "avi", StringComparison.OrdinalIgnoreCase)) + { + contentFeatures = "DLNA.ORG_PN=AVI"; + } + else if (string.Equals(item.FileFormat, "mkv", StringComparison.OrdinalIgnoreCase)) + { + contentFeatures = "DLNA.ORG_PN=MATROSKA"; + } + else if (string.Equals(item.FileFormat, "mp4", StringComparison.OrdinalIgnoreCase)) + { + contentFeatures = "DLNA.ORG_PN=AVC_MP4_MP_HD_720p_AAC"; + } + else if (string.Equals(item.FileFormat, "mpeg", StringComparison.OrdinalIgnoreCase)) + { + contentFeatures = "DLNA.ORG_PN=MPEG_PS_PAL"; + } + else if (string.Equals(item.FileFormat, "ts", StringComparison.OrdinalIgnoreCase)) + { + contentFeatures = "DLNA.ORG_PN=MPEG_PS_PAL"; + } + else if (item.IsVideo) + { + //Default to AVI for video + contentFeatures = "DLNA.ORG_PN=AVI"; + } + else + { + //Default to MP3 for audio + contentFeatures = "DLNA.ORG_PN=MP3"; + } + + return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';'); + } + + #region Audio + + /// + /// Gets the audio URL. + /// + /// The item. + /// The server address. + /// System.String. + internal static string GetAudioUrl(PlaylistItem item, string serverAddress) + { + if (!item.Transcode) + return string.Format("{0}/audio/{1}/stream.{2}?Static=True", serverAddress, item.ItemId, item.FileFormat); + + return string.Format("{0}/audio/{1}/stream.mp3?AudioCodec=Mp3", serverAddress, item.ItemId); + } + + #endregion + + #region Video + + /// + /// Gets the video URL. + /// + /// The device properties. + /// The item. + /// The streams. + /// The server address. + /// The url to send to the device + internal static string GetVideoUrl(DeviceProperties deviceProperties, PlaylistItem item, List streams, string serverAddress) + { + if (!item.Transcode) + return string.Format("{0}/Videos/{1}/stream.{2}?Static=True", serverAddress, item.ItemId, item.FileFormat); + + var videostream = streams.Where(m => m.Type == MediaStreamType.Video).OrderBy(m => m.IsDefault).FirstOrDefault(); + var audiostream = streams.Where(m => m.Type == MediaStreamType.Audio).OrderBy(m => m.IsDefault).FirstOrDefault(); + + var videoCodec = GetVideoCodec(videostream); + var audioCodec = GetAudioCodec(audiostream); + int? videoBitrate = null; + int? audioBitrate = null; + int? audioChannels = null; + + if (videoCodec != VideoCodecs.Copy) + videoBitrate = 2000000; + + if (audioCodec != AudioCodecs.Copy) + { + audioBitrate = 128000; + audioChannels = 2; + } + + string dlnaCommand = BuildDlnaUrl(deviceProperties.UUID, videoCodec, audioCodec, null, null, videoBitrate, audioChannels, audioBitrate, item.StartPositionTicks, "baseline", "3"); + return string.Format("{0}/Videos/{1}/stream.{2}?{3}", serverAddress, item.ItemId, item.FileFormat, dlnaCommand); + } + + /// + /// Gets the video codec. + /// + /// The video stream. + /// + private static VideoCodecs GetVideoCodec(MediaStream videoStream) + { + switch (videoStream.Codec.ToLower()) + { + case "h264": + case "mpeg4": + return VideoCodecs.Copy; + + } + return VideoCodecs.H264; + } + + /// + /// Gets the audio codec. + /// + /// The audio stream. + /// + private static AudioCodecs GetAudioCodec(MediaStream audioStream) + { + if (audioStream != null) + { + switch (audioStream.Codec.ToLower()) + { + case "aac": + case "mp3": + case "wma": + return AudioCodecs.Copy; + + } + } + return AudioCodecs.Aac; + } + + /// + /// Builds the dlna URL. + /// + private static string BuildDlnaUrl(string deviceID, VideoCodecs? videoCodec, AudioCodecs? audioCodec, int? subtitleIndex, int? audiostreamIndex, int? videoBitrate, int? audiochannels, int? audioBitrate, long? startPositionTicks, string profile, string videoLevel) + { + var usCulture = new CultureInfo("en-US"); + + var dlnaparam = string.Format("Params={0};", deviceID); + + dlnaparam += videoCodec.HasValue ? videoCodec.Value + ";" : ";"; + dlnaparam += audioCodec.HasValue ? audioCodec.Value + ";" : ";"; + dlnaparam += audiostreamIndex.HasValue ? audiostreamIndex.Value.ToString(usCulture) + ";" : ";"; + dlnaparam += subtitleIndex.HasValue ? subtitleIndex.Value.ToString(usCulture) + ";" : ";"; + dlnaparam += videoBitrate.HasValue ? videoBitrate.Value.ToString(usCulture) + ";" : ";"; + dlnaparam += audioBitrate.HasValue ? audioBitrate.Value.ToString(usCulture) + ";" : ";"; + dlnaparam += audiochannels.HasValue ? audiochannels.Value.ToString(usCulture) + ";" : ";"; + dlnaparam += startPositionTicks.HasValue ? startPositionTicks.Value.ToString(usCulture) + ";" : ";"; + dlnaparam += profile + ";"; + dlnaparam += videoLevel + ";"; + + return dlnaparam; + } + + #endregion + + } +} diff --git a/MediaBrowser.Dlna/PlayTo/TransportCommands.cs b/MediaBrowser.Dlna/PlayTo/TransportCommands.cs index c0332642f6..5aba8ba947 100644 --- a/MediaBrowser.Dlna/PlayTo/TransportCommands.cs +++ b/MediaBrowser.Dlna/PlayTo/TransportCommands.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Dlna.PlayTo { public class TransportCommands { - List _stateVariables = new List(); + private List _stateVariables = new List(); public List StateVariables { get @@ -19,7 +19,7 @@ namespace MediaBrowser.Dlna.PlayTo } } - List _serviceActions = new List(); + private List _serviceActions = new List(); public List ServiceActions { get diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 495b076467..97fd02e0d8 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -27,6 +27,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; +using MediaBrowser.Dlna.PlayTo; using MediaBrowser.Model.Logging; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.System; @@ -744,6 +745,9 @@ namespace MediaBrowser.ServerApplication // Server implementations list.Add(typeof(ServerApplicationPaths).Assembly); + // Dlna implementations + list.Add(typeof(PlayToServerEntryPoint).Assembly); + list.AddRange(Assemblies.GetAssembliesWithParts()); // Include composable parts in the running assembly