refa player (#1848)

* tweak

* Reapply "opt: audio uri" (#1833)

This reverts commit 8e726f49b2.

* opt: player

* opt: player

* refa: create player

* refa: player

* opt: UaType

* fix: sb seek preview

* opt: setSub

* fix: screenshot

* opt: unnecessary final player state

* opt: player track

* opt FileSource constructor [skip ci]

* fixes

* fix: dispose player

* fix: quote

* update

* fix: multi ua & remove unused loop

* remove unneeded check [skip ci]

---------

Co-authored-by: dom <githubaccount56556@proton.me>
This commit is contained in:
My-Responsitories
2026-02-27 15:51:55 +08:00
committed by GitHub
parent 6782bee11a
commit 7276cde48a
39 changed files with 1063 additions and 1112 deletions

View File

@@ -1,13 +1,14 @@
import 'dart:async' show StreamSubscription, Timer;
import 'dart:convert' show ascii;
import 'dart:io' show Platform, File, Directory;
import 'dart:io' show Platform;
import 'dart:math' show max, min;
import 'dart:ui' as ui;
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/http/browser_ua.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/init.dart';
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';
@@ -31,6 +32,7 @@ import 'package:PiliPlus/plugin/pl_player/models/video_fit_type.dart';
import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart';
import 'package:PiliPlus/services/service_locator.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/asset_utils.dart';
import 'package:PiliPlus/utils/extension/box_ext.dart';
import 'package:PiliPlus/utils/extension/num_ext.dart';
import 'package:PiliPlus/utils/extension/string_ext.dart';
@@ -50,8 +52,7 @@ import 'package:easy_debounce/easy_throttle.dart';
import 'package:floating/floating.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'
show rootBundle, HapticFeedback, Uint8List;
import 'package:flutter/services.dart' show HapticFeedback;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
import 'package:get/get.dart';
@@ -156,8 +157,6 @@ class PlPlayerController with BlockConfigMixin {
///
final RxBool isSliderMoving = false.obs;
/// 是否循环
PlaylistMode _looping = PlaylistMode.none;
bool _autoPlay = false;
// 记录历史记录
@@ -177,7 +176,7 @@ class PlPlayerController with BlockConfigMixin {
late DataSource dataSource;
Timer? _timer;
Timer? _timerForSeek;
StreamSubscription<Duration>? _subForSeek;
Box setting = GStorage.setting;
@@ -255,10 +254,16 @@ class PlPlayerController with BlockConfigMixin {
windowManager.setTitleBarStyle(TitleBarStyle.hidden);
}
late final Size size;
final state = videoController!.player.state;
final width = state.width ?? this.width ?? 16;
final height = state.height ?? this.height ?? 9;
final Size size;
final state = videoPlayerController!.state;
int width = state.width;
int height = state.height;
if (width == 0) {
width = this.width ?? 16;
}
if (height == 0) {
height = this.height ?? 9;
}
if (height > width) {
size = Size(280.0, 280.0 * height / width);
} else {
@@ -299,13 +304,13 @@ class PlPlayerController with BlockConfigMixin {
}
void enterPip({bool isAuto = false}) {
if (videoController != null) {
if (videoPlayerController != null) {
controls = false;
final state = videoController!.player.state;
final state = videoPlayerController!.state;
PageUtils.enterPip(
isAuto: isAuto,
width: state.width ?? width,
height: state.height ?? height,
width: state.width == 0 ? width : state.width,
height: state.height == 0 ? height : state.height,
);
}
}
@@ -387,9 +392,7 @@ class PlPlayerController with BlockConfigMixin {
late int? cacheVideoQa = PlatformUtils.isMobile ? null : Pref.defaultVideoQa;
late int cacheAudioQa = Pref.defaultAudioQa;
bool enableHeart = true;
late final bool enableHA = Pref.enableHA;
late final String hwdec = Pref.hardwareDecoding;
late final String? hwdec = Pref.enableHA ? Pref.hardwareDecoding : null;
late final progressType = Pref.btmProgressBehavior;
late final enableQuickDouble = Pref.enableQuickDouble;
@@ -517,8 +520,8 @@ class PlPlayerController with BlockConfigMixin {
return _instance?.volume.value;
}
static Future<void> setVolumeIfExists(double volumeNew) async {
await _instance?.setVolume(volumeNew);
static Future<void>? setVolumeIfExists(double volumeNew) {
return _instance?.setVolume(volumeNew);
}
Box video = GStorage.video;
@@ -549,29 +552,22 @@ class PlPlayerController with BlockConfigMixin {
// 获取实例 传参
static PlPlayerController getInstance({bool isLive = false}) {
// 如果实例尚未创建,则创建一个新实例
_instance ??= PlPlayerController._();
_instance!
return (_instance ??= PlPlayerController._())
..isLive = isLive
.._playerCount += 1;
return _instance!;
}
bool _processing = false;
bool get processing => _processing;
// offline
bool isFileSource = false;
String? dirPath;
String? typeTag;
int? mediaType;
bool get isFileSource => dataSource is FileSource;
// 初始化资源
Future<void> setDataSource(
DataSource dataSource, {
bool isLive = false,
bool autoplay = true,
// 默认不循环
PlaylistMode looping = PlaylistMode.none,
// 初始化播放位置
Duration? seekTo,
// 初始化播放速度
@@ -591,15 +587,8 @@ class PlPlayerController with BlockConfigMixin {
VideoType? videoType,
VoidCallback? onInit,
Volume? volume,
String? dirPath,
String? typeTag,
int? mediaType,
}) async {
try {
this.dirPath = dirPath;
this.typeTag = typeTag;
this.mediaType = mediaType;
isFileSource = dataSource.type == DataSourceType.file;
_processing = true;
this.isLive = isLive;
_videoType = videoType ?? VideoType.ugc;
@@ -607,7 +596,6 @@ class PlPlayerController with BlockConfigMixin {
this.height = height;
this.dataSource = dataSource;
_autoPlay = autoplay;
_looping = looping;
// 初始化视频倍速
// _playbackSpeed.value = speed;
// 初始化数据加载状态
@@ -634,12 +622,15 @@ class PlPlayerController with BlockConfigMixin {
return;
}
// 配置Player 音轨、字幕等等
_videoPlayerController = await _createVideoController(
dataSource,
_looping,
seekTo,
volume,
);
await _createVideoController(dataSource, seekTo, volume);
if (_playerCount == 0) {
_videoPlayerController?.dispose();
_videoPlayerController = null;
_videoController = null;
return;
}
// 获取视频时长 00:00
this.duration.value = duration ?? _videoPlayerController!.state.duration;
position = buffered.value = sliderPosition = seekTo ?? Duration.zero;
@@ -670,32 +661,11 @@ class PlPlayerController with BlockConfigMixin {
return shadersDirPath!;
}
final dir = Directory(path.join(appSupportDirPath, 'anime_shaders'));
if (!dir.existsSync()) {
await dir.create(recursive: true);
}
final shaderFilesPath =
(Constants.mpvAnime4KShaders + Constants.mpvAnime4KShadersLite)
.map((e) => 'assets/shaders/$e')
.toList();
for (final filePath in shaderFilesPath) {
final fileName = filePath.split('/').last;
final targetFile = File(path.join(dir.path, fileName));
if (targetFile.existsSync()) {
continue;
}
try {
final data = await rootBundle.load(filePath);
final List<int> bytes = data.buffer.asUint8List();
await targetFile.writeAsBytes(bytes);
} catch (e) {
if (kDebugMode) debugPrint('$e');
}
}
return shadersDirPath = dir.path;
return shadersDirPath = await AssetUtils.getOrCopy(
'assets/shaders',
Constants.mpvAnime4KShaders.followedBy(Constants.mpvAnime4KShadersLite),
path.join(appSupportDirPath, 'anime_shaders'),
);
}
late final isAnim = _pgcType == 1 || _pgcType == 4;
@@ -710,12 +680,10 @@ class PlPlayerController with BlockConfigMixin {
setting.put(SettingBoxKey.superResolutionType, type.index);
}
}
pp ??= _videoPlayerController!.platform!;
await pp.waitForPlayerInitialization;
await pp.waitForVideoControllerInitializationIfAttached;
pp ??= _videoPlayerController!;
switch (type) {
case SuperResolutionType.disable:
return pp.command(['change-list', 'glsl-shaders', 'clr', '']);
return pp.command(const ['change-list', 'glsl-shaders', 'clr', '']);
case SuperResolutionType.efficiency:
return pp.command([
'change-list',
@@ -741,10 +709,54 @@ class PlPlayerController with BlockConfigMixin {
static final loudnormRegExp = RegExp('loudnorm=([^,]+)');
Future<Player> _initPlayer() async {
assert(_videoPlayerController == null);
final opt = {
'video-sync': Pref.videoSync,
};
if (Platform.isAndroid) {
opt['volume-max'] = '100';
opt['ao'] = Pref.audioOutput;
} else if (PlatformUtils.isDesktop) {
opt['volume'] = (volume.value * 100).toString();
}
final autosync = Pref.autosync;
if (autosync != '0') {
opt['autosync'] = autosync;
}
final player = await Player.create(
configuration: PlayerConfiguration(
bufferSize: Pref.expandBuffer
? (isLive ? 64 * 1024 * 1024 : 32 * 1024 * 1024)
: (isLive ? 16 * 1024 * 1024 : 4 * 1024 * 1024),
logLevel: kDebugMode ? .warn : .error,
options: opt,
),
);
assert(_videoController == null);
_videoController = await VideoController.create(
player,
configuration: VideoControllerConfiguration(
enableHardwareAcceleration: hwdec != null,
androidAttachSurfaceAfterVideoParameters: false,
hwdec: hwdec,
),
);
player.setMediaHeader(
userAgent: BrowserUa.pc,
referer: HttpString.baseUrl,
);
// await player.setAudioTrack(.auto());
return player;
}
// 配置播放器
Future<Player> _createVideoController(
Future<void> _createVideoController(
DataSource dataSource,
PlaylistMode looping,
Duration? seekTo,
Volume? volume,
) async {
@@ -757,81 +769,34 @@ class PlPlayerController with BlockConfigMixin {
// 初始化时清空弹幕,防止上次重叠
danmakuController?.clear();
Player player =
_videoPlayerController ??
Player(
configuration: PlayerConfiguration(
// 默认缓冲 4M 大小
bufferSize: Pref.expandBuffer
? (isLive ? 64 * 1024 * 1024 : 32 * 1024 * 1024)
: (isLive ? 16 * 1024 * 1024 : 4 * 1024 * 1024),
logLevel: kDebugMode ? MPVLogLevel.warn : MPVLogLevel.error,
),
);
final pp = player.platform!;
if (_videoPlayerController == null) {
if (PlatformUtils.isDesktop) {
pp.setVolume(this.volume.value * 100);
var player = _videoPlayerController;
if (player == null) {
player = await _initPlayer();
if (_playerCount == 0) {
player.dispose();
player = null;
_videoController = null;
return;
}
if (isAnim) {
setShader(superResolutionType.value, pp);
}
// await pp.setProperty('audio-pitch-correction', 'yes'); // default yes
if (Platform.isAndroid) {
await pp.setProperty("volume-max", "100");
await pp.setProperty("ao", Pref.audioOutput);
}
// video-sync=display-resample
await pp.setProperty("video-sync", Pref.videoSync);
final autosync = Pref.autosync;
if (autosync != '0') {
await pp.setProperty("autosync", autosync);
}
// vo=gpu-next & gpu-context=android & gpu-api=opengl
// await pp.setProperty("vo", "gpu-next");
// await pp.setProperty("gpu-context", "android");
// await pp.setProperty("gpu-api", "opengl");
await player.setAudioTrack(AudioTrack.auto());
if (Pref.enableSystemProxy) {
final systemProxyHost = Pref.systemProxyHost;
final systemProxyPort = int.tryParse(Pref.systemProxyPort);
if (systemProxyPort != null && systemProxyHost.isNotEmpty) {
await pp.setProperty(
"http-proxy",
'http://$systemProxyHost:$systemProxyPort',
);
}
_videoPlayerController = player;
}
if (isAnim) await setShader();
final Map<String, String> extras = {};
String video = dataSource.videoSource;
if (dataSource.audioSource case final audio? when (audio.isNotEmpty)) {
if (onlyPlayAudio.value) {
video = audio;
} else {
extras['audio-files'] =
'"${Platform.isWindows ? audio.replaceAll(';', r'\;') : audio.replaceAll(':', r'\:')}"';
}
}
// 音轨
final String audioUri;
if (isFileSource) {
audioUri = onlyPlayAudio.value || mediaType == 1
? ''
: path.join(dirPath!, typeTag!, PathUtils.audioNameType2);
} else if (dataSource.audioSource?.isNotEmpty == true) {
audioUri = Platform.isWindows
? dataSource.audioSource!.replaceAll(';', r'\;')
: dataSource.audioSource!.replaceAll(':', r'\:');
} else {
audioUri = '';
}
await pp.setProperty('audio-files', audioUri);
_videoController ??= VideoController(
player,
configuration: VideoControllerConfiguration(
enableHardwareAcceleration: enableHA,
androidAttachSurfaceAfterVideoParameters: false,
hwdec: enableHA ? hwdec : null,
),
);
player.setPlaylistMode(looping);
final Map<String, String>? filters;
if (Platform.isAndroid) {
if (kDebugMode || Platform.isAndroid) {
String audioNormalization = AudioNormalization.getParamFromConfig(
Pref.audioNormalization,
);
@@ -854,44 +819,23 @@ class PlPlayerController with BlockConfigMixin {
AudioNormalization.getParamFromConfig(Pref.fallbackNormalization),
);
}
filters = audioNormalization.isEmpty
? null
: {'lavfi-complex': '"[aid1] $audioNormalization [ao]"'};
} else {
filters = null;
if (audioNormalization.isNotEmpty) {
extras['lavfi-complex'] = '"[aid1] $audioNormalization [ao]"';
}
}
// if (kDebugMode) debugPrint(filters.toString());
late final String videoUri;
if (isFileSource) {
videoUri = path.join(
dirPath!,
typeTag!,
mediaType == 1
? PathUtils.videoNameType1
: onlyPlayAudio.value
? PathUtils.audioNameType2
: PathUtils.videoNameType2,
);
} else {
videoUri = dataSource.videoSource!;
}
await player.open(
Media(
videoUri,
httpHeaders: dataSource.httpHeaders,
video,
start: seekTo,
extras: filters,
extras: extras.isEmpty ? null : extras,
),
play: false,
);
return player;
}
Future<bool> refreshPlayer() async {
if (isFileSource) {
if (dataSource is FileSource) {
return true;
}
if (_videoPlayerController == null) {
@@ -902,23 +846,21 @@ class PlPlayerController with BlockConfigMixin {
SmartDialog.showToast('视频源为空,请重新进入本页面');
return false;
}
String? audioUri;
if (!isLive) {
if (dataSource.audioSource.isNullOrEmpty) {
SmartDialog.showToast('音频源为空');
} else {
await (_videoPlayerController!.platform!).setProperty(
'audio-files',
Platform.isWindows
? dataSource.audioSource!.replaceAll(';', '\\;')
: dataSource.audioSource!.replaceAll(':', '\\:'),
);
audioUri = Platform.isWindows
? dataSource.audioSource!.replaceAll(';', '\\;')
: dataSource.audioSource!.replaceAll(':', '\\:');
}
}
await _videoPlayerController!.open(
Media(
dataSource.videoSource!,
httpHeaders: dataSource.httpHeaders,
dataSource.videoSource,
start: position,
extras: audioUri == null ? null : {'audio-files': '"$audioUri"'},
),
play: true,
);
@@ -974,14 +916,14 @@ class PlPlayerController with BlockConfigMixin {
return null;
}
Set<StreamSubscription> subscriptions = {};
List<StreamSubscription> subscriptions = [];
final Set<Function(Duration position)> _positionListeners = {};
final Set<Function(PlayerStatus status)> _statusListeners = {};
/// 播放事件监听
void startListeners() {
final controllerStream = videoPlayerController!.stream;
subscriptions = {
subscriptions = [
controllerStream.playing.listen((event) {
WakelockPlus.toggle(enable: event);
if (event) {
@@ -1062,7 +1004,8 @@ class PlPlayerController with BlockConfigMixin {
}
})),
controllerStream.error.listen((String event) {
if (isFileSource && event.startsWith("Failed to open file")) {
if (dataSource is FileSource &&
event.startsWith("Failed to open file")) {
return;
}
if (isLive) {
@@ -1131,7 +1074,7 @@ class PlPlayerController with BlockConfigMixin {
videoPlayerServiceHandler!.onPositionChange(Duration(seconds: event));
}),
],
};
];
}
/// 移除事件监听
@@ -1139,6 +1082,13 @@ class PlPlayerController with BlockConfigMixin {
return Future.wait(subscriptions.map((e) => e.cancel()));
}
void _cancelSubForSeek() {
if (_subForSeek != null) {
_subForSeek!.cancel();
_subForSeek = null;
}
}
/// 跳转至指定位置
Future<void> seekTo(Duration position, {bool isSeek = true}) async {
// if (position >= duration.value) {
@@ -1153,7 +1103,8 @@ class PlPlayerController with BlockConfigMixin {
this.position = position;
updatePositionSecond();
_heartDuration = position.inSeconds;
if (duration.value.inSeconds != 0) {
Future<void> seek() async {
if (isSeek) {
/// 拖动进度条调节时,不等待第一帧,防止抖动
await _videoPlayerController?.stream.buffer.first;
@@ -1164,33 +1115,16 @@ class PlPlayerController with BlockConfigMixin {
} catch (e) {
if (kDebugMode) debugPrint('seek failed: $e');
}
// if (playerStatus.stopped) {
// play();
// }
}
if (duration.value != Duration.zero) {
seek();
} else {
// if (kDebugMode) debugPrint('seek duration else');
_timerForSeek?.cancel();
_timerForSeek = Timer.periodic(const Duration(milliseconds: 200), (
Timer t,
) async {
//_timerForSeek = null;
if (_playerCount == 0) {
_timerForSeek?.cancel();
_timerForSeek = null;
} else if (duration.value != Duration.zero) {
try {
await _videoPlayerController?.stream.buffer.first;
danmakuController?.clear();
await _videoPlayerController?.seek(position);
} catch (e) {
if (kDebugMode) debugPrint('seek failed: $e');
}
// if (playerStatus.isPaused) {
// play();
// }
t.cancel();
_timerForSeek = null;
}
_subForSeek?.cancel();
_subForSeek = duration.listen((_) {
seek();
_cancelSubForSeek();
});
}
}
@@ -1304,7 +1238,7 @@ class PlPlayerController with BlockConfigMixin {
final RxBool volumeIndicator = false.obs;
Timer? volumeTimer;
final RxBool volumeInterceptEventStream = false.obs;
bool volumeInterceptEventStream = false;
static final double maxVolume = PlatformUtils.isDesktop ? 2.0 : 1.0;
Future<void> setVolume(double volume) async {
@@ -1322,11 +1256,11 @@ class PlPlayerController with BlockConfigMixin {
}
}
volumeIndicator.value = true;
volumeInterceptEventStream.value = true;
volumeInterceptEventStream = true;
volumeTimer?.cancel();
volumeTimer = Timer(const Duration(milliseconds: 200), () {
volumeIndicator.value = false;
volumeInterceptEventStream.value = false;
volumeInterceptEventStream = false;
if (PlatformUtils.isDesktop) {
setting.put(SettingBoxKey.desktopVolume, volume.toPrecision(3));
}
@@ -1341,7 +1275,7 @@ class PlPlayerController with BlockConfigMixin {
/// 读取fit
int fitValue = Pref.cacheVideoFit;
Future<void> getVideoFit() async {
void getVideoFit() {
var attr = VideoFitType.values[fitValue];
// 由于none与scaleDown涉及视频原始尺寸需要等待视频加载后再设置否则尺寸会变为0出现错误;
if (attr == VideoFitType.none || attr == VideoFitType.scaleDown) {
@@ -1570,14 +1504,6 @@ class PlPlayerController with BlockConfigMixin {
void removeStatusLister(Function(PlayerStatus status) listener) =>
_statusListeners.remove(listener);
/// 截屏
Future<Uint8List?> screenshot() async {
final Uint8List? screenshot = await _videoPlayerController!.screenshot(
format: 'image/png',
);
return screenshot;
}
// 记录播放记录
Future<void>? makeHeartBeat(
int progress, {
@@ -1657,9 +1583,10 @@ class PlPlayerController with BlockConfigMixin {
}
bool isCloseAll = false;
Future<void> dispose() async {
void dispose() {
// 每次减1最后销毁
cancelLongPressTimer();
_cancelSubForSeek();
if (!isCloseAll && _playerCount > 1) {
_playerCount -= 1;
_heartDuration = 0;
@@ -1681,7 +1608,6 @@ class PlPlayerController with BlockConfigMixin {
}
Utils.channel.setMethodCallHandler(null);
_timer?.cancel();
_timerForSeek?.cancel();
// _position.close();
// _playerEventSubs?.cancel();
// _sliderPosition.close();
@@ -1699,7 +1625,7 @@ class PlPlayerController with BlockConfigMixin {
windowManager.setAlwaysOnTop(false);
}
await removeListeners();
removeListeners();
subscriptions.clear();
_positionListeners.clear();
_statusListeners.clear();
@@ -1781,7 +1707,7 @@ class PlPlayerController with BlockConfigMixin {
},
options: Options(
headers: {
'user-agent': UaType.pc.ua,
'user-agent': BrowserUa.pc,
'referer': 'https://www.bilibili.com/video/$bvid',
},
),
@@ -1802,7 +1728,7 @@ class PlPlayerController with BlockConfigMixin {
void takeScreenshot() {
SmartDialog.showToast('截图中');
videoPlayerController?.screenshot(format: 'image/png').then((value) {
videoPlayerController?.screenshot(format: .png).then((value) {
if (value != null) {
SmartDialog.showToast('点击弹窗保存截图');
showDialog(