Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(mobile): Use hooks to manage Chewie controller for video #7008

Merged
merged 10 commits into from
Feb 23, 2024
179 changes: 179 additions & 0 deletions mobile/lib/modules/asset_viewer/hooks/chewiew_controller_hook.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import 'dart:async';

import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:video_player/video_player.dart';
import 'package:immich_mobile/shared/models/store.dart' as store;
import 'package:wakelock_plus/wakelock_plus.dart';

/// Provides the initialized video player controller
/// If the asset is local, use the local file
/// Otherwise, use a video player with a URL
ChewieController? useChewieController(
Asset asset, {
EdgeInsets controlsSafeAreaMinimum = const EdgeInsets.only(
bottom: 100,
),
bool showOptions = true,
bool showControlsOnInitialize = false,
bool autoPlay = true,
bool autoInitialize = true,
bool allowFullScreen = false,
bool allowedScreenSleep = false,
bool showControls = true,
Widget? customControls,
Widget? placeholder,
Duration hideControlsTimer = const Duration(seconds: 1),
VoidCallback? onPlaying,
VoidCallback? onPaused,
VoidCallback? onVideoEnded,
}) {
return use(
_ChewieControllerHook(
asset: asset,
placeholder: placeholder,
showOptions: showOptions,
controlsSafeAreaMinimum: controlsSafeAreaMinimum,
autoPlay: autoPlay,
allowFullScreen: allowFullScreen,
customControls: customControls,
hideControlsTimer: hideControlsTimer,
showControlsOnInitialize: showControlsOnInitialize,
showControls: showControls,
autoInitialize: autoInitialize,
allowedScreenSleep: allowedScreenSleep,
onPlaying: onPlaying,
onPaused: onPaused,
onVideoEnded: onVideoEnded,
),
);
}

class _ChewieControllerHook extends Hook<ChewieController?> {
final Asset asset;
final EdgeInsets controlsSafeAreaMinimum;
final bool showOptions;
final bool showControlsOnInitialize;
final bool autoPlay;
final bool autoInitialize;
final bool allowFullScreen;
final bool allowedScreenSleep;
final bool showControls;
final Widget? customControls;
final Widget? placeholder;
final Duration hideControlsTimer;
final VoidCallback? onPlaying;
final VoidCallback? onPaused;
final VoidCallback? onVideoEnded;

const _ChewieControllerHook({
required this.asset,
this.controlsSafeAreaMinimum = const EdgeInsets.only(
bottom: 100,
),
this.showOptions = true,
this.showControlsOnInitialize = false,
this.autoPlay = true,
this.autoInitialize = true,
this.allowFullScreen = false,
this.allowedScreenSleep = false,
this.showControls = true,
this.customControls,
this.placeholder,
this.hideControlsTimer = const Duration(seconds: 3),
this.onPlaying,
this.onPaused,
this.onVideoEnded,
});

@override
createState() => _ChewieControllerHookState();
}

class _ChewieControllerHookState
extends HookState<ChewieController?, _ChewieControllerHook> {
ChewieController? chewieController;
VideoPlayerController? videoPlayerController;

@override
void initHook() async {
super.initHook();
unawaited(_initialize());
}

@override
void dispose() {
chewieController?.dispose();
videoPlayerController?.dispose();
super.dispose();
}

@override
ChewieController? build(BuildContext context) {
return chewieController;
}

/// Initializes the chewie controller and video player controller
Future<void> _initialize() async {
if (hook.asset.isLocal && hook.asset.livePhotoVideoId == null) {
// Use a local file for the video player controller
final file = await hook.asset.local!.file;
if (file == null) {
throw Exception('No file found for the video');
}
videoPlayerController = VideoPlayerController.file(file);
} else {
// Use a network URL for the video player controller
final serverEndpoint = store.Store.get(store.StoreKey.serverEndpoint);
final String videoUrl = hook.asset.livePhotoVideoId != null
? '$serverEndpoint/asset/file/${hook.asset.livePhotoVideoId}'
: '$serverEndpoint/asset/file/${hook.asset.remoteId}';

final url = Uri.parse(videoUrl);
final accessToken = store.Store.get(StoreKey.accessToken);

videoPlayerController = VideoPlayerController.networkUrl(
url,
httpHeaders: {"x-immich-user-token": accessToken},
);
}

videoPlayerController!.addListener(() {
final value = videoPlayerController!.value;
if (value.isPlaying) {
WakelockPlus.enable();
hook.onPlaying?.call();
} else if (!value.isPlaying) {
WakelockPlus.disable();
hook.onPaused?.call();
}

if (value.position == value.duration) {
WakelockPlus.disable();
hook.onVideoEnded?.call();
}
});

await videoPlayerController!.initialize();

setState(() {
chewieController = ChewieController(
videoPlayerController: videoPlayerController!,
controlsSafeAreaMinimum: hook.controlsSafeAreaMinimum,
showOptions: hook.showOptions,
showControlsOnInitialize: hook.showControlsOnInitialize,
autoPlay: hook.autoPlay,
autoInitialize: hook.autoInitialize,
allowFullScreen: hook.allowFullScreen,
allowedScreenSleep: hook.allowedScreenSleep,
showControls: hook.showControls,
customControls: hook.customControls,
placeholder: hook.placeholder,
hideControlsTimer: hook.hideControlsTimer,
);
});
}
}
22 changes: 12 additions & 10 deletions mobile/lib/modules/asset_viewer/ui/video_player_controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provi
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/center_play_button.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';
import 'package:video_player/video_player.dart';

class VideoPlayerControls extends ConsumerStatefulWidget {
Expand Down Expand Up @@ -66,7 +66,9 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
children: [
if (_displayBufferingIndicator)
const Center(
child: ImmichLoadingIndicator(),
child: DelayedLoadingIndicator(
fadeInDuration: Duration(milliseconds: 400),
),
)
else
_buildHitArea(),
Expand All @@ -79,6 +81,7 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
@override
void dispose() {
_dispose();

super.dispose();
}

Expand All @@ -92,6 +95,7 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
final oldController = _chewieController;
_chewieController = ChewieController.of(context);
controller = chewieController.videoPlayerController;
_latestValue = controller.value;

if (oldController != chewieController) {
_dispose();
Expand All @@ -106,12 +110,10 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>

return GestureDetector(
onTap: () {
if (_latestValue.isPlaying) {
ref.read(showControlsProvider.notifier).show = false;
} else {
if (!_latestValue.isPlaying) {
_playPause();
ref.read(showControlsProvider.notifier).show = false;
}
ref.read(showControlsProvider.notifier).show = false;
},
child: CenterPlayButton(
backgroundColor: Colors.black54,
Expand All @@ -131,10 +133,11 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
}

Future<void> _initialize() async {
ref.read(showControlsProvider.notifier).show = false;
_mute(ref.read(videoPlayerControlsProvider.select((value) => value.mute)));

controller.addListener(_updateState);
_latestValue = controller.value;
controller.addListener(_updateState);

if (controller.value.isPlaying || chewieController.autoPlay) {
_startHideTimer();
Expand Down Expand Up @@ -167,9 +170,8 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
}

void _startHideTimer() {
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
? ChewieController.defaultHideControlsTimer
: chewieController.hideControlsTimer;
final hideControlsTimer = chewieController.hideControlsTimer;
_hideTimer?.cancel();
_hideTimer = Timer(hideControlsTimer, () {
ref.read(showControlsProvider.notifier).show = false;
});
Expand Down
16 changes: 15 additions & 1 deletion mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,18 @@ class GalleryViewerPage extends HookConsumerWidget {
);
}

useEffect(
() {
if (ref.read(showControlsProvider)) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
return null;
},
[],
);

ref.listen(showControlsProvider, (_, show) {
if (show) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
Expand Down Expand Up @@ -794,7 +806,9 @@ class GalleryViewerPage extends HookConsumerWidget {
minScale: 1.0,
basePosition: Alignment.center,
child: VideoViewerPage(
onPlaying: () => isPlayingVideo.value = true,
onPlaying: () {
isPlayingVideo.value = true;
},
onPaused: () =>
WidgetsBinding.instance.addPostFrameCallback(
(_) => isPlayingVideo.value = false,
Expand Down
Loading
Loading