From b9096f3e998bab2241b0ac9d817dc7c1d9d59d2a Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:48:23 -0400 Subject: [PATCH] feat(server): use tonemapx for software tone-mapping (#13785) --- docs/docs/install/config-file.md | 1 - i18n/en.json | 2 - .../lib/model/system_config_f_fmpeg_dto.dart | Bin 8990 -> 8786 bytes open-api/immich-openapi-specs.json | 5 - open-api/typescript-sdk/src/fetch-client.ts | 1 - server/src/config.ts | 2 - server/src/dtos/system-config.dto.ts | 6 - server/src/interfaces/media.interface.ts | 1 + ...1730227312171-RemoveNplFromSystemConfig.ts | 12 ++ server/src/repositories/media.repository.ts | 1 + server/src/services/media.service.spec.ts | 110 +++++++++--- server/src/services/media.service.ts | 2 +- .../services/system-config.service.spec.ts | 1 - server/src/utils/media.ts | 156 +++++++----------- server/test/fixtures/media.stub.ts | 25 +++ .../settings/ffmpeg/ffmpeg-settings.svelte | 9 - 16 files changed, 181 insertions(+), 153 deletions(-) create mode 100644 server/src/migrations/1730227312171-RemoveNplFromSystemConfig.ts diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index ed902f39cf..24d747e93a 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -26,7 +26,6 @@ The default configuration looks like this: "bframes": -1, "refs": 0, "gopSize": 0, - "npl": 0, "temporalAQ": false, "cqMode": "auto", "twoPass": false, diff --git a/i18n/en.json b/i18n/en.json index d607e088b3..72e3e1e1bf 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -305,8 +305,6 @@ "transcoding_threads_description": "Higher values lead to faster encoding, but leave less room for the server to process other tasks while active. This value should not be more than the number of CPU cores. Maximizes utilization if set to 0.", "transcoding_tone_mapping": "Tone-mapping", "transcoding_tone_mapping_description": "Attempts to preserve the appearance of HDR videos when converted to SDR. Each algorithm makes different tradeoffs for color, detail and brightness. Hable preserves detail, Mobius preserves color, and Reinhard preserves brightness.", - "transcoding_tone_mapping_npl": "Tone-mapping NPL", - "transcoding_tone_mapping_npl_description": "Colors will be adjusted to look normal for a display of this brightness. Counter-intuitively, lower values increase the brightness of the video and vice versa since it compensates for the brightness of the display. 0 sets this value automatically.", "transcoding_transcode_policy": "Transcode policy", "transcoding_transcode_policy_description": "Policy for when a video should be transcoded. HDR videos will always be transcoded (except if transcoding is disabled).", "transcoding_two_pass_encoding": "Two-pass encoding", 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 73f7d35aecc308b99104f1563f4073b3d8bda355..0acfc9e8fbf9d87e825ba9756ede153ef2213236 100644 GIT binary patch delta 46 zcmV+}0MY-RM$$yE*aEZf0=@#X&;@=2vt|hl1G9$=XackO4mJa`P7*5yv#J=I36oGF E&c*E!_5c6? delta 127 zcmccQGS6+p4JPKif}F_*m_;_?tpbS8#wo=N zR&B^5x>=bgm5Bo=YO4a0YU8tD763BTV-;)_N-{Ew^+2NQ1+Cb?8ZJu8fla(4zJe1h IP@{Ma0OH~+a{vGU diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index ef488fe5c0..8465b6bb40 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -11621,10 +11621,6 @@ "maxBitrate": { "type": "string" }, - "npl": { - "minimum": 0, - "type": "integer" - }, "preferredHwDevice": { "type": "string" }, @@ -11673,7 +11669,6 @@ "crf", "gopSize", "maxBitrate", - "npl", "preferredHwDevice", "preset", "refs", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 2318155473..6a66906f31 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1104,7 +1104,6 @@ export type SystemConfigFFmpegDto = { crf: number; gopSize: number; maxBitrate: string; - npl: number; preferredHwDevice: string; preset: string; refs: number; diff --git a/server/src/config.ts b/server/src/config.ts index 7a7a7b71ac..2b74f00e7a 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -36,7 +36,6 @@ export interface SystemConfig { bframes: number; refs: number; gopSize: number; - npl: number; temporalAQ: boolean; cqMode: CQMode; twoPass: boolean; @@ -178,7 +177,6 @@ export const defaults = Object.freeze({ bframes: -1, refs: 0, gopSize: 0, - npl: 0, temporalAQ: false, cqMode: CQMode.AUTO, twoPass: false, diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 7e7a8e0879..ec1e272ab3 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -134,12 +134,6 @@ export class SystemConfigFFmpegDto { @ApiProperty({ type: 'integer' }) gopSize!: number; - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - npl!: number; - @ValidateBoolean() temporalAQ!: boolean; diff --git a/server/src/interfaces/media.interface.ts b/server/src/interfaces/media.interface.ts index 2bc8ccde36..d8d7395ea7 100644 --- a/server/src/interfaces/media.interface.ts +++ b/server/src/interfaces/media.interface.ts @@ -59,6 +59,7 @@ export interface VideoStreamInfo { frameCount: number; isHDR: boolean; bitrate: number; + pixelFormat: string; } export interface AudioStreamInfo { diff --git a/server/src/migrations/1730227312171-RemoveNplFromSystemConfig.ts b/server/src/migrations/1730227312171-RemoveNplFromSystemConfig.ts new file mode 100644 index 0000000000..2c929191dd --- /dev/null +++ b/server/src/migrations/1730227312171-RemoveNplFromSystemConfig.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveNplFromSystemConfig1730227312171 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + update system_metadata + set value = value #- '{ffmpeg,npl}' + where key = 'system-config' and value->'ffmpeg'->'npl' is not null`); + } + + public async down(): Promise {} +} diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index d76d226f44..f38e150c55 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -126,6 +126,7 @@ export class MediaRepository implements IMediaRepository { rotation: this.parseInt(stream.rotation), isHDR: stream.color_transfer === 'smpte2084' || stream.color_transfer === 'arib-std-b67', bitrate: this.parseInt(stream.bit_rate), + pixelFormat: stream.pix_fmt || 'yuv420p', })), audioStreams: results.streams .filter((stream) => stream.codec_type === 'audio') diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 65166f4293..df1a04dff8 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -9,7 +9,6 @@ import { AudioCodec, Colorspace, ImageFormat, - ToneMapping, TranscodeHWAccel, TranscodePolicy, VideoCodec, @@ -410,7 +409,7 @@ describe(MediaService.name, () => { '-frames:v 1', '-update 1', '-v verbose', - String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,scale=-2:1440:flags=lanczos+accurate_rnd+full_chroma_int:out_color_matrix=bt601:out_range=pc,format=yuv420p`, + String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,scale=-2:1440:flags=lanczos+accurate_rnd+full_chroma_int:out_range=pc`, ], twoPass: false, }), @@ -445,7 +444,7 @@ describe(MediaService.name, () => { '-frames:v 1', '-update 1', '-v verbose', - String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=709:t=601:m=470bg:range=pc,format=yuv420p`, + String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p`, ], twoPass: false, }), @@ -482,7 +481,7 @@ describe(MediaService.name, () => { '-frames:v 1', '-update 1', '-v verbose', - String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=709:t=601:m=470bg:range=pc,format=yuv420p`, + String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p`, ], twoPass: false, }), @@ -1328,7 +1327,7 @@ describe(MediaService.name, () => { '-map 0:0', '-map 0:1', '-v verbose', - '-vf scale=-2:720,format=yuv420p', + '-vf scale=-2:720', '-preset 12', '-crf 23', ]), @@ -1454,7 +1453,7 @@ describe(MediaService.name, () => { '-map 0:1', '-g 256', '-v verbose', - '-vf format=nv12,hwupload_cuda,scale_cuda=-2:720', + '-vf hwupload_cuda,scale_cuda=-2:720:format=nv12', '-preset p1', '-cq:v 23', ]), @@ -1586,7 +1585,7 @@ describe(MediaService.name, () => { 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', + 'tonemap_cuda=desat=0:matrix=bt709:primaries=bt709:range=pc:tonemap=hable:tonemap_mode=lum:transfer=bt709:peak=100:format=nv12', ), ]), twoPass: false, @@ -1594,6 +1593,24 @@ describe(MediaService.name, () => { ); }); + it('should set format to nv12 for nvenc if input is not yuv420p', async () => { + mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit); + 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', + expect.objectContaining({ + inputOptions: expect.arrayContaining(['-hwaccel cuda', '-hwaccel_output_format cuda']), + outputOptions: expect.arrayContaining([expect.stringContaining('format=nv12')]), + twoPass: false, + }), + ); + }); + it('should set options for qsv', async () => { storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); @@ -1616,7 +1633,7 @@ describe(MediaService.name, () => { '-refs 5', '-g 256', '-v verbose', - '-vf format=nv12,hwupload=extra_hw_frames=64,scale_qsv=-1:720:mode=hq', + '-vf hwupload=extra_hw_frames=64,scale_qsv=-1:720:mode=hq:format=nv12', '-preset 7', '-global_quality:v 23', '-maxrate 10000k', @@ -1748,7 +1765,7 @@ describe(MediaService.name, () => { ]), outputOptions: expect.arrayContaining([ expect.stringContaining( - 'hwmap=derive_device=opencl,tonemap_opencl=desat=0:format=nv12:matrix=bt709:primaries=bt709:range=pc:tonemap=hable:transfer=bt709,hwmap=derive_device=qsv:reverse=1,format=qsv', + 'hwmap=derive_device=opencl,tonemap_opencl=desat=0:format=nv12:matrix=bt709:primaries=bt709:transfer=bt709:range=pc:tonemap=hable:tonemap_mode=lum:peak=100,hwmap=derive_device=qsv:reverse=1,format=qsv', ), ]), twoPass: false, @@ -1776,6 +1793,32 @@ describe(MediaService.name, () => { ); }); + it('should set format to nv12 for qsv if input is not yuv420p', async () => { + storageMock.readdir.mockResolvedValue(['renderD128']); + mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit); + systemMock.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHWAccel.QSV, 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', + expect.objectContaining({ + inputOptions: expect.arrayContaining([ + '-hwaccel qsv', + '-hwaccel_output_format qsv', + '-async_depth 4', + '-threads 1', + ]), + outputOptions: expect.arrayContaining([expect.stringContaining('format=nv12')]), + twoPass: false, + }), + ); + }); + it('should set options for vaapi', async () => { storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); @@ -1799,7 +1842,7 @@ describe(MediaService.name, () => { '-map 0:1', '-g 256', '-v verbose', - '-vf format=nv12,hwupload,scale_vaapi=-2:720:mode=hq:out_range=pc', + '-vf hwupload=extra_hw_frames=64,scale_vaapi=-2:720:mode=hq:out_range=pc:format=nv12', '-compression_level 7', '-rc_mode 1', ]), @@ -1970,7 +2013,7 @@ describe(MediaService.name, () => { ); }); - it('should use hardware tone-mapping for qsv if hardware decoding is enabled and should tone map', async () => { + it('should use hardware tone-mapping for vaapi if hardware decoding is enabled and should tone map', async () => { storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); systemMock.get.mockResolvedValue({ @@ -1987,7 +2030,7 @@ describe(MediaService.name, () => { inputOptions: expect.arrayContaining(['-hwaccel vaapi', '-hwaccel_output_format vaapi', '-threads 1']), outputOptions: expect.arrayContaining([ expect.stringContaining( - 'hwmap=derive_device=opencl,tonemap_opencl=desat=0:format=nv12:matrix=bt709:primaries=bt709:range=pc:tonemap=hable:transfer=bt709,hwmap=derive_device=vaapi:reverse=1,format=vaapi', + 'hwmap=derive_device=opencl,tonemap_opencl=desat=0:format=nv12:matrix=bt709:primaries=bt709:transfer=bt709:range=pc:tonemap=hable:tonemap_mode=lum:peak=100,hwmap=derive_device=vaapi:reverse=1,format=vaapi', ), ]), twoPass: false, @@ -1995,6 +2038,27 @@ describe(MediaService.name, () => { ); }); + it('should set format to nv12 for vaapi if input is not yuv420p', async () => { + storageMock.readdir.mockResolvedValue(['renderD128']); + mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit); + systemMock.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHWAccel.VAAPI, 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', + expect.objectContaining({ + inputOptions: expect.arrayContaining(['-hwaccel vaapi', '-hwaccel_output_format vaapi', '-threads 1']), + outputOptions: expect.arrayContaining([expect.stringContaining('format=nv12')]), + twoPass: false, + }), + ); + }); + it('should use preferred device for vaapi when hardware decoding', async () => { storageMock.readdir.mockResolvedValue(['renderD128', 'renderD129', 'renderD130']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); @@ -2140,7 +2204,7 @@ describe(MediaService.name, () => { inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']), outputOptions: expect.arrayContaining([ expect.stringContaining( - 'scale_rkrga=-2:720:format=p010:afbc=1,hwmap=derive_device=opencl:mode=read,tonemap_opencl=format=nv12:r=pc:p=bt709:t=bt709:m=bt709:tonemap=hable:desat=0,hwmap=derive_device=rkmpp:mode=write:reverse=1,format=drm_prime', + 'scale_rkrga=-2:720:format=p010:afbc=1,hwmap=derive_device=opencl:mode=read,tonemap_opencl=format=nv12:r=pc:p=bt709:t=bt709:m=bt709:tonemap=hable:desat=0:tonemap_mode=lum:peak=100,hwmap=derive_device=rkmpp:mode=write:reverse=1,format=drm_prime', ), ]), twoPass: false, @@ -2164,7 +2228,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: expect.arrayContaining([ expect.stringContaining( - 'zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=709:t=709:m=709:range=pc,format=yuv420p', + 'tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p', ), ]), twoPass: false, @@ -2188,7 +2252,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: expect.arrayContaining([ expect.stringContaining( - 'zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=709:t=709:m=709:range=pc,format=yuv420p', + 'tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p', ), ]), twoPass: false, @@ -2209,7 +2273,7 @@ describe(MediaService.name, () => { outputOptions: expect.arrayContaining([ '-c:v h264', '-c:a copy', - '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=709:t=709:m=709:range=pc,format=yuv420p', + '-vf tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p', ]), twoPass: false, }), @@ -2229,16 +2293,16 @@ describe(MediaService.name, () => { outputOptions: expect.arrayContaining([ '-c:v h264', '-c:a copy', - '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=709:t=709:m=709:range=pc,format=yuv420p', + '-vf tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p', ]), twoPass: false, }), ); }); - it('should set npl to 250 for reinhard and mobius tone-mapping algorithms', async () => { - mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); - systemMock.get.mockResolvedValue({ ffmpeg: { tonemap: ToneMapping.MOBIUS } }); + it('should transcode when policy is required and video is not yuv420p', async () => { + mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit); + systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mediaMock.transcode).toHaveBeenCalledWith( @@ -2246,11 +2310,7 @@ describe(MediaService.name, () => { 'upload/encoded-video/user-id/as/se/asset-id.mp4', expect.objectContaining({ inputOptions: expect.any(Array), - outputOptions: expect.arrayContaining([ - '-c:v h264', - '-c:a copy', - '-vf zscale=t=linear:npl=250,tonemap=mobius:desat=0,zscale=p=709:t=709:m=709:range=pc,format=yuv420p', - ]), + outputOptions: expect.arrayContaining(['-c:v h264', '-c:a copy', '-vf format=yuv420p']), twoPass: false, }), ); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index cce1324da9..9058d08ff6 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -413,7 +413,7 @@ export class MediaService extends BaseService { const isLargerThanTargetBitrate = stream.bitrate > this.parseBitrateToBps(ffmpegConfig.maxBitrate); const isTargetVideoCodec = ffmpegConfig.acceptedVideoCodecs.includes(stream.codecName as VideoCodec); - const isRequired = !isTargetVideoCodec || stream.isHDR; + const isRequired = !isTargetVideoCodec || !stream.pixelFormat.endsWith('420p'); switch (ffmpegConfig.transcode) { case TranscodePolicy.DISABLED: { diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 2ad7c78ca2..26284d52b5 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -65,7 +65,6 @@ const updatedConfig = Object.freeze({ bframes: -1, refs: 0, gopSize: 0, - npl: 0, temporalAQ: false, cqMode: CQMode.AUTO, twoPass: false, diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index 03d57296d8..f61b472b75 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -149,7 +149,11 @@ export class BaseConfig implements VideoCodecSWConfig { options.push(`scale=${this.getScaling(videoStream)}`); } - options.push(...this.getToneMapping(videoStream), 'format=yuv420p'); + options.push(...this.getToneMapping(videoStream)); + if (options.length === 0 && !videoStream.pixelFormat.endsWith('420p')) { + options.push(`format=yuv420p`); + } + return options; } @@ -271,33 +275,20 @@ export class BaseConfig implements VideoCodecSWConfig { getColors() { return { - primaries: '709', - transfer: '709', - matrix: '709', + primaries: 'bt709', + transfer: 'bt709', + matrix: 'bt709', }; } - getNPL() { - if (this.config.npl <= 0) { - // since hable already outputs a darker image, we use a lower npl value for it - return this.config.tonemap === ToneMapping.HABLE ? 100 : 250; - } else { - return this.config.npl; - } - } - getToneMapping(videoStream: VideoStreamInfo) { if (!this.shouldToneMap(videoStream)) { return []; } - const colors = this.getColors(); - - return [ - `zscale=t=linear:npl=${this.getNPL()}`, - `tonemap=${this.config.tonemap}:desat=0`, - `zscale=p=${colors.primaries}:t=${colors.transfer}:m=${colors.matrix}:range=pc`, - ]; + const { primaries, transfer, matrix } = this.getColors(); + const options = `tonemapx=tonemap=${this.config.tonemap}:desat=0:p=${primaries}:t=${transfer}:m=${matrix}:r=pc:peak=100:format=yuv420p`; + return [options]; } getAudioCodec(): string { @@ -395,19 +386,14 @@ export class ThumbnailConfig extends BaseConfig { } getFilterOptions(videoStream: VideoStreamInfo): string[] { - const options = [ + return [ 'fps=12:eof_action=pass:round=down', 'thumbnail=12', String.raw`select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20)`, 'trim=end_frame=2', 'reverse', + ...super.getFilterOptions(videoStream), ]; - if (this.shouldScale(videoStream)) { - options.push(`scale=${this.getScaling(videoStream)}`); - } - - options.push(...this.getToneMapping(videoStream), 'format=yuv420p'); - return options; } getPresetOptions() { @@ -423,19 +409,7 @@ export class ThumbnailConfig extends BaseConfig { } getScaling(videoStream: VideoStreamInfo) { - let options = super.getScaling(videoStream) + ':flags=lanczos+accurate_rnd+full_chroma_int'; - if (!this.shouldToneMap(videoStream)) { - options += ':out_color_matrix=bt601:out_range=pc'; - } - return options; - } - - getColors() { - return { - primaries: '709', - transfer: '601', - matrix: '470bg', - }; + return super.getScaling(videoStream) + ':flags=lanczos+accurate_rnd+full_chroma_int:out_range=pc'; } } @@ -559,9 +533,9 @@ export class NvencSwDecodeConfig extends BaseHWConfig { getFilterOptions(videoStream: VideoStreamInfo) { const options = this.getToneMapping(videoStream); - options.push('format=nv12', 'hwupload_cuda'); + options.push('hwupload_cuda'); if (this.shouldScale(videoStream)) { - options.push(`scale_cuda=${this.getScaling(videoStream)}`); + options.push(`scale_cuda=${this.getScaling(videoStream)}:format=nv12`); } return options; @@ -622,6 +596,8 @@ export class NvencHwDecodeConfig extends NvencSwDecodeConfig { options.push(...this.getToneMapping(videoStream)); if (options.length > 0) { options[options.length - 1] += ':format=nv12'; + } else if (!videoStream.pixelFormat.endsWith('420p')) { + options.push('format=nv12'); } return options; } @@ -631,14 +607,16 @@ export class NvencHwDecodeConfig extends NvencSwDecodeConfig { return []; } - const colors = this.getColors(); + const { matrix, primaries, transfer } = this.getColors(); const tonemapOptions = [ 'desat=0', - `matrix=${colors.matrix}`, - `primaries=${colors.primaries}`, + `matrix=${matrix}`, + `primaries=${primaries}`, 'range=pc', `tonemap=${this.config.tonemap}`, - `transfer=${colors.transfer}`, + 'tonemap_mode=lum', + `transfer=${transfer}`, + 'peak=100', ]; return [`tonemap_cuda=${tonemapOptions.join(':')}`]; @@ -651,14 +629,6 @@ export class NvencHwDecodeConfig extends NvencSwDecodeConfig { getOutputThreadOptions() { return []; } - - getColors() { - return { - primaries: 'bt709', - transfer: 'bt709', - matrix: 'bt709', - }; - } } export class QsvSwDecodeConfig extends BaseHWConfig { @@ -687,9 +657,9 @@ export class QsvSwDecodeConfig extends BaseHWConfig { getFilterOptions(videoStream: VideoStreamInfo) { const options = this.getToneMapping(videoStream); - options.push('format=nv12', 'hwupload=extra_hw_frames=64'); + options.push('hwupload=extra_hw_frames=64'); if (this.shouldScale(videoStream)) { - options.push(`scale_qsv=${this.getScaling(videoStream)}:mode=hq`); + options.push(`scale_qsv=${this.getScaling(videoStream)}:mode=hq:format=nv12`); } return options; } @@ -764,15 +734,18 @@ export class QsvHwDecodeConfig extends QsvSwDecodeConfig { getFilterOptions(videoStream: VideoStreamInfo) { const options = []; - if (this.shouldScale(videoStream) || !this.shouldToneMap(videoStream)) { + const tonemapOptions = this.getToneMapping(videoStream); + if (this.shouldScale(videoStream) || tonemapOptions.length === 0) { let scaling = `scale_qsv=${this.getScaling(videoStream)}:async_depth=4:mode=hq`; - if (!this.shouldToneMap(videoStream)) { + if (tonemapOptions.length === 0) { scaling += ':format=nv12'; } options.push(scaling); } - - options.push(...this.getToneMapping(videoStream)); + options.push(...tonemapOptions); + if (options.length === 0 && !videoStream.pixelFormat.endsWith('420p')) { + options.push('format=nv12'); + } return options; } @@ -781,15 +754,17 @@ export class QsvHwDecodeConfig extends QsvSwDecodeConfig { return []; } - const colors = this.getColors(); + const { matrix, primaries, transfer } = this.getColors(); const tonemapOptions = [ 'desat=0', 'format=nv12', - `matrix=${colors.matrix}`, - `primaries=${colors.primaries}`, + `matrix=${matrix}`, + `primaries=${primaries}`, + `transfer=${transfer}`, 'range=pc', `tonemap=${this.config.tonemap}`, - `transfer=${colors.transfer}`, + 'tonemap_mode=lum', + 'peak=100', ]; return [ @@ -802,14 +777,6 @@ export class QsvHwDecodeConfig extends QsvSwDecodeConfig { getInputThreadOptions() { return [`-threads 1`]; } - - getColors() { - return { - primaries: 'bt709', - transfer: 'bt709', - matrix: 'bt709', - }; - } } export class VaapiSwDecodeConfig extends BaseHWConfig { @@ -828,9 +795,9 @@ export class VaapiSwDecodeConfig extends BaseHWConfig { getFilterOptions(videoStream: VideoStreamInfo) { const options = this.getToneMapping(videoStream); - options.push('format=nv12', 'hwupload'); + options.push('hwupload=extra_hw_frames=64'); if (this.shouldScale(videoStream)) { - options.push(`scale_vaapi=${this.getScaling(videoStream)}:mode=hq:out_range=pc`); + options.push(`scale_vaapi=${this.getScaling(videoStream)}:mode=hq:out_range=pc:format=nv12`); } return options; @@ -901,15 +868,18 @@ export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig { getFilterOptions(videoStream: VideoStreamInfo) { const options = []; - if (this.shouldScale(videoStream) || !this.shouldToneMap(videoStream)) { + const tonemapOptions = this.getToneMapping(videoStream); + if (this.shouldScale(videoStream) || tonemapOptions.length === 0) { let scaling = `scale_vaapi=${this.getScaling(videoStream)}:mode=hq:out_range=pc`; - if (!this.shouldToneMap(videoStream)) { + if (tonemapOptions.length === 0) { scaling += ':format=nv12'; } options.push(scaling); } - - options.push(...this.getToneMapping(videoStream)); + options.push(...tonemapOptions); + if (options.length === 0 && !videoStream.pixelFormat.endsWith('420p')) { + options.push('format=nv12'); + } return options; } @@ -918,15 +888,17 @@ export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig { return []; } - const colors = this.getColors(); + const { matrix, primaries, transfer } = this.getColors(); const tonemapOptions = [ 'desat=0', 'format=nv12', - `matrix=${colors.matrix}`, - `primaries=${colors.primaries}`, + `matrix=${matrix}`, + `primaries=${primaries}`, + `transfer=${transfer}`, 'range=pc', `tonemap=${this.config.tonemap}`, - `transfer=${colors.transfer}`, + 'tonemap_mode=lum', + 'peak=100', ]; return [ @@ -939,14 +911,6 @@ export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig { getInputThreadOptions() { return [`-threads 1`]; } - - getColors() { - return { - primaries: 'bt709', - transfer: 'bt709', - matrix: 'bt709', - }; - } } export class RkmppSwDecodeConfig extends BaseHWConfig { @@ -1014,11 +978,11 @@ export class RkmppHwDecodeConfig extends RkmppSwDecodeConfig { getFilterOptions(videoStream: VideoStreamInfo) { if (this.shouldToneMap(videoStream)) { - const colors = this.getColors(); + const { primaries, transfer, matrix } = 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`, + `tonemap_opencl=format=nv12:r=pc:p=${primaries}:t=${transfer}:m=${matrix}:tonemap=${this.config.tonemap}:desat=0:tonemap_mode=lum:peak=100`, 'hwmap=derive_device=rkmpp:mode=write:reverse=1', 'format=drm_prime', ]; @@ -1027,12 +991,4 @@ export class RkmppHwDecodeConfig extends RkmppSwDecodeConfig { } return []; } - - getColors() { - return { - primaries: 'bt709', - transfer: 'bt709', - matrix: 'bt709', - }; - } } diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts index cdcdfd4d5e..082959c227 100644 --- a/server/test/fixtures/media.stub.ts +++ b/server/test/fixtures/media.stub.ts @@ -17,6 +17,7 @@ const probeStubDefaultVideoStream: VideoStreamInfo[] = [ rotation: 0, isHDR: false, bitrate: 0, + pixelFormat: 'yuv420p', }, ]; @@ -43,6 +44,7 @@ export const probeStub = { rotation: 0, isHDR: false, bitrate: 0, + pixelFormat: 'yuv420p', }, { index: 1, @@ -53,6 +55,7 @@ export const probeStub = { rotation: 0, isHDR: false, bitrate: 0, + pixelFormat: 'yuv420p', }, ], }), @@ -68,6 +71,7 @@ export const probeStub = { rotation: 0, isHDR: false, bitrate: 0, + pixelFormat: 'yuv420p', }, ], }), @@ -83,6 +87,7 @@ export const probeStub = { rotation: 0, isHDR: false, bitrate: 0, + pixelFormat: 'yuv420p', }, ], }), @@ -102,6 +107,23 @@ export const probeStub = { rotation: 0, isHDR: true, bitrate: 0, + pixelFormat: 'yuv420p10le', + }, + ], + }), + videoStream10Bit: Object.freeze({ + ...probeStubDefault, + videoStreams: [ + { + index: 0, + height: 480, + width: 480, + codecName: 'h264', + frameCount: 100, + rotation: 0, + isHDR: false, + bitrate: 0, + pixelFormat: 'yuv420p10le', }, ], }), @@ -117,6 +139,7 @@ export const probeStub = { rotation: 90, isHDR: false, bitrate: 0, + pixelFormat: 'yuv420p', }, ], }), @@ -132,6 +155,7 @@ export const probeStub = { rotation: 0, isHDR: false, bitrate: 0, + pixelFormat: 'yuv420p', }, ], }), @@ -147,6 +171,7 @@ export const probeStub = { rotation: 0, isHDR: false, bitrate: 0, + pixelFormat: 'yuv420p', }, ], }), 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 42cc004c52..8f5b587ae6 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 @@ -343,15 +343,6 @@ subtitle={$t('admin.transcoding_advanced_options_description')} >
- -