1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

feat(all): ffmpeg quality options improvements (#2161)

* feat: change target scaling to resolution in ffmpeg config

* feat(microservices): scale vertical video correctly, only scale if video is larger than target
This commit is contained in:
Zack Pollard 2023-04-04 02:42:53 +01:00 committed by GitHub
parent 9076f3e69e
commit 808d6423be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 65 additions and 35 deletions

Binary file not shown.

View File

@ -16,7 +16,7 @@ import { AssetEntity, AssetType, TranscodePreset } from '@app/infra/entities';
import { Process, Processor } from '@nestjs/bull';
import { Inject, Logger } from '@nestjs/common';
import { Job } from 'bull';
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
import ffmpeg, { FfprobeData, FfprobeStream } from 'fluent-ffmpeg';
import { join } from 'path';
@Processor(QueueName.VIDEO_CONVERSION)
@ -74,22 +74,22 @@ export class VideoTranscodeProcessor {
async runVideoEncode(asset: AssetEntity, savedEncodedPath: string): Promise<void> {
const config = await this.systemConfigService.getConfig();
const videoStream = await this.getVideoStream(asset);
const transcode = await this.needsTranscoding(asset, config.ffmpeg);
const transcode = await this.needsTranscoding(videoStream, config.ffmpeg);
if (transcode) {
//TODO: If video or audio are already the correct format, don't re-encode, copy the stream
return this.runFFMPEGPipeLine(asset, savedEncodedPath);
return this.runFFMPEGPipeLine(asset, videoStream, savedEncodedPath);
}
}
async needsTranscoding(asset: AssetEntity, ffmpegConfig: SystemConfigFFmpegDto): Promise<boolean> {
async needsTranscoding(videoStream: FfprobeStream, ffmpegConfig: SystemConfigFFmpegDto): Promise<boolean> {
switch (ffmpegConfig.transcode) {
case TranscodePreset.ALL:
return true;
case TranscodePreset.REQUIRED:
{
const videoStream = await this.getVideoStream(asset);
if (videoStream.codec_name !== ffmpegConfig.targetVideoCodec) {
return true;
}
@ -97,12 +97,13 @@ export class VideoTranscodeProcessor {
break;
case TranscodePreset.OPTIMAL: {
const videoStream = await this.getVideoStream(asset);
if (videoStream.codec_name !== ffmpegConfig.targetVideoCodec) {
return true;
}
const videoHeightThreshold = 1080;
const config = await this.systemConfigService.getConfig();
const videoHeightThreshold = Number.parseInt(config.ffmpeg.targetResolution);
return !videoStream.height || videoStream.height > videoHeightThreshold;
}
}
@ -125,22 +126,45 @@ export class VideoTranscodeProcessor {
return longestVideoStream;
}
async runFFMPEGPipeLine(asset: AssetEntity, savedEncodedPath: string): Promise<void> {
async runFFMPEGPipeLine(asset: AssetEntity, videoStream: FfprobeStream, savedEncodedPath: string): Promise<void> {
const config = await this.systemConfigService.getConfig();
const ffmpegOptions = [
`-crf ${config.ffmpeg.crf}`,
`-preset ${config.ffmpeg.preset}`,
`-vcodec ${config.ffmpeg.targetVideoCodec}`,
`-acodec ${config.ffmpeg.targetAudioCodec}`,
// Makes a second pass moving the moov atom to the beginning of
// the file for improved playback speed.
`-movflags faststart`,
];
if (!videoStream.height || !videoStream.width) {
this.logger.error('Height or width undefined for video stream');
return;
}
const streamHeight = videoStream.height;
const streamWidth = videoStream.width;
const targetResolution = Number.parseInt(config.ffmpeg.targetResolution);
let scaling = `-2:${targetResolution}`;
const shouldScale = Math.min(streamHeight, streamWidth) > targetResolution;
const videoIsRotated = Math.abs(Number.parseInt(`${videoStream.rotation ?? 0}`)) === 90;
if (streamHeight > streamWidth || videoIsRotated) {
scaling = `${targetResolution}:-2`;
}
if (shouldScale) {
ffmpegOptions.push(`-vf scale=${scaling}`);
}
return new Promise((resolve, reject) => {
ffmpeg(asset.originalPath)
.outputOptions([
`-crf ${config.ffmpeg.crf}`,
`-preset ${config.ffmpeg.preset}`,
`-vcodec ${config.ffmpeg.targetVideoCodec}`,
`-acodec ${config.ffmpeg.targetAudioCodec}`,
`-vf scale=${config.ffmpeg.targetScaling}`,
// Makes a second pass moving the moov atom to the beginning of
// the file for improved playback speed.
`-movflags faststart`,
])
.outputOptions(ffmpegOptions)
.output(savedEncodedPath)
.on('start', () => {
this.logger.log('Start Converting Video');

View File

@ -4644,7 +4644,7 @@
"targetAudioCodec": {
"type": "string"
},
"targetScaling": {
"targetResolution": {
"type": "string"
},
"transcode": {
@ -4661,7 +4661,7 @@
"preset",
"targetVideoCodec",
"targetAudioCodec",
"targetScaling",
"targetResolution",
"transcode"
]
},

View File

@ -15,7 +15,7 @@ export class SystemConfigFFmpegDto {
targetAudioCodec!: string;
@IsString()
targetScaling!: string;
targetResolution!: string;
@IsEnum(TranscodePreset)
transcode!: TranscodePreset;

View File

@ -13,7 +13,7 @@ const defaults: SystemConfig = Object.freeze({
preset: 'ultrafast',
targetVideoCodec: 'h264',
targetAudioCodec: 'aac',
targetScaling: '1280:-2',
targetResolution: '720',
transcode: TranscodePreset.REQUIRED,
},
oauth: {

View File

@ -16,7 +16,7 @@ const updatedConfig = Object.freeze({
crf: 'a new value',
preset: 'ultrafast',
targetAudioCodec: 'aac',
targetScaling: '1280:-2',
targetResolution: '720',
targetVideoCodec: 'h264',
transcode: TranscodePreset.REQUIRED,
},

View File

@ -401,7 +401,7 @@ export const systemConfigStub = {
crf: '23',
preset: 'ultrafast',
targetAudioCodec: 'aac',
targetScaling: '1280:-2',
targetResolution: '720',
targetVideoCodec: 'h264',
transcode: TranscodePreset.REQUIRED,
},

View File

@ -17,7 +17,7 @@ export enum SystemConfigKey {
FFMPEG_PRESET = 'ffmpeg.preset',
FFMPEG_TARGET_VIDEO_CODEC = 'ffmpeg.targetVideoCodec',
FFMPEG_TARGET_AUDIO_CODEC = 'ffmpeg.targetAudioCodec',
FFMPEG_TARGET_SCALING = 'ffmpeg.targetScaling',
FFMPEG_TARGET_RESOLUTION = 'ffmpeg.targetResolution',
FFMPEG_TRANSCODE = 'ffmpeg.transcode',
OAUTH_ENABLED = 'oauth.enabled',
OAUTH_ISSUER_URL = 'oauth.issuerUrl',
@ -45,7 +45,7 @@ export interface SystemConfig {
preset: string;
targetVideoCodec: string;
targetAudioCodec: string;
targetScaling: string;
targetResolution: string;
transcode: TranscodePreset;
};
oauth: {

View File

@ -2034,7 +2034,7 @@ export interface SystemConfigFFmpegDto {
* @type {string}
* @memberof SystemConfigFFmpegDto
*/
'targetScaling': string;
'targetResolution': string;
/**
*
* @type {string}

View File

@ -113,12 +113,18 @@
isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="SCALING (-vf scale=)"
bind:value={ffmpegConfig.targetScaling}
required={true}
isEdited={!(ffmpegConfig.targetScaling == savedConfig.targetScaling)}
<SettingSelect
label="TARGET RESOLUTION"
bind:value={ffmpegConfig.targetResolution}
options={[
{ value: '2160', text: '4k' },
{ value: '1440', text: '1440p' },
{ value: '1080', text: '1080p' },
{ value: '720', text: '720p' },
{ value: '480', text: '480p' }
]}
name="resolution"
isEdited={!(ffmpegConfig.targetResolution == savedConfig.targetResolution)}
/>
<SettingSelect
@ -129,7 +135,7 @@
{ value: SystemConfigFFmpegDtoTranscodeEnum.All, text: 'All videos' },
{
value: SystemConfigFFmpegDtoTranscodeEnum.Optimal,
text: 'Videos higher than 1080p or not in the desired format'
text: 'Videos higher than target resolution or not in the desired format'
},
{
value: SystemConfigFFmpegDtoTranscodeEnum.Required,