// lib/widgets/cast/dlna.dart // // Implementazione DLNA minimale ma reale, pensata per funzionare // con dlna_cast_controller.dart e upnp.dart, inclusi Sony Bravia. // // Espone: // - DLNADevice // - DLNAInfo // - PlayType // - DLNAHttp // // Richiede nel pubspec: // http: // xml: // // E usa: // lib/ref/upnp.dart // import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:xml/xml.dart'; import 'package:aves/ref/upnp.dart'; @immutable class DLNAInfo { final String friendlyName; const DLNAInfo({ required this.friendlyName, }); } enum PlayType { Video, Image, } class DLNAHttp { const DLNAHttp._(); static Future postSoap({ required Uri url, required String serviceType, required String action, required String body, Duration timeout = const Duration(seconds: 5), }) async { final envelope = ''' $body '''; final headers = { 'Content-Type': 'text/xml; charset="utf-8"', 'SOAPACTION': '"$serviceType#$action"', 'Connection': 'Close', }; final resp = await http .post(url, headers: headers, body: utf8.encode(envelope)) .timeout(timeout); if (resp.statusCode >= 400) { throw HttpException( 'DLNA SOAP error ${resp.statusCode} for $action on $url', uri: url, ); } return resp; } } class DLNADevice { final Uri avTransportUrl; final Uri connectionManagerUrl; final DLNAInfo info; DLNADevice({ required this.avTransportUrl, required this.connectionManagerUrl, required this.info, }); //────────────────────────────────────────────── // AVTransport: SetAVTransportURI //────────────────────────────────────────────── Future setUrl( String url, { required String title, required PlayType type, }) async { // Molti renderer (inclusi Sony Bravia) accettano metadata vuoto. // Se servirà, si può estendere con DIDL-Lite completo. final escapedUrl = _xmlEscape(url); final body = ''' 0 $escapedUrl '''; await DLNAHttp.postSoap( url: avTransportUrl, serviceType: 'urn:schemas-upnp-org:service:AVTransport:1', action: 'SetAVTransportURI', body: body, ); } //────────────────────────────────────────────── // AVTransport: Play //────────────────────────────────────────────── Future play() async { final body = ''' 0 1 '''; await DLNAHttp.postSoap( url: avTransportUrl, serviceType: 'urn:schemas-upnp-org:service:AVTransport:1', action: 'Play', body: body, ); } //────────────────────────────────────────────── // AVTransport: Stop //────────────────────────────────────────────── Future stop() async { final body = ''' 0 '''; await DLNAHttp.postSoap( url: avTransportUrl, serviceType: 'urn:schemas-upnp-org:service:AVTransport:1', action: 'Stop', body: body, ); } //────────────────────────────────────────────── // ConnectionManager: GetProtocolInfo //────────────────────────────────────────────── Future> getSinkSupportedMimeTypes() async { final body = Upnp.getProtocolInfoActionXml(); final resp = await DLNAHttp.postSoap( url: connectionManagerUrl, serviceType: Upnp.upnpServiceTypeConnectionManager, action: 'GetProtocolInfo', body: _extractBody(body), ); final text = resp.body; final doc = XmlDocument.parse(text); // Cerchiamo l'elemento che contiene il sink protocol info. // I nomi variano tra device, quindi cerchiamo per contenuto. final sinkNode = doc .findAllElements('Sink') .followedBy(doc.findAllElements('SinkProtocolInfo')) .cast() .firstWhere( (e) => e != null && e.innerText.trim().isNotEmpty, orElse: () => null, ); if (sinkNode == null) { return {}; } final sinkText = sinkNode.innerText.trim(); if (sinkText.isEmpty) { return {}; } final protocolInfo = UpnpProtocolInfo(sinkText); // Ritorniamo solo i MIME type (contentFormat). return protocolInfo.entries.map((e) => e.contentFormat).toSet(); } //────────────────────────────────────────────── // Helper //────────────────────────────────────────────── static String _xmlEscape(String value) { return value .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } // Upnp.getProtocolInfoActionXml() restituisce già un envelope completo. // Qui estraiamo solo il body interno per riusarlo nel nostro envelope. static String _extractBody(String envelope) { try { final doc = XmlDocument.parse(envelope); final body = doc.findAllElements('s:Body').firstOrNull ?? doc.findAllElements('Body').first; return body.children.map((n) => n.toXmlString()).join(); } catch (_) { // In caso di problemi, usiamo l'XML così com'è. return envelope; } } } extension _FirstOrNull on Iterable { E? get firstOrNull { final it = iterator; if (!it.moveNext()) return null; return it.current; } }