diff --git a/i18n/en.json b/i18n/en.json index 43f325a34a..4de12c5cc7 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -411,7 +411,7 @@ "transcoding_tone_mapping": "Tone-mapping", "transcoding_tone_mapping_description": "Attempts to preserve the appearance of HDR videos when converted to SDR. Each algorithm makes different tradeoffs for color, detail and brightness. Hable preserves detail, Mobius preserves color, and Reinhard preserves brightness.", "transcoding_transcode_policy": "Transcode policy", - "transcoding_transcode_policy_description": "Policy for when a video should be transcoded. HDR videos will always be transcoded (except if transcoding is disabled).", + "transcoding_transcode_policy_description": "Policy for when a video should be transcoded. HDR videos and videos with a pixel format other than YUV 4:2:0 will always be transcoded (except if transcoding is disabled).", "transcoding_two_pass_encoding": "Two-pass encoding", "transcoding_two_pass_encoding_setting_description": "Transcode in two passes to produce better encoded videos. When max bitrate is enabled (required for it to work with H.264 and HEVC), this mode uses a bitrate range based on the max bitrate and ignores CRF. For VP9, CRF can be used if max bitrate is disabled.", "transcoding_video_codec": "Video codec", diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index fc825fb273..12440fb263 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -2015,6 +2015,13 @@ describe(MediaService.name, () => { ); }); + it('should not transcode when policy bitrate and bitrate lower than max bitrate', async () => { + mocks.media.probe.mockResolvedValue(probeStub.videoStream40Mbps); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Bitrate, maxBitrate: '50M' } }); + await sut.handleVideoConversion({ id: 'video-id' }); + expect(mocks.media.transcode).not.toHaveBeenCalled(); + }); + it('should transcode when policy bitrate and bitrate higher than max bitrate', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream40Mbps); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Bitrate, maxBitrate: '30M' } }); @@ -2030,19 +2037,18 @@ describe(MediaService.name, () => { ); }); - it('should transcode when max bitrate is not a number', async () => { + it('should not transcode when max bitrate is not a number', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream40Mbps); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Bitrate, maxBitrate: 'foo' } }); await sut.handleVideoConversion({ id: 'video-id' }); - expect(mocks.media.transcode).toHaveBeenCalledWith( - '/original/path.ext', - expect.any(String), - expect.objectContaining({ - inputOptions: expect.any(Array), - outputOptions: expect.any(Array), - twoPass: false, - }), - ); + expect(mocks.media.transcode).not.toHaveBeenCalled(); + }); + + it('should not transcode when max bitrate is 0', async () => { + mocks.media.probe.mockResolvedValue(probeStub.videoStream40Mbps); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Bitrate, maxBitrate: '0' } }); + await sut.handleVideoConversion({ id: 'video-id' }); + expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should not scale resolution if no target resolution', async () => { diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 3555d7d108..e49b8c10af 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -717,7 +717,8 @@ export class MediaService extends BaseService { const scalingEnabled = ffmpegConfig.targetResolution !== 'original'; const targetRes = Number.parseInt(ffmpegConfig.targetResolution); const isLargerThanTargetRes = scalingEnabled && Math.min(stream.height, stream.width) > targetRes; - const isLargerThanTargetBitrate = stream.bitrate > this.parseBitrateToBps(ffmpegConfig.maxBitrate); + const maxBitrate = this.parseBitrateToBps(ffmpegConfig.maxBitrate); + const isLargerThanTargetBitrate = maxBitrate > 0 && stream.bitrate > maxBitrate; const isTargetVideoCodec = ffmpegConfig.acceptedVideoCodecs.includes(stream.codecName as VideoCodec); const isRequired = !isTargetVideoCodec || !stream.pixelFormat.endsWith('420p'); @@ -769,6 +770,7 @@ export class MediaService extends BaseService { const bitrateValue = Number.parseInt(bitrateString); if (Number.isNaN(bitrateValue)) { + this.logger.log(`Maximum bitrate '${bitrateString} is not a number and will be ignored.`); return 0; } diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts index 727f5ae7cf..f80ad70c8f 100644 --- a/server/test/fixtures/media.stub.ts +++ b/server/test/fixtures/media.stub.ts @@ -112,7 +112,7 @@ export const probeStub = { }), videoStream40Mbps: Object.freeze({ ...probeStubDefault, - videoStreams: [{ ...probeStubDefaultVideoStream[0], bitrate: 40_000_000 }], + videoStreams: [{ ...probeStubDefaultVideoStream[0], bitrate: 40_000_000, codecName: 'h264' }], }), videoStreamMTS: Object.freeze({ ...probeStubDefault,