Skip to content

Commit dfe6d27

Browse files
feat: sqlite video player (#19792)
* feat: video player * use remote asset id in local query * fix: error from pre-caching beyond total assets * fix: flipped local videos * incorrect aspect ratio on iOS * ignore other storage id during equals check --------- Co-authored-by: shenlong-tanwen <[email protected]>
1 parent 51ab749 commit dfe6d27

31 files changed

+832
-82
lines changed

mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ data class PlatformAsset (
8787
val updatedAt: Long? = null,
8888
val width: Long? = null,
8989
val height: Long? = null,
90-
val durationInSeconds: Long
90+
val durationInSeconds: Long,
91+
val orientation: Long
9192
)
9293
{
9394
companion object {
@@ -100,7 +101,8 @@ data class PlatformAsset (
100101
val width = pigeonVar_list[5] as Long?
101102
val height = pigeonVar_list[6] as Long?
102103
val durationInSeconds = pigeonVar_list[7] as Long
103-
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds)
104+
val orientation = pigeonVar_list[8] as Long
105+
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation)
104106
}
105107
}
106108
fun toList(): List<Any?> {
@@ -113,6 +115,7 @@ data class PlatformAsset (
113115
width,
114116
height,
115117
durationInSeconds,
118+
orientation,
116119
)
117120
}
118121
override fun equals(other: Any?): Boolean {

mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ open class NativeSyncApiImplBase(context: Context) {
4040
MediaStore.MediaColumns.BUCKET_ID,
4141
MediaStore.MediaColumns.WIDTH,
4242
MediaStore.MediaColumns.HEIGHT,
43-
MediaStore.MediaColumns.DURATION
43+
MediaStore.MediaColumns.DURATION,
44+
MediaStore.MediaColumns.ORIENTATION,
4445
)
4546

4647
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
@@ -74,6 +75,8 @@ open class NativeSyncApiImplBase(context: Context) {
7475
val widthColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
7576
val heightColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT)
7677
val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
78+
val orientationColumn =
79+
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
7780

7881
while (c.moveToNext()) {
7982
val id = c.getLong(idColumn).toString()
@@ -101,6 +104,7 @@ open class NativeSyncApiImplBase(context: Context) {
101104
val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0
102105
else c.getLong(durationColumn) / 1000
103106
val bucketId = c.getString(bucketIdColumn)
107+
val orientation = c.getInt(orientationColumn)
104108

105109
val asset = PlatformAsset(
106110
id,
@@ -110,7 +114,8 @@ open class NativeSyncApiImplBase(context: Context) {
110114
modifiedAt,
111115
width,
112116
height,
113-
duration
117+
duration,
118+
orientation.toLong(),
114119
)
115120
yield(AssetResult.ValidAsset(asset, bucketId))
116121
}

mobile/drift_schemas/main/drift_schema_v1.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mobile/ios/Runner/Sync/Messages.g.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ struct PlatformAsset: Hashable {
138138
var width: Int64? = nil
139139
var height: Int64? = nil
140140
var durationInSeconds: Int64
141+
var orientation: Int64
141142

142143

143144
// swift-format-ignore: AlwaysUseLowerCamelCase
@@ -150,6 +151,7 @@ struct PlatformAsset: Hashable {
150151
let width: Int64? = nilOrValue(pigeonVar_list[5])
151152
let height: Int64? = nilOrValue(pigeonVar_list[6])
152153
let durationInSeconds = pigeonVar_list[7] as! Int64
154+
let orientation = pigeonVar_list[8] as! Int64
153155

154156
return PlatformAsset(
155157
id: id,
@@ -159,7 +161,8 @@ struct PlatformAsset: Hashable {
159161
updatedAt: updatedAt,
160162
width: width,
161163
height: height,
162-
durationInSeconds: durationInSeconds
164+
durationInSeconds: durationInSeconds,
165+
orientation: orientation
163166
)
164167
}
165168
func toList() -> [Any?] {
@@ -172,6 +175,7 @@ struct PlatformAsset: Hashable {
172175
width,
173176
height,
174177
durationInSeconds,
178+
orientation,
175179
]
176180
}
177181
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {

mobile/ios/Runner/Sync/MessagesImpl.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ extension PHAsset {
2727
updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) },
2828
width: Int64(pixelWidth),
2929
height: Int64(pixelHeight),
30-
durationInSeconds: Int64(duration)
30+
durationInSeconds: Int64(duration),
31+
orientation: 0
3132
)
3233
}
3334
}
@@ -169,7 +170,8 @@ class NativeSyncApiImpl: NativeSyncApi {
169170
id: asset.localIdentifier,
170171
name: "",
171172
type: 0,
172-
durationInSeconds: 0
173+
durationInSeconds: 0,
174+
orientation: 0
173175
)
174176
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
175177
continue

mobile/lib/domain/models/asset/base_asset.model.dart

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ sealed class BaseAsset {
2525
final int? height;
2626
final int? durationInSeconds;
2727
final bool isFavorite;
28+
final String? livePhotoVideoId;
2829

2930
const BaseAsset({
3031
required this.name,
@@ -36,18 +37,12 @@ sealed class BaseAsset {
3637
this.height,
3738
this.durationInSeconds,
3839
this.isFavorite = false,
40+
this.livePhotoVideoId,
3941
});
4042

4143
bool get isImage => type == AssetType.image;
4244
bool get isVideo => type == AssetType.video;
4345

44-
double? get aspectRatio {
45-
if (width != null && height != null && height! > 0) {
46-
return width! / height!;
47-
}
48-
return null;
49-
}
50-
5146
bool get hasRemote =>
5247
storage == AssetState.remote || storage == AssetState.merged;
5348
bool get hasLocal =>

mobile/lib/domain/models/asset/local_asset.model.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ part of 'base_asset.model.dart';
33
class LocalAsset extends BaseAsset {
44
final String id;
55
final String? remoteId;
6+
final int orientation;
67

78
const LocalAsset({
89
required this.id,
@@ -16,6 +17,8 @@ class LocalAsset extends BaseAsset {
1617
super.height,
1718
super.durationInSeconds,
1819
super.isFavorite = false,
20+
super.livePhotoVideoId,
21+
this.orientation = 0,
1922
});
2023

2124
@override
@@ -38,18 +41,20 @@ class LocalAsset extends BaseAsset {
3841
durationInSeconds: ${durationInSeconds ?? "<NA>"},
3942
remoteId: ${remoteId ?? "<NA>"}
4043
isFavorite: $isFavorite,
44+
orientation: $orientation,
4145
}''';
4246
}
4347

4448
@override
4549
bool operator ==(Object other) {
4650
if (other is! LocalAsset) return false;
4751
if (identical(this, other)) return true;
48-
return super == other && id == other.id && remoteId == other.remoteId;
52+
return super == other && id == other.id && orientation == other.orientation;
4953
}
5054

5155
@override
52-
int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode;
56+
int get hashCode =>
57+
super.hashCode ^ id.hashCode ^ remoteId.hashCode ^ orientation.hashCode;
5358

5459
LocalAsset copyWith({
5560
String? id,
@@ -63,6 +68,7 @@ class LocalAsset extends BaseAsset {
6368
int? height,
6469
int? durationInSeconds,
6570
bool? isFavorite,
71+
int? orientation,
6672
}) {
6773
return LocalAsset(
6874
id: id ?? this.id,
@@ -76,6 +82,7 @@ class LocalAsset extends BaseAsset {
7682
height: height ?? this.height,
7783
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
7884
isFavorite: isFavorite ?? this.isFavorite,
85+
orientation: orientation ?? this.orientation,
7986
);
8087
}
8188
}

mobile/lib/domain/models/asset/remote_asset.model.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class RemoteAsset extends BaseAsset {
3030
super.isFavorite = false,
3131
this.thumbHash,
3232
this.visibility = AssetVisibility.timeline,
33+
super.livePhotoVideoId,
3334
});
3435

3536
@override
@@ -65,7 +66,6 @@ class RemoteAsset extends BaseAsset {
6566
return super == other &&
6667
id == other.id &&
6768
ownerId == other.ownerId &&
68-
localId == other.localId &&
6969
thumbHash == other.thumbHash &&
7070
visibility == other.visibility;
7171
}

mobile/lib/domain/models/setting.model.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ enum Setting<T> {
55
groupAssetsBy<int>(StoreKey.groupAssetsBy, 0),
66
showStorageIndicator<bool>(StoreKey.storageIndicator, true),
77
loadOriginal<bool>(StoreKey.loadOriginal, false),
8+
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, false),
89
preferRemoteImage<bool>(StoreKey.preferRemoteImage, false),
910
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
1011
;

mobile/lib/domain/services/asset.service.dart

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
22
import 'package:immich_mobile/domain/models/exif.model.dart';
33
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
44
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
5+
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
6+
import 'package:platform/platform.dart';
57

68
class AssetService {
79
final RemoteAssetRepository _remoteAssetRepository;
810
final DriftLocalAssetRepository _localAssetRepository;
11+
final Platform _platform;
912

1013
const AssetService({
1114
required RemoteAssetRepository remoteAssetRepository,
1215
required DriftLocalAssetRepository localAssetRepository,
1316
}) : _remoteAssetRepository = remoteAssetRepository,
14-
_localAssetRepository = localAssetRepository;
17+
_localAssetRepository = localAssetRepository,
18+
_platform = const LocalPlatform();
1519

1620
Stream<BaseAsset?> watchAsset(BaseAsset asset) {
1721
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
@@ -21,10 +25,40 @@ class AssetService {
2125
}
2226

2327
Future<ExifInfo?> getExif(BaseAsset asset) async {
24-
if (asset is LocalAsset || asset is! RemoteAsset) {
28+
if (!asset.hasRemote) {
2529
return null;
2630
}
2731

28-
return _remoteAssetRepository.getExif(asset.id);
32+
final id =
33+
asset is LocalAsset ? asset.remoteId! : (asset as RemoteAsset).id;
34+
return _remoteAssetRepository.getExif(id);
35+
}
36+
37+
Future<double> getAspectRatio(BaseAsset asset) async {
38+
bool isFlipped;
39+
double? width;
40+
double? height;
41+
42+
if (asset.hasRemote) {
43+
final exif = await getExif(asset);
44+
isFlipped = ExifDtoConverter.isOrientationFlipped(exif?.orientation);
45+
width = exif?.width ?? asset.width?.toDouble();
46+
height = exif?.height ?? asset.height?.toDouble();
47+
} else if (asset is LocalAsset) {
48+
isFlipped = _platform.isAndroid &&
49+
(asset.orientation == 90 || asset.orientation == 270);
50+
width = asset.width?.toDouble();
51+
height = asset.height?.toDouble();
52+
} else {
53+
isFlipped = false;
54+
}
55+
56+
final orientedWidth = isFlipped ? height : width;
57+
final orientedHeight = isFlipped ? width : height;
58+
if (orientedWidth != null && orientedHeight != null && orientedHeight > 0) {
59+
return orientedWidth / orientedHeight;
60+
}
61+
62+
return 1.0;
2963
}
3064
}

0 commit comments

Comments
 (0)