upnp2 test

This commit is contained in:
Thibault Deckers 2023-10-27 18:18:58 +03:00
parent 59582483e3
commit 902ceca268
5 changed files with 109 additions and 35 deletions

5
lib/ref/upnp.dart Normal file
View file

@ -0,0 +1,5 @@
class Upnp {
static const String ssdpQueryAll = 'ssdp:all';
static const String upnpServiceTypeAVTransport = 'urn:schemas-upnp-org:service:AVTransport:1';
static const String upnpDeviceTypeMediaRenderer = 'urn:schemas-upnp-org:device:MediaRenderer:1';
}

View file

@ -1,7 +1,10 @@
import 'dart:async';
import 'package:aves/ref/upnp.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:dlna_dart/dlna.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:upnp2/upnp.dart';
class CastDialog extends StatefulWidget { class CastDialog extends StatefulWidget {
static const routeName = '/dialog/cast'; static const routeName = '/dialog/cast';
@ -13,26 +16,60 @@ class CastDialog extends StatefulWidget {
} }
class _CastDialogState extends State<CastDialog> { class _CastDialogState extends State<CastDialog> {
final DLNAManager _dlnaManager = DLNAManager(); final DeviceDiscoverer _discoverer = DeviceDiscoverer();
final Map<String, DLNADevice> _seenRenderers = {}; Timer? _discoverySearchTimer;
int _queryIndex = 0;
static const String upnpDeviceTypeMediaRenderer = 'urn:schemas-upnp-org:device:MediaRenderer:1'; final Map<String, Device> _seenRenderers = {};
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_discoverClients(
_dlnaManager.start().then((deviceManager) { [
deviceManager.devices.stream.listen((devices) { Upnp.ssdpQueryAll,
_seenRenderers.addAll(Map.fromEntries(devices.entries.where((kv) => kv.value.info.deviceType == upnpDeviceTypeMediaRenderer))); Upnp.upnpServiceTypeAVTransport,
Upnp.upnpDeviceTypeMediaRenderer,
],
).listen((client) async {
try {
final device = await client.getDevice();
if (device != null) {
final uuid = device.uuid;
if (uuid != null && device.deviceType == Upnp.upnpDeviceTypeMediaRenderer) {
_seenRenderers[uuid] = device;
setState(() {}); setState(() {});
}
}
} catch (e) {
debugPrint('TLAD failed to get device e=$e');
}
}); });
}); }
Stream<DiscoveredClient> _discoverClients(List<String> queries) async* {
await _discoverer.start(
ipv6: false,
onError: (e) => debugPrint('cast: failed to start discoverer with error=$e'),
);
_search(queries);
_discoverySearchTimer = Timer.periodic(const Duration(seconds: 5), (_) => _search(queries));
await for (var client in _discoverer.clients) {
yield client;
}
}
void _search(List<String> queries) {
final searchTarget = queries[_queryIndex];
debugPrint('cast: search target=$searchTarget');
_discoverer.search(searchTarget);
_queryIndex = (_queryIndex + 1) % queries.length;
} }
@override @override
void dispose() { void dispose() {
_dlnaManager.stop(); _discoverySearchTimer?.cancel();
_discoverySearchTimer = null;
_discoverer.stop();
super.dispose(); super.dispose();
} }
@ -49,8 +86,8 @@ class _CastDialogState extends State<CastDialog> {
), ),
), ),
..._seenRenderers.values.map((dev) => ListTile( ..._seenRenderers.values.map((dev) => ListTile(
title: Text(dev.info.friendlyName), title: Text(dev.friendlyName ?? dev.uuid!),
onTap: () => Navigator.maybeOf(context)?.pop<DLNADevice>(dev), onTap: () => Navigator.maybeOf(context)?.pop<Device>(dev),
)), )),
], ],
actions: const [ actions: const [

View file

@ -2,19 +2,19 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/ref/upnp.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/dialogs/cast_dialog.dart'; import 'package:aves/widgets/dialogs/cast_dialog.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:dlna_dart/dlna.dart';
import 'package:dlna_dart/xmlParser.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:network_info_plus/network_info_plus.dart'; import 'package:network_info_plus/network_info_plus.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:upnp2/upnp.dart';
mixin CastMixin { mixin CastMixin {
DLNADevice? _renderer; Device? _renderer;
HttpServer? _mediaServer; HttpServer? _mediaServer;
bool get isCasting => _renderer != null && _mediaServer != null; bool get isCasting => _renderer != null && _mediaServer != null;
@ -25,7 +25,7 @@ mixin CastMixin {
final renderer = await _selectRenderer(context); final renderer = await _selectRenderer(context);
_renderer = renderer; _renderer = renderer;
if (renderer == null) return; if (renderer == null) return;
debugPrint('cast: select renderer `${renderer.info.friendlyName}` at ${renderer.info.URLBase}'); debugPrint('cast: select renderer `${renderer.friendlyName}` at ${renderer.urlBase}');
final ip = await NetworkInfo().getWifiIP(); final ip = await NetworkInfo().getWifiIP();
if (ip == null) return; if (ip == null) return;
@ -66,12 +66,12 @@ mixin CastMixin {
await _mediaServer?.close(); await _mediaServer?.close();
_mediaServer = null; _mediaServer = null;
await _renderer?.stop(); // await _renderer?.stop();
_renderer = null; _renderer = null;
} }
Future<DLNADevice?> _selectRenderer(BuildContext context) async { Future<Device?> _selectRenderer(BuildContext context) async {
return await showDialog<DLNADevice?>( return await showDialog<Device?>(
context: context, context: context,
builder: (context) => const CastDialog(), builder: (context) => const CastDialog(),
routeSettings: const RouteSettings(name: CastDialog.routeName), routeSettings: const RouteSettings(name: CastDialog.routeName),
@ -85,12 +85,12 @@ mixin CastMixin {
debugPrint('cast: set entry=$entry'); debugPrint('cast: set entry=$entry');
try { try {
await renderer.setUrl( await _setAVTransportURI(
'$_serverBaseUrl/${entry.id}', '$_serverBaseUrl/${entry.id}',
title: entry.bestTitle ?? '', entry.bestTitle ?? '${entry.id}',
type: entry.isVideo ? PlayType.Video : PlayType.Image, entry.mimeType,
); );
await renderer.play(); await _play();
} catch (error, stack) { } catch (error, stack) {
await reportService.recordError(error, stack); await reportService.recordError(error, stack);
} }
@ -100,4 +100,36 @@ mixin CastMixin {
final server = _mediaServer; final server = _mediaServer;
return server != null ? 'http://${server.address.host}:${server.port}' : null; return server != null ? 'http://${server.address.host}:${server.port}' : null;
} }
Future<Service?> get _avTransportService async {
return _renderer!.getService(Upnp.upnpServiceTypeAVTransport);
}
Future<Map<String, String>> _setAVTransportURI(String url, String title, String mimeType) async {
final service = await _avTransportService;
if (service == null) return {};
var meta = '';
if (MimeTypes.isVideo(mimeType)) {
meta = '''<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sec="http://www.sec.co.kr/"><item id="false" parentID="1" restricted="0"><dc:title>$title</dc:title><dc:creator>unkown</dc:creator><upnp:class>object.item.videoItem</upnp:class><res resolution="4"></res></item></DIDL-Lite>''';
} else if (MimeTypes.isImage(mimeType)) {
meta = '''<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sec="http://www.sec.co.kr/"><item id="false" parentID="1" restricted="0"><dc:title>$title</dc:title><dc:creator>unkown</dc:creator><upnp:class>object.item.imageItem</upnp:class><res resolution="4"></res></item></DIDL-Lite>''';
}
var args = {
'InstanceID': 0,
'CurrentURI': url,
'CurrentURIMetaData': meta,
};
return service.invokeAction('SetAVTransportURI', args);
}
Future<Map<String, String>> _play() async {
final service = await _avTransportService;
if (service == null) return {};
return service.invokeAction('Play', {
'InstanceID': 0,
'Speed': 1,
});
}
} }

View file

@ -302,14 +302,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.0" version: "7.0.0"
dlna_dart:
dependency: "direct main"
description:
name: dlna_dart
sha256: ae07c1c53077bbf58756fa589f936968719b0085441981d33e74f82f89d1d281
url: "https://pub.dev"
source: hosted
version: "0.0.8"
dynamic_color: dynamic_color:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1551,6 +1543,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0+1" version: "1.0.0+1"
upnp2:
dependency: "direct main"
description:
name: upnp2
sha256: "60902d702809a9802ed6fe953cf8ebebfbab1ef8d99e516665b74b6a4522cad4"
url: "https://pub.dev"
source: hosted
version: "3.0.11"
uri_parser: uri_parser:
dependency: transitive dependency: transitive
description: description:

View file

@ -117,7 +117,7 @@ dependencies:
volume_controller: volume_controller:
xml: xml:
dlna_dart: upnp2:
network_info_plus: network_info_plus:
shelf: shelf: