aves_mio100/lib/widgets/cast/dlna_manager.dart
2026-07-06 14:52:45 +02:00

232 lines
6.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<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}");
print("DLNA: sending multiST MSEARCH…");
// ⭐ 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 MSEARCH…");
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 = <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) {
final loc = Uri.parse(location);
// ⭐ Filtro antiduplicati 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<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";
// ⭐ 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;
}
}