235 lines
6.8 KiB
Dart
235 lines
6.8 KiB
Dart
// 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<http.Response> postSoap({
|
|
required Uri url,
|
|
required String serviceType,
|
|
required String action,
|
|
required String body,
|
|
Duration timeout = const Duration(seconds: 5),
|
|
}) async {
|
|
final envelope = '''
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
|
|
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
<s:Body>
|
|
$body
|
|
</s:Body>
|
|
</s:Envelope>
|
|
''';
|
|
|
|
final headers = <String, String>{
|
|
'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<void> 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 = '''
|
|
<u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
|
|
<InstanceID>0</InstanceID>
|
|
<CurrentURI>$escapedUrl</CurrentURI>
|
|
<CurrentURIMetaData></CurrentURIMetaData>
|
|
</u:SetAVTransportURI>
|
|
''';
|
|
|
|
await DLNAHttp.postSoap(
|
|
url: avTransportUrl,
|
|
serviceType: 'urn:schemas-upnp-org:service:AVTransport:1',
|
|
action: 'SetAVTransportURI',
|
|
body: body,
|
|
);
|
|
}
|
|
|
|
//──────────────────────────────────────────────
|
|
// AVTransport: Play
|
|
//──────────────────────────────────────────────
|
|
Future<void> play() async {
|
|
final body = '''
|
|
<u:Play xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
|
|
<InstanceID>0</InstanceID>
|
|
<Speed>1</Speed>
|
|
</u:Play>
|
|
''';
|
|
|
|
await DLNAHttp.postSoap(
|
|
url: avTransportUrl,
|
|
serviceType: 'urn:schemas-upnp-org:service:AVTransport:1',
|
|
action: 'Play',
|
|
body: body,
|
|
);
|
|
}
|
|
|
|
//──────────────────────────────────────────────
|
|
// AVTransport: Stop
|
|
//──────────────────────────────────────────────
|
|
Future<void> stop() async {
|
|
final body = '''
|
|
<u:Stop xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
|
|
<InstanceID>0</InstanceID>
|
|
</u:Stop>
|
|
''';
|
|
|
|
await DLNAHttp.postSoap(
|
|
url: avTransportUrl,
|
|
serviceType: 'urn:schemas-upnp-org:service:AVTransport:1',
|
|
action: 'Stop',
|
|
body: body,
|
|
);
|
|
}
|
|
|
|
//──────────────────────────────────────────────
|
|
// ConnectionManager: GetProtocolInfo
|
|
//──────────────────────────────────────────────
|
|
Future<Set<String>> 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<XmlElement?>()
|
|
.firstWhere(
|
|
(e) => e != null && e.innerText.trim().isNotEmpty,
|
|
orElse: () => null,
|
|
);
|
|
|
|
if (sinkNode == null) {
|
|
return <String>{};
|
|
}
|
|
|
|
final sinkText = sinkNode.innerText.trim();
|
|
if (sinkText.isEmpty) {
|
|
return <String>{};
|
|
}
|
|
|
|
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<E> on Iterable<E> {
|
|
E? get firstOrNull {
|
|
final it = iterator;
|
|
if (!it.moveNext()) return null;
|
|
return it.current;
|
|
}
|
|
}
|