From 284f8c035e36369fc0e73df2b40e6e5c9b021155 Mon Sep 17 00:00:00 2001 From: Mert Alev <38161441+Ulbert@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:25:20 -0500 Subject: [PATCH] optimized seeking, cleanup --- .../lib/pages/common/gallery_viewer.page.dart | 14 +- .../lib/pages/common/native_video_loader.dart | 5 +- .../common/native_video_viewer.page.dart | 193 ++++++++---------- .../lib/pages/common/video_viewer.page.dart | 3 +- .../video_player_controls_provider.dart | 35 +--- .../video_player_value_provider.dart | 71 ++++--- .../custom_video_player_controls.dart | 15 +- 7 files changed, 146 insertions(+), 190 deletions(-) diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index fe03a922fd..8ce72af91b 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -47,7 +47,7 @@ class GalleryViewerPage extends HookConsumerWidget { this.initialIndex = 0, this.heroOffset = 0, this.showStack = false, - }) : controller = PageController(initialPage: initialIndex, keepPage: false); + }) : controller = PageController(initialPage: initialIndex); final PageController controller; @@ -328,7 +328,9 @@ class GalleryViewerPage extends HookConsumerWidget { final ImageProvider provider = ImmichImage.imageProvider(asset: a); + ref.read(videoPlaybackValueProvider.notifier).reset(); if (a.isImage && !isPlayingVideo.value) { + ref.read(showControlsProvider.notifier).show = false; return PhotoViewGalleryPageOptions( onDragStart: (_, details, __) { log.info('Drag start'); @@ -363,13 +365,11 @@ class GalleryViewerPage extends HookConsumerWidget { ); } else { log.info('Loading asset ${a.id} (index $index) as video'); - ref.read(videoPlaybackValueProvider.notifier).value = - VideoPlaybackValue.uninitialized(); return PhotoViewGalleryPageOptions.customChild( - // onDragStart: (_, details, __) => - // localPosition.value = details.localPosition, - // onDragUpdate: (_, details, __) => - // handleSwipeUpDown(details), + onDragStart: (_, details, __) => + localPosition.value = details.localPosition, + onDragUpdate: (_, details, __) => + handleSwipeUpDown(details), // heroAttributes: PhotoViewHeroAttributes( // tag: isFromDto // ? '${currentAsset.remoteId}-$heroOffset' diff --git a/mobile/lib/pages/common/native_video_loader.dart b/mobile/lib/pages/common/native_video_loader.dart index 1d7bdae221..6c683d8c53 100644 --- a/mobile/lib/pages/common/native_video_loader.dart +++ b/mobile/lib/pages/common/native_video_loader.dart @@ -173,9 +173,8 @@ class NativeVideoLoader extends HookConsumerWidget { child: GestureDetector( behavior: HitTestBehavior.deferToChild, child: PopScope( - onPopInvokedWithResult: (didPop, _) => ref - .read(videoPlaybackValueProvider.notifier) - .value = VideoPlaybackValue.uninitialized(), + onPopInvokedWithResult: (didPop, _) => + ref.read(videoPlaybackValueProvider.notifier).reset(), child: SizedBox( height: size.height, width: size.width, diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart index d3eefcde6a..537a1d1bd3 100644 --- a/mobile/lib/pages/common/native_video_viewer.page.dart +++ b/mobile/lib/pages/common/native_video_viewer.page.dart @@ -12,23 +12,6 @@ import 'package:logging/logging.dart'; import 'package:native_video_player/native_video_player.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; -// @override -// void dispose() { -// bufferingTimer.value.cancel(); -// try { -// controller.value?.onPlaybackPositionChanged -// .removeListener(onPlaybackPositionChanged); -// controller.value?.onPlaybackStatusChanged -// .removeListener(onPlaybackPositionChanged); -// controller.value?.onPlaybackReady.removeListener(onPlaybackReady); -// controller.value?.onPlaybackEnded.removeListener(onPlaybackEnded); -// controller.value?.stop(); -// } catch (_) { -// // Consume error from the controller -// } -// super.dispose(); -// } - class NativeVideoViewerPage extends HookConsumerWidget { final VideoSource videoSource; final double aspectRatio; @@ -75,7 +58,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { } // timer to mark videos as buffering if the position does not change - // useInterval(const Duration(seconds: 5), checkIfBuffering); + useInterval(const Duration(seconds: 5), checkIfBuffering); // When the volume changes, set the volume ref.listen(videoPlayerControlsProvider.select((value) => value.mute), @@ -86,19 +69,22 @@ class NativeVideoViewerPage extends HookConsumerWidget { return; } + final playbackInfo = playerController.playbackInfo; + if (playbackInfo == null) { + log.info('No playback info to update'); + return; + } + try { - if (mute) { + if (mute && playbackInfo.volume != 0.0) { log.info('Muting video'); playerController.setVolume(0.0); - log.info('Muted video'); - } else { + } else if (!mute && playbackInfo.volume != 0.7) { log.info('Unmuting video'); playerController.setVolume(0.7); - log.info('Unmuted video'); } } catch (error) { log.severe('Error setting volume: $error'); - // Consume error from the controller } }); @@ -108,20 +94,28 @@ class NativeVideoViewerPage extends HookConsumerWidget { final playerController = controller.value; if (playerController == null) { log.info('No controller to seek to'); - // No seeeking if there is no video + return; + } + + final playbackInfo = playerController.playbackInfo; + if (playbackInfo == null) { + log.info('No playback info to update'); return; } // Find the position to seek to - final Duration seek = duration * (position / 100.0); - try { - log.info('Seeking to position: ${seek.inSeconds}'); - playerController.seekTo(seek.inSeconds); - log.info('Seeked to position: ${seek.inSeconds}'); - } catch (error) { - log.severe('Error seeking to position $position: $error'); - // Consume error from the controller + final int seek = (duration * (position / 100.0)).inSeconds; + if (seek != playbackInfo.position) { + log.info('Seeking to position: $seek from ${playbackInfo.position}'); + try { + playerController.seekTo(seek); + } catch (error) { + log.severe('Error seeking to position $position: $error'); + } } + + ref.read(videoPlaybackValueProvider.notifier).position = + Duration(seconds: seek); }); // // When the custom video controls pause or play @@ -131,15 +125,14 @@ class NativeVideoViewerPage extends HookConsumerWidget { if (pause) { log.info('Pausing video'); controller.value?.pause(); - log.info('Paused video'); + WakelockPlus.disable(); } else { log.info('Playing video'); controller.value?.play(); - log.info('Played video'); + WakelockPlus.enable(); } } catch (error) { log.severe('Error pausing or playing video: $error'); - // Consume error from the controller } }); @@ -148,69 +141,77 @@ class NativeVideoViewerPage extends HookConsumerWidget { log.info('onPlaybackReady: Playing video'); controller.value?.play(); controller.value?.setVolume(0.9); - log.info('onPlaybackReady: Played video'); + WakelockPlus.enable(); } catch (error) { log.severe('Error playing video: $error'); - // Consume error from the controller } } - void onPlaybackPositionChanged() { - if (controller.value == null || !context.mounted) { + void onPlaybackStatusChanged() { + final videoController = controller.value; + if (videoController == null || !context.mounted) { log.info('No controller to update'); return; } - log.info('Updating video playback'); final videoPlayback = VideoPlaybackValue.fromNativeController(controller.value!); ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback; - log.info('Updated video playback'); + + if (videoPlayback.state == VideoPlaybackState.playing) { + // Sync with the controls playing + WakelockPlus.enable(); + log.info('Video is playing; enabled wakelock'); + } else { + // Sync with the controls pause + WakelockPlus.disable(); + log.info('Video is not playing; disabled wakelock'); + } + } + + void onPlaybackPositionChanged() { + final videoController = controller.value; + if (videoController == null || !context.mounted) { + log.info('No controller to update'); + return; + } + + final playbackInfo = videoController.playbackInfo; + if (playbackInfo == null) { + log.info('No playback info to update'); + return; + } + + ref.read(videoPlaybackValueProvider.notifier).position = + Duration(seconds: playbackInfo.position); // Check if the video is buffering - if (videoPlayback.state == VideoPlaybackState.playing) { - log.info('Updating video: checking if playing video is buffering'); - isBuffering.value = - lastVideoPosition.value == videoPlayback.position.inSeconds; - lastVideoPosition.value = videoPlayback.position.inSeconds; + if (playbackInfo.status == PlaybackStatus.playing) { log.info('Updating playing video position'); + isBuffering.value = lastVideoPosition.value == playbackInfo.position; + lastVideoPosition.value = playbackInfo.position; } else { - log.info('Updating video: video is not playing'); + log.info('Updating non-playing video position'); isBuffering.value = false; lastVideoPosition.value = -1; - log.info('Updated non-playing video position'); - } - final state = videoPlayback.state; - - // Enable the WakeLock while the video is playing - if (state == VideoPlaybackState.playing) { - log.info('Syncing with the controls playing'); - // Sync with the controls playing - // WakelockPlus.enable(); - log.info('Synced with the controls playing'); - } else { - log.info('Syncing with the controls pause'); - // Sync with the controls pause - // WakelockPlus.disable(); - log.info('Synced with the controls pause'); } } void onPlaybackEnded() { - try { - log.info('onPlaybackEnded: Video ended'); - if (loopVideo) { - log.info('onPlaybackEnded: Looping video'); + log.info('onPlaybackEnded: Video ended'); + if (loopVideo) { + log.info('onPlaybackEnded: Looping video'); + try { controller.value?.play(); - log.info('onPlaybackEnded: Looped video'); + } catch (error) { + log.severe('Error looping video: $error'); } - } catch (error) { - log.severe('Error looping video: $error'); - // Consume error from the controller + } else { + WakelockPlus.disable(); } } - Future initController(NativeVideoPlayerController nc) async { + void initController(NativeVideoPlayerController nc) { if (controller.value != null) { log.info('initController: Controller already initialized'); return; @@ -218,27 +219,21 @@ class NativeVideoViewerPage extends HookConsumerWidget { log.info('initController: adding onPlaybackPositionChanged listener'); nc.onPlaybackPositionChanged.addListener(onPlaybackPositionChanged); - log.info('initController: added onPlaybackPositionChanged listener'); log.info('initController: adding onPlaybackStatusChanged listener'); - nc.onPlaybackStatusChanged.addListener(onPlaybackPositionChanged); - log.info('initController: added onPlaybackStatusChanged listener'); + nc.onPlaybackStatusChanged.addListener(onPlaybackStatusChanged); log.info('initController: adding onPlaybackReady listener'); nc.onPlaybackReady.addListener(onPlaybackReady); - log.info('initController: added onPlaybackReady listener'); log.info('initController: adding onPlaybackEnded listener'); nc.onPlaybackEnded.addListener(onPlaybackEnded); - log.info('initController: added onPlaybackEnded listener'); log.info('initController: loading video source'); nc.loadVideoSource(videoSource); - log.info('initController: loaded video source'); log.info('initController: setting controller'); controller.value = nc; - log.info('initController: set controller'); Timer(const Duration(milliseconds: 200), checkIfBuffering); } @@ -246,55 +241,45 @@ class NativeVideoViewerPage extends HookConsumerWidget { () { log.info('useEffect: resetting video player controls'); ref.read(videoPlayerControlsProvider.notifier).reset(); - log.info('useEffect: resetting video player controls'); if (isMotionVideo) { // ignore: prefer-extracting-callbacks - log.info('useEffect: disabled showing video player controls'); + log.info('useEffect: disabling showing video player controls'); ref.read(showControlsProvider.notifier).show = false; - log.info('useEffect: disabled showing video player controls'); } return () { + final playerController = controller.value; + if (playerController == null) { + log.info('No controller to dispose'); + return; + } try { - final playerController = controller.value; - if (playerController == null) { - log.info('No controller to dispose'); - return; - } + log.info('Stopping video'); + playerController.stop(); + log.info('Removing onPlaybackPositionChanged listener'); playerController.onPlaybackPositionChanged .removeListener(onPlaybackPositionChanged); - log.info('Removed onPlaybackPositionChanged listener'); log.info('Removing onPlaybackStatusChanged listener'); playerController.onPlaybackStatusChanged - .removeListener(onPlaybackPositionChanged); - log.info('Removed onPlaybackStatusChanged listener'); + .removeListener(onPlaybackStatusChanged); log.info('Removing onPlaybackReady listener'); playerController.onPlaybackReady.removeListener(onPlaybackReady); - log.info('Removed onPlaybackReady listener'); log.info('Removing onPlaybackEnded listener'); playerController.onPlaybackEnded.removeListener(onPlaybackEnded); - log.info('Removed onPlaybackEnded listener'); - - log.info('Stopping video'); - playerController.stop(); - log.info('Stopped video'); - - log.info('Disposing controller'); - controller.value = null; - log.info('Disposed controller'); - - // log.info('Disabling Wakelock'); - // WakelockPlus.disable(); - // log.info('Disabled Wakelock'); } catch (error) { log.severe('Error during useEffect cleanup: $error'); - // Consume error from the controller } + + log.info('Disposing controller'); + controller.value = null; + + log.info('Disabling Wakelock'); + WakelockPlus.disable(); }; }, [videoSource], diff --git a/mobile/lib/pages/common/video_viewer.page.dart b/mobile/lib/pages/common/video_viewer.page.dart index 573f7277f2..4a7617f4d5 100644 --- a/mobile/lib/pages/common/video_viewer.page.dart +++ b/mobile/lib/pages/common/video_viewer.page.dart @@ -124,8 +124,7 @@ class VideoViewerPage extends HookConsumerWidget { return PopScope( onPopInvokedWithResult: (didPop, _) { - ref.read(videoPlaybackValueProvider.notifier).value = - VideoPlaybackValue.uninitialized(); + ref.read(videoPlaybackValueProvider.notifier).reset(); }, child: AnimatedSwitcher( duration: const Duration(milliseconds: 400), diff --git a/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart b/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart index 9920951cb6..20f8fd7d2e 100644 --- a/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart +++ b/mobile/lib/providers/asset_viewer/video_player_controls_provider.dart @@ -1,7 +1,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; class VideoPlaybackControls { - VideoPlaybackControls({ + const VideoPlaybackControls({ required this.position, required this.mute, required this.pause, @@ -17,15 +17,14 @@ final videoPlayerControlsProvider = return VideoPlayerControls(ref); }); +const videoPlayerControlsDefault = VideoPlaybackControls( + position: 0, + pause: false, + mute: false, +); + class VideoPlayerControls extends StateNotifier { - VideoPlayerControls(this.ref) - : super( - VideoPlaybackControls( - position: 0, - pause: false, - mute: false, - ), - ); + VideoPlayerControls(this.ref) : super(videoPlayerControlsDefault); final Ref ref; @@ -36,15 +35,7 @@ class VideoPlayerControls extends StateNotifier { } void reset() { - if (state.position == 0 && !state.mute && !state.pause) { - return; - } - - state = VideoPlaybackControls( - position: 0, - pause: false, - mute: false, - ); + state = videoPlayerControlsDefault; } double get position => state.position; @@ -115,14 +106,6 @@ class VideoPlayerControls extends StateNotifier { } void restart() { - if (state.position > 0 || !state.pause) { - state = VideoPlaybackControls( - position: 0, - mute: state.mute, - pause: false, - ); - } - state = VideoPlaybackControls( position: 0, mute: state.mute, diff --git a/mobile/lib/providers/asset_viewer/video_player_value_provider.dart b/mobile/lib/providers/asset_viewer/video_player_value_provider.dart index bffe6c7cf6..d9677c7774 100644 --- a/mobile/lib/providers/asset_viewer/video_player_value_provider.dart +++ b/mobile/lib/providers/asset_viewer/video_player_value_provider.dart @@ -23,7 +23,7 @@ class VideoPlaybackValue { /// The volume of the video final double volume; - VideoPlaybackValue({ + const VideoPlaybackValue({ required this.position, required this.duration, required this.state, @@ -33,32 +33,31 @@ class VideoPlaybackValue { factory VideoPlaybackValue.fromNativeController( NativeVideoPlayerController controller, ) { - PlaybackInfo? playbackInfo; - VideoInfo? videoInfo; - try { - playbackInfo = controller.playbackInfo; - videoInfo = controller.videoInfo; - } catch (_) { - // Consume error from the controller + final playbackInfo = controller.playbackInfo; + final videoInfo = controller.videoInfo; + + if (playbackInfo == null || videoInfo == null) { + return videoPlaybackValueDefault; } - late VideoPlaybackState s; - if (playbackInfo?.status == null) { - s = VideoPlaybackState.initializing; - } else if (playbackInfo?.status == PlaybackStatus.stopped && - (playbackInfo?.positionFraction == 1 || - playbackInfo?.positionFraction == 0)) { - s = VideoPlaybackState.completed; - } else if (playbackInfo?.status == PlaybackStatus.playing) { - s = VideoPlaybackState.playing; - } else { - s = VideoPlaybackState.paused; + + late final VideoPlaybackState status; + switch (playbackInfo.status) { + case PlaybackStatus.playing: + status = VideoPlaybackState.playing; + break; + case PlaybackStatus.paused: + status = VideoPlaybackState.paused; + break; + case PlaybackStatus.stopped: + status = VideoPlaybackState.completed; + break; } return VideoPlaybackValue( - position: Duration(seconds: playbackInfo?.position ?? 0), - duration: Duration(seconds: videoInfo?.duration ?? 0), - state: s, - volume: playbackInfo?.volume ?? 0.0, + position: Duration(seconds: playbackInfo.position), + duration: Duration(seconds: videoInfo.duration), + state: status, + volume: playbackInfo.volume, ); } @@ -85,15 +84,6 @@ class VideoPlaybackValue { ); } - factory VideoPlaybackValue.uninitialized() { - return VideoPlaybackValue( - position: Duration.zero, - duration: Duration.zero, - state: VideoPlaybackState.initializing, - volume: 0.0, - ); - } - VideoPlaybackValue copyWith({ Duration? position, Duration? duration, @@ -109,16 +99,20 @@ class VideoPlaybackValue { } } +const VideoPlaybackValue videoPlaybackValueDefault = VideoPlaybackValue( + position: Duration.zero, + duration: Duration.zero, + state: VideoPlaybackState.initializing, + volume: 0.0, +); + final videoPlaybackValueProvider = StateNotifierProvider((ref) { return VideoPlaybackValueState(ref); }); class VideoPlaybackValueState extends StateNotifier { - VideoPlaybackValueState(this.ref) - : super( - VideoPlaybackValue.uninitialized(), - ); + VideoPlaybackValueState(this.ref) : super(videoPlaybackValueDefault); final Ref ref; @@ -129,6 +123,7 @@ class VideoPlaybackValueState extends StateNotifier { } set position(Duration value) { + if (state.position == value) return; state = VideoPlaybackValue( position: value, duration: state.duration, @@ -136,4 +131,8 @@ class VideoPlaybackValueState extends StateNotifier { volume: state.volume, ); } + + void reset() { + state = videoPlaybackValueDefault; + } } diff --git a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart index d53f268ae5..2fd0a38edc 100644 --- a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart +++ b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; @@ -29,10 +28,9 @@ class CustomVideoPlayerControls extends HookConsumerWidget { } }, ); - - final showBuffering = useState(false); final VideoPlaybackState state = - ref.watch(videoPlaybackValueProvider).state; + ref.watch(videoPlaybackValueProvider.select((value) => value.state)); + final showBuffering = state == VideoPlaybackState.buffering; /// Shows the controls and starts the timer to hide them void showControlsAndStartHideTimer() { @@ -52,16 +50,9 @@ class CustomVideoPlayerControls extends HookConsumerWidget { showControlsAndStartHideTimer(); }); - ref.listen(videoPlaybackValueProvider.select((value) => value.state), - (_, state) { - // Show buffering - showBuffering.value = state == VideoPlaybackState.buffering; - }); - /// Toggles between playing and pausing depending on the state of the video void togglePlay() { showControlsAndStartHideTimer(); - final state = ref.read(videoPlaybackValueProvider).state; if (state == VideoPlaybackState.playing) { ref.read(videoPlayerControlsProvider.notifier).pause(); } else if (state == VideoPlaybackState.completed) { @@ -78,7 +69,7 @@ class CustomVideoPlayerControls extends HookConsumerWidget { absorbing: !ref.watch(showControlsProvider), child: Stack( children: [ - if (showBuffering.value) + if (showBuffering) const Center( child: DelayedLoadingIndicator( fadeInDuration: Duration(milliseconds: 400),