import 'dart:async' show StreamSubscription, Timer; import 'dart:convert' show ascii; import 'dart:io' show Platform; import 'dart:math' show max, min; import 'dart:ui' as ui; import 'package:PiliPlus/http/browser_ua.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/models/common/account_type.dart'; import 'package:PiliPlus/models/common/video/video_type.dart'; import 'package:PiliPlus/models/user/danmaku_rule.dart'; import 'package:PiliPlus/models/video/play/url.dart'; import 'package:PiliPlus/models_new/video/video_shot/data.dart'; import 'package:PiliPlus/pages/danmaku/danmaku_model.dart'; import 'package:PiliPlus/pages/sponsor_block/block_mixin.dart'; import 'package:PiliPlus/plugin/pl_player/models/data_source.dart'; import 'package:PiliPlus/plugin/pl_player/models/data_status.dart'; import 'package:PiliPlus/plugin/pl_player/models/double_tap_type.dart'; import 'package:PiliPlus/plugin/pl_player/models/duration.dart'; import 'package:PiliPlus/plugin/pl_player/models/heart_beat_type.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; 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/device_utils.dart'; import 'package:PiliPlus/utils/extension/box_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:archive/archive.dart' show getCrc32; import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:floating/floating.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show HapticFeedback, DeviceOrientation; 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_ce/hive.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:native_device_orientation/native_device_orientation.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:window_manager/window_manager.dart'; typedef PlayCallback = Future? Function(); class PlPlayerController with BlockConfigMixin { Player? _videoPlayerController; VideoController? _videoController; // 添加一个私有静态变量来保存实例 static PlPlayerController? _instance; // 流事件 监听播放状态变化 // StreamSubscription? _playerEventSubs; /// [playerStatus] has a [status] observable final playerStatus = PlPlayerStatus(PlayerStatus.playing); /// final Rx dataStatus = Rx(DataStatus.none); // bool controlsEnabled = false; /// 响应数据 /// 带有Seconds的变量只在秒数更新时更新,以避免频繁触发重绘 // 播放位置 Duration position = Duration.zero; final RxInt positionSeconds = 0.obs; /// 进度条位置 Duration sliderPosition = Duration.zero; final RxInt sliderPositionSeconds = 0.obs; // 展示使用 final Rx sliderTempPosition = Rx(Duration.zero); /// 视频时长 final Rx duration = Rx(Duration.zero); /// 视频缓冲 final Rx buffered = Rx(Duration.zero); final RxInt bufferedSeconds = 0.obs; int _playerCount = 0; late double lastPlaybackSpeed = 1.0; final RxDouble _playbackSpeed = Pref.playSpeedDefault.obs; late final RxDouble _longPressSpeed = Pref.longPressSpeedDefault.obs; /// 音量控制条 final RxDouble volume = RxDouble( PlatformUtils.isDesktop ? Pref.desktopVolume : 1.0, ); /// 亮度控制条 final RxDouble brightness = (-1.0).obs; /// 是否展示控制条 final RxBool showControls = false.obs; /// 亮度控制条展示/隐藏 final RxBool showBrightnessStatus = false.obs; /// 是否长按倍速 final RxBool longPressStatus = false.obs; /// 屏幕锁 为true时,关闭控制栏 final RxBool controlsLock = false.obs; /// 全屏状态 final RxBool isFullScreen = false.obs; // 默认投稿视频格式 bool isLive = false; bool _isVertical = false; /// 视频比例 final Rx videoFit = Rx(VideoFitType.contain); /// 后台播放 late final RxBool continuePlayInBackground = Pref.continuePlayInBackground.obs; /// final RxBool isSliderMoving = false.obs; bool _autoPlay = false; // 记录历史记录 int? _aid; String? _bvid; int? cid; int? _epid; int? _seasonId; int? _pgcType; VideoType _videoType = VideoType.ugc; int _heartDuration = 0; int? width; int? height; late final tryLook = !Accounts.get(AccountType.video).isLogin; late DataSource dataSource; Timer? _timer; StreamSubscription? _subForSeek; Box setting = GStorage.setting; // final Durations durations; String get bvid => _bvid!; /// 视频播放速度 double get playbackSpeed => _playbackSpeed.value; // 长按倍速 double get longPressSpeed => _longPressSpeed.value; /// [videoPlayerController] instance of Player Player? get videoPlayerController => _videoPlayerController; /// [videoController] instance of Player VideoController? get videoController => _videoController; bool isMuted = false; /// 听视频 late final RxBool onlyPlayAudio = false.obs; /// 镜像 late final RxBool flipX = false.obs; late final RxBool flipY = false.obs; final RxBool isBuffering = true.obs; /// 全屏方向 bool get isVertical => _isVertical; /// 弹幕开关 late final RxBool _enableShowDanmaku = Pref.enableShowDanmaku.obs; late final RxBool _enableShowLiveDanmaku = Pref.enableShowLiveDanmaku.obs; RxBool get enableShowDanmaku => isLive ? _enableShowLiveDanmaku : _enableShowDanmaku; bool get isPipMode => (Platform.isAndroid && Floating().isPipMode) || (PlatformUtils.isDesktop && isDesktopPip); late bool isDesktopPip = false; late Rect _lastWindowBounds; late final RxBool isAlwaysOnTop = false.obs; Future setAlwaysOnTop(bool value) { isAlwaysOnTop.value = value; return windowManager.setAlwaysOnTop(value); } Future exitDesktopPip() { isDesktopPip = false; return Future.wait([ windowManager.setTitleBarStyle(TitleBarStyle.normal), windowManager.setMinimumSize(const Size(400, 700)), windowManager.setBounds(_lastWindowBounds), setAlwaysOnTop(false), windowManager.setAspectRatio(0), ]); } Future enterDesktopPip() async { if (isFullScreen.value) return; isDesktopPip = true; _lastWindowBounds = await windowManager.getBounds(); windowManager.setTitleBarStyle(TitleBarStyle.hidden); 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 { size = Size(280.0 * width / height, 280.0); } await windowManager.setMinimumSize(size); setAlwaysOnTop(true); windowManager ..setSize(size) ..setAspectRatio(width / height); } void toggleDesktopPip() { if (isDesktopPip) { exitDesktopPip(); } else { enterDesktopPip(); } } void enterPip({bool isAuto = false}) { if (videoPlayerController != null) { controls = false; final state = videoPlayerController!.state; PageUtils.enterPip( isAuto: isAuto, width: state.width == 0 ? width : state.width, height: state.height == 0 ? height : state.height, ); } } // 弹幕相关配置 late RuleFilter filters = Pref.danmakuFilterRule; // 关联弹幕控制器 DanmakuController? danmakuController; bool showDanmaku = true; Set dmState = {}; late final String midHash = getCrc32( ascii.encode(Accounts.main.mid.toString()), 0, ).toRadixString(16); late final RxDouble danmakuOpacity = Pref.danmakuOpacity.obs; late List speedList = Pref.speedList; late final showControlDuration = const Duration(seconds: 3); // 字幕 late double subtitleFontScale = Pref.subtitleFontScale; late double subtitleFontScaleFS = Pref.subtitleFontScaleFS; late int subtitlePaddingH = Pref.subtitlePaddingH; late int subtitlePaddingB = Pref.subtitlePaddingB; late double subtitleBgOpacity = Pref.subtitleBgOpacity; late double subtitleStrokeWidth = Pref.subtitleStrokeWidth; late int subtitleFontWeight = Pref.subtitleFontWeight; // settings late final fastForBackwardDuration = Duration( seconds: Pref.fastForBackwardDuration, ); late final horizontalSeasonPanel = Pref.horizontalSeasonPanel; late final horizontalPreview = Pref.horizontalPreview; late final keyboardControl = Pref.keyboardControl; late int? cacheVideoQa = PlatformUtils.isMobile ? null : Pref.defaultVideoQa; late int cacheAudioQa = Pref.defaultAudioQa; bool enableHeart = true; late final String? hwdec = Pref.hardwareDecoding; late final offset = Pref.sliderDuration * 1000; num get sliderScale => offset; // 播放顺序相关 late PlayRepeat playRepeat = Pref.playRepeat; TextStyle get subTitleStyle => TextStyle( height: 1.5, fontSize: 16 * (isFullScreen.value ? subtitleFontScaleFS : subtitleFontScale), letterSpacing: 0.1, wordSpacing: 0.1, color: Colors.white, fontWeight: FontWeight.values[subtitleFontWeight], backgroundColor: subtitleBgOpacity == 0 ? null : Colors.black.withValues(alpha: subtitleBgOpacity), ); late final Rx subtitleConfig = getSubConfig.obs; SubtitleViewConfiguration get getSubConfig { final subTitleStyle = this.subTitleStyle; return SubtitleViewConfiguration( style: subTitleStyle, strokeStyle: subtitleBgOpacity == 0 ? subTitleStyle.copyWith( color: null, background: null, backgroundColor: null, foreground: Paint() ..color = Colors.black ..style = PaintingStyle.stroke ..strokeWidth = subtitleStrokeWidth, ) : null, padding: EdgeInsets.only( left: subtitlePaddingH.toDouble(), right: subtitlePaddingH.toDouble(), bottom: subtitlePaddingB.toDouble(), ), textScaleFactor: 1, ); } void updateSubtitleStyle() { subtitleConfig.value = getSubConfig; } void onUpdatePadding(EdgeInsets padding) { subtitlePaddingB = padding.bottom.round().clamp(0, 200); putSubtitleSettings(); } void updateSliderPositionSecond() { int newSecond = sliderPosition.inSeconds; if (sliderPositionSeconds.value != newSecond) { sliderPositionSeconds.value = newSecond; } } void updatePositionSecond() { int newSecond = position.inSeconds; if (positionSeconds.value != newSecond) { positionSeconds.value = newSecond; } } void updateBufferedSecond() { int newSecond = buffered.value.inSeconds; if (bufferedSeconds.value != newSecond) { bufferedSeconds.value = newSecond; } } static PlPlayerController? get instance => _instance; static bool instanceExists() { return _instance != null; } static void setPlayCallBack(PlayCallback? playCallBack) { _playCallBack = playCallBack; } static PlayCallback? _playCallBack; static Future? playIfExists() { // await _instance?.play(repeat: repeat, hideControls: hideControls); return _playCallBack?.call(); } // try to get PlayerStatus static PlayerStatus? getPlayerStatusIfExists() { return _instance?.playerStatus.value; } static Future pauseIfExists({ bool notify = true, bool isInterrupt = false, }) async { if (_instance?.playerStatus.isPlaying ?? false) { await _instance?.pause(notify: notify, isInterrupt: isInterrupt); } } static Future seekToIfExists( Duration position, { bool isSeek = true, }) async { await _instance?.seekTo(position, isSeek: isSeek); } static double? getVolumeIfExists() { return _instance?.volume.value; } static Future? setVolumeIfExists( double volumeNew, { bool showIndicator = true, }) { return _instance?.setVolume(volumeNew, showIndicator: showIndicator); } Box video = GStorage.video; bool visible = true; DeviceOrientation? _orientation; StreamSubscription? _orientationListener; void _stopOrientationListener() { _orientationListener?.cancel(); _orientationListener = null; } void _onOrientationChanged(OrientationParams param) { _orientation = param.orientation; if (!visible) return; final orientation = param.orientation; final isFullScreen = this.isFullScreen.value; if (param.isAutoRotate != true && (!isFullScreen || _isVertical || orientation == .portraitUp || orientation == .portraitDown)) { return; } switch (orientation) { case .portraitUp: if (!_isVertical && controlsLock.value) return; if (!horizontalScreen && !_isVertical && isFullScreen) { if (!isManualFS) { triggerFullScreen(status: false, orientation: orientation); } } else { portraitUpMode(); } case .portraitDown: if (!horizontalScreen) return; if (!_isVertical && controlsLock.value) return; portraitDownMode(); case .landscapeLeft: if (!horizontalScreen && !isFullScreen) { triggerFullScreen(orientation: orientation, isManualFS: false); } else { landscapeLeftMode(); } case .landscapeRight: if (!horizontalScreen && !isFullScreen) { triggerFullScreen(orientation: orientation, isManualFS: false); } else { landscapeRightMode(); } } } // 添加一个私有构造函数 PlPlayerController._() { if (PlatformUtils.isMobile) { _orientationListener = NativeDeviceOrientationPlatform.instance .onOrientationChanged(useSensor: Platform.isAndroid) .listen(_onOrientationChanged); } if (!Accounts.heartbeat.isLogin || Pref.historyPause) { enableHeart = false; } } // 获取实例 传参 static PlPlayerController getInstance({bool isLive = false}) { // 如果实例尚未创建,则创建一个新实例 return (_instance ??= PlPlayerController._()) ..isLive = isLive .._playerCount += 1; } // offline bool get isFileSource => dataSource is FileSource; // 初始化资源 Future setDataSource( DataSource dataSource, { bool isLive = false, bool autoplay = true, // 初始化播放位置 Duration? seekTo, // 初始化播放速度 double speed = 1.0, int? width, int? height, Duration? duration, // 方向 bool? isVertical, // 记录历史记录 int? aid, String? bvid, int? cid, int? epid, int? seasonId, int? pgcType, VideoType? videoType, VoidCallback? onInit, Volume? volume, }) async { try { this.isLive = isLive; _videoType = videoType ?? VideoType.ugc; this.width = width; this.height = height; this.dataSource = dataSource; _autoPlay = autoplay; // 初始化视频倍速 // _playbackSpeed.value = speed; // 初始化数据加载状态 dataStatus.value = DataStatus.loading; // 初始化全屏方向 _isVertical = isVertical ?? false; _aid = aid; _bvid = bvid; this.cid = cid; _epid = epid; _seasonId = seasonId; _pgcType = pgcType; _clearPreview(); cancelLongPressTimer(); if (_videoPlayerController != null && _videoPlayerController!.state.playing) { await pause(notify: false); } if (_playerCount == 0) { return; } // 配置Player 音轨、字幕等等 await _createVideoController(dataSource, seekTo, volume); if (_playerCount == 0) { _removeListeners(); _videoPlayerController?.dispose(); _videoPlayerController = null; _videoController = null; return; } // 获取视频时长 00:00 this.duration.value = duration ?? _videoPlayerController!.state.duration; position = buffered.value = sliderPosition = seekTo ?? Duration.zero; updatePositionSecond(); updateSliderPositionSecond(); updateBufferedSecond(); // 数据加载完成 dataStatus.value = DataStatus.loaded; await _initializePlayer(); onInit?.call(); } catch (err, stackTrace) { dataStatus.value = DataStatus.error; if (kDebugMode) { debugPrint(stackTrace.toString()); debugPrint('plPlayer err: $err'); } } } Future _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 player = await Player.create( configuration: PlayerConfiguration( bufferSize: 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()); _startListeners(player); return player; } // 配置播放器 Future _createVideoController( DataSource dataSource, Duration? seekTo, Volume? volume, ) async { isBuffering.value = false; buffered.value = Duration.zero; _heartDuration = 0; position = Duration.zero; // 初始化时清空弹幕,防止上次重叠 danmakuController?.clear(); var player = _videoPlayerController; if (player == null) { player = await _initPlayer(); if (_playerCount == 0) { _removeListeners(); player.dispose(); player = null; _videoController = null; return; } _videoPlayerController = player; } final Map 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'\:')}"'; } } await player.open( Media( video, start: seekTo, extras: extras.isEmpty ? null : extras, ), play: false, ); } Future? refreshPlayer() { if (dataSource is FileSource) { return null; } if (_videoPlayerController?.current.isNotEmpty ?? false) { return _videoPlayerController!.open( _videoPlayerController!.current.last.copyWith(start: position), play: true, ); } return null; } // 开始播放 Future _initializePlayer() async { if (_instance == null) return; // 设置倍速 if (isLive) { await setPlaybackSpeed(1.0); } else { if (_videoPlayerController?.state.rate != _playbackSpeed.value) { await setPlaybackSpeed(_playbackSpeed.value); } } _initVideoFit(); // if (_looping) { // await setLooping(_looping); // } // 跳转播放 // if (seekTo != Duration.zero) { // await this.seekTo(seekTo); // } // 自动播放 if (_autoPlay) { playIfExists(); // await play(duration: duration); } } List? _subscriptions; final Set> _positionListeners = {}; final Set> _statusListeners = {}; /// 播放事件监听 void _startListeners(NativePlayer player) { assert(_subscriptions == null); final stream = player.stream; _subscriptions = [ stream.playing.listen((event) { WakelockPlus.toggle(enable: event); if (event) { playerStatus.value = PlayerStatus.playing; } else { playerStatus.value = PlayerStatus.paused; } videoPlayerServiceHandler?.onStatusChange( playerStatus.value, isBuffering.value, isLive, ); /// 触发回调事件 for (final element in _statusListeners) { element(event ? PlayerStatus.playing : PlayerStatus.paused); } if (videoPlayerController!.state.position.inSeconds != 0) { makeHeartBeat(positionSeconds.value, type: HeartBeatType.status); } }), stream.completed.listen((event) { if (event) { playerStatus.value = PlayerStatus.completed; /// 触发回调事件 for (final element in _statusListeners) { element(PlayerStatus.completed); } } else { // playerStatus.value = PlayerStatus.playing; } makeHeartBeat(positionSeconds.value, type: HeartBeatType.completed); }), stream.position.listen((event) { position = event; updatePositionSecond(); if (!isSliderMoving.value) { sliderPosition = event; updateSliderPositionSecond(); } /// 触发回调事件 for (final element in _positionListeners) { element(event); } makeHeartBeat(event.inSeconds); }), stream.duration.listen((Duration event) { duration.value = event; }), stream.buffer.listen((Duration event) { buffered.value = event; updateBufferedSecond(); }), stream.buffering.listen((bool event) { isBuffering.value = event; videoPlayerServiceHandler?.onStatusChange( playerStatus.value, event, isLive, ); }), // 媒体通知监听 if (videoPlayerServiceHandler != null) ...[ playerStatus.listen((PlayerStatus event) { videoPlayerServiceHandler!.onStatusChange( event, isBuffering.value, isLive, ); }), positionSeconds.listen((int event) { videoPlayerServiceHandler!.onPositionChange(Duration(seconds: event)); }), ], ]; } /// 移除事件监听 void _removeListeners() { _subscriptions?.forEach((e) => e.cancel()); _subscriptions?.clear(); _subscriptions = null; } void _cancelSubForSeek() { if (_subForSeek != null) { _subForSeek!.cancel(); _subForSeek = null; } } /// 跳转至指定位置 Future seekTo(Duration position, {bool isSeek = true}) async { // if (position >= duration.value) { // position = duration.value - const Duration(milliseconds: 100); // } if (_playerCount == 0) { return; } if (position < Duration.zero) { position = Duration.zero; } this.position = position; updatePositionSecond(); _heartDuration = position.inSeconds; Future seek() async { if (isSeek) { /// 拖动进度条调节时,不等待第一帧,防止抖动 await _videoPlayerController?.stream.buffer.first; } danmakuController?.clear(); try { await _videoPlayerController?.seek(position); } catch (e) { if (kDebugMode) debugPrint('seek failed: $e'); } } if (duration.value != Duration.zero) { seek(); } else { // if (kDebugMode) debugPrint('seek duration else'); _subForSeek?.cancel(); _subForSeek = duration.listen((_) { seek(); _cancelSubForSeek(); }); } } /// 设置倍速 Future setPlaybackSpeed(double speed) async { lastPlaybackSpeed = playbackSpeed; if (speed == _videoPlayerController?.state.rate) { return; } await _videoPlayerController?.setRate(speed); _playbackSpeed.value = speed; if (danmakuController != null) { try { DanmakuOption currentOption = danmakuController!.option; double defaultDuration = currentOption.duration * lastPlaybackSpeed; double defaultStaticDuration = currentOption.staticDuration * lastPlaybackSpeed; DanmakuOption updatedOption = currentOption.copyWith( duration: defaultDuration / speed, staticDuration: defaultStaticDuration / speed, ); danmakuController!.updateOption(updatedOption); } catch (_) {} } } // 还原默认速度 double playSpeedDefault = Pref.playSpeedDefault; Future setDefaultSpeed() async { await _videoPlayerController?.setRate(playSpeedDefault); _playbackSpeed.value = playSpeedDefault; } /// 播放视频 Future play({bool repeat = false, bool hideControls = true}) async { if (_playerCount == 0) return; // 播放时自动隐藏控制条 controls = !hideControls; // repeat为true,将从头播放 if (repeat) { // await seekTo(Duration.zero); await seekTo(Duration.zero, isSeek: false); } await _videoPlayerController?.play(); audioSessionHandler?.setActive(true); playerStatus.value = PlayerStatus.playing; // screenManager.setOverlays(false); } /// 暂停播放 Future pause({bool notify = true, bool isInterrupt = false}) async { await _videoPlayerController?.pause(); playerStatus.value = PlayerStatus.paused; // 主动暂停时让出音频焦点 if (!isInterrupt) { audioSessionHandler?.setActive(false); } } bool tripling = false; /// 隐藏控制条 void hideTaskControls() { _timer?.cancel(); _timer = Timer(showControlDuration, () { if (!isSliderMoving.value && !tripling) { controls = false; } _timer = null; }); } /// 调整播放时间 void onChangedSlider(int v) { sliderPosition = Duration(seconds: v); updateSliderPositionSecond(); } void onChangedSliderStart([Duration? value]) { if (value != null) { sliderTempPosition.value = value; } isSliderMoving.value = true; } bool? cancelSeek; bool? hasToast; void onUpdatedSliderProgress(Duration value) { sliderTempPosition.value = value; sliderPosition = value; updateSliderPositionSecond(); } void onChangedSliderEnd() { cancelSeek = null; hasToast = null; isSliderMoving.value = false; hideTaskControls(); } final RxBool volumeIndicator = false.obs; Timer? volumeTimer; bool volumeInterceptEventStream = false; static final double maxVolume = PlatformUtils.isDesktop ? 2.0 : 1.0; Future setVolume(double volume, {bool showIndicator = true}) async { if (this.volume.value != volume) { this.volume.value = volume; try { if (PlatformUtils.isDesktop) { _videoPlayerController!.setVolume(volume * 100); } else { FlutterVolumeController.updateShowSystemUI(false); await FlutterVolumeController.setVolume(volume); } } catch (err) { if (kDebugMode) debugPrint(err.toString()); } } if (showIndicator) { volumeIndicator.value = true; } volumeInterceptEventStream = true; volumeTimer?.cancel(); volumeTimer = Timer(const Duration(milliseconds: 200), () { volumeIndicator.value = false; volumeInterceptEventStream = false; if (PlatformUtils.isDesktop) { setting.put(SettingBoxKey.desktopVolume, volume.toPrecision(3)); } }); } /// Toggle Change the videofit accordingly void toggleVideoFit(VideoFitType value) { _prefFit = videoFit.value = value; video.put(VideoBoxKey.cacheVideoFit, value.index); } /// 读取fit var _prefFit = VideoFitType.values[Pref.cacheVideoFit]; void _initVideoFit() { if (_prefFit == .fill && _isVertical) { videoFit.value = .contain; } else { videoFit.value = _prefFit; } } set controls(bool visible) { showControls.value = visible; _timer?.cancel(); if (visible) { hideTaskControls(); } } Timer? longPressTimer; void cancelLongPressTimer() { longPressTimer?.cancel(); longPressTimer = null; } /// 设置长按倍速状态 live模式下禁用 Future setLongPressStatus(bool val) async { if (isLive) { return; } if (controlsLock.value) { return; } if (longPressStatus.value == val) { return; } if (val) { if (playerStatus.isPlaying) { longPressStatus.value = val; HapticFeedback.lightImpact(); await setPlaybackSpeed(longPressSpeed); } } else { // if (kDebugMode) debugPrint('$playbackSpeed'); longPressStatus.value = val; await setPlaybackSpeed(lastPlaybackSpeed); } } bool get _isCompleted => videoPlayerController!.state.completed || (duration.value - position).inMilliseconds <= 50; // 双击播放、暂停 Future onDoubleTapCenter() async { if (!isLive && _isCompleted) { await videoPlayerController!.seek(Duration.zero); videoPlayerController!.play(); } else { videoPlayerController!.playOrPause(); } } final RxBool mountSeekBackwardButton = false.obs; final RxBool mountSeekForwardButton = false.obs; void onDoubleTapSeekBackward() { mountSeekBackwardButton.value = true; } void onDoubleTapSeekForward() { mountSeekForwardButton.value = true; } void onForward(Duration duration) { onForwardBackward(position + duration); } void onBackward(Duration duration) { onForwardBackward(position - duration); } void onForwardBackward(Duration duration) { seekTo( duration.clamp(Duration.zero, videoPlayerController!.state.duration), isSeek: false, ).whenComplete(play); } void doubleTapFuc(DoubleTapType type) { switch (type) { case DoubleTapType.left: // 双击左边区域 👈 onDoubleTapSeekBackward(); break; case DoubleTapType.center: onDoubleTapCenter(); break; case DoubleTapType.right: // 双击右边区域 👈 onDoubleTapSeekForward(); break; } } /// 关闭控制栏 void onLockControl(bool val) { controlsLock.value = val; if (!val && showControls.value) { showControls.refresh(); } controls = !val; } void _setFullScreen(bool val) { isFullScreen.value = val; updateSubtitleStyle(); } double screenRatio = 0.0; bool isManualFS = true; late final horizontalScreen = Pref.horizontalScreen; Future? changeOrientation({ required bool isVertical, DeviceOrientation? orientation, }) { if (orientation == null && isVertical) { return portraitUpMode(); } else { // https://github.com/flutter/flutter/issues/73651 // https://github.com/flutter/flutter/issues/183708 if (Platform.isAndroid) { if ((orientation ?? _orientation) == .landscapeRight) { return landscapeRightMode(); } else { return landscapeLeftMode(); } } else { if (orientation == .landscapeLeft) { return landscapeLeftMode(); } else { return landscapeRightMode(); } } } } // 全屏 bool _fsProcessing = false; Future triggerFullScreen({ bool status = true, bool inAppFullScreen = false, DeviceOrientation? orientation, bool isManualFS = true, }) async { if (isDesktopPip) return; if (isFullScreen.value == status) return; if (_fsProcessing) return; _fsProcessing = true; this.isManualFS = isManualFS; try { if (status) { if (PlatformUtils.isMobile) { hideSystemBar(); await changeOrientation( isVertical: isVertical, orientation: orientation, ); } else { await enterDesktopFullScreen(inAppFullScreen: inAppFullScreen); } } else { if (PlatformUtils.isMobile) { showSystemBar(); await resetScreenRotation(); } else { await exitDesktopFullScreen(); } } } finally { _setFullScreen(status); _fsProcessing = false; } } void addPositionListener(ValueChanged listener) { if (_playerCount == 0) return; _positionListeners.add(listener); } void removePositionListener(ValueChanged listener) => _positionListeners.remove(listener); void addStatusLister(ValueChanged listener) { if (_playerCount == 0) return; _statusListeners.add(listener); } void removeStatusLister(ValueChanged listener) => _statusListeners.remove(listener); // 记录播放记录 Future? makeHeartBeat( int progress, { HeartBeatType type = .playing, bool isManual = false, dynamic aid, dynamic bvid, dynamic cid, dynamic epid, dynamic seasonId, dynamic pgcType, VideoType? videoType, }) { if (isLive || !enableHeart || progress == 0 || (playerStatus.isPaused && !isManual)) { return null; } Future send() { return VideoHttp.heartBeat( aid: aid ?? _aid, bvid: bvid ?? _bvid, cid: cid ?? this.cid, progress: progress, epid: epid ?? _epid, seasonId: seasonId ?? _seasonId, subType: pgcType ?? _pgcType, videoType: videoType ?? _videoType, ); } switch (type) { case .playing: if (progress - _heartDuration >= 5) { _heartDuration = progress; return send(); } case .status: if (progress - _heartDuration >= 2) { _heartDuration = progress; return send(); } case .completed: if (playerStatus.isCompleted && (duration.value - position).inMilliseconds <= 1000) { progress = -1; } return send(); } return null; } void setPlayRepeat(PlayRepeat type) { playRepeat = type; video.put(VideoBoxKey.playRepeat, type.index); } void putSubtitleSettings() { setting.putAllNE({ SettingBoxKey.subtitleFontScale: subtitleFontScale, SettingBoxKey.subtitleFontScaleFS: subtitleFontScaleFS, SettingBoxKey.subtitlePaddingH: subtitlePaddingH, SettingBoxKey.subtitlePaddingB: subtitlePaddingB, SettingBoxKey.subtitleBgOpacity: subtitleBgOpacity, SettingBoxKey.subtitleStrokeWidth: subtitleStrokeWidth, SettingBoxKey.subtitleFontWeight: subtitleFontWeight, }); } bool _isCloseAll = false; bool get isCloseAll => _isCloseAll; Future? resetScreenRotation() { if (horizontalScreen) { return fullMode(); } else { return portraitUpMode(); } } void onCloseAll() { _isCloseAll = true; dispose(); Get.until((route) => route.isFirst); } void dispose() { // 每次减1,最后销毁 resetScreenRotation(); cancelLongPressTimer(); _cancelSubForSeek(); if (!_isCloseAll && _playerCount > 1) { _playerCount -= 1; _heartDuration = 0; return; } _playerCount = 0; danmakuController = null; _stopOrientationListener(); setPlayCallBack(null); dmState.clear(); _clearPreview(); _timer?.cancel(); if (PlatformUtils.isDesktop && isAlwaysOnTop.value) { windowManager.setAlwaysOnTop(false); } _removeListeners(); _positionListeners.clear(); _statusListeners.clear(); if (playerStatus.isPlaying) { WakelockPlus.disable(); } if (kDebugMode) { debugPrint('dispose player'); } _videoPlayerController?.dispose(); _videoPlayerController = null; _videoController = null; _instance = null; videoPlayerServiceHandler?.clear(); } static void updatePlayCount() { if (_instance?._playerCount == 1) { _instance?.dispose(); } else { _instance?._playerCount -= 1; } } void setContinuePlayInBackground() { continuePlayInBackground.value = !continuePlayInBackground.value; setting.put( SettingBoxKey.continuePlayInBackground, continuePlayInBackground.value, ); } void setOnlyPlayAudio() { onlyPlayAudio.value = !onlyPlayAudio.value; videoPlayerController?.setVideoTrack( onlyPlayAudio.value ? VideoTrack.no() : VideoTrack.auto(), ); } late final Map previewCache = {}; LoadingState? videoShot; late final RxBool showPreview = false.obs; late final previewIndex = RxnInt(); void updatePreviewIndex(int seconds) { if (videoShot == null) { videoShot = LoadingState.loading(); getVideoShot(); return; } if (videoShot case Success(:final response)) { showPreview.value = true; previewIndex.value = max( 0, (response.index.where((item) => item <= seconds).length - 2), ); } } void _clearPreview() { showPreview.value = false; previewIndex.value = null; videoShot = null; for (final i in previewCache.values) { i?.dispose(); } previewCache.clear(); } Future getVideoShot() async { videoShot = await VideoHttp.videoshot(bvid: bvid, cid: cid!); } void takeScreenshot() { SmartDialog.showToast('截图中'); videoPlayerController?.screenshot(format: .png).then((value) { if (value != null) { SmartDialog.showToast('点击弹窗保存截图'); showDialog( context: Get.context!, builder: (context) => GestureDetector( onTap: () { Get.back(); ImageUtils.saveByteImg( bytes: value, fileName: 'screenshot_${ImageUtils.time}', ); }, child: Align( alignment: Alignment.centerRight, child: Padding( padding: const EdgeInsets.only(right: 12), child: ConstrainedBox( constraints: BoxConstraints( maxWidth: min(DeviceUtils.size.width / 3, 350), ), child: DecoratedBox( decoration: BoxDecoration( border: Border.all( width: 5, color: ThemeUtils.theme.colorScheme.surface, ), ), child: Padding( padding: const EdgeInsets.all(5), child: Image.memory(value), ), ), ), ), ), ), ); } else { SmartDialog.showToast('截图失败'); } }); } void onPopInvokedWithResult(bool didPop, Object? result) { if (didPop) { if (playerStatus.isPlaying) { pause(); } setPlayCallBack(null); return; } if (controlsLock.value) { onLockControl(false); return; } if (isDesktopPip) { exitDesktopPip(); return; } if (isFullScreen.value) { triggerFullScreen(status: false); return; } Get.back(); } }