From 2c67090e3cdb931748d92cafdcccf34a99897d38 Mon Sep 17 00:00:00 2001 From: Sergey Kondrikov Date: Tue, 28 Mar 2023 22:03:43 +0300 Subject: [PATCH] feat(server): add transcode presets (#2084) * feat: add transcode presets * Add migration * chore: generate api * refactor: use enum type instead of string for transcode option * chore: generate api * refactor: enhance readability of runVideoEncode method * refactor: reuse SettingSelect for transcoding presets * refactor: simplify return statement * chore: regenerate api * fix: correct label attribute * Update import * fix test --------- Co-authored-by: Alex --- mobile/openapi/doc/SystemConfigFFmpegDto.md | Bin 595 -> 594 bytes .../lib/model/system_config_f_fmpeg_dto.dart | Bin 4932 -> 7964 bytes .../test/system_config_f_fmpeg_dto_test.dart | Bin 1126 -> 1122 bytes .../processors/video-transcode.processor.ts | 41 +++++++++++++++--- server/immich-openapi-specs.json | 11 +++-- .../dto/system-config-ffmpeg.dto.ts | 7 +-- .../src/system-config/system-config.core.ts | 4 +- .../system-config.service.spec.ts | 4 +- server/libs/domain/test/fixtures.ts | 3 +- .../src/db/entities/system-config.entity.ts | 10 ++++- .../1679751316282-UpdateTranscodeOption.ts | 27 ++++++++++++ web/src/api/open-api/api.ts | 13 +++++- .../settings/ffmpeg/ffmpeg-settings.svelte | 31 +++++++++---- .../admin-page/settings/setting-select.svelte | 11 ++--- 14 files changed, 128 insertions(+), 34 deletions(-) create mode 100644 server/libs/infra/src/db/migrations/1679751316282-UpdateTranscodeOption.ts diff --git a/mobile/openapi/doc/SystemConfigFFmpegDto.md b/mobile/openapi/doc/SystemConfigFFmpegDto.md index cfe86a5afb03b23ece04cf408103aa850eba61cf..11ccb1fa0bff6859b3a40c6cdda84051fa579c48 100644 GIT binary patch delta 15 Xcmcc2a*1WbbjHbyjAE15F)jrFFAW8V delta 26 icmcb_a+zhrbVfeMoE$AJg&GAdt)%??oXJZV*8u=|d&M{FaN#X*eSlb_D0)D(HSY3ex6C;e~Djx)v8@u!Sl(+?^nInmIpB3_2yo2 zt8w!S#~aS@ZhLzh4n?M!&@s~-7zjQEPW4Y?J;q8nOq4v0BOXEjlqES2I_MWlbfSue zPR!20H?JQ4!NSpqgb#6o@iBD77wo6`^fm1 zfaR5zrt>y*_iR9OiJj;;wB-Ry66jKq?=2UpT6ovO0s$NkIni;7>5bQoRj=|MJ&T`S zd+VivHGI5ksQ6cTtawB@N)R5!nWmXr483M;N zgiHW}N9h8<-?8Cz$O&)*=f0CT04+<7ZJEeSAiZt50aU$4Ofwj=@EKW~D4TASP63S@ zzH_z&f?2W=3$;`;LhOSV8T*FJgSz%!ue(^ZcV6U)duNGO@8$dd%e>qr3!G0xI&%Dg zATU+YkIrHVX&MJ+x*2Llze3Cy1bs3#5Hsre+zClLj^6eg4skq4Q#n1#lO%u`$Ek9Z zAsh!u&wmHTVGF5Y$iYr5f-Db1o@FBpG9y}3dFU@d;K@0KRb1yv@Tdcy$tgeOY763( z+#{A`mn1KPUs8fiPMmxy;`4Np@RSS9qlZdKW!oGpITbD#B$G>MV4*V@BOEiBQa>1S z|M4M&cyz)Mdc^61WE=RYTNaUCONDKx^5B$56)5D8XI!C);;4Nwicy}C)M8|rV6@3( zq5O0}D}`mQ4zgFG?^na8sc*sAP|Ad)$Ig`AW;oG#+iU_5=sp?3?XDS4b&f)eFPv~} zZ#!CAYWnSk0W@*VaXwbOcZy%VoA+I6VOElK`zdWJ$u{=yy7uMWicckxE#tJr-|BL( zaEthe<%!;@bK5>|*CNxRLpSOln+Q3JSHF)?8e*TmWRK B5}^P9 diff --git a/server/apps/microservices/src/processors/video-transcode.processor.ts b/server/apps/microservices/src/processors/video-transcode.processor.ts index f8fd85759..18c18d491 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 1e5f48f16..338283e76 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 5dc75c62d..6ccae3b95 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 b43a69b30..57458cee2 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 34ed99be1..a0bd08dfb 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 aab3cb52f..ef4f79bd6 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 0c47534cb..6e0237b42 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 000000000..989622e83 --- /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 c139fef28..7f9636277 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 e6746f5ae..be1775ef2 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 9f6ff7636..ae72bd651 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}