From 76f8d030ceb383c7abe78d92e5df1bb905ba5903 Mon Sep 17 00:00:00 2001 From: t4keda Date: Tue, 30 Jan 2024 02:40:02 +0100 Subject: [PATCH] added a configuration option to select the dri node in transcoding (#6376) * added a configuration option to select the dri node in transcoding * chore: open api * refactor: get hawrdware device --------- Co-authored-by: Jason Rasmussen --- mobile/openapi/doc/SystemConfigFFmpegDto.md | Bin 1236 -> 1277 bytes .../lib/model/system_config_f_fmpeg_dto.dart | Bin 7691 -> 8046 bytes .../test/system_config_f_fmpeg_dto_test.dart | Bin 2537 -> 2656 bytes open-api/immich-openapi-specs.json | 4 + open-api/typescript-sdk/client/api.ts | 6 ++ server/src/domain/media/media.service.spec.ts | 71 ++++++++++++++++++ server/src/domain/media/media.util.ts | 33 +++++++- .../dto/system-config-ffmpeg.dto.ts | 3 + .../system-config/system-config.core.ts | 1 + .../system-config.service.spec.ts | 1 + .../infra/entities/system-config.entity.ts | 2 + .../settings/ffmpeg/ffmpeg-settings.svelte | 7 ++ 12 files changed, 125 insertions(+), 3 deletions(-) diff --git a/mobile/openapi/doc/SystemConfigFFmpegDto.md b/mobile/openapi/doc/SystemConfigFFmpegDto.md index 25726bb3daf84d5e737743b0c33d14a640d36400..05fe1c443734017526a12f4749a5bd424f0ced3f 100644 GIT binary patch delta 31 mcmcb@`ImD;4-;QnYEe;YibuIiYFTD->g0YViOro%-b?_-!V7)? delta 12 Tcmey%d4+RB57XwYOdd=CB`*Zl 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 fca090bd3bdc9f8960ac01a323d4a97b6b90b809..b1c0f278a98c7d2a24a54493f68ffb28693a0fcf 100644 GIT binary patch delta 259 zcmeCSd1tp_H4|T2YEe;YibuIiYFTD->f``skahy83MCnt#d?^^kMd=+qB(Q&R}n=) QOqB{EmYnD+{bct70B#OouK)l5 delta 46 zcmaE7*KM<5HPhw`OkWu{KVsR3)S{x)6pwP3)UwRv)X5iFRnUbT*mXA_VO3=T008Y0*8l(j delta 12 TcmaDL@=|!iIo8dtY|< { ); }); + it('should set options for qsv with custom dri node', async () => { + storageMock.readdir.mockResolvedValue(['renderD128']); + mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); + configMock.load.mockResolvedValue([ + { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.QSV }, + { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' }, + { key: SystemConfigKey.FFMPEG_PREFERRED_HW_DEVICE, value: '/dev/dri/renderD128' }, + ]); + assetMock.getByIds.mockResolvedValue([assetStub.video]); + await sut.handleVideoConversion({ id: assetStub.video.id }); + expect(mediaMock.transcode).toHaveBeenCalledWith( + '/original/path.ext', + 'upload/encoded-video/user-id/as/se/asset-id.mp4', + { + inputOptions: ['-init_hw_device qsv=hw,child_device=/dev/dri/renderD128', '-filter_hw_device hw'], + outputOptions: [ + `-c:v h264_qsv`, + '-c:a aac', + '-movflags faststart', + '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', + '-bf 7', + '-refs 5', + '-g 256', + '-v verbose', + '-vf format=nv12,hwupload=extra_hw_frames=64,scale_qsv=-1:720', + '-preset 7', + '-global_quality 23', + '-maxrate 10000k', + '-bufsize 20000k', + ], + twoPass: false, + }, + ); + }); + it('should omit preset for qsv if invalid', async () => { storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); @@ -1613,6 +1650,40 @@ describe(MediaService.name, () => { ); }); + it('should select specific gpu node if selected', async () => { + storageMock.readdir.mockResolvedValue(['renderD129', 'card1', 'card0', 'renderD128']); + mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); + configMock.load.mockResolvedValue([ + { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI }, + { key: SystemConfigKey.FFMPEG_PREFERRED_HW_DEVICE, value: '/dev/dri/renderD128' }, + ]); + assetMock.getByIds.mockResolvedValue([assetStub.video]); + await sut.handleVideoConversion({ id: assetStub.video.id }); + expect(mediaMock.transcode).toHaveBeenCalledWith( + '/original/path.ext', + 'upload/encoded-video/user-id/as/se/asset-id.mp4', + { + inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'], + outputOptions: [ + `-c:v h264_vaapi`, + '-c:a aac', + '-movflags faststart', + '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', + '-g 256', + '-v verbose', + '-vf format=nv12,hwupload,scale_vaapi=-2:720', + '-compression_level 7', + '-qp 23', + '-global_quality 23', + '-rc_mode 1', + ], + twoPass: false, + }, + ); + }); + it('should fallback to sw transcoding if hw transcoding fails', async () => { storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); diff --git a/server/src/domain/media/media.util.ts b/server/src/domain/media/media.util.ts index 6741be5df..6166a6d5c 100644 --- a/server/src/domain/media/media.util.ts +++ b/server/src/domain/media/media.util.ts @@ -285,6 +285,20 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig { } return this.config.gopSize; } + + getPreferredHardwareDevice(): string | null { + const device = this.config.preferredHwDevice; + if (device === 'auto') { + return null; + } + + const deviceName = device.replace('/dev/dri/', ''); + if (!this.devices.includes(deviceName)) { + throw new Error(`Device '${device}' does not exist`); + } + + return device; + } } export class ThumbnailConfig extends BaseConfig { @@ -463,7 +477,14 @@ export class QSVConfig extends BaseHWConfig { if (!this.devices.length) { throw Error('No QSV device found'); } - return ['-init_hw_device qsv=hw', '-filter_hw_device hw']; + + let qsvString = ''; + const hwDevice = this.getPreferredHardwareDevice(); + if (hwDevice !== null) { + qsvString = `,child_device=${hwDevice}`; + } + + return [`-init_hw_device qsv=hw${qsvString}`, '-filter_hw_device hw']; } getBaseOutputOptions(videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { @@ -527,9 +548,15 @@ export class QSVConfig extends BaseHWConfig { export class VAAPIConfig extends BaseHWConfig { getBaseInputOptions() { if (this.devices.length === 0) { - throw Error('No VAAPI device found'); + throw new Error('No VAAPI device found'); } - return [`-init_hw_device vaapi=accel:/dev/dri/${this.devices[0]}`, '-filter_hw_device accel']; + + let hwDevice = this.getPreferredHardwareDevice(); + if (hwDevice === null) { + hwDevice = `/dev/dri/${this.devices[0]}`; + } + + return [`-init_hw_device vaapi=accel:${hwDevice}`, '-filter_hw_device accel']; } getFilterOptions(videoStream: VideoStreamInfo) { diff --git a/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts b/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts index e82ce4d7e..2783e35e6 100644 --- a/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts +++ b/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts @@ -78,6 +78,9 @@ export class SystemConfigFFmpegDto { @IsBoolean() twoPass!: boolean; + @IsString() + preferredHwDevice!: string; + @IsEnum(TranscodePolicy) @ApiProperty({ enumName: 'TranscodePolicy', enum: TranscodePolicy }) transcode!: TranscodePolicy; diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index 926703d0d..6be0ee81a 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -43,6 +43,7 @@ export const defaults = Object.freeze({ temporalAQ: false, cqMode: CQMode.AUTO, twoPass: false, + preferredHwDevice: 'auto', transcode: TranscodePolicy.REQUIRED, tonemap: ToneMapping.HABLE, accel: TranscodeHWAccel.DISABLED, diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index d32fcb82e..469e118a9 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -55,6 +55,7 @@ const updatedConfig = Object.freeze({ temporalAQ: false, cqMode: CQMode.AUTO, twoPass: false, + preferredHwDevice: 'auto', transcode: TranscodePolicy.REQUIRED, accel: TranscodeHWAccel.DISABLED, tonemap: ToneMapping.HABLE, diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/infra/entities/system-config.entity.ts index e280d0ce7..f07dd760b 100644 --- a/server/src/infra/entities/system-config.entity.ts +++ b/server/src/infra/entities/system-config.entity.ts @@ -30,6 +30,7 @@ export enum SystemConfigKey { FFMPEG_TEMPORAL_AQ = 'ffmpeg.temporalAQ', FFMPEG_CQ_MODE = 'ffmpeg.cqMode', FFMPEG_TWO_PASS = 'ffmpeg.twoPass', + FFMPEG_PREFERRED_HW_DEVICE = 'ffmpeg.preferredHwDevice', FFMPEG_TRANSCODE = 'ffmpeg.transcode', FFMPEG_ACCEL = 'ffmpeg.accel', FFMPEG_TONEMAP = 'ffmpeg.tonemap', @@ -176,6 +177,7 @@ export interface SystemConfig { temporalAQ: boolean; cqMode: CQMode; twoPass: boolean; + preferredHwDevice: string; transcode: TranscodePolicy; accel: TranscodeHWAccel; tonemap: ToneMapping; 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 bac314d66..0dc6a85a1 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 @@ -282,6 +282,13 @@ bind:checked={config.ffmpeg.temporalAQ} isEdited={config.ffmpeg.temporalAQ !== savedConfig.ffmpeg.temporalAQ} /> +