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/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<CastDialog> {
final DLNAManager _dlnaManager = DLNAManager();
final Map<String, DLNADevice> _seenRenderers = {};
static const String upnpDeviceTypeMediaRenderer = 'urn:schemas-upnp-org:device:MediaRenderer:1';
final DeviceDiscoverer _discoverer = DeviceDiscoverer();
Timer? _discoverySearchTimer;
int _queryIndex = 0;
final Map<String, Device> _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<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
void dispose() {
_dlnaManager.stop();
_discoverySearchTimer?.cancel();
_discoverySearchTimer = null;
_discoverer.stop();
super.dispose();
}
@ -49,8 +86,8 @@ class _CastDialogState extends State<CastDialog> {
),
),
..._seenRenderers.values.map((dev) => ListTile(
title: Text(dev.info.friendlyName),
onTap: () => Navigator.maybeOf(context)?.pop<DLNADevice>(dev),
title: Text(dev.friendlyName ?? dev.uuid!),
onTap: () => Navigator.maybeOf(context)?.pop<Device>(dev),
)),
],
actions: const [

View file

@ -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<DLNADevice?> _selectRenderer(BuildContext context) async {
return await showDialog<DLNADevice?>(
Future<Device?> _selectRenderer(BuildContext context) async {
return await showDialog<Device?>(
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<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"
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:

View file

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