232 lines
6.6 KiB
Text
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;
|
|
}
|
|
}
|