mirror of
https://github.com/KevinMidboe/immich.git
synced 2026-01-07 09:45:50 +00:00
refactor(server): modularize getFfmpegOptions (#3138)
* refactored `getFfmpegOptions` refactor transcoding, make separate service * fixed enum casing * use `Logger` instead of `console.log` * review suggestions * use enum for `getHandler` * fixed formatting * Update server/src/domain/media/media.util.ts Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * Update server/src/domain/media/media.util.ts Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * More specific imports, renamed codec classes * simplified code * removed unused import * added tests * added base implementation for bitrate and threads --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { AssetEntity, AssetType, TranscodePreset } from '@app/infra/entities';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { AssetEntity, AssetType, TranscodePolicy, VideoCodec } from '@app/infra/entities';
|
||||
import { Inject, Injectable, Logger, UnsupportedMediaTypeException } from '@nestjs/common';
|
||||
import { join } from 'path';
|
||||
import { IAssetRepository, WithoutProperty } from '../asset';
|
||||
import { usePagination } from '../domain.util';
|
||||
@@ -9,6 +9,7 @@ import { ISystemConfigRepository, SystemConfigFFmpegDto } from '../system-config
|
||||
import { SystemConfigCore } from '../system-config/system-config.core';
|
||||
import { JPEG_THUMBNAIL_SIZE, WEBP_THUMBNAIL_SIZE } from './media.constant';
|
||||
import { AudioStreamInfo, IMediaRepository, VideoStreamInfo } from './media.repository';
|
||||
import { H264Config, HEVCConfig, VP9Config } from './media.util';
|
||||
|
||||
@Injectable()
|
||||
export class MediaService {
|
||||
@@ -82,7 +83,7 @@ export class MediaService {
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleGenerateWepbThumbnail({ id }: IEntityJob) {
|
||||
async handleGenerateWebpThumbnail({ id }: IEntityJob) {
|
||||
const [asset] = await this.assetRepository.getByIds([id]);
|
||||
if (!asset || !asset.resizePath) {
|
||||
return false;
|
||||
@@ -152,11 +153,16 @@ export class MediaService {
|
||||
return false;
|
||||
}
|
||||
|
||||
const outputOptions = this.getFfmpegOptions(mainVideoStream, config);
|
||||
const twoPass = this.eligibleForTwoPass(config);
|
||||
let transcodeOptions;
|
||||
try {
|
||||
transcodeOptions = this.getCodecConfig(config).getOptions(mainVideoStream);
|
||||
} catch (err) {
|
||||
this.logger.error(`An error occurred while configuring transcoding options: ${err}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logger.log(`Start encoding video ${asset.id} ${outputOptions}`);
|
||||
await this.mediaRepository.transcode(input, output, { outputOptions, twoPass });
|
||||
this.logger.log(`Start encoding video ${asset.id} ${JSON.stringify(transcodeOptions)}`);
|
||||
await this.mediaRepository.transcode(input, output, transcodeOptions);
|
||||
|
||||
this.logger.log(`Encoding success ${asset.id}`);
|
||||
|
||||
@@ -199,16 +205,16 @@ export class MediaService {
|
||||
const isLargerThanTargetRes = scalingEnabled && Math.min(videoStream.height, videoStream.width) > targetRes;
|
||||
|
||||
switch (ffmpegConfig.transcode) {
|
||||
case TranscodePreset.DISABLED:
|
||||
case TranscodePolicy.DISABLED:
|
||||
return false;
|
||||
|
||||
case TranscodePreset.ALL:
|
||||
case TranscodePolicy.ALL:
|
||||
return true;
|
||||
|
||||
case TranscodePreset.REQUIRED:
|
||||
case TranscodePolicy.REQUIRED:
|
||||
return !allTargetsMatching;
|
||||
|
||||
case TranscodePreset.OPTIMAL:
|
||||
case TranscodePolicy.OPTIMAL:
|
||||
return !allTargetsMatching || isLargerThanTargetRes;
|
||||
|
||||
default:
|
||||
@@ -216,99 +222,16 @@ export class MediaService {
|
||||
}
|
||||
}
|
||||
|
||||
private getFfmpegOptions(stream: VideoStreamInfo, ffmpeg: SystemConfigFFmpegDto) {
|
||||
const options = [
|
||||
`-vcodec ${ffmpeg.targetVideoCodec}`,
|
||||
`-acodec ${ffmpeg.targetAudioCodec}`,
|
||||
// Makes a second pass moving the moov atom to the beginning of
|
||||
// the file for improved playback speed.
|
||||
'-movflags faststart',
|
||||
'-fps_mode passthrough',
|
||||
];
|
||||
|
||||
// video dimensions
|
||||
const videoIsRotated = Math.abs(stream.rotation) === 90;
|
||||
const scalingEnabled = ffmpeg.targetResolution !== 'original';
|
||||
const targetResolution = Number.parseInt(ffmpeg.targetResolution);
|
||||
const isVideoVertical = stream.height > stream.width || videoIsRotated;
|
||||
const scaling = isVideoVertical ? `${targetResolution}:-2` : `-2:${targetResolution}`;
|
||||
const shouldScale = scalingEnabled && Math.min(stream.height, stream.width) > targetResolution;
|
||||
|
||||
// video codec
|
||||
const isVP9 = ffmpeg.targetVideoCodec === 'vp9';
|
||||
const isH264 = ffmpeg.targetVideoCodec === 'h264';
|
||||
const isH265 = ffmpeg.targetVideoCodec === 'hevc';
|
||||
|
||||
// transcode efficiency
|
||||
const limitThreads = ffmpeg.threads > 0;
|
||||
const maxBitrateValue = Number.parseInt(ffmpeg.maxBitrate) || 0;
|
||||
const constrainMaximumBitrate = maxBitrateValue > 0;
|
||||
const bitrateUnit = ffmpeg.maxBitrate.trim().substring(maxBitrateValue.toString().length); // use inputted unit if provided
|
||||
|
||||
if (shouldScale) {
|
||||
options.push(`-vf scale=${scaling}`);
|
||||
private getCodecConfig(config: SystemConfigFFmpegDto) {
|
||||
switch (config.targetVideoCodec) {
|
||||
case VideoCodec.H264:
|
||||
return new H264Config(config);
|
||||
case VideoCodec.HEVC:
|
||||
return new HEVCConfig(config);
|
||||
case VideoCodec.VP9:
|
||||
return new VP9Config(config);
|
||||
default:
|
||||
throw new UnsupportedMediaTypeException(`Codec '${config.targetVideoCodec}' is unsupported`);
|
||||
}
|
||||
|
||||
if (isH264 || isH265) {
|
||||
options.push(`-preset ${ffmpeg.preset}`);
|
||||
}
|
||||
|
||||
if (isVP9) {
|
||||
// vp9 doesn't have presets, but does have a similar setting -cpu-used, from 0-5, 0 being the slowest
|
||||
const presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast'];
|
||||
const speed = Math.min(presets.indexOf(ffmpeg.preset), 5); // values over 5 require realtime mode, which is its own can of worms since it overrides -crf and -threads
|
||||
if (speed >= 0) {
|
||||
options.push(`-cpu-used ${speed}`);
|
||||
}
|
||||
options.push('-row-mt 1'); // better multithreading
|
||||
}
|
||||
|
||||
if (limitThreads) {
|
||||
options.push(`-threads ${ffmpeg.threads}`);
|
||||
|
||||
// x264 and x265 handle threads differently than one might expect
|
||||
// https://x265.readthedocs.io/en/latest/cli.html#cmdoption-pools
|
||||
if (isH264 || isH265) {
|
||||
options.push(`-${isH265 ? 'x265' : 'x264'}-params "pools=none"`);
|
||||
options.push(`-${isH265 ? 'x265' : 'x264'}-params "frame-threads=${ffmpeg.threads}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// two-pass mode for x264/x265 uses bitrate ranges, so it requires a max bitrate from which to derive a target and min bitrate
|
||||
if (constrainMaximumBitrate && ffmpeg.twoPass) {
|
||||
const targetBitrateValue = Math.ceil(maxBitrateValue / 1.45); // recommended by https://developers.google.com/media/vp9/settings/vod
|
||||
const minBitrateValue = targetBitrateValue / 2;
|
||||
|
||||
options.push(`-b:v ${targetBitrateValue}${bitrateUnit}`);
|
||||
options.push(`-minrate ${minBitrateValue}${bitrateUnit}`);
|
||||
options.push(`-maxrate ${maxBitrateValue}${bitrateUnit}`);
|
||||
} else if (constrainMaximumBitrate || isVP9) {
|
||||
// for vp9, these flags work for both one-pass and two-pass
|
||||
options.push(`-crf ${ffmpeg.crf}`);
|
||||
if (isVP9) {
|
||||
options.push(`-b:v ${maxBitrateValue}${bitrateUnit}`);
|
||||
} else {
|
||||
options.push(`-maxrate ${maxBitrateValue}${bitrateUnit}`);
|
||||
// -bufsize is the peak possible bitrate at any moment, while -maxrate is the max rolling average bitrate
|
||||
// needed for -maxrate to be enforced
|
||||
options.push(`-bufsize ${maxBitrateValue * 2}${bitrateUnit}`);
|
||||
}
|
||||
} else {
|
||||
options.push(`-crf ${ffmpeg.crf}`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private eligibleForTwoPass(ffmpeg: SystemConfigFFmpegDto) {
|
||||
if (!ffmpeg.twoPass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isVP9 = ffmpeg.targetVideoCodec === 'vp9';
|
||||
const maxBitrateValue = Number.parseInt(ffmpeg.maxBitrate) || 0;
|
||||
const constrainMaximumBitrate = maxBitrateValue > 0;
|
||||
|
||||
return constrainMaximumBitrate || isVP9;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user