# Phase 3 — Smart TV casting via DLNA/UPnP ## Context Phase 1 (HTTP relay) and Phase 2 (Chromecast) are shipped. The relay in `cast-relay.js` already serves a standard fMP4 stream over HTTP — any DLNA-capable TV can play from that URL. What is missing is *discovery* (finding the TV) and *control* (telling it to play). ## What exists | Layer | File | Role | |-------|------|------| | Relay | `cast-relay.js` | HTTP server, BoxAccumulator, ring buffer, `finish()` for clean EOS | | Chromecast | `cast-chromecast.js` | mDNS discovery, CASTV2 connect/reload/disconnect | | IPC | `main.js` lines 743-795 | `cast:*` + `cast:chromecast:*` handlers | | Bridge | `preload.js` | `cast.start/push/stop/finish/status/discover` + chromecast methods | | Platform | `platform.js` | `cast.*` namespace, `capabilities.lanCast` gate | | UI | `app.js` VideoPlayer | Cast button, device picker, seek-aware restart, generation counter | The relay is device-agnostic — Chromecast and DLNA share it. Phase 3 adds a second device backend alongside `cast-chromecast.js`, not a second relay. ## Protocol DLNA media rendering uses two protocols: 1. **SSDP** (Simple Service Discovery Protocol) — UDP multicast on `239.255.255.250:1900`. The client sends `M-SEARCH` for `urn:schemas-upnp-org:device:MediaRenderer:1` and collects responses (location URL → XML device description → friendly name, icon, services). 2. **UPnP AV** — SOAP over HTTP to the `AVTransport:1` service on the TV. Key actions: - `SetAVTransportURI(uri, metadata)` — point the TV at the relay URL. `metadata` is a DIDL-Lite XML snippet with content type, title, etc. - `Play` / `Stop` / `Pause` - `Seek(target)` — if the TV supports it (many don't for live streams) - `GetTransportInfo` — current state (PLAYING, STOPPED, etc.) ## Implementation plan ### 1. `cast-dlna.js` (~150-200 lines) ``` class CastDLNA { constructor() async discover(): Device[] // Send M-SEARCH, collect SSDP responses for 6 seconds // Fetch each device's XML description // Parse friendly name + AVTransport control URL // Return [{ id, name, host, controlUrl }] async play(device, mediaUrl): void // POST SOAP SetAVTransportURI to device.controlUrl // POST SOAP Play async stop(): void // POST SOAP Stop to connected device async getStatus(): { state, ... } // POST SOAP GetTransportInfo getConnectedDevice(): { name } | null } ``` **Dependencies:** None required. SSDP is raw UDP, UPnP AV is HTTP+SOAP — both can be done with Node builtins (`dgram`, `http`, `xml` parsing with a small helper or regex for the limited DIDL-Lite we generate). Avoid pulling `node-ssdp` or `upnp-client` — they're heavy and unmaintained. Keep it self-contained like `cast-chromecast.js`. SSDP implementation notes: - Bind UDP socket to `0.0.0.0` (not the LAN IP — multicast responses come back to the sending port) - Send to `239.255.255.250:1900` - `MX: 3` (max wait), `ST: urn:schemas-upnp-org:device:MediaRenderer:1` - Parse `LOCATION:` header from responses, fetch the XML, extract `friendlyName` and the `AVTransport` `controlURL` UPnP AV SOAP envelope (SetAVTransportURI): ```xml 0 {mediaUrl} {didlLite} ``` ### 2. IPC channels Add to `main.js`: - `cast:dlna:discover` → `castDLNA.discover()` - `cast:dlna:play` → `castDLNA.play(device, mediaUrl)` - `cast:dlna:stop` → `castDLNA.stop()` - `cast:dlna:status` → `castDLNA.getStatus()` ### 3. Bridge + platform Add to `preload.js` `cast` object: - `dlnaDiscover`, `dlnaPlay`, `dlnaStop`, `dlnaStatus` Add to `platform.js` `cast` namespace: - Same four methods ### 4. UI changes The device picker already lists Chromecast devices. Extend it: - Run `cast:discover` (Chromecast mDNS) and `cast:dlna:discover` (SSDP) in parallel - Display both in the picker, with a type indicator (cast icon vs TV icon) - On click: if Chromecast → existing `chromecastConnect` flow; if DLNA → `cast.start()` (relay) then `dlnaPlay(device, relayUrl)` - Seek restart: same pattern as Chromecast — `cast.stop()`, `cast.start()` with new init segment, then `dlnaPlay()` with the new URL (DLNA has no `reload`, it's just SetAVTransportURI + Play again) - `onStreamEnd` `finish()` should work as-is — the TV sees HTTP EOF ### 5. Firewall SSDP needs UDP 1900 (outbound multicast + inbound responses). The relay ports (TCP 19550-19553) are already opened for Chromecast. Add to the firewall instructions: ``` sudo firewall-cmd --zone=FedoraWorkstation --add-port=1900/udp ``` ## Key differences from Chromecast | | Chromecast | DLNA/Smart TV | |---|---|---| | Discovery | mDNS (bonjour-service) | SSDP (raw UDP) | | Control | CASTV2 (TLS, protobuf) | UPnP AV (HTTP, SOAP/XML) | | Connection | Persistent (TLS socket) | Stateless (one SOAP call per action) | | Reload | `player.load()` on same session | `SetAVTransportURI` + `Play` (new URL) | | Dependencies | `castv2-client`, `bonjour-service` | None (Node builtins) | | Status | Continuous (`player.on('status')`) | Polling (`GetTransportInfo`) | The stateless nature of UPnP AV is actually simpler — no persistent connection to manage, no error handler that can call `_cleanup()` and lose state. Each action is a self-contained HTTP request. ## Edge cases to handle - **TV goes to sleep during playback** — `GetTransportInfo` returns `STOPPED` or the HTTP request times out. Surface this in the UI. - **Multiple AVTransport services** — some TVs expose more than one. Pick the first, or the one whose `serviceId` contains `AVTransport`. - **Content-Type negotiation** — some TVs reject `video/mp4` for live streams. The DIDL-Lite metadata should specify `protocolInfo="http-get:*:video/mp4:*"`. - **No seek support** — most TVs don't support `Seek` on live/progressive streams. The seek-restart pattern (stop → new URL → play) handles this. ## Testing Samsung, LG, and Sony TVs are the most common DLNA renderers. VLC also acts as a DLNA renderer (`vlc --play-and-exit ` or via its DLNA renderer mode) and is useful for development without a physical TV. ## Estimated scope - `cast-dlna.js`: ~150-200 lines (SSDP + UPnP AV, no deps) - `main.js`: ~20 lines (4 IPC handlers) - `preload.js`: ~5 lines - `platform.js`: ~15 lines - `app.js`: ~30 lines (parallel discovery, device type routing) - Total: ~250 lines of new code