diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 3cdf87a74..b185f4d0c 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -909,6 +909,21 @@ export const CLIPMode = { export type CLIPMode = typeof CLIPMode[keyof typeof CLIPMode]; +/** + * + * @export + * @enum {string} + */ + +export const CQMode = { + Auto: 'auto', + Cqp: 'cqp', + Icq: 'icq' +} as const; + +export type CQMode = typeof CQMode[keyof typeof CQMode]; + + /** * * @export @@ -2812,24 +2827,54 @@ export interface SystemConfigFFmpegDto { * @memberof SystemConfigFFmpegDto */ 'accel': TranscodeHWAccel; + /** + * + * @type {number} + * @memberof SystemConfigFFmpegDto + */ + 'bframes': number; + /** + * + * @type {CQMode} + * @memberof SystemConfigFFmpegDto + */ + 'cqMode': CQMode; /** * * @type {number} * @memberof SystemConfigFFmpegDto */ 'crf': number; + /** + * + * @type {number} + * @memberof SystemConfigFFmpegDto + */ + 'gopSize': number; /** * * @type {string} * @memberof SystemConfigFFmpegDto */ 'maxBitrate': string; + /** + * + * @type {number} + * @memberof SystemConfigFFmpegDto + */ + 'npl': number; /** * * @type {string} * @memberof SystemConfigFFmpegDto */ 'preset': string; + /** + * + * @type {number} + * @memberof SystemConfigFFmpegDto + */ + 'refs': number; /** * * @type {AudioCodec} @@ -2848,6 +2893,12 @@ export interface SystemConfigFFmpegDto { * @memberof SystemConfigFFmpegDto */ 'targetVideoCodec': VideoCodec; + /** + * + * @type {boolean} + * @memberof SystemConfigFFmpegDto + */ + 'temporalAQ': boolean; /** * * @type {number} diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 5c6b65f46..7f2f8e0ff 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -37,6 +37,7 @@ doc/BulkIdResponseDto.md doc/BulkIdsDto.md doc/CLIPConfig.md doc/CLIPMode.md +doc/CQMode.md doc/ChangePasswordDto.md doc/CheckDuplicateAssetDto.md doc/CheckDuplicateAssetResponseDto.md @@ -198,6 +199,7 @@ lib/model/check_existing_assets_response_dto.dart lib/model/classification_config.dart lib/model/clip_config.dart lib/model/clip_mode.dart +lib/model/cq_mode.dart lib/model/create_album_dto.dart lib/model/create_profile_image_response_dto.dart lib/model/create_tag_dto.dart @@ -324,6 +326,7 @@ test/check_existing_assets_response_dto_test.dart test/classification_config_test.dart test/clip_config_test.dart test/clip_mode_test.dart +test/cq_mode_test.dart test/create_album_dto_test.dart test/create_profile_image_response_dto_test.dart test/create_tag_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 5972ea91c..f5ffe5319 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/CQMode.md b/mobile/openapi/doc/CQMode.md new file mode 100644 index 000000000..0375443a1 Binary files /dev/null and b/mobile/openapi/doc/CQMode.md differ diff --git a/mobile/openapi/doc/SystemConfigFFmpegDto.md b/mobile/openapi/doc/SystemConfigFFmpegDto.md index 2aee23f8d..b5c06b95d 100644 Binary files a/mobile/openapi/doc/SystemConfigFFmpegDto.md and b/mobile/openapi/doc/SystemConfigFFmpegDto.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index b31f92e8c..bd921bf67 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index f660f07e9..b4ca5a716 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index fcedc766d..c64bacac8 100644 Binary files a/mobile/openapi/lib/api_helper.dart and b/mobile/openapi/lib/api_helper.dart differ diff --git a/mobile/openapi/lib/model/cq_mode.dart b/mobile/openapi/lib/model/cq_mode.dart new file mode 100644 index 000000000..510d0a600 Binary files /dev/null and b/mobile/openapi/lib/model/cq_mode.dart 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 3e676c0ef..7911b4643 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/cq_mode_test.dart b/mobile/openapi/test/cq_mode_test.dart new file mode 100644 index 000000000..13d4a7b0c Binary files /dev/null and b/mobile/openapi/test/cq_mode_test.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 b12c696e7..dd9d3f5cb 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/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 99526294a..a61a16a76 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -5415,6 +5415,14 @@ ], "type": "string" }, + "CQMode": { + "enum": [ + "auto", + "cqp", + "icq" + ], + "type": "string" + }, "ChangePasswordDto": { "properties": { "newPassword": { @@ -7001,15 +7009,30 @@ "accel": { "$ref": "#/components/schemas/TranscodeHWAccel" }, + "bframes": { + "type": "integer" + }, + "cqMode": { + "$ref": "#/components/schemas/CQMode" + }, "crf": { "type": "integer" }, + "gopSize": { + "type": "integer" + }, "maxBitrate": { "type": "string" }, + "npl": { + "type": "integer" + }, "preset": { "type": "string" }, + "refs": { + "type": "integer" + }, "targetAudioCodec": { "$ref": "#/components/schemas/AudioCodec" }, @@ -7019,6 +7042,9 @@ "targetVideoCodec": { "$ref": "#/components/schemas/VideoCodec" }, + "temporalAQ": { + "type": "boolean" + }, "threads": { "type": "integer" }, @@ -7037,12 +7063,18 @@ "threads", "targetVideoCodec", "targetAudioCodec", + "bframes", + "refs", + "gopSize", + "npl", + "cqMode", "transcode", "accel", "tonemap", "preset", "targetResolution", "maxBitrate", + "temporalAQ", "twoPass" ], "type": "object" diff --git a/server/src/domain/media/media.service.spec.ts b/server/src/domain/media/media.service.spec.ts index d59da447a..355ba7873 100644 --- a/server/src/domain/media/media.service.spec.ts +++ b/server/src/domain/media/media.service.spec.ts @@ -311,10 +311,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf format=yuv420p', '-preset ultrafast', @@ -350,10 +352,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf format=yuv420p', '-preset ultrafast', @@ -374,10 +378,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-preset ultrafast', @@ -401,10 +407,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf format=yuv420p', '-preset ultrafast', @@ -426,10 +434,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=720:-2,format=yuv420p', '-preset ultrafast', @@ -451,10 +461,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-preset ultrafast', @@ -476,10 +488,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-preset ultrafast', @@ -525,10 +539,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-preset ultrafast', @@ -555,10 +571,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-preset ultrafast', @@ -582,10 +600,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-preset ultrafast', @@ -611,10 +631,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 vp9', - '-c:a:0 aac', + '-c:v vp9', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-cpu-used 5', @@ -642,10 +664,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 vp9', - '-c:a:0 aac', + '-c:v vp9', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-cpu-used 2', @@ -672,10 +696,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 vp9', - '-c:a:0 aac', + '-c:v vp9', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-row-mt 1', @@ -701,10 +727,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 vp9', - '-c:a:0 aac', + '-c:v vp9', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-cpu-used 5', @@ -729,10 +757,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-preset ultrafast', @@ -757,10 +787,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-preset ultrafast', @@ -785,10 +817,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 hevc', - '-c:a:0 aac', + '-c:v hevc', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-preset ultrafast', @@ -816,10 +850,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 hevc', - '-c:a:0 aac', + '-c:v hevc', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-preset ultrafast', @@ -878,17 +914,15 @@ describe(MediaService.name, () => { outputOptions: [ '-tune hq', '-qmin 0', - '-g 250', - '-bf 3', - '-b_ref_mode middle', - '-temporal-aq 1', '-rc-lookahead 20', '-i_qfactor 0.75', - '-b_qfactor 1.1', - `-c:v:0 h264_nvenc`, - '-c:a:0 aac', + `-c:v h264_nvenc`, + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', + '-g 256', '-v verbose', '-vf format=nv12,hwupload_cuda,scale_cuda=-2:720', '-preset p1', @@ -918,17 +952,15 @@ describe(MediaService.name, () => { outputOptions: [ '-tune hq', '-qmin 0', - '-g 250', - '-bf 3', - '-b_ref_mode middle', - '-temporal-aq 1', '-rc-lookahead 20', '-i_qfactor 0.75', - '-b_qfactor 1.1', - `-c:v:0 h264_nvenc`, - '-c:a:0 aac', + `-c:v h264_nvenc`, + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', + '-g 256', '-v verbose', '-vf format=nv12,hwupload_cuda,scale_cuda=-2:720', '-preset p1', @@ -954,17 +986,15 @@ describe(MediaService.name, () => { outputOptions: [ '-tune hq', '-qmin 0', - '-g 250', - '-bf 3', - '-b_ref_mode middle', - '-temporal-aq 1', '-rc-lookahead 20', '-i_qfactor 0.75', - '-b_qfactor 1.1', - `-c:v:0 h264_nvenc`, - '-c:a:0 aac', + `-c:v h264_nvenc`, + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', + '-g 256', '-v verbose', '-vf format=nv12,hwupload_cuda,scale_cuda=-2:720', '-preset p1', @@ -991,17 +1021,15 @@ describe(MediaService.name, () => { outputOptions: [ '-tune hq', '-qmin 0', - '-g 250', - '-bf 3', - '-b_ref_mode middle', - '-temporal-aq 1', '-rc-lookahead 20', '-i_qfactor 0.75', - '-b_qfactor 1.1', - `-c:v:0 h264_nvenc`, - '-c:a:0 aac', + `-c:v h264_nvenc`, + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', + '-g 256', '-v verbose', '-vf format=nv12,hwupload_cuda,scale_cuda=-2:720', '-cq:v 23', @@ -1024,17 +1052,15 @@ describe(MediaService.name, () => { outputOptions: [ '-tune hq', '-qmin 0', - '-g 250', - '-bf 3', - '-b_ref_mode middle', - '-temporal-aq 1', '-rc-lookahead 20', '-i_qfactor 0.75', - '-b_qfactor 1.1', - `-c:v:0 h264_nvenc`, - '-c:a:0 aac', + `-c:v h264_nvenc`, + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', + '-g 256', '-v verbose', '-vf format=nv12,hwupload_cuda,scale_cuda=-2:720', '-preset p1', @@ -1060,14 +1086,15 @@ describe(MediaService.name, () => { { inputOptions: ['-init_hw_device qsv=hw', '-filter_hw_device hw'], outputOptions: [ - '-g 256', - '-extbrc 1', - '-refs 5', - '-bf 7', - `-c:v:0 h264_qsv`, - '-c:a:0 aac', + `-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', @@ -1095,14 +1122,15 @@ describe(MediaService.name, () => { { inputOptions: ['-init_hw_device qsv=hw', '-filter_hw_device hw'], outputOptions: [ - '-g 256', - '-extbrc 1', - '-refs 5', - '-bf 7', - `-c:v:0 h264_qsv`, - '-c:a:0 aac', + `-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', '-global_quality 23', @@ -1127,14 +1155,15 @@ describe(MediaService.name, () => { { inputOptions: ['-init_hw_device qsv=hw', '-filter_hw_device hw'], outputOptions: [ - '-g 256', - '-extbrc 1', - '-refs 5', - '-bf 7', - `-c:v:0 vp9_qsv`, - '-c:a:0 aac', + `-c:v vp9_qsv`, + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', + '-bf 7', + '-refs 5', + '-g 256', '-low_power 1', '-v verbose', '-vf format=nv12,hwupload=extra_hw_frames=64,scale_qsv=-1:720', @@ -1170,10 +1199,13 @@ describe(MediaService.name, () => { { inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'], outputOptions: [ - `-c:v:0 h264_vaapi`, - '-c:a:0 aac', + `-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', @@ -1199,10 +1231,13 @@ describe(MediaService.name, () => { { inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'], outputOptions: [ - `-c:v:0 h264_vaapi`, - '-c:a:0 aac', + `-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', @@ -1230,10 +1265,13 @@ describe(MediaService.name, () => { { inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'], outputOptions: [ - `-c:v:0 h264_vaapi`, - '-c:a:0 aac', + `-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', '-qp 23', @@ -1257,10 +1295,13 @@ describe(MediaService.name, () => { { inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/card1', '-filter_hw_device accel'], outputOptions: [ - `-c:v:0 h264_vaapi`, - '-c:a:0 aac', + `-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', @@ -1280,10 +1321,13 @@ describe(MediaService.name, () => { { inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD129', '-filter_hw_device accel'], outputOptions: [ - `-c:v:0 h264_vaapi`, - '-c:a:0 aac', + `-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', @@ -1310,10 +1354,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-preset ultrafast', @@ -1345,10 +1391,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p', '-preset ultrafast', @@ -1370,10 +1418,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p', '-preset ultrafast', @@ -1395,10 +1445,12 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - '-c:v:0 h264', - '-c:a:0 aac', + '-c:v h264', + '-c:a aac', '-movflags faststart', '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', '-v verbose', '-vf zscale=t=linear:npl=250,tonemap=mobius:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p', '-preset ultrafast', diff --git a/server/src/domain/media/media.util.ts b/server/src/domain/media/media.util.ts index 546555d26..b276f20d0 100644 --- a/server/src/domain/media/media.util.ts +++ b/server/src/domain/media/media.util.ts @@ -1,4 +1,4 @@ -import { ToneMapping, TranscodeHWAccel, VideoCodec } from '@app/infra/entities'; +import { CQMode, ToneMapping, TranscodeHWAccel, VideoCodec } from '@app/infra/entities'; import { SystemConfigFFmpegDto } from '../system-config/dto'; import { AudioStreamInfo, @@ -9,9 +9,10 @@ import { VideoStreamInfo, } from './media.repository'; class BaseConfig implements VideoCodecSWConfig { + presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast']; constructor(protected config: SystemConfigFFmpegDto) {} - getOptions(videoStream: VideoStreamInfo, audioStream: AudioStreamInfo) { + getOptions(videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { const options = { inputOptions: this.getBaseInputOptions(), outputOptions: this.getBaseOutputOptions(videoStream, audioStream).concat('-v verbose'), @@ -32,15 +33,30 @@ class BaseConfig implements VideoCodecSWConfig { return []; } - getBaseOutputOptions(videoStream: VideoStreamInfo, audioStream: AudioStreamInfo) { - return [ - `-c:v:${videoStream.index} ${this.getVideoCodec()}`, - `-c:a:${audioStream.index} ${this.getAudioCodec()}`, + getBaseOutputOptions(videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { + const options = [ + `-c:v ${this.getVideoCodec()}`, + `-c:a ${this.getAudioCodec()}`, // Makes a second pass moving the moov atom to the // beginning of the file for improved playback speed. '-movflags faststart', '-fps_mode passthrough', + // explicitly selects the video stream instead of leaving it up to FFmpeg + `-map 0:${videoStream.index}`, ]; + if (audioStream) { + options.push(`-map 0:${audioStream.index}`); + } + if (this.getBFrames() > -1) { + options.push(`-bf ${this.getBFrames()}`); + } + if (this.getRefs() > 0) { + options.push(`-refs ${this.getRefs()}`); + } + if (this.getGopSize() > 0) { + options.push(`-g ${this.getGopSize()}`); + } + return options; } getFilterOptions(videoStream: VideoStreamInfo) { @@ -72,12 +88,12 @@ class BaseConfig implements VideoCodecSWConfig { } else if (bitrates.max > 0) { // -bufsize is the peak possible bitrate at any moment, while -maxrate is the max rolling average bitrate return [ - `-crf ${this.config.crf}`, + `-${this.useCQP() ? 'q:v' : 'crf'} ${this.config.crf}`, `-maxrate ${bitrates.max}${bitrates.unit}`, `-bufsize ${bitrates.max * 2}${bitrates.unit}`, ]; } else { - return [`-crf ${this.config.crf}`]; + return [`-${this.useCQP() ? 'q:v' : 'crf'} ${this.config.crf}`]; } } @@ -149,8 +165,7 @@ class BaseConfig implements VideoCodecSWConfig { } getPresetIndex() { - const presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast']; - return presets.indexOf(this.config.preset); + return this.presets.indexOf(this.config.preset); } getColors() { @@ -161,14 +176,20 @@ class BaseConfig implements VideoCodecSWConfig { }; } + 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() { const colors = this.getColors(); - // npl stands for nominal peak luminance - // lower npl values result in brighter output (compensating for dimmer screens) - // since hable already outputs a darker image, we use a lower npl value for it - const npl = this.config.tonemap === ToneMapping.HABLE ? 100 : 250; + return [ - `zscale=t=linear:npl=${npl}`, + `zscale=t=linear:npl=${this.getNPL()}`, `tonemap=${this.config.tonemap}:desat=0`, `zscale=p=${colors.primaries}:t=${colors.transfer}:m=${colors.matrix}:range=pc`, ]; @@ -181,6 +202,22 @@ class BaseConfig implements VideoCodecSWConfig { getVideoCodec(): string { return this.config.targetVideoCodec; } + + getBFrames() { + return this.config.bframes; + } + + getRefs() { + return this.config.refs; + } + + getGopSize() { + return this.config.gopSize; + } + + useCQP() { + return this.config.cqMode === CQMode.CQP; + } } export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig { @@ -216,6 +253,13 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig { getVideoCodec(): string { return `${this.config.targetVideoCodec}_${this.config.accel}`; } + + getGopSize() { + if (this.config.gopSize <= 0) { + return 256; + } + return this.config.gopSize; + } } export class ThumbnailConfig extends BaseConfig { @@ -294,7 +338,7 @@ export class VP9Config extends BaseConfig { ]; } - return [`-crf ${this.config.crf}`, `-b:v ${bitrates.max}${bitrates.unit}`]; + return [`-${this.useCQP() ? 'q:v' : 'crf'} ${this.config.crf}`, `-b:v ${bitrates.max}${bitrates.unit}`]; } getThreadOptions() { @@ -311,20 +355,23 @@ export class NVENCConfig extends BaseHWConfig { return ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']; } - getBaseOutputOptions(videoStream: VideoStreamInfo, audioStream: AudioStreamInfo) { - return [ + getBaseOutputOptions(videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { + const options = [ // below settings recommended from https://docs.nvidia.com/video-technologies/video-codec-sdk/12.0/ffmpeg-with-nvidia-gpu/index.html#command-line-for-latency-tolerant-high-quality-transcoding '-tune hq', '-qmin 0', - '-g 250', - '-bf 3', - '-b_ref_mode middle', - '-temporal-aq 1', '-rc-lookahead 20', '-i_qfactor 0.75', - '-b_qfactor 1.1', ...super.getBaseOutputOptions(videoStream, audioStream), ]; + if (this.getBFrames() > 0) { + options.push('-b_ref_mode middle'); + options.push('-b_qfactor 1.1'); + } + if (this.config.temporalAQ) { + options.push('-temporal-aq 1'); + } + return options; } getFilterOptions(videoStream: VideoStreamInfo) { @@ -369,6 +416,14 @@ export class NVENCConfig extends BaseHWConfig { getThreadOptions() { return []; } + + getRefs() { + const bframes = this.getBFrames(); + if (bframes > 0 && bframes < 3 && this.config.refs < 3) { + return 0; + } + return this.config.refs; + } } export class QSVConfig extends BaseHWConfig { @@ -379,15 +434,8 @@ export class QSVConfig extends BaseHWConfig { return ['-init_hw_device qsv=hw', '-filter_hw_device hw']; } - getBaseOutputOptions(videoStream: VideoStreamInfo, audioStream: AudioStreamInfo) { - // recommended from https://github.com/intel/media-delivery/blob/master/doc/benchmarks/intel-iris-xe-max-graphics/intel-iris-xe-max-graphics.md - const options = [ - '-g 256', - '-extbrc 1', - '-refs 5', - '-bf 7', - ...super.getBaseOutputOptions(videoStream, audioStream), - ]; + getBaseOutputOptions(videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { + const options = super.getBaseOutputOptions(videoStream, audioStream); // VP9 requires enabling low power mode https://git.ffmpeg.org/gitweb/ffmpeg.git/commit/33583803e107b6d532def0f9d949364b01b6ad5a if (this.config.targetVideoCodec === VideoCodec.VP9) { options.push('-low_power 1'); @@ -415,11 +463,7 @@ export class QSVConfig extends BaseHWConfig { getBitrateOptions() { const options = []; - if (this.config.targetVideoCodec !== VideoCodec.VP9) { - options.push(`-global_quality ${this.config.crf}`); - } else { - options.push(`-q:v ${this.config.crf}`); - } + options.push(`-${this.useCQP() ? 'q:v' : 'global_quality'} ${this.config.crf}`); const bitrates = this.getBitrateDistribution(); if (bitrates.max > 0) { options.push(`-maxrate ${bitrates.max}${bitrates.unit}`); @@ -427,6 +471,25 @@ export class QSVConfig extends BaseHWConfig { } return options; } + + // recommended from https://github.com/intel/media-delivery/blob/master/doc/benchmarks/intel-iris-xe-max-graphics/intel-iris-xe-max-graphics.md + getBFrames() { + if (this.config.bframes < 0) { + return 7; + } + return this.config.bframes; + } + + getRefs() { + if (this.config.refs <= 0) { + return 5; + } + return this.config.refs; + } + + useCQP() { + return this.config.cqMode === CQMode.CQP || this.config.targetVideoCodec === VideoCodec.VP9; + } } export class VAAPIConfig extends BaseHWConfig { @@ -458,16 +521,30 @@ export class VAAPIConfig extends BaseHWConfig { getBitrateOptions() { const bitrates = this.getBitrateDistribution(); + const options = []; + + if (this.config.targetVideoCodec === VideoCodec.VP9) { + options.push('-bsf:v vp9_raw_reorder,vp9_superframe'); + } + // VAAPI doesn't allow setting both quality and max bitrate if (bitrates.max > 0) { - return [ + options.push( `-b:v ${bitrates.target}${bitrates.unit}`, `-maxrate ${bitrates.max}${bitrates.unit}`, `-minrate ${bitrates.min}${bitrates.unit}`, '-rc_mode 3', - ]; // variable bitrate + ); // variable bitrate + } else if (this.useCQP()) { + options.push(`-qp ${this.config.crf}`, `-global_quality ${this.config.crf}`, '-rc_mode 1'); } else { - return [`-qp ${this.config.crf}`, `-global_quality ${this.config.crf}`, '-rc_mode 1']; // constant quality + options.push(`-global_quality ${this.config.crf}`, '-rc_mode 4'); } + + return options; + } + + useCQP() { + return this.config.cqMode !== CQMode.ICQ || this.config.targetVideoCodec === VideoCodec.VP9; } } 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 a5bd4cc6a..cd1d1c52f 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 @@ -1,4 +1,4 @@ -import { AudioCodec, ToneMapping, TranscodeHWAccel, TranscodePolicy, VideoCodec } from '@app/infra/entities'; +import { AudioCodec, CQMode, ToneMapping, TranscodeHWAccel, TranscodePolicy, VideoCodec } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsBoolean, IsEnum, IsInt, IsString, Max, Min } from 'class-validator'; @@ -34,6 +34,39 @@ export class SystemConfigFFmpegDto { @IsString() maxBitrate!: string; + @IsInt() + @Min(-1) + @Max(16) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + bframes!: number; + + @IsInt() + @Min(0) + @Max(6) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + refs!: number; + + @IsInt() + @Min(0) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + gopSize!: number; + + @IsInt() + @Min(0) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + npl!: number; + + @IsBoolean() + temporalAQ!: boolean; + + @IsEnum(CQMode) + @ApiProperty({ enumName: 'CQMode', enum: CQMode }) + cqMode!: CQMode; + @IsBoolean() twoPass!: boolean; diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index f4154cd15..1236c15e8 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -1,5 +1,6 @@ import { AudioCodec, + CQMode, SystemConfig, SystemConfigEntity, SystemConfigKey, @@ -30,6 +31,12 @@ export const defaults = Object.freeze({ targetAudioCodec: AudioCodec.AAC, targetResolution: '720', maxBitrate: '0', + bframes: -1, + refs: 0, + gopSize: 0, + npl: 0, + temporalAQ: false, + cqMode: CQMode.AUTO, twoPass: false, transcode: TranscodePolicy.REQUIRED, tonemap: ToneMapping.HABLE, 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 3e0d8b470..28016c20d 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -1,5 +1,6 @@ import { AudioCodec, + CQMode, SystemConfig, SystemConfigEntity, SystemConfigKey, @@ -41,6 +42,12 @@ const updatedConfig = Object.freeze({ targetResolution: '720', targetVideoCodec: VideoCodec.H264, maxBitrate: '0', + bframes: -1, + refs: 0, + gopSize: 0, + npl: 0, + temporalAQ: false, + cqMode: CQMode.AUTO, twoPass: false, transcode: TranscodePolicy.REQUIRED, accel: TranscodeHWAccel.DISABLED, diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/infra/entities/system-config.entity.ts index f124a1306..941058959 100644 --- a/server/src/infra/entities/system-config.entity.ts +++ b/server/src/infra/entities/system-config.entity.ts @@ -21,6 +21,12 @@ export enum SystemConfigKey { FFMPEG_TARGET_AUDIO_CODEC = 'ffmpeg.targetAudioCodec', FFMPEG_TARGET_RESOLUTION = 'ffmpeg.targetResolution', FFMPEG_MAX_BITRATE = 'ffmpeg.maxBitrate', + FFMPEG_BFRAMES = 'ffmpeg.bframes', + FFMPEG_REFS = 'ffmpeg.refs', + FFMPEG_GOP_SIZE = 'ffmpeg.gopSize', + FFMPEG_NPL = 'ffmpeg.npl', + FFMPEG_TEMPORAL_AQ = 'ffmpeg.temporalAQ', + FFMPEG_CQ_MODE = 'ffmpeg.cqMode', FFMPEG_TWO_PASS = 'ffmpeg.twoPass', FFMPEG_TRANSCODE = 'ffmpeg.transcode', FFMPEG_ACCEL = 'ffmpeg.accel', @@ -105,6 +111,12 @@ export enum ToneMapping { DISABLED = 'disabled', } +export enum CQMode { + AUTO = 'auto', + CQP = 'cqp', + ICQ = 'icq', +} + export interface SystemConfig { ffmpeg: { crf: number; @@ -114,6 +126,12 @@ export interface SystemConfig { targetAudioCodec: AudioCodec; targetResolution: string; maxBitrate: string; + bframes: number; + refs: number; + gopSize: number; + npl: number; + temporalAQ: boolean; + cqMode: CQMode; twoPass: boolean; transcode: TranscodePolicy; accel: TranscodeHWAccel; diff --git a/server/src/infra/repositories/media.repository.ts b/server/src/infra/repositories/media.repository.ts index a26170865..4012cbf13 100644 --- a/server/src/infra/repositories/media.repository.ts +++ b/server/src/infra/repositories/media.repository.ts @@ -32,7 +32,6 @@ export class MediaRepository implements IMediaRepository { async probe(input: string): Promise { const results = await probe(input); - return { format: { formatName: results.format.format_name, diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts index f5988b9c6..8cde51cad 100644 --- a/server/test/fixtures/media.stub.ts +++ b/server/test/fixtures/media.stub.ts @@ -20,7 +20,7 @@ const probeStubDefaultVideoStream: VideoStreamInfo[] = [ ]; const probeStubDefaultAudioStream: AudioStreamInfo[] = [ - { index: 0, codecName: 'aac', codecType: 'audio', frameCount: 100 }, + { index: 1, codecName: 'aac', codecType: 'audio', frameCount: 100 }, ]; const probeStubDefault: VideoInfo = { @@ -119,7 +119,7 @@ export const probeStub = { }), audioStreamMp3: Object.freeze({ ...probeStubDefault, - audioStreams: [{ index: 0, codecType: 'audio', codecName: 'aac', frameCount: 100 }], + audioStreams: [{ index: 1, codecType: 'audio', codecName: 'aac', frameCount: 100 }], }), matroskaContainer: Object.freeze({ ...probeStubDefault, diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 3cdf87a74..b185f4d0c 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -909,6 +909,21 @@ export const CLIPMode = { export type CLIPMode = typeof CLIPMode[keyof typeof CLIPMode]; +/** + * + * @export + * @enum {string} + */ + +export const CQMode = { + Auto: 'auto', + Cqp: 'cqp', + Icq: 'icq' +} as const; + +export type CQMode = typeof CQMode[keyof typeof CQMode]; + + /** * * @export @@ -2812,24 +2827,54 @@ export interface SystemConfigFFmpegDto { * @memberof SystemConfigFFmpegDto */ 'accel': TranscodeHWAccel; + /** + * + * @type {number} + * @memberof SystemConfigFFmpegDto + */ + 'bframes': number; + /** + * + * @type {CQMode} + * @memberof SystemConfigFFmpegDto + */ + 'cqMode': CQMode; /** * * @type {number} * @memberof SystemConfigFFmpegDto */ 'crf': number; + /** + * + * @type {number} + * @memberof SystemConfigFFmpegDto + */ + 'gopSize': number; /** * * @type {string} * @memberof SystemConfigFFmpegDto */ 'maxBitrate': string; + /** + * + * @type {number} + * @memberof SystemConfigFFmpegDto + */ + 'npl': number; /** * * @type {string} * @memberof SystemConfigFFmpegDto */ 'preset': string; + /** + * + * @type {number} + * @memberof SystemConfigFFmpegDto + */ + 'refs': number; /** * * @type {AudioCodec} @@ -2848,6 +2893,12 @@ export interface SystemConfigFFmpegDto { * @memberof SystemConfigFFmpegDto */ 'targetVideoCodec': VideoCodec; + /** + * + * @type {boolean} + * @memberof SystemConfigFFmpegDto + */ + 'temporalAQ': boolean; /** * * @type {number} 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 59be28f13..a75b1ab0b 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 @@ -6,6 +6,7 @@ import { api, AudioCodec, + CQMode, SystemConfigFFmpegDto, ToneMapping, TranscodeHWAccel, @@ -19,6 +20,7 @@ import HelpCircleOutline from 'svelte-material-icons/HelpCircleOutline.svelte'; import { isEqual } from 'lodash-es'; import { fade } from 'svelte/transition'; + import SettingAccordion from '../setting-accordion.svelte'; export let ffmpegConfig: SystemConfigFFmpegDto; // this is the config that is being edited export let disabled = false; @@ -112,7 +114,7 @@ desc="Video quality level. Typical values are 23 for H.264, 28 for HEVC, and 31 for VP9. Lower is better, but takes longer to encode and produces larger files." bind:value={ffmpegConfig.crf} required={true} - isEdited={!(ffmpegConfig.crf == savedConfig.crf)} + isEdited={ffmpegConfig.crf !== savedConfig.crf} /> - - + + +
+ + + + + +
+
+ + +
+ + + + + + + +
+
diff --git a/web/src/lib/components/admin-page/settings/setting-input-field.svelte b/web/src/lib/components/admin-page/settings/setting-input-field.svelte index 843e799b3..376f2ef13 100644 --- a/web/src/lib/components/admin-page/settings/setting-input-field.svelte +++ b/web/src/lib/components/admin-page/settings/setting-input-field.svelte @@ -13,8 +13,8 @@ export let inputType: SettingInputFieldType; export let value: string | number; - export let min = Number.MIN_VALUE.toString(); - export let max = Number.MAX_VALUE.toString(); + export let min = Number.MIN_SAFE_INTEGER.toString(); + export let max = Number.MAX_SAFE_INTEGER.toString(); export let step = '1'; export let label = ''; export let desc = '';