feat(all): transcoding improvements (#2171)
* test: rename some fixtures and add text for vertical video conversion * feat: transcode video asset when audio or container don't match target * chore: add niceness to the ffmpeg command to allow other processes to be prioritised * chore: change video conversion queue to one concurrency * feat: add transcode disabled preset to completely turn off transcoding * linter * Change log level and remove unused await * opps forgot to save * better logging --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import { IAssetJob, IBaseJob, IJobRepository, JobName } from '../job';
|
||||
import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
|
||||
import { ISystemConfigRepository, SystemConfigFFmpegDto } from '../system-config';
|
||||
import { SystemConfigCore } from '../system-config/system-config.core';
|
||||
import { IMediaRepository, VideoStreamInfo } from './media.repository';
|
||||
import { AudioStreamInfo, IMediaRepository, VideoStreamInfo } from './media.repository';
|
||||
|
||||
@Injectable()
|
||||
export class MediaService {
|
||||
@@ -127,23 +127,27 @@ export class MediaService {
|
||||
const output = join(outputFolder, `${asset.id}.mp4`);
|
||||
this.storageRepository.mkdirSync(outputFolder);
|
||||
|
||||
const { streams } = await this.mediaRepository.probe(input);
|
||||
const stream = await this.getLongestStream(streams);
|
||||
if (!stream) {
|
||||
const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input);
|
||||
const mainVideoStream = this.getMainVideoStream(videoStreams);
|
||||
const mainAudioStream = this.getMainAudioStream(audioStreams);
|
||||
const containerExtension = format.formatName;
|
||||
if (!mainVideoStream || !mainAudioStream || !containerExtension) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { ffmpeg: config } = await this.configCore.getConfig();
|
||||
|
||||
const required = this.isTranscodeRequired(stream, config);
|
||||
const required = this.isTranscodeRequired(mainVideoStream, mainAudioStream, containerExtension, config);
|
||||
if (!required) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = this.getFfmpegOptions(stream, config);
|
||||
const options = this.getFfmpegOptions(mainVideoStream, config);
|
||||
|
||||
this.logger.log(`Start encoding video ${asset.id} ${options}`);
|
||||
await this.mediaRepository.transcode(input, output, options);
|
||||
|
||||
this.logger.log(`Converting Success ${asset.id}`);
|
||||
this.logger.log(`Encoding success ${asset.id}`);
|
||||
|
||||
await this.assetRepository.save({ id: asset.id, encodedVideoPath: output });
|
||||
} catch (error: any) {
|
||||
@@ -151,32 +155,48 @@ export class MediaService {
|
||||
}
|
||||
}
|
||||
|
||||
private getLongestStream(streams: VideoStreamInfo[]): VideoStreamInfo | null {
|
||||
return streams
|
||||
.filter((stream) => stream.codecType === 'video')
|
||||
.sort((stream1, stream2) => stream2.frameCount - stream1.frameCount)[0];
|
||||
private getMainVideoStream(streams: VideoStreamInfo[]): VideoStreamInfo | null {
|
||||
return streams.sort((stream1, stream2) => stream2.frameCount - stream1.frameCount)[0];
|
||||
}
|
||||
|
||||
private isTranscodeRequired(stream: VideoStreamInfo, ffmpegConfig: SystemConfigFFmpegDto): boolean {
|
||||
if (!stream.height || !stream.width) {
|
||||
private getMainAudioStream(streams: AudioStreamInfo[]): AudioStreamInfo | null {
|
||||
return streams[0];
|
||||
}
|
||||
|
||||
private isTranscodeRequired(
|
||||
videoStream: VideoStreamInfo,
|
||||
audioStream: AudioStreamInfo,
|
||||
containerExtension: string,
|
||||
ffmpegConfig: SystemConfigFFmpegDto,
|
||||
): boolean {
|
||||
if (!videoStream.height || !videoStream.width) {
|
||||
this.logger.error('Skipping transcode, height or width undefined for video stream');
|
||||
return false;
|
||||
}
|
||||
|
||||
const isTargetVideoCodec = stream.codecName === ffmpegConfig.targetVideoCodec;
|
||||
const isTargetVideoCodec = videoStream.codecName === ffmpegConfig.targetVideoCodec;
|
||||
const isTargetAudioCodec = audioStream.codecName === ffmpegConfig.targetAudioCodec;
|
||||
const isTargetContainer = ['mov,mp4,m4a,3gp,3g2,mj2', 'mp4', 'mov'].includes(containerExtension);
|
||||
|
||||
this.logger.debug(audioStream.codecName, audioStream.codecType, containerExtension);
|
||||
|
||||
const allTargetsMatching = isTargetVideoCodec && isTargetAudioCodec && isTargetContainer;
|
||||
|
||||
const targetResolution = Number.parseInt(ffmpegConfig.targetResolution);
|
||||
const isLargerThanTargetResolution = Math.min(stream.height, stream.width) > targetResolution;
|
||||
const isLargerThanTargetResolution = Math.min(videoStream.height, videoStream.width) > targetResolution;
|
||||
|
||||
switch (ffmpegConfig.transcode) {
|
||||
case TranscodePreset.DISABLED:
|
||||
return false;
|
||||
|
||||
case TranscodePreset.ALL:
|
||||
return true;
|
||||
|
||||
case TranscodePreset.REQUIRED:
|
||||
return !isTargetVideoCodec;
|
||||
return !allTargetsMatching;
|
||||
|
||||
case TranscodePreset.OPTIMAL:
|
||||
return !isTargetVideoCodec || isLargerThanTargetResolution;
|
||||
return !allTargetsMatching || isLargerThanTargetResolution;
|
||||
|
||||
default:
|
||||
return false;
|
||||
@@ -184,8 +204,6 @@ export class MediaService {
|
||||
}
|
||||
|
||||
private getFfmpegOptions(stream: VideoStreamInfo, ffmpeg: SystemConfigFFmpegDto) {
|
||||
// TODO: If video or audio are already the correct format, don't re-encode, copy the stream
|
||||
|
||||
const options = [
|
||||
`-crf ${ffmpeg.crf}`,
|
||||
`-preset ${ffmpeg.preset}`,
|
||||
|
||||
Reference in New Issue
Block a user