0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-07 00:50:23 -05:00

feat(server, web): Added TranscodePolicy "Bitrate higher than max bitrate or not in accepted format" (#6479)

* chore: rebase

* chore: open api

* Add Database-Migration for setting targetCodec as acceptedCodec if it was set by admin

* Add TranscodePolicy setting, to only transcode files with a bitrate higher than set max bitrate

* Rename enum value of TranscodePolicy

* calculate max_bitrate according to "k" and "m" suffix for comparison

* remove migration

* minor changes

* UnitTest for Bitrate Policy

* Fix UnitTest

* Add missing output options

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
This commit is contained in:
Markus 2024-01-31 02:25:07 +01:00 committed by GitHub
parent 149bc71eba
commit 87c38d1832
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 81 additions and 1 deletions

Binary file not shown.

View file

@ -9923,6 +9923,7 @@
"enum": [
"all",
"optimal",
"bitrate",
"required",
"disabled"
],

View file

@ -4370,6 +4370,7 @@ export type TranscodeHWAccel = typeof TranscodeHWAccel[keyof typeof TranscodeHWA
export const TranscodePolicy = {
All: 'all',
Optimal: 'optimal',
Bitrate: 'bitrate',
Required: 'required',
Disabled: 'disabled'
} as const;

View file

@ -557,6 +557,37 @@ describe(MediaService.name, () => {
);
});
it('should transcode when policy Bitrate and bitrate higher than max bitrate', async () => {
mediaMock.probe.mockResolvedValue(probeStub.videoStream40Mbps);
configMock.load.mockResolvedValue([
{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.BITRATE },
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '30M' },
]);
await sut.handleVideoConversion({ id: assetStub.video.id });
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
{
inputOptions: [],
outputOptions: [
'-c:v h264',
'-c:a aac',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
'-map 0:1',
'-v verbose',
'-vf scale=-2:720,format=yuv420p',
'-preset ultrafast',
'-crf 23',
'-maxrate 30M',
'-bufsize 60M',
],
twoPass: false,
},
);
});
it('should not scale resolution if no target resolution', async () => {
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
configMock.load.mockResolvedValue([

View file

@ -264,6 +264,7 @@ 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;
}
@ -275,7 +276,14 @@ export class MediaService {
const { ffmpeg: config } = await this.configCore.getConfig();
const required = this.isTranscodeRequired(asset, mainVideoStream, mainAudioStream, containerExtension, config);
const required = this.isTranscodeRequired(
asset,
mainVideoStream,
mainAudioStream,
containerExtension,
config,
bitrate,
);
if (!required) {
if (asset.encodedVideoPath) {
this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`);
@ -326,6 +334,7 @@ export class MediaService {
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);
@ -342,6 +351,7 @@ export class MediaService {
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);
switch (ffmpegConfig.transcode) {
case TranscodePolicy.DISABLED:
@ -356,6 +366,9 @@ export class MediaService {
case TranscodePolicy.OPTIMAL:
return !allTargetsMatching || isLargerThanTargetRes || videoStream.isHDR;
case TranscodePolicy.BITRATE:
return !allTargetsMatching || isLargerThanTargetBitrate || videoStream.isHDR;
default:
return false;
}
@ -424,4 +437,20 @@ export class MediaService {
return true;
}
}
parseBitrateToBps(bitrateString: string) {
const bitrateValue = Number.parseInt(bitrateString);
if (isNaN(bitrateValue)) {
return 0;
}
if (bitrateString.toLowerCase().endsWith('k')) {
return bitrateValue * 1000; // Kilobits per second to bits per second
} else if (bitrateString.toLowerCase().endsWith('m')) {
return bitrateValue * 1000000; // Megabits per second to bits per second
} else {
return bitrateValue;
}
}
}

View file

@ -32,6 +32,7 @@ export interface VideoFormat {
formatName?: string;
formatLongName?: string;
duration: number;
bitrate: number;
}
export interface VideoInfo {

View file

@ -108,6 +108,7 @@ export enum SystemConfigKey {
export enum TranscodePolicy {
ALL = 'all',
OPTIMAL = 'optimal',
BITRATE = 'bitrate',
REQUIRED = 'required',
DISABLED = 'disabled',
}

View file

@ -47,6 +47,7 @@ export class MediaRepository implements IMediaRepository {
formatName: results.format.format_name,
formatLongName: results.format.format_long_name,
duration: results.format.duration || 0,
bitrate: results.format.bit_rate ?? 0,
},
videoStreams: results.streams
.filter((stream) => stream.codec_type === 'video')

View file

@ -4,6 +4,7 @@ const probeStubDefaultFormat: VideoFormat = {
formatName: 'mov,mp4,m4a,3gp,3g2,mj2',
formatLongName: 'QuickTime / MOV',
duration: 0,
bitrate: 0,
};
const probeStubDefaultVideoStream: VideoStreamInfo[] = [
@ -87,6 +88,15 @@ export const probeStub = {
},
],
}),
videoStream40Mbps: Object.freeze<VideoInfo>({
...probeStubDefault,
format: {
formatName: 'mov,mp4,m4a,3gp,3g2,mj2',
formatLongName: 'QuickTime / MOV',
duration: 0,
bitrate: 40000000,
},
}),
videoStreamHDR: Object.freeze<VideoInfo>({
...probeStubDefault,
videoStreams: [
@ -157,6 +167,7 @@ export const probeStub = {
formatName: 'matroska,webm',
formatLongName: 'Matroska / WebM',
duration: 0,
bitrate: 0,
},
}),
};

View file

@ -183,6 +183,10 @@
value: TranscodePolicy.Optimal,
text: 'Videos higher than target resolution or not in an accepted format',
},
{
value: TranscodePolicy.Bitrate,
text: 'Videos higher than max bitrate or not in an accepted format',
},
{
value: TranscodePolicy.Required,
text: 'Only videos not in an accepted format',