Files
immich/mobile/lib/pages/common/native_video_loader.dart
2024-11-03 19:42:46 -05:00

170 lines
5.4 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/asset.service.dart';
import 'package:logging/logging.dart';
import 'package:native_video_player/native_video_player.dart';
import 'package:photo_manager/photo_manager.dart';
class NativeVideoLoader extends HookConsumerWidget {
final Asset asset;
final bool isMotionVideo;
final Widget placeholder;
final bool showControls;
final Duration hideControlsTimer;
final bool loopVideo;
const NativeVideoLoader({
super.key,
required this.asset,
required this.placeholder,
this.isMotionVideo = false,
this.showControls = true,
this.hideControlsTimer = const Duration(seconds: 5),
this.loopVideo = false,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
// fast path for aspect ratio
final initAspectRatio = useMemoized(
() {
if (asset.exifInfo == null) {
return null;
}
final width = asset.orientatedWidth?.toDouble();
final height = asset.orientatedHeight?.toDouble();
return width != null && height != null && width > 0 && height > 0
? width / height
: null;
},
);
final localEntity = useMemoized(
() => asset.localId != null ? AssetEntity.fromId(asset.localId!) : null,
);
Future<double> calculateAspectRatio() async {
late final double? orientatedWidth;
late final double? orientatedHeight;
if (asset.exifInfo != null) {
orientatedWidth = asset.orientatedWidth?.toDouble();
orientatedHeight = asset.orientatedHeight?.toDouble();
} else if (localEntity != null) {
final entity = await localEntity;
orientatedWidth = entity?.orientatedWidth.toDouble();
orientatedHeight = entity?.orientatedHeight.toDouble();
} else {
final entity = await ref.read(assetServiceProvider).loadExif(asset);
orientatedWidth = entity.orientatedWidth?.toDouble();
orientatedHeight = entity.orientatedHeight?.toDouble();
}
if (orientatedWidth != null &&
orientatedHeight != null &&
orientatedWidth > 0 &&
orientatedHeight > 0) {
return orientatedWidth / orientatedHeight;
}
return 1.0;
}
final aspectRatioFuture = useMemoized(
() => initAspectRatio == null ? calculateAspectRatio() : null,
);
final log = Logger('NativeVideoLoader');
log.info('Building NativeVideoLoader');
Future<VideoSource> createLocalSource() async {
log.info('Loading video from local storage');
final entity = await localEntity;
if (entity == null) {
throw Exception('No entity found for the video');
}
final file = await entity.file;
if (file == null) {
throw Exception('No file found for the video');
}
return await VideoSource.init(
path: file.path,
type: VideoSourceType.file,
);
}
Future<VideoSource> createRemoteSource() async {
log.info('Loading video from server');
// 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';
return await VideoSource.init(
path: videoUrl,
type: VideoSourceType.network,
headers: ApiService.getRequestHeaders(),
);
}
Future<VideoSource> createSource(Asset asset) async {
if (asset.isLocal && asset.livePhotoVideoId == null) {
return createLocalSource();
}
return createRemoteSource();
}
final size = MediaQuery.sizeOf(context);
return SizedBox(
height: size.height,
width: size.width,
child: GestureDetector(
behavior: HitTestBehavior.deferToChild,
child: PopScope(
onPopInvokedWithResult: (didPop, _) => ref
.read(videoPlaybackValueProvider.notifier)
.value = VideoPlaybackValue.uninitialized(),
child: SizedBox(
height: size.height,
width: size.width,
child: FutureBuilder(
key: ValueKey(asset.id),
future: aspectRatioFuture,
initialData: initAspectRatio,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return placeholder;
}
return NativeVideoViewerPage(
key: ValueKey(asset.id),
videoSource: createSource(asset),
duration: asset.duration,
aspectRatio: snapshot.data as double,
isMotionVideo: isMotionVideo,
hideControlsTimer: hideControlsTimer,
loopVideo: loopVideo,
);
},
),
),
),
),
);
}
}