diff --git a/LocalAudioBroadcast/ControlPoint.cs b/LocalAudioBroadcast/ControlPoint.cs index 802380f..d90a1a8 100644 --- a/LocalAudioBroadcast/ControlPoint.cs +++ b/LocalAudioBroadcast/ControlPoint.cs @@ -1,190 +1,252 @@ -// Copyright 2014 Mario Guggenberger -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -using System; -using System.Collections.Generic; -using System.Text; -using OpenSource.UPnP; -using System.Web; - -namespace LocalAudioBroadcast { - class ControlPoint { - - private const string UPNP_DEVICE_RENDERER = "urn:schemas-upnp-org:device:MediaRenderer:1"; - private const string UPNP_SERVICE_AVTRANSPORT = "urn:schemas-upnp-org:service:AVTransport:1"; - private const string UPNP_SERVICE_CONTROL = "urn:schemas-upnp-org:service:RenderingControl:1"; - - private UPnPSmartControlPoint scp; - private UPnPDevice device; - - public event UPnPSmartControlPoint.DeviceHandler OnAddedDevice; - public event UPnPSmartControlPoint.DeviceHandler OnRemovedDevice; - - public ControlPoint() { - scp = new UPnPSmartControlPoint(null, null, UPNP_DEVICE_RENDERER); - scp.OnAddedDevice += new UPnPSmartControlPoint.DeviceHandler(ControlPoint_OnAddedDevice); - scp.OnRemovedDevice += new UPnPSmartControlPoint.DeviceHandler(ControlPoint_OnRemovedDevice); - } - - public void Rescan() { - scp.Rescan(); - } - - private void ControlPoint_OnAddedDevice(UPnPSmartControlPoint sender, UPnPDevice device) { - Console.WriteLine("OnAddedDevice(" + device.FriendlyName + ", " + device.RemoteEndPoint + ", " + device.UniqueDeviceName + ")"); - - foreach (UPnPService service in device.Services) { - Console.WriteLine(device.FriendlyName + " service: " + service.ServiceURN); - } - - // only add device if necessary services are available - if (device.GetServices(UPNP_SERVICE_CONTROL).Length > 0 && device.GetServices(UPNP_SERVICE_AVTRANSPORT).Length > 0) { - if (OnAddedDevice != null) - OnAddedDevice(sender, device); - } - } - - private void ControlPoint_OnRemovedDevice(UPnPSmartControlPoint sender, UPnPDevice device) { - Console.WriteLine("OnRemovedDevice(" + device.FriendlyName + ", " + device.RemoteEndPoint + ", " + device.UniqueDeviceName + ")"); - - if (this.device != null && this.device.UniqueDeviceName == device.UniqueDeviceName) { - this.device = null; - } - - if (OnRemovedDevice != null) OnRemovedDevice(sender, device); - } - - public UPnPDevice Device { - get { return device; } - set { device = value; } - } - - public int GetVolume() { - if (device == null) return -1; - var service = device.GetServices(UPNP_SERVICE_CONTROL)[0]; - - UPnPArgument[] args = new UPnPArgument[] { - new UPnPArgument("InstanceID", (uint)0), - new UPnPArgument("Channel", "Master"), - new UPnPArgument("CurrentVolume", null) - }; - - service.InvokeSync("GetVolume", args); - return (int)(ushort)args[2].DataValue; - } - - public void SetVolume(int volume) { - if (device == null) return; - var service = device.GetServices(UPNP_SERVICE_CONTROL)[0]; - - UPnPArgument[] args = new UPnPArgument[] { - new UPnPArgument("InstanceID", (uint)0), - new UPnPArgument("Channel", "Master"), - new UPnPArgument("DesiredVolume", (ushort)volume) - }; - - service.InvokeSync("SetVolume", args); - } - - public void VolumeIncrease() { - SetVolume(GetVolume() + 1); - } - - public void VolumeDecrease() { - SetVolume(GetVolume() - 1); - } - - public bool GetMute() { - if (device == null) return false; - var service = device.GetServices(UPNP_SERVICE_CONTROL)[0]; - - UPnPArgument[] args = new UPnPArgument[] { - new UPnPArgument("InstanceID", (uint)0), - new UPnPArgument("Channel", "Master"), - new UPnPArgument("CurrentMute", null) - }; - - service.InvokeSync("GetMute", args); - return (bool)args[2].DataValue; - } - - public void SetMute(bool mute) { - if (device == null) return; - var service = device.GetServices(UPNP_SERVICE_CONTROL)[0]; - - UPnPArgument[] args = new UPnPArgument[] { - new UPnPArgument("InstanceID", (uint)0), - new UPnPArgument("Channel", "Master"), - new UPnPArgument("DesiredMute", mute) - }; - - service.InvokeSync("SetMute", args); - } - - public void MuteToggle() { - SetMute(!GetMute()); - } - - public void Play() { - if (device == null) return; - var service = device.GetServices(UPNP_SERVICE_AVTRANSPORT)[0]; - - UPnPArgument[] args = new UPnPArgument[] { - new UPnPArgument("InstanceID", (uint)0), - new UPnPArgument("Speed", "1") - }; - - service.InvokeSync("Play", args); - } - - public void Pause() { - if (device == null) return; - var service = device.GetServices(UPNP_SERVICE_AVTRANSPORT)[0]; - - UPnPArgument[] args = new UPnPArgument[] { - new UPnPArgument("InstanceID", (uint)0) - }; - - service.InvokeSync("Pause", args); - } - - public void Stop() { - if (device == null) return; - var service = device.GetServices(UPNP_SERVICE_AVTRANSPORT)[0]; - - UPnPArgument[] args = new UPnPArgument[] { - new UPnPArgument("InstanceID", (uint)0) - }; - - service.InvokeSync("Stop", args); - } - - public void SetAVTransportURI() { - if (device == null) return; - var service = device.GetServices(UPNP_SERVICE_AVTRANSPORT)[0]; - - UPnPArgument[] args = new UPnPArgument[] { - new UPnPArgument("InstanceID", (uint)0), - new UPnPArgument("CurrentURI", DirectoryServer.S1), - new UPnPArgument("CurrentURIMetaData", null) - }; - - service.InvokeSync("SetAVTransportURI", args); - } - - public void Playback() { - SetAVTransportURI(); - Play(); - } - } -} +// Copyright 2014 Mario Guggenberger +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +using System; +using System.Collections.Generic; +using System.Text; +using OpenSource.UPnP; +using System.Web; + +namespace LocalAudioBroadcast { + class ControlPoint { + + private const string UPNP_DEVICE_RENDERER = "urn:schemas-upnp-org:device:MediaRenderer:1"; + private const string UPNP_SERVICE_AVTRANSPORT = "urn:schemas-upnp-org:service:AVTransport:1"; + private const string UPNP_SERVICE_CONTROL = "urn:schemas-upnp-org:service:RenderingControl:1"; + + private UPnPSmartControlPoint scp; + private UPnPDevice device; + + public event UPnPSmartControlPoint.DeviceHandler OnAddedDevice; + public event UPnPSmartControlPoint.DeviceHandler OnRemovedDevice; + + public ControlPoint() { + scp = new UPnPSmartControlPoint(null, null, UPNP_DEVICE_RENDERER); + scp.OnAddedDevice += new UPnPSmartControlPoint.DeviceHandler(ControlPoint_OnAddedDevice); + scp.OnRemovedDevice += new UPnPSmartControlPoint.DeviceHandler(ControlPoint_OnRemovedDevice); + } + + public void Rescan() { + scp.Rescan(); + } + + private void ControlPoint_OnAddedDevice(UPnPSmartControlPoint sender, UPnPDevice device) { + Console.WriteLine("OnAddedDevice(" + device.FriendlyName + ", " + device.RemoteEndPoint + ", " + device.UniqueDeviceName + ")"); + + foreach (UPnPService service in device.Services) { + Console.WriteLine(device.FriendlyName + " service: " + service.ServiceURN); + } + + // only add device if necessary services are available + if (device.GetServices(UPNP_SERVICE_CONTROL).Length > 0 && device.GetServices(UPNP_SERVICE_AVTRANSPORT).Length > 0) { + if (OnAddedDevice != null) + OnAddedDevice(sender, device); + } + } + + private void ControlPoint_OnRemovedDevice(UPnPSmartControlPoint sender, UPnPDevice device) { + Console.WriteLine("OnRemovedDevice(" + device.FriendlyName + ", " + device.RemoteEndPoint + ", " + device.UniqueDeviceName + ")"); + + if (this.device != null && this.device.UniqueDeviceName == device.UniqueDeviceName) { + this.device = null; + } + + if (OnRemovedDevice != null) OnRemovedDevice(sender, device); + } + + public UPnPDevice Device { + get { return device; } + set { device = value; } + } + + public int GetVolume() { + if (device == null) return -1; + var service = device.GetServices(UPNP_SERVICE_CONTROL)[0]; + + UPnPArgument[] args = new UPnPArgument[] { + new UPnPArgument("InstanceID", (uint)0), + new UPnPArgument("Channel", "Master"), + new UPnPArgument("CurrentVolume", null) + }; + + service.InvokeSync("GetVolume", args); + return (int)(ushort)args[2].DataValue; + } + + public void SetVolume(int volume) { + if (device == null) return; + var service = device.GetServices(UPNP_SERVICE_CONTROL)[0]; + + UPnPArgument[] args = new UPnPArgument[] { + new UPnPArgument("InstanceID", (uint)0), + new UPnPArgument("Channel", "Master"), + new UPnPArgument("DesiredVolume", (ushort)volume) + }; + + service.InvokeSync("SetVolume", args); + } + + public void VolumeIncrease() { + SetVolume(GetVolume() + 1); + } + + public void VolumeDecrease() { + SetVolume(GetVolume() - 1); + } + + public bool GetMute() { + if (device == null) return false; + var service = device.GetServices(UPNP_SERVICE_CONTROL)[0]; + + UPnPArgument[] args = new UPnPArgument[] { + new UPnPArgument("InstanceID", (uint)0), + new UPnPArgument("Channel", "Master"), + new UPnPArgument("CurrentMute", null) + }; + + service.InvokeSync("GetMute", args); + return (bool)args[2].DataValue; + } + + public void SetMute(bool mute) { + if (device == null) return; + var service = device.GetServices(UPNP_SERVICE_CONTROL)[0]; + + UPnPArgument[] args = new UPnPArgument[] { + new UPnPArgument("InstanceID", (uint)0), + new UPnPArgument("Channel", "Master"), + new UPnPArgument("DesiredMute", mute) + }; + + service.InvokeSync("SetMute", args); + } + + public void MuteToggle() { + SetMute(!GetMute()); + } + + public void Play() { + if (device == null) return; + var service = device.GetServices(UPNP_SERVICE_AVTRANSPORT)[0]; + + UPnPArgument[] args = new UPnPArgument[] { + new UPnPArgument("InstanceID", (uint)0), + new UPnPArgument("Speed", "1") + }; + + service.InvokeSync("Play", args); + } + + public void Pause() { + if (device == null) return; + var service = device.GetServices(UPNP_SERVICE_AVTRANSPORT)[0]; + + UPnPArgument[] args = new UPnPArgument[] { + new UPnPArgument("InstanceID", (uint)0) + }; + + service.InvokeSync("Pause", args); + } + + public void Stop() { + if (device == null) return; + var service = device.GetServices(UPNP_SERVICE_AVTRANSPORT)[0]; + + UPnPArgument[] args = new UPnPArgument[] { + new UPnPArgument("InstanceID", (uint)0) + }; + + service.InvokeSync("Stop", args); + } + + public void SetAVTransportURI() { + if (device == null) return; + var service = device.GetServices(UPNP_SERVICE_AVTRANSPORT)[0]; + + /* + * Why does an empty/null CurrentURIMetaData not work with all renderers? + * + * ---------------------------------------------------------------------- + * + * UPnP AVTransport:3 Service (2013, for UPnP 1.0) + * http://upnp.org/specs/av/av4/ + * + * 2.4.1 SetAVTransportURI + * + * A control point can supply metadata associated with the specified resource, using + * a DIDL-Lite XML Fragment (defined in the ContentDirectory service specification), + * in argument CurrentURIMetaData. + * If a control point does not want to use this feature it can supply the empty string * for the CurrentURIMetaData argument. + * + * ---------------------------------------------------------------------- + * + * DLNA Part 1-1 (March 2014) + * http://www.dlna.org/dlna-for-industry/guidelines + * + * 7.4.1.6.8.3 + * + * This is a mandate over what UPnP normally allows as optional behavior. + * + * For a Push Controller (+PU+) that does not contain a CDS, the DIDL-Lite metadata + * can typically be created from a DIDL-Lite XML fragment template containing only + * the minimal properties as described in 7.4.1.6.14.9 or example, since the @id + * property value only needs to be unique within the scope of the DIDL-Lite XML + * fragment, the @id property value can be any value chosen by the Push Controller; + * the @parent property can have a value of −1, and the @restricted property value + * can be either 0 or 1. + * + * 7.4.1.6.14.9 + * + * If a UPnP AV MediaRenderer control point specifies a value for the CurrentURIMetaData + * argument of an AVT:SetAVTransportURI request, then the control point shall follow + * these restrictions for the value of the CurrentURIMetaData argument, as follows: + * + * - compliant with the DIDL-Lite schema; + * - exactly one element; + * - exactly one or element; + * - exactly one element and value; + * - a minimum of zero and a maximum of one element and value; + * - exactly one element and value; + * - a minimum of one element. + * + * All other XML elements are permitted as long as they are properly declared with + * their namespaces. + * + * The provided metadata shall represent the metadata of the content indicated by the + * CurrentURI input argument. One of the elements shall be the element that + * contains the URI specified in the CurrentURI input argument. + * + * ---------------------------------------------------------------------- + * + * Conclusion: + * Some renderers are UPnP certified and accept empty metadata, others are + * DLNA specified and demand the metadata. + */ + + var defaultItem = DirectoryServer.Directory.GetItem(0); + + UPnPArgument[] args = new UPnPArgument[] { + new UPnPArgument("InstanceID", (uint)0), + new UPnPArgument("CurrentURI", defaultItem.Uri), + new UPnPArgument("CurrentURIMetaData", defaultItem.GetDidl()) + }; + + service.InvokeSync("SetAVTransportURI", args); + } + + public void Playback() { + SetAVTransportURI(); + Play(); + } + } +} diff --git a/LocalAudioBroadcast/DidlUtil.cs b/LocalAudioBroadcast/DidlUtil.cs index 472d54f..3b6c937 100644 --- a/LocalAudioBroadcast/DidlUtil.cs +++ b/LocalAudioBroadcast/DidlUtil.cs @@ -1,4 +1,5 @@ -// Copyright 2014 Mario Guggenberger +using NAudio.Wave; +// Copyright 2014 Mario Guggenberger // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -52,5 +53,19 @@ public static string EndDidl() { return ""; } + public static String GenerateCaptureDeviceItem(int itemId, CaptureDevice captureDevice, String uri) { + int sampleRate = captureDevice.MMDevice.AudioClient.MixFormat.SampleRate; + /* The channels and bitDepth are fixed, as requested from the WasapiLoopbackCapture2 + * in LoopbackModule.Start(). */ + int channels = 2; + int bitDepth = 16; + int bitRate = sampleRate * channels * (bitDepth / 2) * 8; + + return DidlUtil.GetMusicItem(itemId + "", "0", "1", + captureDevice.Name, "N/A", "N/A", "N/A", "0", + bitRate + "", sampleRate + "", channels + "", bitDepth + "", + StreamingFormat.DefaultFormat.GetNetworkFormatDescriptor(sampleRate, channels), + uri, "object.item.audioItem.musicTrack"); + } } } diff --git a/LocalAudioBroadcast/Directory.cs b/LocalAudioBroadcast/Directory.cs new file mode 100644 index 0000000..01977e2 --- /dev/null +++ b/LocalAudioBroadcast/Directory.cs @@ -0,0 +1,80 @@ +// Copyright 2014 Mario Guggenberger +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +using NAudio.Wave; +using System; +using System.Collections.Generic; +using System.Text; + +namespace LocalAudioBroadcast { + class Directory { + + private List items; + + public Directory(string baseUri) { + Init(baseUri); + } + + public void Init(string baseUri) { + items = new List(); + + CaptureDevice defaultDevice = WasapiLoopbackCapture2.GetDefaultLoopbackCaptureDevice(); + List devices = WasapiLoopbackCapture2.GetLoopbackCaptureDevices(); + + int itemId = 0; + items.Add(new Item { + Uri = baseUri, + Definition = DidlUtil.GenerateCaptureDeviceItem(++itemId, defaultDevice, baseUri) + }); + + int deviceId = 0; + foreach (CaptureDevice captureDevice in devices) { + if (captureDevice != defaultDevice) { + string uri = baseUri + "?id=" + deviceId; + items.Add(new Item { + Uri = uri, + Definition = DidlUtil.GenerateCaptureDeviceItem(++itemId, captureDevice, uri) + }); + } + deviceId++; + } + } + + public uint Count { + get { return (uint)items.Count;} + } + + public string GetDirectoryDidl() { + string list = ""; + + foreach (Item item in items) { + list += item.Definition; + } + + return DidlUtil.BeginDidl() + list + DidlUtil.EndDidl(); + } + + public Item GetItem(int index) { + return items[index]; + } + + public class Item { + public string Uri { get; set; } + public string Definition { get; set; } + + public string GetDidl() { + return DidlUtil.BeginDidl() + Definition + DidlUtil.EndDidl(); + } + } + } +} diff --git a/LocalAudioBroadcast/DirectoryServer.cs b/LocalAudioBroadcast/DirectoryServer.cs index 26b639c..5409a4e 100644 --- a/LocalAudioBroadcast/DirectoryServer.cs +++ b/LocalAudioBroadcast/DirectoryServer.cs @@ -19,9 +19,6 @@ using OpenSource.UPnP; using System.Web; using System.Net; -using NAudio.Wave; -using NAudio.CoreAudioApi; -using System.Collections.Generic; using System.Net.Sockets; namespace LocalAudioBroadcast @@ -78,8 +75,9 @@ public DirectoryServer() ContentDirectory.Evented_SystemUpdateID = 0; } - public static string S1, S2; - public static uint S2count; + // TODO: THESE STATIC VARS ARE AN UGLY HACK + public static string S1; + public static Directory Directory; public void Start() { @@ -104,40 +102,8 @@ public void Start() S1 = HttpBaseURL + "capture"; - S2 = DidlUtil.BeginDidl(); - - CaptureDevice defaultDevice = WasapiLoopbackCapture2.GetDefaultLoopbackCaptureDevice(); - List devices = WasapiLoopbackCapture2.GetLoopbackCaptureDevices(); - - int itemId = 0; - S2 += GenerateCaptureDeviceEntry(++itemId, defaultDevice, S1); - - int deviceId = 0; - foreach (CaptureDevice captureDevice in devices) { - if (captureDevice != defaultDevice) { - S2 += GenerateCaptureDeviceEntry(++itemId, captureDevice, S1 + "?id=" + deviceId); - } - deviceId++; - } - - S2 += DidlUtil.EndDidl(); - S2count = (uint)itemId; + Directory = new Directory(S1); } - - private String GenerateCaptureDeviceEntry(int itemId, CaptureDevice captureDevice, String url) { - int sampleRate = captureDevice.MMDevice.AudioClient.MixFormat.SampleRate; - /* The channels and bitDepth are fixed, as requested from the WasapiLoopbackCapture2 - * in LoopbackModule.Start(). */ - int channels = 2; - int bitDepth = 16; - int bitRate = sampleRate * channels * (bitDepth / 2) * 8; - - return DidlUtil.GetMusicItem(itemId+"", "0", "1", - captureDevice.Name, "N/A", "N/A", "N/A", "0", - bitRate + "", sampleRate + "", channels + "", bitDepth + "", - StreamingFormat.DefaultFormat.GetNetworkFormatDescriptor(sampleRate, channels), - url, "object.item.audioItem.musicTrack"); - } public void Stop() { @@ -207,9 +173,9 @@ public void ContentDirectory_Browse(System.String ObjectID, DvContentDirectory.E if (BrowseFlag == DvContentDirectory.Enum_A_ARG_TYPE_BrowseFlag.BROWSEDIRECTCHILDREN) { switch (ObjectID) { case "0": // root - NumberReturned = S2count; - TotalMatches = S2count; - Result = S2; + NumberReturned = Directory.Count; + TotalMatches = Directory.Count; + Result = Directory.GetDirectoryDidl(); break; } } diff --git a/LocalAudioBroadcast/LocalAudioBroadcast.csproj b/LocalAudioBroadcast/LocalAudioBroadcast.csproj index 79cb5fa..703a340 100644 --- a/LocalAudioBroadcast/LocalAudioBroadcast.csproj +++ b/LocalAudioBroadcast/LocalAudioBroadcast.csproj @@ -102,6 +102,7 @@ + diff --git a/LocalAudioBroadcast/MainForm.cs b/LocalAudioBroadcast/MainForm.cs index ce1bde4..b64ef5d 100644 --- a/LocalAudioBroadcast/MainForm.cs +++ b/LocalAudioBroadcast/MainForm.cs @@ -166,6 +166,9 @@ private void rbFormat_CheckedChanged(object sender, EventArgs e) { StreamingFormat.DefaultFormat = format; Settings.Default.StreamingFormat = format.Id; Settings.Default.Save(); + + // reinitialize directory after format change to reflect the current format in the item XMl definitions + DirectoryServer.Directory.Init(DirectoryServer.S1); } } }