0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-31 00:43:56 -05:00

optimized seeking, cleanup

This commit is contained in:
Mert Alev 2024-11-04 15:25:20 -05:00
parent b461318641
commit 284f8c035e
7 changed files with 146 additions and 190 deletions

View file

@ -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'

View file

@ -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,

View file

@ -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<void> 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],

View file

@ -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),

View file

@ -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<VideoPlaybackControls> {
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<VideoPlaybackControls> {
}
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<VideoPlaybackControls> {
}
void restart() {
if (state.position > 0 || !state.pause) {
state = VideoPlaybackControls(
position: 0,
mute: state.mute,
pause: false,
);
}
state = VideoPlaybackControls(
position: 0,
mute: state.mute,

View file

@ -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<VideoPlaybackValueState, VideoPlaybackValue>((ref) {
return VideoPlaybackValueState(ref);
});
class VideoPlaybackValueState extends StateNotifier<VideoPlaybackValue> {
VideoPlaybackValueState(this.ref)
: super(
VideoPlaybackValue.uninitialized(),
);
VideoPlaybackValueState(this.ref) : super(videoPlaybackValueDefault);
final Ref ref;
@ -129,6 +123,7 @@ class VideoPlaybackValueState extends StateNotifier<VideoPlaybackValue> {
}
set position(Duration value) {
if (state.position == value) return;
state = VideoPlaybackValue(
position: value,
duration: state.duration,
@ -136,4 +131,8 @@ class VideoPlaybackValueState extends StateNotifier<VideoPlaybackValue> {
volume: state.volume,
);
}
void reset() {
state = videoPlaybackValueDefault;
}
}

View file

@ -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),