Skip to content

Commit

Permalink
Merge pull request #82 from WhiteXero/main
Browse files Browse the repository at this point in the history
New features: DLNA video cast & External video player support
  • Loading branch information
Predidit authored Jul 22, 2024
2 parents 04434d9 + e3426ac commit 98f9562
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 12 deletions.
35 changes: 34 additions & 1 deletion android/app/src/main/kotlin/com/example/kazumi/MainActivity.kt
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)
}
}
45 changes: 37 additions & 8 deletions ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,40 @@ import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
// let channel = FlutterMethodChannel(name: "com.predidit.kazumi/intent",
// binaryMessenger: controller.binaryMessenger)
// channel.setMethodCallHandler({
// (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
// if call.method == "openVideoWithMime" {
// guard let args = call.arguments else { return }
// if let myArgs = args as? [String: Any],
// let url = myArgs["url"] as? String,
// let mimeType = myArgs["mimeType"] as? String {
// self.openVideoWithMime(url: url, mimeType: mimeType)
// }
// result(nil)
// } else {
// result(FlutterMethodNotImplemented)
// }
// })
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

// private func openVideoWithMime(url: String, mimeType: String) {
// if let videoUrl = URL(string: url) {
// let player = AVPlayer(url: videoUrl)
// let playerViewController = AVPlayerViewController()
// playerViewController.player = player
//
// UIApplication.shared.keyWindow?.rootViewController?.present(playerViewController, animated: true, completion: {
// playerViewController.player!.play()
// })
// }
// }
}
11 changes: 9 additions & 2 deletions lib/pages/player/player_item.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'package:kazumi/utils/remote.dart';
import 'package:kazumi/utils/utils.dart';
import 'package:kazumi/utils/webdav.dart';
import 'package:provider/provider.dart';
Expand Down Expand Up @@ -781,8 +782,7 @@ class _PlayerItemState extends State<PlayerItem>
),
child: const Row(
children: <Widget>[
Icon(
Icons.fast_forward,
Icon(Icons.fast_forward,
color: Colors.white),
Text(
' 倍速播放',
Expand Down Expand Up @@ -917,6 +917,13 @@ class _PlayerItemState extends State<PlayerItem>
child: dtb.DragToMoveArea(
child: SizedBox(height: 40)),
),
IconButton(
color: Colors.white,
icon: const Icon(Icons.cast),
onPressed: () {
RemotePlay().castVideo(context);
},
),
TextButton(
style: ButtonStyle(
padding:
Expand Down
1 change: 0 additions & 1 deletion lib/pages/webview_desktop/webview_desktop_controller.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:kazumi/utils/utils.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:kazumi/pages/video/video_controller.dart';
import 'package:kazumi/pages/player/player_controller.dart';
Expand Down
178 changes: 178 additions & 0 deletions lib/utils/remote.dart
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;
}
}
}
8 changes: 8 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.5.2"
dlna_dart:
dependency: "direct main"
description:
name: dlna_dart
sha256: ae07c1c53077bbf58756fa589f936968719b0085441981d33e74f82f89d1d281
url: "https://pub.dev"
source: hosted
version: "0.0.8"
expressions:
dependency: transitive
description:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ dependencies:
animated_search_bar: ^2.7.1
webdav_client: ^1.2.2
tray_manager: ^0.2.3
dlna_dart: ^0.0.8
webview_windows:
git:
url: https://github.com/Predidit/flutter-webview-windows.git
Expand Down

0 comments on commit 98f9562

Please sign in to comment.