get sink supported mime types
This commit is contained in:
parent
fe46a1d388
commit
23bccca538
3 changed files with 137 additions and 26 deletions
38
lib/ref/upnp.dart
Normal file
38
lib/ref/upnp.dart
Normal file
|
@ -0,0 +1,38 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class Upnp {
|
||||
static const String upnpDeviceTypeMediaRenderer = 'urn:schemas-upnp-org:device:MediaRenderer:1';
|
||||
static const String upnpServiceTypeConnectionManager = 'urn:schemas-upnp-org:service:ConnectionManager:1';
|
||||
|
||||
static String getProtocolInfoActionXml() {
|
||||
return '''<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
||||
<s:Body>
|
||||
<u:GetProtocolInfo xmlns:u="urn:schemas-upnp-org:service:ConnectionManager:1">
|
||||
<InstanceID>0</InstanceID>
|
||||
</u:GetProtocolInfo>
|
||||
</s:Body>
|
||||
</s:Envelope>''';
|
||||
}
|
||||
}
|
||||
|
||||
class UpnpProtocolInfo {
|
||||
late final Set<UpnpProtocolInfoEntry> entries;
|
||||
|
||||
UpnpProtocolInfo(String text) {
|
||||
entries = text.split(',').where((v) => v.isNotEmpty).map(UpnpProtocolInfoEntry.new).toSet();
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class UpnpProtocolInfoEntry {
|
||||
late final String protocol, network, contentFormat, additionalInfo;
|
||||
|
||||
UpnpProtocolInfoEntry(String text) {
|
||||
final parts = text.split(':');
|
||||
protocol = parts[0];
|
||||
network = parts[1];
|
||||
contentFormat = parts[2];
|
||||
additionalInfo = parts[3];
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
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';
|
||||
|
@ -16,15 +17,16 @@ 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';
|
||||
|
||||
@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)));
|
||||
_seenRenderers.addAll(Map.fromEntries(devices.entries.where((kv) => kv.value.info.deviceType == Upnp.upnpDeviceTypeMediaRenderer).map((kv) {
|
||||
final device = kv.value;
|
||||
return MapEntry(device.info.friendlyName, device);
|
||||
})));
|
||||
setState(() {});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:aves/model/entry/entry.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/widgets/dialogs/cast_dialog.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -12,6 +14,7 @@ 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:xml/xml.dart';
|
||||
|
||||
mixin CastMixin {
|
||||
DLNADevice? _renderer;
|
||||
|
@ -30,32 +33,33 @@ mixin CastMixin {
|
|||
final ip = await NetworkInfo().getWifiIP();
|
||||
if (ip == null) return;
|
||||
|
||||
Set<String>? supportedMimeTypes;
|
||||
|
||||
final handler = const Pipeline().addHandler((request) async {
|
||||
debugPrint('cast: received request for id=${request.url}');
|
||||
final id = int.tryParse(request.url.path);
|
||||
if (id != null) {
|
||||
final entry = entries.firstWhereOrNull((v) => v.id == id);
|
||||
if (entry != null) {
|
||||
final bytes = await mediaFetchService.getImage(
|
||||
entry.uri,
|
||||
entry.mimeType,
|
||||
rotationDegrees: entry.rotationDegrees,
|
||||
isFlipped: entry.isFlipped,
|
||||
pageId: entry.pageId,
|
||||
sizeBytes: entry.sizeBytes,
|
||||
);
|
||||
debugPrint('cast: send ${bytes.length} bytes for entry=$entry');
|
||||
return Response.ok(
|
||||
bytes,
|
||||
headers: {
|
||||
'Content-Type': entry.mimeType,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (id == null) {
|
||||
return Response.notFound('invalid url=${request.url}');
|
||||
}
|
||||
return Response.notFound('no resource for url=${request.url}');
|
||||
|
||||
final entry = entries.firstWhereOrNull((v) => v.id == id);
|
||||
if (entry == null) {
|
||||
return Response.notFound('no resource for url=${request.url}');
|
||||
}
|
||||
|
||||
if (supportedMimeTypes == null) {
|
||||
// do not call `GetProtocolInfo` before serving files,
|
||||
// as it somehow makes `Play` time out (but not `SetAVTransportURI`)
|
||||
supportedMimeTypes = await renderer.getSinkSupportedMimeTypes();
|
||||
debugPrint('cast: supported MIME types=$supportedMimeTypes');
|
||||
}
|
||||
|
||||
// TODO TLAD [cast] transcode when MIME type is not supported by renderer
|
||||
|
||||
return await _sendEntry(entry);
|
||||
});
|
||||
_mediaServer = await shelf_io.serve(handler, ip, 8080);
|
||||
debugPrint('cast: serving media on $_serverBaseUrl}');
|
||||
debugPrint('cast: serving media on $_serverBaseUrl');
|
||||
}
|
||||
|
||||
Future<void> stopCast() async {
|
||||
|
@ -83,14 +87,15 @@ mixin CastMixin {
|
|||
final renderer = _renderer;
|
||||
if (server == null || renderer == null) return;
|
||||
|
||||
debugPrint('cast: set entry=$entry');
|
||||
try {
|
||||
debugPrint('cast: set entry=$entry');
|
||||
await renderer.setUrl(
|
||||
'$_serverBaseUrl/${entry.id}',
|
||||
title: entry.bestTitle ?? '',
|
||||
type: entry.isVideo ? PlayType.Video : PlayType.Image,
|
||||
);
|
||||
await renderer.play();
|
||||
debugPrint('cast: play entry=$entry');
|
||||
unawaited(renderer.play());
|
||||
} catch (error, stack) {
|
||||
await reportService.recordError(error, stack);
|
||||
}
|
||||
|
@ -100,4 +105,70 @@ mixin CastMixin {
|
|||
final server = _mediaServer;
|
||||
return server != null ? 'http://${server.address.host}:${server.port}' : null;
|
||||
}
|
||||
|
||||
Future<Response> _sendEntry(AvesEntry entry) async {
|
||||
// TODO TLAD [cast] providing downscaled versions is suitable when properly serving with `MediaServer`, as the renderer can pick what is best
|
||||
final bytes = await mediaFetchService.getImage(
|
||||
entry.uri,
|
||||
entry.mimeType,
|
||||
rotationDegrees: entry.rotationDegrees,
|
||||
isFlipped: entry.isFlipped,
|
||||
pageId: entry.pageId,
|
||||
sizeBytes: entry.sizeBytes,
|
||||
);
|
||||
|
||||
debugPrint('cast: send ${bytes.length} bytes for entry=$entry');
|
||||
return Response.ok(
|
||||
bytes,
|
||||
headers: {
|
||||
'Content-Type': entry.mimeType,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension DLNADeviceExtra on DLNADevice {
|
||||
Future<String> requestCustom({
|
||||
required String serviceId,
|
||||
required String serviceType,
|
||||
required String action,
|
||||
required String data,
|
||||
}) async {
|
||||
return DLNAHttp.post(
|
||||
Uri.parse(controlURL(serviceId)),
|
||||
Map.from({
|
||||
'SOAPAction': '"$serviceType#$action"',
|
||||
'Content-Type': 'text/xml',
|
||||
}),
|
||||
const Utf8Encoder().convert(data),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, UpnpProtocolInfo>> getProtocolInfo() async {
|
||||
final result = await requestCustom(
|
||||
serviceId: 'ConnectionManager',
|
||||
serviceType: Upnp.upnpServiceTypeConnectionManager,
|
||||
action: 'GetProtocolInfo',
|
||||
data: Upnp.getProtocolInfoActionXml(),
|
||||
);
|
||||
final doc = XmlDocument.parse(result);
|
||||
final sink = UpnpProtocolInfo(doc.findAllElements('Sink').first.innerText);
|
||||
final source = UpnpProtocolInfo(doc.findAllElements('Source').first.innerText);
|
||||
return {
|
||||
'sink': sink,
|
||||
'source': source,
|
||||
};
|
||||
}
|
||||
|
||||
Future<Set<String>?> getSinkSupportedMimeTypes() async {
|
||||
final sinkProtocolInfo = (await getProtocolInfo())['sink'];
|
||||
if (sinkProtocolInfo != null) {
|
||||
final byProtocol = groupBy<UpnpProtocolInfoEntry, String>(sinkProtocolInfo.entries, (v) => v.protocol);
|
||||
final httpGet = byProtocol['http-get'];
|
||||
if (httpGet != null) {
|
||||
return httpGet.map((v) => v.contentFormat).toSet();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue