feat(server): only transcode streams that require it (#7106)

This commit is contained in:
Mert
2024-02-14 11:24:39 -05:00
committed by GitHub
parent b823dfffdc
commit 5ff68d4cdb
7 changed files with 169 additions and 97 deletions
+78 -48
View File
@@ -6,6 +6,7 @@ import {
Colorspace,
TranscodeHWAccel,
TranscodePolicy,
TranscodeTarget,
VideoCodec,
} from '@app/infra/entities';
import { ImmichLogger } from '@app/infra/logger';
@@ -197,7 +198,7 @@ export class MediaService {
}
const mainAudioStream = this.getMainStream(audioStreams);
const config = { ...ffmpeg, targetResolution: size.toString() };
const options = new ThumbnailConfig(config).getOptions(mainVideoStream, mainAudioStream);
const options = new ThumbnailConfig(config).getOptions(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream);
await this.mediaRepository.transcode(asset.originalPath, path, options);
break;
}
@@ -267,7 +268,6 @@ 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;
}
@@ -279,15 +279,8 @@ export class MediaService {
const { ffmpeg: config } = await this.configCore.getConfig();
const required = this.isTranscodeRequired(
asset,
mainVideoStream,
mainAudioStream,
containerExtension,
config,
bitrate,
);
if (!required) {
const target = this.getTranscodeTarget(config, mainVideoStream, mainAudioStream);
if (target === TranscodeTarget.NONE) {
if (asset.encodedVideoPath) {
this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`);
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [asset.encodedVideoPath] } });
@@ -299,13 +292,15 @@ export class MediaService {
let transcodeOptions;
try {
transcodeOptions = await this.getCodecConfig(config).then((c) => c.getOptions(mainVideoStream, mainAudioStream));
transcodeOptions = await this.getCodecConfig(config).then((c) =>
c.getOptions(target, mainVideoStream, mainAudioStream),
);
} catch (error) {
this.logger.error(`An error occurred while configuring transcoding options: ${error}`);
return false;
}
this.logger.log(`Start encoding video ${asset.id} ${JSON.stringify(transcodeOptions)}`);
this.logger.log(`Started encoding video ${asset.id} ${JSON.stringify(transcodeOptions)}`);
try {
await this.mediaRepository.transcode(input, output, transcodeOptions);
} catch (error) {
@@ -316,11 +311,13 @@ export class MediaService {
);
}
config.accel = TranscodeHWAccel.DISABLED;
transcodeOptions = await this.getCodecConfig(config).then((c) => c.getOptions(mainVideoStream, mainAudioStream));
transcodeOptions = await this.getCodecConfig(config).then((c) =>
c.getOptions(target, mainVideoStream, mainAudioStream),
);
await this.mediaRepository.transcode(input, output, transcodeOptions);
}
this.logger.log(`Encoding success ${asset.id}`);
this.logger.log(`Successfully encoded ${asset.id}`);
await this.assetRepository.save({ id: asset.id, encodedVideoPath: output });
@@ -331,55 +328,88 @@ export class MediaService {
return streams.sort((stream1, stream2) => stream2.frameCount - stream1.frameCount)[0];
}
private isTranscodeRequired(
asset: AssetEntity,
videoStream: VideoStreamInfo,
private getTranscodeTarget(
config: SystemConfigFFmpegDto,
videoStream: VideoStreamInfo | null,
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);
const isTargetAudioCodec =
audioStream == null || ffmpegConfig.acceptedAudioCodecs.includes(audioStream.codecName as AudioCodec);
): TranscodeTarget {
if (videoStream == null && audioStream == null) {
return TranscodeTarget.NONE;
}
this.logger.verbose(
`${asset.id}: AudioCodecName ${audioStream?.codecName ?? 'None'}, AudioStreamCodecType ${
audioStream?.codecType ?? 'None'
}, containerExtension ${containerExtension}`,
);
const isAudioTranscodeRequired = this.isAudioTranscodeRequired(config, audioStream);
const isVideoTranscodeRequired = this.isVideoTranscodeRequired(config, videoStream);
const allTargetsMatching = isTargetVideoCodec && isTargetAudioCodec && isTargetContainer;
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);
if (isAudioTranscodeRequired && isVideoTranscodeRequired) {
return TranscodeTarget.ALL;
}
if (isAudioTranscodeRequired) {
return TranscodeTarget.AUDIO;
}
if (isVideoTranscodeRequired) {
return TranscodeTarget.VIDEO;
}
return TranscodeTarget.NONE;
}
private isAudioTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream: AudioStreamInfo | null): boolean {
if (stream == null) {
return false;
}
switch (ffmpegConfig.transcode) {
case TranscodePolicy.DISABLED: {
return false;
}
case TranscodePolicy.ALL: {
return true;
}
case TranscodePolicy.REQUIRED: {
return !allTargetsMatching || videoStream.isHDR;
}
case TranscodePolicy.OPTIMAL: {
return !allTargetsMatching || isLargerThanTargetRes || videoStream.isHDR;
}
case TranscodePolicy.REQUIRED:
case TranscodePolicy.OPTIMAL:
case TranscodePolicy.BITRATE: {
return !allTargetsMatching || isLargerThanTargetBitrate || videoStream.isHDR;
return !ffmpegConfig.acceptedAudioCodecs.includes(stream.codecName as AudioCodec);
}
default: {
throw new Error(`Unsupported transcode policy: ${ffmpegConfig.transcode}`);
}
}
}
private isVideoTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream: VideoStreamInfo | null): boolean {
if (stream == null) {
return false;
}
const scalingEnabled = ffmpegConfig.targetResolution !== 'original';
const targetRes = Number.parseInt(ffmpegConfig.targetResolution);
const isLargerThanTargetRes = scalingEnabled && Math.min(stream.height, stream.width) > targetRes;
const isLargerThanTargetBitrate = stream.bitrate > this.parseBitrateToBps(ffmpegConfig.maxBitrate);
const isTargetVideoCodec = ffmpegConfig.acceptedVideoCodecs.includes(stream.codecName as VideoCodec);
const isRequired = !isTargetVideoCodec || stream.isHDR;
switch (ffmpegConfig.transcode) {
case TranscodePolicy.DISABLED: {
return false;
}
case TranscodePolicy.ALL: {
return true;
}
case TranscodePolicy.REQUIRED: {
return isRequired;
}
case TranscodePolicy.OPTIMAL: {
return isRequired || isLargerThanTargetRes;
}
case TranscodePolicy.BITRATE: {
return isRequired || isLargerThanTargetBitrate;
}
default: {
throw new Error(`Unsupported transcode policy: ${ffmpegConfig.transcode}`);
}
}
}