1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-01 08:31:59 +00:00

refactor: native_video_player

This commit is contained in:
shenlong-tanwen 2024-08-29 03:31:13 +05:30 committed by mertalev
parent 6f3ceb58b8
commit 5ebac69647
No known key found for this signature in database
GPG key ID: CA85EF6600C9E8AD
9 changed files with 247 additions and 231 deletions

View file

@ -13,7 +13,6 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/pages/common/download_panel.dart'; import 'package:immich_mobile/pages/common/download_panel.dart';
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
import 'package:immich_mobile/pages/common/video_viewer.page.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
@ -353,10 +352,6 @@ class GalleryViewerPage extends HookConsumerWidget {
), ),
); );
} else { } else {
final useNativePlayer =
a.isLocal && a.livePhotoVideoId == null;
debugPrint("asset.isLocal ${asset.isLocal}");
debugPrint("build video player $useNativePlayer");
return PhotoViewGalleryPageOptions.customChild( return PhotoViewGalleryPageOptions.customChild(
onDragStart: (_, details, __) => onDragStart: (_, details, __) =>
localPosition.value = details.localPosition, localPosition.value = details.localPosition,
@ -371,24 +366,19 @@ class GalleryViewerPage extends HookConsumerWidget {
maxScale: 1.0, maxScale: 1.0,
minScale: 1.0, minScale: 1.0,
basePosition: Alignment.center, basePosition: Alignment.center,
child: useNativePlayer child: NativeVideoViewerPage(
? NativeVideoViewerPage( key: ValueKey(a),
key: ValueKey(a), asset: a,
asset: a, isMotionVideo: a.livePhotoVideoId != null,
) loopVideo: shouldLoopVideo.value,
: VideoViewerPage( placeholder: Image(
key: ValueKey(a), image: provider,
asset: a, fit: BoxFit.contain,
isMotionVideo: a.livePhotoVideoId != null, height: context.height,
loopVideo: shouldLoopVideo.value, width: context.width,
placeholder: Image( alignment: Alignment.center,
image: provider, ),
fit: BoxFit.contain, ),
height: context.height,
width: context.width,
alignment: Alignment.center,
),
),
); );
} }
}, },

View file

@ -1,158 +1,226 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/asset_viewer/native_video_player_controller_provider.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_controller_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
import 'package:immich_mobile/widgets/asset_viewer/video_player.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/widgets/asset_viewer/custom_video_player_controls.dart';
import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
import 'package:native_video_player/native_video_player.dart'; import 'package:native_video_player/native_video_player.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
class NativeVideoViewerPage extends ConsumerStatefulWidget { class NativeVideoViewerPage extends HookConsumerWidget {
final Asset asset; final Asset asset;
final bool isMotionVideo;
final Widget? placeholder; final Widget? placeholder;
final bool showControls;
final Duration hideControlsTimer;
final bool loopVideo;
const NativeVideoViewerPage({ const NativeVideoViewerPage({
super.key, super.key,
required this.asset, required this.asset,
this.isMotionVideo = false,
this.placeholder, this.placeholder,
this.showControls = true,
this.hideControlsTimer = const Duration(seconds: 5),
this.loopVideo = false,
}); });
@override @override
NativeVideoViewerPageState createState() => NativeVideoViewerPageState(); Widget build(BuildContext context, WidgetRef ref) {
} final controller = useState<NativeVideoPlayerController?>(null);
class NativeVideoViewerPageState extends ConsumerState<NativeVideoViewerPage> { Future<VideoSource> createSource(Asset asset) async {
NativeVideoPlayerController? _controller; if (asset.isLocal && asset.livePhotoVideoId == null) {
final file = await asset.local!.file;
if (file == null) {
throw Exception('No file found for the video');
}
return await VideoSource.init(
path: file.path,
type: VideoSourceType.file,
);
} else {
// Use a network URL for the video player controller
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final String videoUrl = asset.livePhotoVideoId != null
? '$serverEndpoint/assets/${asset.livePhotoVideoId}/video/playback'
: '$serverEndpoint/assets/${asset.remoteId}/video/playback';
bool isAutoplayEnabled = false; return await VideoSource.init(
bool isPlaybackLoopEnabled = false; path: videoUrl,
type: VideoSourceType.network,
double videoWidth = 0; headers: ApiService.getRequestHeaders(),
double videoHeight = 0; );
}
Future<void> _initController(NativeVideoPlayerController controller) async {
_controller = controller;
_controller?. //
onPlaybackStatusChanged
.addListener(_onPlaybackStatusChanged);
_controller?. //
onPlaybackPositionChanged
.addListener(_onPlaybackPositionChanged);
_controller?. //
onPlaybackSpeedChanged
.addListener(_onPlaybackSpeedChanged);
_controller?. //
onVolumeChanged
.addListener(_onPlaybackVolumeChanged);
_controller?. //
onPlaybackReady
.addListener(_onPlaybackReady);
_controller?. //
onPlaybackEnded
.addListener(_onPlaybackEnded);
await _loadVideoSource();
}
Future<void> _loadVideoSource() async {
final videoSource = await _createVideoSource();
await _controller?.loadVideoSource(videoSource);
}
Future<VideoSource> _createVideoSource() async {
final file = await widget.asset.local!.file;
if (file == null) {
throw Exception('No file found for the video');
} }
return await VideoSource.init( // When the volume changes, set the volume
path: file.path, ref.listen(videoPlayerControlsProvider.select((value) => value.mute),
type: VideoSourceType.file, (_, mute) {
if (mute) {
controller.value?.setVolume(0.0);
} else {
controller.value?.setVolume(0.7);
}
});
// When the position changes, seek to the position
ref.listen(videoPlayerControlsProvider.select((value) => value.position),
(_, position) {
if (controller.value == null) {
// No seeeking if there is no video
return;
}
// Find the position to seek to
final Duration seek = asset.duration * (position / 100.0);
controller.value?.seekTo(seek.inSeconds);
});
// When the custom video controls paus or plays
ref.listen(videoPlayerControlsProvider.select((value) => value.pause),
(_, pause) {
if (pause) {
controller.value?.pause();
} else {
controller.value?.play();
}
});
void updateVideoPlayback() {
if (controller.value == null || !context.mounted) {
return;
}
final videoPlayback =
VideoPlaybackValue.fromNativeController(controller.value!);
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
final state = videoPlayback.state;
// Enable the WakeLock while the video is playing
if (state == VideoPlaybackState.playing) {
// Sync with the controls playing
WakelockPlus.enable();
} else {
// Sync with the controls pause
WakelockPlus.disable();
}
}
void onPlaybackReady() {
controller.value?.play();
}
void onPlaybackPositionChanged() {
updateVideoPlayback();
}
void onPlaybackEnded() {
if (loopVideo) {
controller.value?.play();
}
}
Future<void> initController(NativeVideoPlayerController nc) async {
if (controller.value != null) {
return;
}
controller.value = nc;
controller.value?.onPlaybackPositionChanged
.addListener(onPlaybackPositionChanged);
controller.value?.onPlaybackStatusChanged
.addListener(onPlaybackPositionChanged);
controller.value?.onPlaybackReady.addListener(onPlaybackReady);
controller.value?.onPlaybackEnded.addListener(onPlaybackEnded);
final videoSource = await createSource(asset);
controller.value?.loadVideoSource(videoSource);
}
useEffect(
() {
Future.microtask(
() => ref.read(videoPlayerControlsProvider.notifier).reset(),
);
if (isMotionVideo) {
// ignore: prefer-extracting-callbacks
Future.microtask(() {
ref.read(showControlsProvider.notifier).show = false;
});
}
return () {
controller.value?.onPlaybackPositionChanged
.removeListener(onPlaybackPositionChanged);
controller.value?.onPlaybackStatusChanged
.removeListener(onPlaybackPositionChanged);
controller.value?.onPlaybackReady.removeListener(onPlaybackReady);
controller.value?.onPlaybackEnded.removeListener(onPlaybackEnded);
};
},
[],
); );
}
@override void updatePlayback(VideoPlaybackValue value) =>
void dispose() { ref.read(videoPlaybackValueProvider.notifier).value = value;
_controller?. //
onPlaybackStatusChanged
.removeListener(_onPlaybackStatusChanged);
_controller?. //
onPlaybackPositionChanged
.removeListener(_onPlaybackPositionChanged);
_controller?. //
onPlaybackSpeedChanged
.removeListener(_onPlaybackSpeedChanged);
_controller?. //
onVolumeChanged
.removeListener(_onPlaybackVolumeChanged);
_controller?. //
onPlaybackReady
.removeListener(_onPlaybackReady);
_controller?. //
onPlaybackEnded
.removeListener(_onPlaybackEnded);
_controller = null;
super.dispose();
}
void _onPlaybackReady() { final size = MediaQuery.sizeOf(context);
final videoInfo = _controller?.videoInfo;
if (videoInfo != null) {
videoWidth = videoInfo.width.toDouble();
videoHeight = videoInfo.height.toDouble();
}
setState(() {});
_controller?.play();
}
void _onPlaybackStatusChanged() { return SizedBox(
setState(() {}); height: size.height,
} width: size.width,
child: GestureDetector(
void _onPlaybackPositionChanged() { behavior: HitTestBehavior.deferToChild,
setState(() {}); child: PopScope(
} onPopInvokedWithResult: (didPop, _) =>
updatePlayback(VideoPlaybackValue.uninitialized()),
void _onPlaybackSpeedChanged() { child: SizedBox(
setState(() {}); height: size.height,
} width: size.width,
child: Stack(
void _onPlaybackVolumeChanged() { children: [
setState(() {}); Center(
} child: AspectRatio(
aspectRatio: (asset.width ?? 1) / (asset.height ?? 1),
void _onPlaybackEnded() { child: NativeVideoPlayerView(
if (isPlaybackLoopEnabled) { onViewReady: initController,
_controller?.play(); ),
} ),
} ),
if (showControls)
@override Center(
Widget build(BuildContext context) { child: CustomVideoPlayerControls(
return PopScope( hideTimerDuration: hideControlsTimer,
onPopInvoked: (pop) {}, ),
child: SizedBox( ),
height: videoHeight, Visibility(
width: videoWidth, visible: controller.value == null,
child: AspectRatio( child: Stack(
aspectRatio: 16 / 9, children: [
child: NativeVideoPlayerView( if (placeholder != null) placeholder!,
onViewReady: _initController, const Positioned.fill(
child: Center(
child: DelayedLoadingIndicator(
fadeInDuration: Duration(milliseconds: 500),
),
),
),
],
),
),
],
),
), ),
), ),
), ),
); );
} }
// final Asset asset;
// final Widget? placeholder;
// final Duration hideControlsTimer;
// final bool showControls;
// final bool showDownloadingIndicator;
// final bool loopVideo;
} }

View file

@ -1,5 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:native_video_player/native_video_player.dart';
final nativePlayerControllerProvider =
StateProvider((ref) => NativeVideoPlayerController(0));

View file

@ -1,4 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:native_video_player/native_video_player.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
enum VideoPlaybackState { enum VideoPlaybackState {
@ -29,6 +30,32 @@ class VideoPlaybackValue {
required this.volume, required this.volume,
}); });
factory VideoPlaybackValue.fromNativeController(
NativeVideoPlayerController controller,
) {
final playbackInfo = controller.playbackInfo;
final videoInfo = controller.videoInfo;
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;
}
return VideoPlaybackValue(
position: Duration(seconds: playbackInfo?.position ?? 0),
duration: Duration(seconds: videoInfo?.duration ?? 0),
state: s,
volume: playbackInfo?.volume ?? 0.0,
);
}
factory VideoPlaybackValue.fromController(VideoPlayerController? controller) { factory VideoPlaybackValue.fromController(VideoPlayerController? controller) {
final video = controller?.value; final video = controller?.value;
late VideoPlaybackState s; late VideoPlaybackState s;

View file

@ -4,9 +4,9 @@ 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/show_controls.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
import 'package:immich_mobile/utils/hooks/timer_hook.dart';
import 'package:immich_mobile/widgets/asset_viewer/center_play_button.dart'; import 'package:immich_mobile/widgets/asset_viewer/center_play_button.dart';
import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
import 'package:immich_mobile/utils/hooks/timer_hook.dart';
class CustomVideoPlayerControls extends HookConsumerWidget { class CustomVideoPlayerControls extends HookConsumerWidget {
final Duration hideTimerDuration; final Duration hideTimerDuration;
@ -86,12 +86,8 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
) )
else else
GestureDetector( GestureDetector(
onTap: () { onTap: () =>
if (state != VideoPlaybackState.playing) { ref.read(showControlsProvider.notifier).show = false,
togglePlay();
}
ref.read(showControlsProvider.notifier).show = false;
},
child: CenterPlayButton( child: CenterPlayButton(
backgroundColor: Colors.black54, backgroundColor: Colors.black54,
iconColor: Colors.white, iconColor: Colors.white,

View file

@ -1,63 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:native_video_player/native_video_player.dart';
import 'package:video_player/video_player.dart';
class NativeVideoPlayer extends HookConsumerWidget {
final VideoPlayerController controller;
final bool isMotionVideo;
final Widget? placeholder;
final Duration hideControlsTimer;
final bool showControls;
final bool showDownloadingIndicator;
final bool loopVideo;
final Asset asset;
const NativeVideoPlayer({
super.key,
required this.controller,
required this.isMotionVideo,
this.placeholder,
required this.hideControlsTimer,
required this.showControls,
required this.showDownloadingIndicator,
required this.loopVideo,
required this.asset,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return NativeVideoPlayerView(
onViewReady: (controller) async {
try {
String path = '';
VideoSourceType type = VideoSourceType.file;
if (asset.isLocal && asset.livePhotoVideoId == null) {
// Use a local file for the video player controller
final file = await asset.local!.file;
if (file == null) {
throw Exception('No file found for the video');
}
path = file.path;
type = VideoSourceType.file;
final videoSource = await VideoSource.init(
path: path,
type: type,
);
await controller.loadVideoSource(videoSource);
await controller.play();
Future.delayed(const Duration(milliseconds: 100), () async {
await controller.setVolume(0.5);
});
}
} catch (e) {
print('Error loading video: $e');
}
},
);
}
}

View file

@ -2,9 +2,9 @@ import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/pages/common/video_viewer.page.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; import 'package:immich_mobile/utils/hooks/blurhash_hook.dart';
import 'package:immich_mobile/widgets/common/immich_image.dart'; import 'package:immich_mobile/widgets/common/immich_image.dart';
@ -68,10 +68,9 @@ class MemoryCard extends StatelessWidget {
} else { } else {
return Hero( return Hero(
tag: 'memory-${asset.id}', tag: 'memory-${asset.id}',
child: VideoViewerPage( child: NativeVideoViewerPage(
key: ValueKey(asset), key: ValueKey(asset),
asset: asset, asset: asset,
showDownloadingIndicator: false,
placeholder: SizedBox.expand( placeholder: SizedBox.expand(
child: ImmichImage( child: ImmichImage(
asset, asset,

View file

@ -1027,10 +1027,11 @@ packages:
native_video_player: native_video_player:
dependency: "direct main" dependency: "direct main"
description: description:
name: native_video_player path: "."
sha256: "8df92df138c13ebf9df6b30525f9c4198534705fd450a98da14856d3a0e48cd4" ref: "feat/headers"
url: "https://pub.dev" resolved-ref: "568c76e1552791f06dcf44b45d3373cad12913ed"
source: hosted url: "https://github.com/immich-app/native_video_player"
source: git
version: "1.3.1" version: "1.3.1"
nested: nested:
dependency: transitive dependency: transitive

View file

@ -64,7 +64,10 @@ dependencies:
async: ^2.11.0 async: ^2.11.0
dynamic_color: ^1.7.0 #package to apply system theme dynamic_color: ^1.7.0 #package to apply system theme
background_downloader: ^8.5.5 background_downloader: ^8.5.5
native_video_player: ^1.3.1 native_video_player:
git:
url: https://github.com/immich-app/native_video_player
ref: feat/headers
#image editing packages #image editing packages
crop_image: ^1.0.13 crop_image: ^1.0.13