-
Notifications
You must be signed in to change notification settings - Fork 166
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #82 from WhiteXero/main
New features: DLNA video cast & External video player support
- Loading branch information
Showing
7 changed files
with
267 additions
and
12 deletions.
There are no files selected for viewing
35 changes: 34 additions & 1 deletion
35
android/app/src/main/kotlin/com/example/kazumi/MainActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,38 @@ | ||
package com.example.kazumi | ||
|
||
import android.content.Intent | ||
import android.net.Uri | ||
import android.os.Bundle | ||
import androidx.annotation.NonNull | ||
import io.flutter.embedding.engine.FlutterEngine | ||
import io.flutter.plugin.common.MethodChannel | ||
import io.flutter.embedding.android.FlutterActivity | ||
|
||
class MainActivity: FlutterActivity() | ||
class MainActivity: FlutterActivity() { | ||
private val CHANNEL = "com.predidit.kazumi/intent" | ||
|
||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { | ||
super.configureFlutterEngine(flutterEngine) | ||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> | ||
if (call.method == "openWithMime") { | ||
val url = call.argument<String>("url") | ||
val mimeType = call.argument<String>("mimeType") | ||
if (url != null && mimeType != null) { | ||
openWithMime(url, mimeType) | ||
result.success(null) | ||
} else { | ||
result.error("INVALID_ARGUMENT", "URL and MIME type required", null) | ||
} | ||
} else { | ||
result.notImplemented() | ||
} | ||
} | ||
} | ||
|
||
private fun openWithMime(url: String, mimeType: String) { | ||
val intent = Intent() | ||
intent.action = Intent.ACTION_VIEW | ||
intent.setDataAndType(Uri.parse(url), mimeType) | ||
startActivity(intent) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
import 'dart:async'; | ||
import 'dart:io'; | ||
|
||
import 'package:dlna_dart/dlna.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:flutter/services.dart'; | ||
import 'package:flutter_modular/flutter_modular.dart'; | ||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; | ||
import 'package:url_launcher/url_launcher_string.dart'; | ||
|
||
import '../pages/player/player_controller.dart'; | ||
|
||
class RemotePlay { | ||
|
||
// 注意:仍需开发非 Android 设备的远程播放功能。 | ||
// 外部播放使用 MethodChannel 方法实现,而这只在 Android 设备中有效。 | ||
// 对于 Windows 设备,使用了 url_launcher 以支持浏览器的播放。 | ||
// 在 Windows 设备上,使用 scheme 的方案没有效果。VLC / PotPlayer 等主流播放器更倾向于使用 CLI 命令。 | ||
// 而对于 iOS / Mac 设备,由于没有设备,无法进行开发与验证。 | ||
// 可行的 iOS / Mac 处理代码,请参见 ios/Runner/AppDelegate.swift 的注释部分。 | ||
|
||
static const platform = MethodChannel('com.predidit.kazumi/intent'); | ||
|
||
castVideo(BuildContext context) async { | ||
final searcher = DLNAManager(); | ||
final dlna = await searcher.start(); | ||
final String video = Modular.get<PlayerController>().videoUrl; | ||
List<Widget> dlnaDevice = []; | ||
SmartDialog.show(builder: (context) { | ||
return StatefulBuilder(builder: (context, setState) { | ||
return AlertDialog( | ||
title: const Text('远程播放'), | ||
content: SingleChildScrollView( | ||
child: Column( | ||
children: dlnaDevice, | ||
), | ||
), | ||
actions: [ | ||
TextButton( | ||
onPressed: () async { | ||
searcher.stop(); | ||
if (Platform.isAndroid) { | ||
if (await _launchURLWithMIME(video, 'video/mp4')) { | ||
SmartDialog.dismiss(); | ||
SmartDialog.showToast( | ||
'尝试唤起外部播放器',displayType: SmartToastType.onlyRefresh); | ||
} | ||
else | ||
{ | ||
SmartDialog.showToast( | ||
'无法使用外部播放器',displayType: SmartToastType.onlyRefresh); | ||
} | ||
} | ||
else if (Platform.isWindows) { | ||
SmartDialog.dismiss(); | ||
if (await canLaunchUrlString(video) == true) { | ||
launchUrlString(video); | ||
SmartDialog.showToast( | ||
'尝试唤起外部播放器',displayType: SmartToastType.onlyRefresh); | ||
} | ||
else { | ||
SmartDialog.showToast( | ||
'无法使用外部播放器',displayType: SmartToastType.onlyRefresh); | ||
} | ||
} | ||
else { | ||
SmartDialog.showToast( | ||
'暂不支持该设备', | ||
displayType: SmartToastType.onlyRefresh | ||
); | ||
} | ||
}, | ||
child: const Text('外部播放'), | ||
), | ||
const SizedBox(width: 20), | ||
TextButton( | ||
onPressed: () { | ||
searcher.stop(); | ||
SmartDialog.dismiss(); | ||
}, | ||
child: Text( | ||
'退出', | ||
style: TextStyle(color: Theme.of(context).colorScheme.outline), | ||
), | ||
), | ||
TextButton( | ||
onPressed: () { | ||
setState(() {}); | ||
SmartDialog.showToast( | ||
'开始搜索', | ||
displayType: SmartToastType.onlyRefresh | ||
); | ||
dlna.devices.stream.listen((deviceList) { | ||
dlnaDevice = []; | ||
deviceList.forEach((key, value) async { | ||
debugPrint('Key: $key'); | ||
debugPrint( | ||
'Value: ${value.info.friendlyName} ${value.info.deviceType} ${value.info.URLBase}'); | ||
setState(() { | ||
dlnaDevice.add(ListTile( | ||
leading: _deviceUPnPIcon( | ||
value.info.deviceType.split(':')[3]), | ||
title: Text(value.info.friendlyName), | ||
subtitle: Text(value.info.deviceType.split(':')[3]), | ||
onTap: () { | ||
try { | ||
SmartDialog.showToast( | ||
'尝试投屏至 ${value.info.friendlyName}', | ||
displayType: SmartToastType.onlyRefresh); | ||
DLNADevice(value.info).setUrl(video); | ||
DLNADevice(value.info).play(); | ||
} | ||
catch (e) { | ||
debugPrint('DLNA Error: $e'); | ||
SmartDialog.showNotify(msg: 'DLNA 异常: $e \n尝试重新进入 DLNA 投屏或切换设备', notifyType: NotifyType.alert); | ||
} | ||
})); | ||
}); | ||
}); | ||
}); | ||
Timer(const Duration(seconds: 30), () { | ||
SmartDialog.showToast( | ||
'已搜索30s,若未发现设备请尝试重新进入 DLNA 投屏', | ||
displayType: SmartToastType.onlyRefresh, | ||
); | ||
}); | ||
}, | ||
child: Text( | ||
'搜索', | ||
style: | ||
TextStyle(color: Theme.of(context).colorScheme.outline), | ||
)), | ||
], | ||
); | ||
}); | ||
}); | ||
} | ||
|
||
Icon _deviceUPnPIcon(String deviceType) { | ||
switch (deviceType) { | ||
case 'MediaRenderer': | ||
return const Icon(Icons.cast_connected); | ||
case 'MediaServer': | ||
return const Icon(Icons.cast_connected); | ||
case 'InternetGatewayDevice': | ||
return const Icon(Icons.router); | ||
case 'BasicDevice': | ||
return const Icon(Icons.device_hub); | ||
case 'DimmableLight': | ||
return const Icon(Icons.lightbulb); | ||
case 'WLANAccessPoint': | ||
return const Icon(Icons.lan); | ||
case 'WLANConnectionDevice': | ||
return const Icon(Icons.wifi_tethering); | ||
case 'Printer': | ||
return const Icon(Icons.print); | ||
case 'Scanner': | ||
return const Icon(Icons.scanner); | ||
case 'DigitalSecurityCamera': | ||
return const Icon(Icons.camera_enhance_outlined); | ||
default: | ||
return const Icon(Icons.question_mark); | ||
} | ||
} | ||
|
||
Future<bool> _launchURLWithMIME(String url, String mimeType) async { | ||
try { | ||
await platform.invokeMethod('openWithMime', <String, String>{ | ||
'url': url, | ||
'mimeType': mimeType | ||
}); | ||
return true; | ||
} on PlatformException catch (e) { | ||
debugPrint("Failed to open with mime: '${e.message}'."); | ||
return false; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters