1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-17 01:06:46 +01:00

make aspect ratio logic reusable, optimizations

This commit is contained in:
mertalev 2024-11-17 10:45:19 -05:00
parent 60715059f7
commit 4d1d902773
No known key found for this signature in database
GPG key ID: CA85EF6600C9E8AD
4 changed files with 166 additions and 115 deletions

View file

@ -89,6 +89,34 @@ class Asset {
set local(AssetEntity? assetEntity) => _local = assetEntity; set local(AssetEntity? assetEntity) => _local = assetEntity;
@ignore
bool _didUpdateLocal = false;
@ignore
bool get didUpdateLocal => _didUpdateLocal;
Future<AssetEntity> get localAsync async {
final currentLocal = local;
if (currentLocal == null) {
throw Exception('Asset $fileName has no local data');
}
if (_didUpdateLocal) {
return currentLocal;
}
final updatedLocal = _didUpdateLocal
? currentLocal
: await currentLocal.obtainForNewProperties();
if (updatedLocal == null) {
throw Exception('Could not fetch local data for $fileName');
}
local = updatedLocal;
_didUpdateLocal = true;
return updatedLocal;
}
Id id = Isar.autoIncrement; Id id = Isar.autoIncrement;
/// stores the raw SHA1 bytes as a base64 String /// stores the raw SHA1 bytes as a base64 String
@ -147,9 +175,36 @@ class Asset {
int stackCount; int stackCount;
/// Aspect ratio of the asset /// Aspect ratio of the asset
/// Returns null if the asset has no sync access to the exif info
@ignore @ignore
double? get aspectRatio => double? get aspectRatio {
width == null || height == null ? 0 : width! / height!; late final double? orientatedWidth;
late final double? orientatedHeight;
if (exifInfo != null) {
orientatedWidth = this.orientatedWidth?.toDouble();
orientatedHeight = this.orientatedHeight?.toDouble();
} else if (didUpdateLocal) {
final currentLocal = local;
if (currentLocal == null) {
throw Exception('Asset $fileName has no local data');
}
orientatedWidth = currentLocal.orientatedWidth.toDouble();
orientatedHeight = currentLocal.orientatedHeight.toDouble();
} else {
orientatedWidth = null;
orientatedHeight = null;
}
if (orientatedWidth != null &&
orientatedHeight != null &&
orientatedWidth > 0 &&
orientatedHeight > 0) {
return orientatedWidth / orientatedHeight;
}
return null;
}
/// `true` if this [Asset] is present on the device /// `true` if this [Asset] is present on the device
@ignore @ignore

View file

@ -42,7 +42,7 @@ class GalleryStackedChildren extends HookConsumerWidget {
} }
return Padding( return Padding(
key: ValueKey(currentAsset), key: ValueKey(currentAsset.id),
padding: const EdgeInsets.only(right: 5), padding: const EdgeInsets.only(right: 5),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {

View file

@ -49,9 +49,7 @@ class NativeVideoViewerPage extends HookConsumerWidget {
final lastVideoPosition = useRef(-1); final lastVideoPosition = useRef(-1);
final isBuffering = useRef(false); final isBuffering = useRef(false);
if (isPlayingMotionVideo != null) { useListenable(isPlayingMotionVideo);
useListenable(isPlayingMotionVideo);
}
final showMotionVideo = final showMotionVideo =
isPlayingMotionVideo != null && isPlayingMotionVideo!.value; isPlayingMotionVideo != null && isPlayingMotionVideo!.value;
@ -62,124 +60,67 @@ class NativeVideoViewerPage extends HookConsumerWidget {
final currentAsset = useState(ref.read(currentAssetProvider)); final currentAsset = useState(ref.read(currentAssetProvider));
final isCurrent = currentAsset.value == asset; final isCurrent = currentAsset.value == asset;
// used to show the placeholder during hero animations for remote videos to avoid a stutter // Used to show the placeholder during hero animations for remote videos to avoid a stutter
final isVisible = useState(asset.isLocal || asset.isMotionPhoto); final isVisible = useState(asset.isLocal || asset.isMotionPhoto);
final log = Logger('NativeVideoViewerPage'); final log = Logger('NativeVideoViewerPage');
final localEntity = useMemoized(() {
if (!asset.isLocal || asset.isMotionPhoto) {
return null;
}
final local = asset.local;
if (local == null || local.orientation > 0) {
return Future.value(local);
}
return local.obtainForNewProperties();
});
Future<double?> calculateAspectRatio() async {
if (!context.mounted) {
return null;
}
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;
if (entity != null) {
asset.local = entity;
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;
}
Future<VideoSource?> createSource() async { Future<VideoSource?> createSource() async {
if (!context.mounted) { if (!context.mounted) {
return null; return null;
} }
if (localEntity != null) { try {
final file = await (await localEntity)!.file; final local = asset.local;
if (file == null) { if (local != null && !asset.isMotionPhoto) {
throw Exception('No file found for the video'); final file = await local.file;
if (file == null) {
throw Exception('No file found for the video');
}
final source = await VideoSource.init(
path: file.path,
type: VideoSourceType.file,
);
return source;
} }
// 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';
final source = await VideoSource.init( final source = await VideoSource.init(
path: file.path, path: videoUrl,
type: VideoSourceType.file, type: VideoSourceType.network,
headers: ApiService.getRequestHeaders(),
); );
return source; return source;
} catch (error) {
log.severe(
'Error creating video source for asset ${asset.fileName}: $error',
);
return null;
} }
// 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';
final source = await VideoSource.init(
path: videoUrl,
type: VideoSourceType.network,
headers: ApiService.getRequestHeaders(),
);
return source;
} }
final videoSource = useState<VideoSource?>(null); final videoSource = useMemoized<Future<VideoSource?>>(() => createSource());
final aspectRatio = useState<double?>(null); final aspectRatio = useState<double?>(asset.aspectRatio);
useMemoized( useMemoized(
() async { () async {
if (!context.mounted) { if (!context.mounted || aspectRatio.value != null) {
return null; return null;
} }
late final VideoSource? videoSourceRes;
late final double? aspectRatioRes;
try { try {
(videoSourceRes, aspectRatioRes) = aspectRatio.value =
await (createSource(), calculateAspectRatio()).wait; await ref.read(assetServiceProvider).getAspectRatio(asset);
} catch (error) { } catch (error) {
log.severe( log.severe(
'Error initializing video for asset ${asset.fileName}: $error', 'Error getting aspect ratio for asset ${asset.fileName}: $error',
);
return;
}
if (videoSourceRes == null || aspectRatioRes == null) {
return;
}
// if opening a remote video from a hero animation, delay visibility to avoid a stutter
if (!asset.isLocal && isCurrent) {
Timer(
const Duration(milliseconds: 200),
() => isVisible.value = true,
); );
} }
videoSource.value = videoSourceRes;
aspectRatio.value = aspectRatioRes;
}, },
); );
@ -197,7 +138,7 @@ class NativeVideoViewerPage extends HookConsumerWidget {
} }
} }
// timer to mark videos as buffering if the position does not change // 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 // When the volume changes, set the volume
@ -286,7 +227,11 @@ class NativeVideoViewerPage extends HookConsumerWidget {
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback; ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
try { try {
await videoController.play(); if (asset.isVideo ||
isPlayingMotionVideo == null ||
isPlayingMotionVideo!.value) {
await videoController.play();
}
await videoController.setVolume(0.9); await videoController.setVolume(0.9);
} catch (error) { } catch (error) {
log.severe('Error playing video: $error'); log.severe('Error playing video: $error');
@ -362,6 +307,24 @@ class NativeVideoViewerPage extends HookConsumerWidget {
} }
} }
void onToggleMotionVideo() async {
final videoController = controller.value;
if (videoController == null || !context.mounted) {
return;
}
try {
if (isPlayingMotionVideo!.value) {
await videoController.seekTo(0);
await videoController.play();
} else {
await videoController.pause();
}
} catch (error) {
log.severe('Error toggling motion video: $error');
}
}
void removeListeners(NativeVideoPlayerController controller) { void removeListeners(NativeVideoPlayerController controller) {
controller.onPlaybackPositionChanged controller.onPlaybackPositionChanged
.removeListener(onPlaybackPositionChanged); .removeListener(onPlaybackPositionChanged);
@ -371,19 +334,24 @@ class NativeVideoViewerPage extends HookConsumerWidget {
controller.onPlaybackEnded.removeListener(onPlaybackEnded); controller.onPlaybackEnded.removeListener(onPlaybackEnded);
} }
void initController(NativeVideoPlayerController nc) { void initController(NativeVideoPlayerController nc) async {
if (controller.value != null) { if (controller.value != null) {
return; return;
} }
ref.read(videoPlayerControlsProvider.notifier).reset(); ref.read(videoPlayerControlsProvider.notifier).reset();
ref.read(videoPlaybackValueProvider.notifier).reset(); ref.read(videoPlaybackValueProvider.notifier).reset();
final source = await videoSource;
if (source == null) {
return;
}
nc.onPlaybackPositionChanged.addListener(onPlaybackPositionChanged); nc.onPlaybackPositionChanged.addListener(onPlaybackPositionChanged);
nc.onPlaybackStatusChanged.addListener(onPlaybackStatusChanged); nc.onPlaybackStatusChanged.addListener(onPlaybackStatusChanged);
nc.onPlaybackReady.addListener(onPlaybackReady); nc.onPlaybackReady.addListener(onPlaybackReady);
nc.onPlaybackEnded.addListener(onPlaybackEnded); nc.onPlaybackEnded.addListener(onPlaybackEnded);
nc.loadVideoSource(videoSource.value!);
nc.loadVideoSource(source);
controller.value = nc; controller.value = nc;
Timer(const Duration(milliseconds: 200), checkIfBuffering); Timer(const Duration(milliseconds: 200), checkIfBuffering);
} }
@ -399,7 +367,7 @@ class NativeVideoViewerPage extends HookConsumerWidget {
return; return;
} }
// no need to delay video playback when swiping from an image to a video // No need to delay video playback when swiping from an image to a video
if (curAsset != null && !curAsset.isVideo) { if (curAsset != null && !curAsset.isVideo) {
currentAsset.value = value; currentAsset.value = value;
onPlaybackReady(); onPlaybackReady();
@ -421,7 +389,24 @@ class NativeVideoViewerPage extends HookConsumerWidget {
useEffect( useEffect(
() { () {
// If opening a remote video from a hero animation, delay visibility to avoid a stutter
final timer = isVisible.value
? null
: Timer(
const Duration(milliseconds: 300),
() => isVisible.value = true,
);
if (isPlayingMotionVideo != null) {
isPlayingMotionVideo!.addListener(onToggleMotionVideo);
}
return () { return () {
timer?.cancel();
if (isPlayingMotionVideo != null) {
isPlayingMotionVideo!.removeListener(onToggleMotionVideo);
}
final playerController = controller.value; final playerController = controller.value;
if (playerController == null) { if (playerController == null) {
return; return;
@ -442,23 +427,24 @@ class NativeVideoViewerPage extends HookConsumerWidget {
// This remains under the video to avoid flickering // This remains under the video to avoid flickering
// For motion videos, this is the image portion of the asset // For motion videos, this is the image portion of the asset
Center(key: ValueKey(asset.id), child: image), Center(key: ValueKey(asset.id), child: image),
Visibility.maintain( if (aspectRatio.value != null)
key: ValueKey(asset), Visibility.maintain(
visible: (asset.isVideo || showMotionVideo) && isVisible.value,
child: Center(
key: ValueKey(asset), key: ValueKey(asset),
child: AspectRatio( visible: (asset.isVideo || showMotionVideo) && isVisible.value,
child: Center(
key: ValueKey(asset), key: ValueKey(asset),
aspectRatio: aspectRatio.value!, child: AspectRatio(
child: isCurrent key: ValueKey(asset),
? NativeVideoPlayerView( aspectRatio: aspectRatio.value!,
key: ValueKey(asset), child: isCurrent
onViewReady: initController, ? NativeVideoPlayerView(
) key: ValueKey(asset),
: null, onViewReady: initController,
)
: null,
),
), ),
), ),
),
if (showControls) const Center(child: CustomVideoPlayerControls()), if (showControls) const Center(child: CustomVideoPlayerControls()),
], ],
); );

View file

@ -402,4 +402,14 @@ class AssetService {
return exifInfo?.description ?? ""; return exifInfo?.description ?? "";
} }
Future<double> getAspectRatio(Asset asset) async {
if (asset.isLocal) {
await asset.localAsync;
} else if (asset.isRemote) {
asset = await loadExif(asset);
}
return asset.aspectRatio ?? 1.0;
}
} }