mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
feat: support dynaudnorm & webp (#1186)
* feat: support dynaudnorm & webp
* Revert "remove audio_normalization"
This reverts commit 477b59ce89.
* feat: save webp
* mod: strokeWidth
* feat: webp preset
* feat: save webp select qa
* upgrade volume_controller
This commit is contained in:
committed by
GitHub
parent
f0828ea18c
commit
e8a674ca2a
54
lib/common/widgets/loading_widget.dart
Normal file
54
lib/common/widgets/loading_widget.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class LoadingWidget extends StatelessWidget {
|
||||
const LoadingWidget({
|
||||
super.key,
|
||||
this.msg = 'loading...',
|
||||
required this.progress,
|
||||
});
|
||||
|
||||
///loading msg
|
||||
final String msg;
|
||||
final RxDouble progress;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final onSurfaceVariant = theme.colorScheme.onSurfaceVariant;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.dialogTheme.backgroundColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
//loading animation
|
||||
RepaintBoundary.wrap(
|
||||
Obx(
|
||||
() => CustomPaint(
|
||||
size: const Size.square(40),
|
||||
painter: ArcPainter(
|
||||
color: onSurfaceVariant,
|
||||
strokeWidth: 3,
|
||||
sweepAngle: progress.value * 2 * pi,
|
||||
),
|
||||
),
|
||||
),
|
||||
0,
|
||||
),
|
||||
//msg
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: Text(msg, style: TextStyle(color: onSurfaceVariant)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
10
lib/models/common/audio_normalization.dart
Normal file
10
lib/models/common/audio_normalization.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
enum AudioNormalization {
|
||||
disable('禁用'),
|
||||
// ref https://github.com/KRTirtho/spotube/commit/da10ab2e291d4ba4d3082b9a6ae535639fb8f1b7
|
||||
dynaudnorm('预设 dynaudnorm', 'dynaudnorm=g=5:f=250:r=0.9:p=0.5'),
|
||||
custom('自定义参数');
|
||||
|
||||
final String title;
|
||||
final String param;
|
||||
const AudioNormalization(this.title, [this.param = '']);
|
||||
}
|
||||
@@ -1,34 +1,19 @@
|
||||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
enum VideoDecodeFormatType {
|
||||
DVH1,
|
||||
AV1,
|
||||
HEVC,
|
||||
AVC,
|
||||
}
|
||||
|
||||
extension VideoDecodeFormatTypeExt on VideoDecodeFormatType {
|
||||
String get description => const ['DVH1', 'AV1', 'HEVC', 'AVC'][index];
|
||||
|
||||
static const List<String> _codeList = ['dvh1', 'av01', 'hev1', 'avc1'];
|
||||
String get code => _codeList[index];
|
||||
|
||||
static VideoDecodeFormatType? fromCode(String code) {
|
||||
final index = _codeList.indexOf(code);
|
||||
if (index != -1) {
|
||||
return VideoDecodeFormatType.values[index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static VideoDecodeFormatType? fromString(String val) {
|
||||
var result = VideoDecodeFormatType.values.first;
|
||||
for (var i in _codeList) {
|
||||
if (val.startsWith(i)) {
|
||||
result = VideoDecodeFormatType.values[_codeList.indexOf(i)];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
DVH1('dvh1'),
|
||||
AV1('av01'),
|
||||
HEVC('hev1'),
|
||||
AVC('avc1');
|
||||
|
||||
String get description => name;
|
||||
final String code;
|
||||
|
||||
const VideoDecodeFormatType(this.code);
|
||||
|
||||
static VideoDecodeFormatType fromCode(String code) =>
|
||||
values.firstWhere((i) => i.code == code);
|
||||
|
||||
static VideoDecodeFormatType fromString(String val) =>
|
||||
values.firstWhere((i) => val.startsWith(i.code));
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:PiliPlus/common/widgets/radio_widget.dart';
|
||||
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
|
||||
import 'package:PiliPlus/grpc/reply.dart';
|
||||
import 'package:PiliPlus/http/fav.dart';
|
||||
import 'package:PiliPlus/models/common/audio_normalization.dart';
|
||||
import 'package:PiliPlus/models/common/dynamic/dynamics_type.dart';
|
||||
import 'package:PiliPlus/models/common/member/tab_type.dart';
|
||||
import 'package:PiliPlus/models/common/reply/reply_sort_type.dart';
|
||||
@@ -438,6 +439,94 @@ List<SettingsModel> get extraSettings => [
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
SettingsModel(
|
||||
settingsType: SettingsType.normal,
|
||||
title: '音量均衡',
|
||||
setKey: SettingBoxKey.audioNormalization,
|
||||
leading: const Icon(Icons.multitrack_audio),
|
||||
getSubtitle: () {
|
||||
String audioNormalization = Pref.audioNormalization;
|
||||
// TODO: remove next version
|
||||
if (audioNormalization == '2') {
|
||||
GStorage.setting.put(SettingBoxKey.audioNormalization, '1');
|
||||
audioNormalization = '1';
|
||||
}
|
||||
audioNormalization = switch (audioNormalization) {
|
||||
'0' => AudioNormalization.disable.title,
|
||||
'1' => AudioNormalization.dynaudnorm.title,
|
||||
_ => audioNormalization,
|
||||
};
|
||||
return '当前:「$audioNormalization」';
|
||||
},
|
||||
onTap: (setState) async {
|
||||
String? result = await showDialog(
|
||||
context: Get.context!,
|
||||
builder: (context) {
|
||||
String audioNormalization = Pref.audioNormalization;
|
||||
final values = {'0', '1', audioNormalization, '2'};
|
||||
return SelectDialog<String>(
|
||||
title: '音量均衡',
|
||||
value: audioNormalization,
|
||||
values: values
|
||||
.map(
|
||||
(e) => (
|
||||
e,
|
||||
switch (e) {
|
||||
'0' => AudioNormalization.disable.title,
|
||||
'1' => AudioNormalization.dynaudnorm.title,
|
||||
'2' => AudioNormalization.custom.title,
|
||||
_ => e,
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (result != null) {
|
||||
if (result == '2') {
|
||||
String param = '';
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('自定义参数'),
|
||||
content: TextField(
|
||||
autofocus: true,
|
||||
onChanged: (value) => param = value,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
await GStorage.setting.put(
|
||||
SettingBoxKey.audioNormalization,
|
||||
param,
|
||||
);
|
||||
setState();
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await GStorage.setting.put(SettingBoxKey.audioNormalization, result);
|
||||
setState();
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
SettingsModel(
|
||||
settingsType: SettingsType.normal,
|
||||
title: '超分辨率',
|
||||
|
||||
@@ -242,7 +242,7 @@ List<SettingsModel> get videoSettings => [
|
||||
title: '首选解码格式',
|
||||
leading: const Icon(Icons.movie_creation_outlined),
|
||||
getSubtitle: () =>
|
||||
'首选解码格式:${VideoDecodeFormatTypeExt.fromCode(Pref.defaultDecode)!.description},请根据设备支持情况与需求调整',
|
||||
'首选解码格式:${VideoDecodeFormatType.fromCode(Pref.defaultDecode).description},请根据设备支持情况与需求调整',
|
||||
onTap: (setState) async {
|
||||
String? result = await showDialog(
|
||||
context: Get.context!,
|
||||
@@ -266,7 +266,7 @@ List<SettingsModel> get videoSettings => [
|
||||
settingsType: SettingsType.normal,
|
||||
title: '次选解码格式',
|
||||
getSubtitle: () =>
|
||||
'非杜比视频次选:${VideoDecodeFormatTypeExt.fromCode(Pref.secondDecode)!.description},仍无则选择首个提供的解码格式',
|
||||
'非杜比视频次选:${VideoDecodeFormatType.fromCode(Pref.secondDecode).description},仍无则选择首个提供的解码格式',
|
||||
leading: const Icon(Icons.swap_horizontal_circle_outlined),
|
||||
onTap: (setState) async {
|
||||
String? result = await showDialog(
|
||||
|
||||
@@ -56,12 +56,12 @@ import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
|
||||
import 'package:get/get.dart' hide ContextExtensionss;
|
||||
import 'package:get/get_navigation/src/dialog/dialog_route.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:volume_controller/volume_controller.dart';
|
||||
|
||||
class VideoDetailController extends GetxController
|
||||
with GetTickerProviderStateMixin {
|
||||
@@ -993,6 +993,33 @@ class VideoDetailController extends GetxController
|
||||
}
|
||||
}
|
||||
|
||||
VideoItem findVideoByQa(int qa) {
|
||||
/// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl
|
||||
final videoList = data.dash!.video!.where((i) => i.id == qa).toList();
|
||||
|
||||
final currentDecodeFormats = this.currentDecodeFormats.code;
|
||||
final defaultDecodeFormats = VideoDecodeFormatType.fromString(
|
||||
cacheDecode,
|
||||
).code;
|
||||
final secondDecodeFormats = VideoDecodeFormatType.fromString(
|
||||
cacheSecondDecode,
|
||||
).code;
|
||||
|
||||
VideoItem? video;
|
||||
for (var i in videoList) {
|
||||
final codec = i.codecs!;
|
||||
if (codec.startsWith(currentDecodeFormats)) {
|
||||
video = i;
|
||||
break;
|
||||
} else if (codec.startsWith(defaultDecodeFormats)) {
|
||||
video = i;
|
||||
} else if (video == null && codec.startsWith(secondDecodeFormats)) {
|
||||
video = i;
|
||||
}
|
||||
}
|
||||
return video ?? videoList.first;
|
||||
}
|
||||
|
||||
/// 更新画质、音质
|
||||
void updatePlayer() {
|
||||
autoPlay.value = true;
|
||||
@@ -1001,76 +1028,17 @@ class VideoDetailController extends GetxController
|
||||
plPlayerController.isBuffering.value = false;
|
||||
plPlayerController.buffered.value = Duration.zero;
|
||||
|
||||
/// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl
|
||||
List<VideoItem> videoList = data.dash!.video!
|
||||
.where((i) => i.id == currentVideoQa.value.code)
|
||||
.toList();
|
||||
|
||||
final List<String> supportDecodeFormats = videoList
|
||||
.map((e) => e.codecs!)
|
||||
.toList();
|
||||
VideoDecodeFormatType defaultDecodeFormats =
|
||||
VideoDecodeFormatTypeExt.fromString(cacheDecode)!;
|
||||
VideoDecodeFormatType secondDecodeFormats =
|
||||
VideoDecodeFormatTypeExt.fromString(cacheSecondDecode)!;
|
||||
try {
|
||||
// 当前视频没有对应格式返回第一个
|
||||
int flag = 0;
|
||||
for (var i in supportDecodeFormats) {
|
||||
if (i.startsWith(currentDecodeFormats.code)) {
|
||||
flag = 1;
|
||||
break;
|
||||
} else if (i.startsWith(defaultDecodeFormats.code)) {
|
||||
flag = 2;
|
||||
} else if (i.startsWith(secondDecodeFormats.code)) {
|
||||
if (flag == 0) {
|
||||
flag = 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (flag == 1) {
|
||||
//currentDecodeFormats
|
||||
firstVideo = videoList.firstWhere(
|
||||
(i) => i.codecs!.startsWith(currentDecodeFormats.code),
|
||||
orElse: () => videoList.first,
|
||||
);
|
||||
} else {
|
||||
if (currentVideoQa.value == VideoQuality.dolbyVision) {
|
||||
currentDecodeFormats = VideoDecodeFormatTypeExt.fromString(
|
||||
videoList.first.codecs!,
|
||||
)!;
|
||||
firstVideo = videoList.first;
|
||||
} else if (flag == 2) {
|
||||
//defaultDecodeFormats
|
||||
currentDecodeFormats = defaultDecodeFormats;
|
||||
firstVideo = videoList.firstWhere(
|
||||
(i) => i.codecs!.startsWith(defaultDecodeFormats.code),
|
||||
orElse: () => videoList.first,
|
||||
);
|
||||
} else if (flag == 4) {
|
||||
//secondDecodeFormats
|
||||
currentDecodeFormats = secondDecodeFormats;
|
||||
firstVideo = videoList.firstWhere(
|
||||
(i) => i.codecs!.startsWith(secondDecodeFormats.code),
|
||||
orElse: () => videoList.first,
|
||||
);
|
||||
} else if (flag == 0) {
|
||||
currentDecodeFormats = VideoDecodeFormatTypeExt.fromString(
|
||||
supportDecodeFormats.first,
|
||||
)!;
|
||||
firstVideo = videoList.first;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
SmartDialog.showToast('DecodeFormats error: $err');
|
||||
final video = findVideoByQa(currentVideoQa.value.code);
|
||||
if (firstVideo.codecs != video.codecs) {
|
||||
currentDecodeFormats = VideoDecodeFormatType.fromString(video.codecs!);
|
||||
}
|
||||
|
||||
videoUrl = firstVideo.baseUrl!;
|
||||
firstVideo = video;
|
||||
videoUrl = video.baseUrl!;
|
||||
|
||||
/// 根据currentAudioQa 重新设置audioUrl
|
||||
if (currentAudioQa != null) {
|
||||
final AudioItem firstAudio = data.dash!.audio!.firstWhere(
|
||||
(AudioItem i) => i.id == currentAudioQa!.code,
|
||||
final firstAudio = data.dash!.audio!.firstWhere(
|
||||
(i) => i.id == currentAudioQa!.code,
|
||||
orElse: () => data.dash!.audio!.first,
|
||||
);
|
||||
audioUrl = firstAudio.baseUrl ?? '';
|
||||
@@ -1204,7 +1172,7 @@ class VideoDetailController extends GetxController
|
||||
quality: VideoQuality.fromCode(data.quality!),
|
||||
);
|
||||
setVideoHeight();
|
||||
currentDecodeFormats = VideoDecodeFormatTypeExt.fromString('avc1')!;
|
||||
currentDecodeFormats = VideoDecodeFormatType.fromString('avc1');
|
||||
currentVideoQa = Rx(VideoQuality.fromCode(data.quality!));
|
||||
if (autoPlay.value || plPlayerController.preInitPlayer) {
|
||||
await playerInit();
|
||||
@@ -1256,9 +1224,9 @@ class VideoDetailController extends GetxController
|
||||
)
|
||||
.codecs!;
|
||||
// 默认从设置中取AV1
|
||||
currentDecodeFormats = VideoDecodeFormatTypeExt.fromString(cacheDecode)!;
|
||||
currentDecodeFormats = VideoDecodeFormatType.fromString(cacheDecode);
|
||||
VideoDecodeFormatType secondDecodeFormats =
|
||||
VideoDecodeFormatTypeExt.fromString(cacheSecondDecode)!;
|
||||
VideoDecodeFormatType.fromString(cacheSecondDecode);
|
||||
// 当前视频没有对应格式返回第一个
|
||||
int flag = 0;
|
||||
for (var i in supportDecodeFormats) {
|
||||
@@ -1272,9 +1240,9 @@ class VideoDetailController extends GetxController
|
||||
if (flag == 2) {
|
||||
currentDecodeFormats = secondDecodeFormats;
|
||||
} else if (flag == 0) {
|
||||
currentDecodeFormats = VideoDecodeFormatTypeExt.fromString(
|
||||
currentDecodeFormats = VideoDecodeFormatType.fromString(
|
||||
supportDecodeFormats.first,
|
||||
)!;
|
||||
);
|
||||
}
|
||||
|
||||
/// 取出符合当前解码格式的videoItem
|
||||
@@ -1539,7 +1507,7 @@ class VideoDetailController extends GetxController
|
||||
if (idx == 0) {
|
||||
if (preference == SubtitlePrefType.on ||
|
||||
(preference == SubtitlePrefType.auto &&
|
||||
(await FlutterVolumeController.getVolume() ?? 0) <= 0)) {
|
||||
await VolumeController.instance.getVolume() <= 0)) {
|
||||
idx = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class ActionItem extends StatelessWidget {
|
||||
animation: animation!,
|
||||
builder: (context, child) => CustomPaint(
|
||||
size: const Size.square(28),
|
||||
painter: _ArcPainter(
|
||||
painter: ArcPainter(
|
||||
color: primary,
|
||||
sweepAngle: animation!.value,
|
||||
),
|
||||
@@ -110,13 +110,15 @@ class ActionItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _ArcPainter extends CustomPainter {
|
||||
const _ArcPainter({
|
||||
class ArcPainter extends CustomPainter {
|
||||
const ArcPainter({
|
||||
required this.color,
|
||||
required this.sweepAngle,
|
||||
this.strokeWidth = 2,
|
||||
});
|
||||
final Color color;
|
||||
final double sweepAngle;
|
||||
final double strokeWidth;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
@@ -126,7 +128,7 @@ class _ArcPainter extends CustomPainter {
|
||||
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 2
|
||||
..strokeWidth = strokeWidth
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final rect = Rect.fromCircle(
|
||||
@@ -140,7 +142,7 @@ class _ArcPainter extends CustomPainter {
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _ArcPainter oldDelegate) {
|
||||
bool shouldRepaint(covariant ArcPainter oldDelegate) {
|
||||
return sweepAngle != oldDelegate.sweepAngle || color != oldDelegate.color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,154 @@ class PostPanel extends CommonCollapseSlidePage {
|
||||
|
||||
@override
|
||||
State<PostPanel> createState() => _PostPanelState();
|
||||
|
||||
static void updateSegment({
|
||||
required bool isFirst,
|
||||
required PostSegmentModel item,
|
||||
required double value,
|
||||
}) {
|
||||
if (isFirst) {
|
||||
item.segment.first = value;
|
||||
} else {
|
||||
item.segment.second = value;
|
||||
}
|
||||
if (item.category == SegmentType.poi_highlight ||
|
||||
item.actionType == ActionType.full) {
|
||||
item.segment.second = value;
|
||||
}
|
||||
}
|
||||
|
||||
static Widget segmentWidget(
|
||||
ThemeData theme, {
|
||||
required PostSegmentModel item,
|
||||
required double currentPos,
|
||||
required double videoDuration,
|
||||
}) {
|
||||
List<Widget> segment(BuildContext context, bool isFirst) {
|
||||
String value = DurationUtil.formatDuration(
|
||||
isFirst ? item.segment.first : item.segment.second,
|
||||
);
|
||||
return [
|
||||
Text(
|
||||
'${isFirst ? '开始' : '结束'}: $value',
|
||||
),
|
||||
iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: '设为当前',
|
||||
icon: Icons.my_location,
|
||||
onPressed: () {
|
||||
updateSegment(
|
||||
isFirst: isFirst,
|
||||
item: item,
|
||||
value: currentPos,
|
||||
);
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
),
|
||||
iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: isFirst ? '视频开头' : '视频结尾',
|
||||
icon: isFirst ? Icons.first_page : Icons.last_page,
|
||||
onPressed: () {
|
||||
updateSegment(
|
||||
isFirst: isFirst,
|
||||
item: item,
|
||||
value: isFirst ? 0 : videoDuration,
|
||||
);
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
),
|
||||
iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: '编辑',
|
||||
icon: Icons.edit,
|
||||
onPressed: () {
|
||||
showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
String initV = value;
|
||||
return AlertDialog(
|
||||
content: TextFormField(
|
||||
initialValue: value,
|
||||
autofocus: true,
|
||||
onChanged: (value) => initV = value,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[\d:.]+')),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: theme.colorScheme.outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: initV),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
).then((res) {
|
||||
if (res != null) {
|
||||
try {
|
||||
List<num> split = res
|
||||
.split(':')
|
||||
.reversed
|
||||
.map(num.parse)
|
||||
.toList();
|
||||
double duration = 0;
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
duration += split[i] * pow(60, i);
|
||||
}
|
||||
if (duration <= videoDuration) {
|
||||
updateSegment(
|
||||
isFirst: isFirst,
|
||||
item: item,
|
||||
value: duration,
|
||||
);
|
||||
(context as Element).markNeedsBuild();
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
final child = Builder(
|
||||
builder: (context) => Row(
|
||||
spacing: 5,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: segment(context, true),
|
||||
),
|
||||
);
|
||||
if (item.category != SegmentType.poi_highlight) {
|
||||
return Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 16,
|
||||
children: [
|
||||
child,
|
||||
Builder(
|
||||
builder: (context) => Row(
|
||||
spacing: 5,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: segment(context, false),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
|
||||
@@ -162,130 +310,6 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
|
||||
);
|
||||
}
|
||||
|
||||
void updateSegment({
|
||||
required bool isFirst,
|
||||
required PostSegmentModel item,
|
||||
required double value,
|
||||
}) {
|
||||
if (isFirst) {
|
||||
item.segment.first = value;
|
||||
} else {
|
||||
item.segment.second = value;
|
||||
}
|
||||
if (item.category == SegmentType.poi_highlight ||
|
||||
item.actionType == ActionType.full) {
|
||||
item.segment.second = value;
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> segmentWidget(
|
||||
BuildContext context,
|
||||
ThemeData theme, {
|
||||
required PostSegmentModel item,
|
||||
required bool isFirst,
|
||||
}) {
|
||||
String value = DurationUtil.formatDuration(
|
||||
isFirst ? item.segment.first : item.segment.second,
|
||||
);
|
||||
return [
|
||||
Text(
|
||||
'${isFirst ? '开始' : '结束'}: $value',
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: '设为当前',
|
||||
icon: Icons.my_location,
|
||||
onPressed: () {
|
||||
updateSegment(
|
||||
isFirst: isFirst,
|
||||
item: item,
|
||||
value: currentPos,
|
||||
);
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: isFirst ? '视频开头' : '视频结尾',
|
||||
icon: isFirst ? Icons.first_page : Icons.last_page,
|
||||
onPressed: () {
|
||||
updateSegment(
|
||||
isFirst: isFirst,
|
||||
item: item,
|
||||
value: isFirst ? 0 : videoDuration,
|
||||
);
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: '编辑',
|
||||
icon: Icons.edit,
|
||||
onPressed: () {
|
||||
showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
String initV = value;
|
||||
return AlertDialog(
|
||||
content: TextFormField(
|
||||
initialValue: value,
|
||||
autofocus: true,
|
||||
onChanged: (value) => initV = value,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[\d:.]+')),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: theme.colorScheme.outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: initV),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
).then((res) {
|
||||
if (res != null) {
|
||||
try {
|
||||
List<num> split = res
|
||||
.split(':')
|
||||
.reversed
|
||||
.map(num.parse)
|
||||
.toList();
|
||||
double duration = 0;
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
duration += split[i] * pow(60, i);
|
||||
}
|
||||
if (duration <= videoDuration) {
|
||||
updateSegment(
|
||||
isFirst: isFirst,
|
||||
item: item,
|
||||
value: duration,
|
||||
);
|
||||
(context as Element).markNeedsBuild();
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _onPost() {
|
||||
Request()
|
||||
.post(
|
||||
@@ -376,42 +400,13 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (item.actionType != ActionType.full) ...[
|
||||
Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 16,
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: segmentWidget(
|
||||
context,
|
||||
theme,
|
||||
isFirst: true,
|
||||
item: item,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (item.category != SegmentType.poi_highlight)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: segmentWidget(
|
||||
context,
|
||||
theme,
|
||||
isFirst: false,
|
||||
item: item,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
if (item.actionType != ActionType.full)
|
||||
PostPanel.segmentWidget(
|
||||
theme,
|
||||
item: item,
|
||||
currentPos: currentPos,
|
||||
videoDuration: videoDuration,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 16,
|
||||
@@ -430,14 +425,14 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
|
||||
}
|
||||
switch (e) {
|
||||
case SegmentType.poi_highlight:
|
||||
updateSegment(
|
||||
PostPanel.updateSegment(
|
||||
isFirst: false,
|
||||
item: item,
|
||||
value: item.segment.first,
|
||||
);
|
||||
break;
|
||||
case SegmentType.exclusive_access:
|
||||
updateSegment(
|
||||
PostPanel.updateSegment(
|
||||
isFirst: true,
|
||||
item: item,
|
||||
value: 0,
|
||||
@@ -491,7 +486,7 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
|
||||
onSelected: (e) {
|
||||
item.actionType = e;
|
||||
if (e == ActionType.full) {
|
||||
updateSegment(
|
||||
PostPanel.updateSegment(
|
||||
isFirst: true,
|
||||
item: item,
|
||||
value: 0,
|
||||
|
||||
@@ -837,7 +837,7 @@ class HeaderControlState extends TripleState<HeaderControl> {
|
||||
}
|
||||
videoDetailCtr
|
||||
..currentDecodeFormats =
|
||||
VideoDecodeFormatTypeExt.fromString(i)!
|
||||
VideoDecodeFormatType.fromString(i)
|
||||
..updatePlayer();
|
||||
Get.back();
|
||||
},
|
||||
@@ -846,7 +846,7 @@ class HeaderControlState extends TripleState<HeaderControl> {
|
||||
right: 20,
|
||||
),
|
||||
title: Text(
|
||||
VideoDecodeFormatTypeExt.fromString(i)!.description,
|
||||
VideoDecodeFormatType.fromString(i).description,
|
||||
),
|
||||
subtitle: Text(
|
||||
i,
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/ua_type.dart';
|
||||
import 'package:PiliPlus/http/video.dart';
|
||||
import 'package:PiliPlus/models/common/account_type.dart';
|
||||
import 'package:PiliPlus/models/common/audio_normalization.dart';
|
||||
import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart';
|
||||
import 'package:PiliPlus/models/common/super_resolution_type.dart';
|
||||
import 'package:PiliPlus/models/common/video/video_type.dart';
|
||||
@@ -43,7 +44,6 @@ import 'package:flutter/foundation.dart' show kDebugMode;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
@@ -52,6 +52,7 @@ import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
import 'package:volume_controller/volume_controller.dart';
|
||||
|
||||
class PlPlayerController {
|
||||
Player? _videoPlayerController;
|
||||
@@ -717,7 +718,16 @@ class PlPlayerController {
|
||||
if (isAnim) {
|
||||
setShader(superResolutionType.value, pp);
|
||||
}
|
||||
await pp.setProperty("af", "scaletempo2=max-speed=8");
|
||||
String audioNormalization = Pref.audioNormalization;
|
||||
audioNormalization = switch (audioNormalization) {
|
||||
'0' => '',
|
||||
'1' => ',${AudioNormalization.dynaudnorm.param}',
|
||||
_ => ',$audioNormalization',
|
||||
};
|
||||
await pp.setProperty(
|
||||
"af",
|
||||
"scaletempo2=max-speed=8$audioNormalization",
|
||||
);
|
||||
if (Platform.isAndroid) {
|
||||
await pp.setProperty("volume-max", "100");
|
||||
String ao = Pref.useOpenSLES
|
||||
@@ -943,7 +953,12 @@ class PlPlayerController {
|
||||
isLive,
|
||||
);
|
||||
}),
|
||||
if (kDebugMode)
|
||||
videoPlayerController!.stream.log.listen(((PlayerLog log) {
|
||||
debugPrint(log.toString());
|
||||
})),
|
||||
videoPlayerController!.stream.error.listen((String event) {
|
||||
debugPrint('MPV Exception: $event');
|
||||
if (isLive) {
|
||||
if (event.startsWith('tcp: ffurl_read returned ') ||
|
||||
event.startsWith("Failed to open https://") ||
|
||||
@@ -982,16 +997,13 @@ class PlPlayerController {
|
||||
);
|
||||
} else if (event.startsWith('Could not open codec')) {
|
||||
SmartDialog.showToast('无法加载解码器, $event,可能会切换至软解');
|
||||
} else {
|
||||
if (!onlyPlayAudio.value) {
|
||||
if (event.startsWith("Failed to open .") ||
|
||||
event.startsWith("Cannot open") ||
|
||||
event.startsWith("Can not open")) {
|
||||
return;
|
||||
}
|
||||
SmartDialog.showToast('视频加载错误, $event');
|
||||
if (kDebugMode) debugPrint('视频加载错误, $event');
|
||||
} else if (!onlyPlayAudio.value) {
|
||||
if (event.startsWith("Failed to open .") ||
|
||||
event.startsWith("Cannot open") ||
|
||||
event.startsWith("Can not open")) {
|
||||
return;
|
||||
}
|
||||
SmartDialog.showToast('视频加载错误, $event');
|
||||
}
|
||||
}),
|
||||
// videoPlayerController!.stream.volume.listen((event) {
|
||||
@@ -1200,7 +1212,7 @@ class PlPlayerController {
|
||||
Future<void> getCurrentVolume() async {
|
||||
// mac try...catch
|
||||
try {
|
||||
_currentVolume.value = (await FlutterVolumeController.getVolume())!;
|
||||
_currentVolume.value = await VolumeController.instance.getVolume();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@@ -1219,8 +1231,9 @@ class PlPlayerController {
|
||||
volume.value = volumeNew;
|
||||
|
||||
try {
|
||||
FlutterVolumeController.updateShowSystemUI(false);
|
||||
await FlutterVolumeController.setVolume(volumeNew);
|
||||
await (VolumeController.instance..showSystemUI = false).setVolume(
|
||||
volumeNew,
|
||||
);
|
||||
} catch (err) {
|
||||
if (kDebugMode) debugPrint(err.toString());
|
||||
}
|
||||
|
||||
@@ -4,10 +4,15 @@ import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget.dart';
|
||||
import 'package:PiliPlus/common/widgets/pair.dart';
|
||||
import 'package:PiliPlus/common/widgets/progress_bar/audio_video_progress_bar.dart';
|
||||
import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart';
|
||||
import 'package:PiliPlus/common/widgets/view_safe_area.dart';
|
||||
import 'package:PiliPlus/http/init.dart';
|
||||
import 'package:PiliPlus/models/common/sponsor_block/action_type.dart';
|
||||
import 'package:PiliPlus/models/common/sponsor_block/post_segment_model.dart';
|
||||
import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart';
|
||||
import 'package:PiliPlus/models/common/super_resolution_type.dart';
|
||||
import 'package:PiliPlus/models/common/video/video_quality.dart';
|
||||
import 'package:PiliPlus/models/video/play/url.dart';
|
||||
@@ -18,6 +23,7 @@ import 'package:PiliPlus/models_new/video/video_shot/data.dart';
|
||||
import 'package:PiliPlus/pages/common/common_intro_controller.dart';
|
||||
import 'package:PiliPlus/pages/video/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/introduction/pgc/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/post_panel/view.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/controller.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/models/bottom_control_type.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/models/bottom_progress_behavior.dart';
|
||||
@@ -31,11 +37,14 @@ import 'package:PiliPlus/plugin/pl_player/widgets/backward_seek.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/widgets/bottom_control.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/widgets/common_btn.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/widgets/forward_seek.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/widgets/mpv_convert_webp.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/widgets/play_pause_btn.dart';
|
||||
import 'package:PiliPlus/utils/duration_util.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/storage_key.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
@@ -44,7 +53,6 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart' hide ContextExtensionss;
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
@@ -52,6 +60,7 @@ import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
import 'package:saver_gallery/saver_gallery.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
import 'package:volume_controller/volume_controller.dart';
|
||||
|
||||
class PLVideoPlayer extends StatefulWidget {
|
||||
const PLVideoPlayer({
|
||||
@@ -203,12 +212,12 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
videoController = plPlayerController.videoController!;
|
||||
Future.microtask(() async {
|
||||
try {
|
||||
FlutterVolumeController.updateShowSystemUI(true);
|
||||
_volumeValue.value = (await FlutterVolumeController.getVolume())!;
|
||||
FlutterVolumeController.addListener((double value) {
|
||||
final volumeCtr = VolumeController.instance..showSystemUI = true;
|
||||
_volumeValue.value = await volumeCtr.getVolume();
|
||||
volumeCtr.addListener((double value) {
|
||||
if (mounted && !_volumeInterceptEventStream.value) {
|
||||
_volumeValue.value = value;
|
||||
if (Platform.isIOS && !FlutterVolumeController.showSystemUI) {
|
||||
if (Platform.isIOS && !volumeCtr.showSystemUI) {
|
||||
_volumeIndicator.value = true;
|
||||
_volumeTimer?.cancel();
|
||||
_volumeTimer = Timer(const Duration(milliseconds: 800), () {
|
||||
@@ -239,9 +248,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
|
||||
Future<void> setVolume(double value) async {
|
||||
try {
|
||||
FlutterVolumeController.updateShowSystemUI(false);
|
||||
await FlutterVolumeController.setVolume(value);
|
||||
await (VolumeController.instance..showSystemUI = false).setVolume(value);
|
||||
} catch (_) {}
|
||||
|
||||
_volumeValue.value = value;
|
||||
_volumeIndicator.value = true;
|
||||
_volumeInterceptEventStream.value = true;
|
||||
@@ -273,7 +282,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
_listener?.cancel();
|
||||
_controlsListener?.cancel();
|
||||
animationController.dispose();
|
||||
FlutterVolumeController.removeListener();
|
||||
VolumeController.instance.removeListener();
|
||||
transformationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -403,6 +412,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
/// 超分辨率
|
||||
BottomControlType.superResolution => Obx(
|
||||
() => PopupMenuButton<SuperResolutionType>(
|
||||
tooltip: '超分辨率',
|
||||
requestFocus: false,
|
||||
initialValue: plPlayerController.superResolutionType.value,
|
||||
color: Colors.black.withValues(alpha: 0.8),
|
||||
@@ -511,6 +521,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
/// 画面比例
|
||||
BottomControlType.fit => Obx(
|
||||
() => PopupMenuButton<VideoFitType>(
|
||||
tooltip: '画面比例',
|
||||
requestFocus: false,
|
||||
initialValue: plPlayerController.videoFit.value,
|
||||
color: Colors.black.withValues(alpha: 0.8),
|
||||
@@ -545,6 +556,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
() => widget.videoDetailController?.subtitles.isEmpty == true
|
||||
? const SizedBox.shrink()
|
||||
: PopupMenuButton<int>(
|
||||
tooltip: '选择字幕',
|
||||
requestFocus: false,
|
||||
initialValue: widget
|
||||
.videoDetailController!
|
||||
@@ -597,6 +609,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
/// 播放速度
|
||||
BottomControlType.speed => Obx(
|
||||
() => PopupMenuButton<double>(
|
||||
tooltip: '倍速',
|
||||
requestFocus: false,
|
||||
initialValue: plPlayerController.playbackSpeed,
|
||||
color: Colors.black.withValues(alpha: 0.8),
|
||||
@@ -650,6 +663,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
}
|
||||
}
|
||||
return PopupMenuButton<int>(
|
||||
tooltip: '画质',
|
||||
requestFocus: false,
|
||||
initialValue: currentVideoQa.code,
|
||||
color: Colors.black.withValues(alpha: 0.8),
|
||||
@@ -1655,6 +1669,10 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
size: 20,
|
||||
color: Colors.white,
|
||||
),
|
||||
onLongPress:
|
||||
(Platform.isAndroid || kDebugMode) && !isLive
|
||||
? screenshotWebp
|
||||
: null,
|
||||
onTap: () {
|
||||
SmartDialog.showToast('截图中');
|
||||
plPlayerController.videoPlayerController
|
||||
@@ -1857,6 +1875,155 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> screenshotWebp() async {
|
||||
final videoCtr = widget.videoDetailController!;
|
||||
final videoInfo = widget.videoDetailController!.data;
|
||||
final ids = videoInfo.dash!.video!.map((i) => i.id!).toSet();
|
||||
final video = videoCtr.findVideoByQa(ids.reduce((p, n) => p < n ? p : n));
|
||||
|
||||
VideoQuality qa = video.quality;
|
||||
String? url = video.baseUrl;
|
||||
if (url == null) return;
|
||||
|
||||
final ctr = plPlayerController;
|
||||
final theme = Theme.of(context);
|
||||
final currentPos = ctr.position.value.inMilliseconds / 1000.0;
|
||||
final duration = ctr.durationSeconds.value.inMilliseconds / 1000.0;
|
||||
final segment = Pair(first: currentPos, second: currentPos + 10.0);
|
||||
final model = PostSegmentModel(
|
||||
segment: segment,
|
||||
category: SegmentType.sponsor,
|
||||
actionType: ActionType.skip,
|
||||
);
|
||||
final isPlay = ctr.playerStatus.playing;
|
||||
if (isPlay) ctr.pause();
|
||||
|
||||
WebpPreset preset = WebpPreset.def;
|
||||
|
||||
final success =
|
||||
await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: const Text('动态截图'),
|
||||
content: Column(
|
||||
spacing: 12,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PostPanel.segmentWidget(
|
||||
theme,
|
||||
item: model,
|
||||
currentPos: currentPos,
|
||||
videoDuration: duration,
|
||||
),
|
||||
Builder(
|
||||
builder: (context) => PopupMenuButton(
|
||||
initialValue: qa.code,
|
||||
onSelected: (value) {
|
||||
if (value == qa.code) return;
|
||||
final video = videoCtr.findVideoByQa(value);
|
||||
url = video.baseUrl;
|
||||
qa = video.quality;
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
itemBuilder: (_) => videoInfo.supportFormats!
|
||||
.map(
|
||||
(i) => PopupMenuItem<int>(
|
||||
enabled: ids.contains(i.quality),
|
||||
value: i.quality,
|
||||
child: Text(i.newDesc ?? ''),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: Text('转码画质:${qa.shortDesc}'),
|
||||
),
|
||||
),
|
||||
Builder(
|
||||
builder: (context) => PopupMenuButton(
|
||||
initialValue: preset,
|
||||
onSelected: (value) {
|
||||
if (preset == value) return;
|
||||
preset = value;
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
itemBuilder: (_) => WebpPreset.values
|
||||
.map(
|
||||
(i) => PopupMenuItem(value: i, child: Text(i.name)),
|
||||
)
|
||||
.toList(),
|
||||
child: Text('webp预设:${preset.name}(${preset.desc})'),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'*转码使用软解,速度可能慢于播放,请不要选择过长的时间段或过高画质',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (segment.first < segment.second) {
|
||||
Get.back(result: true);
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
if (!success) return;
|
||||
|
||||
final progress = 0.0.obs;
|
||||
final name =
|
||||
'${ctr.cid}-${segment.first.toStringAsFixed(3)}_${segment.second.toStringAsFixed(3)}.webp';
|
||||
final file = '${await Utils.temporaryDirectory}/$name';
|
||||
|
||||
final mpv = MpvConvertWebp(
|
||||
url!,
|
||||
file,
|
||||
segment.first,
|
||||
segment.second,
|
||||
progress: progress,
|
||||
preset: preset,
|
||||
);
|
||||
final future = mpv.convert().whenComplete(
|
||||
() => SmartDialog.dismiss(status: SmartStatus.loading),
|
||||
);
|
||||
|
||||
SmartDialog.showLoading(
|
||||
backType: SmartBackType.normal,
|
||||
builder: (_) => LoadingWidget(progress: progress, msg: '正在保存,可能需要较长时间'),
|
||||
onDismiss: () async {
|
||||
if (progress.value < 1.0) {
|
||||
mpv.dispose();
|
||||
}
|
||||
if (await future) {
|
||||
await SaverGallery.saveFile(
|
||||
filePath: file,
|
||||
fileName: name,
|
||||
androidRelativePath: 'Pictures/Screenshots',
|
||||
skipIfExists: false,
|
||||
);
|
||||
SmartDialog.showToast('$name已保存到相册/截图');
|
||||
} else {
|
||||
SmartDialog.showToast('转码出现错误或已取消');
|
||||
}
|
||||
progress.close();
|
||||
File(file).delSync();
|
||||
if (isPlay) ctr.play();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildDmChart(
|
||||
|
||||
186
lib/plugin/pl_player/widgets/mpv_convert_webp.dart
Normal file
186
lib/plugin/pl_player/widgets/mpv_convert_webp.dart
Normal file
@@ -0,0 +1,186 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:PiliPlus/http/constants.dart';
|
||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get_rx/get_rx.dart';
|
||||
import 'package:media_kit/ffi/src/allocation.dart';
|
||||
import 'package:media_kit/ffi/src/utf8.dart';
|
||||
import 'package:media_kit/generated/libmpv/bindings.dart' as generated;
|
||||
import 'package:media_kit/src/player/native/core/initializer.dart';
|
||||
import 'package:media_kit/src/player/native/core/native_library.dart';
|
||||
|
||||
class MpvConvertWebp {
|
||||
final _mpv = generated.MPV(DynamicLibrary.open(NativeLibrary.path));
|
||||
late final Pointer<generated.mpv_handle> _ctx;
|
||||
final _completer = Completer<bool>();
|
||||
|
||||
bool _success = false;
|
||||
|
||||
final String url;
|
||||
final String outFile;
|
||||
final double start;
|
||||
final double duration;
|
||||
final RxDouble? progress;
|
||||
final WebpPreset preset;
|
||||
|
||||
MpvConvertWebp(
|
||||
this.url,
|
||||
this.outFile,
|
||||
this.start,
|
||||
double end, {
|
||||
this.progress,
|
||||
this.preset = WebpPreset.def,
|
||||
}) : duration = end - start;
|
||||
|
||||
Future<void> _init() async {
|
||||
_ctx = await Initializer.create(
|
||||
NativeLibrary.path,
|
||||
_onEvent,
|
||||
options: {
|
||||
'o': outFile,
|
||||
'start': start.toStringAsFixed(3),
|
||||
'end': (start + duration).toStringAsFixed(3),
|
||||
'of': 'webp',
|
||||
'ovc': 'libwebp_anim',
|
||||
'ofopts': 'loop=0',
|
||||
'ovcopts': 'preset=${preset.flag}',
|
||||
},
|
||||
);
|
||||
_setHeader();
|
||||
if (progress != null) {
|
||||
_observeProperty('time-pos');
|
||||
}
|
||||
final level = (kDebugMode ? 'info' : 'error').toNativeUtf8();
|
||||
_mpv.mpv_request_log_messages(_ctx, level.cast());
|
||||
calloc.free(level);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
Initializer.dispose(_ctx);
|
||||
_mpv.mpv_terminate_destroy(_ctx);
|
||||
if (!_completer.isCompleted) _completer.complete(false);
|
||||
}
|
||||
|
||||
Future<bool> convert() async {
|
||||
await _init();
|
||||
_command(['loadfile', url]);
|
||||
return _completer.future;
|
||||
}
|
||||
|
||||
Future<void> _onEvent(Pointer<generated.mpv_event> event) async {
|
||||
switch (event.ref.event_id) {
|
||||
case generated.mpv_event_id.MPV_EVENT_PROPERTY_CHANGE:
|
||||
final prop = event.ref.data.cast<generated.mpv_event_property>().ref;
|
||||
if (prop.name.cast<Utf8>().toDartString() == 'time-pos' &&
|
||||
prop.format == generated.mpv_format.MPV_FORMAT_DOUBLE) {
|
||||
progress!.value = (prop.data.cast<Double>().value - start) / duration;
|
||||
}
|
||||
break;
|
||||
case generated.mpv_event_id.MPV_EVENT_FILE_LOADED:
|
||||
_success = true;
|
||||
break;
|
||||
case generated.mpv_event_id.MPV_EVENT_LOG_MESSAGE:
|
||||
final log = event.ref.data.cast<generated.mpv_event_log_message>().ref;
|
||||
final prefix = log.prefix.cast<Utf8>().toDartString().trim();
|
||||
final level = log.level.cast<Utf8>().toDartString().trim();
|
||||
final text = log.text.cast<Utf8>().toDartString().trim();
|
||||
debugPrint('WebpConvert: $level $prefix : $text');
|
||||
if (kDebugMode) {
|
||||
_success = level != 'error' && level != 'fatal';
|
||||
} else {
|
||||
_success = false;
|
||||
}
|
||||
break;
|
||||
case generated.mpv_event_id.MPV_EVENT_END_FILE ||
|
||||
generated.mpv_event_id.MPV_EVENT_SHUTDOWN:
|
||||
progress?.value = 1;
|
||||
_completer.complete(_success);
|
||||
dispose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _command(List<String> args) {
|
||||
final pointers = args.map((e) => e.toNativeUtf8()).toList();
|
||||
final arr = calloc<Pointer<Utf8>>(128);
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
arr[i] = pointers[i];
|
||||
}
|
||||
|
||||
_mpv.mpv_command(_ctx, arr.cast());
|
||||
|
||||
calloc.free(arr);
|
||||
pointers.forEach(calloc.free);
|
||||
}
|
||||
|
||||
void _observeProperty(String property) {
|
||||
final name = property.toNativeUtf8();
|
||||
_mpv.mpv_observe_property(
|
||||
_ctx,
|
||||
property.hashCode,
|
||||
name.cast(),
|
||||
generated.mpv_format.MPV_FORMAT_DOUBLE,
|
||||
);
|
||||
|
||||
calloc.free(name);
|
||||
}
|
||||
|
||||
void _setHeader() {
|
||||
final property = 'http-header-fields'.toNativeUtf8();
|
||||
// Allocate & fill the [mpv_node] with the headers.
|
||||
final value = calloc<generated.mpv_node>();
|
||||
final valRef = value.ref
|
||||
..format = generated.mpv_format.MPV_FORMAT_NODE_ARRAY;
|
||||
valRef.u.list = calloc<generated.mpv_node_list>();
|
||||
final valList = valRef.u.list.ref
|
||||
..num = 2
|
||||
..values = calloc<generated.mpv_node>(2);
|
||||
|
||||
const entries = [
|
||||
(
|
||||
'user-agent',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
|
||||
),
|
||||
('referer', HttpString.baseUrl),
|
||||
];
|
||||
for (int i = 0; i < 2; i++) {
|
||||
final (k, v) = entries[i];
|
||||
valList.values[i]
|
||||
..format = generated.mpv_format.MPV_FORMAT_STRING
|
||||
..u.string = '$k: $v'.toNativeUtf8().cast();
|
||||
}
|
||||
_mpv.mpv_set_property(
|
||||
_ctx,
|
||||
property.cast(),
|
||||
generated.mpv_format.MPV_FORMAT_NODE,
|
||||
value.cast(),
|
||||
);
|
||||
// Free the allocated memory.
|
||||
calloc.free(property);
|
||||
for (int i = 0; i < valList.num; i++) {
|
||||
calloc.free(valList.values[i].u.string);
|
||||
}
|
||||
calloc
|
||||
..free(valList.values)
|
||||
..free(valRef.u.list)
|
||||
..free(value);
|
||||
}
|
||||
}
|
||||
|
||||
enum WebpPreset {
|
||||
none('none', '无', '不使用预设'),
|
||||
def('default', '默认', '默认预设'),
|
||||
picture('picture', '图片', '数码照片,如人像、室内拍摄'),
|
||||
photo('photo', '照片', '户外摄影,自然光环境'),
|
||||
drawing('drawing', '绘图', '手绘或线稿,高对比度细节'),
|
||||
icon('icon', '图标', '小型彩色图像'),
|
||||
text('text', '文本', '文字类');
|
||||
|
||||
final String flag;
|
||||
final String name;
|
||||
final String desc;
|
||||
|
||||
const WebpPreset(this.flag, this.name, this.desc);
|
||||
}
|
||||
@@ -91,6 +91,7 @@ class SettingBoxKey {
|
||||
refreshDragPercentage = 'refreshDragPercentage',
|
||||
refreshDisplacement = 'refreshDisplacement',
|
||||
showHotRcmd = 'showHotRcmd',
|
||||
audioNormalization = 'audioNormalization',
|
||||
superResolutionType = 'superResolutionType',
|
||||
preInitPlayer = 'preInitPlayer',
|
||||
mainTabBarView = 'mainTabBarView',
|
||||
|
||||
@@ -405,6 +405,9 @@ class Pref {
|
||||
static bool get showHotRcmd =>
|
||||
_setting.get(SettingBoxKey.showHotRcmd, defaultValue: false);
|
||||
|
||||
static String get audioNormalization =>
|
||||
_setting.get(SettingBoxKey.audioNormalization, defaultValue: '0');
|
||||
|
||||
static SuperResolutionType get superResolutionType {
|
||||
SuperResolutionType? superResolutionType;
|
||||
final index = _setting.get(SettingBoxKey.superResolutionType);
|
||||
|
||||
39
pubspec.lock
39
pubspec.lock
@@ -517,14 +517,6 @@ packages:
|
||||
url: "https://github.com/bggRGjQaUbCoE/extended_nested_scroll_view.git"
|
||||
source: git
|
||||
version: "6.2.1"
|
||||
fading_edge_scrollview:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: fading_edge_scrollview
|
||||
sha256: "1f84fe3ea8e251d00d5735e27502a6a250e4aa3d3b330d3fdcb475af741464ef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -782,14 +774,6 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_volume_controller:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_volume_controller
|
||||
sha256: "15f2c25bc4632ac5e8d42a208fe07c3224a4ee66b155d1ac86945b3db2bb58d9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -1157,12 +1141,13 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.11"
|
||||
media_kit_libs_android_video:
|
||||
dependency: transitive
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: media_kit_libs_android_video
|
||||
sha256: adff9b571b8ead0867f9f91070f8df39562078c0eb3371d88b9029a2d547d7b7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "libs/android/media_kit_libs_android_video"
|
||||
ref: "version_1.2.5"
|
||||
resolved-ref: f89452bc27af26324a83961c8286d4f41432a5f9
|
||||
url: "https://github.com/My-Responsitories/media-kit.git"
|
||||
source: git
|
||||
version: "1.3.7"
|
||||
media_kit_libs_ios_video:
|
||||
dependency: transitive
|
||||
@@ -1205,7 +1190,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.0.11"
|
||||
media_kit_native_event_loop:
|
||||
dependency: transitive
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: media_kit_native_event_loop
|
||||
sha256: "7d82e3b3e9ded5c35c3146c5ba1da3118d1dd8ac3435bac7f29f458181471b40"
|
||||
@@ -1217,8 +1202,8 @@ packages:
|
||||
description:
|
||||
path: media_kit_video
|
||||
ref: "version_1.2.5"
|
||||
resolved-ref: "4785f3223ff04d86b1644ab114e3960723ddf7cd"
|
||||
url: "https://github.com/bggRGjQaUbCoE/media-kit.git"
|
||||
resolved-ref: f89452bc27af26324a83961c8286d4f41432a5f9
|
||||
url: "https://github.com/My-Responsitories/media-kit.git"
|
||||
source: git
|
||||
version: "1.2.5"
|
||||
meta:
|
||||
@@ -1971,13 +1956,13 @@ packages:
|
||||
source: hosted
|
||||
version: "15.0.2"
|
||||
volume_controller:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: volume_controller
|
||||
sha256: c71d4c62631305df63b72da79089e078af2659649301807fa746088f365cb48e
|
||||
sha256: d75039e69c0d90e7810bfd47e3eedf29ff8543ea7a10392792e81f9bded7edf5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.8"
|
||||
version: "3.4.0"
|
||||
wakelock_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
16
pubspec.yaml
16
pubspec.yaml
@@ -104,7 +104,7 @@ dependencies:
|
||||
# media_kit_video: ^1.2.5 # For video rendering.
|
||||
media_kit_video:
|
||||
git:
|
||||
url: https://github.com/bggRGjQaUbCoE/media-kit.git
|
||||
url: https://github.com/My-Responsitories/media-kit.git
|
||||
path: media_kit_video
|
||||
ref: version_1.2.5
|
||||
media_kit_libs_video: 1.0.5
|
||||
@@ -114,7 +114,7 @@ dependencies:
|
||||
audio_session: ^0.2.2
|
||||
|
||||
# 音量、亮度、屏幕控制
|
||||
flutter_volume_controller: ^1.3.3
|
||||
volume_controller: ^2.0.7
|
||||
wakelock_plus: ^1.2.8
|
||||
# universal_platform: ^1.1.0
|
||||
auto_orientation:
|
||||
@@ -214,17 +214,23 @@ dependencies:
|
||||
collection: any
|
||||
material_color_utilities: any
|
||||
flutter_cache_manager: any
|
||||
|
||||
http2: any
|
||||
|
||||
dependency_overrides:
|
||||
screen_brightness: ^2.0.1
|
||||
screen_brightness: ^2.1.7
|
||||
path: ^1.9.1
|
||||
mime: ^2.0.0
|
||||
fading_edge_scrollview: ^4.1.1
|
||||
rxdart: ^0.28.0
|
||||
media_kit: 1.1.11
|
||||
media_kit_libs_video: 1.0.5
|
||||
media_kit_native_event_loop: ^1.0.9
|
||||
font_awesome_flutter: 10.9.0
|
||||
media_kit_libs_android_video:
|
||||
git:
|
||||
url: https://github.com/My-Responsitories/media-kit.git
|
||||
path: libs/android/media_kit_libs_android_video
|
||||
ref: version_1.2.5
|
||||
volume_controller: ^3.4.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user