diff --git a/mobile/openapi/doc/SystemConfigFFmpegDto.md b/mobile/openapi/doc/SystemConfigFFmpegDto.md index cfe86a5afb..11ccb1fa0b 100644 Binary files a/mobile/openapi/doc/SystemConfigFFmpegDto.md and b/mobile/openapi/doc/SystemConfigFFmpegDto.md differ 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 9c929b652d..d9e1ad6696 100644 Binary files a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart and b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart differ 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 e0b862f1a2..62297cb2bb 100644 Binary files a/mobile/openapi/test/system_config_f_fmpeg_dto_test.dart and b/mobile/openapi/test/system_config_f_fmpeg_dto_test.dart differ diff --git a/server/apps/microservices/src/processors/video-transcode.processor.ts b/server/apps/microservices/src/processors/video-transcode.processor.ts index f8fd857597..18c18d4910 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 1e5f48f163..338283e761 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 5dc75c62d3..6ccae3b95d 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 b43a69b300..57458cee23 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 34ed99be15..a0bd08dfb1 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 aab3cb52fe..ef4f79bd6d 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 0c47534cbd..6e0237b428 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 0000000000..989622e831 --- /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 c139fef28f..7f96362777 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 e6746f5ae8..be1775ef21 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 9f6ff7636e..ae72bd6513 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}