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 76 additions and 46 deletions

View File

@ -12,7 +12,7 @@ Name | Type | Description | Notes
**preset** | **String** | | **preset** | **String** | |
**targetVideoCodec** | **String** | | **targetVideoCodec** | **String** | |
**targetAudioCodec** | **String** | | **targetAudioCodec** | **String** | |
**targetScaling** | **String** | | **targetResolution** | **String** | |
**transcode** | **String** | | **transcode** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -17,7 +17,7 @@ class SystemConfigFFmpegDto {
required this.preset, required this.preset,
required this.targetVideoCodec, required this.targetVideoCodec,
required this.targetAudioCodec, required this.targetAudioCodec,
required this.targetScaling, required this.targetResolution,
required this.transcode, required this.transcode,
}); });
@ -29,7 +29,7 @@ class SystemConfigFFmpegDto {
String targetAudioCodec; String targetAudioCodec;
String targetScaling; String targetResolution;
SystemConfigFFmpegDtoTranscodeEnum transcode; SystemConfigFFmpegDtoTranscodeEnum transcode;
@ -39,7 +39,7 @@ class SystemConfigFFmpegDto {
other.preset == preset && other.preset == preset &&
other.targetVideoCodec == targetVideoCodec && other.targetVideoCodec == targetVideoCodec &&
other.targetAudioCodec == targetAudioCodec && other.targetAudioCodec == targetAudioCodec &&
other.targetScaling == targetScaling && other.targetResolution == targetResolution &&
other.transcode == transcode; other.transcode == transcode;
@override @override
@ -49,11 +49,11 @@ class SystemConfigFFmpegDto {
(preset.hashCode) + (preset.hashCode) +
(targetVideoCodec.hashCode) + (targetVideoCodec.hashCode) +
(targetAudioCodec.hashCode) + (targetAudioCodec.hashCode) +
(targetScaling.hashCode) + (targetResolution.hashCode) +
(transcode.hashCode); (transcode.hashCode);
@override @override
String toString() => 'SystemConfigFFmpegDto[crf=$crf, preset=$preset, targetVideoCodec=$targetVideoCodec, targetAudioCodec=$targetAudioCodec, targetScaling=$targetScaling, transcode=$transcode]'; String toString() => 'SystemConfigFFmpegDto[crf=$crf, preset=$preset, targetVideoCodec=$targetVideoCodec, targetAudioCodec=$targetAudioCodec, targetResolution=$targetResolution, transcode=$transcode]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -61,7 +61,7 @@ class SystemConfigFFmpegDto {
json[r'preset'] = this.preset; json[r'preset'] = this.preset;
json[r'targetVideoCodec'] = this.targetVideoCodec; json[r'targetVideoCodec'] = this.targetVideoCodec;
json[r'targetAudioCodec'] = this.targetAudioCodec; json[r'targetAudioCodec'] = this.targetAudioCodec;
json[r'targetScaling'] = this.targetScaling; json[r'targetResolution'] = this.targetResolution;
json[r'transcode'] = this.transcode; json[r'transcode'] = this.transcode;
return json; return json;
} }
@ -89,7 +89,7 @@ class SystemConfigFFmpegDto {
preset: mapValueOfType<String>(json, r'preset')!, preset: mapValueOfType<String>(json, r'preset')!,
targetVideoCodec: mapValueOfType<String>(json, r'targetVideoCodec')!, targetVideoCodec: mapValueOfType<String>(json, r'targetVideoCodec')!,
targetAudioCodec: mapValueOfType<String>(json, r'targetAudioCodec')!, targetAudioCodec: mapValueOfType<String>(json, r'targetAudioCodec')!,
targetScaling: mapValueOfType<String>(json, r'targetScaling')!, targetResolution: mapValueOfType<String>(json, r'targetResolution')!,
transcode: SystemConfigFFmpegDtoTranscodeEnum.fromJson(json[r'transcode'])!, transcode: SystemConfigFFmpegDtoTranscodeEnum.fromJson(json[r'transcode'])!,
); );
} }
@ -144,7 +144,7 @@ class SystemConfigFFmpegDto {
'preset', 'preset',
'targetVideoCodec', 'targetVideoCodec',
'targetAudioCodec', 'targetAudioCodec',
'targetScaling', 'targetResolution',
'transcode', 'transcode',
}; };
} }

View File

@ -36,8 +36,8 @@ void main() {
// TODO // TODO
}); });
// String targetScaling // String targetResolution
test('to test the property `targetScaling`', () async { test('to test the property `targetResolution`', () async {
// TODO // TODO
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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