mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:29:32 -05:00 
			
		
		
		
	fix(server): scale transcoded videos if dimensions are odd (#6461)
scale if odd resolution
This commit is contained in:
		
							parent
							
								
									b98d1bf9d3
								
							
						
					
					
						commit
						9a2fa21b28
					
				@ -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 () => {
 | 
					    it('should transcode when audio doesnt match target', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.audioStreamMp3);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.audioStreamMp3);
 | 
				
			||||||
      configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.OPTIMAL }]);
 | 
					      configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.OPTIMAL }]);
 | 
				
			||||||
 | 
				
			|||||||
@ -122,15 +122,24 @@ class BaseConfig implements VideoCodecSWConfig {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getTargetResolution(videoStream: VideoStreamInfo) {
 | 
					  getTargetResolution(videoStream: VideoStreamInfo) {
 | 
				
			||||||
 | 
					    let target;
 | 
				
			||||||
    if (this.config.targetResolution === 'original') {
 | 
					    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) {
 | 
					  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) {
 | 
					  shouldToneMap(videoStream: VideoStreamInfo) {
 | 
				
			||||||
@ -146,7 +155,10 @@ class BaseConfig implements VideoCodecSWConfig {
 | 
				
			|||||||
  getSize(videoStream: VideoStreamInfo) {
 | 
					  getSize(videoStream: VideoStreamInfo) {
 | 
				
			||||||
    const smaller = this.getTargetResolution(videoStream);
 | 
					    const smaller = this.getTargetResolution(videoStream);
 | 
				
			||||||
    const factor = Math.max(videoStream.height, videoStream.width) / Math.min(videoStream.height, videoStream.width);
 | 
					    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 };
 | 
					    return this.isVideoVertical(videoStream) ? { width: smaller, height: larger } : { width: larger, height: smaller };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										30
									
								
								server/test/fixtures/media.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										30
									
								
								server/test/fixtures/media.stub.ts
									
									
									
									
										vendored
									
									
								
							@ -117,6 +117,36 @@ export const probeStub = {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
 | 
					  videoStreamOddHeight: Object.freeze<VideoInfo>({
 | 
				
			||||||
 | 
					    ...probeStubDefault,
 | 
				
			||||||
 | 
					    videoStreams: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        index: 0,
 | 
				
			||||||
 | 
					        height: 355,
 | 
				
			||||||
 | 
					        width: 1586,
 | 
				
			||||||
 | 
					        codecName: 'h264',
 | 
				
			||||||
 | 
					        codecType: 'video',
 | 
				
			||||||
 | 
					        frameCount: 100,
 | 
				
			||||||
 | 
					        rotation: 0,
 | 
				
			||||||
 | 
					        isHDR: false,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					  videoStreamOddWidth: Object.freeze<VideoInfo>({
 | 
				
			||||||
 | 
					    ...probeStubDefault,
 | 
				
			||||||
 | 
					    videoStreams: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        index: 0,
 | 
				
			||||||
 | 
					        height: 1586,
 | 
				
			||||||
 | 
					        width: 355,
 | 
				
			||||||
 | 
					        codecName: 'h264',
 | 
				
			||||||
 | 
					        codecType: 'video',
 | 
				
			||||||
 | 
					        frameCount: 100,
 | 
				
			||||||
 | 
					        rotation: 0,
 | 
				
			||||||
 | 
					        isHDR: false,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
  audioStreamMp3: Object.freeze<VideoInfo>({
 | 
					  audioStreamMp3: Object.freeze<VideoInfo>({
 | 
				
			||||||
    ...probeStubDefault,
 | 
					    ...probeStubDefault,
 | 
				
			||||||
    audioStreams: [{ index: 1, codecType: 'audio', codecName: 'aac', frameCount: 100 }],
 | 
					    audioStreams: [{ index: 1, codecType: 'audio', codecName: 'aac', frameCount: 100 }],
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user