diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 5fd947e86097f..909b9d02e32d3 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1637,7 +1637,10 @@ describe(MediaService.name, () => { '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', expect.objectContaining({ - inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']), + inputOptions: expect.arrayContaining([ + '-init_hw_device qsv=hw,child_device=/dev/dri/renderD128', + '-filter_hw_device hw', + ]), outputOptions: expect.arrayContaining([ `-c:v h264_qsv`, '-c:a copy', @@ -1696,7 +1699,10 @@ describe(MediaService.name, () => { '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', expect.objectContaining({ - inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']), + inputOptions: expect.arrayContaining([ + '-init_hw_device qsv=hw,child_device=/dev/dri/renderD128', + '-filter_hw_device hw', + ]), outputOptions: expect.not.arrayContaining([expect.stringContaining('-preset')]), twoPass: false, }), @@ -1713,7 +1719,10 @@ describe(MediaService.name, () => { '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', expect.objectContaining({ - inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']), + inputOptions: expect.arrayContaining([ + '-init_hw_device qsv=hw,child_device=/dev/dri/renderD128', + '-filter_hw_device hw', + ]), outputOptions: expect.arrayContaining(['-low_power 1']), twoPass: false, }), @@ -1730,6 +1739,26 @@ describe(MediaService.name, () => { expect(loggerMock.debug).toHaveBeenCalledWith('No devices found in /dev/dri.'); }); + it('should prefer higher index renderD* device for qsv', async () => { + storageMock.readdir.mockResolvedValue(['card1', 'renderD129', 'card0', 'renderD128']); + mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); + systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } }); + 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', + expect.objectContaining({ + inputOptions: expect.arrayContaining([ + '-init_hw_device qsv=hw,child_device=/dev/dri/renderD129', + '-filter_hw_device hw', + ]), + outputOptions: expect.arrayContaining([`-c:v h264_qsv`]), + twoPass: false, + }), + ); + }); + it('should use hardware decoding for qsv if enabled', async () => { storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); @@ -1750,6 +1779,7 @@ describe(MediaService.name, () => { '-async_depth 4', '-noautorotate', '-threads 1', + '-qsv_device /dev/dri/renderD128', ]), outputOptions: expect.arrayContaining([ expect.stringContaining('scale_qsv=-1:720:async_depth=4:mode=hq:format=nv12'), @@ -1939,28 +1969,8 @@ describe(MediaService.name, () => { ); }); - it('should prefer gpu for vaapi if available', async () => { - storageMock.readdir.mockResolvedValue(['renderD129', 'card1', 'card0', 'renderD128']); - mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); - systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } }); - 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', - expect.objectContaining({ - inputOptions: expect.arrayContaining([ - '-init_hw_device vaapi=accel:/dev/dri/card1', - '-filter_hw_device accel', - ]), - outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]), - twoPass: false, - }), - ); - }); - - it('should prefer higher index gpu node', async () => { - storageMock.readdir.mockResolvedValue(['renderD129', 'renderD130', 'renderD128']); + it('should prefer higher index renderD* device for vaapi', async () => { + storageMock.readdir.mockResolvedValue(['card1', 'renderD129', 'card0', 'renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); @@ -1970,7 +1980,7 @@ describe(MediaService.name, () => { 'upload/encoded-video/user-id/as/se/asset-id.mp4', expect.objectContaining({ inputOptions: expect.arrayContaining([ - '-init_hw_device vaapi=accel:/dev/dri/renderD130', + '-init_hw_device vaapi=accel:/dev/dri/renderD129', '-filter_hw_device accel', ]), outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]), @@ -2020,6 +2030,7 @@ describe(MediaService.name, () => { '-hwaccel_output_format vaapi', '-noautorotate', '-threads 1', + '-hwaccel_device /dev/dri/renderD128', ]), outputOptions: expect.arrayContaining([ expect.stringContaining('scale_vaapi=-2:720:mode=hq:out_range=pc:format=nv12'), diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index c7df4d27a76a4..226f95b4bb69e 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -322,14 +322,14 @@ export class BaseConfig implements VideoCodecSWConfig { } export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig { - protected devices: string[]; + protected device: string; constructor( protected config: SystemConfigFFmpegDto, devices: string[] = [], ) { super(config); - this.devices = this.validateDevices(devices); + this.device = this.getDevice(devices); } getSupportedCodecs() { @@ -337,18 +337,29 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig { } validateDevices(devices: string[]) { - return devices - .filter((device) => device.startsWith('renderD') || device.startsWith('card')) - .sort((a, b) => { - // order GPU devices first - if (a.startsWith('card') && b.startsWith('renderD')) { - return -1; - } - if (a.startsWith('renderD') && b.startsWith('card')) { - return 1; - } - return -a.localeCompare(b); - }); + if (devices.length === 0) { + throw new Error('No /dev/dri devices found. If using Docker, make sure at least one /dev/dri device is mounted'); + } + + return devices.filter(function (device) { + return device.startsWith('renderD') || device.startsWith('card'); + }); + } + + getDevice(devices: string[]) { + if (this.config.preferredHwDevice === 'auto') { + // eslint-disable-next-line unicorn/no-array-reduce + return `/dev/dri/${this.validateDevices(devices).reduce(function (a, b) { + return a.localeCompare(b) < 0 ? b : a; + })}`; + } + + const deviceName = this.config.preferredHwDevice.replace('/dev/dri/', ''); + if (!devices.includes(deviceName)) { + throw new Error(`Device '${deviceName}' does not exist. If using Docker, make sure this device is mounted`); + } + + return `/dev/dri/${deviceName}`; } getVideoCodec(): string { @@ -361,20 +372,6 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig { } return this.config.gopSize; } - - getPreferredHardwareDevice(): string | undefined { - const device = this.config.preferredHwDevice; - if (device === 'auto') { - return; - } - - const deviceName = device.replace('/dev/dri/', ''); - if (!this.devices.includes(deviceName)) { - throw new Error(`Device '${device}' does not exist`); - } - - return `/dev/dri/${deviceName}`; - } } export class ThumbnailConfig extends BaseConfig { @@ -513,12 +510,16 @@ export class AV1Config extends BaseConfig { } export class NvencSwDecodeConfig extends BaseHWConfig { + getDevice() { + return '0'; + } + getSupportedCodecs() { return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.AV1]; } getBaseInputOptions() { - return ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']; + return [`-init_hw_device cuda=cuda:${this.device}`, '-filter_hw_device cuda']; } getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { @@ -641,17 +642,7 @@ export class NvencHwDecodeConfig extends NvencSwDecodeConfig { export class QsvSwDecodeConfig extends BaseHWConfig { getBaseInputOptions() { - if (this.devices.length === 0) { - throw new Error('No QSV device found'); - } - - let qsvString = ''; - const hwDevice = this.getPreferredHardwareDevice(); - if (hwDevice) { - qsvString = `,child_device=${hwDevice}`; - } - - return [`-init_hw_device qsv=hw${qsvString}`, '-filter_hw_device hw']; + return [`-init_hw_device qsv=hw,child_device=${this.device}`, '-filter_hw_device hw']; } getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { @@ -721,23 +712,14 @@ export class QsvSwDecodeConfig extends BaseHWConfig { export class QsvHwDecodeConfig extends QsvSwDecodeConfig { getBaseInputOptions() { - if (this.devices.length === 0) { - throw new Error('No QSV device found'); - } - - const options = [ + return [ '-hwaccel qsv', '-hwaccel_output_format qsv', '-async_depth 4', '-noautorotate', + `-qsv_device ${this.device}`, ...this.getInputThreadOptions(), ]; - const hwDevice = this.getPreferredHardwareDevice(); - if (hwDevice) { - options.push(`-qsv_device ${hwDevice}`); - } - - return options; } getFilterOptions(videoStream: VideoStreamInfo) { @@ -789,16 +771,7 @@ export class QsvHwDecodeConfig extends QsvSwDecodeConfig { export class VaapiSwDecodeConfig extends BaseHWConfig { getBaseInputOptions() { - if (this.devices.length === 0) { - throw new Error('No VAAPI device found'); - } - - let hwDevice = this.getPreferredHardwareDevice(); - if (!hwDevice) { - hwDevice = `/dev/dri/${this.devices[0]}`; - } - - return [`-init_hw_device vaapi=accel:${hwDevice}`, '-filter_hw_device accel']; + return [`-init_hw_device vaapi=accel:${this.device}`, '-filter_hw_device accel']; } getFilterOptions(videoStream: VideoStreamInfo) { @@ -856,22 +829,13 @@ export class VaapiSwDecodeConfig extends BaseHWConfig { export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig { getBaseInputOptions() { - if (this.devices.length === 0) { - throw new Error('No VAAPI device found'); - } - - const options = [ + return [ '-hwaccel vaapi', '-hwaccel_output_format vaapi', '-noautorotate', + `-hwaccel_device ${this.device}`, ...this.getInputThreadOptions(), ]; - const hwDevice = this.getPreferredHardwareDevice(); - if (hwDevice) { - options.push(`-hwaccel_device ${hwDevice}`); - } - - return options; } getFilterOptions(videoStream: VideoStreamInfo) { @@ -934,9 +898,6 @@ export class RkmppSwDecodeConfig extends BaseHWConfig { } getBaseInputOptions(): string[] { - if (this.devices.length === 0) { - throw new Error('No RKMPP device found'); - } return []; } @@ -987,10 +948,6 @@ export class RkmppHwDecodeConfig extends RkmppSwDecodeConfig { } getBaseInputOptions() { - if (this.devices.length === 0) { - throw new Error('No RKMPP device found'); - } - return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga', '-noautorotate']; }