Skip to content

Commit

Permalink
add selectable video quality in player
Browse files Browse the repository at this point in the history
add quality options if hls file provided
add dependency : flutter_hls_parser for format parsing
fix reverse button in android
  • Loading branch information
appdevelpo committed Dec 6, 2023
1 parent b6610ed commit 69afabf
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 12 deletions.
69 changes: 63 additions & 6 deletions lib/controllers/watch/video_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -56,21 +58,23 @@ class VideoPlayerController extends GetxController {
final subtitles = <ExtensionBangumiWatchSubtitle>[].obs;
final keyboardShortcuts = <ShortcutActivator, VoidCallback>{};
final selectedSubtitle = 0.obs;

final currentQality = "null".obs;
final qualityUrls = <String, String>{};
// 是否已经自动跳转到上次播放进度
bool _isAutoSeekPosition = false;

Map<String, String>? videoheaders = {};
final messageQueue = <Message>[];

final Rx<Widget?> cuurentMessageWidget = Rx(null);

final speed = 1.0.obs;

final torrentMediaFileList = <String>[].obs;

final currentTorrentFile = ''.obs;

String _torrenHash = "";
final ReceivePort qualityRereceivePort = ReceivePort();
Isolate? qualityReceiver;
// 复制当前 context

@override
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<MainController>().btServerisRunning.value == false) {
Expand All @@ -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());

Expand All @@ -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<String> 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!));
Expand All @@ -315,6 +354,7 @@ class VideoPlayerController extends GetxController {
await Future.delayed(const Duration(seconds: 3));

play();

return;
}
sendMessage(
Expand All @@ -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);
Expand Down
96 changes: 94 additions & 2 deletions lib/views/pages/watch/video/video_player_content.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -21,7 +22,8 @@ class VideoPlayerConten extends StatefulWidget {
class _VideoPlayerContenState extends State<VideoPlayerConten> {
late final _c = Get.find<VideoPlayerController>(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: [
Expand Down Expand Up @@ -196,7 +198,52 @@ class _VideoPlayerContenState extends State<VideoPlayerConten> {
),
];
},
)
),
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)))))
],
),
),
Expand Down Expand Up @@ -280,6 +327,51 @@ class _VideoPlayerContenState extends State<VideoPlayerConten> {
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<String>(
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(
Expand Down
15 changes: 12 additions & 3 deletions lib/views/widgets/detail/detail_episodes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,28 @@ class _DetailEpisodesState extends State<DetailEpisodes> {
),
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,
);
},
Expand Down
8 changes: 8 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 69afabf

Please sign in to comment.