From 69afabf6149459b048a3f25de2d300bdc410d73f Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Wed, 6 Dec 2023 19:31:14 +0800 Subject: [PATCH] add selectable video quality in player add quality options if hls file provided add dependency : flutter_hls_parser for format parsing fix reverse button in android --- lib/controllers/watch/video_controller.dart | 69 +++++++++++-- .../watch/video/video_player_content.dart | 96 ++++++++++++++++++- lib/views/widgets/detail/detail_episodes.dart | 15 ++- pubspec.lock | 8 ++ pubspec.yaml | 2 +- 5 files changed, 178 insertions(+), 12 deletions(-) diff --git a/lib/controllers/watch/video_controller.dart b/lib/controllers/watch/video_controller.dart index 1c39fec5..c16ccf67 100644 --- a/lib/controllers/watch/video_controller.dart +++ b/lib/controllers/watch/video_controller.dart @@ -3,9 +3,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; import 'package:flutter/material.dart'; -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' as dio; import 'package:file_picker/file_picker.dart'; import 'package:flutter/services.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; @@ -29,6 +30,7 @@ import 'package:path/path.dart' as path; import 'package:fluent_ui/fluent_ui.dart' as fluent; import 'package:crypto/crypto.dart'; import 'package:miru_app/utils/miru_storage.dart'; +import 'package:flutter_hls_parser/flutter_hls_parser.dart'; class VideoPlayerController extends GetxController { final String title; @@ -56,10 +58,11 @@ class VideoPlayerController extends GetxController { final subtitles = [].obs; final keyboardShortcuts = {}; final selectedSubtitle = 0.obs; - + final currentQality = "null".obs; + final qualityUrls = {}; // 是否已经自动跳转到上次播放进度 bool _isAutoSeekPosition = false; - + Map? videoheaders = {}; final messageQueue = []; final Rx cuurentMessageWidget = Rx(null); @@ -67,10 +70,11 @@ class VideoPlayerController extends GetxController { final speed = 1.0.obs; final torrentMediaFileList = [].obs; - final currentTorrentFile = ''.obs; String _torrenHash = ""; + final ReceivePort qualityRereceivePort = ReceivePort(); + Isolate? qualityReceiver; // 复制当前 context @override @@ -162,12 +166,26 @@ class VideoPlayerController extends GetxController { index.value++; } }); - + //畫質的listener + qualityRereceivePort.listen((message) async { + debugPrint("${message.keys} get"); + final resolution = message['resolution']; + final urls = message['urls']; + qualityUrls.addAll(Map.fromIterables(resolution, urls)); + qualityRereceivePort.close(); + qualityReceiver!.kill(); + }); + //讀取現在的畫質 + player.stream.height.listen((event) async { + final width = player.state.width; + currentQality.value = "${width}x$event"; + }); // 自动恢复上次播放进度 player.stream.duration.listen((event) async { if (_isAutoSeekPosition || event.inSeconds == 0) { return; } + // 获取上次播放进度 final history = await DatabaseService.getHistoryByPackageAndUrl( runtime.extension.package, @@ -254,6 +272,7 @@ class VideoPlayerController extends GetxController { selectedSubtitle.value = -1; final playUrl = playList[index.value].url; final watchData = await runtime.watch(playUrl) as ExtensionBangumiWatch; + videoheaders = watchData.headers; if (watchData.type == ExtensionWatchBangumiType.torrent) { if (Get.find().btServerisRunning.value == false) { @@ -269,7 +288,7 @@ class VideoPlayerController extends GetxController { await MiruDirectory.getCacheDirectory, 'temp.torrent', ); - await Dio().download(watchData.url, torrentFile); + await dio.Dio().download(watchData.url, torrentFile); final file = File(torrentFile); _torrenHash = await BTServerApi.addTorrent(file.readAsBytesSync()); @@ -291,6 +310,26 @@ class VideoPlayerController extends GetxController { } playTorrentFile(torrentMediaFileList.first); } else { + //背景取得畫質 + qualityReceiver = await Isolate.spawn((SendPort sendport) async { + dio.Dio dioReq = dio.Dio(); + try { + dio.Response response = await dioReq.get(watchData.url, + options: dio.Options(headers: watchData.headers)); + debugPrint(response.data); + final playList = await HlsPlaylistParser.create().parseString( + Uri.parse(watchData.url), response.data) as HlsMasterPlaylist; + List urlList = + playList.mediaPlaylistUrls.map((e) => e.toString()).toList(); + final resolution = playList.variants + .map((it) => "${it.format.width}x${it.format.height}"); + debugPrint("get sources"); + sendport.send({'resolution': resolution, 'urls': urlList}); + } catch (error) { + debugPrint('Error: $error'); + } + }, qualityRereceivePort.sendPort); + await player.open(Media(watchData.url, httpHeaders: watchData.headers)); if (watchData.audioTrack != null) { await player.setAudioTrack(AudioTrack.uri(watchData.audioTrack!)); @@ -315,6 +354,7 @@ class VideoPlayerController extends GetxController { await Future.delayed(const Duration(seconds: 3)); play(); + return; } sendMessage( @@ -338,6 +378,23 @@ class VideoPlayerController extends GetxController { isFullScreen.value = !isFullScreen.value; } + switchQuality(String qualityUrl) async { + final currentSecond = player.state.position.inSeconds; + try { + await player.open(Media(qualityUrl, httpHeaders: videoheaders)); + //跳轉到切換之前的時間 + Timer.periodic(const Duration(seconds: 1), (timer) { + player.seek(Duration(seconds: currentSecond)); + if (player.state.position.inSeconds == currentSecond) { + timer.cancel(); + } + }); + } catch (e) { + await Future.delayed(const Duration(seconds: 3)); + player.open(Media(qualityUrl, httpHeaders: videoheaders)); + } + } + onExit() async { if (_torrenHash.isNotEmpty) { BTServerApi.removeTorrent(_torrenHash); diff --git a/lib/views/pages/watch/video/video_player_content.dart b/lib/views/pages/watch/video/video_player_content.dart index a1ad98ea..bb848484 100644 --- a/lib/views/pages/watch/video/video_player_content.dart +++ b/lib/views/pages/watch/video/video_player_content.dart @@ -1,3 +1,4 @@ +import 'package:fluent_ui/fluent_ui.dart' as fluent; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:media_kit_video/media_kit_video.dart'; @@ -21,7 +22,8 @@ class VideoPlayerConten extends StatefulWidget { class _VideoPlayerContenState extends State { late final _c = Get.find(tag: widget.tag); final speeds = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0]; - + String? selected; + final menuController = fluent.FlyoutController(); Widget _buildDesktop(BuildContext context) { final topButtonBar = Row( children: [ @@ -196,7 +198,52 @@ class _VideoPlayerContenState extends State { ), ]; }, - ) + ), + TextButton( + onPressed: () { + fluent.showDialog( + context: context, + builder: (contex) { + return fluent.ContentDialog( + title: Text("choose-quality".i18n), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (final q in _c.qualityUrls.entries) + Row(children: [ + fluent.RadioButton( + checked: selected == q.key || + q.key == _c.currentQality.value, + onChanged: (checked) { + // debugPrint("$boolean"); + setState(() { + if (checked) { + selected = q.key; + _c.switchQuality( + _c.qualityUrls[q.key]!); + Navigator.pop(context); + } + }); + }), + Text( + q.key, + style: const TextStyle( + fontSize: 20, color: Colors.white), + ), + ]) + ], + ), + actions: [ + fluent.Button( + onPressed: () => Navigator.pop(context), + child: Text("cancel".i18n)) + ], + ); + }); + }, + child: Obx(() => (Text(_c.currentQality.value, + style: const TextStyle(color: Colors.white))))) ], ), ), @@ -280,6 +327,51 @@ class _VideoPlayerContenState extends State { data: Theme.of(context), child: Row( children: [ + SizedBox( + // height: 8, + // width: 10, + child: TextButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text("choose-quality".i18n), + content: Column( + children: [ + for (final q in _c.qualityUrls.entries) + RadioListTile( + title: Text(q.key), + value: q.value, + groupValue: _c.qualityUrls[ + _c.currentQality.value], + onChanged: (value) { + Navigator.pop(context); + // widget.applyValue(value as T); + debugPrint( + "$value value changed"); + + _c.currentQality.value = _c + .qualityUrls.keys + .firstWhere( + (element) => + _c.qualityUrls[ + element] == + value, + orElse: () => _c + .qualityUrls + .keys + .first); + setState(() {}); + _c.switchQuality(value!); + }, + ), + ], + ), + )); + }, + child: Obx(() => (Text(_c.currentQality.value, + style: const TextStyle(color: Colors.white)))))), + const SizedBox(width: 2), Padding( padding: const EdgeInsets.only(right: 8.0), child: PopupMenuButton( diff --git a/lib/views/widgets/detail/detail_episodes.dart b/lib/views/widgets/detail/detail_episodes.dart index 4a4bf6ca..eb9246d6 100644 --- a/lib/views/widgets/detail/detail_episodes.dart +++ b/lib/views/widgets/detail/detail_episodes.dart @@ -90,19 +90,28 @@ class _DetailEpisodesState extends State { ), Expanded( child: ListView.builder( - reverse: isRevered, padding: const EdgeInsets.all(0), itemCount: episodes.isEmpty ? 0 : episodes[c.selectEpGroup.value].urls.length, itemBuilder: (context, index) { return ListTile( - title: Text(episodes[c.selectEpGroup.value].urls[index].name), + title: isRevered + ? Text(episodes[c.selectEpGroup.value] + .urls[episodes[c.selectEpGroup.value].urls.length - + 1 - + index] + .name) + : Text(episodes[c.selectEpGroup.value].urls[index].name), onTap: () { c.goWatch( context, episodes[c.selectEpGroup.value].urls, - index, + isRevered + ? episodes[c.selectEpGroup.value].urls.length - + 1 - + index + : index, c.selectEpGroup.value, ); }, diff --git a/pubspec.lock b/pubspec.lock index dced9361..9905a1da 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -438,6 +438,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + flutter_hls_parser: + dependency: "direct main" + description: + name: flutter_hls_parser + sha256: f4b7df4f927623aea5c72006232693e07fdd11438f600c08670d6a23c1aa85c8 + url: "https://pub.dev" + source: hosted + version: "2.0.1" flutter_i18n: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 1f8344f2..785bd265 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,7 +56,7 @@ dependencies: android_intent_plus: ^4.0.2 crypto: ^3.0.3 extended_image: ^8.2.0 - + flutter_hls_parser: ^2.0.1 dev_dependencies: flutter_test: sdk: flutter