mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 06:31:58 +00:00
feat(server): fully accelerated nvenc (#9452)
* use arrayContaining * libplacebo for nvenc update dockerfile * tweaks * update nvenc options * tweak settings * refactor * toggle for hardware decoding, software / hardware decoding for nvenc and rkmpp * fix software tone-mapping not being applied * separate configs for hw/sw * update api * add hw decode toggle * fix mutating config * remove `version` flag * fix config type * remove submodule * handle temporal AQ * remove duplicate tests * use `tonemap_opencl` * wording * update docs
This commit is contained in:
parent
64636c0618
commit
d8eca168ca
15 changed files with 240 additions and 63 deletions
|
@ -1,9 +1,7 @@
|
||||||
version: "3.8"
|
|
||||||
|
|
||||||
# Configurations for hardware-accelerated machine learning
|
# Configurations for hardware-accelerated machine learning
|
||||||
|
|
||||||
# If using Unraid or another platform that doesn't allow multiple Compose files,
|
# 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.
|
# 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.
|
# See https://immich.app/docs/features/ml-hardware-acceleration for info on usage.
|
||||||
|
@ -30,7 +28,7 @@ services:
|
||||||
|
|
||||||
openvino:
|
openvino:
|
||||||
device_cgroup_rules:
|
device_cgroup_rules:
|
||||||
- "c 189:* rmw"
|
- 'c 189:* rmw'
|
||||||
devices:
|
devices:
|
||||||
- /dev/dri:/dev/dri
|
- /dev/dri:/dev/dri
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
version: "3.8"
|
|
||||||
|
|
||||||
# Configurations for hardware-accelerated transcoding
|
# Configurations for hardware-accelerated transcoding
|
||||||
|
|
||||||
# If using Unraid or another platform that doesn't allow multiple Compose files,
|
# If using Unraid or another platform that doesn't allow multiple Compose files,
|
||||||
|
|
|
@ -22,7 +22,8 @@ You do not need to redo any transcoding jobs after enabling hardware acceleratio
|
||||||
- WSL2 does not support Quick Sync.
|
- WSL2 does not support Quick Sync.
|
||||||
- Raspberry Pi is currently not supported.
|
- Raspberry Pi is currently not supported.
|
||||||
- Two-pass mode is only supported for NVENC. Other APIs will ignore this setting.
|
- 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
|
- Hardware dependent
|
||||||
- Codec support varies, but H.264 and HEVC are usually supported.
|
- Codec support varies, but H.264 and HEVC are usually supported.
|
||||||
- Notably, NVIDIA and AMD GPUs do not support VP9 encoding.
|
- 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.
|
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.
|
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
|
#### Single Compose File
|
||||||
|
|
||||||
|
|
BIN
mobile/openapi/doc/SystemConfigFFmpegDto.md
generated
BIN
mobile/openapi/doc/SystemConfigFFmpegDto.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart
generated
BIN
mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/system_config_f_fmpeg_dto_test.dart
generated
BIN
mobile/openapi/test/system_config_f_fmpeg_dto_test.dart
generated
Binary file not shown.
|
@ -10050,6 +10050,9 @@
|
||||||
"accel": {
|
"accel": {
|
||||||
"$ref": "#/components/schemas/TranscodeHWAccel"
|
"$ref": "#/components/schemas/TranscodeHWAccel"
|
||||||
},
|
},
|
||||||
|
"accelDecode": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"acceptedAudioCodecs": {
|
"acceptedAudioCodecs": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/AudioCodec"
|
"$ref": "#/components/schemas/AudioCodec"
|
||||||
|
@ -10125,6 +10128,7 @@
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"accel",
|
"accel",
|
||||||
|
"accelDecode",
|
||||||
"acceptedAudioCodecs",
|
"acceptedAudioCodecs",
|
||||||
"acceptedVideoCodecs",
|
"acceptedVideoCodecs",
|
||||||
"bframes",
|
"bframes",
|
||||||
|
|
|
@ -863,6 +863,7 @@ export type AssetFullSyncDto = {
|
||||||
};
|
};
|
||||||
export type SystemConfigFFmpegDto = {
|
export type SystemConfigFFmpegDto = {
|
||||||
accel: TranscodeHWAccel;
|
accel: TranscodeHWAccel;
|
||||||
|
accelDecode: boolean;
|
||||||
acceptedAudioCodecs: AudioCodec[];
|
acceptedAudioCodecs: AudioCodec[];
|
||||||
acceptedVideoCodecs: VideoCodec[];
|
acceptedVideoCodecs: VideoCodec[];
|
||||||
bframes: number;
|
bframes: number;
|
||||||
|
|
|
@ -97,6 +97,7 @@ export interface SystemConfig {
|
||||||
preferredHwDevice: string;
|
preferredHwDevice: string;
|
||||||
transcode: TranscodePolicy;
|
transcode: TranscodePolicy;
|
||||||
accel: TranscodeHWAccel;
|
accel: TranscodeHWAccel;
|
||||||
|
accelDecode: boolean;
|
||||||
tonemap: ToneMapping;
|
tonemap: ToneMapping;
|
||||||
};
|
};
|
||||||
job: Record<ConcurrentQueueName, { concurrency: number }>;
|
job: Record<ConcurrentQueueName, { concurrency: number }>;
|
||||||
|
@ -228,6 +229,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||||
transcode: TranscodePolicy.REQUIRED,
|
transcode: TranscodePolicy.REQUIRED,
|
||||||
tonemap: ToneMapping.HABLE,
|
tonemap: ToneMapping.HABLE,
|
||||||
accel: TranscodeHWAccel.DISABLED,
|
accel: TranscodeHWAccel.DISABLED,
|
||||||
|
accelDecode: false,
|
||||||
},
|
},
|
||||||
job: {
|
job: {
|
||||||
[QueueName.BACKGROUND_TASK]: { concurrency: 5 },
|
[QueueName.BACKGROUND_TASK]: { concurrency: 5 },
|
||||||
|
|
|
@ -132,6 +132,9 @@ export class SystemConfigFFmpegDto {
|
||||||
@ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel })
|
@ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel })
|
||||||
accel!: TranscodeHWAccel;
|
accel!: TranscodeHWAccel;
|
||||||
|
|
||||||
|
@ValidateBoolean()
|
||||||
|
accelDecode!: boolean;
|
||||||
|
|
||||||
@IsEnum(ToneMapping)
|
@IsEnum(ToneMapping)
|
||||||
@ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping })
|
@ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping })
|
||||||
tonemap!: ToneMapping;
|
tonemap!: ToneMapping;
|
||||||
|
|
|
@ -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 () => {
|
it('should set options for qsv', async () => {
|
||||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
|
@ -1633,8 +1678,9 @@ describe(MediaService.name, () => {
|
||||||
|
|
||||||
it('should set options for rkmpp', async () => {
|
it('should set options for rkmpp', async () => {
|
||||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||||
|
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
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]);
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
|
@ -1663,10 +1709,12 @@ describe(MediaService.name, () => {
|
||||||
|
|
||||||
it('should set vbr options for rkmpp when max bitrate is enabled', async () => {
|
it('should set vbr options for rkmpp when max bitrate is enabled', async () => {
|
||||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||||
|
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||||
systemMock.get.mockResolvedValue({
|
systemMock.get.mockResolvedValue({
|
||||||
ffmpeg: {
|
ffmpeg: {
|
||||||
accel: TranscodeHWAccel.RKMPP,
|
accel: TranscodeHWAccel.RKMPP,
|
||||||
|
accelDecode: true,
|
||||||
maxBitrate: '10000k',
|
maxBitrate: '10000k',
|
||||||
targetVideoCodec: VideoCodec.HEVC,
|
targetVideoCodec: VideoCodec.HEVC,
|
||||||
},
|
},
|
||||||
|
@ -1686,9 +1734,10 @@ describe(MediaService.name, () => {
|
||||||
|
|
||||||
it('should set cqp options for rkmpp when max bitrate is disabled', async () => {
|
it('should set cqp options for rkmpp when max bitrate is disabled', async () => {
|
||||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||||
|
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
systemMock.get.mockResolvedValue({
|
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]);
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
|
@ -1707,7 +1756,9 @@ describe(MediaService.name, () => {
|
||||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||||
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
|
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
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]);
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
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 () => {
|
it('should tonemap when policy is required and video is hdr', async () => {
|
||||||
|
|
|
@ -36,9 +36,11 @@ import {
|
||||||
AV1Config,
|
AV1Config,
|
||||||
H264Config,
|
H264Config,
|
||||||
HEVCConfig,
|
HEVCConfig,
|
||||||
NVENCConfig,
|
NvencHwDecodeConfig,
|
||||||
|
NvencSwDecodeConfig,
|
||||||
QSVConfig,
|
QSVConfig,
|
||||||
RKMPPConfig,
|
RkmppHwDecodeConfig,
|
||||||
|
RkmppSwDecodeConfig,
|
||||||
ThumbnailConfig,
|
ThumbnailConfig,
|
||||||
VAAPIConfig,
|
VAAPIConfig,
|
||||||
VP9Config,
|
VP9Config,
|
||||||
|
@ -360,8 +362,7 @@ export class MediaService {
|
||||||
`Error occurred during transcoding. Retrying with ${config.accel.toUpperCase()} acceleration disabled.`,
|
`Error occurred during transcoding. Retrying with ${config.accel.toUpperCase()} acceleration disabled.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
config.accel = TranscodeHWAccel.DISABLED;
|
transcodeOptions = await this.getCodecConfig({ ...config, accel: TranscodeHWAccel.DISABLED }).then((c) =>
|
||||||
transcodeOptions = await this.getCodecConfig(config).then((c) =>
|
|
||||||
c.getOptions(target, mainVideoStream, mainAudioStream),
|
c.getOptions(target, mainVideoStream, mainAudioStream),
|
||||||
);
|
);
|
||||||
await this.mediaRepository.transcode(input, output, transcodeOptions);
|
await this.mediaRepository.transcode(input, output, transcodeOptions);
|
||||||
|
@ -494,7 +495,7 @@ export class MediaService {
|
||||||
let handler: VideoCodecHWConfig;
|
let handler: VideoCodecHWConfig;
|
||||||
switch (config.accel) {
|
switch (config.accel) {
|
||||||
case TranscodeHWAccel.NVENC: {
|
case TranscodeHWAccel.NVENC: {
|
||||||
handler = new NVENCConfig(config);
|
handler = config.accelDecode ? new NvencHwDecodeConfig(config) : new NvencSwDecodeConfig(config);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TranscodeHWAccel.QSV: {
|
case TranscodeHWAccel.QSV: {
|
||||||
|
@ -506,7 +507,10 @@ export class MediaService {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TranscodeHWAccel.RKMPP: {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|
|
@ -66,6 +66,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||||
preferredHwDevice: 'auto',
|
preferredHwDevice: 'auto',
|
||||||
transcode: TranscodePolicy.REQUIRED,
|
transcode: TranscodePolicy.REQUIRED,
|
||||||
accel: TranscodeHWAccel.DISABLED,
|
accel: TranscodeHWAccel.DISABLED,
|
||||||
|
accelDecode: false,
|
||||||
tonemap: ToneMapping.HABLE,
|
tonemap: ToneMapping.HABLE,
|
||||||
},
|
},
|
||||||
logging: {
|
logging: {
|
||||||
|
|
|
@ -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;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
getBaseInputOptions(videoStream: VideoStreamInfo): string[] {
|
getBaseInputOptions(videoStream: VideoStreamInfo): string[] {
|
||||||
return [];
|
return this.getInputThreadOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
|
getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
|
||||||
|
@ -80,11 +84,7 @@ class BaseConfig implements VideoCodecSWConfig {
|
||||||
options.push(`scale=${this.getScaling(videoStream)}`);
|
options.push(`scale=${this.getScaling(videoStream)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldToneMap(videoStream)) {
|
options.push(...this.getToneMapping(videoStream), 'format=yuv420p');
|
||||||
options.push(...this.getToneMapping());
|
|
||||||
}
|
|
||||||
options.push('format=yuv420p');
|
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +112,11 @@ class BaseConfig implements VideoCodecSWConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getThreadOptions(): Array<string> {
|
getInputThreadOptions(): Array<string> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getOutputThreadOptions(): Array<string> {
|
||||||
if (this.config.threads <= 0) {
|
if (this.config.threads <= 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -218,7 +222,11 @@ class BaseConfig implements VideoCodecSWConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getToneMapping() {
|
getToneMapping(videoStream: VideoStreamInfo) {
|
||||||
|
if (!this.shouldToneMap(videoStream)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const colors = this.getColors();
|
const colors = this.getColors();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -348,8 +356,8 @@ export class ThumbnailConfig extends BaseConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class H264Config extends BaseConfig {
|
export class H264Config extends BaseConfig {
|
||||||
getThreadOptions() {
|
getOutputThreadOptions() {
|
||||||
const options = super.getThreadOptions();
|
const options = super.getOutputThreadOptions();
|
||||||
if (this.config.threads === 1) {
|
if (this.config.threads === 1) {
|
||||||
options.push('-x264-params frame-threads=1:pools=none');
|
options.push('-x264-params frame-threads=1:pools=none');
|
||||||
}
|
}
|
||||||
|
@ -359,8 +367,8 @@ export class H264Config extends BaseConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HEVCConfig extends BaseConfig {
|
export class HEVCConfig extends BaseConfig {
|
||||||
getThreadOptions() {
|
getOutputThreadOptions() {
|
||||||
const options = super.getThreadOptions();
|
const options = super.getOutputThreadOptions();
|
||||||
if (this.config.threads === 1) {
|
if (this.config.threads === 1) {
|
||||||
options.push('-x265-params frame-threads=1:pools=none');
|
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}`];
|
return [`-${this.useCQP() ? 'q:v' : 'crf'} ${this.config.crf}`, `-b:v ${bitrates.max}${bitrates.unit}`];
|
||||||
}
|
}
|
||||||
|
|
||||||
getThreadOptions() {
|
getOutputThreadOptions() {
|
||||||
return ['-row-mt 1', ...super.getThreadOptions()];
|
return ['-row-mt 1', ...super.getOutputThreadOptions()];
|
||||||
}
|
}
|
||||||
|
|
||||||
eligibleForTwoPass() {
|
eligibleForTwoPass() {
|
||||||
|
@ -425,7 +433,7 @@ export class AV1Config extends BaseConfig {
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
getThreadOptions() {
|
getOutputThreadOptions() {
|
||||||
return []; // Already set above with svtav1-params
|
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() {
|
getSupportedCodecs() {
|
||||||
return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.AV1];
|
return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.AV1];
|
||||||
}
|
}
|
||||||
|
@ -462,7 +470,7 @@ export class NVENCConfig extends BaseHWConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||||
const options = this.shouldToneMap(videoStream) ? this.getToneMapping() : [];
|
const options = this.getToneMapping(videoStream);
|
||||||
options.push('format=nv12', 'hwupload_cuda');
|
options.push('format=nv12', 'hwupload_cuda');
|
||||||
if (this.shouldScale(videoStream)) {
|
if (this.shouldScale(videoStream)) {
|
||||||
options.push(`scale_cuda=${this.getScaling(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 {
|
export class QSVConfig extends BaseHWConfig {
|
||||||
getBaseInputOptions() {
|
getBaseInputOptions() {
|
||||||
if (this.devices.length === 0) {
|
if (this.devices.length === 0) {
|
||||||
|
@ -538,7 +592,7 @@ export class QSVConfig extends BaseHWConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||||
const options = this.shouldToneMap(videoStream) ? this.getToneMapping() : [];
|
const options = this.getToneMapping(videoStream);
|
||||||
options.push('format=nv12', 'hwupload=extra_hw_frames=64');
|
options.push('format=nv12', 'hwupload=extra_hw_frames=64');
|
||||||
if (this.shouldScale(videoStream)) {
|
if (this.shouldScale(videoStream)) {
|
||||||
options.push(`scale_qsv=${this.getScaling(videoStream)}`);
|
options.push(`scale_qsv=${this.getScaling(videoStream)}`);
|
||||||
|
@ -604,7 +658,7 @@ export class VAAPIConfig extends BaseHWConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||||
const options = this.shouldToneMap(videoStream) ? this.getToneMapping() : [];
|
const options = this.getToneMapping(videoStream);
|
||||||
options.push('format=nv12', 'hwupload');
|
options.push('format=nv12', 'hwupload');
|
||||||
if (this.shouldScale(videoStream)) {
|
if (this.shouldScale(videoStream)) {
|
||||||
options.push(`scale_vaapi=${this.getScaling(videoStream)}`);
|
options.push(`scale_vaapi=${this.getScaling(videoStream)}`);
|
||||||
|
@ -656,47 +710,22 @@ export class VAAPIConfig extends BaseHWConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RKMPPConfig extends BaseHWConfig {
|
export class RkmppSwDecodeConfig extends BaseHWConfig {
|
||||||
private hasOpenCL: boolean;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected config: SystemConfigFFmpegDto,
|
protected config: SystemConfigFFmpegDto,
|
||||||
devices: string[] = [],
|
devices: string[] = [],
|
||||||
hasOpenCL: boolean = false,
|
|
||||||
) {
|
) {
|
||||||
super(config, devices);
|
super(config, devices);
|
||||||
this.hasOpenCL = hasOpenCL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
eligibleForTwoPass(): boolean {
|
eligibleForTwoPass(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getBaseInputOptions(videoStream: VideoStreamInfo) {
|
getBaseInputOptions(): string[] {
|
||||||
if (this.devices.length === 0) {
|
if (this.devices.length === 0) {
|
||||||
throw new Error('No RKMPP device found');
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -734,3 +763,29 @@ export class RKMPPConfig extends BaseHWConfig {
|
||||||
return `${this.config.targetVideoCodec}_rkmpp`;
|
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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -276,6 +276,15 @@
|
||||||
isEdited={config.ffmpeg.accel !== savedConfig.ffmpeg.accel}
|
isEdited={config.ffmpeg.accel !== savedConfig.ffmpeg.accel}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SettingSwitch
|
||||||
|
id="hardware-decoding"
|
||||||
|
title="HARDWARE DECODING"
|
||||||
|
{disabled}
|
||||||
|
subtitle="Applies only to NVENC and RKMPP. Enables end-to-end acceleration instead of only accelerating encoding. May not work on all videos."
|
||||||
|
bind:checked={config.ffmpeg.accelDecode}
|
||||||
|
isEdited={config.ffmpeg.accelDecode !== savedConfig.ffmpeg.accelDecode}
|
||||||
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="CONSTANT QUALITY MODE"
|
label="CONSTANT QUALITY MODE"
|
||||||
desc="ICQ is better than CQP, but some hardware acceleration devices do not support this mode. Setting this option will prefer the specified mode when using quality-based encoding. Ignored by NVENC as it does not support ICQ."
|
desc="ICQ is better than CQP, but some hardware acceleration devices do not support this mode. Setting this option will prefer the specified mode when using quality-based encoding. Ignored by NVENC as it does not support ICQ."
|
||||||
|
@ -297,6 +306,7 @@
|
||||||
bind:checked={config.ffmpeg.temporalAQ}
|
bind:checked={config.ffmpeg.temporalAQ}
|
||||||
isEdited={config.ffmpeg.temporalAQ !== savedConfig.ffmpeg.temporalAQ}
|
isEdited={config.ffmpeg.temporalAQ !== savedConfig.ffmpeg.temporalAQ}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="PREFERRED HARDWARE DEVICE"
|
label="PREFERRED HARDWARE DEVICE"
|
||||||
|
|
Loading…
Reference in a new issue