232 lines
6.4 KiB
Dart
232 lines
6.4 KiB
Dart
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 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 = <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 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<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;
|
||
}
|
||
}
|