parent
902ceca268
commit
fe46a1d388
5 changed files with 35 additions and 109 deletions
|
@ -1,5 +0,0 @@
|
||||||
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';
|
|
||||||
}
|
|
|
@ -1,10 +1,7 @@
|
||||||
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';
|
||||||
|
@ -16,60 +13,26 @@ class CastDialog extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CastDialogState extends State<CastDialog> {
|
class _CastDialogState extends State<CastDialog> {
|
||||||
final DeviceDiscoverer _discoverer = DeviceDiscoverer();
|
final DLNAManager _dlnaManager = DLNAManager();
|
||||||
Timer? _discoverySearchTimer;
|
final Map<String, DLNADevice> _seenRenderers = {};
|
||||||
int _queryIndex = 0;
|
|
||||||
final Map<String, Device> _seenRenderers = {};
|
static const String upnpDeviceTypeMediaRenderer = 'urn:schemas-upnp-org:device:MediaRenderer:1';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_discoverClients(
|
|
||||||
[
|
_dlnaManager.start().then((deviceManager) {
|
||||||
Upnp.ssdpQueryAll,
|
deviceManager.devices.stream.listen((devices) {
|
||||||
Upnp.upnpServiceTypeAVTransport,
|
_seenRenderers.addAll(Map.fromEntries(devices.entries.where((kv) => kv.value.info.deviceType == upnpDeviceTypeMediaRenderer)));
|
||||||
Upnp.upnpDeviceTypeMediaRenderer,
|
setState(() {});
|
||||||
],
|
});
|
||||||
).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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_discoverySearchTimer?.cancel();
|
_dlnaManager.stop();
|
||||||
_discoverySearchTimer = null;
|
|
||||||
_discoverer.stop();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,8 +49,8 @@ class _CastDialogState extends State<CastDialog> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
..._seenRenderers.values.map((dev) => ListTile(
|
..._seenRenderers.values.map((dev) => ListTile(
|
||||||
title: Text(dev.friendlyName ?? dev.uuid!),
|
title: Text(dev.info.friendlyName),
|
||||||
onTap: () => Navigator.maybeOf(context)?.pop<Device>(dev),
|
onTap: () => Navigator.maybeOf(context)?.pop<DLNADevice>(dev),
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
actions: const [
|
actions: const [
|
||||||
|
|
|
@ -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/ref/mime_types.dart';
|
import 'package:aves/model/entry/extensions/props.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 {
|
||||||
Device? _renderer;
|
DLNADevice? _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.friendlyName}` at ${renderer.urlBase}');
|
debugPrint('cast: select renderer `${renderer.info.friendlyName}` at ${renderer.info.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<Device?> _selectRenderer(BuildContext context) async {
|
Future<DLNADevice?> _selectRenderer(BuildContext context) async {
|
||||||
return await showDialog<Device?>(
|
return await showDialog<DLNADevice?>(
|
||||||
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 _setAVTransportURI(
|
await renderer.setUrl(
|
||||||
'$_serverBaseUrl/${entry.id}',
|
'$_serverBaseUrl/${entry.id}',
|
||||||
entry.bestTitle ?? '${entry.id}',
|
title: entry.bestTitle ?? '',
|
||||||
entry.mimeType,
|
type: entry.isVideo ? PlayType.Video : PlayType.Image,
|
||||||
);
|
);
|
||||||
await _play();
|
await renderer.play();
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
await reportService.recordError(error, stack);
|
await reportService.recordError(error, stack);
|
||||||
}
|
}
|
||||||
|
@ -100,36 +100,4 @@ 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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
16
pubspec.lock
16
pubspec.lock
|
@ -302,6 +302,14 @@ 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:
|
||||||
|
@ -1543,14 +1551,6 @@ 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:
|
||||||
|
|
|
@ -117,7 +117,7 @@ dependencies:
|
||||||
volume_controller:
|
volume_controller:
|
||||||
xml:
|
xml:
|
||||||
|
|
||||||
upnp2:
|
dlna_dart:
|
||||||
network_info_plus:
|
network_info_plus:
|
||||||
shelf:
|
shelf:
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue