mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -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 () => {
 | 
			
		||||
      mediaMock.probe.mockResolvedValue(probeStub.audioStreamMp3);
 | 
			
		||||
      configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.OPTIMAL }]);
 | 
			
		||||
 | 
			
		||||
@ -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 };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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>({
 | 
			
		||||
    ...probeStubDefault,
 | 
			
		||||
    audioStreams: [{ index: 1, codecType: 'audio', codecName: 'aac', frameCount: 100 }],
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user