aves_mio0.31/lib/widgets/cast/dlna_manager.dar
2026-07-18 13:39:22 +02:00

232 lines
6.6 KiB
Text

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<List<DLNADevice>> 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 = <DLNADevice>[];
final completer = Completer<void>();
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<DLNADevice?> pickDevice() async {
final devices = await discover();
if (devices.isEmpty) return null;
return showDialog<DLNADevice>(
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<DLNADevice?> _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;
}
}