Skip to content
Shiny.Net.HttpServer v1 - A lightweight feature rich HTTP Server - Tunnels, Websockets, AOT, ASPNET Featureset, & Works EVERYWHERE!Let me see!

SSDP & UPnP

SSDP is how UPnP devices announce themselves: routers, DLNA media servers, Sonos speakers, Roku boxes, smart TVs, and most consumer network hardware. Register AddSsdp() and inject ISsdpManager.

builder.Services.AddSsdp();

The quickest way to see what is on the network:

var devices = await ssdp.SearchAll(TimeSpan.FromSeconds(5), ct);
foreach (var device in devices)
Console.WriteLine($"{device.Udn} {device.Server} {device.Location}");

SearchAll sends ssdp:all, which every device answers once per advertisement — a root device with two embedded devices and three services replies eight times. They are folded into one SsdpDevice per UDN for you, but it is a lot of traffic. To list devices without their internals:

var roots = await ssdp.Search(SsdpConstants.RootDevice, TimeSpan.FromSeconds(5), ct);

You can also search for a specific device or service type:

var servers = await ssdp.Search("urn:schemas-upnp-org:device:MediaServer:1", ct: ct);

Version matching follows the UPnP rule — a device implementing version 2 answers a search for version 1.

Browse never completes on its own. It repeats the search periodically, listens for unsolicited ssdp:alive and ssdp:byebye announcements in between, and expires devices whose advertisements age out.

var devices = new Dictionary<string, SsdpDevice>();
await foreach (var result in ssdp.Browse(SsdpConstants.RootDevice, ct))
{
switch (result.Status)
{
case SsdpBrowseStatus.Found:
// upsert - a device re-announcing itself emits Found again
devices[result.Device.Udn] = result.Device;
break;
case SsdpBrowseStatus.Lost:
// only Udn is populated on a Lost result
devices.Remove(result.Device.Udn);
break;
}
}

Always key on Udn. A dual-homed or dual-stack device advertises several locations and answers from several source addresses for one physical box; the UDN is the only stable identity.

new SsdpBrowseConfig("urn:schemas-upnp-org:device:MediaServer:1")
{
MaxWait = TimeSpan.FromSeconds(3), // the MX header, clamped to 1-5s
SearchInterval = TimeSpan.FromSeconds(60), // how often the search repeats
ListenForNotifications = true, // also track alive/byebye between searches
FriendlyName = "My App" // sent as CPFN.UPNP.ORG
}

Do not drop MaxWait below 2 seconds to make a scan feel snappier. It does not speed anything up — it just tells slow embedded devices to answer within a window they cannot meet.

An SSDP advertisement carries almost no information — a UDN, a SERVER string, and a URL. Anything human-readable lives in the description document:

var description = await ssdp.GetDescription(device, ct: ct);
Console.WriteLine(description.FriendlyName); // "Living Room"
Console.WriteLine(description.Manufacturer);
Console.WriteLine(description.ModelName);
foreach (var service in description.Services)
Console.WriteLine(service.ServiceType);
foreach (var icon in description.Icons)
Console.WriteLine($"{icon.Width}x{icon.Height} {icon.Url}");

Embedded devices share the parent’s document and each carry their own UDN:

foreach (var d in description.Flatten())
Console.WriteLine($"{d.Udn} {d.FriendlyName}");

Fetch once and cache it. Re-announcements are frequent, and the document only changes when SsdpDevice.ConfigId changes.

Descriptions are also capped at 512KB, time out after 10 seconds, do not follow redirects, and are parsed with DTD processing disabled.

Advertise your own device. You are responsible for actually serving the description document at the location you advertise — this only announces that it exists.

await using var publication = await ssdp.Publish(
new SsdpDeviceRegistration("uuid:" + Guid.NewGuid(), new Uri("http://192.168.1.20:8080/desc.xml"))
{
DeviceType = "urn:schemas-upnp-org:device:MediaServer:1",
ServiceTypes = ["urn:schemas-upnp-org:service:ContentDirectory:1"],
MaxAge = TimeSpan.FromMinutes(30)
},
ct
);

This advertises four things — upnp:rootdevice, the UDN, the device type, and the service type — sends each announcement three times as UDP requires, re-announces before MaxAge expires, increments its boot id and re-announces when the host’s addresses change, and answers matching M-SEARCH requests. Disposing sends ssdp:byebye for every advertised type.

Keep the UDN stable across restarts. Control points treat it as the device’s identity.

Member Notes
Udn The device identity — key your collections on this
Location Description URL; null when only a goodbye has been seen
Server The raw SERVER header, often the only clue before fetching the description
NotificationTypes Every advertisement seen from this device
IsRootDevice, DeviceType, ServiceTypes Computed from NotificationTypes
SourceAddress, InterfaceIndex Where it was seen
BootId, ConfigId UPnP 1.1 only. A ConfigId change means re-fetch the description
SearchPort Set when the device does not listen for unicast searches on 1900
Headers Every raw header, case-insensitive — vendors put useful things here

Udn, DeviceType, FriendlyName, Manufacturer, ManufacturerUrl, ModelName, ModelNumber, ModelDescription, ModelUrl, SerialNumber, Upc, PresentationUrl, Icons, Services, Devices, and Flatten(). All URLs are resolved to absolute against the document’s base.

ServiceType, ServiceId, ScpdUrl, ControlUrl, EventSubscriptionUrl.

  • No SOAP action invocation. ControlUrl is surfaced so you can drive your own SOAP client, but there is no Invoke, no argument marshalling, and no SCPD action metadata.
  • No GENA eventing. EventSubscriptionUrl is surfaced; there is no subscription, no callback listener, and no state-variable notification handling.

Both are substantially larger projects than discovery, and eventing in particular requires an embedded HTTP server on every platform.