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

feat(mobile): native_video_player #12104

Merged
merged 55 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
520ab6b
add native player library
alextran1502 Aug 9, 2024
28f1ca1
splitup the player
alextran1502 Aug 9, 2024
f336239
stateful widget
alextran1502 Aug 10, 2024
4010481
refactor: native_video_player
shenlong-tanwen Aug 28, 2024
57d1f78
fix: handle buffering
shenlong-tanwen Aug 29, 2024
01e7274
turn on volume when video plays
alextran1502 Aug 29, 2024
81ae5e4
fix: aspect ratio
shenlong-tanwen Sep 10, 2024
a2933d2
fix: handle remote asset orientation
shenlong-tanwen Oct 1, 2024
ef76510
refinements and fixes
mertalev Nov 7, 2024
026e7f7
clean up logging
mertalev Nov 7, 2024
625b0bb
refactor aspect ratio calculation
mertalev Nov 7, 2024
6a6e973
removed unnecessary import
mertalev Nov 7, 2024
d50b889
transitive dependencies
alextran1502 Nov 7, 2024
0a142ea
fixed referencing uninitialized orientation
mertalev Nov 7, 2024
49af664
use correct ref to build android
alextran1502 Nov 8, 2024
bd69a94
higher res placeholder for local videos
mertalev Nov 9, 2024
59434b6
slightly lower delay
mertalev Nov 9, 2024
6a8ae25
await things
mertalev Nov 9, 2024
b3e7b3a
fix controls when swiping between image and video
mertalev Nov 9, 2024
a91d261
linting
mertalev Nov 9, 2024
3e0e813
extra smooth seeking, add comments
mertalev Nov 12, 2024
8599ec2
chore: generate router page
alextran1502 Nov 14, 2024
ed3152f
use current asset provider and loadAsset
mertalev Nov 15, 2024
da4bf27
fix stack handling
mertalev Nov 16, 2024
e37378b
improved motion photo handling
mertalev Nov 17, 2024
e9b1967
use visibility for motion videos
mertalev Nov 17, 2024
a76e8c5
error handling for async calls
mertalev Nov 17, 2024
05d69f7
fix duplicate key error
mertalev Nov 17, 2024
6b2ebf5
maybe fix duplicate key error
mertalev Nov 17, 2024
946672b
increase delay for hero animation
mertalev Nov 17, 2024
1a7904f
faster initialization for remote videos
mertalev Nov 17, 2024
10f17bf
ensure dimensions for memory cards
mertalev Nov 17, 2024
74ac655
make aspect ratio logic reusable, optimizations
mertalev Nov 17, 2024
beffb92
refactor: move exif search from aspect ratio to orientation
mertalev Nov 17, 2024
7ff0066
local orientation on ios is unreliable; prefer remote
mertalev Nov 17, 2024
ce7e215
fix no audio in silent mode on ios
mertalev Nov 17, 2024
d82993b
increase bottom bar opacity to account for hdr
mertalev Nov 17, 2024
6c45d04
remove unused import
mertalev Nov 17, 2024
238ca98
fix live photo play button not updating
mertalev Nov 18, 2024
34bdf56
fix map marker -> galleryviewer
mertalev Nov 18, 2024
7fd2c77
remove video_player
mertalev Nov 22, 2024
2e8dddb
fix hdr playback on android
mertalev Dec 1, 2024
62d7909
fix looping
mertalev Dec 1, 2024
37eb004
remove unused dependencies
mertalev Dec 1, 2024
0f7a470
update to latest player commit
mertalev Dec 1, 2024
bae5239
fix player controls hiding when video is not playing
mertalev Dec 3, 2024
cfaa4f9
fix restart video
mertalev Dec 3, 2024
71660ab
stop showing motion video after ending when looping is disabled
mertalev Dec 3, 2024
b76f81b
delay video initialization to avoid placeholder flicker
mertalev Dec 4, 2024
fecfce6
faster animation
mertalev Dec 4, 2024
1415fc9
shorter delay
mertalev Dec 4, 2024
80a89ee
small delay for image -> video on android
mertalev Dec 4, 2024
4c95f6d
Merge branch 'main' into mobile/native-video-player
alextran1502 Dec 4, 2024
c562def
fix: lint
alextran1502 Dec 4, 2024
0dc3ae0
hide stacked children when controls are hidden, avoid bottom bar drop…
mertalev Dec 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions mobile/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ if (keystorePropertiesFile.exists()) {
}

android {
compileSdkVersion 34
compileSdkVersion 35

compileOptions {
sourceCompatibility JavaVersion.VERSION_17
Expand All @@ -47,7 +47,7 @@ android {
defaultConfig {
applicationId "app.alextran.immich"
minSdkVersion 26
targetSdkVersion 34
targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
Expand Down
2 changes: 1 addition & 1 deletion mobile/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
android:value="true" />

<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
Expand Down
4 changes: 2 additions & 2 deletions mobile/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ subprojects {
if (project.plugins.hasPlugin("com.android.application") ||
project.plugins.hasPlugin("com.android.library")) {
project.android {
compileSdkVersion 34
buildToolsVersion "34.0.0"
compileSdkVersion 35
buildToolsVersion "35.0.0"
}
}
}
Expand Down
19 changes: 9 additions & 10 deletions mobile/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ PODS:
- maplibre_gl (0.0.1):
- Flutter
- MapLibre (= 5.14.0-pre3)
- native_video_player (1.0.0):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
Expand Down Expand Up @@ -93,9 +95,6 @@ PODS:
- Toast (4.0.0)
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
- wakelock_plus (0.0.1):
- Flutter

Expand All @@ -115,6 +114,7 @@ DEPENDENCIES:
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
Expand All @@ -124,7 +124,6 @@ DEPENDENCIES:
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)

SPEC REPOS:
Expand Down Expand Up @@ -168,6 +167,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/isar_flutter_libs/ios"
maplibre_gl:
:path: ".symlinks/plugins/maplibre_gl/ios"
native_video_player:
:path: ".symlinks/plugins/native_video_player/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
Expand All @@ -186,15 +187,13 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqflite/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"

SPEC CHECKSUMS:
background_downloader: 9f788ffc5de45acf87d6380e91ca0841066c18cf
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Expand All @@ -210,20 +209,20 @@ SPEC CHECKSUMS:
isar_flutter_libs: fdf730ca925d05687f36d7f1d355e482529ed097
MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef
maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
native_video_player: d12af78a1a4a8cf09775a5177d5b392def6fd23c
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1

PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
Expand Down
75 changes: 43 additions & 32 deletions mobile/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,48 +1,59 @@
import UIKit
import shared_preferences_foundation
import Flutter
import BackgroundTasks
import Flutter
import UIKit
import path_provider_ios
import photo_manager
import permission_handler_apple
import photo_manager
import shared_preferences_foundation

@main
@objc class AppDelegate: FlutterAppDelegate {

override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {

// Required for flutter_local_notification
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
// Required for flutter_local_notification
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}

do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("Failed to set audio session category. Error: \(error)")
}

GeneratedPluginRegistrant.register(with: self)
BackgroundServicePlugin.registerBackgroundProcessing()

BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)

BackgroundServicePlugin.setPluginRegistrantCallback { registry in
if !registry.hasPlugin("org.cocoapods.path-provider-ios") {
FLTPathProviderPlugin.register(
with: registry.registrar(forPlugin: "org.cocoapods.path-provider-ios")!)
}

if !registry.hasPlugin("org.cocoapods.photo-manager") {
PhotoManagerPlugin.register(
with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!)
}

GeneratedPluginRegistrant.register(with: self)
BackgroundServicePlugin.registerBackgroundProcessing()

BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)

BackgroundServicePlugin.setPluginRegistrantCallback { registry in
if !registry.hasPlugin("org.cocoapods.path-provider-ios") {
FLTPathProviderPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.path-provider-ios")!)
}

if !registry.hasPlugin("org.cocoapods.photo-manager") {
PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!)
}

if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!)
}

if !registry.hasPlugin("org.cocoapods.permission-handler-apple") {
PermissionHandlerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!)
}
if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") {
SharedPreferencesPlugin.register(
with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!)
}

return super.application(application, didFinishLaunchingWithOptions: launchOptions)

if !registry.hasPlugin("org.cocoapods.permission-handler-apple") {
PermissionHandlerPlugin.register(
with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!)
}
}

return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

}
2 changes: 1 addition & 1 deletion mobile/lib/constants/immich_colors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const String defaultColorPresetName = "indigo";
const Color immichBrandColorLight = Color(0xFF4150AF);
const Color immichBrandColorDark = Color(0xFFACCBFA);
const Color whiteOpacity75 = Color.fromARGB((0.75 * 255) ~/ 1, 255, 255, 255);
const Color blackOpacity90 = Color.fromARGB((0.90 * 255) ~/ 1, 0, 0, 0);
const Color red400 = Color(0xFFEF5350);
const Color grey200 = Color(0xFFEEEEEE);

final Map<ImmichColorPreset, ImmichTheme> _themePresetsMap = {
ImmichColorPreset.indigo: ImmichTheme(
Expand Down
115 changes: 88 additions & 27 deletions mobile/lib/entities/asset.entity.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:io';

import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/utils/hash.dart';
Expand All @@ -22,12 +23,8 @@ class Asset {
durationInSeconds = remote.duration.toDuration()?.inSeconds ?? 0,
type = remote.type.toAssetType(),
fileName = remote.originalFileName,
height = isFlipped(remote)
? remote.exifInfo?.exifImageWidth?.toInt()
: remote.exifInfo?.exifImageHeight?.toInt(),
width = isFlipped(remote)
? remote.exifInfo?.exifImageHeight?.toInt()
: remote.exifInfo?.exifImageWidth?.toInt(),
height = remote.exifInfo?.exifImageHeight?.toInt(),
width = remote.exifInfo?.exifImageWidth?.toInt(),
livePhotoVideoId = remote.livePhotoVideoId,
ownerId = fastHash(remote.ownerId),
exifInfo =
Expand Down Expand Up @@ -93,6 +90,27 @@ class Asset {

set local(AssetEntity? assetEntity) => _local = assetEntity;

@ignore
bool _didUpdateLocal = false;

@ignore
Future<AssetEntity> get localAsync async {
final local = this.local;
if (local == null) {
throw Exception('Asset $fileName has no local data');
}

final updatedLocal =
_didUpdateLocal ? local : await local.obtainForNewProperties();
if (updatedLocal == null) {
throw Exception('Could not fetch local data for $fileName');
}

this.local = updatedLocal;
_didUpdateLocal = true;
return updatedLocal;
}

Id id = Isar.autoIncrement;

/// stores the raw SHA1 bytes as a base64 String
Expand Down Expand Up @@ -150,10 +168,21 @@ class Asset {

int stackCount;

/// Aspect ratio of the asset
/// Returns null if the asset has no sync access to the exif info
@ignore
double? get aspectRatio =>
width == null || height == null ? 0 : width! / height!;
double? get aspectRatio {
final orientatedWidth = this.orientatedWidth;
final orientatedHeight = this.orientatedHeight;

if (orientatedWidth != null &&
orientatedHeight != null &&
orientatedWidth > 0 &&
orientatedHeight > 0) {
return orientatedWidth.toDouble() / orientatedHeight.toDouble();
}

return null;
}

/// `true` if this [Asset] is present on the device
@ignore
Expand All @@ -172,6 +201,12 @@ class Asset {
@ignore
bool get isImage => type == AssetType.image;

@ignore
bool get isVideo => type == AssetType.video;

@ignore
bool get isMotionPhoto => livePhotoVideoId != null;

@ignore
AssetState get storage {
if (isRemote && isLocal) {
Expand All @@ -192,6 +227,50 @@ class Asset {
@ignore
set byteHash(List<int> hash) => checksum = base64.encode(hash);

/// Returns null if the asset has no sync access to the exif info
@ignore
@pragma('vm:prefer-inline')
bool? get isFlipped {
final exifInfo = this.exifInfo;
if (exifInfo != null) {
return exifInfo.isFlipped;
}

if (_didUpdateLocal && Platform.isAndroid) {
final local = this.local;
if (local == null) {
throw Exception('Asset $fileName has no local data');
}
return local.orientation == 90 || local.orientation == 270;
}

return null;
}

/// Returns null if the asset has no sync access to the exif info
@ignore
@pragma('vm:prefer-inline')
int? get orientatedHeight {
final isFlipped = this.isFlipped;
if (isFlipped == null) {
return null;
}

return isFlipped ? width : height;
}

/// Returns null if the asset has no sync access to the exif info
@ignore
@pragma('vm:prefer-inline')
int? get orientatedWidth {
final isFlipped = this.isFlipped;
if (isFlipped == null) {
return null;
}

return isFlipped ? height : width;
}

@override
bool operator ==(other) {
if (other is! Asset) return false;
Expand Down Expand Up @@ -511,21 +590,3 @@ extension AssetsHelper on IsarCollection<Asset> {
return where().anyOf(ids, (q, String e) => q.localIdEqualTo(e));
}
}

/// Returns `true` if this [int] is flipped 90° clockwise
bool isRotated90CW(int orientation) {
return [7, 8, -90].contains(orientation);
}

/// Returns `true` if this [int] is flipped 270° clockwise
bool isRotated270CW(int orientation) {
return [5, 6, 90].contains(orientation);
}

/// Returns `true` if this [Asset] is flipped 90° or 270° clockwise
bool isFlipped(AssetResponseDto response) {
final int orientation =
int.tryParse(response.exifInfo?.orientation ?? '0') ?? 0;
return orientation != 0 &&
(isRotated90CW(orientation) || isRotated270CW(orientation));
}
Loading
Loading