From 902ceca2681e5764f7f1293b41f6dab4f8c00fa5 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 27 Oct 2023 18:18:58 +0300 Subject: [PATCH] upnp2 test --- lib/ref/upnp.dart | 5 +++ lib/widgets/dialogs/cast_dialog.dart | 65 +++++++++++++++++++++------ lib/widgets/viewer/controls/cast.dart | 56 ++++++++++++++++++----- pubspec.lock | 16 +++---- pubspec.yaml | 2 +- 5 files changed, 109 insertions(+), 35 deletions(-) create mode 100644 lib/ref/upnp.dart diff --git a/lib/ref/upnp.dart b/lib/ref/upnp.dart new file mode 100644 index 000000000..08368534d --- /dev/null +++ b/lib/ref/upnp.dart @@ -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'; +} diff --git a/lib/widgets/dialogs/cast_dialog.dart b/lib/widgets/dialogs/cast_dialog.dart index 4ac33b993..bd7e6257e 100644 --- a/lib/widgets/dialogs/cast_dialog.dart +++ b/lib/widgets/dialogs/cast_dialog.dart @@ -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/dialogs/aves_dialog.dart'; -import 'package:dlna_dart/dlna.dart'; import 'package:flutter/material.dart'; +import 'package:upnp2/upnp.dart'; class CastDialog extends StatefulWidget { static const routeName = '/dialog/cast'; @@ -13,26 +16,60 @@ class CastDialog extends StatefulWidget { } class _CastDialogState extends State { - final DLNAManager _dlnaManager = DLNAManager(); - final Map _seenRenderers = {}; - - static const String upnpDeviceTypeMediaRenderer = 'urn:schemas-upnp-org:device:MediaRenderer:1'; + final DeviceDiscoverer _discoverer = DeviceDiscoverer(); + Timer? _discoverySearchTimer; + int _queryIndex = 0; + final Map _seenRenderers = {}; @override void initState() { super.initState(); - - _dlnaManager.start().then((deviceManager) { - deviceManager.devices.stream.listen((devices) { - _seenRenderers.addAll(Map.fromEntries(devices.entries.where((kv) => kv.value.info.deviceType == upnpDeviceTypeMediaRenderer))); - setState(() {}); - }); + _discoverClients( + [ + Upnp.ssdpQueryAll, + 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(() {}); + } + } + } catch (e) { + debugPrint('TLAD failed to get device e=$e'); + } }); } + Stream _discoverClients(List 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 queries) { + final searchTarget = queries[_queryIndex]; + debugPrint('cast: search target=$searchTarget'); + _discoverer.search(searchTarget); + _queryIndex = (_queryIndex + 1) % queries.length; + } + @override void dispose() { - _dlnaManager.stop(); + _discoverySearchTimer?.cancel(); + _discoverySearchTimer = null; + _discoverer.stop(); super.dispose(); } @@ -49,8 +86,8 @@ class _CastDialogState extends State { ), ), ..._seenRenderers.values.map((dev) => ListTile( - title: Text(dev.info.friendlyName), - onTap: () => Navigator.maybeOf(context)?.pop(dev), + title: Text(dev.friendlyName ?? dev.uuid!), + onTap: () => Navigator.maybeOf(context)?.pop(dev), )), ], actions: const [ diff --git a/lib/widgets/viewer/controls/cast.dart b/lib/widgets/viewer/controls/cast.dart index d0219e6b4..50a73e724 100644 --- a/lib/widgets/viewer/controls/cast.dart +++ b/lib/widgets/viewer/controls/cast.dart @@ -2,19 +2,19 @@ import 'dart:async'; import 'dart:io'; 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/widgets/dialogs/cast_dialog.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:network_info_plus/network_info_plus.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as shelf_io; +import 'package:upnp2/upnp.dart'; mixin CastMixin { - DLNADevice? _renderer; + Device? _renderer; HttpServer? _mediaServer; bool get isCasting => _renderer != null && _mediaServer != null; @@ -25,7 +25,7 @@ mixin CastMixin { final renderer = await _selectRenderer(context); _renderer = renderer; 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(); if (ip == null) return; @@ -66,12 +66,12 @@ mixin CastMixin { await _mediaServer?.close(); _mediaServer = null; - await _renderer?.stop(); + // await _renderer?.stop(); _renderer = null; } - Future _selectRenderer(BuildContext context) async { - return await showDialog( + Future _selectRenderer(BuildContext context) async { + return await showDialog( context: context, builder: (context) => const CastDialog(), routeSettings: const RouteSettings(name: CastDialog.routeName), @@ -85,12 +85,12 @@ mixin CastMixin { debugPrint('cast: set entry=$entry'); try { - await renderer.setUrl( + await _setAVTransportURI( '$_serverBaseUrl/${entry.id}', - title: entry.bestTitle ?? '', - type: entry.isVideo ? PlayType.Video : PlayType.Image, + entry.bestTitle ?? '${entry.id}', + entry.mimeType, ); - await renderer.play(); + await _play(); } catch (error, stack) { await reportService.recordError(error, stack); } @@ -100,4 +100,36 @@ mixin CastMixin { final server = _mediaServer; return server != null ? 'http://${server.address.host}:${server.port}' : null; } + + Future get _avTransportService async { + return _renderer!.getService(Upnp.upnpServiceTypeAVTransport); + } + + Future> _setAVTransportURI(String url, String title, String mimeType) async { + final service = await _avTransportService; + if (service == null) return {}; + + var meta = ''; + if (MimeTypes.isVideo(mimeType)) { + meta = '''$titleunkownobject.item.videoItem'''; + } else if (MimeTypes.isImage(mimeType)) { + meta = '''$titleunkownobject.item.imageItem'''; + } + var args = { + 'InstanceID': 0, + 'CurrentURI': url, + 'CurrentURIMetaData': meta, + }; + return service.invokeAction('SetAVTransportURI', args); + } + + Future> _play() async { + final service = await _avTransportService; + if (service == null) return {}; + + return service.invokeAction('Play', { + 'InstanceID': 0, + 'Speed': 1, + }); + } } diff --git a/pubspec.lock b/pubspec.lock index ff4437047..712dca187 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -302,14 +302,6 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: @@ -1551,6 +1543,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d26fc9ddc..3a3ff4b85 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -117,7 +117,7 @@ dependencies: volume_controller: xml: - dlna_dart: + upnp2: network_info_plus: shelf: