feat(server): only transcode streams that require it (#7106)
This commit is contained in:
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user