mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
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 <jrasm91@gmail.com>
This commit is contained in:
parent
68f8525eb1
commit
76f8d030ce
12 changed files with 125 additions and 3 deletions
BIN
mobile/openapi/doc/SystemConfigFFmpegDto.md
generated
BIN
mobile/openapi/doc/SystemConfigFFmpegDto.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart
generated
BIN
mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/system_config_f_fmpeg_dto_test.dart
generated
BIN
mobile/openapi/test/system_config_f_fmpeg_dto_test.dart
generated
Binary file not shown.
|
@ -9422,6 +9422,9 @@
|
||||||
"npl": {
|
"npl": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"preferredHwDevice": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"preset": {
|
"preset": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -9463,6 +9466,7 @@
|
||||||
"gopSize",
|
"gopSize",
|
||||||
"maxBitrate",
|
"maxBitrate",
|
||||||
"npl",
|
"npl",
|
||||||
|
"preferredHwDevice",
|
||||||
"preset",
|
"preset",
|
||||||
"refs",
|
"refs",
|
||||||
"targetAudioCodec",
|
"targetAudioCodec",
|
||||||
|
|
6
open-api/typescript-sdk/client/api.ts
generated
6
open-api/typescript-sdk/client/api.ts
generated
|
@ -3712,6 +3712,12 @@ export interface SystemConfigFFmpegDto {
|
||||||
* @memberof SystemConfigFFmpegDto
|
* @memberof SystemConfigFFmpegDto
|
||||||
*/
|
*/
|
||||||
'npl': number;
|
'npl': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SystemConfigFFmpegDto
|
||||||
|
*/
|
||||||
|
'preferredHwDevice': string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
|
|
@ -1380,6 +1380,43 @@ describe(MediaService.name, () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 () => {
|
it('should omit preset for qsv if invalid', async () => {
|
||||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
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 () => {
|
it('should fallback to sw transcoding if hw transcoding fails', async () => {
|
||||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
|
|
|
@ -285,6 +285,20 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig {
|
||||||
}
|
}
|
||||||
return this.config.gopSize;
|
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 {
|
export class ThumbnailConfig extends BaseConfig {
|
||||||
|
@ -463,7 +477,14 @@ export class QSVConfig extends BaseHWConfig {
|
||||||
if (!this.devices.length) {
|
if (!this.devices.length) {
|
||||||
throw Error('No QSV device found');
|
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) {
|
getBaseOutputOptions(videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
|
||||||
|
@ -527,9 +548,15 @@ export class QSVConfig extends BaseHWConfig {
|
||||||
export class VAAPIConfig extends BaseHWConfig {
|
export class VAAPIConfig extends BaseHWConfig {
|
||||||
getBaseInputOptions() {
|
getBaseInputOptions() {
|
||||||
if (this.devices.length === 0) {
|
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) {
|
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||||
|
|
|
@ -78,6 +78,9 @@ export class SystemConfigFFmpegDto {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
twoPass!: boolean;
|
twoPass!: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
preferredHwDevice!: string;
|
||||||
|
|
||||||
@IsEnum(TranscodePolicy)
|
@IsEnum(TranscodePolicy)
|
||||||
@ApiProperty({ enumName: 'TranscodePolicy', enum: TranscodePolicy })
|
@ApiProperty({ enumName: 'TranscodePolicy', enum: TranscodePolicy })
|
||||||
transcode!: TranscodePolicy;
|
transcode!: TranscodePolicy;
|
||||||
|
|
|
@ -43,6 +43,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||||
temporalAQ: false,
|
temporalAQ: false,
|
||||||
cqMode: CQMode.AUTO,
|
cqMode: CQMode.AUTO,
|
||||||
twoPass: false,
|
twoPass: false,
|
||||||
|
preferredHwDevice: 'auto',
|
||||||
transcode: TranscodePolicy.REQUIRED,
|
transcode: TranscodePolicy.REQUIRED,
|
||||||
tonemap: ToneMapping.HABLE,
|
tonemap: ToneMapping.HABLE,
|
||||||
accel: TranscodeHWAccel.DISABLED,
|
accel: TranscodeHWAccel.DISABLED,
|
||||||
|
|
|
@ -55,6 +55,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||||
temporalAQ: false,
|
temporalAQ: false,
|
||||||
cqMode: CQMode.AUTO,
|
cqMode: CQMode.AUTO,
|
||||||
twoPass: false,
|
twoPass: false,
|
||||||
|
preferredHwDevice: 'auto',
|
||||||
transcode: TranscodePolicy.REQUIRED,
|
transcode: TranscodePolicy.REQUIRED,
|
||||||
accel: TranscodeHWAccel.DISABLED,
|
accel: TranscodeHWAccel.DISABLED,
|
||||||
tonemap: ToneMapping.HABLE,
|
tonemap: ToneMapping.HABLE,
|
||||||
|
|
|
@ -30,6 +30,7 @@ export enum SystemConfigKey {
|
||||||
FFMPEG_TEMPORAL_AQ = 'ffmpeg.temporalAQ',
|
FFMPEG_TEMPORAL_AQ = 'ffmpeg.temporalAQ',
|
||||||
FFMPEG_CQ_MODE = 'ffmpeg.cqMode',
|
FFMPEG_CQ_MODE = 'ffmpeg.cqMode',
|
||||||
FFMPEG_TWO_PASS = 'ffmpeg.twoPass',
|
FFMPEG_TWO_PASS = 'ffmpeg.twoPass',
|
||||||
|
FFMPEG_PREFERRED_HW_DEVICE = 'ffmpeg.preferredHwDevice',
|
||||||
FFMPEG_TRANSCODE = 'ffmpeg.transcode',
|
FFMPEG_TRANSCODE = 'ffmpeg.transcode',
|
||||||
FFMPEG_ACCEL = 'ffmpeg.accel',
|
FFMPEG_ACCEL = 'ffmpeg.accel',
|
||||||
FFMPEG_TONEMAP = 'ffmpeg.tonemap',
|
FFMPEG_TONEMAP = 'ffmpeg.tonemap',
|
||||||
|
@ -176,6 +177,7 @@ export interface SystemConfig {
|
||||||
temporalAQ: boolean;
|
temporalAQ: boolean;
|
||||||
cqMode: CQMode;
|
cqMode: CQMode;
|
||||||
twoPass: boolean;
|
twoPass: boolean;
|
||||||
|
preferredHwDevice: string;
|
||||||
transcode: TranscodePolicy;
|
transcode: TranscodePolicy;
|
||||||
accel: TranscodeHWAccel;
|
accel: TranscodeHWAccel;
|
||||||
tonemap: ToneMapping;
|
tonemap: ToneMapping;
|
||||||
|
|
|
@ -282,6 +282,13 @@
|
||||||
bind:checked={config.ffmpeg.temporalAQ}
|
bind:checked={config.ffmpeg.temporalAQ}
|
||||||
isEdited={config.ffmpeg.temporalAQ !== savedConfig.ffmpeg.temporalAQ}
|
isEdited={config.ffmpeg.temporalAQ !== savedConfig.ffmpeg.temporalAQ}
|
||||||
/>
|
/>
|
||||||
|
<SettingInputField
|
||||||
|
inputType={SettingInputFieldType.TEXT}
|
||||||
|
label="PREFERRED HARDWARE DEVICE FOR TRANSCODING"
|
||||||
|
desc="Applies only to VAAPI and QSV. Sets the dri node used for hardware transcoding. Set to 'auto' to let immich decide for you"
|
||||||
|
bind:value={config.ffmpeg.preferredHwDevice}
|
||||||
|
isEdited={config.ffmpeg.preferredHwDevice !== savedConfig.ffmpeg.preferredHwDevice}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingAccordion>
|
</SettingAccordion>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue