Shiny.Push
5.6.3
Prefix Reserved
There is a newer prerelease version of this package available.
See the version list below for details.
See the version list below for details.
dotnet add package Shiny.Push --version 5.6.3
NuGet\Install-Package Shiny.Push -Version 5.6.3
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Shiny.Push" Version="5.6.3" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Shiny.Push" Version="5.6.3" />
<PackageReference Include="Shiny.Push" />
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Shiny.Push --version 5.6.3
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
#r "nuget: Shiny.Push, 5.6.3"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Shiny.Push@5.6.3
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Shiny.Push&version=5.6.3
#tool nuget:?package=Shiny.Push&version=5.6.3
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
Shiny Client for .NET
<img src="https://github.com/shinyorg/shiny/raw/master/art/logo.png" width="100" />
Shiny is a cross-platform framework designed to make working with device services and background processes easy, testable, and consistent while bringing things like dependency injection & logging in a structured way to your code!
Features
- Handles all of the cruft like Permissions, main thread traversal, persistent storage and app restarts
- Brings your infrastructure to the background
- Provides logging to ensure you know when your services fail in the background
- Gives a clean & testable API surface for your code
- Native AOT & trim-friendly across all modules
- Cross-platform: iOS, Android, tvOS, Mac Catalyst, macOS, Windows, Linux, and Blazor WebAssembly (where the platform allows)
Modules
- Background Jobs - periodic background work backed by
BGTaskScheduler(iOS/tvOS),WorkManager(Android), COM-activated background tasks (Windows), and an in-process managed runner for Linux/macOS/Blazor WASM - HTTP Transfers - resumable background uploads/downloads on
NSURLSession(iOS/tvOS), anHttpClient-driven managed loop with Range-based resume (Android, Windows, Linux, macOS, .NET base), and Service Worker Background Sync (Blazor WASM). Pause/resume support - pause stops a transfer without cancelling it (downloads continue from where they left off, uploads restart). First-class Azure Blob Storage and AWS S3 (SigV4) request builders included.AddTransferProgress()puts progress in front of the user - an iOS Live Activity, or the Android foreground-service notification promoted to an Android 16 live update - from one manager, with no code in your transfer delegate - Data Sync - bidirectional JSON record sync over HTTP with the same platform tiers as HTTP Transfers: outbox + inbox on
NSURLSession(iOS/tvOS/Mac Catalyst), Foreground Service + HttpClient (Android),HttpClient+ connectivity loop (Windows/Linux/macOS), and LocalStorage-backed HttpClient (Blazor WASM). Includes batched outbox, tombstones, conflict resolution, retry with exponential backoff, and AOT-safe serialization throughShiny.Json - Network Discovery - three local-network discovery protocols in one package, each with browse, resolve, and publish
- mDNS/DNS-SD (Bonjour/Zeroconf) - backed by
NSNetServiceon iOS/tvOS/Mac Catalyst/macOS (so nocom.apple.developer.networking.multicastentitlement is needed),NsdManageron Android (no multicast lock), and a dependency-free managed responder on UDP 5353 for Windows, Linux, macOS console, and server .NET.await foreach (var r in mdns.Browse("_http._tcp", ct))for live discovery,BrowseOnce(...)for a one-shot scan, andPublish(...)to advertise your own service with TXT metadata - SSDP/UPnP - find routers, media servers, Sonos, Roku, and smart TVs.
ssdp.SearchAll()for a one-shot sweep,Browse(...)for a live list keyed on UDN withssdp:alive/byebyetracking and BOOTID-aware expiry,GetDescription(...)to fetch and parse the device description (friendly name, model, icons, service list), andPublish(...)to advertise your own root device. Discovery and description only - no SOAP action invocation, no GENA eventing - WS-Discovery - find ONVIF cameras, WSD printers/scanners, and Windows machines. Probe/Resolve/Hello/Bye on both the 2005 (ONVIF/Windows) and 2009 (OASIS) profiles, with correct QName-prefix resolution for
Types, RFC 3986 segment-prefix scope matching, andProbeOnvifCameras(...)as a shortcut - Platform requirements differ by protocol. mDNS needs no entitlement anywhere. SSDP and WS-Discovery have no OS-level API on any platform, so they use raw multicast: iOS requires the Apple-approved
com.apple.developer.networking.multicastentitlement, Android needsCHANGE_WIFI_MULTICAST_STATE(the multicast lock is acquired for you) plusACCESS_LOCAL_NETWORKfrom Android 17. Sandboxed hosts still need their usual network permission -com.apple.security.network.client/.serveron Mac Catalyst & macOS,privateNetworkClientServerfor packaged Windows apps. A missing one throwsDiscoveryPermissionExceptionnaming exactly what to add, rather than silently finding nothing
- mDNS/DNS-SD (Bonjour/Zeroconf) - backed by
- Wi-Fi - scan for access points, join and leave networks, manage the networks the device has saved, watch the current network's SSID/signal/IP/DNS, and host a hotspot
- Platform reach is genuinely uneven, and the API says so rather than pretending otherwise. Every manager publishes a
WifiCapabilitiesflags property; anything unavailable throwsWifiNotSupportedExceptionnaming the specific limit (an Apple entitlement, an Android API level that revoked the call, a platform with no such concept). Check the flag to branch, catch the exception as a backstop IWifiManager-Scan(ct)returns oneWifiNetworkper BSSID with SSID, security scheme, dBm + 0-100 signal, frequency, band and channel.Connect(new WifiConnectionRequest(ssid) { Passphrase = ... })joins and waits for DHCP rather than returning on association, and withRememberleft on also persists the network for later (aWifiNetworkSuggestionon Android 11+, an ordinary profile elsewhere).await GetCurrentNetwork(ct)reports the joined network - SSID, BSSID, security, signal, plus every IP, DNS resolver, gateway and mask - andChangedfires with the newWifiNetworkInfo?(null when Wi-Fi drops), de-duplicated so the chatty native watchers behind it do not leak through, delivering the current network once on subscribe. Reading the SSID is asynchronous because both phone platforms stopped answering synchronously: iOS 14 replacedCNCopyCurrentNetworkInfowithNEHotspotNetwork.fetchCurrent, and Android 12 redacts the SSID and BSSID out of every pull-style read no matter what permissions are held, handing them out only through aNetworkCallbackregistered withFLAG_INCLUDE_LOCATION_INFO- Known networks -
GetKnownNetworks()lists what the device has saved asKnownWifiNetwork(opaque platformId, SSID, security, hidden),Forget(id)deletes one, andConnect(id)rejoins one without handing the passphrase over again. The scope differs and the API says which: iOS/Mac Catalyst and Android only ever disclose your own app's entries (NEHotspotConfigurationManager.getConfiguredSSIDs, network suggestions), while Windows, macOS and Linux hand back every profile on the machine.Connect(id)works on Windows, macOS, Linux and Android below API 29 - iOS and modern Android treat a saved network as a standing hint the OS acts on, with no call to force it. TheIdis the platform's own handle: a NetworkManager connection UUID on Linux, a numeric network id on legacy Android, the SSID everywhere else IWifiHotspot-Start(...)returns anIHotspotSessioncarrying the SSID and passphrase actually in use; dispose it to bring the access point down.GetClients()lists joined devices with MAC and address on Windows and Linux. Android raises a local-only hotspot (clients reach the device, not the internet) and picks the SSID/passphrase itself, Windows shares the machine's internet connection, Linux runs NetworkManager AP mode with DHCP + NAT, and iOS/macOS have no hotspot API at all- Backends -
WifiManager+ConnectivityManageron Android (specifier-based joins from API 29, legacyWifiConfigurationbelow),NEHotspotConfiguration+ CaptiveNetwork on iOS/Mac Catalyst, CoreWLAN on macOS,WiFiAdapter+NetworkOperatorTetheringManager+Radioon Windows, and NetworkManager over D-Bus on Linux (separateShiny.Net.Wifi.Linuxpackage). Saved profiles are outside WinRT entirely, so Windows reacheswlanapi.dlldirectly for those three calls. The plain .NET target still reports IP/DNS off the wireless interface and raisesChanged; the Wi-Fi-specific calls throw - Permissions - Android needs
ACCESS_WIFI_STATE,CHANGE_WIFI_STATE,ACCESS_FINE_LOCATIONandNEARBY_WIFI_DEVICES(API 33+); iOS needs the Hotspot Configuration and Access WiFi Information capabilities plusNSLocationWhenInUseUsageDescription; macOS needs the location usage description; Windows needswiFiControlandradios; Linux gates the mutating calls behind polkit. A scan that comes back empty because location was refused throwsWifiPermissionExceptioninstead of looking like an empty neighbourhood
- Platform reach is genuinely uneven, and the API says so rather than pretending otherwise. Every manager publishes a
- Screen Recording - record the screen to an MP4 with optional microphone and system audio
- What "the screen" means is not the same everywhere, and the API says so rather than pretending otherwise. Android, macOS, Windows and Linux record the system screen, so other apps end up in the file. iOS and Mac Catalyst record your own app's UI only - that is all ReplayKit offers without a Broadcast Upload Extension, which a NuGet package cannot deliver. Every recorder publishes a
ScreenRecorderCapabilitiesflags property; anything unavailable throwsScreenRecorderNotSupportedExceptionnaming the specific limit. The flags are read off the instance because they differ within a platform: macOS 15 gains microphone capture and loses pause (SCRecordingOutputwrites the file itself and cannot be detached mid-recording) where macOS 12.3-14 has it the other way round IScreenRecorder-await recorder.Start(new ScreenRecordingRequest { IncludeMicrophone = true, MaxWidth = 1280 })returns anIScreenRecordingthat does not come back until frames are genuinely being written - so a consent dialog, a compositor picker or an Android foreground-service promotion all complete first.Pause()/Resume()close the gap in the timeline rather than leaving a frozen stretch,Stop()returns aScreenRecordingResultwith the path, duration, size and theMimeTypeactually produced (browsers disagree - Safari and Chrome give MP4, Firefox gives WebM), and disposing without stopping cancels and deletes the partial file. A request asking for something outsideCapabilitiesthrows before any native call, because a recording that silently came out without the microphone is worse than one that refused to startGetTargets()lists displays, windows and applications on macOS and Windows, so an app can offer its own picker. Linux and the browser hand selection to the compositor and throw here - their picker appears duringStartinstead- The OS ending it on its own is a first-class outcome.
IScreenRecording.Faultedfires with a reason - the user hit Android's cast notification or the browser's "Stop sharing" bar, iOS took the screen for an incoming call, a monitor was unplugged, the encoder failed - and carries whatever was salvaged, so a recording cut short still yields a playable file rather than a silent truncation.MaxDurationstops cleanly and reports the same way - Backends - MediaProjection + MediaCodec/MediaMuxer on Android (MediaRecorder takes a single audio source and playback capture is not one of them, so app audio forces the whole pipeline down to MediaCodec), ReplayKit
startCaptureinto anAVAssetWriteron iOS/tvOS/Mac Catalyst (startRecordingonly surrenders its file through a share sheet, which is no use to a library), ScreenCaptureKit on macOS -SCRecordingOutputon 15+ and the sameAVAssetWriterbelow it - Windows.Graphics.Capture into aMediaStreamSourcetranscoded by Media Foundation on Windows, and the xdg-desktop-portal ScreenCast API drivinggst-launch-1.0orffmpegon Linux (separateShiny.ScreenRecorder.Linuxpackage). Blazor WebAssembly usesgetDisplayMedia+MediaRecorder(separateShiny.ScreenRecorder.Blazorpackage) and is the only platform where pause is native and the result has no file path - there is no filesystem, soOpenRead()is the portable accessor - Permissions - Android needs
FOREGROUND_SERVICE,FOREGROUND_SERVICE_MEDIA_PROJECTIONandRECORD_AUDIOfor audio, plus a per-recording consent dialog that cannot be pre-granted; iOS needsNSMicrophoneUsageDescriptionand the app in the foreground; macOS needs the Screen Recording grant (and a relaunch after it is first given); Windows needs 1903+ and thegraphicsCapturecapability when packaged; Linux needs a portal plus GStreamer or FFmpeg, all probed at runtime so a machine missing them reportsNonerather than failing at record time. Windows has no audio at all -Windows.Graphics.Capturecaptures pixels only
- What "the screen" means is not the same everywhere, and the API says so rather than pretending otherwise. Android, macOS, Windows and Linux record the system screen, so other apps end up in the file. iOS and Mac Catalyst record your own app's UI only - that is all ReplayKit offers without a Broadcast Upload Extension, which a NuGet package cannot deliver. Every recorder publishes a
- BluetoothLE Client - scan, connect, GATT, and L2CAP CoC on iOS/tvOS/macOS, Android, Windows, Linux (BlueZ), and Blazor WebAssembly (Web Bluetooth). L2CAP file transfers in both directions -
await peripheral.UploadFile(psm, path, onProgress: ...)/DownloadFile(...)- with the same transfer metrics as HTTP Transfers (percent complete, bytes/sec, ETA), plus Rx*WithProgressvariants.IPeripheral.Mtureports the usable payload per GATT operation (the negotiated ATT MTU minus the 3-byte ATT header) on every platform - fragment writes to it directly. Writes without response on Apple wait on CoreBluetooth's flow control without ever missing its ready callback, so a full send buffer paces the stream instead of wedging the peripheral's operation queue.ConnectionConfig(AutoConnect: true)- the default - keeps the link up on every platform, re-establishing it after a peripheral drops out of range or is power-cycled. On iOS/Mac Catalyst/macOS and Android it also survives the user toggling Bluetooth off and on - an adapter power-down runs the same teardown a real disconnect gets, soWhenStatusChanged()never lies about a dead link, connects issued while the adapter is off are parked and replayed rather than silently dropped, and a scan started while the link is down no longer evicts the peripheral waiting to reconnect.Scan()is parked the same way: on Apple platforms a CoreBluetooth central reportsUnknownuntil it finishes powering on, and a scan issued into that window used to be dropped without a word - it now starts as soon as the adapter is up, and resumes by itself after a power cycle.CancelConnection()stays final - BluetoothLE Hosting - GATT server, advertising, iBeacon broadcasting, and L2CAP CoC listeners on iOS/macOS, Android, Windows, and Linux (BlueZ over D-Bus, plus AF_BLUETOOTH sockets for L2CAP).
OpenL2CapFileServer(rootDirectory)turns a PSM into a file server that connected centrals can push to and pull from, with size limits, per-request authorization, path-traversal protection, and progress callbacks.AddService(...)andStartAdvertising(...)wait out the peripheral manager's power-on handshake before talking to CoreBluetooth, so calling either at startup completes instead of hanging on a callback the OS never sends.Notify(...)on Apple waits out a full transmit queue and retries until CoreBluetooth accepts the value, rather than hanging on a missed ready callback or silently dropping the notification.Notify(data, cancellationToken, ...centrals)stops waiting when the token is cancelled or Bluetooth powers off, and thecentralsyou name are honoured on every platform - Apple used to send to every subscriber regardless, and Android sent a plainNotify(data)to nobody. Subscribers are tracked whether or not you pass a subscribe hook, so generated request/response replies reach the central that wrote. On AndroidNotifywaits for each central'sonNotificationSentbefore sending it the next value, so a burst is paced instead of silently losing everything after the first, and a central that enabled indications now gets indications. A bundled source generator turns[BleService]/[L2CapService]partial classes into the imperative registrations for you - handler signatures bind by type, GATT status and offset handling is generated, and each connected central gets a partial{Service}Contextyou can stamp your own properties onto (all reflection-free, so it stays AOT-safe).AdvertisementOptionsnow carriesServiceData,ManufacturerData,IsConnectableandIncludeTxPower, so a peripheral can put an arbitrary payload on the air (Android, Windows and Linux - Apple'sstartAdvertisingreads only a local name and service UUIDs, and throws rather than silently dropping the rest).AdvertiseBeacon(...)is now correct on every platform: it built the packet fromGuid.ToByteArray()andBitConverter.GetBytes(), both little-endian, and shipped a byte-swapped UUID, major and minor that no receiver could match - and prefixed AltBeacon's0xBE 0xACwhere iBeacon wants0x02 0x15. Windows gained iBeacon broadcasting, which previously threw, and Linux gained advertising outright -StartAdvertisingandAdvertiseBeaconwere both unimplemented stubs. BlueZ inverts the model: rather than handing a payload to an API, the app exports anorg.bluez.LEAdvertisement1D-Bus object that BlueZ then calls back into the process to read, so Shiny exports one and registers its path withLEAdvertisingManager1. BlueZ's ownRelease()is honoured, so an adapter power-down or abluetoothdrestart clearsIsAdvertisinginstead of leaving it stuck on. Linux hosts GATT services too -AddServiceused to throw - by exporting theObjectManager/GattService1/GattCharacteristic1tree BlueZ reads and registering it withGattManager1. BlueZ only reports whether any central has notifications enabled and fans every value out to all of them, so on LinuxNotifycannot address a single central - Beacons - iBeacon and Eddystone ranging, background region monitoring, and broadcasting on iOS, Mac Catalyst, macOS, Android, Windows, Linux and Blazor WebAssembly
- Distance estimates are filtered, not raw. BLE RSSI swings 10 dBm between consecutive advertisements from a stationary beacon, which a naive calculation turns into metres of jitter. Every sample goes through a windowed filter (20s by default) that discards the top and bottom tenth before averaging, then into a pluggable
IBeaconDistanceEstimator-PathLossDistanceEstimator(d = 10^((txPower-rssi)/(10n)),nconfigurable for cluttered rooms) by default, with the Radius Networks/AltBeaconCurveFitDistanceEstimatoravailable for parity with the wider Android ecosystem.Proximityis derived from the resulting distance against Apple's own boundaries (0.5m / 3m), all of it tunable throughBeaconRangingOptions - iBeacon takes a different route on Apple, because it has to. CoreBluetooth strips Apple's own iBeacon manufacturer data out of every scan result, so no BLE scan can ever see one on iOS, Mac Catalyst or macOS - ranging and monitoring there go through CoreLocation and cost a location permission rather than a Bluetooth one. Everywhere else the same shared managed engine parses the advertisement directly. Monitoring uses
CLMonitor+CLBeaconIdentityConditionon iOS/Mac Catalyst 18+ (the same pathShiny.Locationstakes for geofences, cold-start replay suppression included) andCLLocationManagerregion monitoring below it, where the 20-region-per-app cap is enforced with a clear error instead of iOS silently dropping the excess - Eddystone works the same everywhere, Apple included - it rides in service data under
0xFEAA, which CoreBluetooth passes through untouched.IEddystoneScannerdecodes UID (namespace/instance), URL (with the full scheme and top-level-domain compression table), and TLM telemetry - battery volts, temperature, advertisement count and uptime, with "mains powered" and "no sensor fitted" reported asnullrather than as zero and 128°C. Encrypted TLM is handed back intact for a caller holding the identity key; EID frames surface their raw rotating identifier. Filtering the scan on0xFEAAis also what lets it keep running when an iOS app is backgrounded - Region monitoring infers exits from silence, and now does it correctly. BLE has no "left the area" event, so a region that has gone
RegionExitTimeout(30s by default) without a matching advertisement is treated as exited. The pre-revival module got the entry side wrong in a way that mattered: it seeded the first sighting as
- Distance estimates are filtered, not raw. BLE RSSI swings 10 dBm between consecutive advertisements from a stationary beacon, which a naive calculation turns into metres of jitter. Every sample goes through a windowed filter (20s by default) that discards the top and bottom tenth before averaging, then into a pluggable