diff --git a/server/src/domain/media/media.service.spec.ts b/server/src/domain/media/media.service.spec.ts index abc4fab583..abd9d8c752 100644 --- a/server/src/domain/media/media.service.spec.ts +++ b/server/src/domain/media/media.service.spec.ts @@ -600,6 +600,66 @@ describe(MediaService.name, () => { ); }); + it('should always scale video if height is uneven', async () => { + mediaMock.probe.mockResolvedValue(probeStub.videoStreamOddHeight); + configMock.load.mockResolvedValue([ + { key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.ALL }, + { key: SystemConfigKey.FFMPEG_TARGET_RESOLUTION, value: 'original' }, + ]); + 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: [ + '-c:v h264', + '-c:a aac', + '-movflags faststart', + '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', + '-v verbose', + `-vf scale=-2:354,format=yuv420p`, + '-preset ultrafast', + '-crf 23', + ], + twoPass: false, + }, + ); + }); + + it('should always scale video if width is uneven', async () => { + mediaMock.probe.mockResolvedValue(probeStub.videoStreamOddWidth); + configMock.load.mockResolvedValue([ + { key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.ALL }, + { key: SystemConfigKey.FFMPEG_TARGET_RESOLUTION, value: 'original' }, + ]); + 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: [ + '-c:v h264', + '-c:a aac', + '-movflags faststart', + '-fps_mode passthrough', + '-map 0:0', + '-map 0:1', + '-v verbose', + `-vf scale=354:-2,format=yuv420p`, + '-preset ultrafast', + '-crf 23', + ], + twoPass: false, + }, + ); + }); + it('should transcode when audio doesnt match target', async () => { mediaMock.probe.mockResolvedValue(probeStub.audioStreamMp3); configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.OPTIMAL }]); diff --git a/server/src/domain/media/media.util.ts b/server/src/domain/media/media.util.ts index 38e68835b1..4b6b417286 100644 --- a/server/src/domain/media/media.util.ts +++ b/server/src/domain/media/media.util.ts @@ -122,15 +122,24 @@ class BaseConfig implements VideoCodecSWConfig { } getTargetResolution(videoStream: VideoStreamInfo) { + let target; if (this.config.targetResolution === 'original') { - return Math.min(videoStream.height, videoStream.width); + target = Math.min(videoStream.height, videoStream.width); + } else { + target = Number.parseInt(this.config.targetResolution); } - return Number.parseInt(this.config.targetResolution); + if (target % 2 !== 0) { + target -= 1; + } + + return target; } shouldScale(videoStream: VideoStreamInfo) { - return Math.min(videoStream.height, videoStream.width) > this.getTargetResolution(videoStream); + const oddDimensions = videoStream.height % 2 !== 0 || videoStream.width % 2 !== 0; + const largerThanTarget = Math.min(videoStream.height, videoStream.width) > this.getTargetResolution(videoStream); + return oddDimensions || largerThanTarget; } shouldToneMap(videoStream: VideoStreamInfo) { @@ -146,7 +155,10 @@ class BaseConfig implements VideoCodecSWConfig { getSize(videoStream: VideoStreamInfo) { const smaller = this.getTargetResolution(videoStream); const factor = Math.max(videoStream.height, videoStream.width) / Math.min(videoStream.height, videoStream.width); - const larger = Math.round(smaller * factor); + let larger = Math.round(smaller * factor); + if (larger % 2 !== 0) { + larger -= 1; + } return this.isVideoVertical(videoStream) ? { width: smaller, height: larger } : { width: larger, height: smaller }; } diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts index 8cde51cadc..969e857211 100644 --- a/server/test/fixtures/media.stub.ts +++ b/server/test/fixtures/media.stub.ts @@ -117,6 +117,36 @@ export const probeStub = { }, ], }), + videoStreamOddHeight: Object.freeze({ + ...probeStubDefault, + videoStreams: [ + { + index: 0, + height: 355, + width: 1586, + codecName: 'h264', + codecType: 'video', + frameCount: 100, + rotation: 0, + isHDR: false, + }, + ], + }), + videoStreamOddWidth: Object.freeze({ + ...probeStubDefault, + videoStreams: [ + { + index: 0, + height: 1586, + width: 355, + codecName: 'h264', + codecType: 'video', + frameCount: 100, + rotation: 0, + isHDR: false, + }, + ], + }), audioStreamMp3: Object.freeze({ ...probeStubDefault, audioStreams: [{ index: 1, codecType: 'audio', codecName: 'aac', frameCount: 100 }],