├── lib ├── UPnP.dll └── UPNP_AV.dll ├── Jishi.Intel.SonosUPnP ├── MediaInfo.cs ├── PlayerStatus.cs ├── SonosTrack.cs ├── PlayerInfo.cs ├── SonosZone.cs ├── SonosDIDL.cs ├── SonosItem.cs ├── Properties │ └── AssemblyInfo.cs ├── Jishi.Intel.SonosUPnP.csproj ├── SonosDiscovery.cs └── SonosPlayer.cs ├── Tests ├── packages.config ├── Properties │ └── AssemblyInfo.cs ├── Tests.csproj └── Tests.cs ├── README.md └── Jishi.Intel.SonosUPnP.sln /lib/UPnP.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jishi/Jishi.Intel.SonosUPnP/HEAD/lib/UPnP.dll -------------------------------------------------------------------------------- /lib/UPNP_AV.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jishi/Jishi.Intel.SonosUPnP/HEAD/lib/UPNP_AV.dll -------------------------------------------------------------------------------- /Jishi.Intel.SonosUPnP/MediaInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Jishi.Intel.SonosUPnP 2 | { 3 | public class MediaInfo 4 | { 5 | public uint NrOfTracks { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /Jishi.Intel.SonosUPnP/PlayerStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Jishi.Intel.SonosUPnP 2 | { 3 | public enum PlayerStatus 4 | { 5 | Stopped, 6 | Playing, 7 | Paused 8 | } 9 | } -------------------------------------------------------------------------------- /Tests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Jishi.Intel.SonosUPnP/SonosTrack.cs: -------------------------------------------------------------------------------- 1 | namespace Jishi.Intel.SonosUPnP 2 | { 3 | public class SonosTrack 4 | { 5 | public string Uri { get; set; } 6 | public string MetaData { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Jishi.Intel.SonosUPnP 2 | ===================== 3 | 4 | Simplified managed UPnP lib aimed for Sonos. Prebuilt version can be found here: 5 | http://upload.grabbarna.se/files/Jishi.Intel.SonosUPnP-0.2a-debug.zip 6 | 7 | Changelog 8 | --------- 9 | 10 | 0.2 Added support for browsing favorites, minor changes in the API 11 | -------------------------------------------------------------------------------- /Jishi.Intel.SonosUPnP/PlayerInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Jishi.Intel.SonosUPnP 4 | { 5 | public class PlayerInfo 6 | { 7 | public string TrackURI { get; set; } 8 | public uint TrackIndex { get; set; } 9 | public string TrackMetaData { get; set; } 10 | public TimeSpan RelTime { get; set; } 11 | public TimeSpan TrackDuration { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /Jishi.Intel.SonosUPnP/SonosZone.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Jishi.Intel.SonosUPnP 5 | { 6 | public class SonosZone 7 | { 8 | private string CoordinatorUUID; 9 | private IList players = new List(); 10 | 11 | public SonosZone(string coordinator) 12 | { 13 | CoordinatorUUID = coordinator; 14 | } 15 | 16 | public void AddPlayer(SonosPlayer player) 17 | { 18 | if (player.UUID == CoordinatorUUID) 19 | { 20 | Coordinator = player; 21 | } 22 | 23 | players.Add(player); 24 | } 25 | 26 | public SonosPlayer Coordinator { get; set; } 27 | 28 | public IList Players 29 | { 30 | get { return players; } 31 | set { players = value; } 32 | } 33 | 34 | public string Name 35 | { 36 | get { return string.Join(" + ", players.Select(p => p.Name)); } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Jishi.Intel.SonosUPnP/SonosDIDL.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Xml.Linq; 3 | 4 | namespace Jishi.Intel.SonosUPnP 5 | { 6 | public class SonosDIDL 7 | { 8 | public string AlbumArtURI { get; set; } 9 | public string Title { get; set; } 10 | public string Artist { get; set; } 11 | public string Album { get; set; } 12 | public string Uri { get; set; } 13 | public string Description { get; set; } 14 | 15 | public static IList Parse(string xml) 16 | { 17 | var didl = XElement.Parse(xml); 18 | return Parse(didl); 19 | } 20 | 21 | public static IList Parse(XElement didl) 22 | { 23 | XNamespace ns = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; 24 | XNamespace dc = "http://purl.org/dc/elements/1.1/"; 25 | XNamespace upnp = "urn:schemas-upnp-org:metadata-1-0/upnp/"; 26 | XNamespace r = "urn:schemas-rinconnetworks-com:metadata-1-0/"; 27 | 28 | var items = didl.Elements(ns + "item"); 29 | 30 | var list = new List(); 31 | 32 | foreach (var item in items) 33 | { 34 | var response = new SonosDIDL(); 35 | response.AlbumArtURI = item.Element(upnp + "albumArtURI").Value; 36 | response.Artist = item.Element(dc + "creator").Value; 37 | response.Title = item.Element(dc + "title").Value; 38 | list.Add(response); 39 | } 40 | 41 | return list; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /Jishi.Intel.SonosUPnP.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jishi.Intel.SonosUPnP", "Jishi.Intel.SonosUPnP\Jishi.Intel.SonosUPnP.csproj", "{9DE76BE9-A61B-4754-92DE-05B9FD2EEC5A}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{48E40C2A-93EE-4B14-A511-E5A8E9A68DC2}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {9DE76BE9-A61B-4754-92DE-05B9FD2EEC5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {9DE76BE9-A61B-4754-92DE-05B9FD2EEC5A}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {9DE76BE9-A61B-4754-92DE-05B9FD2EEC5A}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {9DE76BE9-A61B-4754-92DE-05B9FD2EEC5A}.Release|Any CPU.Build.0 = Release|Any CPU 18 | {48E40C2A-93EE-4B14-A511-E5A8E9A68DC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {48E40C2A-93EE-4B14-A511-E5A8E9A68DC2}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {48E40C2A-93EE-4B14-A511-E5A8E9A68DC2}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {48E40C2A-93EE-4B14-A511-E5A8E9A68DC2}.Release|Any CPU.Build.0 = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | EndGlobal 27 | -------------------------------------------------------------------------------- /Jishi.Intel.SonosUPnP/SonosItem.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Xml.Linq; 3 | 4 | namespace Jishi.Intel.SonosUPnP 5 | { 6 | public class SonosItem 7 | { 8 | public virtual SonosTrack Track { get; set; } 9 | public virtual SonosDIDL DIDL { get; set; } 10 | 11 | public static IList Parse(string xmlString) 12 | { 13 | var xml = XElement.Parse(xmlString); 14 | XNamespace ns = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; 15 | XNamespace dc = "http://purl.org/dc/elements/1.1/"; 16 | XNamespace upnp = "urn:schemas-upnp-org:metadata-1-0/upnp/"; 17 | XNamespace r = "urn:schemas-rinconnetworks-com:metadata-1-0/"; 18 | 19 | var items = xml.Elements(ns + "item"); 20 | 21 | var list = new List(); 22 | 23 | foreach (var item in items) 24 | { 25 | var track = new SonosTrack(); 26 | track.Uri = (string) item.Element(ns + "res"); 27 | track.MetaData = (string) item.Element(r + "resMD"); 28 | 29 | 30 | // fix didl if exist 31 | var didl = new SonosDIDL(); 32 | didl.AlbumArtURI = (string) item.Element(upnp + "albumArtURI"); 33 | didl.Artist = (string) item.Element(dc + "creator"); 34 | didl.Title = (string) item.Element(dc + "title"); 35 | didl.Description = (string) item.Element( r + "description" ); 36 | 37 | list.Add(new SonosItem 38 | { 39 | Track = track, 40 | DIDL = didl 41 | }); 42 | } 43 | 44 | return list; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Microsoft")] 12 | [assembly: AssemblyProduct("Tests")] 13 | [assembly: AssemblyCopyright("Copyright © Microsoft 2013")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("4f7a4f52-a93b-4906-900c-697f30375473")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /Jishi.Intel.SonosUPnP/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle( "Jishi.Intel.SonosUPnP" )] 9 | [assembly: AssemblyDescription( "" )] 10 | [assembly: AssemblyConfiguration( "" )] 11 | [assembly: AssemblyCompany( "None" )] 12 | [assembly: AssemblyProduct( "Jishi.Intel.SonosUPnP" )] 13 | [assembly: AssemblyCopyright( "Copyright © Jimmy Shimizu 2013" )] 14 | [assembly: AssemblyTrademark( "" )] 15 | [assembly: AssemblyCulture( "" )] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible( false )] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid( "87e02507-b31e-4f75-91fe-f0293280116b" )] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion( "0.0.0.2" )] 36 | [assembly: AssemblyFileVersion( "0.0.0.2" )] 37 | -------------------------------------------------------------------------------- /Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {48E40C2A-93EE-4B14-A511-E5A8E9A68DC2} 8 | Library 9 | Properties 10 | Tests 11 | Tests 12 | v4.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | ..\packages\Machine.Specifications.0.5.12\lib\net40\Machine.Specifications.dll 35 | 36 | 37 | ..\packages\Machine.Specifications.0.5.12\lib\net40\Machine.Specifications.Clr4.dll 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {9de76be9-a61b-4754-92de-05b9fd2eec5a} 57 | Jishi.Intel.SonosUPnP 58 | 59 | 60 | 61 | 68 | -------------------------------------------------------------------------------- /Jishi.Intel.SonosUPnP/Jishi.Intel.SonosUPnP.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | 8.0.30703 7 | 2.0 8 | {9DE76BE9-A61B-4754-92DE-05B9FD2EEC5A} 9 | Library 10 | Properties 11 | Jishi.Intel.SonosUPnP 12 | Jishi.Intel.SonosUPnP 13 | v4.0 14 | 512 15 | Client 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | False 44 | ..\lib\UPnP.dll 45 | 46 | 47 | False 48 | ..\lib\UPNP_AV.dll 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 72 | -------------------------------------------------------------------------------- /Jishi.Intel.SonosUPnP/SonosDiscovery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Xml.Linq; 7 | using OpenSource.UPnP; 8 | 9 | namespace Jishi.Intel.SonosUPnP 10 | { 11 | public class SonosDiscovery 12 | { 13 | private IList zones = new List(); 14 | private IList players = new List(); 15 | private UPnPSmartControlPoint ControlPoint { get; set; } 16 | 17 | private IDictionary playerDevices = new Dictionary(); 18 | private Timer stateChangedTimer; 19 | 20 | public virtual void StartScan() 21 | { 22 | ControlPoint = new UPnPSmartControlPoint(OnDeviceAdded, OnServiceAdded, "urn:schemas-upnp-org:device:ZonePlayer:1"); 23 | } 24 | 25 | public IList Players 26 | { 27 | get { return players; } 28 | set { players = value; } 29 | } 30 | 31 | public IList Zones 32 | { 33 | get { return zones; } 34 | set { zones = value; } 35 | } 36 | 37 | public event Action TopologyChanged; 38 | 39 | private void OnServiceAdded(UPnPSmartControlPoint sender, UPnPService service) 40 | { 41 | } 42 | 43 | private void OnDeviceAdded(UPnPSmartControlPoint cp, UPnPDevice device) 44 | { 45 | Console.WriteLine("found player " + device); 46 | //players.Add(new SonosPlayer(device)); 47 | // we need to save these for future reference 48 | lock (playerDevices) 49 | { 50 | playerDevices[device.UniqueDeviceName] = device; 51 | } 52 | 53 | // okay, we will try and notify the players that they have been found now. 54 | var player = players.FirstOrDefault(p => p.UUID == device.UniqueDeviceName); 55 | if (player != null) 56 | { 57 | player.SetDevice(device); 58 | } 59 | 60 | // Subscribe to events 61 | var topologyService = device.GetService("urn:upnp-org:serviceId:ZoneGroupTopology"); 62 | topologyService.Subscribe(600, (service, subscribeok) => 63 | { 64 | if (!subscribeok) return; 65 | 66 | var stateVariable = service.GetStateVariableObject("ZoneGroupState"); 67 | stateVariable.OnModified += OnZoneGroupStateChanged; 68 | }); 69 | } 70 | 71 | private void OnZoneGroupStateChanged(UPnPStateVariable sender, object newvalue) 72 | { 73 | Console.WriteLine(sender.Value); 74 | 75 | // Avoid multiple state changes and consolidate them 76 | if (stateChangedTimer != null) 77 | stateChangedTimer.Dispose(); 78 | stateChangedTimer = new Timer((state) => HandleZoneXML(sender.Value.ToString()), null, TimeSpan.FromMilliseconds(700), 79 | TimeSpan.FromMilliseconds(-1)); 80 | } 81 | 82 | private void HandleZoneXML(string xml) 83 | { 84 | var doc = XElement.Parse(xml); 85 | lock (zones) 86 | { 87 | zones.Clear(); 88 | foreach (var zoneXML in doc.Descendants("ZoneGroup")) 89 | { 90 | CreateZone(zoneXML); 91 | } 92 | } 93 | 94 | lock (players) 95 | { 96 | players.Clear(); 97 | lock (zones) 98 | { 99 | players = zones.SelectMany(z => z.Players).ToList(); 100 | } 101 | } 102 | 103 | if (TopologyChanged != null) 104 | TopologyChanged.Invoke(); 105 | } 106 | 107 | private void CreateZone(XElement zoneXml) 108 | { 109 | var players = zoneXml.Descendants("ZoneGroupMember").Where(x => x.Attribute("Invisible") == null).ToList(); 110 | if (players.Count > 0) 111 | { 112 | var zone = new SonosZone((string) zoneXml.Attribute("Coordinator")); 113 | 114 | foreach (var playerXml in players) 115 | { 116 | var player = new SonosPlayer 117 | { 118 | Name = (string) playerXml.Attribute("ZoneName"), 119 | UUID = (string) playerXml.Attribute("UUID"), 120 | ControlPoint = ControlPoint 121 | }; 122 | 123 | zone.AddPlayer(player); 124 | Players.Add(player); 125 | 126 | // This can happen before or after the topology event... 127 | if (playerDevices.ContainsKey(player.UUID)) 128 | { 129 | player.SetDevice(playerDevices[player.UUID]); 130 | } 131 | } 132 | 133 | Zones.Add(zone); 134 | } 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /Tests/Tests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Text; 7 | using System.Threading; 8 | using Jishi.Intel.SonosUPnP; 9 | using Machine.Specifications; 10 | 11 | 12 | namespace Tests 13 | { 14 | public class DiscoveryBase 15 | { 16 | Establish context = () => 17 | { 18 | d = new SonosDiscovery(); 19 | d.StartScan(); 20 | Thread.Sleep( 5000 ); 21 | }; 22 | 23 | 24 | protected static SonosDiscovery d; 25 | } 26 | 27 | public class simple_discovery : DiscoveryBase 28 | { 29 | Because of = () => { }; 30 | 31 | It should_have_zones = () => { d.Zones.Count.ShouldBeGreaterThan( 0 ); }; 32 | It should_have_players = () => { d.Players.Count.ShouldBeGreaterThan( 0 ); }; 33 | } 34 | 35 | public class simple_play : DiscoveryBase 36 | { 37 | Because of = () => 38 | { 39 | player = d.Players.First( p => p.Name == "Kitchen" ); 40 | uri = "x-sonos-spotify:" + Uri.EscapeDataString( "spotify:track:5UnX1hCXQzmUzw2dn3L9nY" ) + "?sid=9&flags=0"; 41 | var didl = 42 | string.Format( 43 | @"{1}object.item.audioItem.musicTrackSA_RINCON2311_jishi", 44 | Uri.EscapeDataString( "spotify:track:5UnX1hCXQzmUzw2dn3L9nY" ), 45 | "Foo" 46 | ); 47 | 48 | Console.WriteLine( "playing on " + player.Name ); 49 | player.SetAVTransportURI( new SonosTrack { Uri = uri, MetaData = didl } ); 50 | Thread.Sleep( 1000 ); 51 | player.Play(); 52 | }; 53 | 54 | It should_be_playing = () => { player.CurrentStatus.ShouldEqual( PlayerStatus.Playing ); }; 55 | It should_be_correct_track = () => { player.CurrentTrack.Uri.ShouldEqual( uri ); }; 56 | It should_have_players = () => { d.Players.Count.ShouldBeGreaterThan( 0 ); }; 57 | private static SonosPlayer player; 58 | private static string uri; 59 | } 60 | 61 | public class simple_seek : DiscoveryBase 62 | { 63 | Because of = () => 64 | { 65 | player = d.Players.First( p => p.Name == "Kitchen" ); 66 | uri = "x-sonos-spotify:" + Uri.EscapeDataString( "spotify:track:5UnX1hCXQzmUzw2dn3L9nY" ) + "?sid=9&flags=0"; 67 | var didl = 68 | string.Format( 69 | @"{1}object.item.audioItem.musicTrackSA_RINCON2311_jishi", 70 | Uri.EscapeDataString( "spotify:track:5UnX1hCXQzmUzw2dn3L9nY" ), 71 | "Foo" 72 | ); 73 | 74 | Console.WriteLine( "playing on " + player.Name ); 75 | var trackPosition = player.Enqueue( new SonosTrack { Uri = uri, MetaData = didl } ); 76 | player.Seek( trackPosition ); 77 | 78 | }; 79 | 80 | It should_be_playing = () => { player.CurrentStatus.ShouldEqual( PlayerStatus.Playing ); }; 81 | It should_be_correct_track = () => { player.CurrentTrack.Uri.ShouldEqual( uri ); }; 82 | It should_have_players = () => { d.Players.Count.ShouldBeGreaterThan( 0 ); }; 83 | private static SonosPlayer player; 84 | private static string uri; 85 | } 86 | 87 | public class pause : DiscoveryBase 88 | { 89 | Because of = () => 90 | { 91 | player = d.Players.First( p => p.Name == "Kitchen" ); 92 | player.Pause(); 93 | 94 | }; 95 | 96 | It should_have_players = () => { }; 97 | private static SonosPlayer player; 98 | private static string uri; 99 | } 100 | 101 | public class get_favorites : DiscoveryBase 102 | { 103 | Because of = () => 104 | { 105 | zone = d.Zones.First(); 106 | 107 | favorites = zone.Coordinator.GetFavorites(); 108 | Console.WriteLine(favorites); 109 | 110 | var favorite = favorites.First(); 111 | 112 | // Enqueue 113 | zone.Coordinator.Enqueue(favorite.Track); 114 | 115 | }; 116 | 117 | It should_have_favorites = () => { favorites.Count.ShouldBeGreaterThan(0); }; 118 | 119 | private static SonosZone zone; 120 | private static IList favorites; 121 | } 122 | 123 | public class get_artists : DiscoveryBase 124 | { 125 | Because of = () => 126 | { 127 | zone = d.Zones.First(); 128 | 129 | result = zone.Coordinator.GetArtists(null, 0, 2); 130 | 131 | result2 = zone.Coordinator.GetArtists( null, 2, 2 ); 132 | 133 | }; 134 | 135 | It should_have_startIndex = () => { result.StartingIndex.ShouldEqual(0u); }; 136 | It should_have_result = () => { result.NumberReturned.ShouldBeGreaterThan( 0u ); }; 137 | It should_have_more_total = () => { result.TotalMatches.ShouldBeGreaterThan( 2 ); }; 138 | It should_have_startIndex2 = () => { result2.StartingIndex.ShouldEqual( 2u ); }; 139 | It should_have_result2 = () => { result2.NumberReturned.ShouldBeGreaterThan( 0u ); }; 140 | It should_have_more_total2 = () => { result2.TotalMatches.ShouldBeGreaterThan( 2 ); }; 141 | 142 | private static SonosZone zone; 143 | private static SearchResult result; 144 | private static SearchResult result2; 145 | } 146 | } -------------------------------------------------------------------------------- /Jishi.Intel.SonosUPnP/SonosPlayer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using System.Xml.Linq; 7 | using OpenSource.UPnP; 8 | 9 | namespace Jishi.Intel.SonosUPnP 10 | { 11 | public class SonosPlayer 12 | { 13 | private UPnPDevice device; 14 | private UPnPDevice mediaRenderer; 15 | private UPnPService avTransport; 16 | private UPnPDevice mediaServer; 17 | private UPnPService renderingControl; 18 | private UPnPService contentDirectory; 19 | private PlayerState currentState = new PlayerState(); 20 | private Timer positionTimer; 21 | 22 | public string Name { get; set; } 23 | public string UUID { get; set; } 24 | 25 | public event Action StateChanged; 26 | 27 | private void SubscribeToEvents() 28 | { 29 | AVTransport.Subscribe(600, (service, subscribeok) => 30 | { 31 | if (!subscribeok) 32 | return; 33 | 34 | var lastChangeStateVariable = service.GetStateVariableObject("LastChange"); 35 | lastChangeStateVariable.OnModified += ChangeTriggered; 36 | }); 37 | } 38 | 39 | private void ChangeTriggered(UPnPStateVariable sender, object value) 40 | { 41 | Console.WriteLine("LastChange from {0}", UUID); 42 | var newState = sender.Value; 43 | Console.WriteLine(newState); 44 | ParseChangeXML((string) newState); 45 | } 46 | 47 | private void ParseChangeXML(string newState) 48 | { 49 | var xEvent = XElement.Parse(newState); 50 | XNamespace ns = "urn:schemas-upnp-org:metadata-1-0/AVT/"; 51 | XNamespace r = "urn:schemas-rinconnetworks-com:metadata-1-0/"; 52 | 53 | 54 | var instance = xEvent.Element(ns + "InstanceID"); 55 | 56 | // We can receive other types of change events here. 57 | if (instance.Element(ns + "TransportState") == null) 58 | { 59 | return; 60 | } 61 | 62 | var preliminaryState = new PlayerState 63 | { 64 | TransportState = instance.Element(ns + "TransportState").Attribute("val").Value, 65 | NumberOfTracks = instance.Element(ns + "NumberOfTracks").Attribute("val").Value, 66 | CurrentTrack = instance.Element(ns + "CurrentTrack").Attribute("val").Value, 67 | CurrentTrackDuration = 68 | ParseDuration(instance.Element(ns + "CurrentTrackDuration").Attribute("val").Value), 69 | CurrentTrackMetaData = instance.Element(ns + "CurrentTrackMetaData").Attribute("val").Value, 70 | NextTrackMetaData = instance.Element(r + "NextTrackMetaData").Attribute("val").Value 71 | }; 72 | 73 | currentState = preliminaryState; 74 | 75 | // every time we have got a state change, do a PositionInfo 76 | try 77 | { 78 | var positionInfo = GetPositionInfo(); 79 | CurrentState.RelTime = positionInfo.RelTime; 80 | } 81 | catch (Exception) 82 | { 83 | // void 84 | } 85 | 86 | CurrentState.LastStateChange = DateTime.Now; 87 | 88 | if (StateChanged != null) 89 | StateChanged.Invoke(this); 90 | } 91 | 92 | private TimeSpan ParseDuration(string value) 93 | { 94 | if (string.IsNullOrEmpty(value)) 95 | return TimeSpan.FromSeconds(0); 96 | return TimeSpan.Parse(value); 97 | } 98 | 99 | public UPnPSmartControlPoint ControlPoint { get; set; } 100 | 101 | public UPnPDevice Device { get; set; } 102 | 103 | public UPnPDevice MediaRenderer 104 | { 105 | get 106 | { 107 | if (mediaRenderer != null) 108 | return mediaRenderer; 109 | if (Device == null) 110 | return null; 111 | mediaRenderer = 112 | Device.EmbeddedDevices.FirstOrDefault(d => d.DeviceURN == "urn:schemas-upnp-org:device:MediaRenderer:1"); 113 | return mediaRenderer; 114 | } 115 | } 116 | 117 | public UPnPDevice MediaServer 118 | { 119 | get 120 | { 121 | if (mediaServer != null) 122 | return mediaServer; 123 | if (Device == null) 124 | return null; 125 | mediaServer = 126 | Device.EmbeddedDevices.FirstOrDefault(d => d.DeviceURN == "urn:schemas-upnp-org:device:MediaServer:1"); 127 | return mediaServer; 128 | } 129 | } 130 | 131 | public UPnPService RenderingControl 132 | { 133 | get 134 | { 135 | if (renderingControl != null) 136 | return renderingControl; 137 | if (MediaRenderer == null) 138 | return null; 139 | renderingControl = MediaRenderer.GetService("urn:upnp-org:serviceId:RenderingControl"); 140 | return renderingControl; 141 | } 142 | } 143 | 144 | public UPnPService AVTransport 145 | { 146 | get 147 | { 148 | if (avTransport != null) 149 | return avTransport; 150 | if (MediaRenderer == null) 151 | return null; 152 | avTransport = MediaRenderer.GetService("urn:upnp-org:serviceId:AVTransport"); 153 | return avTransport; 154 | } 155 | } 156 | 157 | public UPnPService ContentDirectory 158 | { 159 | get 160 | { 161 | if (contentDirectory != null) 162 | return contentDirectory; 163 | if (MediaServer == null) 164 | return null; 165 | contentDirectory = MediaServer.GetService("urn:upnp-org:serviceId:ContentDirectory"); 166 | return contentDirectory; 167 | } 168 | } 169 | 170 | public PlayerState CurrentState 171 | { 172 | get { return currentState; } 173 | } 174 | 175 | public PlayerStatus CurrentStatus 176 | { 177 | get { return GetPlayerStatus(); } 178 | } 179 | 180 | public PlayerInfo PlayerInfo 181 | { 182 | get { return GetPositionInfo(); } 183 | } 184 | 185 | public MediaInfo MediaInfo 186 | { 187 | get { return GetMediaInfo(); } 188 | } 189 | 190 | public SonosTrack CurrentTrack 191 | { 192 | get { throw new NotImplementedException(); } 193 | } 194 | 195 | public Uri BaseUrl 196 | { 197 | get { return Device.BaseURL; } 198 | } 199 | 200 | public void SetDevice(UPnPDevice playerDevice) 201 | { 202 | Device = playerDevice; 203 | // Subscribe to LastChange event 204 | SubscribeToEvents(); 205 | 206 | // Start a timer that polls for PositionInfo 207 | //StartPolling(); 208 | } 209 | 210 | public void StartPolling() 211 | { 212 | if (positionTimer != null) 213 | { 214 | return; 215 | } 216 | 217 | if (CurrentStatus != PlayerStatus.Playing) 218 | { 219 | return; 220 | } 221 | 222 | positionTimer = new Timer(UpdateState, null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(30)); 223 | } 224 | 225 | private void UpdateState(object state) 226 | { 227 | var positionInfo = GetPositionInfo(); 228 | CurrentState.RelTime = positionInfo.RelTime; 229 | CurrentState.LastStateChange = DateTime.Now; 230 | 231 | if (StateChanged != null) 232 | StateChanged.Invoke(this); 233 | } 234 | 235 | public PlayerStatus GetPlayerStatus() 236 | { 237 | if (AVTransport == null) 238 | { 239 | return PlayerStatus.Stopped; 240 | } 241 | var arguments = new UPnPArgument[4]; 242 | arguments[0] = new UPnPArgument("InstanceID", 0u); 243 | arguments[1] = new UPnPArgument("CurrentTransportState", ""); 244 | arguments[2] = new UPnPArgument("CurrentTransportStatus", ""); 245 | arguments[3] = new UPnPArgument("CurrentSpeed", ""); 246 | 247 | try 248 | { 249 | AVTransport.InvokeSync("GetTransportInfo", arguments); 250 | } 251 | catch (UPnPInvokeException ex) 252 | { 253 | return PlayerStatus.Stopped; 254 | } 255 | 256 | PlayerStatus status; 257 | 258 | switch ((string) arguments[1].DataValue) 259 | { 260 | case "PLAYING": 261 | return PlayerStatus.Playing; 262 | break; 263 | case "PAUSED": 264 | return PlayerStatus.Paused; 265 | break; 266 | default: 267 | return PlayerStatus.Stopped; 268 | } 269 | } 270 | 271 | public MediaInfo GetMediaInfo() 272 | { 273 | var arguments = new UPnPArgument[10]; 274 | arguments[0] = new UPnPArgument("InstanceID", 0u); 275 | arguments[1] = new UPnPArgument("NrTracks", 0u); 276 | arguments[2] = new UPnPArgument("MediaDuration", null); 277 | arguments[3] = new UPnPArgument("CurrentURI", null); 278 | arguments[4] = new UPnPArgument("CurrentURIMetaData", null); 279 | arguments[5] = new UPnPArgument("NextURI", null); 280 | arguments[6] = new UPnPArgument("NextURIMetaData", null); 281 | arguments[7] = new UPnPArgument("PlayMedium", null); 282 | arguments[8] = new UPnPArgument("RecordMedium", null); 283 | arguments[9] = new UPnPArgument("WriteStatus", null); 284 | AVTransport.InvokeSync("GetMediaInfo", arguments); 285 | 286 | return new MediaInfo 287 | { 288 | NrOfTracks = (uint) arguments[1].DataValue 289 | }; 290 | } 291 | 292 | public PlayerInfo GetPositionInfo() 293 | { 294 | var arguments = new UPnPArgument[9]; 295 | arguments[0] = new UPnPArgument("InstanceID", 0u); 296 | arguments[1] = new UPnPArgument("Track", 0u); 297 | arguments[2] = new UPnPArgument("TrackDuration", null); 298 | arguments[3] = new UPnPArgument("TrackMetaData", null); 299 | arguments[4] = new UPnPArgument("TrackURI", null); 300 | arguments[5] = new UPnPArgument("RelTime", null); 301 | arguments[6] = new UPnPArgument("AbsTime", null); 302 | arguments[7] = new UPnPArgument("RelCount", 0); 303 | arguments[8] = new UPnPArgument("AbsCount", 0); 304 | AVTransport.InvokeSync("GetPositionInfo", arguments); 305 | 306 | TimeSpan trackDuration; 307 | TimeSpan relTime; 308 | 309 | TimeSpan.TryParse((string) arguments[2].DataValue, out trackDuration); 310 | TimeSpan.TryParse((string) arguments[5].DataValue, out relTime); 311 | return new PlayerInfo 312 | { 313 | TrackIndex = (uint) arguments[1].DataValue, 314 | TrackMetaData = (string) arguments[3].DataValue, 315 | TrackURI = (string) arguments[4].DataValue, 316 | TrackDuration = trackDuration, 317 | RelTime = relTime 318 | }; 319 | } 320 | 321 | public void SetAVTransportURI(SonosTrack track) 322 | { 323 | var arguments = new UPnPArgument[3]; 324 | arguments[0] = new UPnPArgument("InstanceID", 0u); 325 | arguments[1] = new UPnPArgument("CurrentURI", track.Uri); 326 | arguments[2] = new UPnPArgument("CurrentURIMetaData", track.MetaData); 327 | AVTransport.InvokeAsync("SetAVTransportURI", arguments); 328 | } 329 | 330 | public void Play() 331 | { 332 | var arguments = new UPnPArgument[2]; 333 | arguments[0] = new UPnPArgument("InstanceID", 0u); 334 | arguments[1] = new UPnPArgument("Speed", "1"); 335 | AVTransport.InvokeAsync("Play", arguments); 336 | } 337 | 338 | public uint Enqueue(SonosTrack track, bool asNext = false) 339 | { 340 | var arguments = new UPnPArgument[8]; 341 | arguments[0] = new UPnPArgument("InstanceID", 0u); 342 | arguments[1] = new UPnPArgument("EnqueuedURI", track.Uri); 343 | arguments[2] = new UPnPArgument("EnqueuedURIMetaData", track.MetaData); 344 | arguments[3] = new UPnPArgument("DesiredFirstTrackNumberEnqueued", 0u); 345 | arguments[4] = new UPnPArgument("EnqueueAsNext", asNext); 346 | arguments[5] = new UPnPArgument("FirstTrackNumberEnqueued", null); 347 | arguments[6] = new UPnPArgument("NumTracksAdded", null); 348 | arguments[7] = new UPnPArgument("NewQueueLength", null); 349 | AVTransport.InvokeSync("AddURIToQueue", arguments); 350 | 351 | return (uint) arguments[5].DataValue; 352 | } 353 | 354 | public void Seek(uint position) 355 | { 356 | var arguments = new UPnPArgument[3]; 357 | arguments[0] = new UPnPArgument("InstanceID", 0u); 358 | arguments[1] = new UPnPArgument("Unit", "TRACK_NR"); 359 | arguments[2] = new UPnPArgument("Target", position.ToString()); 360 | AVTransport.InvokeAsync("Seek", arguments); 361 | } 362 | 363 | public void Pause() 364 | { 365 | var arguments = new UPnPArgument[1]; 366 | arguments[0] = new UPnPArgument("InstanceID", 0u); 367 | AVTransport.InvokeAsync("Pause", arguments); 368 | } 369 | 370 | public IList GetQueue() 371 | { 372 | var searchResult = Browse("Q:0"); 373 | return SonosItem.Parse(searchResult.Result); 374 | } 375 | 376 | public virtual IList GetFavorites() 377 | { 378 | var searchResult = Browse("FV:2"); 379 | var tracks = SonosItem.Parse(searchResult.Result); 380 | return tracks; 381 | } 382 | 383 | public virtual SearchResult GetArtists(string objectId = null, uint startIndex = 0, uint requestedCount = 100) 384 | { 385 | if (objectId == null) 386 | objectId = "A:ARTIST"; 387 | 388 | var searchResult = Browse(objectId, startIndex, requestedCount); 389 | return searchResult; 390 | } 391 | 392 | private SearchResult Browse(string action, uint startIndex = 0u, uint requestedCount = 100u) 393 | { 394 | var arguments = new UPnPArgument[10]; 395 | arguments[0] = new UPnPArgument("ObjectID", action); 396 | arguments[1] = new UPnPArgument("BrowseFlag", "BrowseDirectChildren"); 397 | arguments[2] = new UPnPArgument("Filter", ""); 398 | arguments[3] = new UPnPArgument("StartingIndex", startIndex); 399 | arguments[4] = new UPnPArgument("RequestedCount", requestedCount); 400 | arguments[5] = new UPnPArgument("SortCriteria", ""); 401 | arguments[6] = new UPnPArgument("Result", ""); 402 | arguments[7] = new UPnPArgument("NumberReturned", 0u); 403 | arguments[8] = new UPnPArgument("TotalMatches", 0u); 404 | arguments[9] = new UPnPArgument("UpdateID", 0u); 405 | 406 | ContentDirectory.InvokeSync("Browse", arguments); 407 | 408 | var result = arguments[6].DataValue as string; 409 | return new SearchResult 410 | { 411 | Result = result, 412 | StartingIndex = startIndex, 413 | NumberReturned = (uint) arguments[7].DataValue, 414 | TotalMatches = (uint) arguments[8].DataValue 415 | }; 416 | } 417 | } 418 | 419 | public class SearchResult 420 | { 421 | public string Result { get; set; } 422 | public uint StartingIndex { get; set; } 423 | public uint NumberReturned { get; set; } 424 | public uint TotalMatches { get; set; } 425 | } 426 | 427 | public class PlayerState 428 | { 429 | public string TransportState { get; set; } 430 | public string NumberOfTracks { get; set; } 431 | public string CurrentTrack { get; set; } 432 | public TimeSpan CurrentTrackDuration { get; set; } 433 | public string CurrentTrackMetaData { get; set; } 434 | public DateTime LastStateChange { get; set; } 435 | public TimeSpan RelTime { get; set; } 436 | public string NextTrackMetaData { get; set; } 437 | } 438 | } --------------------------------------------------------------------------------