diff --git a/mobile/openapi/doc/SystemConfigFFmpegDto.md b/mobile/openapi/doc/SystemConfigFFmpegDto.md index cfe86a5afb03b..11ccb1fa0bff6 100644 --- a/mobile/openapi/doc/SystemConfigFFmpegDto.md +++ b/mobile/openapi/doc/SystemConfigFFmpegDto.md @@ -13,7 +13,7 @@ Name | Type | Description | Notes **targetVideoCodec** | **String** | | **targetAudioCodec** | **String** | | **targetScaling** | **String** | | -**transcodeAll** | **bool** | | +**transcode** | **String** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart index 9c929b652d4a6..d9e1ad66960d6 100644 --- a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart +++ b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart @@ -18,7 +18,7 @@ class SystemConfigFFmpegDto { required this.targetVideoCodec, required this.targetAudioCodec, required this.targetScaling, - required this.transcodeAll, + required this.transcode, }); String crf; @@ -31,7 +31,7 @@ class SystemConfigFFmpegDto { String targetScaling; - bool transcodeAll; + SystemConfigFFmpegDtoTranscodeEnum transcode; @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigFFmpegDto && @@ -40,7 +40,7 @@ class SystemConfigFFmpegDto { other.targetVideoCodec == targetVideoCodec && other.targetAudioCodec == targetAudioCodec && other.targetScaling == targetScaling && - other.transcodeAll == transcodeAll; + other.transcode == transcode; @override int get hashCode => @@ -50,10 +50,10 @@ class SystemConfigFFmpegDto { (targetVideoCodec.hashCode) + (targetAudioCodec.hashCode) + (targetScaling.hashCode) + - (transcodeAll.hashCode); + (transcode.hashCode); @override - String toString() => 'SystemConfigFFmpegDto[crf=$crf, preset=$preset, targetVideoCodec=$targetVideoCodec, targetAudioCodec=$targetAudioCodec, targetScaling=$targetScaling, transcodeAll=$transcodeAll]'; + String toString() => 'SystemConfigFFmpegDto[crf=$crf, preset=$preset, targetVideoCodec=$targetVideoCodec, targetAudioCodec=$targetAudioCodec, targetScaling=$targetScaling, transcode=$transcode]'; Map toJson() { final json = {}; @@ -62,7 +62,7 @@ class SystemConfigFFmpegDto { json[r'targetVideoCodec'] = this.targetVideoCodec; json[r'targetAudioCodec'] = this.targetAudioCodec; json[r'targetScaling'] = this.targetScaling; - json[r'transcodeAll'] = this.transcodeAll; + json[r'transcode'] = this.transcode; return json; } @@ -90,7 +90,7 @@ class SystemConfigFFmpegDto { targetVideoCodec: mapValueOfType(json, r'targetVideoCodec')!, targetAudioCodec: mapValueOfType(json, r'targetAudioCodec')!, targetScaling: mapValueOfType(json, r'targetScaling')!, - transcodeAll: mapValueOfType(json, r'transcodeAll')!, + transcode: SystemConfigFFmpegDtoTranscodeEnum.fromJson(json[r'transcode'])!, ); } return null; @@ -145,7 +145,84 @@ class SystemConfigFFmpegDto { 'targetVideoCodec', 'targetAudioCodec', 'targetScaling', - 'transcodeAll', + 'transcode', }; } + +class SystemConfigFFmpegDtoTranscodeEnum { + /// Instantiate a new enum with the provided [value]. + const SystemConfigFFmpegDtoTranscodeEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const all = SystemConfigFFmpegDtoTranscodeEnum._(r'all'); + static const optimal = SystemConfigFFmpegDtoTranscodeEnum._(r'optimal'); + static const required_ = SystemConfigFFmpegDtoTranscodeEnum._(r'required'); + + /// List of all possible values in this [enum][SystemConfigFFmpegDtoTranscodeEnum]. + static const values = [ + all, + optimal, + required_, + ]; + + static SystemConfigFFmpegDtoTranscodeEnum? fromJson(dynamic value) => SystemConfigFFmpegDtoTranscodeEnumTypeTransformer().decode(value); + + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SystemConfigFFmpegDtoTranscodeEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [SystemConfigFFmpegDtoTranscodeEnum] to String, +/// and [decode] dynamic data back to [SystemConfigFFmpegDtoTranscodeEnum]. +class SystemConfigFFmpegDtoTranscodeEnumTypeTransformer { + factory SystemConfigFFmpegDtoTranscodeEnumTypeTransformer() => _instance ??= const SystemConfigFFmpegDtoTranscodeEnumTypeTransformer._(); + + const SystemConfigFFmpegDtoTranscodeEnumTypeTransformer._(); + + String encode(SystemConfigFFmpegDtoTranscodeEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a SystemConfigFFmpegDtoTranscodeEnum. + /// + /// 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. + SystemConfigFFmpegDtoTranscodeEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data.toString()) { + case r'all': return SystemConfigFFmpegDtoTranscodeEnum.all; + case r'optimal': return SystemConfigFFmpegDtoTranscodeEnum.optimal; + case r'required': return SystemConfigFFmpegDtoTranscodeEnum.required_; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [SystemConfigFFmpegDtoTranscodeEnumTypeTransformer] instance. + static SystemConfigFFmpegDtoTranscodeEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/test/system_config_f_fmpeg_dto_test.dart b/mobile/openapi/test/system_config_f_fmpeg_dto_test.dart index e0b862f1a2043..62297cb2bb938 100644 --- a/mobile/openapi/test/system_config_f_fmpeg_dto_test.dart +++ b/mobile/openapi/test/system_config_f_fmpeg_dto_test.dart @@ -41,8 +41,8 @@ void main() { // TODO }); - // bool transcodeAll - test('to test the property `transcodeAll`', () async { + // String transcode + test('to test the property `transcode`', () async { // TODO }); diff --git a/server/apps/microservices/src/processors/video-transcode.processor.ts b/server/apps/microservices/src/processors/video-transcode.processor.ts index f8fd8575974d0..18c18d49100fd 100644 --- a/server/apps/microservices/src/processors/video-transcode.processor.ts +++ b/server/apps/microservices/src/processors/video-transcode.processor.ts @@ -8,10 +8,11 @@ import { QueueName, StorageCore, StorageFolder, + SystemConfigFFmpegDto, SystemConfigService, WithoutProperty, } from '@app/domain'; -import { AssetEntity, AssetType } from '@app/infra/db/entities'; +import { AssetEntity, AssetType, TranscodePreset } from '@app/infra/db/entities'; import { Process, Processor } from '@nestjs/bull'; import { Inject, Logger } from '@nestjs/common'; import { Job } from 'bull'; @@ -74,10 +75,41 @@ export class VideoTranscodeProcessor { async runVideoEncode(asset: AssetEntity, savedEncodedPath: string): Promise { const config = await this.systemConfigService.getConfig(); - if (config.ffmpeg.transcodeAll) { + const transcode = await this.needsTranscoding(asset, config.ffmpeg); + if (transcode) { + //TODO: If video or audio are already the correct format, don't re-encode, copy the stream return this.runFFMPEGPipeLine(asset, savedEncodedPath); } + } + async needsTranscoding(asset: AssetEntity, ffmpegConfig: SystemConfigFFmpegDto): Promise { + switch (ffmpegConfig.transcode) { + case TranscodePreset.ALL: + return true; + + case TranscodePreset.REQUIRED: + { + const videoStream = await this.getVideoStream(asset); + if (videoStream.codec_name !== ffmpegConfig.targetVideoCodec) { + return true; + } + } + break; + + case TranscodePreset.OPTIMAL: { + const videoStream = await this.getVideoStream(asset); + if (videoStream.codec_name !== ffmpegConfig.targetVideoCodec) { + return true; + } + + const videoHeightThreshold = 1080; + return !videoStream.height || videoStream.height > videoHeightThreshold; + } + } + return false; + } + + async getVideoStream(asset: AssetEntity): Promise { const videoInfo = await this.runFFProbePipeline(asset); const videoStreams = videoInfo.streams.filter((stream) => { @@ -90,10 +122,7 @@ export class VideoTranscodeProcessor { return stream2Frames - stream1Frames; })[0]; - //TODO: If video or audio are already the correct format, don't re-encode, copy the stream - if (longestVideoStream.codec_name !== config.ffmpeg.targetVideoCodec) { - return this.runFFMPEGPipeLine(asset, savedEncodedPath); - } + return longestVideoStream; } async runFFMPEGPipeLine(asset: AssetEntity, savedEncodedPath: string): Promise { diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 1e5f48f1639f8..338283e761687 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -4601,8 +4601,13 @@ "targetScaling": { "type": "string" }, - "transcodeAll": { - "type": "boolean" + "transcode": { + "type": "string", + "enum": [ + "all", + "optimal", + "required" + ] } }, "required": [ @@ -4611,7 +4616,7 @@ "targetVideoCodec", "targetAudioCodec", "targetScaling", - "transcodeAll" + "transcode" ] }, "SystemConfigOAuthDto": { diff --git a/server/libs/domain/src/system-config/dto/system-config-ffmpeg.dto.ts b/server/libs/domain/src/system-config/dto/system-config-ffmpeg.dto.ts index 5dc75c62d31be..6ccae3b95d144 100644 --- a/server/libs/domain/src/system-config/dto/system-config-ffmpeg.dto.ts +++ b/server/libs/domain/src/system-config/dto/system-config-ffmpeg.dto.ts @@ -1,4 +1,5 @@ -import { IsBoolean, IsString } from 'class-validator'; +import { IsEnum, IsString } from 'class-validator'; +import { TranscodePreset } from '@app/infra/db/entities'; export class SystemConfigFFmpegDto { @IsString() @@ -16,6 +17,6 @@ export class SystemConfigFFmpegDto { @IsString() targetScaling!: string; - @IsBoolean() - transcodeAll!: boolean; + @IsEnum(TranscodePreset) + transcode!: TranscodePreset; } diff --git a/server/libs/domain/src/system-config/system-config.core.ts b/server/libs/domain/src/system-config/system-config.core.ts index b43a69b300f5e..57458cee23f69 100644 --- a/server/libs/domain/src/system-config/system-config.core.ts +++ b/server/libs/domain/src/system-config/system-config.core.ts @@ -1,4 +1,4 @@ -import { SystemConfig, SystemConfigEntity, SystemConfigKey } from '@app/infra/db/entities'; +import { SystemConfig, SystemConfigEntity, SystemConfigKey, TranscodePreset } from '@app/infra/db/entities'; import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import * as _ from 'lodash'; import { Subject } from 'rxjs'; @@ -14,7 +14,7 @@ const defaults: SystemConfig = Object.freeze({ targetVideoCodec: 'h264', targetAudioCodec: 'aac', targetScaling: '1280:-2', - transcodeAll: false, + transcode: TranscodePreset.REQUIRED, }, oauth: { enabled: false, diff --git a/server/libs/domain/src/system-config/system-config.service.spec.ts b/server/libs/domain/src/system-config/system-config.service.spec.ts index 34ed99be15b4d..a0bd08dfb1ae5 100644 --- a/server/libs/domain/src/system-config/system-config.service.spec.ts +++ b/server/libs/domain/src/system-config/system-config.service.spec.ts @@ -1,4 +1,4 @@ -import { SystemConfigEntity, SystemConfigKey } from '@app/infra/db/entities'; +import { SystemConfigEntity, SystemConfigKey, TranscodePreset } from '@app/infra/db/entities'; import { BadRequestException } from '@nestjs/common'; import { newJobRepositoryMock, newSystemConfigRepositoryMock, systemConfigStub } from '../../test'; import { IJobRepository, JobName } from '../job'; @@ -18,7 +18,7 @@ const updatedConfig = Object.freeze({ targetAudioCodec: 'aac', targetScaling: '1280:-2', targetVideoCodec: 'h264', - transcodeAll: false, + transcode: TranscodePreset.REQUIRED, }, oauth: { autoLaunch: true, diff --git a/server/libs/domain/test/fixtures.ts b/server/libs/domain/test/fixtures.ts index aab3cb52feb64..ef4f79bd6d3d8 100644 --- a/server/libs/domain/test/fixtures.ts +++ b/server/libs/domain/test/fixtures.ts @@ -6,6 +6,7 @@ import { SharedLinkEntity, SharedLinkType, SystemConfig, + TranscodePreset, UserEntity, UserTokenEntity, } from '@app/infra/db/entities'; @@ -401,7 +402,7 @@ export const systemConfigStub = { targetAudioCodec: 'aac', targetScaling: '1280:-2', targetVideoCodec: 'h264', - transcodeAll: false, + transcode: TranscodePreset.REQUIRED, }, oauth: { autoLaunch: false, diff --git a/server/libs/infra/src/db/entities/system-config.entity.ts b/server/libs/infra/src/db/entities/system-config.entity.ts index 0c47534cbd8b9..6e0237b428c21 100644 --- a/server/libs/infra/src/db/entities/system-config.entity.ts +++ b/server/libs/infra/src/db/entities/system-config.entity.ts @@ -18,7 +18,7 @@ export enum SystemConfigKey { FFMPEG_TARGET_VIDEO_CODEC = 'ffmpeg.targetVideoCodec', FFMPEG_TARGET_AUDIO_CODEC = 'ffmpeg.targetAudioCodec', FFMPEG_TARGET_SCALING = 'ffmpeg.targetScaling', - FFMPEG_TRANSCODE_ALL = 'ffmpeg.transcodeAll', + FFMPEG_TRANSCODE = 'ffmpeg.transcode', OAUTH_ENABLED = 'oauth.enabled', OAUTH_ISSUER_URL = 'oauth.issuerUrl', OAUTH_CLIENT_ID = 'oauth.clientId', @@ -33,6 +33,12 @@ export enum SystemConfigKey { STORAGE_TEMPLATE = 'storageTemplate.template', } +export enum TranscodePreset { + ALL = 'all', + OPTIMAL = 'optimal', + REQUIRED = 'required', +} + export interface SystemConfig { ffmpeg: { crf: string; @@ -40,7 +46,7 @@ export interface SystemConfig { targetVideoCodec: string; targetAudioCodec: string; targetScaling: string; - transcodeAll: boolean; + transcode: TranscodePreset; }; oauth: { enabled: boolean; diff --git a/server/libs/infra/src/db/migrations/1679751316282-UpdateTranscodeOption.ts b/server/libs/infra/src/db/migrations/1679751316282-UpdateTranscodeOption.ts new file mode 100644 index 0000000000000..989622e831fef --- /dev/null +++ b/server/libs/infra/src/db/migrations/1679751316282-UpdateTranscodeOption.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateTranscodeOption1679751316282 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE system_config + SET + key = 'ffmpeg.transcode', + value = '"all"' + WHERE + key = 'ffmpeg.transcodeAll' AND value = 'true' + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE system_config + SET + key = 'ffmpeg.transcodeAll', + value = 'true' + WHERE + key = 'ffmpeg.transcode' AND value = '"all"' + `); + + await queryRunner.query(`DELETE FROM "system_config" WHERE key = 'ffmpeg.transcode'`); + } +} diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index c139fef28faa8..7f96362777469 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -1987,11 +1987,20 @@ export interface SystemConfigFFmpegDto { 'targetScaling': string; /** * - * @type {boolean} + * @type {string} * @memberof SystemConfigFFmpegDto */ - 'transcodeAll': boolean; + 'transcode': SystemConfigFFmpegDtoTranscodeEnum; } + +export const SystemConfigFFmpegDtoTranscodeEnum = { + All: 'all', + Optimal: 'optimal', + Required: 'required' +} as const; + +export type SystemConfigFFmpegDtoTranscodeEnum = typeof SystemConfigFFmpegDtoTranscodeEnum[keyof typeof SystemConfigFFmpegDtoTranscodeEnum]; + /** * * @export diff --git a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte index e6746f5ae8393..be1775ef21617 100644 --- a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte +++ b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte @@ -3,11 +3,10 @@ notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; - import { api, SystemConfigFFmpegDto } from '@api'; + import { api, SystemConfigFFmpegDto, SystemConfigFFmpegDtoTranscodeEnum } from '@api'; import SettingButtonsRow from '../setting-buttons-row.svelte'; import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte'; import SettingSelect from '../setting-select.svelte'; - import SettingSwitch from '../setting-switch.svelte'; import { isEqual } from 'lodash-es'; import { fade } from 'svelte/transition'; @@ -105,7 +104,12 @@ @@ -117,11 +121,22 @@ isEdited={!(ffmpegConfig.targetScaling == savedConfig.targetScaling)} /> - diff --git a/web/src/lib/components/admin-page/settings/setting-select.svelte b/web/src/lib/components/admin-page/settings/setting-select.svelte index 9f6ff7636e737..ae72bd65139af 100644 --- a/web/src/lib/components/admin-page/settings/setting-select.svelte +++ b/web/src/lib/components/admin-page/settings/setting-select.svelte @@ -3,8 +3,9 @@ import { fly } from 'svelte/transition'; export let value: string; - export let options: string[]; + export let options: { value: string; text: string }[]; export let label = ''; + export let name = ''; export let isEdited = false; const handleChange = (e: Event) => { @@ -14,7 +15,7 @@
- + {#if isEdited}