From a623ceee47b11639564b77495bc0d64f05a1c69f Mon Sep 17 00:00:00 2001 From: dom Date: Fri, 26 Jun 2026 12:45:41 +0800 Subject: [PATCH] refactor progress bar Signed-off-by: dom --- .../audio_video_progress_bar.dart | 90 ++++--- lib/pages/audio/controller.dart | 37 ++- lib/pages/audio/view.dart | 18 +- lib/pages/danmaku/view.dart | 2 +- lib/pages/video/controller.dart | 10 +- lib/pages/video/introduction/ugc/view.dart | 4 +- lib/pages/video/post_panel/view.dart | 5 +- lib/pages/video/view_point/view.dart | 2 +- lib/plugin/pl_player/controller.dart | 245 ++++++------------ lib/plugin/pl_player/view/view.dart | 203 ++++++--------- .../pl_player/widgets/bottom_control.dart | 36 ++- lib/services/shutdown_timer_service.dart | 2 +- 12 files changed, 263 insertions(+), 391 deletions(-) diff --git a/lib/common/widgets/progress_bar/audio_video_progress_bar.dart b/lib/common/widgets/progress_bar/audio_video_progress_bar.dart index e3bf9cd19..52a766b30 100644 --- a/lib/common/widgets/progress_bar/audio_video_progress_bar.dart +++ b/lib/common/widgets/progress_bar/audio_video_progress_bar.dart @@ -29,7 +29,7 @@ class ProgressBar extends LeafRenderObjectWidget { super.key, required this.progress, required this.total, - this.buffered = .zero, + this.buffered = 0, this.onSeek, this.onDragStart, this.onDragUpdate, @@ -48,16 +48,19 @@ class ProgressBar extends LeafRenderObjectWidget { /// The elapsed playing time of the media. /// /// This should not be greater than the [total] time. - final Duration progress; + /// seconds + final int progress; /// The total duration of the media. - final Duration total; + /// seconds + final int total; /// The currently buffered content of the media. /// /// This is useful for streamed content. If you are playing a local file /// then you can leave this out. - final Duration buffered; + /// seconds + final int buffered; /// A callback when user moves the thumb. /// @@ -70,7 +73,7 @@ class ProgressBar extends LeafRenderObjectWidget { /// If you want continuous duration updates as the user moves the thumb, /// see [onDragUpdate], where the provided [ThumbDragDetails] has a /// `timeStamp` with the seek duration on it. - final ValueChanged? onSeek; + final OnSeek? onSeek; /// A callback when the user starts to move the thumb. /// @@ -224,7 +227,7 @@ class ProgressBar extends LeafRenderObjectWidget { ..add(StringProperty('total', total.toString())) ..add(StringProperty('buffered', buffered.toString())) ..add( - ObjectFlagProperty>( + ObjectFlagProperty( 'onSeek', onSeek, ifNull: 'unimplemented', @@ -271,6 +274,8 @@ class ProgressBar extends LeafRenderObjectWidget { } } +typedef OnSeek = void Function(int milliseconds); + /// The callback signature for when the thumb begins a horizontal drag. typedef ThumbDragStartCallback = void Function(ThumbDragDetails details); @@ -281,13 +286,13 @@ typedef ThumbDragUpdateCallback = void Function(ThumbDragDetails details); /// Data to pass back on drag callback events class ThumbDragDetails { const ThumbDragDetails({ - this.timeStamp = Duration.zero, + this.seconds = 0, this.globalPosition = Offset.zero, this.localPosition = Offset.zero, }); /// The duration position of the thumb on the progress bar - final Duration timeStamp; + final int seconds; /// The global position of the drag event moving the thumb on the progress bar. final Offset globalPosition; @@ -298,7 +303,7 @@ class ThumbDragDetails { @override String toString() => '${objectRuntimeType(this, 'ThumbDragDetails')}(' - 'time: $timeStamp, ' + 'time: $seconds, ' 'global: $globalPosition, ' 'local: $localPosition)'; } @@ -320,7 +325,7 @@ class _EagerHorizontalDragGestureRecognizer class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { RenderProgressBar({ - required Duration progress, + required this._progress, required this._total, required this._buffered, this._onSeek, @@ -351,7 +356,6 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { ..onCancel = _finishDrag; } if (!_userIsDraggingThumb) { - _progress = progress; _thumbValue = _proportionOfTotal(_progress); } } @@ -377,14 +381,11 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { bool _userIsDraggingThumb = false; void _onDragStart(DragStartDetails details) { - if (onDragStart == null) { - return; - } _userIsDraggingThumb = true; _updateThumbPosition(details.localPosition); onDragStart?.call( ThumbDragDetails( - timeStamp: _currentThumbDuration(), + seconds: _currentThumbDuration(), globalPosition: details.globalPosition, localPosition: details.localPosition, ), @@ -392,13 +393,10 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { } void _onDragUpdate(DragUpdateDetails details) { - if (onDragUpdate == null) { - return; - } _updateThumbPosition(details.localPosition); onDragUpdate?.call( ThumbDragDetails( - timeStamp: _currentThumbDuration(), + seconds: _currentThumbDuration(), globalPosition: details.globalPosition, localPosition: details.localPosition, ), @@ -406,11 +404,8 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { } void _onDragEnd(DragEndDetails details) { - if (onSeek == null) { - return; - } onDragEnd?.call(); - onSeek?.call(_currentThumbDuration()); + onSeek?.call(_currentThumbDurationInMilliseconds()); _finishDrag(); } @@ -419,9 +414,12 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { markNeedsPaint(); } - Duration _currentThumbDuration() { - final thumbMilliseconds = _thumbValue * total.inMilliseconds; - return Duration(milliseconds: thumbMilliseconds.round()); + int _currentThumbDuration() { + return (_thumbValue * total).round(); + } + + int _currentThumbDurationInMilliseconds() { + return (_thumbValue * total * 1000).round(); } // This needs to stay in sync with the layout. This could be a potential @@ -438,7 +436,7 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { double barEnd = size.width - barCapRadius; final barWidth = barEnd - barStart; final position = (dx - barStart).clamp(0.0, barWidth); - _thumbValue = (position / barWidth); + _thumbValue = position / barWidth; _progress = _currentThumbDuration(); markNeedsPaint(); } @@ -446,9 +444,9 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { /// The play location of the media. /// /// This is used to update the thumb value and the left time label. - Duration get progress => _progress; - Duration _progress = Duration.zero; - set progress(Duration value) { + int get progress => _progress; + int _progress; + set progress(int value) { final clamp = _clampDuration(value); if (_progress == clamp) { return; @@ -461,10 +459,10 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { } /// The total time length of the media. - Duration get total => _total; - Duration _total; - set total(Duration value) { - final clamp = (value.isNegative) ? Duration.zero : value; + int get total => _total; + int _total; + set total(int value) { + final clamp = (value.isNegative) ? 0 : value; if (_total == clamp) { return; } @@ -476,9 +474,9 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { } /// The buffered length of the media when streaming. - Duration get buffered => _buffered; - Duration _buffered; - set buffered(Duration value) { + int get buffered => _buffered; + int _buffered; + set buffered(int value) { final clamp = _clampDuration(value); if (_buffered == clamp) { return; @@ -487,16 +485,16 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { markNeedsPaint(); } - Duration _clampDuration(Duration value) { - if (value.isNegative) return Duration.zero; + int _clampDuration(int value) { + if (value.isNegative) return 0; if (value.compareTo(_total) > 0) return _total; return value; } /// A callback for the audio duration position to where the thumb was moved. - ValueChanged? get onSeek => _onSeek; - ValueChanged? _onSeek; - set onSeek(ValueChanged? value) { + OnSeek? get onSeek => _onSeek; + OnSeek? _onSeek; + set onSeek(OnSeek? value) { if (value == _onSeek) { return; } @@ -716,7 +714,7 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { _drawBar( canvas: canvas, availableSize: localSize, - widthProportion: _proportionOfTotal(_progress), + widthProportion: _thumbValue, color: progressBarColor, ); } @@ -756,11 +754,11 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { canvas.drawCircle(center, thumbRadius, thumbPaint); } - double _proportionOfTotal(Duration duration) { - if (total.inMilliseconds == 0) { + double _proportionOfTotal(int duration) { + if (total == 0) { return 0.0; } - return (duration.inMilliseconds / total.inMilliseconds).clamp(0.0, 1.0); + return (duration / total).clamp(0.0, 1.0); } @override diff --git a/lib/pages/audio/controller.dart b/lib/pages/audio/controller.dart index 148fb2d42..a03fbd5ed 100644 --- a/lib/pages/audio/controller.dart +++ b/lib/pages/audio/controller.dart @@ -74,8 +74,8 @@ class AudioController extends GetxController late int cacheAudioQa; late bool isDragging = false; - final Rx position = Duration.zero.obs; - final Rx duration = Duration.zero.obs; + final RxInt position = RxInt(0); + final RxInt duration = RxInt(0); late final AnimationController animController; @@ -291,7 +291,7 @@ class AudioController extends GetxController if (audios.isEmpty) { return; } - position.value = Duration.zero; + position.value = 0; final audio = audios.findClosestTarget( (e) => e.id <= cacheAudioQa, (a, b) => a.id > b.id ? a : b, @@ -304,7 +304,7 @@ class AudioController extends GetxController return; } final durl = durls.first; - position.value = Duration.zero; + position.value = 0; _onOpenMedia(VideoUtils.getCdnUrl(durl.playUrls)); } } @@ -350,13 +350,16 @@ class AudioController extends GetxController _subscriptions = [ stream.position.listen((position) { if (isDragging) return; - if (position.inSeconds != this.position.value.inSeconds) { - this.position.value = position; + final seconds = position.inSeconds; + if (seconds != this.position.value) { + this.position.value = seconds; _videoDetailController?.playedTime = position; videoPlayerServiceHandler?.onPositionChange(position); } }), - stream.duration.listen(duration.call), + stream.duration.listen((duration) { + this.duration.value = duration.inSeconds; + }), stream.playing.listen((playing) { final PlayerStatus playerStatus; if (playing) { @@ -369,7 +372,7 @@ class AudioController extends GetxController videoPlayerServiceHandler?.onStatusChange(playerStatus, false, false); }), stream.completed.listen((completed) { - _videoDetailController?.playedTime = duration.value; + _videoDetailController?.playedTime = player!.state.duration; videoPlayerServiceHandler?.onStatusChange( PlayerStatus.completed, false, @@ -626,14 +629,8 @@ class AudioController extends GetxController ); } - void playOrPause() { - if (player case final player?) { - if ((duration.value - position.value).inMilliseconds < 50) { - player.seek(Duration.zero).whenComplete(player.play); - } else { - player.playOrPause(); - } - } + Future? playOrPause() { + return player?.playOrPause(); } bool playPrev() { @@ -747,15 +744,15 @@ class AudioController extends GetxController BlockConfigMixin get blockConfig => this; @override - int get currPosInMilliseconds => position.value.inMilliseconds; + int get currPosInMilliseconds => player?.state.position.inMilliseconds ?? 0; + + @override + int? get timeLength => player?.state.duration.inMilliseconds ?? 0; @override Future? seekTo(Duration duration, {required bool isSeek}) => onSeek(duration); - @override - int? get timeLength => duration.value.inMilliseconds; - @override bool get autoPlay => true; diff --git a/lib/pages/audio/view.dart b/lib/pages/audio/view.dart index 20119a600..87981a6e8 100644 --- a/lib/pages/audio/view.dart +++ b/lib/pages/audio/view.dart @@ -755,19 +755,19 @@ class _AudioPageState extends State { } void _onDragStart(ThumbDragDetails details) { - // do nothing + _controller + ..isDragging = true + ..position.value = details.seconds; } void _onDragUpdate(ThumbDragDetails details) { - _controller - ..isDragging = true - ..position.value = details.timeStamp; + _controller.position.value = details.seconds; } - void _onSeek(Duration value) { + void _onSeek(int milliseconds) { _controller - ..player?.seek(value) - ..isDragging = false; + ..isDragging = false + ..player?.seek(Duration(milliseconds: milliseconds)); } Widget _buildProgressBar(ColorScheme colorScheme) { @@ -839,7 +839,7 @@ class _AudioPageState extends State { final position = _controller.position.value; if (_controller.player != null) { return Text( - DurationUtils.formatDuration(position.inSeconds), + DurationUtils.formatDuration(position), ); } return const SizedBox.shrink(); @@ -848,7 +848,7 @@ class _AudioPageState extends State { final duration = _controller.duration.value; if (_controller.player != null) { return Text( - DurationUtils.formatDuration(duration.inSeconds), + DurationUtils.formatDuration(duration), ); } return const SizedBox.shrink(); diff --git a/lib/pages/danmaku/view.dart b/lib/pages/danmaku/view.dart index 233080bee..222598aec 100644 --- a/lib/pages/danmaku/view.dart +++ b/lib/pages/danmaku/view.dart @@ -57,7 +57,7 @@ class _PlDanmakuState extends State { } else { _plDanmakuController.queryDanmaku( PlDanmakuController.calcSegment( - playerController.position.inMilliseconds, + playerController.positionInMilliseconds, ), ); } diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index 84065e0bb..ba171bbd1 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -565,7 +565,7 @@ class VideoDetailController extends GetxController bool get preInitPlayer => plPlayerController.preInitPlayer; @override int get currPosInMilliseconds => - defaultST?.inMilliseconds ?? plPlayerController.position.inMilliseconds; + defaultST?.inMilliseconds ?? plPlayerController.positionInMilliseconds; @override Future seekTo(Duration duration, {required bool isSeek}) => plPlayerController.seekTo(duration, isSeek: isSeek); @@ -646,7 +646,7 @@ class VideoDetailController extends GetxController final child = SendDanmakuPanel( cid: cid.value, bvid: bvid, - progress: plPlayerController.position.inMilliseconds, + progress: plPlayerController.positionInMilliseconds, initialValue: savedDanmaku, onSave: (danmaku) => savedDanmaku = danmaku, onSuccess: (danmakuModel) { @@ -707,10 +707,10 @@ class VideoDetailController extends GetxController final currentVideoQa = this.currentVideoQa.value; if (currentVideoQa == null) return; _autoPlay.value = true; - playedTime = plPlayerController.position; + playedTime = plPlayerController.videoPlayerController?.state.position; plPlayerController ..isBuffering.value = false - ..buffered.value = Duration.zero; + ..buffered.value = 0; firstVideo = findVideoByQa(currentVideoQa.code, setCodecs: true); videoUrl = VideoUtils.getCdnUrl(firstVideo.playUrls); @@ -1003,7 +1003,7 @@ class VideoDetailController extends GetxController PostSegmentModel( segment: Pair( first: 0, - second: plPlayerController.position.inMilliseconds / 1000, + second: plPlayerController.positionInMilliseconds / 1000, ), category: SegmentType.sponsor, actionType: ActionType.skip, diff --git a/lib/pages/video/introduction/ugc/view.dart b/lib/pages/video/introduction/ugc/view.dart index 32c642bf0..8e4dd7f3e 100644 --- a/lib/pages/video/introduction/ugc/view.dart +++ b/lib/pages/video/introduction/ugc/view.dart @@ -650,9 +650,7 @@ class _UgcIntroPanelState extends State { videoDetailCtr.data.timeLength ?? videoDetailCtr .plPlayerController - .duration - .value - .inMilliseconds; + .durationInMilliseconds; if (duration > 0) { final ytbId = youtubeRegExp .firstMatch(matchStr) diff --git a/lib/pages/video/post_panel/view.dart b/lib/pages/video/post_panel/view.dart index 52f33a528..71417dcf0 100644 --- a/lib/pages/video/post_panel/view.dart +++ b/lib/pages/video/post_panel/view.dart @@ -183,10 +183,9 @@ class _PostPanelState extends State late final PlPlayerController plPlayerController = widget.plPlayerController; late final List list = videoDetailController.postList; - double get videoDuration => - plPlayerController.duration.value.inMilliseconds / 1000; + double get videoDuration => plPlayerController.durationInMilliseconds / 1000; - double currentPos() => plPlayerController.position.inMilliseconds / 1000; + double currentPos() => plPlayerController.positionInMilliseconds / 1000; @override Widget buildPage(ThemeData theme) { diff --git a/lib/pages/video/view_point/view.dart b/lib/pages/video/view_point/view.dart index 294d16703..58dec4a41 100644 --- a/lib/pages/video/view_point/view.dart +++ b/lib/pages/video/view_point/view.dart @@ -103,7 +103,7 @@ class _ViewPointsPageState extends State final segment = videoDetailController.viewPointList[index]; if (currentIndex == -1 && segment.from != null && segment.to != null) { final positionSeconds = - videoDetailController.plPlayerController.positionSeconds.value; + videoDetailController.plPlayerController.position.value; if (positionSeconds >= segment.from! && positionSeconds < segment.to!) { currentIndex = index; diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 990fafc78..81f5303f6 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -72,38 +72,31 @@ class PlPlayerController with BlockConfigMixin { Player? _videoPlayerController; VideoController? _videoController; - // 添加一个私有静态变量来保存实例 static PlPlayerController? _instance; - // 流事件 监听播放状态变化 - // StreamSubscription? _playerEventSubs; + final playerStatus = PlPlayerStatus(.playing); - /// [playerStatus] has a [status] observable - final playerStatus = PlPlayerStatus(PlayerStatus.playing); + final Rx dataStatus = Rx(.none); - /// - final Rx dataStatus = Rx(DataStatus.none); + Duration? seekToPos; + bool hasToasted = false; + final RxBool isSeeking = false.obs; - // bool controlsEnabled = false; + final RxInt position = RxInt(0); - /// 响应数据 - /// 带有Seconds的变量只在秒数更新时更新,以避免频繁触发重绘 - // 播放位置 - Duration position = Duration.zero; - final RxInt positionSeconds = 0.obs; + int get positionInMilliseconds => + videoPlayerController?.state.position.inMilliseconds ?? 0; - /// 进度条位置 - Duration sliderPosition = Duration.zero; - final RxInt sliderPositionSeconds = 0.obs; - // 展示使用 - final Rx sliderTempPosition = Rx(Duration.zero); + final RxInt buffered = RxInt(0); - /// 视频时长 - final Rx duration = Rx(Duration.zero); + final RxInt duration = RxInt(0); - /// 视频缓冲 - final Rx buffered = Rx(Duration.zero); - final RxInt bufferedSeconds = 0.obs; + int durationInMilliseconds = 0; + + void updateDuration(Duration value) { + duration.value = value.inSeconds; + durationInMilliseconds = value.inMilliseconds; + } int _playerCount = 0; @@ -111,44 +104,31 @@ class PlPlayerController with BlockConfigMixin { final RxDouble _playbackSpeed = Pref.playSpeedDefault.obs; late final RxDouble _longPressSpeed = Pref.longPressSpeedDefault.obs; - /// 音量控制条 final RxDouble volume = RxDouble( PlatformUtils.isDesktop ? Pref.desktopVolume : 1.0, ); final setSystemBrightness = Pref.setSystemBrightness; - /// 亮度控制条 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); + final Rx videoFit = Rx(.contain); - /// 后台播放 late final RxBool continuePlayInBackground = Pref.continuePlayInBackground.obs; - /// - final RxBool isSliderMoving = false.obs; - bool _autoPlay = false; // 记录历史记录 @@ -168,7 +148,7 @@ class PlPlayerController with BlockConfigMixin { late DataSource dataSource; Timer? _timer; - StreamSubscription? _subForSeek; + StreamSubscription? _subForSeek; Box setting = GStorage.setting; @@ -387,8 +367,7 @@ class PlPlayerController with BlockConfigMixin { ? Pref.sliderDuration / 100 : Pref.sliderDuration * 1000; - num get sliderScale => - isRelative ? duration.value.inMilliseconds * offset : offset; + num get sliderScale => isRelative ? durationInMilliseconds * offset : offset; // 播放顺序相关 late PlayRepeat playRepeat = Pref.playRepeat; @@ -441,27 +420,6 @@ class PlPlayerController with BlockConfigMixin { 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() { @@ -475,7 +433,6 @@ class PlPlayerController with BlockConfigMixin { static PlayCallback? _playCallBack; static Future? playIfExists() { - // await _instance?.play(repeat: repeat, hideControls: hideControls); return _playCallBack?.call(); } @@ -688,14 +645,10 @@ class PlPlayerController with BlockConfigMixin { return; } - // 获取视频时长 00:00 - this.duration.value = duration ?? _videoPlayerController!.state.duration; - position = buffered.value = sliderPosition = seekTo ?? Duration.zero; - updatePositionSecond(); - updateSliderPositionSecond(); - updateBufferedSecond(); - // 数据加载完成 - dataStatus.value = DataStatus.loaded; + updateDuration(duration ?? _videoPlayerController!.state.duration); + position.value = buffered.value = seekTo?.inSeconds ?? 0; + + dataStatus.value = .loaded; if (autoFullScreenFlag && autoEnterFullScreen) { triggerFullScreen(status: true); @@ -821,10 +774,7 @@ class PlPlayerController with BlockConfigMixin { Volume? volume, ) async { isBuffering.value = false; - buffered.value = Duration.zero; _heartDuration = 0; - position = Duration.zero; - // 初始化时清空弹幕,防止上次重叠 danmakuController?.clear(); var player = _videoPlayerController; @@ -906,7 +856,10 @@ class PlPlayerController with BlockConfigMixin { return null; } if (_videoPlayerController case final ctr? when (ctr.current.isNotEmpty)) { - return ctr.open(ctr.current.last.copyWith(start: position), play: true); + return ctr.open( + ctr.current.last.copyWith(start: ctr.state.position), + play: true, + ); } return null; } @@ -948,9 +901,10 @@ class PlPlayerController with BlockConfigMixin { assert(_subscriptions == null); final stream = player.stream; _subscriptions = [ - stream.playing.listen((event) { - WakelockPlus.toggle(enable: event); - if (event) { + /// playing + stream.playing.listen((bool playing) { + WakelockPlus.toggle(enable: playing); + if (playing) { if (_isAutoEnterPip) { if (_isCurrVideoPage) { enterPip(autoEnter: true); @@ -958,64 +912,68 @@ class PlPlayerController with BlockConfigMixin { _disableAutoEnterPip(); } } - playerStatus.value = PlayerStatus.playing; + playerStatus.value = .playing; } else { _disableAutoEnterPip(); - playerStatus.value = PlayerStatus.paused; + playerStatus.value = .paused; } + videoPlayerServiceHandler?.onStatusChange( playerStatus.value, isBuffering.value, isLive, ); - /// 触发回调事件 for (final element in _statusListeners) { - element(event ? PlayerStatus.playing : PlayerStatus.paused); + element(playing ? .playing : .paused); } - if (videoPlayerController!.state.position.inSeconds != 0) { - makeHeartBeat(positionSeconds.value, type: HeartBeatType.status); + + final seconds = videoPlayerController!.state.position.inSeconds; + if (seconds != 0) { + makeHeartBeat(seconds, type: .status); } }), - stream.completed.listen((event) { - if (event) { - playerStatus.value = PlayerStatus.completed; - /// 触发回调事件 + ///completed + stream.completed.listen((bool completed) { + if (completed) { + playerStatus.value = .completed; + for (final element in _statusListeners) { - element(PlayerStatus.completed); + element(.completed); } - } else { - // playerStatus.value = PlayerStatus.playing; + + makeHeartBeat(-1, type: .completed); } - makeHeartBeat(positionSeconds.value, type: HeartBeatType.completed); }), - stream.position.listen((event) { - position = event; - updatePositionSecond(); - if (!isSliderMoving.value) { - sliderPosition = event; - updateSliderPositionSecond(); + + /// position + stream.position.listen((Duration position) { + final posInSeconds = position.inSeconds; + + if (posInSeconds != this.position.value) { + if (!isSeeking.value) { + this.position.value = posInSeconds; + } + + videoPlayerServiceHandler?.onPositionChange(position); } - /// 触发回调事件 for (final element in _positionListeners) { - element(event); + element(position); } - makeHeartBeat(event.inSeconds); + + makeHeartBeat(posInSeconds); }), - stream.duration.listen((Duration event) { - duration.value = event; + stream.duration.listen(updateDuration), + stream.buffer.listen((Duration buffer) { + buffered.value = buffer.inSeconds; }), - stream.buffer.listen((Duration event) { - buffered.value = event; - updateBufferedSecond(); - }), - stream.buffering.listen((bool event) { - isBuffering.value = event; + stream.buffering.listen((bool buffering) { + isBuffering.value = buffering; videoPlayerServiceHandler?.onStatusChange( playerStatus.value, - event, + buffering, isLive, ); }), @@ -1056,7 +1014,7 @@ class PlPlayerController with BlockConfigMixin { // if (kDebugMode) { // debugPrint("_buffered.value: ${_buffered.value}"); // } - if (isBuffering.value && buffered.value == Duration.zero) { + if (isBuffering.value && buffered.value == 0) { SmartDialog.showToast( '视频链接打开失败,重试中', displayTime: const Duration(milliseconds: 500), @@ -1079,16 +1037,6 @@ class PlPlayerController with BlockConfigMixin { // SmartDialog.showToast('视频加载错误, $event'); } }), - // controllerStream.volume.listen((event) { - // if (!mute.value && _volumeBeforeMute != event) { - // _volumeBeforeMute = event / 100; - // } - // }), - // 媒体通知监听 - if (videoPlayerServiceHandler != null) - positionSeconds.listen((int event) { - videoPlayerServiceHandler!.onPositionChange(Duration(seconds: event)); - }), ]; } @@ -1108,17 +1056,12 @@ class PlPlayerController with BlockConfigMixin { /// 跳转至指定位置 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 { @@ -1134,7 +1077,7 @@ class PlPlayerController with BlockConfigMixin { } } - if (duration.value != Duration.zero) { + if (duration.value != 0) { seek(); } else { // if (kDebugMode) debugPrint('seek duration else'); @@ -1214,42 +1157,22 @@ class PlPlayerController with BlockConfigMixin { void hideTaskControls() { _timer?.cancel(); _timer = Timer(showControlDuration, () { - if (!isSliderMoving.value && !tripling) { + if (!isSeeking.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() { - if (cancelSeek != true) { + void onSeekEnd() { + if (seekToPos != null) { feedBack(); } - cancelSeek = null; - hasToast = null; - isSliderMoving.value = false; + if (showSeekPreview) { + showPreview.value = false; + } + hasToasted = false; + isSeeking.value = false; hideTaskControls(); } @@ -1350,13 +1273,9 @@ class PlPlayerController with BlockConfigMixin { } } - bool get _isCompleted => - videoPlayerController!.state.completed || - (duration.value - position).inMilliseconds <= 50; - // 双击播放、暂停 Future onDoubleTapCenter() async { - if (!isLive && _isCompleted) { + if (!isLive && videoPlayerController!.state.completed) { await videoPlayerController!.seek(Duration.zero); videoPlayerController!.play(); } else { @@ -1376,11 +1295,11 @@ class PlPlayerController with BlockConfigMixin { } void onForward(Duration duration) { - onForwardBackward(position + duration); + onForwardBackward(videoPlayerController!.state.position + duration); } void onBackward(Duration duration) { - onForwardBackward(position - duration); + onForwardBackward(videoPlayerController!.state.position - duration); } void onForwardBackward(Duration duration) { @@ -1568,7 +1487,7 @@ class PlPlayerController with BlockConfigMixin { } case .completed: if (playerStatus.isCompleted && - (duration.value - position).inMilliseconds <= 1000) { + (durationInMilliseconds - positionInMilliseconds) <= 1000) { progress = -1; } return send(); @@ -1734,7 +1653,7 @@ class PlPlayerController with BlockConfigMixin { Future takeScreenshot() async { SmartDialog.showToast('截图中'); final time = DurationUtils.formatDuration( - position.inMilliseconds / 1000, + positionInMilliseconds / 1000, ).replaceAll(':', '-'); final image = await videoPlayerController?.screenshot(); if (image != null) { diff --git a/lib/plugin/pl_player/view/view.dart b/lib/plugin/pl_player/view/view.dart index 88a17ab68..d472358e5 100644 --- a/lib/plugin/pl_player/view/view.dart +++ b/lib/plugin/pl_player/view/view.dart @@ -447,10 +447,10 @@ class _PLVideoPlayerState extends State BottomControlType.time => Obx( () => _VideoTime( position: DurationUtils.formatDuration( - plPlayerController.positionSeconds.value, + plPlayerController.position.value, ), duration: DurationUtils.formatDuration( - plPlayerController.duration.value.inSeconds, + plPlayerController.duration.value, ), ), ), @@ -961,6 +961,42 @@ class _PLVideoPlayerState extends State showRestoreScaleBtn.value = scale != 1.0; } + void _onHorizontalDragStart() { + plPlayerController.isSeeking.value = true; + } + + void _onHorizontalDragUpdate(double dx) { + final curPos = + plPlayerController.seekToPos?.inMilliseconds ?? + plPlayerController.position.value * 1000; + final posDelta = (plPlayerController.sliderScale * dx / maxWidth).round(); + final newPos = (curPos + posDelta).clamp( + 0, + plPlayerController.durationInMilliseconds, + ); + final seconds = newPos ~/ 1000; + plPlayerController + ..seekToPos = Duration(milliseconds: newPos) + ..position.value = seconds; + if (!plPlayerController.isFileSource && + plPlayerController.showSeekPreview) { + plPlayerController.updatePreviewIndex(seconds); + } + } + + void _onHorizontalDragEnd() { + plPlayerController.onSeekEnd(); + if (plPlayerController.seekToPos case final seekToPos?) { + plPlayerController + ..seekTo(seekToPos, isSeek: false) + ..seekToPos = null; + } else { + plPlayerController.position.value = + plPlayerController.videoPlayerController?.state.position.inSeconds ?? + 0; + } + } + void _onPanUpdate(ScaleUpdateDetails details) { if (_gestureType == null) { final cumulativeDelta = details.localFocalPoint - _initialFocalPoint!; @@ -968,6 +1004,7 @@ class _PLVideoPlayerState extends State final dx = cumulativeDelta.dx.abs(); final dy = cumulativeDelta.dy.abs(); if (dx > 3 * dy) { + _onHorizontalDragStart(); _gestureType = .horizontal; } else if (dy > 3 * dx) { if (!plPlayerController.enableSlideVolumeBrightness && @@ -1010,22 +1047,17 @@ class _PLVideoPlayerState extends State // live模式下禁用 if (plPlayerController.isLive) return; - final int curSliderPosition = - plPlayerController.sliderPosition.inMilliseconds; - final int newPos = - (curSliderPosition + - (plPlayerController.sliderScale * delta.dx / maxWidth) - .round()) - .clamp(0, plPlayerController.duration.value.inMilliseconds); - final Duration result = Duration(milliseconds: newPos); final height = maxHeight * 0.125; if (details.localFocalPoint.dy <= height && (details.localFocalPoint.dx >= maxWidth * 0.875 || details.localFocalPoint.dx <= maxWidth * 0.125)) { - plPlayerController.cancelSeek = true; - plPlayerController.showPreview.value = false; - if (plPlayerController.hasToast != true) { - plPlayerController.hasToast = true; + if (!plPlayerController.hasToasted) { + plPlayerController + ..seekToPos = null + ..hasToasted = true; + if (plPlayerController.showSeekPreview) { + plPlayerController.showPreview.value = false; + } SmartDialog.showAttach( targetContext: context, alignment: Alignment.center, @@ -1046,21 +1078,12 @@ class _PLVideoPlayerState extends State ), ); } - } else { - if (plPlayerController.cancelSeek == true) { - plPlayerController - ..cancelSeek = null - ..hasToast = null; - } - } - plPlayerController - ..onUpdatedSliderProgress(result) - ..onChangedSliderStart(); - if (!plPlayerController.isFileSource && - plPlayerController.showSeekPreview && - plPlayerController.cancelSeek != true) { - plPlayerController.updatePreviewIndex(newPos ~/ 1000); + return; + } else if (plPlayerController.hasToasted) { + plPlayerController.hasToasted = false; } + + _onHorizontalDragUpdate(delta.dx); } else if (_gestureType == .left) { // 左边区域 👈 final double level = maxHeight * 3; @@ -1110,21 +1133,8 @@ class _PLVideoPlayerState extends State } void _onPanEnd(ScaleEndDetails details) { - if (plPlayerController.showSeekPreview) { - plPlayerController.showPreview.value = false; - } - if (plPlayerController.isSliderMoving.value) { - if (plPlayerController.cancelSeek == true) { - plPlayerController.onUpdatedSliderProgress( - plPlayerController.position, - ); - } else { - plPlayerController.seekTo( - plPlayerController.sliderPosition, - isSeek: false, - ); - } - plPlayerController.onChangedSliderEnd(); + if (_gestureType == .horizontal) { + _onHorizontalDragEnd(); } _initialFocalPoint = null; _gestureType = null; @@ -1274,6 +1284,7 @@ class _PLVideoPlayerState extends State final dx = pan.dx.abs(); final dy = pan.dy.abs(); if (dx > 3 * dy) { + _onHorizontalDragStart(); _gestureType = .horizontal; } else if (dy > 3 * dx) { _gestureType = .right; @@ -1284,28 +1295,7 @@ class _PLVideoPlayerState extends State if (_gestureType == .horizontal) { if (plPlayerController.isLive) return; - final delta = event.localPanDelta; - final int curSliderPosition = - plPlayerController.sliderPosition.inMilliseconds; - final int newPos = - (curSliderPosition + - (plPlayerController.sliderScale * delta.dx / maxWidth) - .round()) - .clamp(0, plPlayerController.duration.value.inMilliseconds); - final Duration result = Duration(milliseconds: newPos); - if (plPlayerController.cancelSeek == true) { - plPlayerController - ..cancelSeek = null - ..hasToast = null; - } - plPlayerController - ..onUpdatedSliderProgress(result) - ..onChangedSliderStart(); - if (!plPlayerController.isFileSource && - plPlayerController.showSeekPreview && - plPlayerController.cancelSeek != true) { - plPlayerController.updatePreviewIndex(newPos ~/ 1000); - } + _onHorizontalDragUpdate(event.localPanDelta.dx); } else if (_gestureType == .right) { if (!plPlayerController.enableSlideVolumeBrightness) { return; @@ -1328,11 +1318,8 @@ class _PLVideoPlayerState extends State } void _onPointerPanZoomEnd(PointerPanZoomEndEvent event) { - plPlayerController.showPreview.value = false; - if (plPlayerController.isSliderMoving.value) { - plPlayerController - ..seekTo(plPlayerController.sliderPosition, isSeek: false) - ..onChangedSliderEnd(); + if (_gestureType == .horizontal) { + _onHorizontalDragEnd(); } _gestureType = null; } @@ -1455,9 +1442,7 @@ class _PLVideoPlayerState extends State child: Obx( () => AnimatedOpacity( curve: Curves.easeInOut, - opacity: plPlayerController.isSliderMoving.value - ? 1.0 - : 0.0, + opacity: plPlayerController.isSeeking.value ? 1.0 : 0.0, duration: const Duration(milliseconds: 150), child: Container( decoration: const BoxDecoration( @@ -1473,27 +1458,22 @@ class _PLVideoPlayerState extends State mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - Obx(() { - return Text( + Obx( + () => Text( DurationUtils.formatDuration( - plPlayerController - .sliderTempPosition - .value - .inSeconds, + plPlayerController.position.value, ), style: textStyle, - ); - }), + ), + ), const Text('/', style: textStyle), Obx( - () { - return Text( - DurationUtils.formatDuration( - plPlayerController.duration.value.inSeconds, - ), - style: textStyle, - ); - }, + () => Text( + DurationUtils.formatDuration( + plPlayerController.duration.value, + ), + style: textStyle, + ), ), ], ), @@ -1727,20 +1707,18 @@ class _PLVideoPlayerState extends State case .alwaysShow: offstage = showControls; case .alwaysHide: - if (!plPlayerController.isSliderMoving.value) { + if (!plPlayerController.isSeeking.value) { return const SizedBox.shrink(); } offstage = showControls; case .onlyShowFullScreen: offstage = showControls || - (!isFullScreen && - !plPlayerController.isSliderMoving.value); + (!isFullScreen && !plPlayerController.isSeeking.value); case .onlyHideFullScreen: offstage = showControls || - (isFullScreen && - !plPlayerController.isSliderMoving.value); + (isFullScreen && !plPlayerController.isSeeking.value); } return Offstage( offstage: offstage, @@ -1748,17 +1726,11 @@ class _PLVideoPlayerState extends State clipBehavior: Clip.none, alignment: Alignment.bottomCenter, children: [ - Obx(() { - final int value = - plPlayerController.sliderPositionSeconds.value; - final int max = - plPlayerController.duration.value.inSeconds; - final int buffer = - plPlayerController.bufferedSeconds.value; - return ProgressBar( - progress: Duration(seconds: value), - buffered: Duration(seconds: buffer), - total: Duration(seconds: max), + Obx( + () => ProgressBar( + progress: plPlayerController.position.value, + buffered: plPlayerController.buffered.value, + total: plPlayerController.duration.value, progressBarColor: primary, baseBarColor: const Color(0x33FFFFFF), bufferedBarColor: bufferedBarColor, @@ -1766,8 +1738,8 @@ class _PLVideoPlayerState extends State thumbGlowColor: thumbGlowColor, barHeight: 3.5, thumbRadius: 2.5, - ); - }), + ), + ), if (plPlayerController.enableBlock && videoDetailController.segmentProgressList.isNotEmpty) Positioned( @@ -1929,7 +1901,8 @@ class _PLVideoPlayerState extends State ), if (plPlayerController.isBuffering.value) Obx(() { - if (plPlayerController.bufferedSeconds.value == 0) { + final buffered = plPlayerController.buffered.value; + if (buffered == 0) { return const Text( '加载中...', style: TextStyle( @@ -1938,10 +1911,8 @@ class _PLVideoPlayerState extends State ), ); } - String bufferStr = plPlayerController.buffered - .toString(); return Text( - bufferStr.substring(0, bufferStr.length - 3), + DurationUtils.formatDuration(buffered), style: const TextStyle( color: Colors.white, fontSize: 12, @@ -2087,11 +2058,6 @@ class _PLVideoPlayerState extends State ); } - late final segment = Pair( - first: plPlayerController.position.inMilliseconds / 1000.0, - second: plPlayerController.position.inMilliseconds / 1000.0, - ); - Future screenshotWebp() async { final videoInfo = videoDetailController.data; final ids = videoInfo.dash!.video!.map((i) => i.id!).toSet(); @@ -2103,8 +2069,9 @@ class _PLVideoPlayerState extends State final ctr = plPlayerController; final theme = Theme.of(context); - final currentPos = ctr.position.inMilliseconds / 1000.0; - final duration = ctr.duration.value.inMilliseconds / 1000.0; + final currentPos = ctr.positionInMilliseconds / 1000.0; + final duration = ctr.durationInMilliseconds / 1000.0; + final segment = Pair(first: currentPos, second: currentPos); final model = PostSegmentModel( segment: segment, category: SegmentType.sponsor, diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index ac449c0ec..c395ceaff 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -27,24 +27,22 @@ class BottomControl extends StatelessWidget { void onDragStart(ThumbDragDetails duration) { feedBack(); - controller.onChangedSliderStart(duration.timeStamp); + controller + ..position.value = duration.seconds + ..isSeeking.value = true; } void onDragUpdate(ThumbDragDetails duration) { if (!controller.isFileSource && controller.showSeekPreview) { - controller.updatePreviewIndex(duration.timeStamp.inSeconds); + controller.updatePreviewIndex(duration.seconds); } - controller.onUpdatedSliderProgress(duration.timeStamp); + controller.position.value = duration.seconds; } - void onSeek(Duration duration) { - if (controller.showSeekPreview) { - controller.showPreview.value = false; - } + void onSeek(int milliseconds) { controller - ..onChangedSliderEnd() - ..onChangedSlider(duration.inSeconds) - ..seekTo(Duration(seconds: duration.inSeconds), isSeek: false); + ..onSeekEnd() + ..seekTo(Duration(milliseconds: milliseconds), isSeek: false); } @override @@ -70,15 +68,11 @@ class BottomControl extends StatelessWidget { clipBehavior: Clip.none, alignment: Alignment.bottomCenter, children: [ - Obx(() { - final int value = controller.sliderPositionSeconds.value; - final int max = controller.duration.value.inSeconds; - return ProgressBar( - progress: Duration(seconds: value), - buffered: Duration( - seconds: controller.bufferedSeconds.value, - ), - total: Duration(seconds: max), + Obx( + () => ProgressBar( + progress: controller.position.value, + buffered: controller.buffered.value, + total: controller.duration.value, progressBarColor: primary, baseBarColor: const Color(0x33FFFFFF), bufferedBarColor: bufferedBarColor, @@ -90,8 +84,8 @@ class BottomControl extends StatelessWidget { onDragStart: onDragStart, onDragUpdate: onDragUpdate, onSeek: onSeek, - ); - }), + ), + ), if (controller.enableBlock && videoDetailController.segmentProgressList.isNotEmpty) Positioned( diff --git a/lib/services/shutdown_timer_service.dart b/lib/services/shutdown_timer_service.dart index 5f34a1d8c..a54c85842 100644 --- a/lib/services/shutdown_timer_service.dart +++ b/lib/services/shutdown_timer_service.dart @@ -108,7 +108,7 @@ class ShutdownTimerService { void _syncProgressAndExit() { if (PlPlayerController.instance case final player?) { final res = player.makeHeartBeat( - player.positionSeconds.value, + player.position.value, type: .completed, isManual: true, );