import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; import 'package:xml/xml.dart'; // ⭐ Usa il DLNADevice VERO (quello con setUrl/play/stop) import 'package:aves/widgets/cast/dlna.dart'; // ⭐ Serve per aprire dialog da qualsiasi punto import 'package:aves/main.dart' show navigatorKey; class DLNAManager { // ------------------------------------------------------------ // SINGLETON // ------------------------------------------------------------ DLNAManager._(); static final instance = DLNAManager._(); static const _ssdpAddress = "239.255.255.250"; static const _ssdpPort = 1900; static const _channel = MethodChannel("aves/dlna"); // ------------------------------------------------------------ // DISCOVERY 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}"); // 🔥 M-SEARCH const search = ''' M-SEARCH * HTTP/1.1 HOST: 239.255.255.250:1900 MAN: "ssdp:discover" MX: 2 ST: urn:schemas-upnp-org:device:MediaRenderer:1 '''; print("DLNA: sending M-SEARCH…"); socket.send( utf8.encode(search), InternetAddress(_ssdpAddress), _ssdpPort, ); 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) { print("DLNA: DEVICE FOUND → ${device.info.friendlyName}"); devices.add(device); } else { print("DLNA: device load FAILED"); } } }); await completer.future; timer.cancel(); socket.close(); print("DLNA: discovery FINISHED → ${devices.length} devices found"); // 🔥 Release multicast lock 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; } // ------------------------------------------------------------ // UI DI SELEZIONE DEVICE DLNA // ------------------------------------------------------------ Future pickDevice() async { final devices = await discover(); if (devices.isEmpty) return null; return showDialog( context: navigatorKey.currentContext!, builder: (context) { return AlertDialog( title: const Text("DLNA Devices"), content: SizedBox( width: 300, height: 300, child: ListView.builder( itemCount: devices.length, itemBuilder: (_, i) { final d = devices[i]; return ListTile( title: Text(d.info.friendlyName), onTap: () => Navigator.pop(context, d), ); }, ), ), ); }, ); } // ------------------------------------------------------------ // CARICA DEVICE DESCRIPTION // ------------------------------------------------------------ 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"; 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; } } // ------------------------------------------------------------ // HELPERS // ------------------------------------------------------------ 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(); } 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; } }