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:
Mert 2024-05-16 13:30:26 -04:00 committed by GitHub
parent 64636c0618
commit d8eca168ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 255 additions and 64 deletions

View File

@ -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:

View File

@ -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,

View File

@ -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

View File

@ -9,6 +9,7 @@ import 'package:openapi/api.dart';
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**accel** | [**TranscodeHWAccel**](TranscodeHWAccel.md) | | **accel** | [**TranscodeHWAccel**](TranscodeHWAccel.md) | |
**accelDecode** | **bool** | |
**acceptedAudioCodecs** | [**List<AudioCodec>**](AudioCodec.md) | | [default to const []] **acceptedAudioCodecs** | [**List<AudioCodec>**](AudioCodec.md) | | [default to const []]
**acceptedVideoCodecs** | [**List<VideoCodec>**](VideoCodec.md) | | [default to const []] **acceptedVideoCodecs** | [**List<VideoCodec>**](VideoCodec.md) | | [default to const []]
**bframes** | **int** | | **bframes** | **int** | |

View File

@ -14,6 +14,7 @@ class SystemConfigFFmpegDto {
/// Returns a new [SystemConfigFFmpegDto] instance. /// Returns a new [SystemConfigFFmpegDto] instance.
SystemConfigFFmpegDto({ SystemConfigFFmpegDto({
required this.accel, required this.accel,
required this.accelDecode,
this.acceptedAudioCodecs = const [], this.acceptedAudioCodecs = const [],
this.acceptedVideoCodecs = const [], this.acceptedVideoCodecs = const [],
required this.bframes, required this.bframes,
@ -37,6 +38,8 @@ class SystemConfigFFmpegDto {
TranscodeHWAccel accel; TranscodeHWAccel accel;
bool accelDecode;
List<AudioCodec> acceptedAudioCodecs; List<AudioCodec> acceptedAudioCodecs;
List<VideoCodec> acceptedVideoCodecs; List<VideoCodec> acceptedVideoCodecs;
@ -87,6 +90,7 @@ class SystemConfigFFmpegDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigFFmpegDto && bool operator ==(Object other) => identical(this, other) || other is SystemConfigFFmpegDto &&
other.accel == accel && other.accel == accel &&
other.accelDecode == accelDecode &&
_deepEquality.equals(other.acceptedAudioCodecs, acceptedAudioCodecs) && _deepEquality.equals(other.acceptedAudioCodecs, acceptedAudioCodecs) &&
_deepEquality.equals(other.acceptedVideoCodecs, acceptedVideoCodecs) && _deepEquality.equals(other.acceptedVideoCodecs, acceptedVideoCodecs) &&
other.bframes == bframes && other.bframes == bframes &&
@ -111,6 +115,7 @@ class SystemConfigFFmpegDto {
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(accel.hashCode) + (accel.hashCode) +
(accelDecode.hashCode) +
(acceptedAudioCodecs.hashCode) + (acceptedAudioCodecs.hashCode) +
(acceptedVideoCodecs.hashCode) + (acceptedVideoCodecs.hashCode) +
(bframes.hashCode) + (bframes.hashCode) +
@ -132,11 +137,12 @@ class SystemConfigFFmpegDto {
(twoPass.hashCode); (twoPass.hashCode);
@override @override
String toString() => 'SystemConfigFFmpegDto[accel=$accel, acceptedAudioCodecs=$acceptedAudioCodecs, acceptedVideoCodecs=$acceptedVideoCodecs, bframes=$bframes, cqMode=$cqMode, crf=$crf, gopSize=$gopSize, maxBitrate=$maxBitrate, npl=$npl, preferredHwDevice=$preferredHwDevice, preset=$preset, refs=$refs, targetAudioCodec=$targetAudioCodec, targetResolution=$targetResolution, targetVideoCodec=$targetVideoCodec, temporalAQ=$temporalAQ, threads=$threads, tonemap=$tonemap, transcode=$transcode, twoPass=$twoPass]'; String toString() => 'SystemConfigFFmpegDto[accel=$accel, accelDecode=$accelDecode, acceptedAudioCodecs=$acceptedAudioCodecs, acceptedVideoCodecs=$acceptedVideoCodecs, bframes=$bframes, cqMode=$cqMode, crf=$crf, gopSize=$gopSize, maxBitrate=$maxBitrate, npl=$npl, preferredHwDevice=$preferredHwDevice, preset=$preset, refs=$refs, targetAudioCodec=$targetAudioCodec, targetResolution=$targetResolution, targetVideoCodec=$targetVideoCodec, temporalAQ=$temporalAQ, threads=$threads, tonemap=$tonemap, transcode=$transcode, twoPass=$twoPass]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'accel'] = this.accel; json[r'accel'] = this.accel;
json[r'accelDecode'] = this.accelDecode;
json[r'acceptedAudioCodecs'] = this.acceptedAudioCodecs; json[r'acceptedAudioCodecs'] = this.acceptedAudioCodecs;
json[r'acceptedVideoCodecs'] = this.acceptedVideoCodecs; json[r'acceptedVideoCodecs'] = this.acceptedVideoCodecs;
json[r'bframes'] = this.bframes; json[r'bframes'] = this.bframes;
@ -168,6 +174,7 @@ class SystemConfigFFmpegDto {
return SystemConfigFFmpegDto( return SystemConfigFFmpegDto(
accel: TranscodeHWAccel.fromJson(json[r'accel'])!, accel: TranscodeHWAccel.fromJson(json[r'accel'])!,
accelDecode: mapValueOfType<bool>(json, r'accelDecode')!,
acceptedAudioCodecs: AudioCodec.listFromJson(json[r'acceptedAudioCodecs']), acceptedAudioCodecs: AudioCodec.listFromJson(json[r'acceptedAudioCodecs']),
acceptedVideoCodecs: VideoCodec.listFromJson(json[r'acceptedVideoCodecs']), acceptedVideoCodecs: VideoCodec.listFromJson(json[r'acceptedVideoCodecs']),
bframes: mapValueOfType<int>(json, r'bframes')!, bframes: mapValueOfType<int>(json, r'bframes')!,
@ -235,6 +242,7 @@ class SystemConfigFFmpegDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'accel', 'accel',
'accelDecode',
'acceptedAudioCodecs', 'acceptedAudioCodecs',
'acceptedVideoCodecs', 'acceptedVideoCodecs',
'bframes', 'bframes',

View File

@ -21,6 +21,11 @@ void main() {
// TODO // TODO
}); });
// bool accelDecode
test('to test the property `accelDecode`', () async {
// TODO
});
// List<AudioCodec> acceptedAudioCodecs (default value: const []) // List<AudioCodec> acceptedAudioCodecs (default value: const [])
test('to test the property `acceptedAudioCodecs`', () async { test('to test the property `acceptedAudioCodecs`', () async {
// TODO // TODO

View File

@ -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",

View File

@ -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;

View File

@ -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 },

View File

@ -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;

View File

@ -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 () => {

View File

@ -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: {

View File

@ -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: {

View File

@ -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 [];
}
}

View File

@ -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"