mirror of
https://github.com/immich-app/immich.git
synced 2025-04-16 12:18:51 +02: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:
parent
149bc71eba
commit
87c38d1832
3
mobile/openapi/lib/model/transcode_policy.dart
generated
3
mobile/openapi/lib/model/transcode_policy.dart
generated
@ -25,6 +25,7 @@ class TranscodePolicy {
|
|||||||
|
|
||||||
static const all = TranscodePolicy._(r'all');
|
static const all = TranscodePolicy._(r'all');
|
||||||
static const optimal = TranscodePolicy._(r'optimal');
|
static const optimal = TranscodePolicy._(r'optimal');
|
||||||
|
static const bitrate = TranscodePolicy._(r'bitrate');
|
||||||
static const required_ = TranscodePolicy._(r'required');
|
static const required_ = TranscodePolicy._(r'required');
|
||||||
static const disabled = TranscodePolicy._(r'disabled');
|
static const disabled = TranscodePolicy._(r'disabled');
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ class TranscodePolicy {
|
|||||||
static const values = <TranscodePolicy>[
|
static const values = <TranscodePolicy>[
|
||||||
all,
|
all,
|
||||||
optimal,
|
optimal,
|
||||||
|
bitrate,
|
||||||
required_,
|
required_,
|
||||||
disabled,
|
disabled,
|
||||||
];
|
];
|
||||||
@ -74,6 +76,7 @@ class TranscodePolicyTypeTransformer {
|
|||||||
switch (data) {
|
switch (data) {
|
||||||
case r'all': return TranscodePolicy.all;
|
case r'all': return TranscodePolicy.all;
|
||||||
case r'optimal': return TranscodePolicy.optimal;
|
case r'optimal': return TranscodePolicy.optimal;
|
||||||
|
case r'bitrate': return TranscodePolicy.bitrate;
|
||||||
case r'required': return TranscodePolicy.required_;
|
case r'required': return TranscodePolicy.required_;
|
||||||
case r'disabled': return TranscodePolicy.disabled;
|
case r'disabled': return TranscodePolicy.disabled;
|
||||||
default:
|
default:
|
||||||
|
@ -9923,6 +9923,7 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"all",
|
"all",
|
||||||
"optimal",
|
"optimal",
|
||||||
|
"bitrate",
|
||||||
"required",
|
"required",
|
||||||
"disabled"
|
"disabled"
|
||||||
],
|
],
|
||||||
|
1
open-api/typescript-sdk/client/api.ts
generated
1
open-api/typescript-sdk/client/api.ts
generated
@ -4370,6 +4370,7 @@ export type TranscodeHWAccel = typeof TranscodeHWAccel[keyof typeof TranscodeHWA
|
|||||||
export const TranscodePolicy = {
|
export const TranscodePolicy = {
|
||||||
All: 'all',
|
All: 'all',
|
||||||
Optimal: 'optimal',
|
Optimal: 'optimal',
|
||||||
|
Bitrate: 'bitrate',
|
||||||
Required: 'required',
|
Required: 'required',
|
||||||
Disabled: 'disabled'
|
Disabled: 'disabled'
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -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 () => {
|
it('should not scale resolution if no target resolution', async () => {
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
|
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||||
configMock.load.mockResolvedValue([
|
configMock.load.mockResolvedValue([
|
||||||
|
@ -264,6 +264,7 @@ export class MediaService {
|
|||||||
const mainVideoStream = this.getMainStream(videoStreams);
|
const mainVideoStream = this.getMainStream(videoStreams);
|
||||||
const mainAudioStream = this.getMainStream(audioStreams);
|
const mainAudioStream = this.getMainStream(audioStreams);
|
||||||
const containerExtension = format.formatName;
|
const containerExtension = format.formatName;
|
||||||
|
const bitrate = format.bitrate;
|
||||||
if (!mainVideoStream || !containerExtension) {
|
if (!mainVideoStream || !containerExtension) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -275,7 +276,14 @@ export class MediaService {
|
|||||||
|
|
||||||
const { ffmpeg: config } = await this.configCore.getConfig();
|
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 (!required) {
|
||||||
if (asset.encodedVideoPath) {
|
if (asset.encodedVideoPath) {
|
||||||
this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`);
|
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,
|
audioStream: AudioStreamInfo | null,
|
||||||
containerExtension: string,
|
containerExtension: string,
|
||||||
ffmpegConfig: SystemConfigFFmpegDto,
|
ffmpegConfig: SystemConfigFFmpegDto,
|
||||||
|
bitrate: number,
|
||||||
): boolean {
|
): boolean {
|
||||||
const isTargetVideoCodec = ffmpegConfig.acceptedVideoCodecs.includes(videoStream.codecName as VideoCodec);
|
const isTargetVideoCodec = ffmpegConfig.acceptedVideoCodecs.includes(videoStream.codecName as VideoCodec);
|
||||||
const isTargetContainer = ['mov,mp4,m4a,3gp,3g2,mj2', 'mp4', 'mov'].includes(containerExtension);
|
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 scalingEnabled = ffmpegConfig.targetResolution !== 'original';
|
||||||
const targetRes = Number.parseInt(ffmpegConfig.targetResolution);
|
const targetRes = Number.parseInt(ffmpegConfig.targetResolution);
|
||||||
const isLargerThanTargetRes = scalingEnabled && Math.min(videoStream.height, videoStream.width) > targetRes;
|
const isLargerThanTargetRes = scalingEnabled && Math.min(videoStream.height, videoStream.width) > targetRes;
|
||||||
|
const isLargerThanTargetBitrate = bitrate > this.parseBitrateToBps(ffmpegConfig.maxBitrate);
|
||||||
|
|
||||||
switch (ffmpegConfig.transcode) {
|
switch (ffmpegConfig.transcode) {
|
||||||
case TranscodePolicy.DISABLED:
|
case TranscodePolicy.DISABLED:
|
||||||
@ -356,6 +366,9 @@ export class MediaService {
|
|||||||
case TranscodePolicy.OPTIMAL:
|
case TranscodePolicy.OPTIMAL:
|
||||||
return !allTargetsMatching || isLargerThanTargetRes || videoStream.isHDR;
|
return !allTargetsMatching || isLargerThanTargetRes || videoStream.isHDR;
|
||||||
|
|
||||||
|
case TranscodePolicy.BITRATE:
|
||||||
|
return !allTargetsMatching || isLargerThanTargetBitrate || videoStream.isHDR;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -424,4 +437,20 @@ export class MediaService {
|
|||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ export interface VideoFormat {
|
|||||||
formatName?: string;
|
formatName?: string;
|
||||||
formatLongName?: string;
|
formatLongName?: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
bitrate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoInfo {
|
export interface VideoInfo {
|
||||||
|
@ -108,6 +108,7 @@ export enum SystemConfigKey {
|
|||||||
export enum TranscodePolicy {
|
export enum TranscodePolicy {
|
||||||
ALL = 'all',
|
ALL = 'all',
|
||||||
OPTIMAL = 'optimal',
|
OPTIMAL = 'optimal',
|
||||||
|
BITRATE = 'bitrate',
|
||||||
REQUIRED = 'required',
|
REQUIRED = 'required',
|
||||||
DISABLED = 'disabled',
|
DISABLED = 'disabled',
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@ export class MediaRepository implements IMediaRepository {
|
|||||||
formatName: results.format.format_name,
|
formatName: results.format.format_name,
|
||||||
formatLongName: results.format.format_long_name,
|
formatLongName: results.format.format_long_name,
|
||||||
duration: results.format.duration || 0,
|
duration: results.format.duration || 0,
|
||||||
|
bitrate: results.format.bit_rate ?? 0,
|
||||||
},
|
},
|
||||||
videoStreams: results.streams
|
videoStreams: results.streams
|
||||||
.filter((stream) => stream.codec_type === 'video')
|
.filter((stream) => stream.codec_type === 'video')
|
||||||
|
11
server/test/fixtures/media.stub.ts
vendored
11
server/test/fixtures/media.stub.ts
vendored
@ -4,6 +4,7 @@ const probeStubDefaultFormat: VideoFormat = {
|
|||||||
formatName: 'mov,mp4,m4a,3gp,3g2,mj2',
|
formatName: 'mov,mp4,m4a,3gp,3g2,mj2',
|
||||||
formatLongName: 'QuickTime / MOV',
|
formatLongName: 'QuickTime / MOV',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
|
bitrate: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const probeStubDefaultVideoStream: VideoStreamInfo[] = [
|
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>({
|
videoStreamHDR: Object.freeze<VideoInfo>({
|
||||||
...probeStubDefault,
|
...probeStubDefault,
|
||||||
videoStreams: [
|
videoStreams: [
|
||||||
@ -157,6 +167,7 @@ export const probeStub = {
|
|||||||
formatName: 'matroska,webm',
|
formatName: 'matroska,webm',
|
||||||
formatLongName: 'Matroska / WebM',
|
formatLongName: 'Matroska / WebM',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
|
bitrate: 0,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
@ -183,6 +183,10 @@
|
|||||||
value: TranscodePolicy.Optimal,
|
value: TranscodePolicy.Optimal,
|
||||||
text: 'Videos higher than target resolution or not in an accepted format',
|
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,
|
value: TranscodePolicy.Required,
|
||||||
text: 'Only videos not in an accepted format',
|
text: 'Only videos not in an accepted format',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user