mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	feat(server, web): Added TranscodePolicy "Bitrate higher than max bitrate or not in accepted format" (#6479)
* chore: rebase * chore: open api * Add Database-Migration for setting targetCodec as acceptedCodec if it was set by admin * Add TranscodePolicy setting, to only transcode files with a bitrate higher than set max bitrate * Rename enum value of TranscodePolicy * calculate max_bitrate according to "k" and "m" suffix for comparison * remove migration * minor changes * UnitTest for Bitrate Policy * Fix UnitTest * Add missing output options --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									149bc71eba
								
							
						
					
					
						commit
						87c38d1832
					
				
							
								
								
									
										3
									
								
								mobile/openapi/lib/model/transcode_policy.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/lib/model/transcode_policy.dart
									
									
									
										generated
									
									
									
								
							@ -25,6 +25,7 @@ class TranscodePolicy {
 | 
			
		||||
 | 
			
		||||
  static const all = TranscodePolicy._(r'all');
 | 
			
		||||
  static const optimal = TranscodePolicy._(r'optimal');
 | 
			
		||||
  static const bitrate = TranscodePolicy._(r'bitrate');
 | 
			
		||||
  static const required_ = TranscodePolicy._(r'required');
 | 
			
		||||
  static const disabled = TranscodePolicy._(r'disabled');
 | 
			
		||||
 | 
			
		||||
@ -32,6 +33,7 @@ class TranscodePolicy {
 | 
			
		||||
  static const values = <TranscodePolicy>[
 | 
			
		||||
    all,
 | 
			
		||||
    optimal,
 | 
			
		||||
    bitrate,
 | 
			
		||||
    required_,
 | 
			
		||||
    disabled,
 | 
			
		||||
  ];
 | 
			
		||||
@ -74,6 +76,7 @@ class TranscodePolicyTypeTransformer {
 | 
			
		||||
      switch (data) {
 | 
			
		||||
        case r'all': return TranscodePolicy.all;
 | 
			
		||||
        case r'optimal': return TranscodePolicy.optimal;
 | 
			
		||||
        case r'bitrate': return TranscodePolicy.bitrate;
 | 
			
		||||
        case r'required': return TranscodePolicy.required_;
 | 
			
		||||
        case r'disabled': return TranscodePolicy.disabled;
 | 
			
		||||
        default:
 | 
			
		||||
 | 
			
		||||
@ -9923,6 +9923,7 @@
 | 
			
		||||
        "enum": [
 | 
			
		||||
          "all",
 | 
			
		||||
          "optimal",
 | 
			
		||||
          "bitrate",
 | 
			
		||||
          "required",
 | 
			
		||||
          "disabled"
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								open-api/typescript-sdk/client/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								open-api/typescript-sdk/client/api.ts
									
									
									
										generated
									
									
									
								
							@ -4370,6 +4370,7 @@ export type TranscodeHWAccel = typeof TranscodeHWAccel[keyof typeof TranscodeHWA
 | 
			
		||||
export const TranscodePolicy = {
 | 
			
		||||
    All: 'all',
 | 
			
		||||
    Optimal: 'optimal',
 | 
			
		||||
    Bitrate: 'bitrate',
 | 
			
		||||
    Required: 'required',
 | 
			
		||||
    Disabled: 'disabled'
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@ -557,6 +557,37 @@ describe(MediaService.name, () => {
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should transcode when policy Bitrate and bitrate higher than max bitrate', async () => {
 | 
			
		||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStream40Mbps);
 | 
			
		||||
      configMock.load.mockResolvedValue([
 | 
			
		||||
        { key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.BITRATE },
 | 
			
		||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '30M' },
 | 
			
		||||
      ]);
 | 
			
		||||
      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:720,format=yuv420p',
 | 
			
		||||
            '-preset ultrafast',
 | 
			
		||||
            '-crf 23',
 | 
			
		||||
            '-maxrate 30M',
 | 
			
		||||
            '-bufsize 60M',
 | 
			
		||||
          ],
 | 
			
		||||
          twoPass: false,
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not scale resolution if no target resolution', async () => {
 | 
			
		||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
 | 
			
		||||
      configMock.load.mockResolvedValue([
 | 
			
		||||
 | 
			
		||||
@ -264,6 +264,7 @@ export class MediaService {
 | 
			
		||||
    const mainVideoStream = this.getMainStream(videoStreams);
 | 
			
		||||
    const mainAudioStream = this.getMainStream(audioStreams);
 | 
			
		||||
    const containerExtension = format.formatName;
 | 
			
		||||
    const bitrate = format.bitrate;
 | 
			
		||||
    if (!mainVideoStream || !containerExtension) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
@ -275,7 +276,14 @@ export class MediaService {
 | 
			
		||||
 | 
			
		||||
    const { ffmpeg: config } = await this.configCore.getConfig();
 | 
			
		||||
 | 
			
		||||
    const required = this.isTranscodeRequired(asset, mainVideoStream, mainAudioStream, containerExtension, config);
 | 
			
		||||
    const required = this.isTranscodeRequired(
 | 
			
		||||
      asset,
 | 
			
		||||
      mainVideoStream,
 | 
			
		||||
      mainAudioStream,
 | 
			
		||||
      containerExtension,
 | 
			
		||||
      config,
 | 
			
		||||
      bitrate,
 | 
			
		||||
    );
 | 
			
		||||
    if (!required) {
 | 
			
		||||
      if (asset.encodedVideoPath) {
 | 
			
		||||
        this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`);
 | 
			
		||||
@ -326,6 +334,7 @@ export class MediaService {
 | 
			
		||||
    audioStream: AudioStreamInfo | null,
 | 
			
		||||
    containerExtension: string,
 | 
			
		||||
    ffmpegConfig: SystemConfigFFmpegDto,
 | 
			
		||||
    bitrate: number,
 | 
			
		||||
  ): boolean {
 | 
			
		||||
    const isTargetVideoCodec = ffmpegConfig.acceptedVideoCodecs.includes(videoStream.codecName as VideoCodec);
 | 
			
		||||
    const isTargetContainer = ['mov,mp4,m4a,3gp,3g2,mj2', 'mp4', 'mov'].includes(containerExtension);
 | 
			
		||||
@ -342,6 +351,7 @@ export class MediaService {
 | 
			
		||||
    const scalingEnabled = ffmpegConfig.targetResolution !== 'original';
 | 
			
		||||
    const targetRes = Number.parseInt(ffmpegConfig.targetResolution);
 | 
			
		||||
    const isLargerThanTargetRes = scalingEnabled && Math.min(videoStream.height, videoStream.width) > targetRes;
 | 
			
		||||
    const isLargerThanTargetBitrate = bitrate > this.parseBitrateToBps(ffmpegConfig.maxBitrate);
 | 
			
		||||
 | 
			
		||||
    switch (ffmpegConfig.transcode) {
 | 
			
		||||
      case TranscodePolicy.DISABLED:
 | 
			
		||||
@ -356,6 +366,9 @@ export class MediaService {
 | 
			
		||||
      case TranscodePolicy.OPTIMAL:
 | 
			
		||||
        return !allTargetsMatching || isLargerThanTargetRes || videoStream.isHDR;
 | 
			
		||||
 | 
			
		||||
      case TranscodePolicy.BITRATE:
 | 
			
		||||
        return !allTargetsMatching || isLargerThanTargetBitrate || videoStream.isHDR;
 | 
			
		||||
 | 
			
		||||
      default:
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
@ -424,4 +437,20 @@ export class MediaService {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  parseBitrateToBps(bitrateString: string) {
 | 
			
		||||
    const bitrateValue = Number.parseInt(bitrateString);
 | 
			
		||||
 | 
			
		||||
    if (isNaN(bitrateValue)) {
 | 
			
		||||
      return 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (bitrateString.toLowerCase().endsWith('k')) {
 | 
			
		||||
      return bitrateValue * 1000; // Kilobits per second to bits per second
 | 
			
		||||
    } else if (bitrateString.toLowerCase().endsWith('m')) {
 | 
			
		||||
      return bitrateValue * 1000000; // Megabits per second to bits per second
 | 
			
		||||
    } else {
 | 
			
		||||
      return bitrateValue;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,7 @@ export interface VideoFormat {
 | 
			
		||||
  formatName?: string;
 | 
			
		||||
  formatLongName?: string;
 | 
			
		||||
  duration: number;
 | 
			
		||||
  bitrate: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface VideoInfo {
 | 
			
		||||
 | 
			
		||||
@ -108,6 +108,7 @@ export enum SystemConfigKey {
 | 
			
		||||
export enum TranscodePolicy {
 | 
			
		||||
  ALL = 'all',
 | 
			
		||||
  OPTIMAL = 'optimal',
 | 
			
		||||
  BITRATE = 'bitrate',
 | 
			
		||||
  REQUIRED = 'required',
 | 
			
		||||
  DISABLED = 'disabled',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -47,6 +47,7 @@ export class MediaRepository implements IMediaRepository {
 | 
			
		||||
        formatName: results.format.format_name,
 | 
			
		||||
        formatLongName: results.format.format_long_name,
 | 
			
		||||
        duration: results.format.duration || 0,
 | 
			
		||||
        bitrate: results.format.bit_rate ?? 0,
 | 
			
		||||
      },
 | 
			
		||||
      videoStreams: results.streams
 | 
			
		||||
        .filter((stream) => stream.codec_type === 'video')
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										11
									
								
								server/test/fixtures/media.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								server/test/fixtures/media.stub.ts
									
									
									
									
										vendored
									
									
								
							@ -4,6 +4,7 @@ const probeStubDefaultFormat: VideoFormat = {
 | 
			
		||||
  formatName: 'mov,mp4,m4a,3gp,3g2,mj2',
 | 
			
		||||
  formatLongName: 'QuickTime / MOV',
 | 
			
		||||
  duration: 0,
 | 
			
		||||
  bitrate: 0,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const probeStubDefaultVideoStream: VideoStreamInfo[] = [
 | 
			
		||||
@ -87,6 +88,15 @@ export const probeStub = {
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  }),
 | 
			
		||||
  videoStream40Mbps: Object.freeze<VideoInfo>({
 | 
			
		||||
    ...probeStubDefault,
 | 
			
		||||
    format: {
 | 
			
		||||
      formatName: 'mov,mp4,m4a,3gp,3g2,mj2',
 | 
			
		||||
      formatLongName: 'QuickTime / MOV',
 | 
			
		||||
      duration: 0,
 | 
			
		||||
      bitrate: 40000000,
 | 
			
		||||
    },
 | 
			
		||||
  }),
 | 
			
		||||
  videoStreamHDR: Object.freeze<VideoInfo>({
 | 
			
		||||
    ...probeStubDefault,
 | 
			
		||||
    videoStreams: [
 | 
			
		||||
@ -157,6 +167,7 @@ export const probeStub = {
 | 
			
		||||
      formatName: 'matroska,webm',
 | 
			
		||||
      formatLongName: 'Matroska / WebM',
 | 
			
		||||
      duration: 0,
 | 
			
		||||
      bitrate: 0,
 | 
			
		||||
    },
 | 
			
		||||
  }),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -183,6 +183,10 @@
 | 
			
		||||
              value: TranscodePolicy.Optimal,
 | 
			
		||||
              text: 'Videos higher than target resolution or not in an accepted format',
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              value: TranscodePolicy.Bitrate,
 | 
			
		||||
              text: 'Videos higher than max bitrate or not in an accepted format',
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              value: TranscodePolicy.Required,
 | 
			
		||||
              text: 'Only videos not in an accepted format',
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user