import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math' show max, min; import 'dart:ui' as ui; import 'package:PiliPlus/common/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'; 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'; 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/dnamaku_model.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/bottom_progress_behavior.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/fullscreen_mode.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/extension.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart' show PageUtils; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:crclib/catalog.dart'; import 'package:dio/dio.dart' show Options; 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'; 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'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:window_manager/window_manager.dart'; class PlPlayerController { Player? _videoPlayerController; VideoController? _videoController; // 添加一个私有静态变量来保存实例 static PlPlayerController? _instance; // 流事件 监听播放状态变化 StreamSubscription? _playerEventSubs; /// [playerStatus] has a [status] observable final PlPlayerStatus playerStatus = PlPlayerStatus(); /// final PlPlayerDataStatus dataStatus = PlPlayerDataStatus(); // bool controlsEnabled = false; /// 响应数据 /// 带有Seconds的变量只在秒数更新时更新,以避免频繁触发重绘 // 播放位置 final Rx _position = Rx(Duration.zero); final RxInt positionSeconds = 0.obs; final Rx _sliderPosition = Rx(Duration.zero); final RxInt sliderPositionSeconds = 0.obs; // 展示使用 final Rx _sliderTempPosition = Rx(Duration.zero); final Rx _duration = Rx(Duration.zero); final Rx durationSeconds = Duration.zero.obs; 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 _currentVolume = RxDouble( Utils.isDesktop ? Pref.desktopVolume : 1.0, ); final RxDouble _currentBrightness = (-1.0).obs; final RxBool _showControls = false.obs; final RxBool _showVolumeStatus = false.obs; final RxBool _showBrightnessStatus = false.obs; final RxBool _longPressStatus = false.obs; final RxBool _controlsLock = false.obs; final RxBool _isFullScreen = false.obs; // 默认投稿视频格式 bool isLive = false; bool _isVertical = false; final Rx _videoFit = Rx(VideoFitType.contain); late StreamSubscription _dataListenerForVideoFit; late StreamSubscription _dataListenerForEnterFullscreen; /// 后台播放 late final RxBool _continuePlayInBackground = Pref.continuePlayInBackground.obs; /// final RxBool _isSliderMoving = false.obs; PlaylistMode _looping = PlaylistMode.none; 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 && Pref.p1080; late DataSource dataSource; Timer? _timer; Timer? _timerForSeek; Timer? _timerForShowingVolume; Box setting = GStorage.setting; // final Durations durations; String get bvid => _bvid!; /// 数据加载监听 Stream get onDataStatusChanged => dataStatus.status.stream; /// 播放状态监听 Stream get onPlayerStatusChanged => playerStatus.status.stream; /// 视频时长 Rx get duration => _duration; Stream get onDurationChanged => _duration.stream; /// 视频当前播放位置 Rx get position => _position; Stream get onPositionChanged => _position.stream; /// 视频播放速度 double get playbackSpeed => _playbackSpeed.value; // 长按倍速 double get longPressSpeed => _longPressSpeed.value; /// 视频缓冲 Rx get buffered => _buffered; Stream get onBufferedChanged => _buffered.stream; /// [videoPlayerController] instance of Player Player? get videoPlayerController => _videoPlayerController; /// [videoController] instance of Player VideoController? get videoController => _videoController; RxBool get isSliderMoving => _isSliderMoving; /// 进度条位置及监听 Rx get sliderPosition => _sliderPosition; Stream get onSliderPositionChanged => _sliderPosition.stream; Rx get sliderTempPosition => _sliderTempPosition; // Stream get onSliderPositionChanged => _sliderPosition.stream; /// 是否展示控制条及监听 RxBool get showControls => _showControls; Stream get onShowControlsChanged => _showControls.stream; /// 音量控制条展示/隐藏 RxBool get showVolumeStatus => _showVolumeStatus; Stream get onShowVolumeStatusChanged => _showVolumeStatus.stream; /// 亮度控制条展示/隐藏 RxBool get showBrightnessStatus => _showBrightnessStatus; Stream get onShowBrightnessStatusChanged => _showBrightnessStatus.stream; /// 音量控制条 RxDouble get volume => _currentVolume; Stream get onVolumeChanged => _currentVolume.stream; late bool isMuted = false; /// 亮度控制条 RxDouble get brightness => _currentBrightness; Stream get onBrightnessChanged => _currentBrightness.stream; /// 是否循环 PlaylistMode get looping => _looping; /// 是否自动播放 bool get autoplay => _autoPlay; /// 视频比例 Rx get videoFit => _videoFit; /// 后台播放 RxBool get continuePlayInBackground => _continuePlayInBackground; /// 听视频 late final RxBool onlyPlayAudio = false.obs; /// 镜像 late final RxBool flipX = false.obs; late final RxBool flipY = false.obs; /// 是否长按倍速 RxBool get longPressStatus => _longPressStatus; RxBool isBuffering = true.obs; /// 屏幕锁 为true时,关闭控制栏 RxBool get controlsLock => _controlsLock; /// 全屏状态 RxBool get isFullScreen => _isFullScreen; /// 全屏方向 bool get isVertical => _isVertical; /// 弹幕开关 late final RxBool _enableShowDanmaku = Pref.enableShowDanmaku.obs; late final RxBool _enableShowLiveDanmaku = Pref.enableShowLiveDanmaku.obs; RxBool get enableShowDanmaku => isLive ? _enableShowLiveDanmaku : _enableShowDanmaku; late final bool autoPiP = Pref.autoPiP; bool get isPipMode => (Platform.isAndroid && Floating().isPipMode) || (Utils.isDesktop && isDesktopPip); late bool isDesktopPip = false; late Rect _lastWindowBounds; Offset initialFocalPoint = Offset.zero; Future exitDesktopPip() async { isDesktopPip = false; await Future.wait([ windowManager.setTitleBarStyle(TitleBarStyle.normal), windowManager.setMinimumSize(const Size(400, 700)), windowManager.setBounds(_lastWindowBounds), windowManager.setAlwaysOnTop(false), setting.putAll({ SettingBoxKey.windowSize: [ _lastWindowBounds.width, _lastWindowBounds.height, ], SettingBoxKey.windowPosition: [ _lastWindowBounds.left, _lastWindowBounds.top, ], }), ]); } Future enterDesktopPip() async { isDesktopPip = true; _lastWindowBounds = await windowManager.getBounds(); 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; if (height > width) { size = Size(280.0, 280.0 * height / width); } else { size = Size(280.0 * width / height, 280.0); } await windowManager.setMinimumSize(size); windowManager ..setSize(size) ..setAlwaysOnTop(true); } void toggleDesktopPip() { if (isDesktopPip) { exitDesktopPip(); } else { enterDesktopPip(); } } late bool _shouldSetPip = false; bool get _isCurrVideoPage { final currentRoute = Get.currentRoute; return currentRoute.startsWith('/video') || currentRoute.startsWith('/liveRoom'); } bool get _isPreviousVideoPage { final previousRoute = Get.previousRoute; return previousRoute.startsWith('/video') || previousRoute.startsWith('/liveRoom'); } void enterPip({bool isAuto = false}) { if (videoController != null) { final state = videoController!.player.state; PageUtils.enterPip( isAuto: isAuto, width: state.width ?? width, height: state.height ?? height, ); } } void disableAutoEnterPipIfNeeded() { if (!_isPreviousVideoPage) { disableAutoEnterPip(); } } void disableAutoEnterPip() { if (_shouldSetPip) { Utils.channel.invokeMethod('setPipAutoEnterEnabled', { 'autoEnable': false, }); } } /// 弹幕权重 late final enableTapDm = Utils.isMobile && Pref.enableTapDm; late int danmakuWeight = Pref.danmakuWeight; late RuleFilter filters = Pref.danmakuFilterRule; // 关联弹幕控制器 DanmakuController? danmakuController; bool showDanmaku = true; Set dmState = {}; late final mergeDanmaku = Pref.mergeDanmaku; late final String midHash = Crc32Xz() .convert(utf8.encode(Accounts.main.mid.toString())) .toRadixString(16); // 弹幕相关配置 late Set blockTypes = Pref.danmakuBlockType; late bool blockColorful = blockTypes.contains(6); late double showArea = Pref.danmakuShowArea; late RxDouble danmakuOpacity = Pref.danmakuOpacity.obs; late double danmakuFontScale = Pref.danmakuFontScale; late double danmakuFontScaleFS = Pref.danmakuFontScaleFS; late double danmakuStrokeWidth = Pref.strokeWidth; late int danmakuFontWeight = Pref.fontWeight; late bool massiveMode = Pref.danmakuMassiveMode; late double danmakuDuration = Pref.danmakuDuration; late double danmakuStaticDuration = Pref.danmakuStaticDuration; late List speedList = Pref.speedList; late bool enableAutoLongPressSpeed = Pref.enableAutoLongPressSpeed; late final showControlDuration = Pref.enableLongShowControl ? const Duration(seconds: 30) : const Duration(seconds: 3); late double subtitleFontScale = Pref.subtitleFontScale; late double subtitleFontScaleFS = Pref.subtitleFontScaleFS; late double danmakuLineHeight = Pref.danmakuLineHeight; late int subtitlePaddingH = Pref.subtitlePaddingH; late int subtitlePaddingB = Pref.subtitlePaddingB; late double subtitleBgOpaticy = Pref.subtitleBgOpaticy; final bool showVipDanmaku = Pref.showVipDanmaku; // loop unswitching final bool showSpecialDanmaku = Pref.showSpecialDanmaku; late double subtitleStrokeWidth = Pref.subtitleStrokeWidth; late int subtitleFontWeight = Pref.subtitleFontWeight; late final pgcSkipType = Pref.pgcSkipType; late final enablePgcSkip = Pref.pgcSkipType != SkipType.disable; // sponsor block late final bool enableSponsorBlock = Pref.enableSponsorBlock || enablePgcSkip; late final double blockLimit = Pref.blockLimit; late final blockSettings = Pref.blockSettings; late final List blockColor = Pref.blockColor; late final Set enableList = blockSettings .where((item) => item.second != SkipType.disable) .map((item) => item.first.name) .toSet(); late final blockServer = Pref.blockServer; // settings late final showFSActionItem = Pref.showFSActionItem; late final enableShrinkVideoSize = Pref.enableShrinkVideoSize; late final darkVideoPage = Pref.darkVideoPage; late final enableSlideVolumeBrightness = Pref.enableSlideVolumeBrightness; late final enableSlideFS = Pref.enableSlideFS; late final enableDragSubtitle = Pref.enableDragSubtitle; late final fastForBackwardDuration = Duration( seconds: Pref.fastForBackwardDuration, ); late final horizontalSeasonPanel = Pref.horizontalSeasonPanel; late final preInitPlayer = Pref.preInitPlayer; late final showRelatedVideo = Pref.showRelatedVideo; late final showVideoReply = Pref.showVideoReply; late final showBangumiReply = Pref.showBangumiReply; late final reverseFromFirst = Pref.reverseFromFirst; late final horizontalPreview = Pref.horizontalPreview; late final showDmChart = Pref.showDmChart; late final showViewPoints = Pref.showViewPoints; late final showFsScreenshotBtn = Pref.showFsScreenshotBtn; late final showFsLockBtn = Pref.showFsLockBtn; late final keyboardControl = Pref.keyboardControl; late final bool autoExitFullscreen = Pref.autoExitFullscreen; late final bool autoPlayEnable = Pref.autoPlayEnable; late final bool enableVerticalExpand = Pref.enableVerticalExpand; late final bool pipNoDanmaku = Pref.pipNoDanmaku; late final bool tempPlayerConf = Pref.tempPlayerConf; late int? cacheVideoQa = Utils.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 progressType = BtmProgressBehavior.values[Pref.btmProgressBehavior]; late final enableQuickDouble = Pref.enableQuickDouble; late final fullScreenGestureReverse = Pref.fullScreenGestureReverse; late final isRelative = Pref.useRelativeSlide; late final offset = isRelative ? Pref.sliderDuration / 100 : Pref.sliderDuration * 1000; num get sliderScale => isRelative ? duration.value.inMilliseconds * offset : offset; // 播放顺序相关 late PlayRepeat playRepeat = PlayRepeat.values[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: subtitleBgOpaticy == 0 ? null : Colors.black.withValues(alpha: subtitleBgOpaticy), ); late final Rx subtitleConfig = _getSubConfig.obs; SubtitleViewConfiguration get _getSubConfig { final subTitleStyle = this.subTitleStyle; return SubtitleViewConfiguration( style: subTitleStyle, strokeStyle: subtitleBgOpaticy == 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.value.inSeconds; if (sliderPositionSeconds.value != newSecond) { sliderPositionSeconds.value = newSecond; } } void updatePositionSecond() { int newSecond = _position.value.inSeconds; if (positionSeconds.value != newSecond) { positionSeconds.value = newSecond; } } void updateDurationSecond() { if (durationSeconds.value != _duration.value) { durationSeconds.value = _duration.value; } } 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(Function? playCallBack) { _playCallBack = playCallBack; } static Function? _playCallBack; static void playIfExists({bool repeat = false, bool hideControls = true}) { // await _instance?.play(repeat: repeat, hideControls: hideControls); _playCallBack?.call(); } // try to get PlayerStatus static PlayerStatus? getPlayerStatusIfExists() { return _instance?.playerStatus.status.value; } static Future pauseIfExists({ bool notify = true, bool isInterrupt = false, }) async { if (_instance?.playerStatus.status.value == PlayerStatus.playing) { 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) async { await _instance?.setVolume(volumeNew); } Box video = GStorage.video; // 添加一个私有构造函数 PlPlayerController._() { if (!Accounts.heartbeat.isLogin || Pref.historyPause) { enableHeart = false; } if (Platform.isAndroid && autoPiP) { Utils.sdkInt.then((sdkInt) { if (sdkInt < 31) { Utils.channel.setMethodCallHandler((call) async { if (call.method == 'onUserLeaveHint') { if (playerStatus.playing && _isCurrVideoPage) { enterPip(); } } }); } else { _shouldSetPip = true; } }); } // _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) { // if (status == PlayerStatus.playing) { // WakelockPlus.enable(); // } else { // WakelockPlus.disable(); // } // }); } // 获取实例 传参 static PlPlayerController getInstance({bool isLive = false}) { // 如果实例尚未创建,则创建一个新实例 _instance ??= PlPlayerController._(); _instance! ..isLive = isLive .._playerCount += 1; return _instance!; } // 初始化资源 Future setDataSource( DataSource dataSource, { bool isLive = false, bool autoplay = true, // 默认不循环 PlaylistMode looping = PlaylistMode.none, // 初始化播放位置 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? callback, Volume? volume, }) async { try { this.isLive = isLive; _videoType = videoType ?? VideoType.ugc; this.width = width; this.height = height; this.dataSource = dataSource; _autoPlay = autoplay; _looping = looping; // 初始化视频倍速 // _playbackSpeed.value = speed; // 初始化数据加载状态 dataStatus.status.value = DataStatus.loading; // 初始化全屏方向 _isVertical = isVertical ?? false; _aid = aid; _bvid = bvid; this.cid = cid; _epid = epid; _seasonId = seasonId; _pgcType = pgcType; if (showSeekPreview) { _clearPreview(); } cancelLongPressTimer(); if (_videoPlayerController != null && _videoPlayerController!.state.playing) { await pause(notify: false); } if (_playerCount == 0) { return; } // 配置Player 音轨、字幕等等 _videoPlayerController = await _createVideoController( dataSource, _looping, seekTo, volume, ); callback?.call(); // 获取视频时长 00:00 _duration.value = duration ?? _videoPlayerController!.state.duration; _position.value = _buffered.value = _sliderPosition.value = seekTo ?? Duration.zero; updateDurationSecond(); updatePositionSecond(); updateSliderPositionSecond(); updateBufferedSecond(); // 数据加载完成 dataStatus.status.value = DataStatus.loaded; // listen the video player events startListeners(); await _initializePlayer(); } catch (err, stackTrace) { dataStatus.status.value = DataStatus.error; if (kDebugMode) { debugPrint(stackTrace.toString()); debugPrint('plPlayer err: $err'); } } } Directory? shadersDirectory; Future copyShadersToExternalDirectory() async { if (shadersDirectory != null) { return shadersDirectory; } final manifestContent = await rootBundle.loadString('AssetManifest.json'); final Map manifestMap = json.decode(manifestContent); final directory = await getApplicationSupportDirectory(); shadersDirectory = Directory(path.join(directory.path, 'anime_shaders')); if (!shadersDirectory!.existsSync()) { await shadersDirectory!.create(recursive: true); } final shaderFiles = manifestMap.keys.where( (String key) => key.startsWith('assets/shaders/') && key.endsWith('.glsl'), ); // int copiedFilesCount = 0; for (var filePath in shaderFiles) { final fileName = filePath.split('/').last; final targetFile = File(path.join(shadersDirectory!.path, fileName)); if (targetFile.existsSync()) { continue; } try { final data = await rootBundle.load(filePath); final List bytes = data.buffer.asUint8List(); await targetFile.writeAsBytes(bytes); // copiedFilesCount++; } catch (e) { if (kDebugMode) debugPrint('$e'); } } return shadersDirectory; } late final isAnim = _pgcType == 1 || _pgcType == 4; late final Rx superResolutionType = (isAnim ? Pref.superResolutionType : SuperResolutionType.disable).obs; Future setShader([SuperResolutionType? type, NativePlayer? pp]) async { if (type == null) { type = superResolutionType.value; } else { superResolutionType.value = type; if (isAnim && !tempPlayerConf) { setting.put(SettingBoxKey.superResolutionType, type.index); } } pp ??= _videoPlayerController!.platform!; await pp.waitForPlayerInitialization; await pp.waitForVideoControllerInitializationIfAttached; switch (type) { case SuperResolutionType.disable: return pp.command(['change-list', 'glsl-shaders', 'clr', '']); case SuperResolutionType.efficiency: return pp.command([ 'change-list', 'glsl-shaders', 'set', Utils.buildShadersAbsolutePath( (await copyShadersToExternalDirectory())?.path ?? '', Constants.mpvAnime4KShadersLite, ), ]); case SuperResolutionType.quality: return pp.command([ 'change-list', 'glsl-shaders', 'set', Utils.buildShadersAbsolutePath( (await copyShadersToExternalDirectory())?.path ?? '', Constants.mpvAnime4KShaders, ), ]); } } static final loudnormRegExp = RegExp('loudnorm=([^,]+)'); // 配置播放器 Future _createVideoController( DataSource dataSource, PlaylistMode looping, Duration? seekTo, Volume? volume, ) async { // 每次配置时先移除监听 removeListeners(); isBuffering.value = false; buffered.value = Duration.zero; _heartDuration = 0; _position.value = Duration.zero; // 初始化时清空弹幕,防止上次重叠 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), ), ); final pp = player.platform!; if (_videoPlayerController == null) { if (Utils.isDesktop) { pp.setVolume(this.volume.value * 100); } if (isAnim) { setShader(superResolutionType.value, pp); } await pp.setProperty( "af", "scaletempo2=max-speed=8", ); if (Platform.isAndroid) { await pp.setProperty("volume-max", "100"); String ao = Pref.useOpenSLES ? "opensles,audiotrack" : "audiotrack,opensles"; await pp.setProperty("ao", ao); } // video-sync=display-resample await pp.setProperty("video-sync", Pref.videoSync); // 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 (dataSource.audioSource?.isNotEmpty == true) { await pp.setProperty( 'audio-files', Platform.isWindows ? dataSource.audioSource!.replaceAll(';', '\\;') : dataSource.audioSource!.replaceAll(':', '\\:'), ); } else { await pp.setProperty('audio-files', ''); } // 字幕 if (dataSource.subFiles?.isNotEmpty == true) { await pp.setProperty( 'sub-files', Platform.isWindows ? dataSource.subFiles!.replaceAll(';', '\\;') : dataSource.subFiles!.replaceAll(':', '\\:'), ); await pp.setProperty("subs-with-matching-audio", "no"); await pp.setProperty("sub-forced-only", "yes"); await pp.setProperty("blend-subtitles", "video"); } _videoController ??= VideoController( player, configuration: VideoControllerConfiguration( enableHardwareAcceleration: enableHA, androidAttachSurfaceAfterVideoParameters: false, hwdec: enableHA ? hwdec : null, ), ); player.setPlaylistMode(looping); final Map? filters; if (Platform.isAndroid) { String audioNormalization = ''; audioNormalization = AudioNormalization.getParamFromConfig( Pref.audioNormalization, ); if (volume != null && volume.isNotEmpty) { audioNormalization = audioNormalization.replaceFirstMapped( loudnormRegExp, (i) => 'loudnorm=${volume.format( Map.fromEntries( i.group(1)!.split(':').map((item) { final parts = item.split('='); return MapEntry(parts[0].toLowerCase(), num.parse(parts[1])); }), ), )}', ); } else { audioNormalization = audioNormalization.replaceFirst( loudnormRegExp, AudioNormalization.getParamFromConfig(Pref.fallbackNormalization), ); } filters = audioNormalization.isEmpty ? null : {'lavfi-complex': '"[aid1] $audioNormalization [ao]"'}; } else { filters = null; } if (kDebugMode) debugPrint(filters.toString()); if (dataSource.type == DataSourceType.asset) { final assetUrl = dataSource.videoSource!.startsWith("asset://") ? dataSource.videoSource! : "asset://${dataSource.videoSource!}"; await player.open( Media( assetUrl, httpHeaders: dataSource.httpHeaders, start: seekTo, extras: filters, ), play: false, ); } else { await player.open( Media( dataSource.videoSource!, httpHeaders: dataSource.httpHeaders, start: seekTo, extras: filters, ), play: false, ); } // 音轨 // player.setAudioTrack( // AudioTrack.uri(dataSource.audioSource!), // ); return player; } Future refreshPlayer() async { if (_videoPlayerController == null) { // SmartDialog.showToast('视频播放器为空,请重新进入本页面'); return false; } if (dataSource.videoSource.isNullOrEmpty) { SmartDialog.showToast('视频源为空,请重新进入本页面'); return false; } if (!isLive) { if (dataSource.audioSource.isNullOrEmpty) { SmartDialog.showToast('音频源为空'); } else { await (_videoPlayerController!.platform!).setProperty( 'audio-files', Platform.isWindows ? dataSource.audioSource!.replaceAll(';', '\\;') : dataSource.audioSource!.replaceAll(':', '\\:'), ); } } await _videoPlayerController!.open( Media( dataSource.videoSource!, httpHeaders: dataSource.httpHeaders, start: _position.value, ), play: true, ); return true; // seekTo(currentPos); } // 开始播放 Future _initializePlayer() async { if (_instance == null) return; // 设置倍速 if (isLive) { await setPlaybackSpeed(1.0); } else { if (_videoPlayerController?.state.rate != _playbackSpeed.value) { await setPlaybackSpeed(_playbackSpeed.value); } } getVideoFit(); // if (_looping) { // await setLooping(_looping); // } // 跳转播放 // if (seekTo != Duration.zero) { // await this.seekTo(seekTo); // } // 自动播放 if (_autoPlay) { playIfExists(); // await play(duration: duration); } } late final bool enableAutoEnter = Pref.enableAutoEnter; Future autoEnterFullscreen() async { if (enableAutoEnter) { Future.delayed(const Duration(milliseconds: 500), () { if (dataStatus.status.value != DataStatus.loaded) { _dataListenerForEnterFullscreen = dataStatus.status.listen((status) { if (status == DataStatus.loaded) { _dataListenerForEnterFullscreen.cancel(); triggerFullScreen(status: true); } }); } else { triggerFullScreen(status: true); } }); } } Set subscriptions = {}; final Set _positionListeners = {}; final Set _statusListeners = {}; /// 播放事件监听 void startListeners() { subscriptions = { videoPlayerController!.stream.playing.listen((event) { if (event) { if (_shouldSetPip) { if (_isCurrVideoPage) { enterPip(isAuto: true); } else { disableAutoEnterPip(); } } playerStatus.status.value = PlayerStatus.playing; } else { disableAutoEnterPip(); playerStatus.status.value = PlayerStatus.paused; } videoPlayerServiceHandler?.onStatusChange( playerStatus.status.value, isBuffering.value, isLive, ); /// 触发回调事件 for (var element in _statusListeners) { element(event ? PlayerStatus.playing : PlayerStatus.paused); } if (videoPlayerController!.state.position.inSeconds != 0) { makeHeartBeat(positionSeconds.value, type: HeartBeatType.status); } }), videoPlayerController!.stream.completed.listen((event) { if (event) { playerStatus.status.value = PlayerStatus.completed; /// 触发回调事件 for (var element in _statusListeners) { element(PlayerStatus.completed); } } else { // playerStatus.status.value = PlayerStatus.playing; } makeHeartBeat(positionSeconds.value, type: HeartBeatType.completed); }), videoPlayerController!.stream.position.listen((event) { _position.value = event; updatePositionSecond(); if (!isSliderMoving.value) { _sliderPosition.value = event; updateSliderPositionSecond(); } /// 触发回调事件 for (var element in _positionListeners) { element(event); } makeHeartBeat(event.inSeconds); }), videoPlayerController!.stream.duration.listen((Duration event) { duration.value = event; }), videoPlayerController!.stream.buffer.listen((Duration event) { _buffered.value = event; updateBufferedSecond(); }), videoPlayerController!.stream.buffering.listen((bool event) { isBuffering.value = event; videoPlayerServiceHandler?.onStatusChange( playerStatus.status.value, event, 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://") || event.startsWith("Can not open external file https://")) { Future.delayed(const Duration(milliseconds: 3000), refreshPlayer); } return; } if (event.startsWith("Failed to open https://") || event.startsWith("Can not open external file https://") || //tcp: ffurl_read returned 0xdfb9b0bb //tcp: ffurl_read returned 0xffffff99 event.startsWith('tcp: ffurl_read returned ')) { EasyThrottle.throttle( 'videoPlayerController!.stream.error.listen', const Duration(milliseconds: 10000), () { Future.delayed(const Duration(milliseconds: 3000), () async { // if (kDebugMode) { // debugPrint("isBuffering.value: ${isBuffering.value}"); // } // if (kDebugMode) { // debugPrint("_buffered.value: ${_buffered.value}"); // } if (isBuffering.value && _buffered.value == Duration.zero) { SmartDialog.showToast( '视频链接打开失败,重试中', displayTime: const Duration(milliseconds: 500), ); if (!await refreshPlayer()) { if (kDebugMode) debugPrint("failed"); } } }); }, ); } else if (event.startsWith('Could not open codec')) { SmartDialog.showToast('无法加载解码器, $event,可能会切换至软解'); } else if (!onlyPlayAudio.value) { if (event.startsWith("error running") || event.startsWith("Failed to open .") || event.startsWith("Cannot open") || event.startsWith("Can not open")) { return; } SmartDialog.showToast('视频加载错误, $event'); } }), // videoPlayerController!.stream.volume.listen((event) { // if (!mute.value && _volumeBeforeMute != event) { // _volumeBeforeMute = event / 100; // } // }), // 媒体通知监听 if (videoPlayerServiceHandler != null) ...[ onPlayerStatusChanged.listen((PlayerStatus event) { videoPlayerServiceHandler!.onStatusChange( event, isBuffering.value, isLive, ); }), onPositionChanged.listen((Duration event) { EasyThrottle.throttle( 'mediaServicePosition', const Duration(seconds: 1), () => videoPlayerServiceHandler!.onPositionChange(event), ); }), ], }; } /// 移除事件监听 Future removeListeners() async { await Future.wait(subscriptions.map((e) => e.cancel())); } /// 跳转至指定位置 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; } _position.value = position; updatePositionSecond(); _heartDuration = position.inSeconds; if (duration.value.inSeconds != 0) { if (isSeek) { /// 拖动进度条调节时,不等待第一帧,防止抖动 await _videoPlayerController?.stream.buffer.first; } danmakuController?.clear(); try { await _videoPlayerController?.seek(position); } catch (e) { if (kDebugMode) debugPrint('seek failed: $e'); } // if (playerStatus.stopped) { // play(); // } } 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.inSeconds != 0) { try { await _videoPlayerController?.stream.buffer.first; danmakuController?.clear(); await _videoPlayerController?.seek(position); } catch (e) { if (kDebugMode) debugPrint('seek failed: $e'); } // if (playerStatus.status.value == PlayerStatus.paused) { // play(); // } t.cancel(); _timerForSeek = null; } }); } } /// 设置倍速 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.status.value = PlayerStatus.playing; // screenManager.setOverlays(false); } /// 暂停播放 Future pause({bool notify = true, bool isInterrupt = false}) async { await _videoPlayerController?.pause(); playerStatus.status.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(double v) { _sliderPosition.value = Duration(seconds: v.floor()); 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 = value; updateSliderPositionSecond(); } void onChangedSliderEnd() { if (cancelSeek != true) { feedBack(); } cancelSeek = null; hasToast = null; _isSliderMoving.value = false; hideTaskControls(); } final RxBool volumeIndicator = false.obs; Timer? volumeTimer; final RxBool volumeInterceptEventStream = false.obs; Future setVolume(double volume) async { if (this.volume.value != volume) { this.volume.value = volume; try { if (Utils.isDesktop) { _videoPlayerController!.setVolume(volume * 100); } else { FlutterVolumeController.updateShowSystemUI(false); await FlutterVolumeController.setVolume(volume); } } catch (err) { if (kDebugMode) debugPrint(err.toString()); } } volumeIndicator.value = true; volumeInterceptEventStream.value = true; volumeTimer?.cancel(); volumeTimer = Timer(const Duration(milliseconds: 200), () { volumeIndicator.value = false; volumeInterceptEventStream.value = false; if (Utils.isDesktop) { setting.put(SettingBoxKey.desktopVolume, volume.toPrecision(3)); } }); } void volumeUpdated() { showVolumeStatus.value = true; _timerForShowingVolume?.cancel(); _timerForShowingVolume = Timer(const Duration(seconds: 1), () { showVolumeStatus.value = false; }); } void setCurrBrightness(double brightness) { _currentBrightness.value = brightness; } /// Toggle Change the videofit accordingly void toggleVideoFit(VideoFitType value) { _videoFit.value = value; video.put(VideoBoxKey.cacheVideoFit, _videoFit.value.index); } /// 读取fit int fitValue = Pref.cacheVideoFit; Future getVideoFit() async { var attr = VideoFitType.values[fitValue]; // 由于none与scaleDown涉及视频原始尺寸,需要等待视频加载后再设置,否则尺寸会变为0,出现错误; if (attr == VideoFitType.none || attr == VideoFitType.scaleDown) { if (buffered.value == Duration.zero) { attr = VideoFitType.contain; _dataListenerForVideoFit = dataStatus.status.listen((status) { if (status == DataStatus.loaded) { _dataListenerForVideoFit.cancel(); var attr = VideoFitType.values[fitValue]; if (attr == VideoFitType.none || attr == VideoFitType.scaleDown) { _videoFit.value = attr; } } }); } // fill不应该在竖屏视频生效 } else if (attr == VideoFitType.fill && isVertical) { attr = VideoFitType.contain; } _videoFit.value = attr; } /// 设置后台播放 Future setBackgroundPlay(bool val) async { videoPlayerServiceHandler?.enableBackgroundPlay = val; if (!tempPlayerConf) { setting.put(SettingBoxKey.enableBackgroundPlay, val); } } set controls(bool visible) { _showControls.value = visible; _timer?.cancel(); if (visible) { hideTaskControls(); } } void hiddenControls(bool val) { showControls.value = val; } 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.status.value == PlayerStatus.playing) { _longPressStatus.value = val; HapticFeedback.lightImpact(); await setPlaybackSpeed( enableAutoLongPressSpeed ? playbackSpeed * 2 : longPressSpeed, ); } } else { // if (kDebugMode) debugPrint('$playbackSpeed'); _longPressStatus.value = val; await setPlaybackSpeed(lastPlaybackSpeed); } } bool get _isCompleted => videoPlayerController!.state.completed || (duration.value - position.value).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.value + duration); } void onBackward(Duration duration) { onForwardBackward(_position.value - duration); } void onForwardBackward(Duration duration) { seekTo( duration.clamp(Duration.zero, videoPlayerController!.state.duration), isSeek: false, ).whenComplete(play); } void doubleTapFuc(DoubleTapType type) { if (!enableQuickDouble) { onDoubleTapCenter(); return; } switch (type) { case DoubleTapType.left: // 双击左边区域 👈 onDoubleTapSeekBackward(); break; case DoubleTapType.center: onDoubleTapCenter(); break; case DoubleTapType.right: // 双击右边区域 👈 onDoubleTapSeekForward(); break; } } /// 关闭控制栏 void onLockControl(bool val) { feedBack(); _controlsLock.value = val; if (!val && _showControls.value) { _showControls.refresh(); } controls = !val; } void toggleFullScreen(bool val) { _isFullScreen.value = val; updateSubtitleStyle(); } late bool isManualFS = true; late final FullScreenMode mode = FullScreenMode.values[Pref.fullScreenMode]; late final horizontalScreen = Pref.horizontalScreen; // 全屏 bool fsProcessing = false; Future triggerFullScreen({ bool status = true, bool inAppFullScreen = false, bool isManualFS = true, FullScreenMode? mode, }) async { if (isFullScreen.value == status) return; if (fsProcessing) { return; } fsProcessing = true; mode ??= this.mode; this.isManualFS = isManualFS; toggleFullScreen(status); if (status) { if (Utils.isMobile) { hideStatusBar(); if (mode == FullScreenMode.none) { fsProcessing = false; return; } if (mode == FullScreenMode.gravity) { await fullAutoModeForceSensor(); fsProcessing = false; return; } late final size = Get.mediaQuery.size; if ((mode == FullScreenMode.vertical || (mode == FullScreenMode.auto && isVertical) || (mode == FullScreenMode.ratio && (isVertical || size.height / size.width < kScreenRatio)))) { await verticalScreenForTwoSeconds(); } else { await landscape(); } } else { await enterDesktopFullscreen(inAppFullScreen: inAppFullScreen); } } else { if (Utils.isMobile) { showStatusBar(); if (mode == FullScreenMode.none) { fsProcessing = false; return; } if (!horizontalScreen) { await verticalScreenForTwoSeconds(); } else { await autoScreen(); } } else { await exitDesktopFullscreen(); } } fsProcessing = false; } void addPositionListener(Function(Duration position) listener) => _positionListeners.add(listener); void removePositionListener(Function(Duration position) listener) => _positionListeners.remove(listener); void addStatusLister(Function(PlayerStatus status) listener) => _statusListeners.add(listener); void removeStatusLister(Function(PlayerStatus status) listener) => _statusListeners.remove(listener); /// 截屏 Future screenshot() async { final Uint8List? screenshot = await _videoPlayerController!.screenshot( format: 'image/png', ); return screenshot; } // 记录播放记录 Future makeHeartBeat( int progress, { HeartBeatType type = HeartBeatType.playing, bool isManual = false, dynamic aid, dynamic bvid, dynamic cid, dynamic epid, dynamic seasonId, dynamic pgcType, VideoType? videoType, }) async { if (isLive) { return; } if (!enableHeart || MineController.anonymity.value || progress == 0) { return; } else if (playerStatus.status.value == PlayerStatus.paused) { if (!isManual) { return; } } bool isComplete = playerStatus.status.value == PlayerStatus.completed || type == HeartBeatType.completed; if ((durationSeconds.value - position.value).inMilliseconds > 1000) { isComplete = false; } // 播放状态变化时,更新 if (type == HeartBeatType.status || type == HeartBeatType.completed) { await VideoHttp.heartBeat( aid: aid ?? _aid, bvid: bvid ?? _bvid, cid: cid ?? this.cid, progress: isComplete ? -1 : progress, epid: epid ?? _epid, seasonId: seasonId ?? _seasonId, subType: pgcType ?? _pgcType, videoType: videoType ?? _videoType, ); return; } // 正常播放时,间隔5秒更新一次 else if (progress - _heartDuration >= 5) { _heartDuration = progress; await 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, ); } } void setPlayRepeat(PlayRepeat type) { playRepeat = type; video.put(VideoBoxKey.playRepeat, type.index); } void putDanmakuSettings() { setting.putAll({ SettingBoxKey.danmakuWeight: danmakuWeight, SettingBoxKey.danmakuBlockType: blockTypes.toList(), SettingBoxKey.danmakuShowArea: showArea, SettingBoxKey.danmakuOpacity: danmakuOpacity.value, SettingBoxKey.danmakuFontScale: danmakuFontScale, SettingBoxKey.danmakuFontScaleFS: danmakuFontScaleFS, SettingBoxKey.danmakuDuration: danmakuDuration, SettingBoxKey.danmakuStaticDuration: danmakuStaticDuration, SettingBoxKey.strokeWidth: danmakuStrokeWidth, SettingBoxKey.fontWeight: danmakuFontWeight, SettingBoxKey.danmakuLineHeight: danmakuLineHeight, }); } void putSubtitleSettings() { setting.putAll({ SettingBoxKey.subtitleFontScale: subtitleFontScale, SettingBoxKey.subtitleFontScaleFS: subtitleFontScaleFS, SettingBoxKey.subtitlePaddingH: subtitlePaddingH, SettingBoxKey.subtitlePaddingB: subtitlePaddingB, SettingBoxKey.subtitleBgOpaticy: subtitleBgOpaticy, SettingBoxKey.subtitleStrokeWidth: subtitleStrokeWidth, SettingBoxKey.subtitleFontWeight: subtitleFontWeight, }); } bool isCloseAll = false; Future dispose() async { // 每次减1,最后销毁 cancelLongPressTimer(); if (!isCloseAll && _playerCount > 1) { _playerCount -= 1; _heartDuration = 0; if (!_isPreviousVideoPage) { pause(); } return; } _playerCount = 0; disableAutoEnterPip(); setPlayCallBack(null); dmState.clear(); _clearPreview(); Utils.channel.setMethodCallHandler(null); _timer?.cancel(); _timerForSeek?.cancel(); _timerForShowingVolume?.cancel(); // _position.close(); _playerEventSubs?.cancel(); // _sliderPosition.close(); // _sliderTempPosition.close(); // _isSliderMoving.close(); // _duration.close(); // _buffered.close(); // _showControls.close(); // _controlsLock.close(); // playerStatus.status.close(); // dataStatus.status.close(); await removeListeners(); _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; if (!tempPlayerConf) { setting.put( SettingBoxKey.continuePlayInBackground, _continuePlayInBackground.value, ); } } void setOnlyPlayAudio() { onlyPlayAudio.value = !onlyPlayAudio.value; videoPlayerController?.setVideoTrack( onlyPlayAudio.value ? VideoTrack.no() : VideoTrack.auto(), ); } Map>? previewCache; LoadingState? videoShot; late final RxBool showPreview = false.obs; late final showSeekPreview = Pref.showSeekPreview; late final Rx previewIndex = Rx(null); void updatePreviewIndex(int seconds) { if (videoShot == null) { videoShot = LoadingState.loading(); getVideoShot(); return; } if (videoShot case Success success) { final data = success.response; if (!showPreview.value) { showPreview.value = true; } previewIndex.value = max( 0, (data.index.where((item) => item <= seconds).length - 2), ); } } void _clearPreview() { showPreview.value = false; previewIndex.value = null; videoShot = null; previewCache ?..forEach((_, ref) { try { ref.target?.dispose(); } catch (_) {} }) ..clear(); previewCache = null; } Future getVideoShot() async { try { var res = await Request().get( '/x/player/videoshot', queryParameters: { // 'aid': IdUtils.bv2av(_bvid), 'bvid': _bvid, 'cid': cid, 'index': 1, }, options: Options( headers: { 'user-agent': UaType.pc.ua, 'referer': 'https://www.bilibili.com/video/$bvid', }, ), ); if (res.data['code'] == 0) { final data = VideoShotData.fromJson(res.data['data']); if (data.index.isNotEmpty) { videoShot = Success(data); return; } } videoShot = const Error(null); } catch (e) { videoShot = const Error(null); if (kDebugMode) debugPrint('getVideoShot: $e'); } } void takeScreenshot() { SmartDialog.showToast('截图中'); videoPlayerController?.screenshot(format: 'image/png').then((value) { if (value != null) { SmartDialog.showToast('点击弹窗保存截图'); Get.dialog( 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(Get.width / 3, 350), ), child: DecoratedBox( decoration: BoxDecoration( border: Border.all( width: 5, color: Get.theme.colorScheme.surface, ), ), child: Padding( padding: const EdgeInsets.all(5), child: Image.memory(value), ), ), ), ), ), ), ); } else { SmartDialog.showToast('截图失败'); } }); } }