mirror of
https://github.com/immich-app/immich.git
synced 2026-07-07 04:57:05 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3beaeba72 | |||
| d0b779d51e | |||
| 61210eab28 | |||
| b6cb534ea6 |
@@ -37,7 +37,6 @@
|
|||||||
<a href="readme_i18n/README_ar_JO.md">العربية</a>
|
<a href="readme_i18n/README_ar_JO.md">العربية</a>
|
||||||
<a href="readme_i18n/README_vi_VN.md">Tiếng Việt</a>
|
<a href="readme_i18n/README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="readme_i18n/README_th_TH.md">ภาษาไทย</a>
|
<a href="readme_i18n/README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="readme_i18n/README_ml_IN.md">മലയാളം</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ You can trigger a database backup manually:
|
|||||||
|
|
||||||
1. Go to **Administration > Job Queues**
|
1. Go to **Administration > Job Queues**
|
||||||
2. Click **Create job** in the top right
|
2. Click **Create job** in the top right
|
||||||
3. Select **Create Database Dump** and click **Confirm**
|
3. Select **Create Database Backup** and click **Confirm**
|
||||||
|
|
||||||
The backup will appear in `UPLOAD_LOCATION/backups` and counts toward your retention limit.
|
The backup will appear in `UPLOAD_LOCATION/backups` and counts toward your retention limit.
|
||||||
|
|
||||||
|
|||||||
@@ -28,15 +28,11 @@ docker image prune
|
|||||||
|
|
||||||
## Versioning Policy
|
## Versioning Policy
|
||||||
|
|
||||||
Immich follows [semantic versioning][semver], which tags releases in the format `<major>.<minor>.<patch>`.
|
Immich follows [semantic versioning][semver], which tags releases in the format `<major>.<minor>.<patch>`. We intend for breaking changes to be limited to major version releases.
|
||||||
We intend for breaking changes, including those to the API or deployment, to be limited to major version releases.
|
You can configure your Docker image to point to the current major version by using a metatag, such as `:v3`.
|
||||||
You can configure your Docker image to point to the current major version by using a metatag, such as `:v3`. These metatags do not follow release candidates.
|
|
||||||
|
|
||||||
The mobile app is typically compatible with the current and prior major version. However, the server is only compatible with the matching major version.
|
Currently, we have no plans to backport patches to earlier versions. We encourage all users to run the most recent release of Immich.
|
||||||
Thus, we recommend upgrading all mobile clients before upgrading the server to ensure compatibility.
|
Switching back to an earlier version, even within the same minor release tag, is not supported.
|
||||||
|
|
||||||
We do not backport patches to earlier versions. We encourage all users to run the most recent stable release of Immich.
|
|
||||||
Downgrading to an earlier version, even within the same minor version, is not supported.
|
|
||||||
|
|
||||||
[semver]: https://semver.org/
|
[semver]: https://semver.org/
|
||||||
|
|
||||||
|
|||||||
@@ -431,10 +431,6 @@
|
|||||||
"transcoding_realtime_description": "Allows transcoding to be performed in real-time as the video is being streamed. Enables quality switching, but may cause higher playback latency and stuttering depending on server capabilities.",
|
"transcoding_realtime_description": "Allows transcoding to be performed in real-time as the video is being streamed. Enables quality switching, but may cause higher playback latency and stuttering depending on server capabilities.",
|
||||||
"transcoding_realtime_enabled": "Enable real-time transcoding",
|
"transcoding_realtime_enabled": "Enable real-time transcoding",
|
||||||
"transcoding_realtime_enabled_description": "If disabled, the server will refuse to start new real-time transcoding sessions.",
|
"transcoding_realtime_enabled_description": "If disabled, the server will refuse to start new real-time transcoding sessions.",
|
||||||
"transcoding_realtime_resolutions": "Resolutions",
|
|
||||||
"transcoding_realtime_resolutions_description": "The resolutions offered for real-time transcoding. A variant is only offered when its resolution is no larger than the source. Higher resolutions may cause playback issues if the server cannot transcode them quickly enough.",
|
|
||||||
"transcoding_realtime_video_codecs": "Video codecs",
|
|
||||||
"transcoding_realtime_video_codecs_description": "The video codecs offered for real-time transcoding. Clients will choose the best option they support during playback. AV1 is more efficient than HEVC, which is more efficient than H.264. When using hardware acceleration, only select the codecs the accelerator can encode. When using software transcoding, note that H.264 is faster than AV1, which is faster than HEVC.",
|
|
||||||
"transcoding_reference_frames": "Reference frames",
|
"transcoding_reference_frames": "Reference frames",
|
||||||
"transcoding_reference_frames_description": "The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically.",
|
"transcoding_reference_frames_description": "The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically.",
|
||||||
"transcoding_required_description": "Only videos not in an accepted format",
|
"transcoding_required_description": "Only videos not in an accepted format",
|
||||||
@@ -597,7 +593,6 @@
|
|||||||
"asset_viewer_settings_title": "Asset Viewer",
|
"asset_viewer_settings_title": "Asset Viewer",
|
||||||
"assets": "Assets",
|
"assets": "Assets",
|
||||||
"assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album",
|
"assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album",
|
||||||
"assets_added_to_album_partial_count": "Added {successCount} out of {totalCount} {totalCount, plural, one {asset} other {assets}} to the album",
|
|
||||||
"assets_added_to_albums_count": "Added {assetTotal, plural, one {# asset} other {# assets}} to {albumTotal, plural, one {# album} other {# albums}}",
|
"assets_added_to_albums_count": "Added {assetTotal, plural, one {# asset} other {# assets}} to {albumTotal, plural, one {# album} other {# albums}}",
|
||||||
"assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} cannot be added to the album",
|
"assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} cannot be added to the album",
|
||||||
"assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} cannot be added to any of the albums",
|
"assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} cannot be added to any of the albums",
|
||||||
|
|||||||
@@ -70,8 +70,7 @@
|
|||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
enableGPUValidationMode = "1"
|
enableGPUValidationMode = "1"
|
||||||
allowLocationSimulation = "YES"
|
allowLocationSimulation = "YES">
|
||||||
queueDebuggingEnabled = "NO">
|
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
|
|||||||
@@ -1,19 +1,65 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
enum AspectRatioPreset {
|
class CropAspectRatio {
|
||||||
free(ratio: null, label: 'Free', icon: Icons.crop_free_rounded),
|
final int? numerator;
|
||||||
square(ratio: 1.0, label: '1:1', icon: Icons.crop_square_rounded),
|
final int? denominator;
|
||||||
ratio16x9(ratio: 16 / 9, label: '16:9', icon: Icons.crop_16_9_rounded),
|
|
||||||
ratio3x2(ratio: 3 / 2, label: '3:2', icon: Icons.crop_3_2_rounded),
|
|
||||||
ratio7x5(ratio: 7 / 5, label: '7:5', icon: Icons.crop_7_5_rounded),
|
|
||||||
ratio9x16(ratio: 9 / 16, label: '9:16', icon: Icons.crop_16_9_rounded, iconRotated: true),
|
|
||||||
ratio2x3(ratio: 2 / 3, label: '2:3', icon: Icons.crop_3_2_rounded, iconRotated: true),
|
|
||||||
ratio5x7(ratio: 5 / 7, label: '5:7', icon: Icons.crop_7_5_rounded, iconRotated: true);
|
|
||||||
|
|
||||||
final double? ratio;
|
final String? customLabel;
|
||||||
final String label;
|
final IconData? icon;
|
||||||
final IconData icon;
|
|
||||||
final bool iconRotated;
|
|
||||||
|
|
||||||
const AspectRatioPreset({required this.ratio, required this.label, required this.icon, this.iconRotated = false});
|
const CropAspectRatio({this.numerator, this.denominator, this.customLabel, this.icon});
|
||||||
|
|
||||||
|
static const free = CropAspectRatio(customLabel: "Free", icon: Icons.crop_free);
|
||||||
|
static const original = CropAspectRatio(customLabel: "Original", icon: Icons.crop_original);
|
||||||
|
|
||||||
|
String get label {
|
||||||
|
return customLabel ?? (numerator != null && denominator != null ? '$numerator:$denominator' : 'Free');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get hasFlippedVariant => numerator != denominator;
|
||||||
|
double? get ratio => (numerator != null && denominator != null) ? numerator! / denominator! : null;
|
||||||
|
|
||||||
|
CropAspectRatio get flipped {
|
||||||
|
return CropAspectRatio(numerator: denominator, denominator: numerator, customLabel: customLabel, icon: icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return other is CropAspectRatio &&
|
||||||
|
other.numerator == numerator &&
|
||||||
|
other.denominator == denominator &&
|
||||||
|
other.customLabel == customLabel &&
|
||||||
|
other.icon == icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return numerator.hashCode ^ denominator.hashCode ^ customLabel.hashCode ^ icon.hashCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const aspectRatioFree = CropAspectRatio(customLabel: "Free", icon: Icons.crop_free);
|
||||||
|
const aspectRatioOriginal = CropAspectRatio(customLabel: "Original", icon: Icons.crop_original);
|
||||||
|
|
||||||
|
final aspectRatioPresets = [
|
||||||
|
CropAspectRatio.free,
|
||||||
|
CropAspectRatio.original,
|
||||||
|
|
||||||
|
const CropAspectRatio(numerator: 1, denominator: 1),
|
||||||
|
|
||||||
|
// lanscape
|
||||||
|
const CropAspectRatio(numerator: 16, denominator: 9),
|
||||||
|
const CropAspectRatio(numerator: 3, denominator: 2),
|
||||||
|
const CropAspectRatio(numerator: 7, denominator: 5),
|
||||||
|
const CropAspectRatio(numerator: 4, denominator: 3),
|
||||||
|
|
||||||
|
// portrait
|
||||||
|
const CropAspectRatio(numerator: 16, denominator: 9).flipped,
|
||||||
|
const CropAspectRatio(numerator: 3, denominator: 2).flipped,
|
||||||
|
const CropAspectRatio(numerator: 7, denominator: 5).flipped,
|
||||||
|
const CropAspectRatio(numerator: 4, denominator: 3).flipped,
|
||||||
|
];
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AspectRatioButton extends StatelessWidget {
|
class _AspectRatioButton extends StatelessWidget {
|
||||||
final AspectRatioPreset ratio;
|
final CropAspectRatio ratio;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final VoidCallback onPressed;
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
@@ -162,15 +162,16 @@ class _AspectRatioButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final color = isSelected ? context.primaryColor : context.themeData.iconTheme.color;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: 36,
|
iconSize: 36,
|
||||||
icon: Transform.rotate(
|
icon: ratio.ratio != null
|
||||||
angle: ratio.iconRotated ? pi / 2 : 0,
|
? _AspectRatioRect(ratio: ratio.ratio!, color: color)
|
||||||
child: Icon(ratio.icon, color: isSelected ? context.primaryColor : context.themeData.iconTheme.color),
|
: Icon(ratio.icon, color: color),
|
||||||
),
|
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
Text(ratio.label, style: context.textTheme.displayMedium),
|
Text(ratio.label, style: context.textTheme.displayMedium),
|
||||||
@@ -179,6 +180,32 @@ class _AspectRatioButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AspectRatioRect extends StatelessWidget {
|
||||||
|
final double ratio;
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
const _AspectRatioRect({required this.ratio, required this.color});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
child: Center(
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: ratio,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: color ?? Colors.transparent, width: 3),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _AspectRatioSelector extends ConsumerWidget {
|
class _AspectRatioSelector extends ConsumerWidget {
|
||||||
const _AspectRatioSelector();
|
const _AspectRatioSelector();
|
||||||
|
|
||||||
@@ -187,22 +214,16 @@ class _AspectRatioSelector extends ConsumerWidget {
|
|||||||
final editorState = ref.watch(editorStateProvider);
|
final editorState = ref.watch(editorStateProvider);
|
||||||
final editorNotifier = ref.read(editorStateProvider.notifier);
|
final editorNotifier = ref.read(editorStateProvider.notifier);
|
||||||
|
|
||||||
// the whole crop view is rotated, so we need to swap the aspect ratio when the rotation is 90 or 270 degrees
|
|
||||||
double? selectedAspectRatio = editorState.aspectRatio;
|
|
||||||
if (editorState.rotationAngle % 180 != 0 && selectedAspectRatio != null) {
|
|
||||||
selectedAspectRatio = 1 / selectedAspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: AspectRatioPreset.values.map((entry) {
|
children: aspectRatioPresets.map((entry) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
child: _AspectRatioButton(
|
child: _AspectRatioButton(
|
||||||
ratio: entry,
|
ratio: entry,
|
||||||
isSelected: selectedAspectRatio == entry.ratio,
|
isSelected: editorState.aspectRatio == entry,
|
||||||
onPressed: () => editorNotifier.setAspectRatio(entry.ratio),
|
onPressed: () => editorNotifier.setAspectRatio(entry),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@@ -357,8 +378,24 @@ class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProvi
|
|||||||
final editorState = ref.watch(editorStateProvider);
|
final editorState = ref.watch(editorStateProvider);
|
||||||
final editorNotifier = ref.read(editorStateProvider.notifier);
|
final editorNotifier = ref.read(editorStateProvider.notifier);
|
||||||
|
|
||||||
ref.listen(editorStateProvider, (_, current) {
|
ref.listen(editorStateProvider, (previous, current) {
|
||||||
cropController.aspectRatio = current.aspectRatio;
|
// Only re-apply the aspect ratio when it changes, otherwise the crop rect will shrink on every rotation
|
||||||
|
if (previous?.aspectRatio != current.aspectRatio) {
|
||||||
|
double? ratio;
|
||||||
|
|
||||||
|
switch (current.aspectRatio) {
|
||||||
|
case CropAspectRatio.original:
|
||||||
|
ratio = current.originalWidth / current.originalHeight;
|
||||||
|
default:
|
||||||
|
ratio = current.aspectRatio.ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.rotationAngle % 180 != 0) {
|
||||||
|
ratio = ratio != null ? 1 / ratio : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
cropController.aspectRatio = ratio;
|
||||||
|
}
|
||||||
|
|
||||||
if (cropController.crop != current.crop) {
|
if (cropController.crop != current.crop) {
|
||||||
cropController.crop = current.crop;
|
cropController.crop = current.crop;
|
||||||
@@ -386,7 +423,9 @@ class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProvi
|
|||||||
1.0,
|
1.0,
|
||||||
1.0,
|
1.0,
|
||||||
),
|
),
|
||||||
child: Container(
|
child: AnimatedContainer(
|
||||||
|
duration: editorState.animationDuration,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
width: (editorState.rotationAngle % 180 == 0) ? baseWidth : baseHeight,
|
width: (editorState.rotationAngle % 180 == 0) ? baseWidth : baseHeight,
|
||||||
height: (editorState.rotationAngle % 180 == 0) ? baseHeight : baseWidth,
|
height: (editorState.rotationAngle % 180 == 0) ? baseHeight : baseWidth,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/aspect_ratios.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/utils/editor.utils.dart';
|
import 'package:immich_mobile/utils/editor.utils.dart';
|
||||||
@@ -60,13 +61,8 @@ class EditorProvider extends Notifier<EditorState> {
|
|||||||
state = state.copyWith(crop: crop, hasUnsavedEdits: true);
|
state = state.copyWith(crop: crop, hasUnsavedEdits: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setAspectRatio(double? aspectRatio) {
|
void setAspectRatio(CropAspectRatio preset) {
|
||||||
if (aspectRatio != null && state.rotationAngle % 180 != 0) {
|
state = state.copyWith(aspectRatio: preset, hasUnsavedEdits: true);
|
||||||
// When rotated 90 or 270 degrees, swap width and height for aspect ratio calculations
|
|
||||||
aspectRatio = 1 / aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.copyWith(aspectRatio: aspectRatio);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetEdits() {
|
void resetEdits() {
|
||||||
@@ -76,19 +72,19 @@ class EditorProvider extends Notifier<EditorState> {
|
|||||||
flipHorizontal: false,
|
flipHorizontal: false,
|
||||||
flipVertical: false,
|
flipVertical: false,
|
||||||
crop: const Rect.fromLTRB(0, 0, 1, 1),
|
crop: const Rect.fromLTRB(0, 0, 1, 1),
|
||||||
aspectRatio: null,
|
aspectRatio: CropAspectRatio.free,
|
||||||
hasUnsavedEdits: true,
|
hasUnsavedEdits: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void rotateCCW() {
|
void rotateCCW() {
|
||||||
_animateRotation(state.rotationAngle - 90);
|
_animateRotation(state.rotationAngle - 90);
|
||||||
state = state.copyWith(hasUnsavedEdits: true);
|
state = state.copyWith(aspectRatio: state.aspectRatio.flipped, hasUnsavedEdits: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void rotateCW() {
|
void rotateCW() {
|
||||||
_animateRotation(state.rotationAngle + 90);
|
_animateRotation(state.rotationAngle + 90);
|
||||||
state = state.copyWith(hasUnsavedEdits: true);
|
state = state.copyWith(aspectRatio: state.aspectRatio.flipped, hasUnsavedEdits: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void flipHorizontally() {
|
void flipHorizontally() {
|
||||||
@@ -117,7 +113,7 @@ class EditorState {
|
|||||||
final bool flipHorizontal;
|
final bool flipHorizontal;
|
||||||
final bool flipVertical;
|
final bool flipVertical;
|
||||||
final Rect crop;
|
final Rect crop;
|
||||||
final double? aspectRatio;
|
final CropAspectRatio aspectRatio;
|
||||||
|
|
||||||
final int originalWidth;
|
final int originalWidth;
|
||||||
final int originalHeight;
|
final int originalHeight;
|
||||||
@@ -132,7 +128,7 @@ class EditorState {
|
|||||||
bool? flipHorizontal,
|
bool? flipHorizontal,
|
||||||
bool? flipVertical,
|
bool? flipVertical,
|
||||||
Rect? crop,
|
Rect? crop,
|
||||||
this.aspectRatio,
|
CropAspectRatio? aspectRatio,
|
||||||
int? originalWidth,
|
int? originalWidth,
|
||||||
int? originalHeight,
|
int? originalHeight,
|
||||||
Duration? animationDuration,
|
Duration? animationDuration,
|
||||||
@@ -145,6 +141,7 @@ class EditorState {
|
|||||||
originalWidth = originalWidth ?? 0,
|
originalWidth = originalWidth ?? 0,
|
||||||
originalHeight = originalHeight ?? 0,
|
originalHeight = originalHeight ?? 0,
|
||||||
crop = crop ?? const Rect.fromLTRB(0, 0, 1, 1),
|
crop = crop ?? const Rect.fromLTRB(0, 0, 1, 1),
|
||||||
|
aspectRatio = aspectRatio ?? CropAspectRatio.free,
|
||||||
hasUnsavedEdits = hasUnsavedEdits ?? false;
|
hasUnsavedEdits = hasUnsavedEdits ?? false;
|
||||||
|
|
||||||
EditorState copyWith({
|
EditorState copyWith({
|
||||||
@@ -152,7 +149,7 @@ class EditorState {
|
|||||||
int? rotationAngle,
|
int? rotationAngle,
|
||||||
bool? flipHorizontal,
|
bool? flipHorizontal,
|
||||||
bool? flipVertical,
|
bool? flipVertical,
|
||||||
double? aspectRatio = double.infinity,
|
CropAspectRatio? aspectRatio,
|
||||||
int? originalWidth,
|
int? originalWidth,
|
||||||
int? originalHeight,
|
int? originalHeight,
|
||||||
Duration? animationDuration,
|
Duration? animationDuration,
|
||||||
@@ -164,7 +161,7 @@ class EditorState {
|
|||||||
rotationAngle: rotationAngle ?? this.rotationAngle,
|
rotationAngle: rotationAngle ?? this.rotationAngle,
|
||||||
flipHorizontal: flipHorizontal ?? this.flipHorizontal,
|
flipHorizontal: flipHorizontal ?? this.flipHorizontal,
|
||||||
flipVertical: flipVertical ?? this.flipVertical,
|
flipVertical: flipVertical ?? this.flipVertical,
|
||||||
aspectRatio: aspectRatio == double.infinity ? this.aspectRatio : aspectRatio,
|
aspectRatio: aspectRatio ?? this.aspectRatio,
|
||||||
animationDuration: animationDuration ?? this.animationDuration,
|
animationDuration: animationDuration ?? this.animationDuration,
|
||||||
originalWidth: originalWidth ?? this.originalWidth,
|
originalWidth: originalWidth ?? this.originalWidth,
|
||||||
originalHeight: originalHeight ?? this.originalHeight,
|
originalHeight: originalHeight ?? this.originalHeight,
|
||||||
|
|||||||
Generated
-1
@@ -452,7 +452,6 @@ Class | Method | HTTP request | Description
|
|||||||
- [FacialRecognitionConfig](doc//FacialRecognitionConfig.md)
|
- [FacialRecognitionConfig](doc//FacialRecognitionConfig.md)
|
||||||
- [FoldersResponse](doc//FoldersResponse.md)
|
- [FoldersResponse](doc//FoldersResponse.md)
|
||||||
- [FoldersUpdate](doc//FoldersUpdate.md)
|
- [FoldersUpdate](doc//FoldersUpdate.md)
|
||||||
- [HlsVideoResolution](doc//HlsVideoResolution.md)
|
|
||||||
- [ImageFormat](doc//ImageFormat.md)
|
- [ImageFormat](doc//ImageFormat.md)
|
||||||
- [IntegrityReport](doc//IntegrityReport.md)
|
- [IntegrityReport](doc//IntegrityReport.md)
|
||||||
- [IntegrityReportResponseDto](doc//IntegrityReportResponseDto.md)
|
- [IntegrityReportResponseDto](doc//IntegrityReportResponseDto.md)
|
||||||
|
|||||||
Generated
-1
@@ -173,7 +173,6 @@ part 'model/face_dto.dart';
|
|||||||
part 'model/facial_recognition_config.dart';
|
part 'model/facial_recognition_config.dart';
|
||||||
part 'model/folders_response.dart';
|
part 'model/folders_response.dart';
|
||||||
part 'model/folders_update.dart';
|
part 'model/folders_update.dart';
|
||||||
part 'model/hls_video_resolution.dart';
|
|
||||||
part 'model/image_format.dart';
|
part 'model/image_format.dart';
|
||||||
part 'model/integrity_report.dart';
|
part 'model/integrity_report.dart';
|
||||||
part 'model/integrity_report_response_dto.dart';
|
part 'model/integrity_report_response_dto.dart';
|
||||||
|
|||||||
Generated
-2
@@ -391,8 +391,6 @@ class ApiClient {
|
|||||||
return FoldersResponse.fromJson(value);
|
return FoldersResponse.fromJson(value);
|
||||||
case 'FoldersUpdate':
|
case 'FoldersUpdate':
|
||||||
return FoldersUpdate.fromJson(value);
|
return FoldersUpdate.fromJson(value);
|
||||||
case 'HlsVideoResolution':
|
|
||||||
return HlsVideoResolutionTypeTransformer().decode(value);
|
|
||||||
case 'ImageFormat':
|
case 'ImageFormat':
|
||||||
return ImageFormatTypeTransformer().decode(value);
|
return ImageFormatTypeTransformer().decode(value);
|
||||||
case 'IntegrityReport':
|
case 'IntegrityReport':
|
||||||
|
|||||||
Generated
-3
@@ -106,9 +106,6 @@ String parameterToString(dynamic value) {
|
|||||||
if (value is Colorspace) {
|
if (value is Colorspace) {
|
||||||
return ColorspaceTypeTransformer().encode(value).toString();
|
return ColorspaceTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
if (value is HlsVideoResolution) {
|
|
||||||
return HlsVideoResolutionTypeTransformer().encode(value).toString();
|
|
||||||
}
|
|
||||||
if (value is ImageFormat) {
|
if (value is ImageFormat) {
|
||||||
return ImageFormatTypeTransformer().encode(value).toString();
|
return ImageFormatTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
|||||||
-94
@@ -1,94 +0,0 @@
|
|||||||
//
|
|
||||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
|
||||||
//
|
|
||||||
// @dart=2.18
|
|
||||||
|
|
||||||
// ignore_for_file: unused_element, unused_import
|
|
||||||
// ignore_for_file: always_put_required_named_parameters_first
|
|
||||||
// ignore_for_file: constant_identifier_names
|
|
||||||
// ignore_for_file: lines_longer_than_80_chars
|
|
||||||
|
|
||||||
part of openapi.api;
|
|
||||||
|
|
||||||
/// HLS video resolution
|
|
||||||
class HlsVideoResolution {
|
|
||||||
/// Instantiate a new enum with the provided [value].
|
|
||||||
const HlsVideoResolution._(this.value);
|
|
||||||
|
|
||||||
/// The underlying value of this enum member.
|
|
||||||
final int value;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => value.toString();
|
|
||||||
|
|
||||||
int toJson() => value;
|
|
||||||
|
|
||||||
static const number480 = HlsVideoResolution._(480);
|
|
||||||
static const number720 = HlsVideoResolution._(720);
|
|
||||||
static const number1080 = HlsVideoResolution._(1080);
|
|
||||||
static const number1440 = HlsVideoResolution._(1440);
|
|
||||||
static const number2160 = HlsVideoResolution._(2160);
|
|
||||||
|
|
||||||
/// List of all possible values in this [enum][HlsVideoResolution].
|
|
||||||
static const values = <HlsVideoResolution>[
|
|
||||||
number480,
|
|
||||||
number720,
|
|
||||||
number1080,
|
|
||||||
number1440,
|
|
||||||
number2160,
|
|
||||||
];
|
|
||||||
|
|
||||||
static HlsVideoResolution? fromJson(dynamic value) => HlsVideoResolutionTypeTransformer().decode(value);
|
|
||||||
|
|
||||||
static List<HlsVideoResolution> listFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final result = <HlsVideoResolution>[];
|
|
||||||
if (json is List && json.isNotEmpty) {
|
|
||||||
for (final row in json) {
|
|
||||||
final value = HlsVideoResolution.fromJson(row);
|
|
||||||
if (value != null) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList(growable: growable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transformation class that can [encode] an instance of [HlsVideoResolution] to int,
|
|
||||||
/// and [decode] dynamic data back to [HlsVideoResolution].
|
|
||||||
class HlsVideoResolutionTypeTransformer {
|
|
||||||
factory HlsVideoResolutionTypeTransformer() => _instance ??= const HlsVideoResolutionTypeTransformer._();
|
|
||||||
|
|
||||||
const HlsVideoResolutionTypeTransformer._();
|
|
||||||
|
|
||||||
int encode(HlsVideoResolution data) => data.value;
|
|
||||||
|
|
||||||
/// Decodes a [dynamic value][data] to a HlsVideoResolution.
|
|
||||||
///
|
|
||||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
|
||||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
|
||||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
|
||||||
///
|
|
||||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
|
||||||
/// and users are still using an old app with the old code.
|
|
||||||
HlsVideoResolution? decode(dynamic data, {bool allowNull = true}) {
|
|
||||||
if (data != null) {
|
|
||||||
switch (data) {
|
|
||||||
case 480: return HlsVideoResolution.number480;
|
|
||||||
case 720: return HlsVideoResolution.number720;
|
|
||||||
case 1080: return HlsVideoResolution.number1080;
|
|
||||||
case 1440: return HlsVideoResolution.number1440;
|
|
||||||
case 2160: return HlsVideoResolution.number2160;
|
|
||||||
default:
|
|
||||||
if (!allowNull) {
|
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Singleton [HlsVideoResolutionTypeTransformer] instance.
|
|
||||||
static HlsVideoResolutionTypeTransformer? _instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -14,40 +14,26 @@ class SystemConfigFFmpegRealtimeDto {
|
|||||||
/// Returns a new [SystemConfigFFmpegRealtimeDto] instance.
|
/// Returns a new [SystemConfigFFmpegRealtimeDto] instance.
|
||||||
SystemConfigFFmpegRealtimeDto({
|
SystemConfigFFmpegRealtimeDto({
|
||||||
required this.enabled,
|
required this.enabled,
|
||||||
this.resolutions = const [],
|
|
||||||
this.videoCodecs = const [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Enable real-time HLS transcoding (alpha)
|
/// Enable real-time HLS transcoding (alpha)
|
||||||
bool enabled;
|
bool enabled;
|
||||||
|
|
||||||
/// Resolutions to use for real-time HLS transcoding
|
|
||||||
List<HlsVideoResolution> resolutions;
|
|
||||||
|
|
||||||
/// Video codecs to use for real-time HLS transcoding
|
|
||||||
List<VideoCodec> videoCodecs;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigFFmpegRealtimeDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SystemConfigFFmpegRealtimeDto &&
|
||||||
other.enabled == enabled &&
|
other.enabled == enabled;
|
||||||
_deepEquality.equals(other.resolutions, resolutions) &&
|
|
||||||
_deepEquality.equals(other.videoCodecs, videoCodecs);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(enabled.hashCode) +
|
(enabled.hashCode);
|
||||||
(resolutions.hashCode) +
|
|
||||||
(videoCodecs.hashCode);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SystemConfigFFmpegRealtimeDto[enabled=$enabled, resolutions=$resolutions, videoCodecs=$videoCodecs]';
|
String toString() => 'SystemConfigFFmpegRealtimeDto[enabled=$enabled]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'enabled'] = this.enabled;
|
json[r'enabled'] = this.enabled;
|
||||||
json[r'resolutions'] = this.resolutions;
|
|
||||||
json[r'videoCodecs'] = this.videoCodecs;
|
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,8 +47,6 @@ class SystemConfigFFmpegRealtimeDto {
|
|||||||
|
|
||||||
return SystemConfigFFmpegRealtimeDto(
|
return SystemConfigFFmpegRealtimeDto(
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
resolutions: HlsVideoResolution.listFromJson(json[r'resolutions']),
|
|
||||||
videoCodecs: VideoCodec.listFromJson(json[r'videoCodecs']),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -111,8 +95,6 @@ class SystemConfigFFmpegRealtimeDto {
|
|||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'enabled',
|
'enabled',
|
||||||
'resolutions',
|
|
||||||
'videoCodecs',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19120,17 +19120,6 @@
|
|||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"HlsVideoResolution": {
|
|
||||||
"description": "HLS video resolution",
|
|
||||||
"enum": [
|
|
||||||
480,
|
|
||||||
720,
|
|
||||||
1080,
|
|
||||||
1440,
|
|
||||||
2160
|
|
||||||
],
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"ImageFormat": {
|
"ImageFormat": {
|
||||||
"description": "Image format",
|
"description": "Image format",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -25765,26 +25754,10 @@
|
|||||||
"enabled": {
|
"enabled": {
|
||||||
"description": "Enable real-time HLS transcoding (alpha)",
|
"description": "Enable real-time HLS transcoding (alpha)",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"description": "Resolutions to use for real-time HLS transcoding",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/HlsVideoResolution"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
},
|
|
||||||
"videoCodecs": {
|
|
||||||
"description": "Video codecs to use for real-time HLS transcoding",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/VideoCodec"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"enabled",
|
"enabled"
|
||||||
"resolutions",
|
|
||||||
"videoCodecs"
|
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2302,10 +2302,6 @@ export type SystemConfigBackupsDto = {
|
|||||||
export type SystemConfigFFmpegRealtimeDto = {
|
export type SystemConfigFFmpegRealtimeDto = {
|
||||||
/** Enable real-time HLS transcoding (alpha) */
|
/** Enable real-time HLS transcoding (alpha) */
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
/** Resolutions to use for real-time HLS transcoding */
|
|
||||||
resolutions: HlsVideoResolution[];
|
|
||||||
/** Video codecs to use for real-time HLS transcoding */
|
|
||||||
videoCodecs: VideoCodec[];
|
|
||||||
};
|
};
|
||||||
export type SystemConfigFFmpegDto = {
|
export type SystemConfigFFmpegDto = {
|
||||||
accel: TranscodeHWAccel;
|
accel: TranscodeHWAccel;
|
||||||
@@ -7633,13 +7629,6 @@ export enum CQMode {
|
|||||||
Cqp = "cqp",
|
Cqp = "cqp",
|
||||||
Icq = "icq"
|
Icq = "icq"
|
||||||
}
|
}
|
||||||
export enum HlsVideoResolution {
|
|
||||||
$480 = 480,
|
|
||||||
$720 = 720,
|
|
||||||
$1080 = 1080,
|
|
||||||
$1440 = 1440,
|
|
||||||
$2160 = 2160
|
|
||||||
}
|
|
||||||
export enum ToneMapping {
|
export enum ToneMapping {
|
||||||
Hable = "hable",
|
Hable = "hable",
|
||||||
Mobius = "mobius",
|
Mobius = "mobius",
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
<p align="center">
|
|
||||||
<br/>
|
|
||||||
<a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: AGPLv3"></a>
|
|
||||||
<a href="https://discord.immich.app">
|
|
||||||
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
|
|
||||||
</a>
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="../design/immich-logo-stacked-light.svg" width="300" title="Immich">
|
|
||||||
</p>
|
|
||||||
<h3 align="center">ഫോട്ടോകളും വീഡിയോകളും കൈകാര്യം ചെയ്യുന്നതിനുള്ള ഉയർന്ന കാര്യക്ഷമതയുള്ള സെൽഫ്-ഹോസ്റ്റഡ് ആപ്ലിക്കേഷൻ</h3>
|
|
||||||
<br/>
|
|
||||||
<a href="https://immich.app">
|
|
||||||
<img src="../design/immich-screenshots.png" title="പ്രധാന സ്ക്രീൻഷോട്ട്">
|
|
||||||
</a>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="../README.md">English</a>
|
|
||||||
<a href="README_ca_ES.md">Català</a>
|
|
||||||
<a href="README_es_ES.md">Español</a>
|
|
||||||
<a href="README_fr_FR.md">Français</a>
|
|
||||||
<a href="README_it_IT.md">Italiano</a>
|
|
||||||
<a href="README_ja_JP.md">日本語</a>
|
|
||||||
<a href="README_ko_KR.md">한국어</a>
|
|
||||||
<a href="README_de_DE.md">Deutsch</a>
|
|
||||||
<a href="README_nl_NL.md">Nederlands</a>
|
|
||||||
<a href="README_tr_TR.md">Türkçe</a>
|
|
||||||
<a href="README_zh_CN.md">简体中文</a>
|
|
||||||
<a href="README_zh_TW.md">正體中文</a>
|
|
||||||
<a href="README_uk_UA.md">Українська</a>
|
|
||||||
<a href="README_ru_RU.md">Русский</a>
|
|
||||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
|
||||||
<a href="README_sv_SE.md">Svenska</a>
|
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> ⚠️ നിങ്ങളുടെ വിലയേറിയ ഫോട്ടോകൾക്കും വീഡിയോകൾക്കും എല്ലായ്പ്പോഴും [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) ബാക്കപ്പ് പ്ലാൻ പിന്തുടരുക!
|
|
||||||
>
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> ഇൻസ്റ്റാളേഷൻ ഗൈഡുകൾ ഉൾപ്പെടെയുള്ള പ്രധാന ഡോക്യുമെന്റേഷൻ https://immich.app/ എന്ന വെബ്സൈറ്റിൽ നിങ്ങൾക്ക് കണ്ടെത്താം.
|
|
||||||
|
|
||||||
## ലിങ്കുകൾ
|
|
||||||
|
|
||||||
- [ഡോക്യുമെന്റേഷൻ](https://docs.immich.app/)
|
|
||||||
- [വിവരങ്ങൾ](https://docs.immich.app/overview/introduction)
|
|
||||||
- [ഇൻസ്റ്റാളേഷൻ](https://docs.immich.app/install/requirements)
|
|
||||||
- [റോഡ്മാപ്പ്](https://immich.app/roadmap)
|
|
||||||
- [ഡെമോ](#ഡെമോ)
|
|
||||||
- [സവിശേഷതകൾ](#സവിശേഷതകൾ)
|
|
||||||
- [വിവർത്തനങ്ങൾ](https://docs.immich.app/developer/translations)
|
|
||||||
- [സംഭാവന നൽകൽ](https://docs.immich.app/overview/support-the-project)
|
|
||||||
|
|
||||||
## ഡെമോ
|
|
||||||
|
|
||||||
ഡെമോ വെബ്സൈറ്റ് [ഇവിടെ](https://demo.immich.app) ആക്സസ് ചെയ്യാം. മൊബൈൽ ആപ്പിന്റെ ഡെമോ കാണാൻ ആപ്പ് ഇൻസ്റ്റാൾ ചെയ്ത ശേഷം, `Server Endpoint URL` എന്ന സെറ്റിങ്ങിൽ നിങ്ങൾക്ക് `https://demo.immich.app` ഉപയോഗിക്കാം.
|
|
||||||
|
|
||||||
### ലോഗിൻ വിവരങ്ങൾ
|
|
||||||
|
|
||||||
| ഇമെയിൽ | പാസ്വേഡ് |
|
|
||||||
| --------------- | -------- |
|
|
||||||
| demo@immich.app | demo |
|
|
||||||
|
|
||||||
## സവിശേഷതകൾ
|
|
||||||
|
|
||||||
| സവിശേഷതകൾ | മൊബൈൽ | വെബ് |
|
|
||||||
| :-------------------------------------------------- | ------ | --- |
|
|
||||||
| ഫോട്ടോകളും വീഡിയോകളും അപ്ലോഡ് ചെയ്യാനും കാണാനും സാധിക്കും | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| ആപ്പ് തുറക്കുമ്പോൾ ഓട്ടോമാറ്റിക് ബാക്കപ്പ് | ഉണ്ട് | N/A |
|
|
||||||
| ഫയലുകളുടെ ഇരട്ടിപ്പ് (duplication) തടയുന്നു | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| ബാക്കപ്പിനായി നിർദ്ദിഷ്ട ആൽബങ്ങൾ തിരഞ്ഞെടുക്കാം | ഉണ്ട് | N/A |
|
|
||||||
| ഫോട്ടോകളും വീഡിയോകളും ലോക്കൽ ഡിവൈസിലേക്ക് ഡൗൺലോഡ് ചെയ്യാം | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| ഒന്നിലധികം ഉപയോക്താക്കൾക്കുള്ള പിന്തുണ (Multi-user) | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| ആൽബങ്ങളും പങ്കിട്ട ആൽബങ്ങളും | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| സ്ക്രബ്ബ് ചെയ്യാവുന്ന/വലിച്ചിഴക്കാവുന്ന സ്ക്രോൾബാർ | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| റോ (RAW) ഫോർമാറ്റുകൾക്കുള്ള പിന്തുണ | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| മെറ്റാഡാറ്റ കാഴ്ച (EXIF, മാപ്പ്) | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| മെറ്റാഡാറ്റ, ഒബ്ജക്റ്റുകൾ, മുഖങ്ങൾ, CLIP എന്നിവ ഉപയോഗിച്ച് തിരയാം | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| അഡ്മിനിസ്ട്രേറ്റീവ് പ്രവർത്തനങ്ങൾ (യൂസർ മാനേജ്മെന്റ്) | ഇല്ല | ഉണ്ട് |
|
|
||||||
| ബാക്ക്ഗ്രൗണ്ട് ബാക്കപ്പ് | ഉണ്ട് | N/A |
|
|
||||||
| വിർച്വൽ സ്ക്രോൾ | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| OAuth സപ്പോർട്ട് | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| API കീകൾ | N/A | ഉണ്ട് |
|
|
||||||
| ലൈവ് ഫോട്ടോ/മോഷൻ ഫോട്ടോ ബാക്കപ്പും പ്ലേബാക്കും | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| 360 ഡിഗ്രി ഇമേജ് ഡിസ്പ്ലേ പിന്തുണ | ഇല്ല | ഉണ്ട് |
|
|
||||||
| ഉപയോക്താവ് നിർവചിക്കുന്ന സ്റ്റോറേജ് ഘടന | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| പബ്ലിക് ഷെയറിംഗ് | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| ആർക്കൈവും പ്രിയപ്പെട്ടവയും (Favorites) | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| ആഗോള മാപ്പ് (Global Map) | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| പങ്കാളി പങ്കിടൽ (Partner Sharing) | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| മുഖം തിരിച്ചറിയലും ക്ലസ്റ്ററിംഗും | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| ഓർമ്മകൾ (Memories - x വർഷങ്ങൾക്ക് മുമ്പ്) | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| ഓഫ്ലൈൻ പിന്തുണ | ഉണ്ട് | ഇല്ല |
|
|
||||||
| റീഡ്-ഒൺലി ഗാലറി | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| അടുക്കിവെച്ച ഫോട്ടോകൾ (Stacked Photos) | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
| ടാഗുകൾ | ഇല്ല | ഉണ്ട് |
|
|
||||||
| ഫോൾഡർ കാഴ്ച | ഉണ്ട് | ഉണ്ട് |
|
|
||||||
|
|
||||||
## വിവർത്തനങ്ങൾ
|
|
||||||
|
|
||||||
വിവർത്തനങ്ങളെക്കുറിച്ച് കൂടുതൽ [ഇവിടെ](https://docs.immich.app/developer/translations) വായിക്കാം.
|
|
||||||
|
|
||||||
<a href="https://hosted.weblate.org/engage/immich/">
|
|
||||||
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Translation status" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
## റെപ്പോസിറ്ററി പ്രവർത്തനം
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## സ്റ്റാർ ചരിത്രം
|
|
||||||
|
|
||||||
<a href="https://star-history.com/#immich-app/immich&type=date&legend=top-left">
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=date&theme=dark" />
|
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=date" />
|
|
||||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=immich-app/immich&type=date" width="100%" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
## സംഭാവന ചെയ്തവർ
|
|
||||||
|
|
||||||
<a href="https://github.com/immich-app/immich/graphs/contributors">
|
|
||||||
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
|
||||||
</a>
|
|
||||||
@@ -35,7 +35,6 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -39,7 +39,6 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -38,7 +38,6 @@
|
|||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@
|
|||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,6 @@
|
|||||||
<a href="README_ar_JO.md">العربية</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
<a href="README_ml_IN.md">മലയാളം</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
AudioCodec,
|
AudioCodec,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
CQMode,
|
CQMode,
|
||||||
HlsVideoResolution,
|
|
||||||
ImageFormat,
|
ImageFormat,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
OAuthTokenEndpointAuthMethod,
|
OAuthTokenEndpointAuthMethod,
|
||||||
@@ -49,8 +48,6 @@ export type SystemConfig = {
|
|||||||
tonemap: ToneMapping;
|
tonemap: ToneMapping;
|
||||||
realtime: {
|
realtime: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
videoCodecs: VideoCodec[];
|
|
||||||
resolutions: HlsVideoResolution[];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
integrityChecks: {
|
integrityChecks: {
|
||||||
@@ -250,8 +247,6 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
accelDecode: true,
|
accelDecode: true,
|
||||||
realtime: {
|
realtime: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
videoCodecs: [VideoCodec.H264, VideoCodec.Hevc],
|
|
||||||
resolutions: [HlsVideoResolution.p480, HlsVideoResolution.p720, HlsVideoResolution.p1080],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
integrityChecks: {
|
integrityChecks: {
|
||||||
|
|||||||
+9
-60
@@ -235,65 +235,14 @@ export const HLS_PLAYLIST_CONTENT_TYPE = 'application/vnd.apple.mpegurl';
|
|||||||
export const HLS_SEGMENT_DURATION = 2;
|
export const HLS_SEGMENT_DURATION = 2;
|
||||||
export const HLS_SEGMENT_FILENAME_REGEX = /^seg_(\d+)\.m4s$/;
|
export const HLS_SEGMENT_FILENAME_REGEX = /^seg_(\d+)\.m4s$/;
|
||||||
export const HLS_VARIANTS = [
|
export const HLS_VARIANTS = [
|
||||||
{ resolution: 480, codec: VideoCodec.Av1, bitrate: 1_000_000 },
|
{ resolution: 480, codec: VideoCodec.Av1, bitrate: 1_000_000, codecString: 'av01.0.04M.08' },
|
||||||
{ resolution: 480, codec: VideoCodec.Hevc, bitrate: 1_200_000 },
|
{ resolution: 480, codec: VideoCodec.Hevc, bitrate: 1_200_000, codecString: 'hvc1.1.6.L90.B0' },
|
||||||
{ resolution: 480, codec: VideoCodec.H264, bitrate: 2_500_000 },
|
{ resolution: 480, codec: VideoCodec.H264, bitrate: 2_500_000, codecString: 'avc1.64001e' },
|
||||||
{ resolution: 720, codec: VideoCodec.Av1, bitrate: 2_000_000 },
|
{ resolution: 720, codec: VideoCodec.Av1, bitrate: 2_000_000, codecString: 'av01.0.08M.08' },
|
||||||
{ resolution: 720, codec: VideoCodec.Hevc, bitrate: 2_500_000 },
|
{ resolution: 720, codec: VideoCodec.Hevc, bitrate: 2_500_000, codecString: 'hvc1.1.6.L93.B0' },
|
||||||
{ resolution: 720, codec: VideoCodec.H264, bitrate: 5_000_000 },
|
{ resolution: 720, codec: VideoCodec.H264, bitrate: 5_000_000, codecString: 'avc1.64001f' },
|
||||||
{ resolution: 1080, codec: VideoCodec.Av1, bitrate: 4_000_000 },
|
{ resolution: 1080, codec: VideoCodec.Av1, bitrate: 4_000_000, codecString: 'av01.0.09M.08' },
|
||||||
{ resolution: 1080, codec: VideoCodec.Hevc, bitrate: 4_500_000 },
|
{ resolution: 1080, codec: VideoCodec.Hevc, bitrate: 4_500_000, codecString: 'hvc1.1.6.L120.B0' },
|
||||||
{ resolution: 1080, codec: VideoCodec.H264, bitrate: 8_000_000 },
|
{ resolution: 1080, codec: VideoCodec.H264, bitrate: 8_000_000, codecString: 'avc1.640028' },
|
||||||
{ resolution: 1440, codec: VideoCodec.Av1, bitrate: 7_000_000 },
|
|
||||||
{ resolution: 1440, codec: VideoCodec.Hevc, bitrate: 8_000_000 },
|
|
||||||
{ resolution: 1440, codec: VideoCodec.H264, bitrate: 14_000_000 },
|
|
||||||
{ resolution: 2160, codec: VideoCodec.Av1, bitrate: 12_000_000 },
|
|
||||||
{ resolution: 2160, codec: VideoCodec.Hevc, bitrate: 14_000_000 },
|
|
||||||
{ resolution: 2160, codec: VideoCodec.H264, bitrate: 25_000_000 },
|
|
||||||
];
|
];
|
||||||
export const HLS_VERSION = 7;
|
export const HLS_VERSION = 7;
|
||||||
|
|
||||||
export type CodecLevel = { maxFrame: number; maxRate: number; token: string };
|
|
||||||
|
|
||||||
// H.264 High profile: token is the hex level_idc.
|
|
||||||
export const H264_LEVELS: CodecLevel[] = [
|
|
||||||
{ maxFrame: 1620, maxRate: 40_500, token: '1e' }, // 3.0
|
|
||||||
{ maxFrame: 3600, maxRate: 108_000, token: '1f' }, // 3.1
|
|
||||||
{ maxFrame: 5120, maxRate: 216_000, token: '20' }, // 3.2
|
|
||||||
{ maxFrame: 8192, maxRate: 245_760, token: '28' }, // 4.0
|
|
||||||
{ maxFrame: 8704, maxRate: 522_240, token: '2a' }, // 4.2
|
|
||||||
{ maxFrame: 22_080, maxRate: 589_824, token: '32' }, // 5.0
|
|
||||||
{ maxFrame: 36_864, maxRate: 983_040, token: '33' }, // 5.1
|
|
||||||
{ maxFrame: 36_864, maxRate: 2_073_600, token: '34' }, // 5.2
|
|
||||||
{ maxFrame: 139_264, maxRate: 4_177_920, token: '3c' }, // 6.0
|
|
||||||
{ maxFrame: 139_264, maxRate: 8_355_840, token: '3d' }, // 6.1
|
|
||||||
{ maxFrame: 139_264, maxRate: 16_711_680, token: '3e' }, // 6.2
|
|
||||||
];
|
|
||||||
|
|
||||||
// HEVC Main profile, Main tier: token is `L` + level_idc (level × 30).
|
|
||||||
export const HEVC_LEVELS: CodecLevel[] = [
|
|
||||||
{ maxFrame: 552_960, maxRate: 16_588_800, token: 'L90' }, // 3.0
|
|
||||||
{ maxFrame: 983_040, maxRate: 33_177_600, token: 'L93' }, // 3.1
|
|
||||||
{ maxFrame: 2_228_224, maxRate: 66_846_720, token: 'L120' }, // 4.0
|
|
||||||
{ maxFrame: 2_228_224, maxRate: 133_693_440, token: 'L123' }, // 4.1
|
|
||||||
{ maxFrame: 8_912_896, maxRate: 267_386_880, token: 'L150' }, // 5.0
|
|
||||||
{ maxFrame: 8_912_896, maxRate: 534_773_760, token: 'L153' }, // 5.1
|
|
||||||
{ maxFrame: 8_912_896, maxRate: 1_069_547_520, token: 'L156' }, // 5.2
|
|
||||||
{ maxFrame: 35_651_584, maxRate: 1_069_547_520, token: 'L180' }, // 6.0
|
|
||||||
{ maxFrame: 35_651_584, maxRate: 2_139_095_040, token: 'L183' }, // 6.1
|
|
||||||
{ maxFrame: 35_651_584, maxRate: 4_278_190_080, token: 'L186' }, // 6.2
|
|
||||||
];
|
|
||||||
|
|
||||||
// AV1 Main profile (0), Main tier (M): token is the two-digit seq_level_idx + `M`.
|
|
||||||
export const AV1_LEVELS: CodecLevel[] = [
|
|
||||||
{ maxFrame: 665_856, maxRate: 19_975_168, token: '04M' }, // 3.0
|
|
||||||
{ maxFrame: 1_065_024, maxRate: 31_950_336, token: '05M' }, // 3.1
|
|
||||||
{ maxFrame: 2_359_296, maxRate: 70_778_880, token: '08M' }, // 4.0
|
|
||||||
{ maxFrame: 2_359_296, maxRate: 141_557_760, token: '09M' }, // 4.1
|
|
||||||
{ maxFrame: 8_912_896, maxRate: 267_386_880, token: '12M' }, // 5.0
|
|
||||||
{ maxFrame: 8_912_896, maxRate: 534_773_760, token: '13M' }, // 5.1
|
|
||||||
{ maxFrame: 8_912_896, maxRate: 1_069_547_520, token: '14M' }, // 5.2
|
|
||||||
{ maxFrame: 35_651_584, maxRate: 1_069_547_520, token: '16M' }, // 6.0
|
|
||||||
{ maxFrame: 35_651_584, maxRate: 2_139_095_040, token: '17M' }, // 6.1
|
|
||||||
{ maxFrame: 35_651_584, maxRate: 4_278_190_080, token: '18M' }, // 6.2
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
AudioCodecSchema,
|
AudioCodecSchema,
|
||||||
ColorspaceSchema,
|
ColorspaceSchema,
|
||||||
CQModeSchema,
|
CQModeSchema,
|
||||||
HlsVideoResolutionSchema,
|
|
||||||
ImageFormatSchema,
|
ImageFormatSchema,
|
||||||
LogLevelSchema,
|
LogLevelSchema,
|
||||||
OAuthTokenEndpointAuthMethodSchema,
|
OAuthTokenEndpointAuthMethodSchema,
|
||||||
@@ -116,8 +115,6 @@ const SystemConfigFFmpegSchema = z
|
|||||||
realtime: z
|
realtime: z
|
||||||
.object({
|
.object({
|
||||||
enabled: configBool.describe('Enable real-time HLS transcoding (alpha)'),
|
enabled: configBool.describe('Enable real-time HLS transcoding (alpha)'),
|
||||||
videoCodecs: z.array(VideoCodecSchema).describe('Video codecs to use for real-time HLS transcoding'),
|
|
||||||
resolutions: z.array(HlsVideoResolutionSchema).describe('Resolutions to use for real-time HLS transcoding'),
|
|
||||||
})
|
})
|
||||||
.meta({ id: 'SystemConfigFFmpegRealtimeDto' }),
|
.meta({ id: 'SystemConfigFFmpegRealtimeDto' }),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -529,19 +529,6 @@ export enum CQMode {
|
|||||||
|
|
||||||
export const CQModeSchema = z.enum(CQMode).describe('CQ mode').meta({ id: 'CQMode' });
|
export const CQModeSchema = z.enum(CQMode).describe('CQ mode').meta({ id: 'CQMode' });
|
||||||
|
|
||||||
export enum HlsVideoResolution {
|
|
||||||
p480 = 480,
|
|
||||||
p720 = 720,
|
|
||||||
p1080 = 1080,
|
|
||||||
p1440 = 1440,
|
|
||||||
p2160 = 2160,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HlsVideoResolutionSchema = z
|
|
||||||
.enum(HlsVideoResolution)
|
|
||||||
.describe('HLS video resolution')
|
|
||||||
.meta({ id: 'HlsVideoResolution', type: 'integer' });
|
|
||||||
|
|
||||||
export enum Colorspace {
|
export enum Colorspace {
|
||||||
Srgb = 'srgb',
|
Srgb = 'srgb',
|
||||||
P3 = 'p3',
|
P3 = 'p3',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||||
import { HlsVideoResolution, VideoCodec } from 'src/enum';
|
import { TranscodeHardwareAcceleration } from 'src/enum';
|
||||||
import { HlsService } from 'src/services/hls.service';
|
import { HlsService } from 'src/services/hls.service';
|
||||||
import { eiffelTower, train, waterfall } from 'test/fixtures/media.stub';
|
import { eiffelTower, train, waterfall } from 'test/fixtures/media.stub';
|
||||||
import { factory } from 'test/small.factory';
|
import { factory } from 'test/small.factory';
|
||||||
@@ -96,79 +96,67 @@ seg_10.m4s
|
|||||||
|
|
||||||
const sessionId = '00000000-0000-0000-0000-000000000000';
|
const sessionId = '00000000-0000-0000-0000-000000000000';
|
||||||
|
|
||||||
const eiffelExpectedMasterAv1 = `#EXTM3U
|
const eiffelExpectedMasterDisabled = `#EXTM3U
|
||||||
#EXT-X-VERSION:7
|
#EXT-X-VERSION:7
|
||||||
#EXT-X-INDEPENDENT-SEGMENTS
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1350000,RESOLUTION=480x852,CODECS="av01.0.04M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=480x852,CODECS="av01.0.04M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/0/playlist.m3u8
|
${sessionId}/0/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1620000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=1200000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/1/playlist.m3u8
|
${sessionId}/1/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=3375000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/2/playlist.m3u8
|
${sessionId}/2/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2700000,RESOLUTION=720x1280,CODECS="av01.0.05M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=2000000,RESOLUTION=720x1280,CODECS="av01.0.08M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/3/playlist.m3u8
|
${sessionId}/3/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=3375000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/4/playlist.m3u8
|
${sessionId}/4/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=6750000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/5/playlist.m3u8
|
${sessionId}/5/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=5400000,RESOLUTION=1080x1920,CODECS="av01.0.08M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=4000000,RESOLUTION=1080x1920,CODECS="av01.0.09M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/6/playlist.m3u8
|
${sessionId}/6/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=6075000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=4500000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/7/playlist.m3u8
|
${sessionId}/7/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=10800000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/8/playlist.m3u8
|
${sessionId}/8/playlist.m3u8
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const eiffelExpectedMasterNoAv1 = `#EXTM3U
|
const eiffelExpectedMasterRkmpp = `#EXTM3U
|
||||||
#EXT-X-VERSION:7
|
#EXT-X-VERSION:7
|
||||||
#EXT-X-INDEPENDENT-SEGMENTS
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1620000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=1200000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/1/playlist.m3u8
|
${sessionId}/1/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=3375000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/2/playlist.m3u8
|
${sessionId}/2/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=3375000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/4/playlist.m3u8
|
${sessionId}/4/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=6750000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/5/playlist.m3u8
|
${sessionId}/5/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=6075000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=4500000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/7/playlist.m3u8
|
${sessionId}/7/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=10800000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910
|
||||||
${sessionId}/8/playlist.m3u8
|
${sessionId}/8/playlist.m3u8
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const waterfallExpectedMasterAv1 = `#EXTM3U
|
const waterfallExpectedMasterDisabled = `#EXTM3U
|
||||||
#EXT-X-VERSION:7
|
#EXT-X-VERSION:7
|
||||||
#EXT-X-INDEPENDENT-SEGMENTS
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1350000,RESOLUTION=480x852,CODECS="av01.0.04M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=480x852,CODECS="av01.0.04M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/0/playlist.m3u8
|
${sessionId}/0/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1620000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=1200000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/1/playlist.m3u8
|
${sessionId}/1/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=3375000,RESOLUTION=480x852,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/2/playlist.m3u8
|
${sessionId}/2/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=2700000,RESOLUTION=720x1280,CODECS="av01.0.05M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=2000000,RESOLUTION=720x1280,CODECS="av01.0.08M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/3/playlist.m3u8
|
${sessionId}/3/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=3375000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/4/playlist.m3u8
|
${sessionId}/4/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=6750000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/5/playlist.m3u8
|
${sessionId}/5/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=5400000,RESOLUTION=1080x1920,CODECS="av01.0.08M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=4000000,RESOLUTION=1080x1920,CODECS="av01.0.09M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/6/playlist.m3u8
|
${sessionId}/6/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=6075000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=4500000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/7/playlist.m3u8
|
${sessionId}/7/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=10800000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
||||||
${sessionId}/8/playlist.m3u8
|
${sessionId}/8/playlist.m3u8
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=9450000,RESOLUTION=1440x2560,CODECS="av01.0.12M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
|
||||||
${sessionId}/9/playlist.m3u8
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=10800000,RESOLUTION=1440x2560,CODECS="hvc1.1.6.L150.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
|
||||||
${sessionId}/10/playlist.m3u8
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=18900000,RESOLUTION=1440x2560,CODECS="avc1.640032,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
|
||||||
${sessionId}/11/playlist.m3u8
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=16200000,RESOLUTION=2160x3840,CODECS="av01.0.12M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
|
||||||
${sessionId}/12/playlist.m3u8
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=18900000,RESOLUTION=2160x3840,CODECS="hvc1.1.6.L150.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
|
||||||
${sessionId}/13/playlist.m3u8
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=33750000,RESOLUTION=2160x3840,CODECS="avc1.640033,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830
|
|
||||||
${sessionId}/14/playlist.m3u8
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
describe(HlsService.name, () => {
|
describe(HlsService.name, () => {
|
||||||
@@ -183,24 +171,9 @@ describe(HlsService.name, () => {
|
|||||||
const auth = factory.auth();
|
const auth = factory.auth();
|
||||||
const assetId = 'asset-1';
|
const assetId = 'asset-1';
|
||||||
|
|
||||||
const allCodecs = [VideoCodec.Av1, VideoCodec.Hevc, VideoCodec.H264];
|
const setup = (asset: typeof eiffelTower | typeof waterfall, accel: TranscodeHardwareAcceleration) => {
|
||||||
const allResolutions = [
|
|
||||||
HlsVideoResolution.p480,
|
|
||||||
HlsVideoResolution.p720,
|
|
||||||
HlsVideoResolution.p1080,
|
|
||||||
HlsVideoResolution.p1440,
|
|
||||||
HlsVideoResolution.p2160,
|
|
||||||
];
|
|
||||||
|
|
||||||
const setup = (
|
|
||||||
asset: typeof eiffelTower | typeof waterfall,
|
|
||||||
videoCodecs?: VideoCodec[],
|
|
||||||
resolutions?: HlsVideoResolution[],
|
|
||||||
) => {
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: true }, accel } });
|
||||||
ffmpeg: { realtime: { enabled: true, videoCodecs, resolutions } },
|
|
||||||
});
|
|
||||||
mocks.videoStream.getForMainPlaylist.mockResolvedValue(asset);
|
mocks.videoStream.getForMainPlaylist.mockResolvedValue(asset);
|
||||||
mocks.crypto.randomUUID.mockReturnValue(sessionId);
|
mocks.crypto.randomUUID.mockReturnValue(sessionId);
|
||||||
mocks.websocket.serverSend.mockImplementation((event, ...rest) => {
|
mocks.websocket.serverSend.mockImplementation((event, ...rest) => {
|
||||||
@@ -211,19 +184,19 @@ describe(HlsService.name, () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
it('offers AV1, HEVC, and H.264 when AV1 is configured and the accelerator supports it', async () => {
|
it('returns main playlist for eiffel-tower (1080p portrait, no acceleration)', async () => {
|
||||||
setup(eiffelTower, allCodecs);
|
setup(eiffelTower, TranscodeHardwareAcceleration.Disabled);
|
||||||
await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterAv1);
|
await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterDisabled);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('omits AV1 when it is not in the configured codecs', async () => {
|
it('returns main playlist for eiffel-tower with RKMPP (no AV1 variants)', async () => {
|
||||||
setup(eiffelTower);
|
setup(eiffelTower, TranscodeHardwareAcceleration.Rkmpp);
|
||||||
await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterNoAv1);
|
await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterRkmpp);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('offers every resolution up to the source and derives 4K codec levels (waterfall, 4K, 29.83fps)', async () => {
|
it('returns main playlist for waterfall (4K landscape) with no acceleration', async () => {
|
||||||
setup(waterfall, allCodecs, allResolutions);
|
setup(waterfall, TranscodeHardwareAcceleration.Disabled);
|
||||||
await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(waterfallExpectedMasterAv1);
|
await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(waterfallExpectedMasterDisabled);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws BadRequestException when realtime transcoding is disabled', async () => {
|
it('throws BadRequestException when realtime transcoding is disabled', async () => {
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { constants } from 'node:fs';
|
import { constants } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { HLS_SEGMENT_DURATION, HLS_SEGMENT_FILENAME_REGEX, HLS_VARIANTS, HLS_VERSION } from 'src/constants';
|
import {
|
||||||
|
HLS_SEGMENT_DURATION,
|
||||||
|
HLS_SEGMENT_FILENAME_REGEX,
|
||||||
|
HLS_VARIANTS,
|
||||||
|
HLS_VERSION,
|
||||||
|
SUPPORTED_HWA_CODECS,
|
||||||
|
} from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
@@ -12,7 +18,7 @@ import { BaseService } from 'src/services/base.service';
|
|||||||
import { VideoPacketInfo, VideoStreamInfo } from 'src/types';
|
import { VideoPacketInfo, VideoStreamInfo } from 'src/types';
|
||||||
import { PendingEvents } from 'src/utils/event';
|
import { PendingEvents } from 'src/utils/event';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
import { getCodecString, getOutputSize } from 'src/utils/media';
|
import { getOutputSize } from 'src/utils/media';
|
||||||
|
|
||||||
type AssetWithStreamInfo = { videoStream: VideoStreamInfo & { timeBase: number }; packets: VideoPacketInfo };
|
type AssetWithStreamInfo = { videoStream: VideoStreamInfo & { timeBase: number }; packets: VideoPacketInfo };
|
||||||
type Segmentation = { fps: number; framesPerSegment: number; segmentCount: number; segmentDuration: number };
|
type Segmentation = { fps: number; framesPerSegment: number; segmentCount: number; segmentDuration: number };
|
||||||
@@ -125,21 +131,18 @@ export class HlsService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private generateMainPlaylist(sessionId: string, ffmpeg: SystemConfigFFmpegDto, asset: AssetWithStreamInfo) {
|
private generateMainPlaylist(sessionId: string, ffmpeg: SystemConfigFFmpegDto, asset: AssetWithStreamInfo) {
|
||||||
const fps = (asset.packets.packetCount * asset.videoStream.timeBase) / asset.packets.totalDuration;
|
const fps = ((asset.packets.packetCount * asset.videoStream.timeBase) / asset.packets.totalDuration).toFixed(3);
|
||||||
const roundedFps = fps.toFixed(3);
|
|
||||||
const sourceResolution = Math.min(asset.videoStream.height, asset.videoStream.width);
|
const sourceResolution = Math.min(asset.videoStream.height, asset.videoStream.width);
|
||||||
const targetResolution = Math.max(sourceResolution, HLS_VARIANTS[0].resolution);
|
const targetResolution = Math.max(sourceResolution, HLS_VARIANTS[0].resolution);
|
||||||
const lines = ['#EXTM3U', `#EXT-X-VERSION:${HLS_VERSION}`, '#EXT-X-INDEPENDENT-SEGMENTS'];
|
const lines = ['#EXTM3U', `#EXT-X-VERSION:${HLS_VERSION}`, '#EXT-X-INDEPENDENT-SEGMENTS'];
|
||||||
const { videoCodecs, resolutions } = ffmpeg.realtime;
|
|
||||||
for (let i = 0; i < HLS_VARIANTS.length; i++) {
|
for (let i = 0; i < HLS_VARIANTS.length; i++) {
|
||||||
const { resolution, bitrate, codec } = HLS_VARIANTS[i];
|
const { resolution, bitrate, codec, codecString } = HLS_VARIANTS[i];
|
||||||
if (resolution > targetResolution || !videoCodecs.includes(codec) || !resolutions.includes(resolution)) {
|
if (resolution > targetResolution || !SUPPORTED_HWA_CODECS[ffmpeg.accel].includes(codec)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const { width, height } = getOutputSize(asset.videoStream, resolution);
|
const { width, height } = getOutputSize(asset.videoStream, resolution);
|
||||||
const codecString = getCodecString(codec, width, height, fps);
|
|
||||||
lines.push(
|
lines.push(
|
||||||
`#EXT-X-STREAM-INF:BANDWIDTH=${Math.round(bitrate * 1.35)},RESOLUTION=${width}x${height},CODECS="${codecString},mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=${roundedFps}`,
|
`#EXT-X-STREAM-INF:BANDWIDTH=${bitrate},RESOLUTION=${width}x${height},CODECS="${codecString},mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=${fps}`,
|
||||||
`${sessionId}/${i}/playlist.m3u8`,
|
`${sessionId}/${i}/playlist.m3u8`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
AudioCodec,
|
AudioCodec,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
CQMode,
|
CQMode,
|
||||||
HlsVideoResolution,
|
|
||||||
ImageFormat,
|
ImageFormat,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
OAuthTokenEndpointAuthMethod,
|
OAuthTokenEndpointAuthMethod,
|
||||||
@@ -77,8 +76,6 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
tonemap: ToneMapping.Hable,
|
tonemap: ToneMapping.Hable,
|
||||||
realtime: {
|
realtime: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
videoCodecs: [VideoCodec.H264, VideoCodec.Hevc],
|
|
||||||
resolutions: [HlsVideoResolution.p480, HlsVideoResolution.p720, HlsVideoResolution.p1080],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
integrityChecks: {
|
integrityChecks: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AUDIO_ENCODER, AV1_LEVELS, CodecLevel, H264_LEVELS, HEVC_LEVELS, SUPPORTED_HWA_CODECS } from 'src/constants';
|
import { AUDIO_ENCODER, SUPPORTED_HWA_CODECS } from 'src/constants';
|
||||||
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
||||||
import {
|
import {
|
||||||
ColorMatrix,
|
ColorMatrix,
|
||||||
@@ -36,29 +36,6 @@ export const getOutputSize = (videoStream: VideoStreamInfo, targetRes: number) =
|
|||||||
return isVideoVertical(videoStream) ? { width: targetRes, height: larger } : { width: larger, height: targetRes };
|
return isVideoVertical(videoStream) ? { width: targetRes, height: larger } : { width: larger, height: targetRes };
|
||||||
};
|
};
|
||||||
|
|
||||||
const pickLevel = (levels: CodecLevel[], frame: number, rate: number) =>
|
|
||||||
levels.find((level) => frame <= level.maxFrame && rate <= level.maxRate) ?? levels.at(-1)!;
|
|
||||||
|
|
||||||
export const getCodecString = (codec: VideoCodec, width: number, height: number, fps: number): string => {
|
|
||||||
switch (codec) {
|
|
||||||
case VideoCodec.H264: {
|
|
||||||
const macroblocks = Math.ceil(width / 16) * Math.ceil(height / 16);
|
|
||||||
return `avc1.6400${pickLevel(H264_LEVELS, macroblocks, macroblocks * fps).token}`;
|
|
||||||
}
|
|
||||||
case VideoCodec.Hevc: {
|
|
||||||
const samples = width * height;
|
|
||||||
return `hvc1.1.6.${pickLevel(HEVC_LEVELS, samples, samples * fps).token}.B0`;
|
|
||||||
}
|
|
||||||
case VideoCodec.Av1: {
|
|
||||||
const samples = width * height;
|
|
||||||
return `av01.0.${pickLevel(AV1_LEVELS, samples, samples * fps).token}.08`;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw new Error(`Codec '${codec}' does not support HLS codec strings`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export class BaseConfig implements VideoCodecSWConfig {
|
export class BaseConfig implements VideoCodecSWConfig {
|
||||||
readonly presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast'];
|
readonly presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast'];
|
||||||
protected constructor(
|
protected constructor(
|
||||||
|
|||||||
@@ -134,13 +134,10 @@ const notifyAddToAlbum = ($t: MessageFormatter, albumId: string, assetIds: strin
|
|||||||
const successCount = results.filter(({ success }) => success).length;
|
const successCount = results.filter(({ success }) => success).length;
|
||||||
const duplicateCount = results.filter(({ error }) => error === 'duplicate').length;
|
const duplicateCount = results.filter(({ error }) => error === 'duplicate').length;
|
||||||
let description = $t('assets_cannot_be_added_to_album_count', { values: { count: assetIds.length } });
|
let description = $t('assets_cannot_be_added_to_album_count', { values: { count: assetIds.length } });
|
||||||
|
if (successCount > 0) {
|
||||||
if (duplicateCount === assetIds.length) {
|
|
||||||
description = $t('assets_were_part_of_album_count', { values: { count: duplicateCount } });
|
|
||||||
} else if (successCount === assetIds.length) {
|
|
||||||
description = $t('assets_added_to_album_count', { values: { count: successCount } });
|
description = $t('assets_added_to_album_count', { values: { count: successCount } });
|
||||||
} else if (successCount > 0) {
|
} else if (duplicateCount > 0) {
|
||||||
description = $t('assets_added_to_album_partial_count', { values: { successCount, totalCount: assetIds.length } });
|
description = $t('assets_were_part_of_album_count', { values: { count: duplicateCount } });
|
||||||
}
|
}
|
||||||
|
|
||||||
toastManager.primary(
|
toastManager.primary(
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
import {
|
import {
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
CQMode,
|
CQMode,
|
||||||
HlsVideoResolution,
|
|
||||||
ToneMapping,
|
ToneMapping,
|
||||||
TranscodeHWAccel,
|
TranscodeHWAccel,
|
||||||
TranscodePolicy,
|
TranscodePolicy,
|
||||||
@@ -402,45 +401,9 @@
|
|||||||
title={$t('admin.transcoding_realtime_enabled')}
|
title={$t('admin.transcoding_realtime_enabled')}
|
||||||
subtitle={$t('admin.transcoding_realtime_enabled_description')}
|
subtitle={$t('admin.transcoding_realtime_enabled_description')}
|
||||||
bind:checked={configToEdit.ffmpeg.realtime.enabled}
|
bind:checked={configToEdit.ffmpeg.realtime.enabled}
|
||||||
isEdited={configToEdit.ffmpeg.realtime.enabled !== config.ffmpeg.realtime.enabled}
|
isEdited={configToEdit.ffmpeg.realtime.enabled !== configToEdit.ffmpeg.realtime.enabled}
|
||||||
{disabled}
|
{disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingCheckboxes
|
|
||||||
label={$t('admin.transcoding_realtime_video_codecs')}
|
|
||||||
desc={$t('admin.transcoding_realtime_video_codecs_description')}
|
|
||||||
disabled={disabled || !configToEdit.ffmpeg.realtime.enabled}
|
|
||||||
bind:value={configToEdit.ffmpeg.realtime.videoCodecs}
|
|
||||||
name="realtimeVideoCodecs"
|
|
||||||
options={[
|
|
||||||
{ value: VideoCodec.H264, text: 'H.264' },
|
|
||||||
{ value: VideoCodec.Hevc, text: 'HEVC' },
|
|
||||||
{ value: VideoCodec.Av1, text: 'AV1' },
|
|
||||||
]}
|
|
||||||
isEdited={!isEqual(
|
|
||||||
sortBy(configToEdit.ffmpeg.realtime.videoCodecs),
|
|
||||||
sortBy(config.ffmpeg.realtime.videoCodecs),
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingCheckboxes
|
|
||||||
label={$t('admin.transcoding_realtime_resolutions')}
|
|
||||||
desc={$t('admin.transcoding_realtime_resolutions_description')}
|
|
||||||
disabled={disabled || !configToEdit.ffmpeg.realtime.enabled}
|
|
||||||
bind:value={configToEdit.ffmpeg.realtime.resolutions}
|
|
||||||
name="realtimeResolutions"
|
|
||||||
options={[
|
|
||||||
{ value: HlsVideoResolution.$480, text: '480p' },
|
|
||||||
{ value: HlsVideoResolution.$720, text: '720p' },
|
|
||||||
{ value: HlsVideoResolution.$1080, text: '1080p' },
|
|
||||||
{ value: HlsVideoResolution.$1440, text: '1440p' },
|
|
||||||
{ value: HlsVideoResolution.$2160, text: '2160p' },
|
|
||||||
]}
|
|
||||||
isEdited={!isEqual(
|
|
||||||
sortBy(configToEdit.ffmpeg.realtime.resolutions),
|
|
||||||
sortBy(config.ffmpeg.realtime.resolutions),
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</SettingAccordion>
|
</SettingAccordion>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script lang="ts" generics="T extends string | number">
|
<script lang="ts" generics="T extends string">
|
||||||
import { Checkbox, Label } from '@immich/ui';
|
import { Checkbox, Label } from '@immich/ui';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { quintOut } from 'svelte/easing';
|
import { quintOut } from 'svelte/easing';
|
||||||
|
|||||||
Reference in New Issue
Block a user