diff --git a/docker/hwaccel.ml.yml b/docker/hwaccel.ml.yml index 3afc5ceafd..d9455d2bb7 100644 --- a/docker/hwaccel.ml.yml +++ b/docker/hwaccel.ml.yml @@ -1,9 +1,7 @@ -version: "3.8" - # Configurations for hardware-accelerated machine learning # If using Unraid or another platform that doesn't allow multiple Compose files, -# you can inline the config for a backend by copying its contents +# you can inline the config for a backend by copying its contents # into the immich-machine-learning service in the docker-compose.yml file. # See https://immich.app/docs/features/ml-hardware-acceleration for info on usage. @@ -30,7 +28,7 @@ services: openvino: device_cgroup_rules: - - "c 189:* rmw" + - 'c 189:* rmw' devices: - /dev/dri:/dev/dri volumes: diff --git a/docker/hwaccel.transcoding.yml b/docker/hwaccel.transcoding.yml index ef9c0a5bb1..bd4e2a46b8 100644 --- a/docker/hwaccel.transcoding.yml +++ b/docker/hwaccel.transcoding.yml @@ -1,5 +1,3 @@ -version: "3.8" - # Configurations for hardware-accelerated transcoding # If using Unraid or another platform that doesn't allow multiple Compose files, diff --git a/docs/docs/features/hardware-transcoding.md b/docs/docs/features/hardware-transcoding.md index cc195a300c..6067e7babb 100644 --- a/docs/docs/features/hardware-transcoding.md +++ b/docs/docs/features/hardware-transcoding.md @@ -22,7 +22,8 @@ You do not need to redo any transcoding jobs after enabling hardware acceleratio - WSL2 does not support Quick Sync. - Raspberry Pi is currently not supported. - Two-pass mode is only supported for NVENC. Other APIs will ignore this setting. -- Only encoding is currently hardware accelerated, so the CPU is still used for software decoding and tone-mapping. +- By default, only encoding is currently hardware accelerated. This means the CPU is still used for software decoding and tone-mapping. + - NVENC and RKMPP can be fully accelerated by enabling hardware decoding in the video transcoding settings. - Hardware dependent - Codec support varies, but H.264 and HEVC are usually supported. - Notably, NVIDIA and AMD GPUs do not support VP9 encoding. @@ -65,6 +66,7 @@ For RKMPP to work: 3. Redeploy the `immich-microservices` container with these updated settings. 4. In the Admin page under `Video transcoding settings`, change the hardware acceleration setting to the appropriate option and save. +5. (Optional) If using a compatible backend, you may enable hardware decoding for optimal performance. #### Single Compose File diff --git a/mobile/openapi/doc/SystemConfigFFmpegDto.md b/mobile/openapi/doc/SystemConfigFFmpegDto.md index 05fe1c4437..28ec94c8cc 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 136f1eec32..3c856bcdbe 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 3c038172bc..873b24089f 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/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index d25076d419..425bf81714 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -10050,6 +10050,9 @@ "accel": { "$ref": "#/components/schemas/TranscodeHWAccel" }, + "accelDecode": { + "type": "boolean" + }, "acceptedAudioCodecs": { "items": { "$ref": "#/components/schemas/AudioCodec" @@ -10125,6 +10128,7 @@ }, "required": [ "accel", + "accelDecode", "acceptedAudioCodecs", "acceptedVideoCodecs", "bframes", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index e174dda002..020188c8a8 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -863,6 +863,7 @@ export type AssetFullSyncDto = { }; export type SystemConfigFFmpegDto = { accel: TranscodeHWAccel; + accelDecode: boolean; acceptedAudioCodecs: AudioCodec[]; acceptedVideoCodecs: VideoCodec[]; bframes: number; diff --git a/server/src/config.ts b/server/src/config.ts index 6a7d2b754c..3ad981f97d 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -97,6 +97,7 @@ export interface SystemConfig { preferredHwDevice: string; transcode: TranscodePolicy; accel: TranscodeHWAccel; + accelDecode: boolean; tonemap: ToneMapping; }; job: Record; @@ -228,6 +229,7 @@ export const defaults = Object.freeze({ transcode: TranscodePolicy.REQUIRED, tonemap: ToneMapping.HABLE, accel: TranscodeHWAccel.DISABLED, + accelDecode: false, }, job: { [QueueName.BACKGROUND_TASK]: { concurrency: 5 }, diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 7cf9bb3f8e..4dcea6ec3d 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -132,6 +132,9 @@ export class SystemConfigFFmpegDto { @ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel }) accel!: TranscodeHWAccel; + @ValidateBoolean() + accelDecode!: boolean; + @IsEnum(ToneMapping) @ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping }) tonemap!: ToneMapping; diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 271987c8f7..044ca764e7 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1335,6 +1335,51 @@ describe(MediaService.name, () => { ); }); + it('should use hardware decoding for nvenc if enabled', async () => { + mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); + systemMock.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHWAccel.NVENC, accelDecode: true }, + }); + 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: expect.arrayContaining([ + '-hwaccel cuda', + '-hwaccel_output_format cuda', + '-noautorotate', + '-threads 1', + ]), + outputOptions: expect.arrayContaining([expect.stringContaining('scale_cuda=-2:720:format=nv12')]), + twoPass: false, + }, + ); + }); + + it('should use hardware tone-mapping for nvenc if hardware decoding is enabled and should tone map', async () => { + mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); + systemMock.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHWAccel.NVENC, accelDecode: true }, + }); + 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: expect.arrayContaining(['-hwaccel cuda', '-hwaccel_output_format cuda']), + outputOptions: expect.arrayContaining([ + expect.stringContaining( + 'tonemap_cuda=desat=0:matrix=bt709:primaries=bt709:range=pc:tonemap=hable:transfer=bt709:format=nv12', + ), + ]), + twoPass: false, + }, + ); + }); + it('should set options for qsv', async () => { storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); @@ -1633,8 +1678,9 @@ describe(MediaService.name, () => { it('should set options for rkmpp', async () => { storageMock.readdir.mockResolvedValue(['renderD128']); + storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true }); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); - systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP } }); + systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mediaMock.transcode).toHaveBeenCalledWith( @@ -1663,10 +1709,12 @@ describe(MediaService.name, () => { it('should set vbr options for rkmpp when max bitrate is enabled', async () => { storageMock.readdir.mockResolvedValue(['renderD128']); + storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true }); mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, + accelDecode: true, maxBitrate: '10000k', targetVideoCodec: VideoCodec.HEVC, }, @@ -1686,9 +1734,10 @@ describe(MediaService.name, () => { it('should set cqp options for rkmpp when max bitrate is disabled', async () => { storageMock.readdir.mockResolvedValue(['renderD128']); + storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true }); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.RKMPP, crf: 30, maxBitrate: '0' }, + ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' }, }); assetMock.getByIds.mockResolvedValue([assetStub.video]); await sut.handleVideoConversion({ id: assetStub.video.id }); @@ -1707,7 +1756,9 @@ describe(MediaService.name, () => { storageMock.readdir.mockResolvedValue(['renderD128']); storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true }); mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); - systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, crf: 30, maxBitrate: '0' } }); + systemMock.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' }, + }); assetMock.getByIds.mockResolvedValue([assetStub.video]); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mediaMock.transcode).toHaveBeenCalledWith( @@ -1724,6 +1775,54 @@ describe(MediaService.name, () => { }, ); }); + + it('should use software decoding and tone-mapping if hardware decoding is disabled', async () => { + storageMock.readdir.mockResolvedValue(['renderD128']); + storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true }); + mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); + systemMock.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: false, crf: 30, maxBitrate: '0' }, + }); + 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: [], + outputOptions: expect.arrayContaining([ + expect.stringContaining( + 'zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p', + ), + ]), + twoPass: false, + }, + ); + }); + + it('should use software decoding and tone-mapping if opencl is not available', async () => { + storageMock.readdir.mockResolvedValue(['renderD128']); + storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => false, isCharacterDevice: () => false }); + mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); + systemMock.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' }, + }); + 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: [], + outputOptions: expect.arrayContaining([ + expect.stringContaining( + 'zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p', + ), + ]), + twoPass: false, + }, + ); + }); }); it('should tonemap when policy is required and video is hdr', async () => { diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 86fe8252ad..dc252b4c1c 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -36,9 +36,11 @@ import { AV1Config, H264Config, HEVCConfig, - NVENCConfig, + NvencHwDecodeConfig, + NvencSwDecodeConfig, QSVConfig, - RKMPPConfig, + RkmppHwDecodeConfig, + RkmppSwDecodeConfig, ThumbnailConfig, VAAPIConfig, VP9Config, @@ -360,8 +362,7 @@ export class MediaService { `Error occurred during transcoding. Retrying with ${config.accel.toUpperCase()} acceleration disabled.`, ); } - config.accel = TranscodeHWAccel.DISABLED; - transcodeOptions = await this.getCodecConfig(config).then((c) => + transcodeOptions = await this.getCodecConfig({ ...config, accel: TranscodeHWAccel.DISABLED }).then((c) => c.getOptions(target, mainVideoStream, mainAudioStream), ); await this.mediaRepository.transcode(input, output, transcodeOptions); @@ -494,7 +495,7 @@ export class MediaService { let handler: VideoCodecHWConfig; switch (config.accel) { case TranscodeHWAccel.NVENC: { - handler = new NVENCConfig(config); + handler = config.accelDecode ? new NvencHwDecodeConfig(config) : new NvencSwDecodeConfig(config); break; } case TranscodeHWAccel.QSV: { @@ -506,7 +507,10 @@ export class MediaService { break; } case TranscodeHWAccel.RKMPP: { - handler = new RKMPPConfig(config, await this.getDevices(), await this.hasOpenCL()); + handler = + config.accelDecode && (await this.hasOpenCL()) + ? new RkmppHwDecodeConfig(config, await this.getDevices()) + : new RkmppSwDecodeConfig(config, await this.getDevices()); break; } default: { diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 61ba8df379..d549267925 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -66,6 +66,7 @@ const updatedConfig = Object.freeze({ preferredHwDevice: 'auto', transcode: TranscodePolicy.REQUIRED, accel: TranscodeHWAccel.DISABLED, + accelDecode: false, tonemap: ToneMapping.HABLE, }, logging: { diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index f43b78464e..6c51c0d303 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -26,14 +26,18 @@ class BaseConfig implements VideoCodecSWConfig { } } - options.outputOptions.push(...this.getPresetOptions(), ...this.getThreadOptions(), ...this.getBitrateOptions()); + options.outputOptions.push( + ...this.getPresetOptions(), + ...this.getOutputThreadOptions(), + ...this.getBitrateOptions(), + ); return options; } // eslint-disable-next-line @typescript-eslint/no-unused-vars getBaseInputOptions(videoStream: VideoStreamInfo): string[] { - return []; + return this.getInputThreadOptions(); } getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { @@ -80,11 +84,7 @@ class BaseConfig implements VideoCodecSWConfig { options.push(`scale=${this.getScaling(videoStream)}`); } - if (this.shouldToneMap(videoStream)) { - options.push(...this.getToneMapping()); - } - options.push('format=yuv420p'); - + options.push(...this.getToneMapping(videoStream), 'format=yuv420p'); return options; } @@ -112,7 +112,11 @@ class BaseConfig implements VideoCodecSWConfig { } } - getThreadOptions(): Array { + getInputThreadOptions(): Array { + return []; + } + + getOutputThreadOptions(): Array { if (this.config.threads <= 0) { return []; } @@ -218,7 +222,11 @@ class BaseConfig implements VideoCodecSWConfig { } } - getToneMapping() { + getToneMapping(videoStream: VideoStreamInfo) { + if (!this.shouldToneMap(videoStream)) { + return []; + } + const colors = this.getColors(); return [ @@ -348,8 +356,8 @@ export class ThumbnailConfig extends BaseConfig { } export class H264Config extends BaseConfig { - getThreadOptions() { - const options = super.getThreadOptions(); + getOutputThreadOptions() { + const options = super.getOutputThreadOptions(); if (this.config.threads === 1) { options.push('-x264-params frame-threads=1:pools=none'); } @@ -359,8 +367,8 @@ export class H264Config extends BaseConfig { } export class HEVCConfig extends BaseConfig { - getThreadOptions() { - const options = super.getThreadOptions(); + getOutputThreadOptions() { + const options = super.getOutputThreadOptions(); if (this.config.threads === 1) { options.push('-x265-params frame-threads=1:pools=none'); } @@ -391,8 +399,8 @@ export class VP9Config extends BaseConfig { return [`-${this.useCQP() ? 'q:v' : 'crf'} ${this.config.crf}`, `-b:v ${bitrates.max}${bitrates.unit}`]; } - getThreadOptions() { - return ['-row-mt 1', ...super.getThreadOptions()]; + getOutputThreadOptions() { + return ['-row-mt 1', ...super.getOutputThreadOptions()]; } eligibleForTwoPass() { @@ -425,7 +433,7 @@ export class AV1Config extends BaseConfig { return options; } - getThreadOptions() { + getOutputThreadOptions() { return []; // Already set above with svtav1-params } @@ -434,7 +442,7 @@ export class AV1Config extends BaseConfig { } } -export class NVENCConfig extends BaseHWConfig { +export class NvencSwDecodeConfig extends BaseHWConfig { getSupportedCodecs() { return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.AV1]; } @@ -462,7 +470,7 @@ export class NVENCConfig extends BaseHWConfig { } getFilterOptions(videoStream: VideoStreamInfo) { - const options = this.shouldToneMap(videoStream) ? this.getToneMapping() : []; + const options = this.getToneMapping(videoStream); options.push('format=nv12', 'hwupload_cuda'); if (this.shouldScale(videoStream)) { options.push(`scale_cuda=${this.getScaling(videoStream)}`); @@ -513,6 +521,52 @@ export class NVENCConfig extends BaseHWConfig { } } +export class NvencHwDecodeConfig extends NvencSwDecodeConfig { + getBaseInputOptions() { + return ['-hwaccel cuda', '-hwaccel_output_format cuda', '-noautorotate', ...this.getInputThreadOptions()]; + } + + getFilterOptions(videoStream: VideoStreamInfo) { + const options = []; + if (this.shouldScale(videoStream)) { + options.push(`scale_cuda=${this.getScaling(videoStream)}`); + } + options.push(...this.getToneMapping(videoStream)); + if (options.length > 0) { + options[options.length - 1] += ':format=nv12'; + } else { + options.push('format=nv12'); + } + return options; + } + + getToneMapping(videoStream: VideoStreamInfo) { + if (!this.shouldToneMap(videoStream)) { + return []; + } + + const colors = this.getColors(); + const tonemapOptions = [ + 'desat=0', + `matrix=${colors.matrix}`, + `primaries=${colors.primaries}`, + 'range=pc', + `tonemap=${this.config.tonemap}`, + `transfer=${colors.transfer}`, + ]; + + return [`tonemap_cuda=${tonemapOptions.join(':')}`]; + } + + getInputThreadOptions() { + return [`-threads ${this.config.threads <= 0 ? 1 : this.config.threads}`]; + } + + getOutputThreadOptions() { + return []; + } +} + export class QSVConfig extends BaseHWConfig { getBaseInputOptions() { if (this.devices.length === 0) { @@ -538,7 +592,7 @@ export class QSVConfig extends BaseHWConfig { } getFilterOptions(videoStream: VideoStreamInfo) { - const options = this.shouldToneMap(videoStream) ? this.getToneMapping() : []; + const options = this.getToneMapping(videoStream); options.push('format=nv12', 'hwupload=extra_hw_frames=64'); if (this.shouldScale(videoStream)) { options.push(`scale_qsv=${this.getScaling(videoStream)}`); @@ -604,7 +658,7 @@ export class VAAPIConfig extends BaseHWConfig { } getFilterOptions(videoStream: VideoStreamInfo) { - const options = this.shouldToneMap(videoStream) ? this.getToneMapping() : []; + const options = this.getToneMapping(videoStream); options.push('format=nv12', 'hwupload'); if (this.shouldScale(videoStream)) { options.push(`scale_vaapi=${this.getScaling(videoStream)}`); @@ -656,47 +710,22 @@ export class VAAPIConfig extends BaseHWConfig { } } -export class RKMPPConfig extends BaseHWConfig { - private hasOpenCL: boolean; - +export class RkmppSwDecodeConfig extends BaseHWConfig { constructor( protected config: SystemConfigFFmpegDto, devices: string[] = [], - hasOpenCL: boolean = false, ) { super(config, devices); - this.hasOpenCL = hasOpenCL; } eligibleForTwoPass(): boolean { return false; } - getBaseInputOptions(videoStream: VideoStreamInfo) { + getBaseInputOptions(): string[] { if (this.devices.length === 0) { throw new Error('No RKMPP device found'); } - return this.shouldToneMap(videoStream) && !this.hasOpenCL - ? [] // disable hardware decoding & filters - : ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']; - } - - getFilterOptions(videoStream: VideoStreamInfo) { - if (this.shouldToneMap(videoStream)) { - if (!this.hasOpenCL) { - return super.getFilterOptions(videoStream); - } - const colors = this.getColors(); - return [ - `scale_rkrga=${this.getScaling(videoStream)}:format=p010:afbc=1`, - 'hwmap=derive_device=opencl:mode=read', - `tonemap_opencl=format=nv12:r=pc:p=${colors.primaries}:t=${colors.transfer}:m=${colors.matrix}:tonemap=${this.config.tonemap}:desat=0`, - 'hwmap=derive_device=rkmpp:mode=write:reverse=1', - 'format=drm_prime', - ]; - } else if (this.shouldScale(videoStream)) { - return [`scale_rkrga=${this.getScaling(videoStream)}:format=nv12:afbc=1`]; - } return []; } @@ -734,3 +763,29 @@ export class RKMPPConfig extends BaseHWConfig { return `${this.config.targetVideoCodec}_rkmpp`; } } + +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']; + } + + getFilterOptions(videoStream: VideoStreamInfo) { + if (this.shouldToneMap(videoStream)) { + const colors = this.getColors(); + return [ + `scale_rkrga=${this.getScaling(videoStream)}:format=p010:afbc=1`, + 'hwmap=derive_device=opencl:mode=read', + `tonemap_opencl=format=nv12:r=pc:p=${colors.primaries}:t=${colors.transfer}:m=${colors.matrix}:tonemap=${this.config.tonemap}:desat=0`, + 'hwmap=derive_device=rkmpp:mode=write:reverse=1', + 'format=drm_prime', + ]; + } else if (this.shouldScale(videoStream)) { + return [`scale_rkrga=${this.getScaling(videoStream)}:format=nv12:afbc=1`]; + } + return []; + } +} 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 bc91c2c993..e194b48ab9 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 @@ -276,6 +276,15 @@ isEdited={config.ffmpeg.accel !== savedConfig.ffmpeg.accel} /> + + +