import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/services.dart'; import 'package:xml/xml.dart'; // ⭐ Usa il DLNADevice VERO (quello con setUrl/play/stop) import 'package:aves/widgets/cast/dlna.dart'; class DLNAManager { DLNAManager._(); static final instance = DLNAManager._(); static const _ssdpAddress = "239.255.255.250"; static const _ssdpPort = 1900; static const _channel = MethodChannel("aves/dlna"); Future> discover() async { print("DLNA: discover() START"); // 🔥 Acquire multicast lock try { print("DLNA: acquiring multicast lock…"); await _channel.invokeMethod("dlnaAcquireLock"); print("DLNA: multicast lock ACQUIRED"); } catch (e) { print("DLNA: ERROR acquiring lock → $e"); } // 🔥 Bind UDP socket print("DLNA: binding UDP socket…"); final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0); socket.broadcastEnabled = true; print("DLNA: socket bound on port ${socket.port}"); print("DLNA: sending multi‑ST M‑SEARCH…"); // ⭐ Lista completa ST per compatibilità massima final searchTargets = [ "urn:schemas-upnp-org:device:MediaRenderer:1", // AirScreen, AirReceiver "ssdp:all", // Sony Bravia "upnp:rootdevice", // Sony Bravia "urn:schemas-upnp-org:service:AVTransport:1", // Sony Bravia "urn:schemas-upnp-org:service:ConnectionManager:1", ]; // ⭐ Multicast M-SEARCH for (final st in searchTargets) { final msg = ''' M-SEARCH * HTTP/1.1 HOST: 239.255.255.250:1900 MAN: "ssdp:discover" MX: 2 ST: $st '''; print("DLNA: sending MULTICAST M-SEARCH for ST=$st"); socket.send( utf8.encode(msg), InternetAddress(_ssdpAddress), _ssdpPort, ); } // ⭐ Broadcast M-SEARCH (compatibilità router strani) print("DLNA: sending BROADCAST M‑SEARCH…"); for (final st in searchTargets) { final msg = ''' M-SEARCH * HTTP/1.1 HOST: 255.255.255.255:1900 MAN: "ssdp:discover" MX: 2 ST: $st '''; socket.send( utf8.encode(msg), InternetAddress("255.255.255.255"), 1900, ); } print("DLNA: M-SEARCH SENT"); final devices = []; final completer = Completer(); final timer = Timer(const Duration(seconds: 2), () { print("DLNA: timeout reached, finishing discovery"); completer.complete(); }); socket.listen((event) async { if (event == RawSocketEvent.read) { final dg = socket.receive(); if (dg == null) return; print("DLNA: received UDP packet from ${dg.address.address}"); final response = utf8.decode(dg.data); if (!response.contains("200 OK")) { print("DLNA: packet ignored (not 200 OK)"); return; } print("DLNA: SSDP response:\n$response"); final location = _extractHeader(response, "LOCATION"); if (location == null) { print("DLNA: LOCATION header missing"); return; } print("DLNA: LOCATION = $location"); final device = await _loadDevice(location); if (device != null) { final loc = Uri.parse(location); // ⭐ Filtro anti‑duplicati basato su LOCATION (origin) + friendlyName final already = devices.any((d) { return d.avTransportUrl.origin == loc.origin && d.info.friendlyName == device.info.friendlyName; }); if (!already) { print("DLNA: DEVICE ADDED (unique) → ${device.info.friendlyName}"); devices.add(device); } else { print("DLNA: DUPLICATE IGNORED for ${device.info.friendlyName}"); } } else { print("DLNA: device load FAILED"); } } }); await completer.future; timer.cancel(); socket.close(); print("DLNA: discovery FINISHED → ${devices.length} devices found"); try { print("DLNA: releasing multicast lock…"); await _channel.invokeMethod("dlnaReleaseLock"); print("DLNA: multicast lock RELEASED"); } catch (e) { print("DLNA: ERROR releasing lock → $e"); } return devices; } // ⭐ Carica il device description e costruisce il DLNADevice VERO Future _loadDevice(String url) async { print("DLNA: loading device description from $url"); try { final client = HttpClient(); final req = await client.getUrl(Uri.parse(url)); final res = await req.close(); final xmlStr = await res.transform(utf8.decoder).join(); final doc = XmlDocument.parse(xmlStr); final device = doc.findAllElements("device").first; final friendly = device.getElement("friendlyName")?.innerText ?? "DLNA"; // ⭐ Trova i servizi AVTransport e ConnectionManager Uri? avTransport; Uri? connectionManager; for (final s in device.findAllElements("service")) { final type = s.getElement("serviceType")?.innerText ?? ""; if (type.contains("AVTransport")) { final ctrl = s.getElement("controlURL")?.innerText ?? ""; avTransport = Uri.parse(_resolveUrl(url, ctrl)); } if (type.contains("ConnectionManager")) { final ctrl = s.getElement("controlURL")?.innerText ?? ""; connectionManager = Uri.parse(_resolveUrl(url, ctrl)); } } if (avTransport == null || connectionManager == null) { print("DLNA: missing AVTransport or ConnectionManager"); return null; } return DLNADevice( avTransportUrl: avTransport, connectionManagerUrl: connectionManager, info: DLNAInfo(friendlyName: friendly), ); } catch (e) { print("DLNA: ERROR parsing device → $e"); return null; } } // ⭐ Risolve URL relativi String _resolveUrl(String base, String path) { final b = Uri.parse(base); return Uri( scheme: b.scheme, host: b.host, port: b.port, path: path.startsWith("/") ? path : "/$path", ).toString(); } // ⭐ Fix definitivo per LOCATION String? _extractHeader(String response, String header) { for (final line in response.split("\r\n")) { if (line.toUpperCase().startsWith("$header:")) { final idx = line.indexOf(":"); if (idx == -1) return null; return line.substring(idx + 1).trim(); } } return null; } }