├── 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 | }
--------------------------------------------------------------------------------