Compare commits
15 Commits
mobile/nat
...
fix/server
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d846f7fc7f | ||
|
|
efb4394c7b | ||
|
|
e2188867a6 | ||
|
|
5e32cc7bd5 | ||
|
|
1d55b5bfc0 | ||
|
|
60afd7b400 | ||
|
|
3f99ef90ec | ||
|
|
380fc06979 | ||
|
|
d34d92dca3 | ||
|
|
10f8e11db1 | ||
|
|
18b93ddc73 | ||
|
|
88ca1f31ad | ||
|
|
c30ef4dfd6 | ||
|
|
e851a9b099 | ||
|
|
e46db37e44 |
@@ -1,4 +1,4 @@
|
||||
FROM node:22.10.0-alpine3.20@sha256:fc95a044b87e95507c60c1f8c829e5d98ddf46401034932499db370c494ef0ff AS core
|
||||
FROM node:22.11.0-alpine3.20@sha256:f265794478aa0b1a23d85a492c8311ed795bc527c3fe7e43453b3c872dcd71a3 AS core
|
||||
|
||||
WORKDIR /usr/src/open-api/typescript-sdk
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
|
||||
10
cli/package-lock.json
generated
10
cli/package-lock.json
generated
@@ -24,7 +24,7 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/node": "^22.8.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
@@ -59,7 +59,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/node": "^22.8.5",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -1378,9 +1378,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz",
|
||||
"integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==",
|
||||
"version": "22.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
||||
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/node": "^22.8.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
|
||||
34
e2e/package-lock.json
generated
34
e2e/package-lock.json
generated
@@ -15,7 +15,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/node": "^22.8.5",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@@ -64,7 +64,7 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/node": "^22.8.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
@@ -99,7 +99,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/node": "^22.8.5",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -1613,9 +1613,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz",
|
||||
"integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==",
|
||||
"version": "22.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
||||
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3294,9 +3294,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/exiftool-vendored": {
|
||||
"version": "28.6.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.6.0.tgz",
|
||||
"integrity": "sha512-Cx8/8ov1tKEacHhsi7FNYdisIhKq/SeQfprYSpYzwBuJwkPmCV8w7tTIvUJRQX9rvopXhBA4eBf1FPXqTZW5vA==",
|
||||
"version": "28.7.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.7.0.tgz",
|
||||
"integrity": "sha512-0zoq6kBS1yPjzJs+p0qZDinWEA72PTKoRk5ETYKfmeRcZAkhv83Y3HCpbb/LdgJJywfm8BcIJGezrBHvL7dVnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3307,14 +3307,14 @@
|
||||
"luxon": "^3.5.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"exiftool-vendored.exe": "12.97.0",
|
||||
"exiftool-vendored.pl": "12.97.0"
|
||||
"exiftool-vendored.exe": "12.99.0",
|
||||
"exiftool-vendored.pl": "12.99.0"
|
||||
}
|
||||
},
|
||||
"node_modules/exiftool-vendored.exe": {
|
||||
"version": "12.97.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.97.0.tgz",
|
||||
"integrity": "sha512-+HxyFigEJOtwRjP7PhEslhZKuVW2V0hvmHPHtbVtNKGfAUGcfc95xNTjASQfKJvc+9ZuvzdEBPkEQmyA/ZYdIw==",
|
||||
"version": "12.99.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.99.0.tgz",
|
||||
"integrity": "sha512-ffpJHCzC9OYJqw4JlPNtCwRy02jwhmnSJEF/QqEjpuIWDEnlRBQP/yWRh1Nw21K1R4FB4yG5PlCgEDu09VQz/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -3323,9 +3323,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/exiftool-vendored.pl": {
|
||||
"version": "12.97.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.97.0.tgz",
|
||||
"integrity": "sha512-mXe9JEH3csfyPWcC7+H6IpfaokDMMr4S45n7MtiobGPdeeh+kFnf1SQ9cxg4sF403P6IKVeYYPbzgKMlpro9eQ==",
|
||||
"version": "12.99.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.99.0.tgz",
|
||||
"integrity": "sha512-qRVEPQxtoerXF+izJ0O7jGAr5o0Uyvnyu7ao5DTKzF+V7Fv3SurE0l43oCeZPFKo/Ld4V7vEylhFCm4IHVZKWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/node": "^22.8.5",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
|
||||
18
machine-learning/poetry.lock
generated
18
machine-learning/poetry.lock
generated
@@ -946,13 +946,13 @@ tqdm = ["tqdm"]
|
||||
|
||||
[[package]]
|
||||
name = "ftfy"
|
||||
version = "6.3.0"
|
||||
version = "6.3.1"
|
||||
description = "Fixes mojibake and other problems with Unicode, after the fact"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "ftfy-6.3.0-py3-none-any.whl", hash = "sha256:17aca296801f44142e3ff2c16f93fbf6a87609ebb3704a9a41dd5d4903396caf"},
|
||||
{file = "ftfy-6.3.0.tar.gz", hash = "sha256:1c7d6418e72b25a7760feb150acf574b86924dbb2e95b32c0b3abbd1ba3d7ad6"},
|
||||
{file = "ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083"},
|
||||
{file = "ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1609,13 +1609,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "locust"
|
||||
version = "2.32.0"
|
||||
version = "2.32.1"
|
||||
description = "Developer-friendly load testing framework"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "locust-2.32.0-py3-none-any.whl", hash = "sha256:e004514332b8631ca91382d11d224baee4ced040c5f5c8b2233800ebcbc73c0e"},
|
||||
{file = "locust-2.32.0.tar.gz", hash = "sha256:d8f7f5d9d4e801b2e7b0ee3f31109333673da744ccedf85e7da0151f2d263dd9"},
|
||||
{file = "locust-2.32.1-py3-none-any.whl", hash = "sha256:3fb5548b4f2b6477fa5229ee55ac3dddbae56e86c3430bf2ba3fee358eb7e7bb"},
|
||||
{file = "locust-2.32.1.tar.gz", hash = "sha256:8c3b1094dbf20860fd2f6e26b68f0c6064dc28054f4462664389d102fce1448b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2749,13 +2749,13 @@ cli = ["click (>=5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.12"
|
||||
version = "0.0.17"
|
||||
description = "A streaming multipart parser for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "python_multipart-0.0.12-py3-none-any.whl", hash = "sha256:43dcf96cf65888a9cd3423544dd0d75ac10f7aa0c3c28a175bbcd00c9ce1aebf"},
|
||||
{file = "python_multipart-0.0.12.tar.gz", hash = "sha256:045e1f98d719c1ce085ed7f7e1ef9d8ccc8c02ba02b5566d5f7521410ced58cb"},
|
||||
{file = "python_multipart-0.0.17-py3-none-any.whl", hash = "sha256:15dc4f487e0a9476cc1201261188ee0940165cffc94429b6fc565c4d3045cb5d"},
|
||||
{file = "python_multipart-0.0.17.tar.gz", hash = "sha256:41330d831cae6e2f22902704ead2826ea038d0419530eadff3ea80175aec5538"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -65,8 +65,6 @@ 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):
|
||||
@@ -117,7 +115,6 @@ 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`)
|
||||
@@ -171,8 +168,6 @@ 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:
|
||||
@@ -215,7 +210,6 @@ SPEC CHECKSUMS:
|
||||
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
|
||||
MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef
|
||||
maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9
|
||||
native_video_player: d12af78a1a4a8cf09775a5177d5b392def6fd23c
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
|
||||
@@ -22,8 +22,12 @@ class Asset {
|
||||
durationInSeconds = remote.duration.toDuration()?.inSeconds ?? 0,
|
||||
type = remote.type.toAssetType(),
|
||||
fileName = remote.originalFileName,
|
||||
height = remote.exifInfo?.exifImageHeight?.toInt(),
|
||||
width = remote.exifInfo?.exifImageWidth?.toInt(),
|
||||
height = isFlipped(remote)
|
||||
? remote.exifInfo?.exifImageWidth?.toInt()
|
||||
: remote.exifInfo?.exifImageHeight?.toInt(),
|
||||
width = isFlipped(remote)
|
||||
? remote.exifInfo?.exifImageHeight?.toInt()
|
||||
: remote.exifInfo?.exifImageWidth?.toInt(),
|
||||
livePhotoVideoId = remote.livePhotoVideoId,
|
||||
ownerId = fastHash(remote.ownerId),
|
||||
exifInfo =
|
||||
@@ -188,14 +192,6 @@ class Asset {
|
||||
@ignore
|
||||
set byteHash(List<int> hash) => checksum = base64.encode(hash);
|
||||
|
||||
@ignore
|
||||
int? get orientatedWidth =>
|
||||
exifInfo != null && exifInfo!.isFlipped ? height : width;
|
||||
|
||||
@ignore
|
||||
int? get orientatedHeight =>
|
||||
exifInfo != null && exifInfo!.isFlipped ? width : height;
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is! Asset) return false;
|
||||
@@ -515,3 +511,21 @@ 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));
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ class ExifInfo {
|
||||
String? state;
|
||||
String? country;
|
||||
String? description;
|
||||
String? orientation;
|
||||
|
||||
@ignore
|
||||
bool get hasCoordinates =>
|
||||
@@ -46,12 +45,6 @@ class ExifInfo {
|
||||
@ignore
|
||||
String get focalLength => mm != null ? mm!.toStringAsFixed(1) : "";
|
||||
|
||||
@ignore
|
||||
bool? _isFlipped;
|
||||
|
||||
@ignore
|
||||
bool get isFlipped => _isFlipped ??= _isOrientationFlipped(orientation);
|
||||
|
||||
@ignore
|
||||
double? get latitude => lat;
|
||||
|
||||
@@ -74,8 +67,7 @@ class ExifInfo {
|
||||
city = dto.city,
|
||||
state = dto.state,
|
||||
country = dto.country,
|
||||
description = dto.description,
|
||||
orientation = dto.orientation;
|
||||
description = dto.description;
|
||||
|
||||
ExifInfo({
|
||||
this.id,
|
||||
@@ -95,7 +87,6 @@ class ExifInfo {
|
||||
this.state,
|
||||
this.country,
|
||||
this.description,
|
||||
this.orientation,
|
||||
});
|
||||
|
||||
ExifInfo copyWith({
|
||||
@@ -116,7 +107,6 @@ class ExifInfo {
|
||||
String? state,
|
||||
String? country,
|
||||
String? description,
|
||||
String? orientation,
|
||||
}) =>
|
||||
ExifInfo(
|
||||
id: id ?? this.id,
|
||||
@@ -136,7 +126,6 @@ class ExifInfo {
|
||||
state: state ?? this.state,
|
||||
country: country ?? this.country,
|
||||
description: description ?? this.description,
|
||||
orientation: orientation ?? this.orientation,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -158,8 +147,7 @@ class ExifInfo {
|
||||
city == other.city &&
|
||||
state == other.state &&
|
||||
country == other.country &&
|
||||
description == other.description &&
|
||||
orientation == other.orientation;
|
||||
description == other.description;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -181,8 +169,7 @@ class ExifInfo {
|
||||
city.hashCode ^
|
||||
state.hashCode ^
|
||||
country.hashCode ^
|
||||
description.hashCode ^
|
||||
orientation.hashCode;
|
||||
description.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -205,21 +192,10 @@ class ExifInfo {
|
||||
state: $state,
|
||||
country: $country,
|
||||
description: $description,
|
||||
orientation: $orientation
|
||||
}""";
|
||||
}
|
||||
}
|
||||
|
||||
bool _isOrientationFlipped(String? orientation) {
|
||||
final value = orientation != null ? int.tryParse(orientation) : null;
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
final isRotated90CW = value == 5 || value == 6 || value == 90;
|
||||
final isRotated270CW = value == 7 || value == 8 || value == -90;
|
||||
return isRotated90CW || isRotated270CW;
|
||||
}
|
||||
|
||||
double? _exposureTimeToSeconds(String? s) {
|
||||
if (s == null) {
|
||||
return null;
|
||||
|
||||
213
mobile/lib/entities/exif_info.entity.g.dart
generated
213
mobile/lib/entities/exif_info.entity.g.dart
generated
@@ -87,18 +87,13 @@ const ExifInfoSchema = CollectionSchema(
|
||||
name: r'model',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'orientation': PropertySchema(
|
||||
id: 14,
|
||||
name: r'orientation',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'state': PropertySchema(
|
||||
id: 15,
|
||||
id: 14,
|
||||
name: r'state',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'timeZone': PropertySchema(
|
||||
id: 16,
|
||||
id: 15,
|
||||
name: r'timeZone',
|
||||
type: IsarType.string,
|
||||
)
|
||||
@@ -159,12 +154,6 @@ int _exifInfoEstimateSize(
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
{
|
||||
final value = object.orientation;
|
||||
if (value != null) {
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
{
|
||||
final value = object.state;
|
||||
if (value != null) {
|
||||
@@ -200,9 +189,8 @@ void _exifInfoSerialize(
|
||||
writer.writeString(offsets[11], object.make);
|
||||
writer.writeFloat(offsets[12], object.mm);
|
||||
writer.writeString(offsets[13], object.model);
|
||||
writer.writeString(offsets[14], object.orientation);
|
||||
writer.writeString(offsets[15], object.state);
|
||||
writer.writeString(offsets[16], object.timeZone);
|
||||
writer.writeString(offsets[14], object.state);
|
||||
writer.writeString(offsets[15], object.timeZone);
|
||||
}
|
||||
|
||||
ExifInfo _exifInfoDeserialize(
|
||||
@@ -227,9 +215,8 @@ ExifInfo _exifInfoDeserialize(
|
||||
make: reader.readStringOrNull(offsets[11]),
|
||||
mm: reader.readFloatOrNull(offsets[12]),
|
||||
model: reader.readStringOrNull(offsets[13]),
|
||||
orientation: reader.readStringOrNull(offsets[14]),
|
||||
state: reader.readStringOrNull(offsets[15]),
|
||||
timeZone: reader.readStringOrNull(offsets[16]),
|
||||
state: reader.readStringOrNull(offsets[14]),
|
||||
timeZone: reader.readStringOrNull(offsets[15]),
|
||||
);
|
||||
return object;
|
||||
}
|
||||
@@ -273,8 +260,6 @@ P _exifInfoDeserializeProp<P>(
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 15:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 16:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
}
|
||||
@@ -1924,155 +1909,6 @@ extension ExifInfoQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> orientationIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'orientation',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition>
|
||||
orientationIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'orientation',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> orientationEqualTo(
|
||||
String? value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'orientation',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition>
|
||||
orientationGreaterThan(
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'orientation',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> orientationLessThan(
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'orientation',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> orientationBetween(
|
||||
String? lower,
|
||||
String? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'orientation',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> orientationStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.startsWith(
|
||||
property: r'orientation',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> orientationEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.endsWith(
|
||||
property: r'orientation',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> orientationContains(
|
||||
String value,
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'orientation',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> orientationMatches(
|
||||
String pattern,
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.matches(
|
||||
property: r'orientation',
|
||||
wildcard: pattern,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> orientationIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'orientation',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition>
|
||||
orientationIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'orientation',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> stateIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
@@ -2541,18 +2377,6 @@ extension ExifInfoQuerySortBy on QueryBuilder<ExifInfo, ExifInfo, QSortBy> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByOrientation() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'orientation', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByOrientationDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'orientation', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByState() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'state', Sort.asc);
|
||||
@@ -2760,18 +2584,6 @@ extension ExifInfoQuerySortThenBy
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByOrientation() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'orientation', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByOrientationDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'orientation', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByState() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'state', Sort.asc);
|
||||
@@ -2889,13 +2701,6 @@ extension ExifInfoQueryWhereDistinct
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QDistinct> distinctByOrientation(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'orientation', caseSensitive: caseSensitive);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, ExifInfo, QDistinct> distinctByState(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -3004,12 +2809,6 @@ extension ExifInfoQueryProperty
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, String?, QQueryOperations> orientationProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'orientation');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<ExifInfo, String?, QQueryOperations> stateProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'state');
|
||||
|
||||
@@ -12,7 +12,7 @@ import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.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/native_video_loader.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/asset_viewer/asset_stack.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
@@ -56,14 +56,18 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
final settings = ref.watch(appSettingsServiceProvider);
|
||||
final loadAsset = renderList.loadAsset;
|
||||
final totalAssets = useState(renderList.totalAssets);
|
||||
final shouldLoopVideo =
|
||||
useState(settings.getSetting<bool>(AppSettingsEnum.loopVideo));
|
||||
final shouldLoopVideo = useState(AppSettingsEnum.loopVideo.defaultValue);
|
||||
final isZoomed = useState(false);
|
||||
final isPlayingVideo = useState(false);
|
||||
final localPosition = useRef<Offset?>(null);
|
||||
final currentIndex = useValueNotifier(initialIndex);
|
||||
final localPosition = useState<Offset?>(null);
|
||||
final currentIndex = useState(initialIndex);
|
||||
final currentAsset = loadAsset(currentIndex.value);
|
||||
|
||||
// Update is playing motion video
|
||||
ref.listen(videoPlaybackValueProvider.select((v) => v.state), (_, state) {
|
||||
isPlayingVideo.value = state == VideoPlaybackState.playing;
|
||||
});
|
||||
|
||||
final stackIndex = useState(-1);
|
||||
final stack = showStack && currentAsset.stackCount > 0
|
||||
? ref.watch(assetStackStateProvider(currentAsset))
|
||||
@@ -77,26 +81,28 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
: stackElements.elementAt(stackIndex.value);
|
||||
|
||||
final isMotionPhoto = asset.livePhotoVideoId != null;
|
||||
// Update is playing motion video
|
||||
if (isMotionPhoto) {
|
||||
ref.listen(videoPlaybackValueProvider.select((v) => v.state), (_, state) {
|
||||
isPlayingVideo.value = state == VideoPlaybackState.playing;
|
||||
});
|
||||
}
|
||||
// Listen provider to prevent autoDispose when navigating to other routes from within the gallery page
|
||||
ref.listen(currentAssetProvider, (_, __) {});
|
||||
useEffect(
|
||||
() {
|
||||
// Delay state update to after the execution of build method
|
||||
ref.read(currentAssetProvider.notifier).set(asset);
|
||||
// Future.microtask(
|
||||
// () => ref.read(currentAssetProvider.notifier).set(asset),
|
||||
// );
|
||||
Future.microtask(
|
||||
() => ref.read(currentAssetProvider.notifier).set(asset),
|
||||
);
|
||||
return null;
|
||||
},
|
||||
[asset],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
shouldLoopVideo.value =
|
||||
settings.getSetting<bool>(AppSettingsEnum.loopVideo);
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
Future<void> precacheNextImage(int index) async {
|
||||
void onError(Object exception, StackTrace? stackTrace) {
|
||||
// swallow error silently
|
||||
@@ -105,7 +111,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
|
||||
try {
|
||||
if (index < totalAssets.value && index >= 0) {
|
||||
log.info('Precaching next image at index $index');
|
||||
final asset = loadAsset(index);
|
||||
await precacheImage(
|
||||
ImmichImage.imageProvider(asset: asset),
|
||||
@@ -185,9 +190,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
}
|
||||
if (isMotionPhoto) {
|
||||
isPlayingVideo.value = false;
|
||||
}
|
||||
isPlayingVideo.value = false;
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
@@ -273,8 +276,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
isZoomed.value = state != PhotoViewScaleState.initial;
|
||||
ref.read(showControlsProvider.notifier).show = !isZoomed.value;
|
||||
},
|
||||
// wantKeepAlive: true,
|
||||
// gaplessPlayback: true,
|
||||
loadingBuilder: (context, event, index) => ClipRect(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
@@ -302,19 +303,13 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
itemCount: totalAssets.value,
|
||||
scrollDirection: Axis.horizontal,
|
||||
onPageChanged: (value) async {
|
||||
log.info('Page changed to $value');
|
||||
final next = currentIndex.value < value ? value + 1 : value - 1;
|
||||
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
|
||||
log.info('Setting current index to $value');
|
||||
currentIndex.value = value;
|
||||
if (stackIndex.value != -1) {
|
||||
stackIndex.value = -1;
|
||||
}
|
||||
if (isMotionPhoto) {
|
||||
isPlayingVideo.value = false;
|
||||
}
|
||||
stackIndex.value = -1;
|
||||
isPlayingVideo.value = false;
|
||||
|
||||
// Wait for page change animation to finish
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
@@ -328,23 +323,17 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
final ImageProvider provider =
|
||||
ImmichImage.imageProvider(asset: a);
|
||||
|
||||
ref.read(videoPlaybackValueProvider.notifier).reset();
|
||||
if (a.isImage && !isPlayingVideo.value) {
|
||||
ref.read(showControlsProvider.notifier).show = false;
|
||||
return PhotoViewGalleryPageOptions(
|
||||
onDragStart: (_, details, __) {
|
||||
log.info('Drag start');
|
||||
localPosition.value = details.localPosition;
|
||||
},
|
||||
onDragUpdate: (_, details, __) {
|
||||
log.info('Drag update');
|
||||
handleSwipeUpDown(details);
|
||||
},
|
||||
onDragStart: (_, details, __) =>
|
||||
localPosition.value = details.localPosition,
|
||||
onDragUpdate: (_, details, __) =>
|
||||
handleSwipeUpDown(details),
|
||||
onTapDown: (_, __, ___) {
|
||||
ref.read(showControlsProvider.notifier).toggle();
|
||||
},
|
||||
onLongPressStart: (_, __, ___) {
|
||||
if (isMotionPhoto) {
|
||||
if (asset.livePhotoVideoId != null) {
|
||||
isPlayingVideo.value = true;
|
||||
}
|
||||
},
|
||||
@@ -364,26 +353,24 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
log.info('Loading asset ${a.id} (index $index) as video');
|
||||
return PhotoViewGalleryPageOptions.customChild(
|
||||
onDragStart: (_, details, __) =>
|
||||
localPosition.value = details.localPosition,
|
||||
onDragUpdate: (_, details, __) =>
|
||||
handleSwipeUpDown(details),
|
||||
// heroAttributes: PhotoViewHeroAttributes(
|
||||
// tag: isFromDto
|
||||
// ? '${currentAsset.remoteId}-$heroOffset'
|
||||
// : currentAsset.id + heroOffset,
|
||||
// ),
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: isFromDto
|
||||
? '${currentAsset.remoteId}-$heroOffset'
|
||||
: currentAsset.id + heroOffset,
|
||||
),
|
||||
filterQuality: FilterQuality.high,
|
||||
initialScale: 1.0,
|
||||
maxScale: 1.0,
|
||||
minScale: 1.0,
|
||||
basePosition: Alignment.center,
|
||||
child: NativeVideoLoader(
|
||||
key: ValueKey(a.id),
|
||||
child: VideoViewerPage(
|
||||
key: ValueKey(a),
|
||||
asset: a,
|
||||
isMotionVideo: isMotionPhoto,
|
||||
isMotionVideo: a.livePhotoVideoId != null,
|
||||
loopVideo: shouldLoopVideo.value,
|
||||
placeholder: Image(
|
||||
image: provider,
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
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) {
|
||||
final log = Logger('NativeVideoLoader');
|
||||
log.info('Building NativeVideoLoader');
|
||||
// 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.isLocal ? AssetEntity.fromId(asset.localId!) : null,
|
||||
// );
|
||||
Future<double> calculateAspectRatio(AssetEntity? localEntity) async {
|
||||
log.info('Calculating aspect ratio');
|
||||
late final double? orientatedWidth;
|
||||
late final double? orientatedHeight;
|
||||
|
||||
if (asset.exifInfo != null) {
|
||||
orientatedWidth = asset.orientatedWidth?.toDouble();
|
||||
orientatedHeight = asset.orientatedHeight?.toDouble();
|
||||
} else if (localEntity != null) {
|
||||
orientatedWidth = localEntity.orientatedWidth.toDouble();
|
||||
orientatedHeight = localEntity.orientatedHeight.toDouble();
|
||||
} else {
|
||||
final entity = await ref.read(assetServiceProvider).loadExif(asset);
|
||||
orientatedWidth = entity.orientatedWidth?.toDouble();
|
||||
orientatedHeight = entity.orientatedHeight?.toDouble();
|
||||
}
|
||||
|
||||
log.info('Calculated aspect ratio');
|
||||
if (orientatedWidth != null &&
|
||||
orientatedHeight != null &&
|
||||
orientatedWidth > 0 &&
|
||||
orientatedHeight > 0) {
|
||||
return orientatedWidth / orientatedHeight;
|
||||
}
|
||||
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// final aspectRatioFuture = useMemoized(() => calculateAspectRatio());
|
||||
|
||||
Future<VideoSource> createLocalSource(AssetEntity? localEntity) async {
|
||||
log.info('Loading video from local storage');
|
||||
if (localEntity == null) {
|
||||
throw Exception('No entity found for the video');
|
||||
}
|
||||
|
||||
final file = await localEntity.file;
|
||||
if (file == null) {
|
||||
throw Exception('No file found for the video');
|
||||
}
|
||||
|
||||
final source = await VideoSource.init(
|
||||
path: file.path,
|
||||
type: VideoSourceType.file,
|
||||
);
|
||||
log.info('Loaded video from local storage');
|
||||
return source;
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
final source = await VideoSource.init(
|
||||
path: videoUrl,
|
||||
type: VideoSourceType.network,
|
||||
headers: ApiService.getRequestHeaders(),
|
||||
);
|
||||
log.info('Loaded video from server');
|
||||
return source;
|
||||
}
|
||||
|
||||
Future<VideoSource> createSource(AssetEntity? localEntity) {
|
||||
if (localEntity != null && asset.livePhotoVideoId == null) {
|
||||
return createLocalSource(localEntity);
|
||||
}
|
||||
|
||||
return createRemoteSource();
|
||||
}
|
||||
|
||||
// final createSourceFuture = useMemoized(() => createSource());
|
||||
|
||||
final combinedFuture = useMemoized(
|
||||
() => Future.delayed(Duration(milliseconds: 1), () async {
|
||||
if (!context.mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final entity =
|
||||
asset.isLocal ? await AssetEntity.fromId(asset.localId!) : null;
|
||||
return (createSource(entity), calculateAspectRatio(entity)).wait;
|
||||
}),
|
||||
);
|
||||
|
||||
final doCleanup = useState(false);
|
||||
ref.listen(videoPlaybackValueProvider.select((value) => value.state),
|
||||
(_, value) {
|
||||
if (value == VideoPlaybackState.initializing) {
|
||||
log.info('Cleaning up video');
|
||||
doCleanup.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// useEffect(() {
|
||||
// Future.microtask(() {
|
||||
// if (!context.mounted) {
|
||||
// return Future.value(null);
|
||||
// }
|
||||
|
||||
// return (createSourceFuture, aspectRatioFuture).wait;
|
||||
// });
|
||||
|
||||
// return () {
|
||||
|
||||
// }
|
||||
// }, [asset.id]);
|
||||
|
||||
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).reset(),
|
||||
child: SizedBox(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
child: doCleanup.value
|
||||
? placeholder
|
||||
: FutureBuilder(
|
||||
key: ValueKey(asset.id),
|
||||
future: combinedFuture,
|
||||
// initialData: initAspectRatio,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
return NativeVideoViewerPage(
|
||||
videoSource: snapshot.data!.$1,
|
||||
aspectRatio: snapshot.data!.$2,
|
||||
duration: asset.duration,
|
||||
isMotionVideo: isMotionVideo,
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
loopVideo: loopVideo,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
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/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_value_provider.dart';
|
||||
import 'package:immich_mobile/utils/hooks/interval_hook.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/custom_video_player_controls.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:native_video_player/native_video_player.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
class NativeVideoViewerPage extends HookConsumerWidget {
|
||||
final VideoSource videoSource;
|
||||
final double aspectRatio;
|
||||
final Duration duration;
|
||||
final bool isMotionVideo;
|
||||
final bool showControls;
|
||||
final Duration hideControlsTimer;
|
||||
final bool loopVideo;
|
||||
|
||||
const NativeVideoViewerPage({
|
||||
super.key,
|
||||
required this.videoSource,
|
||||
required this.aspectRatio,
|
||||
required this.duration,
|
||||
this.isMotionVideo = false,
|
||||
this.showControls = true,
|
||||
this.hideControlsTimer = const Duration(seconds: 5),
|
||||
this.loopVideo = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final controller = useRef<NativeVideoPlayerController?>(null);
|
||||
final lastVideoPosition = useRef(-1);
|
||||
final isBuffering = useRef(false);
|
||||
|
||||
final log = Logger('NativeVideoViewerPage');
|
||||
log.info('Building NativeVideoViewerPage');
|
||||
|
||||
void checkIfBuffering([Timer? timer]) {
|
||||
if (!context.mounted) {
|
||||
return timer?.cancel();
|
||||
}
|
||||
|
||||
log.info('Checking if buffering');
|
||||
final videoPlayback = ref.read(videoPlaybackValueProvider);
|
||||
if ((isBuffering.value ||
|
||||
videoPlayback.state == VideoPlaybackState.initializing) &&
|
||||
videoPlayback.state != VideoPlaybackState.buffering) {
|
||||
log.info('Marking video as buffering');
|
||||
ref.read(videoPlaybackValueProvider.notifier).value =
|
||||
videoPlayback.copyWith(state: VideoPlaybackState.buffering);
|
||||
}
|
||||
}
|
||||
|
||||
// timer to mark videos as buffering if the position does not change
|
||||
useInterval(const Duration(seconds: 5), checkIfBuffering);
|
||||
|
||||
// When the volume changes, set the volume
|
||||
ref.listen(videoPlayerControlsProvider.select((value) => value.mute),
|
||||
(_, mute) {
|
||||
final playerController = controller.value;
|
||||
if (playerController == null) {
|
||||
log.info('No controller to seek to');
|
||||
return;
|
||||
}
|
||||
|
||||
final playbackInfo = playerController.playbackInfo;
|
||||
if (playbackInfo == null) {
|
||||
log.info('No playback info to update');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (mute && playbackInfo.volume != 0.0) {
|
||||
log.info('Muting video');
|
||||
playerController.setVolume(0.0);
|
||||
} else if (!mute && playbackInfo.volume != 0.7) {
|
||||
log.info('Unmuting video');
|
||||
playerController.setVolume(0.7);
|
||||
}
|
||||
} catch (error) {
|
||||
log.severe('Error setting volume: $error');
|
||||
}
|
||||
});
|
||||
|
||||
// When the position changes, seek to the position
|
||||
ref.listen(videoPlayerControlsProvider.select((value) => value.position),
|
||||
(_, position) {
|
||||
final playerController = controller.value;
|
||||
if (playerController == null) {
|
||||
log.info('No controller to seek to');
|
||||
return;
|
||||
}
|
||||
|
||||
final playbackInfo = playerController.playbackInfo;
|
||||
if (playbackInfo == null) {
|
||||
log.info('No playback info to update');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the position to seek to
|
||||
final int seek = (duration * (position / 100.0)).inSeconds;
|
||||
if (seek != playbackInfo.position) {
|
||||
log.info('Seeking to position: $seek from ${playbackInfo.position}');
|
||||
try {
|
||||
playerController.seekTo(seek);
|
||||
} catch (error) {
|
||||
log.severe('Error seeking to position $position: $error');
|
||||
}
|
||||
}
|
||||
|
||||
ref.read(videoPlaybackValueProvider.notifier).position =
|
||||
Duration(seconds: seek);
|
||||
});
|
||||
|
||||
// // When the custom video controls pause or play
|
||||
ref.listen(videoPlayerControlsProvider.select((value) => value.pause),
|
||||
(_, pause) {
|
||||
try {
|
||||
if (pause) {
|
||||
log.info('Pausing video');
|
||||
controller.value?.pause();
|
||||
WakelockPlus.disable();
|
||||
} else {
|
||||
log.info('Playing video');
|
||||
controller.value?.play();
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
} catch (error) {
|
||||
log.severe('Error pausing or playing video: $error');
|
||||
}
|
||||
});
|
||||
|
||||
void onPlaybackReady() {
|
||||
try {
|
||||
log.info('onPlaybackReady: Playing video');
|
||||
controller.value?.play();
|
||||
controller.value?.setVolume(0.9);
|
||||
WakelockPlus.enable();
|
||||
} catch (error) {
|
||||
log.severe('Error playing video: $error');
|
||||
}
|
||||
}
|
||||
|
||||
void onPlaybackStatusChanged() {
|
||||
final videoController = controller.value;
|
||||
if (videoController == null || !context.mounted) {
|
||||
log.info('No controller to update');
|
||||
return;
|
||||
}
|
||||
|
||||
final videoPlayback =
|
||||
VideoPlaybackValue.fromNativeController(controller.value!);
|
||||
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
|
||||
|
||||
if (videoPlayback.state == VideoPlaybackState.playing) {
|
||||
// Sync with the controls playing
|
||||
WakelockPlus.enable();
|
||||
log.info('Video is playing; enabled wakelock');
|
||||
} else {
|
||||
// Sync with the controls pause
|
||||
WakelockPlus.disable();
|
||||
log.info('Video is not playing; disabled wakelock');
|
||||
}
|
||||
}
|
||||
|
||||
void onPlaybackPositionChanged() {
|
||||
final videoController = controller.value;
|
||||
if (videoController == null || !context.mounted) {
|
||||
log.info('No controller to update');
|
||||
return;
|
||||
}
|
||||
|
||||
final playbackInfo = videoController.playbackInfo;
|
||||
if (playbackInfo == null) {
|
||||
log.info('No playback info to update');
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(videoPlaybackValueProvider.notifier).position =
|
||||
Duration(seconds: playbackInfo.position);
|
||||
|
||||
// Check if the video is buffering
|
||||
if (playbackInfo.status == PlaybackStatus.playing) {
|
||||
log.info('Updating playing video position');
|
||||
isBuffering.value = lastVideoPosition.value == playbackInfo.position;
|
||||
lastVideoPosition.value = playbackInfo.position;
|
||||
} else {
|
||||
log.info('Updating non-playing video position');
|
||||
isBuffering.value = false;
|
||||
lastVideoPosition.value = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void onPlaybackEnded() {
|
||||
log.info('onPlaybackEnded: Video ended');
|
||||
if (loopVideo) {
|
||||
log.info('onPlaybackEnded: Looping video');
|
||||
try {
|
||||
controller.value?.play();
|
||||
} catch (error) {
|
||||
log.severe('Error looping video: $error');
|
||||
}
|
||||
} else {
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
}
|
||||
|
||||
void initController(NativeVideoPlayerController nc) {
|
||||
if (controller.value != null) {
|
||||
log.info('initController: Controller already initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('initController: adding onPlaybackPositionChanged listener');
|
||||
nc.onPlaybackPositionChanged.addListener(onPlaybackPositionChanged);
|
||||
|
||||
log.info('initController: adding onPlaybackStatusChanged listener');
|
||||
nc.onPlaybackStatusChanged.addListener(onPlaybackStatusChanged);
|
||||
|
||||
log.info('initController: adding onPlaybackReady listener');
|
||||
nc.onPlaybackReady.addListener(onPlaybackReady);
|
||||
|
||||
log.info('initController: adding onPlaybackEnded listener');
|
||||
nc.onPlaybackEnded.addListener(onPlaybackEnded);
|
||||
|
||||
log.info('initController: loading video source');
|
||||
nc.loadVideoSource(videoSource);
|
||||
|
||||
log.info('initController: setting controller');
|
||||
controller.value = nc;
|
||||
Timer(const Duration(milliseconds: 200), checkIfBuffering);
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
log.info('useEffect: resetting video player controls');
|
||||
ref.read(videoPlayerControlsProvider.notifier).reset();
|
||||
|
||||
if (isMotionVideo) {
|
||||
// ignore: prefer-extracting-callbacks
|
||||
log.info('useEffect: disabling showing video player controls');
|
||||
ref.read(showControlsProvider.notifier).show = false;
|
||||
}
|
||||
|
||||
return () {
|
||||
final playerController = controller.value;
|
||||
if (playerController == null) {
|
||||
log.info('No controller to dispose');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
log.info('Stopping video');
|
||||
playerController.stop();
|
||||
|
||||
log.info('Removing onPlaybackPositionChanged listener');
|
||||
playerController.onPlaybackPositionChanged
|
||||
.removeListener(onPlaybackPositionChanged);
|
||||
|
||||
log.info('Removing onPlaybackStatusChanged listener');
|
||||
playerController.onPlaybackStatusChanged
|
||||
.removeListener(onPlaybackStatusChanged);
|
||||
|
||||
log.info('Removing onPlaybackReady listener');
|
||||
playerController.onPlaybackReady.removeListener(onPlaybackReady);
|
||||
|
||||
log.info('Removing onPlaybackEnded listener');
|
||||
playerController.onPlaybackEnded.removeListener(onPlaybackEnded);
|
||||
} catch (error) {
|
||||
log.severe('Error during useEffect cleanup: $error');
|
||||
}
|
||||
|
||||
log.info('Disposing controller');
|
||||
controller.value = null;
|
||||
|
||||
log.info('Disabling Wakelock');
|
||||
WakelockPlus.disable();
|
||||
};
|
||||
},
|
||||
[videoSource],
|
||||
);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: aspectRatio,
|
||||
child: NativeVideoPlayerView(
|
||||
onViewReady: initController,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showControls)
|
||||
Center(
|
||||
child: CustomVideoPlayerControls(
|
||||
hideTimerDuration: hideControlsTimer,
|
||||
),
|
||||
),
|
||||
// Visibility(
|
||||
// visible: controller.value == null,
|
||||
// child: const Positioned.fill(
|
||||
// child: Center(
|
||||
// child: DelayedLoadingIndicator(
|
||||
// fadeInDuration: Duration(milliseconds: 500),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,8 @@ class VideoViewerPage extends HookConsumerWidget {
|
||||
|
||||
return PopScope(
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
ref.read(videoPlaybackValueProvider.notifier).reset();
|
||||
ref.read(videoPlaybackValueProvider.notifier).value =
|
||||
VideoPlaybackValue.uninitialized();
|
||||
},
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class VideoPlaybackControls {
|
||||
const VideoPlaybackControls({
|
||||
VideoPlaybackControls({
|
||||
required this.position,
|
||||
required this.mute,
|
||||
required this.pause,
|
||||
@@ -17,14 +17,15 @@ final videoPlayerControlsProvider =
|
||||
return VideoPlayerControls(ref);
|
||||
});
|
||||
|
||||
const videoPlayerControlsDefault = VideoPlaybackControls(
|
||||
position: 0,
|
||||
pause: false,
|
||||
mute: false,
|
||||
);
|
||||
|
||||
class VideoPlayerControls extends StateNotifier<VideoPlaybackControls> {
|
||||
VideoPlayerControls(this.ref) : super(videoPlayerControlsDefault);
|
||||
VideoPlayerControls(this.ref)
|
||||
: super(
|
||||
VideoPlaybackControls(
|
||||
position: 0,
|
||||
pause: false,
|
||||
mute: false,
|
||||
),
|
||||
);
|
||||
|
||||
final Ref ref;
|
||||
|
||||
@@ -35,17 +36,17 @@ class VideoPlayerControls extends StateNotifier<VideoPlaybackControls> {
|
||||
}
|
||||
|
||||
void reset() {
|
||||
state = videoPlayerControlsDefault;
|
||||
state = VideoPlaybackControls(
|
||||
position: 0,
|
||||
pause: false,
|
||||
mute: false,
|
||||
);
|
||||
}
|
||||
|
||||
double get position => state.position;
|
||||
bool get mute => state.mute;
|
||||
|
||||
set position(double value) {
|
||||
if (state.position == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = VideoPlaybackControls(
|
||||
position: value,
|
||||
mute: state.mute,
|
||||
@@ -54,10 +55,6 @@ class VideoPlayerControls extends StateNotifier<VideoPlaybackControls> {
|
||||
}
|
||||
|
||||
set mute(bool value) {
|
||||
if (state.mute == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = VideoPlaybackControls(
|
||||
position: state.position,
|
||||
mute: value,
|
||||
@@ -74,10 +71,6 @@ class VideoPlayerControls extends StateNotifier<VideoPlaybackControls> {
|
||||
}
|
||||
|
||||
void pause() {
|
||||
if (state.pause) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = VideoPlaybackControls(
|
||||
position: state.position,
|
||||
mute: state.mute,
|
||||
@@ -86,10 +79,6 @@ class VideoPlayerControls extends StateNotifier<VideoPlaybackControls> {
|
||||
}
|
||||
|
||||
void play() {
|
||||
if (!state.pause) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = VideoPlaybackControls(
|
||||
position: state.position,
|
||||
mute: state.mute,
|
||||
@@ -106,6 +95,12 @@ class VideoPlayerControls extends StateNotifier<VideoPlaybackControls> {
|
||||
}
|
||||
|
||||
void restart() {
|
||||
state = VideoPlaybackControls(
|
||||
position: 0,
|
||||
mute: state.mute,
|
||||
pause: true,
|
||||
);
|
||||
|
||||
state = VideoPlaybackControls(
|
||||
position: 0,
|
||||
mute: state.mute,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:native_video_player/native_video_player.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
enum VideoPlaybackState {
|
||||
@@ -23,44 +22,13 @@ class VideoPlaybackValue {
|
||||
/// The volume of the video
|
||||
final double volume;
|
||||
|
||||
const VideoPlaybackValue({
|
||||
VideoPlaybackValue({
|
||||
required this.position,
|
||||
required this.duration,
|
||||
required this.state,
|
||||
required this.volume,
|
||||
});
|
||||
|
||||
factory VideoPlaybackValue.fromNativeController(
|
||||
NativeVideoPlayerController controller,
|
||||
) {
|
||||
final playbackInfo = controller.playbackInfo;
|
||||
final videoInfo = controller.videoInfo;
|
||||
|
||||
if (playbackInfo == null || videoInfo == null) {
|
||||
return videoPlaybackValueDefault;
|
||||
}
|
||||
|
||||
late final VideoPlaybackState status;
|
||||
switch (playbackInfo.status) {
|
||||
case PlaybackStatus.playing:
|
||||
status = VideoPlaybackState.playing;
|
||||
break;
|
||||
case PlaybackStatus.paused:
|
||||
status = VideoPlaybackState.paused;
|
||||
break;
|
||||
case PlaybackStatus.stopped:
|
||||
status = VideoPlaybackState.completed;
|
||||
break;
|
||||
}
|
||||
|
||||
return VideoPlaybackValue(
|
||||
position: Duration(seconds: playbackInfo.position),
|
||||
duration: Duration(seconds: videoInfo.duration),
|
||||
state: status,
|
||||
volume: playbackInfo.volume,
|
||||
);
|
||||
}
|
||||
|
||||
factory VideoPlaybackValue.fromController(VideoPlayerController? controller) {
|
||||
final video = controller?.value;
|
||||
late VideoPlaybackState s;
|
||||
@@ -84,35 +52,26 @@ class VideoPlaybackValue {
|
||||
);
|
||||
}
|
||||
|
||||
VideoPlaybackValue copyWith({
|
||||
Duration? position,
|
||||
Duration? duration,
|
||||
VideoPlaybackState? state,
|
||||
double? volume,
|
||||
}) {
|
||||
factory VideoPlaybackValue.uninitialized() {
|
||||
return VideoPlaybackValue(
|
||||
position: position ?? this.position,
|
||||
duration: duration ?? this.duration,
|
||||
state: state ?? this.state,
|
||||
volume: volume ?? this.volume,
|
||||
position: Duration.zero,
|
||||
duration: Duration.zero,
|
||||
state: VideoPlaybackState.initializing,
|
||||
volume: 0.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const VideoPlaybackValue videoPlaybackValueDefault = VideoPlaybackValue(
|
||||
position: Duration.zero,
|
||||
duration: Duration.zero,
|
||||
state: VideoPlaybackState.initializing,
|
||||
volume: 0.0,
|
||||
);
|
||||
|
||||
final videoPlaybackValueProvider =
|
||||
StateNotifierProvider<VideoPlaybackValueState, VideoPlaybackValue>((ref) {
|
||||
return VideoPlaybackValueState(ref);
|
||||
});
|
||||
|
||||
class VideoPlaybackValueState extends StateNotifier<VideoPlaybackValue> {
|
||||
VideoPlaybackValueState(this.ref) : super(videoPlaybackValueDefault);
|
||||
VideoPlaybackValueState(this.ref)
|
||||
: super(
|
||||
VideoPlaybackValue.uninitialized(),
|
||||
);
|
||||
|
||||
final Ref ref;
|
||||
|
||||
@@ -123,7 +82,6 @@ class VideoPlaybackValueState extends StateNotifier<VideoPlaybackValue> {
|
||||
}
|
||||
|
||||
set position(Duration value) {
|
||||
if (state.position == value) return;
|
||||
state = VideoPlaybackValue(
|
||||
position: value,
|
||||
duration: state.duration,
|
||||
@@ -131,8 +89,4 @@ class VideoPlaybackValueState extends StateNotifier<VideoPlaybackValue> {
|
||||
volume: state.volume,
|
||||
);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
state = videoPlaybackValueDefault;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
// https://github.com/rrousselGit/flutter_hooks/issues/233#issuecomment-840416638
|
||||
void useInterval(Duration delay, VoidCallback callback) {
|
||||
final savedCallback = useRef(callback);
|
||||
savedCallback.value = callback;
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
final timer = Timer.periodic(delay, (_) => savedCallback.value());
|
||||
return timer.cancel;
|
||||
},
|
||||
[delay],
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/utils/db.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
const int targetVersion = 7;
|
||||
const int targetVersion = 6;
|
||||
|
||||
Future<void> migrateDatabaseIfNeeded(Isar db) async {
|
||||
final int version = Store.get(StoreKey.version, 1);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
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/video_player_controls_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/common/delayed_loading_indicator.dart';
|
||||
import 'package:immich_mobile/utils/hooks/timer_hook.dart';
|
||||
|
||||
class CustomVideoPlayerControls extends HookConsumerWidget {
|
||||
final Duration hideTimerDuration;
|
||||
@@ -28,9 +29,10 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
final showBuffering = useState(false);
|
||||
final VideoPlaybackState state =
|
||||
ref.watch(videoPlaybackValueProvider.select((value) => value.state));
|
||||
final showBuffering = state == VideoPlaybackState.buffering;
|
||||
ref.watch(videoPlaybackValueProvider).state;
|
||||
|
||||
/// Shows the controls and starts the timer to hide them
|
||||
void showControlsAndStartHideTimer() {
|
||||
@@ -50,9 +52,16 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
|
||||
showControlsAndStartHideTimer();
|
||||
});
|
||||
|
||||
ref.listen(videoPlaybackValueProvider.select((value) => value.state),
|
||||
(_, state) {
|
||||
// Show buffering
|
||||
showBuffering.value = state == VideoPlaybackState.buffering;
|
||||
});
|
||||
|
||||
/// Toggles between playing and pausing depending on the state of the video
|
||||
void togglePlay() {
|
||||
showControlsAndStartHideTimer();
|
||||
final state = ref.read(videoPlaybackValueProvider).state;
|
||||
if (state == VideoPlaybackState.playing) {
|
||||
ref.read(videoPlayerControlsProvider.notifier).pause();
|
||||
} else if (state == VideoPlaybackState.completed) {
|
||||
@@ -69,7 +78,7 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
|
||||
absorbing: !ref.watch(showControlsProvider),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (showBuffering)
|
||||
if (showBuffering.value)
|
||||
const Center(
|
||||
child: DelayedLoadingIndicator(
|
||||
fadeInDuration: Duration(milliseconds: 400),
|
||||
@@ -77,8 +86,12 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
|
||||
)
|
||||
else
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
ref.read(showControlsProvider.notifier).show = false,
|
||||
onTap: () {
|
||||
if (state != VideoPlaybackState.playing) {
|
||||
togglePlay();
|
||||
}
|
||||
ref.read(showControlsProvider.notifier).show = false;
|
||||
},
|
||||
child: CenterPlayButton(
|
||||
backgroundColor: Colors.black54,
|
||||
iconColor: Colors.white,
|
||||
|
||||
@@ -15,10 +15,9 @@ class FileInfo extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final textColor = context.isDarkTheme ? Colors.white : Colors.black;
|
||||
|
||||
String resolution =
|
||||
asset.orientatedHeight != null && asset.orientatedWidth != null
|
||||
? "${asset.orientatedHeight} x ${asset.orientatedWidth} "
|
||||
: "";
|
||||
String resolution = asset.width != null && asset.height != null
|
||||
? "${asset.height} x ${asset.width} "
|
||||
: "";
|
||||
String fileSize = asset.exifInfo?.fileSize != null
|
||||
? formatBytes(asset.exifInfo!.fileSize!)
|
||||
: "";
|
||||
|
||||
@@ -2,9 +2,9 @@ import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/pages/common/native_video_loader.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/pages/common/video_viewer.page.dart';
|
||||
import 'package:immich_mobile/utils/hooks/blurhash_hook.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
||||
|
||||
@@ -68,9 +68,10 @@ class MemoryCard extends StatelessWidget {
|
||||
} else {
|
||||
return Hero(
|
||||
tag: 'memory-${asset.id}',
|
||||
child: NativeVideoLoader(
|
||||
key: ValueKey(asset.id),
|
||||
child: VideoViewerPage(
|
||||
key: ValueKey(asset),
|
||||
asset: asset,
|
||||
showDownloadingIndicator: false,
|
||||
placeholder: SizedBox.expand(
|
||||
child: ImmichImage(
|
||||
asset,
|
||||
|
||||
@@ -1024,15 +1024,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
native_video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "feat/headers"
|
||||
resolved-ref: "568c76e1552791f06dcf44b45d3373cad12913ed"
|
||||
url: "https://github.com/immich-app/native_video_player"
|
||||
source: git
|
||||
version: "1.3.1"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -57,10 +57,6 @@ dependencies:
|
||||
async: ^2.11.0
|
||||
dynamic_color: ^1.7.0 #package to apply system theme
|
||||
background_downloader: ^8.5.5
|
||||
native_video_player:
|
||||
git:
|
||||
url: https://github.com/immich-app/native_video_player
|
||||
ref: feat/headers
|
||||
|
||||
#image editing packages
|
||||
crop_image: ^1.0.13
|
||||
|
||||
8
open-api/typescript-sdk/package-lock.json
generated
8
open-api/typescript-sdk/package-lock.json
generated
@@ -12,7 +12,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/node": "^22.8.5",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -22,9 +22,9 @@
|
||||
"integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz",
|
||||
"integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==",
|
||||
"version": "22.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
||||
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/node": "^22.8.5",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
@@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
|
||||
COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl
|
||||
|
||||
# web build
|
||||
FROM node:22.10.0-alpine3.20@sha256:fc95a044b87e95507c60c1f8c829e5d98ddf46401034932499db370c494ef0ff AS web
|
||||
FROM node:22.11.0-alpine3.20@sha256:f265794478aa0b1a23d85a492c8311ed795bc527c3fe7e43453b3c872dcd71a3 AS web
|
||||
|
||||
WORKDIR /usr/src/open-api/typescript-sdk
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
|
||||
58
server/package-lock.json
generated
58
server/package-lock.json
generated
@@ -83,7 +83,7 @@
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/node": "^22.8.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/picomatch": "^3.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
@@ -5110,9 +5110,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz",
|
||||
"integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==",
|
||||
"version": "22.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
||||
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.8"
|
||||
}
|
||||
@@ -8236,9 +8236,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/exiftool-vendored": {
|
||||
"version": "28.6.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.6.0.tgz",
|
||||
"integrity": "sha512-Cx8/8ov1tKEacHhsi7FNYdisIhKq/SeQfprYSpYzwBuJwkPmCV8w7tTIvUJRQX9rvopXhBA4eBf1FPXqTZW5vA==",
|
||||
"version": "28.7.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.7.0.tgz",
|
||||
"integrity": "sha512-0zoq6kBS1yPjzJs+p0qZDinWEA72PTKoRk5ETYKfmeRcZAkhv83Y3HCpbb/LdgJJywfm8BcIJGezrBHvL7dVnQ==",
|
||||
"dependencies": {
|
||||
"@photostructure/tz-lookup": "^11.0.0",
|
||||
"@types/luxon": "^3.4.2",
|
||||
@@ -8247,23 +8247,23 @@
|
||||
"luxon": "^3.5.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"exiftool-vendored.exe": "12.97.0",
|
||||
"exiftool-vendored.pl": "12.97.0"
|
||||
"exiftool-vendored.exe": "12.99.0",
|
||||
"exiftool-vendored.pl": "12.99.0"
|
||||
}
|
||||
},
|
||||
"node_modules/exiftool-vendored.exe": {
|
||||
"version": "12.97.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.97.0.tgz",
|
||||
"integrity": "sha512-+HxyFigEJOtwRjP7PhEslhZKuVW2V0hvmHPHtbVtNKGfAUGcfc95xNTjASQfKJvc+9ZuvzdEBPkEQmyA/ZYdIw==",
|
||||
"version": "12.99.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.99.0.tgz",
|
||||
"integrity": "sha512-ffpJHCzC9OYJqw4JlPNtCwRy02jwhmnSJEF/QqEjpuIWDEnlRBQP/yWRh1Nw21K1R4FB4yG5PlCgEDu09VQz/w==",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/exiftool-vendored.pl": {
|
||||
"version": "12.97.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.97.0.tgz",
|
||||
"integrity": "sha512-mXe9JEH3csfyPWcC7+H6IpfaokDMMr4S45n7MtiobGPdeeh+kFnf1SQ9cxg4sF403P6IKVeYYPbzgKMlpro9eQ==",
|
||||
"version": "12.99.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.99.0.tgz",
|
||||
"integrity": "sha512-qRVEPQxtoerXF+izJ0O7jGAr5o0Uyvnyu7ao5DTKzF+V7Fv3SurE0l43oCeZPFKo/Ld4V7vEylhFCm4IHVZKWA==",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"!win32"
|
||||
@@ -18258,9 +18258,9 @@
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "22.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz",
|
||||
"integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==",
|
||||
"version": "22.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
||||
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
||||
"requires": {
|
||||
"undici-types": "~6.19.8"
|
||||
}
|
||||
@@ -20579,29 +20579,29 @@
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
|
||||
},
|
||||
"exiftool-vendored": {
|
||||
"version": "28.6.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.6.0.tgz",
|
||||
"integrity": "sha512-Cx8/8ov1tKEacHhsi7FNYdisIhKq/SeQfprYSpYzwBuJwkPmCV8w7tTIvUJRQX9rvopXhBA4eBf1FPXqTZW5vA==",
|
||||
"version": "28.7.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.7.0.tgz",
|
||||
"integrity": "sha512-0zoq6kBS1yPjzJs+p0qZDinWEA72PTKoRk5ETYKfmeRcZAkhv83Y3HCpbb/LdgJJywfm8BcIJGezrBHvL7dVnQ==",
|
||||
"requires": {
|
||||
"@photostructure/tz-lookup": "^11.0.0",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"batch-cluster": "^13.0.0",
|
||||
"exiftool-vendored.exe": "12.97.0",
|
||||
"exiftool-vendored.pl": "12.97.0",
|
||||
"exiftool-vendored.exe": "12.99.0",
|
||||
"exiftool-vendored.pl": "12.99.0",
|
||||
"he": "^1.2.0",
|
||||
"luxon": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"exiftool-vendored.exe": {
|
||||
"version": "12.97.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.97.0.tgz",
|
||||
"integrity": "sha512-+HxyFigEJOtwRjP7PhEslhZKuVW2V0hvmHPHtbVtNKGfAUGcfc95xNTjASQfKJvc+9ZuvzdEBPkEQmyA/ZYdIw==",
|
||||
"version": "12.99.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.99.0.tgz",
|
||||
"integrity": "sha512-ffpJHCzC9OYJqw4JlPNtCwRy02jwhmnSJEF/QqEjpuIWDEnlRBQP/yWRh1Nw21K1R4FB4yG5PlCgEDu09VQz/w==",
|
||||
"optional": true
|
||||
},
|
||||
"exiftool-vendored.pl": {
|
||||
"version": "12.97.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.97.0.tgz",
|
||||
"integrity": "sha512-mXe9JEH3csfyPWcC7+H6IpfaokDMMr4S45n7MtiobGPdeeh+kFnf1SQ9cxg4sF403P6IKVeYYPbzgKMlpro9eQ==",
|
||||
"version": "12.99.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.99.0.tgz",
|
||||
"integrity": "sha512-qRVEPQxtoerXF+izJ0O7jGAr5o0Uyvnyu7ao5DTKzF+V7Fv3SurE0l43oCeZPFKo/Ld4V7vEylhFCm4IHVZKWA==",
|
||||
"optional": true
|
||||
},
|
||||
"express": {
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/node": "^22.8.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/picomatch": "^3.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
|
||||
@@ -31,11 +31,23 @@ describe(SearchService.name, () => {
|
||||
|
||||
describe('getDuplicates', () => {
|
||||
it('should get duplicates', async () => {
|
||||
assetMock.getDuplicates.mockResolvedValue([assetStub.hasDupe]);
|
||||
assetMock.getDuplicates.mockResolvedValue([assetStub.hasDupe, assetStub.hasDupe]);
|
||||
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([
|
||||
{ duplicateId: assetStub.hasDupe.duplicateId, assets: [expect.objectContaining({ id: assetStub.hasDupe.id })] },
|
||||
{
|
||||
duplicateId: assetStub.hasDupe.duplicateId,
|
||||
assets: [
|
||||
expect.objectContaining({ id: assetStub.hasDupe.id }),
|
||||
expect.objectContaining({ id: assetStub.hasDupe.id }),
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should update assets with duplicateId', async () => {
|
||||
assetMock.getDuplicates.mockResolvedValue([assetStub.hasDupe]);
|
||||
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([]);
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.hasDupe.id], { duplicateId: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleQueueSearchDuplicates', () => {
|
||||
|
||||
@@ -16,8 +16,24 @@ import { usePagination } from 'src/utils/pagination';
|
||||
export class DuplicateService extends BaseService {
|
||||
async getDuplicates(auth: AuthDto): Promise<DuplicateResponseDto[]> {
|
||||
const res = await this.assetRepository.getDuplicates({ userIds: [auth.user.id] });
|
||||
|
||||
return mapDuplicateResponse(res.map((a) => mapAsset(a, { auth, withStack: true })));
|
||||
const uniqueAssetIds: string[] = [];
|
||||
const duplicates = mapDuplicateResponse(res.map((a) => mapAsset(a, { auth, withStack: true }))).filter(
|
||||
(duplicate) => {
|
||||
if (duplicate.assets.length === 1) {
|
||||
uniqueAssetIds.push(duplicate.assets[0].id);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
);
|
||||
if (uniqueAssetIds.length > 0) {
|
||||
try {
|
||||
await this.assetRepository.updateAll(uniqueAssetIds, { duplicateId: null });
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Failed to remove duplicateId from assets: ${error.message}`);
|
||||
}
|
||||
}
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.QUEUE_DUPLICATE_DETECTION, queue: QueueName.DUPLICATE_DETECTION })
|
||||
|
||||
@@ -2133,7 +2133,7 @@ describe(MediaService.name, () => {
|
||||
'-map 0:1',
|
||||
'-g 256',
|
||||
'-v verbose',
|
||||
'-vf scale_rkrga=-2:720:format=nv12:afbc=1',
|
||||
'-vf scale_rkrga=-2:720:format=nv12:afbc=1:async_depth=4',
|
||||
'-level 51',
|
||||
'-rc_mode CQP',
|
||||
'-qp_init 23',
|
||||
@@ -2204,7 +2204,7 @@ describe(MediaService.name, () => {
|
||||
inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']),
|
||||
outputOptions: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
'scale_rkrga=-2:720:format=p010:afbc=1,hwmap=derive_device=opencl:mode=read,tonemap_opencl=format=nv12:r=pc:p=bt709:t=bt709:m=bt709:tonemap=hable:desat=0:tonemap_mode=lum:peak=100,hwmap=derive_device=rkmpp:mode=write:reverse=1,format=drm_prime',
|
||||
'scale_rkrga=-2:720:format=p010:afbc=1:async_depth=4,hwmap=derive_device=opencl:mode=read,tonemap_opencl=format=nv12:r=pc:p=bt709:t=bt709:m=bt709:tonemap=hable:desat=0:tonemap_mode=lum:peak=100,hwmap=derive_device=rkmpp:mode=write:reverse=1,format=drm_prime',
|
||||
),
|
||||
]),
|
||||
twoPass: false,
|
||||
@@ -2212,6 +2212,28 @@ describe(MediaService.name, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should set hardware decoding options for rkmpp when hardware decoding is enabled with no OpenCL on non-HDR file', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
storageMock.stat.mockResolvedValue({ isFile: () => false, isCharacterDevice: () => false } as Stats);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.noAudioStreams);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
'/original/path.ext',
|
||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||
expect.objectContaining({
|
||||
inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']),
|
||||
outputOptions: expect.arrayContaining([
|
||||
expect.stringContaining('scale_rkrga=-2:720:format=nv12:afbc=1:async_depth=4'),
|
||||
]),
|
||||
twoPass: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use software decoding and tone-mapping if hardware decoding is disabled', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
storageMock.stat.mockResolvedValue({ isFile: () => true, isCharacterDevice: () => true } as Stats);
|
||||
@@ -2236,7 +2258,7 @@ describe(MediaService.name, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should use software decoding and tone-mapping if opencl is not available', async () => {
|
||||
it('should use software tone-mapping if opencl is not available', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
storageMock.stat.mockResolvedValue({ isFile: () => false, isCharacterDevice: () => false } as Stats);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
@@ -2249,7 +2271,7 @@ describe(MediaService.name, () => {
|
||||
'/original/path.ext',
|
||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||
expect.objectContaining({
|
||||
inputOptions: [],
|
||||
inputOptions: expect.any(Array),
|
||||
outputOptions: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
'tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p',
|
||||
|
||||
@@ -329,9 +329,11 @@ export class MediaService extends BaseService {
|
||||
}
|
||||
|
||||
if (ffmpeg.accel === TranscodeHWAccel.DISABLED) {
|
||||
this.logger.log(`Encoding video ${asset.id} without hardware acceleration`);
|
||||
this.logger.log(`Transcoding video ${asset.id} without hardware acceleration`);
|
||||
} else {
|
||||
this.logger.log(`Encoding video ${asset.id} with ${ffmpeg.accel.toUpperCase()} acceleration`);
|
||||
this.logger.log(
|
||||
`Transcoding video ${asset.id} with ${ffmpeg.accel.toUpperCase()}-accelerated encoding and${ffmpeg.accelDecode ? '' : ' software'} decoding`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -341,10 +343,26 @@ export class MediaService extends BaseService {
|
||||
if (ffmpeg.accel === TranscodeHWAccel.DISABLED) {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled`);
|
||||
const config = BaseConfig.create({ ...ffmpeg, accel: TranscodeHWAccel.DISABLED });
|
||||
command = config.getCommand(target, mainVideoStream, mainAudioStream);
|
||||
await this.mediaRepository.transcode(input, output, command);
|
||||
|
||||
let partialFallbackSuccess = false;
|
||||
if (ffmpeg.accelDecode) {
|
||||
try {
|
||||
this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()}-accelerated encoding and software decoding`);
|
||||
const config = BaseConfig.create({ ...ffmpeg, accelDecode: false });
|
||||
command = config.getCommand(target, mainVideoStream, mainAudioStream);
|
||||
await this.mediaRepository.transcode(input, output, command);
|
||||
partialFallbackSuccess = true;
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error occurred during transcoding: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!partialFallbackSuccess) {
|
||||
this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled`);
|
||||
const config = BaseConfig.create({ ...ffmpeg, accel: TranscodeHWAccel.DISABLED });
|
||||
command = config.getCommand(target, mainVideoStream, mainAudioStream);
|
||||
await this.mediaRepository.transcode(input, output, command);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`Successfully encoded ${asset.id}`);
|
||||
@@ -502,7 +520,7 @@ export class MediaService extends BaseService {
|
||||
const maliDeviceStat = await this.storageRepository.stat('/dev/mali0');
|
||||
this.maliOpenCL = maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice();
|
||||
} catch {
|
||||
this.logger.debug('OpenCL not available for transcoding, so RKMPP acceleration will use CPU decoding');
|
||||
this.logger.debug('OpenCL not available for transcoding, so RKMPP acceleration will use CPU tonemapping');
|
||||
this.maliOpenCL = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +58,9 @@ export class BaseConfig implements VideoCodecSWConfig {
|
||||
break;
|
||||
}
|
||||
case TranscodeHWAccel.RKMPP: {
|
||||
handler =
|
||||
config.accelDecode && hasMaliOpenCL
|
||||
? new RkmppHwDecodeConfig(config, devices)
|
||||
: new RkmppSwDecodeConfig(config, devices);
|
||||
handler = config.accelDecode
|
||||
? new RkmppHwDecodeConfig(config, devices, hasMaliOpenCL)
|
||||
: new RkmppSwDecodeConfig(config, devices);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -327,6 +326,32 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig {
|
||||
this.devices = this.validateDevices(devices);
|
||||
}
|
||||
|
||||
getScalingFilter(videoStream: VideoStreamInfo, format: string) {
|
||||
return `scale_${this.config.accel}=${this.getScaling(videoStream)}:format=${format}`;
|
||||
}
|
||||
|
||||
getHwDecodeFilterOptions(videoStream: VideoStreamInfo) {
|
||||
const options = [];
|
||||
const tonemapOptions = this.getToneMapping(videoStream);
|
||||
if (this.shouldScale(videoStream) || tonemapOptions.length === 0) {
|
||||
const format = tonemapOptions.length === 0 ? 'nv12' : 'p010';
|
||||
options.push(this.getScalingFilter(videoStream, format));
|
||||
} else if (tonemapOptions.length > 0) {
|
||||
options.push('format=p010');
|
||||
}
|
||||
options.push(...tonemapOptions);
|
||||
if (options.length === 0 && !videoStream.pixelFormat.endsWith('420p')) {
|
||||
options.push('format=nv12');
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||
return this.config.accelDecode
|
||||
? this.getHwDecodeFilterOptions(videoStream)
|
||||
: [`hwupload_${this.config.accel}`, ...this.getHwDecodeFilterOptions(videoStream)];
|
||||
}
|
||||
|
||||
getSupportedCodecs() {
|
||||
return [VideoCodec.H264, VideoCodec.HEVC];
|
||||
}
|
||||
@@ -370,6 +395,20 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig {
|
||||
|
||||
return `/dev/dri/${deviceName}`;
|
||||
}
|
||||
|
||||
getInputThreadOptions() {
|
||||
if (this.config.accelDecode) {
|
||||
return [`-threads 1`];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
getOutputThreadOptions() {
|
||||
if (this.config.accelDecode) {
|
||||
return [];
|
||||
}
|
||||
return super.getOutputThreadOptions();
|
||||
}
|
||||
}
|
||||
|
||||
export class ThumbnailConfig extends BaseConfig {
|
||||
@@ -504,15 +543,11 @@ export class AV1Config extends BaseConfig {
|
||||
}
|
||||
}
|
||||
|
||||
export class NvencSwDecodeConfig extends BaseHWConfig {
|
||||
export class NvencConfig extends BaseHWConfig {
|
||||
getSupportedCodecs() {
|
||||
return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.AV1];
|
||||
}
|
||||
|
||||
getBaseInputOptions() {
|
||||
return ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'];
|
||||
}
|
||||
|
||||
getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
|
||||
const options = [
|
||||
// below settings recommended from https://docs.nvidia.com/video-technologies/video-codec-sdk/12.0/ffmpeg-with-nvidia-gpu/index.html#command-line-for-latency-tolerant-high-quality-transcoding
|
||||
@@ -531,16 +566,6 @@ export class NvencSwDecodeConfig extends BaseHWConfig {
|
||||
return options;
|
||||
}
|
||||
|
||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||
const options = this.getToneMapping(videoStream);
|
||||
options.push('hwupload_cuda');
|
||||
if (this.shouldScale(videoStream)) {
|
||||
options.push(`scale_cuda=${this.getScaling(videoStream)}:format=nv12`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
getPresetOptions() {
|
||||
let presetIndex = this.getPresetIndex();
|
||||
if (presetIndex < 0) {
|
||||
@@ -570,10 +595,6 @@ export class NvencSwDecodeConfig extends BaseHWConfig {
|
||||
}
|
||||
}
|
||||
|
||||
getThreadOptions() {
|
||||
return [];
|
||||
}
|
||||
|
||||
getRefs() {
|
||||
const bframes = this.getBFrames();
|
||||
if (bframes > 0 && bframes < 3 && this.config.refs < 3) {
|
||||
@@ -581,27 +602,14 @@ export class NvencSwDecodeConfig extends BaseHWConfig {
|
||||
}
|
||||
return this.config.refs;
|
||||
}
|
||||
}
|
||||
|
||||
export class NvencHwDecodeConfig extends NvencSwDecodeConfig {
|
||||
getBaseInputOptions() {
|
||||
if (!this.config.accelDecode) {
|
||||
return ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'];
|
||||
}
|
||||
return ['-hwaccel cuda', '-hwaccel_output_format cuda', '-noautorotate', ...this.getInputThreadOptions()];
|
||||
}
|
||||
|
||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||
const options = [];
|
||||
if (this.shouldScale(videoStream)) {
|
||||
options.push(`scale_cuda=${this.getScaling(videoStream)}`);
|
||||
}
|
||||
options.push(...this.getToneMapping(videoStream));
|
||||
if (options.length > 0) {
|
||||
options[options.length - 1] += ':format=nv12';
|
||||
} else if (!videoStream.pixelFormat.endsWith('420p')) {
|
||||
options.push('format=nv12');
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
getToneMapping(videoStream: VideoStreamInfo) {
|
||||
if (!this.shouldToneMap(videoStream)) {
|
||||
return [];
|
||||
@@ -617,28 +625,36 @@ export class NvencHwDecodeConfig extends NvencSwDecodeConfig {
|
||||
'tonemap_mode=lum',
|
||||
`transfer=${transfer}`,
|
||||
'peak=100',
|
||||
'format=nv12',
|
||||
];
|
||||
|
||||
return [`tonemap_cuda=${tonemapOptions.join(':')}`];
|
||||
}
|
||||
|
||||
getInputThreadOptions() {
|
||||
return [`-threads 1`];
|
||||
}
|
||||
|
||||
getOutputThreadOptions() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export class QsvSwDecodeConfig extends BaseHWConfig {
|
||||
export class QsvConfig extends BaseHWConfig {
|
||||
getBaseInputOptions() {
|
||||
if (this.devices.length === 0) {
|
||||
throw new Error('No QSV device found');
|
||||
}
|
||||
|
||||
let qsvString = '';
|
||||
const hwDevice = this.getPreferredHardwareDevice();
|
||||
if (this.config.accelDecode) {
|
||||
const options = [
|
||||
'-hwaccel qsv',
|
||||
'-hwaccel_output_format qsv',
|
||||
'-async_depth 4',
|
||||
'-noautorotate',
|
||||
...this.getInputThreadOptions(),
|
||||
];
|
||||
if (hwDevice) {
|
||||
options.push(`-qsv_device ${hwDevice}`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
let qsvString = '';
|
||||
if (hwDevice) {
|
||||
qsvString = `,child_device=${hwDevice}`;
|
||||
}
|
||||
@@ -655,15 +671,6 @@ export class QsvSwDecodeConfig extends BaseHWConfig {
|
||||
return options;
|
||||
}
|
||||
|
||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||
const options = this.getToneMapping(videoStream);
|
||||
options.push('hwupload=extra_hw_frames=64');
|
||||
if (this.shouldScale(videoStream)) {
|
||||
options.push(`scale_qsv=${this.getScaling(videoStream)}:mode=hq:format=nv12`);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
getPresetOptions() {
|
||||
let presetIndex = this.getPresetIndex();
|
||||
if (presetIndex < 0) {
|
||||
@@ -709,44 +716,9 @@ export class QsvSwDecodeConfig extends BaseHWConfig {
|
||||
getScaling(videoStream: VideoStreamInfo): string {
|
||||
return super.getScaling(videoStream, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export class QsvHwDecodeConfig extends QsvSwDecodeConfig {
|
||||
getBaseInputOptions() {
|
||||
if (this.devices.length === 0) {
|
||||
throw new Error('No QSV device found');
|
||||
}
|
||||
|
||||
const options = [
|
||||
'-hwaccel qsv',
|
||||
'-hwaccel_output_format qsv',
|
||||
'-async_depth 4',
|
||||
'-noautorotate',
|
||||
...this.getInputThreadOptions(),
|
||||
];
|
||||
const hwDevice = this.getPreferredHardwareDevice();
|
||||
if (hwDevice) {
|
||||
options.push(`-qsv_device ${hwDevice}`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||
const options = [];
|
||||
const tonemapOptions = this.getToneMapping(videoStream);
|
||||
if (this.shouldScale(videoStream) || tonemapOptions.length === 0) {
|
||||
let scaling = `scale_qsv=${this.getScaling(videoStream)}:async_depth=4:mode=hq`;
|
||||
if (tonemapOptions.length === 0) {
|
||||
scaling += ':format=nv12';
|
||||
}
|
||||
options.push(scaling);
|
||||
}
|
||||
options.push(...tonemapOptions);
|
||||
if (options.length === 0 && !videoStream.pixelFormat.endsWith('420p')) {
|
||||
options.push('format=nv12');
|
||||
}
|
||||
return options;
|
||||
getScalingFilter(videoStream: VideoStreamInfo, format: string) {
|
||||
return `scale_qsv=${this.getScaling(videoStream)}:async_depth=4:mode=hq:format=${format}`;
|
||||
}
|
||||
|
||||
getToneMapping(videoStream: VideoStreamInfo): string[] {
|
||||
@@ -773,19 +745,27 @@ export class QsvHwDecodeConfig extends QsvSwDecodeConfig {
|
||||
'hwmap=derive_device=qsv:reverse=1,format=qsv',
|
||||
];
|
||||
}
|
||||
|
||||
getInputThreadOptions() {
|
||||
return [`-threads 1`];
|
||||
}
|
||||
}
|
||||
|
||||
export class VaapiSwDecodeConfig extends BaseHWConfig {
|
||||
export class VaapiConfig extends BaseHWConfig {
|
||||
getBaseInputOptions() {
|
||||
if (this.devices.length === 0) {
|
||||
throw new Error('No VAAPI device found');
|
||||
}
|
||||
|
||||
let hwDevice = this.getPreferredHardwareDevice();
|
||||
if (this.config.accelDecode) {
|
||||
const options = [
|
||||
'-hwaccel vaapi',
|
||||
'-hwaccel_output_format vaapi',
|
||||
'-noautorotate',
|
||||
...this.getInputThreadOptions(),
|
||||
];
|
||||
if (hwDevice) {
|
||||
options.push(`-hwaccel_device ${hwDevice}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hwDevice) {
|
||||
hwDevice = `/dev/dri/${this.devices[0]}`;
|
||||
}
|
||||
@@ -793,16 +773,6 @@ export class VaapiSwDecodeConfig extends BaseHWConfig {
|
||||
return [`-init_hw_device vaapi=accel:${hwDevice}`, '-filter_hw_device accel'];
|
||||
}
|
||||
|
||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||
const options = this.getToneMapping(videoStream);
|
||||
options.push('hwupload=extra_hw_frames=64');
|
||||
if (this.shouldScale(videoStream)) {
|
||||
options.push(`scale_vaapi=${this.getScaling(videoStream)}:mode=hq:out_range=pc:format=nv12`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
getPresetOptions() {
|
||||
let presetIndex = this.getPresetIndex();
|
||||
if (presetIndex < 0) {
|
||||
@@ -844,43 +814,9 @@ export class VaapiSwDecodeConfig extends BaseHWConfig {
|
||||
useCQP() {
|
||||
return this.config.cqMode !== CQMode.ICQ || this.config.targetVideoCodec === VideoCodec.VP9;
|
||||
}
|
||||
}
|
||||
|
||||
export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig {
|
||||
getBaseInputOptions() {
|
||||
if (this.devices.length === 0) {
|
||||
throw new Error('No VAAPI device found');
|
||||
}
|
||||
|
||||
const options = [
|
||||
'-hwaccel vaapi',
|
||||
'-hwaccel_output_format vaapi',
|
||||
'-noautorotate',
|
||||
...this.getInputThreadOptions(),
|
||||
];
|
||||
const hwDevice = this.getPreferredHardwareDevice();
|
||||
if (hwDevice) {
|
||||
options.push(`-hwaccel_device ${hwDevice}`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||
const options = [];
|
||||
const tonemapOptions = this.getToneMapping(videoStream);
|
||||
if (this.shouldScale(videoStream) || tonemapOptions.length === 0) {
|
||||
let scaling = `scale_vaapi=${this.getScaling(videoStream)}:mode=hq:out_range=pc`;
|
||||
if (tonemapOptions.length === 0) {
|
||||
scaling += ':format=nv12';
|
||||
}
|
||||
options.push(scaling);
|
||||
}
|
||||
options.push(...tonemapOptions);
|
||||
if (options.length === 0 && !videoStream.pixelFormat.endsWith('420p')) {
|
||||
options.push('format=nv12');
|
||||
}
|
||||
return options;
|
||||
getScalingFilter(videoStream: VideoStreamInfo, format: string) {
|
||||
return `scale_vaapi=${this.getScaling(videoStream)}:mode=hq:out_range=pc:format=${format}`;
|
||||
}
|
||||
|
||||
getToneMapping(videoStream: VideoStreamInfo): string[] {
|
||||
@@ -907,29 +843,33 @@ export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig {
|
||||
'hwmap=derive_device=vaapi:reverse=1,format=vaapi',
|
||||
];
|
||||
}
|
||||
|
||||
getInputThreadOptions() {
|
||||
return [`-threads 1`];
|
||||
}
|
||||
}
|
||||
|
||||
export class RkmppSwDecodeConfig extends BaseHWConfig {
|
||||
export class RkmppConfig extends BaseHWConfig {
|
||||
protected hasMaliOpenCL: boolean;
|
||||
constructor(
|
||||
protected config: SystemConfigFFmpegDto,
|
||||
devices: string[] = [],
|
||||
hasMaliOpenCL = false,
|
||||
) {
|
||||
super(config, devices);
|
||||
this.hasMaliOpenCL = hasMaliOpenCL;
|
||||
}
|
||||
|
||||
eligibleForTwoPass(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getBaseInputOptions(): string[] {
|
||||
getBaseInputOptions() {
|
||||
if (this.devices.length === 0) {
|
||||
throw new Error('No RKMPP device found');
|
||||
}
|
||||
return [];
|
||||
|
||||
if (this.config.accelDecode) {
|
||||
return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga', '-noautorotate'];
|
||||
}
|
||||
|
||||
return ['-init_hw_device rkmpp=hw', '-filter_hw_device hw'];
|
||||
}
|
||||
|
||||
getPresetOptions() {
|
||||
@@ -962,32 +902,29 @@ export class RkmppSwDecodeConfig extends BaseHWConfig {
|
||||
return [VideoCodec.H264, VideoCodec.HEVC];
|
||||
}
|
||||
|
||||
getVideoCodec(): string {
|
||||
return `${this.config.targetVideoCodec}_rkmpp`;
|
||||
}
|
||||
}
|
||||
|
||||
export class RkmppHwDecodeConfig extends RkmppSwDecodeConfig {
|
||||
getBaseInputOptions() {
|
||||
if (this.devices.length === 0) {
|
||||
throw new Error('No RKMPP device found');
|
||||
}
|
||||
|
||||
return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga', '-noautorotate'];
|
||||
}
|
||||
|
||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||
if (this.shouldToneMap(videoStream)) {
|
||||
const { primaries, transfer, matrix } = this.getColors();
|
||||
if (this.hasMaliOpenCL) {
|
||||
return [
|
||||
// use RKMPP for scaling, OpenCL for tone mapping
|
||||
`scale_rkrga=${this.getScaling(videoStream)}:format=p010:afbc=1:async_depth=4`,
|
||||
'hwmap=derive_device=opencl:mode=read',
|
||||
`tonemap_opencl=format=nv12:r=pc:p=${primaries}:t=${transfer}:m=${matrix}:tonemap=${this.config.tonemap}:desat=0:tonemap_mode=lum:peak=100`,
|
||||
'hwmap=derive_device=rkmpp:mode=write:reverse=1',
|
||||
'format=drm_prime',
|
||||
];
|
||||
}
|
||||
return [
|
||||
`scale_rkrga=${this.getScaling(videoStream)}:format=p010:afbc=1`,
|
||||
'hwmap=derive_device=opencl:mode=read',
|
||||
`tonemap_opencl=format=nv12:r=pc:p=${primaries}:t=${transfer}:m=${matrix}:tonemap=${this.config.tonemap}:desat=0:tonemap_mode=lum:peak=100`,
|
||||
'hwmap=derive_device=rkmpp:mode=write:reverse=1',
|
||||
'format=drm_prime',
|
||||
// use RKMPP for scaling, CPU for tone mapping (only works on RK3588, which supports 10-bit output)
|
||||
`scale_rkrga=${this.getScaling(videoStream)}:format=p010:afbc=1:async_depth=4`,
|
||||
'hwdownload',
|
||||
'format=p010',
|
||||
`tonemapx=tonemap=${this.config.tonemap}:desat=0:p=${primaries}:t=${transfer}:m=${matrix}:r=pc:peak=100:format=yuv420p`,
|
||||
'hwupload',
|
||||
];
|
||||
} else if (this.shouldScale(videoStream)) {
|
||||
return [`scale_rkrga=${this.getScaling(videoStream)}:format=nv12:afbc=1`];
|
||||
return [`scale_rkrga=${this.getScaling(videoStream)}:format=nv12:afbc=1:async_depth=4`];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:22.10.0-alpine3.20@sha256:fc95a044b87e95507c60c1f8c829e5d98ddf46401034932499db370c494ef0ff
|
||||
FROM node:22.11.0-alpine3.20@sha256:f265794478aa0b1a23d85a492c8311ed795bc527c3fe7e43453b3c872dcd71a3
|
||||
|
||||
RUN apk add --no-cache tini
|
||||
USER node
|
||||
|
||||
Reference in New Issue
Block a user