mirror of
https://github.com/immich-app/immich.git
synced 2025-12-25 09:14:58 +03: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:
@@ -1335,6 +1335,51 @@ describe(MediaService.name, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should use hardware decoding for nvenc if enabled', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: { accel: TranscodeHWAccel.NVENC, accelDecode: true },
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
'/original/path.ext',
|
||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||
{
|
||||
inputOptions: expect.arrayContaining([
|
||||
'-hwaccel cuda',
|
||||
'-hwaccel_output_format cuda',
|
||||
'-noautorotate',
|
||||
'-threads 1',
|
||||
]),
|
||||
outputOptions: expect.arrayContaining([expect.stringContaining('scale_cuda=-2:720:format=nv12')]),
|
||||
twoPass: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should use hardware tone-mapping for nvenc if hardware decoding is enabled and should tone map', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: { accel: TranscodeHWAccel.NVENC, accelDecode: true },
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
'/original/path.ext',
|
||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||
{
|
||||
inputOptions: expect.arrayContaining(['-hwaccel cuda', '-hwaccel_output_format cuda']),
|
||||
outputOptions: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
'tonemap_cuda=desat=0:matrix=bt709:primaries=bt709:range=pc:tonemap=hable:transfer=bt709:format=nv12',
|
||||
),
|
||||
]),
|
||||
twoPass: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should set options for qsv', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
@@ -1633,8 +1678,9 @@ describe(MediaService.name, () => {
|
||||
|
||||
it('should set options for rkmpp', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP } });
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
@@ -1663,10 +1709,12 @@ describe(MediaService.name, () => {
|
||||
|
||||
it('should set vbr options for rkmpp when max bitrate is enabled', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: {
|
||||
accel: TranscodeHWAccel.RKMPP,
|
||||
accelDecode: true,
|
||||
maxBitrate: '10000k',
|
||||
targetVideoCodec: VideoCodec.HEVC,
|
||||
},
|
||||
@@ -1686,9 +1734,10 @@ describe(MediaService.name, () => {
|
||||
|
||||
it('should set cqp options for rkmpp when max bitrate is disabled', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: { accel: TranscodeHWAccel.RKMPP, crf: 30, maxBitrate: '0' },
|
||||
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
@@ -1707,7 +1756,9 @@ describe(MediaService.name, () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, crf: 30, maxBitrate: '0' } });
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
@@ -1724,6 +1775,54 @@ describe(MediaService.name, () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should use software decoding and tone-mapping if hardware decoding is disabled', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: false, crf: 30, maxBitrate: '0' },
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
'/original/path.ext',
|
||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||
{
|
||||
inputOptions: [],
|
||||
outputOptions: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
'zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p',
|
||||
),
|
||||
]),
|
||||
twoPass: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should use software decoding and tone-mapping if opencl is not available', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => false, isCharacterDevice: () => false });
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
'/original/path.ext',
|
||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||
{
|
||||
inputOptions: [],
|
||||
outputOptions: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
'zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p',
|
||||
),
|
||||
]),
|
||||
twoPass: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should tonemap when policy is required and video is hdr', async () => {
|
||||
|
||||
@@ -36,9 +36,11 @@ import {
|
||||
AV1Config,
|
||||
H264Config,
|
||||
HEVCConfig,
|
||||
NVENCConfig,
|
||||
NvencHwDecodeConfig,
|
||||
NvencSwDecodeConfig,
|
||||
QSVConfig,
|
||||
RKMPPConfig,
|
||||
RkmppHwDecodeConfig,
|
||||
RkmppSwDecodeConfig,
|
||||
ThumbnailConfig,
|
||||
VAAPIConfig,
|
||||
VP9Config,
|
||||
@@ -360,8 +362,7 @@ export class MediaService {
|
||||
`Error occurred during transcoding. Retrying with ${config.accel.toUpperCase()} acceleration disabled.`,
|
||||
);
|
||||
}
|
||||
config.accel = TranscodeHWAccel.DISABLED;
|
||||
transcodeOptions = await this.getCodecConfig(config).then((c) =>
|
||||
transcodeOptions = await this.getCodecConfig({ ...config, accel: TranscodeHWAccel.DISABLED }).then((c) =>
|
||||
c.getOptions(target, mainVideoStream, mainAudioStream),
|
||||
);
|
||||
await this.mediaRepository.transcode(input, output, transcodeOptions);
|
||||
@@ -494,7 +495,7 @@ export class MediaService {
|
||||
let handler: VideoCodecHWConfig;
|
||||
switch (config.accel) {
|
||||
case TranscodeHWAccel.NVENC: {
|
||||
handler = new NVENCConfig(config);
|
||||
handler = config.accelDecode ? new NvencHwDecodeConfig(config) : new NvencSwDecodeConfig(config);
|
||||
break;
|
||||
}
|
||||
case TranscodeHWAccel.QSV: {
|
||||
@@ -506,7 +507,10 @@ export class MediaService {
|
||||
break;
|
||||
}
|
||||
case TranscodeHWAccel.RKMPP: {
|
||||
handler = new RKMPPConfig(config, await this.getDevices(), await this.hasOpenCL());
|
||||
handler =
|
||||
config.accelDecode && (await this.hasOpenCL())
|
||||
? new RkmppHwDecodeConfig(config, await this.getDevices())
|
||||
: new RkmppSwDecodeConfig(config, await this.getDevices());
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -66,6 +66,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||
preferredHwDevice: 'auto',
|
||||
transcode: TranscodePolicy.REQUIRED,
|
||||
accel: TranscodeHWAccel.DISABLED,
|
||||
accelDecode: false,
|
||||
tonemap: ToneMapping.HABLE,
|
||||
},
|
||||
logging: {
|
||||
|
||||
Reference in New Issue
Block a user