mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
fix(server): scale transcoded videos if dimensions are odd (#6461)
scale if odd resolution
This commit is contained in:
parent
b98d1bf9d3
commit
9a2fa21b28
3 changed files with 106 additions and 4 deletions
|
@ -600,6 +600,66 @@ describe(MediaService.name, () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should always scale video if height is uneven', async () => {
|
||||||
|
mediaMock.probe.mockResolvedValue(probeStub.videoStreamOddHeight);
|
||||||
|
configMock.load.mockResolvedValue([
|
||||||
|
{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.ALL },
|
||||||
|
{ key: SystemConfigKey.FFMPEG_TARGET_RESOLUTION, value: 'original' },
|
||||||
|
]);
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
|
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:354,format=yuv420p`,
|
||||||
|
'-preset ultrafast',
|
||||||
|
'-crf 23',
|
||||||
|
],
|
||||||
|
twoPass: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should always scale video if width is uneven', async () => {
|
||||||
|
mediaMock.probe.mockResolvedValue(probeStub.videoStreamOddWidth);
|
||||||
|
configMock.load.mockResolvedValue([
|
||||||
|
{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.ALL },
|
||||||
|
{ key: SystemConfigKey.FFMPEG_TARGET_RESOLUTION, value: 'original' },
|
||||||
|
]);
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
|
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=354:-2,format=yuv420p`,
|
||||||
|
'-preset ultrafast',
|
||||||
|
'-crf 23',
|
||||||
|
],
|
||||||
|
twoPass: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should transcode when audio doesnt match target', async () => {
|
it('should transcode when audio doesnt match target', async () => {
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.audioStreamMp3);
|
mediaMock.probe.mockResolvedValue(probeStub.audioStreamMp3);
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.OPTIMAL }]);
|
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.OPTIMAL }]);
|
||||||
|
|
|
@ -122,15 +122,24 @@ class BaseConfig implements VideoCodecSWConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
getTargetResolution(videoStream: VideoStreamInfo) {
|
getTargetResolution(videoStream: VideoStreamInfo) {
|
||||||
|
let target;
|
||||||
if (this.config.targetResolution === 'original') {
|
if (this.config.targetResolution === 'original') {
|
||||||
return Math.min(videoStream.height, videoStream.width);
|
target = Math.min(videoStream.height, videoStream.width);
|
||||||
|
} else {
|
||||||
|
target = Number.parseInt(this.config.targetResolution);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Number.parseInt(this.config.targetResolution);
|
if (target % 2 !== 0) {
|
||||||
|
target -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldScale(videoStream: VideoStreamInfo) {
|
shouldScale(videoStream: VideoStreamInfo) {
|
||||||
return Math.min(videoStream.height, videoStream.width) > this.getTargetResolution(videoStream);
|
const oddDimensions = videoStream.height % 2 !== 0 || videoStream.width % 2 !== 0;
|
||||||
|
const largerThanTarget = Math.min(videoStream.height, videoStream.width) > this.getTargetResolution(videoStream);
|
||||||
|
return oddDimensions || largerThanTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldToneMap(videoStream: VideoStreamInfo) {
|
shouldToneMap(videoStream: VideoStreamInfo) {
|
||||||
|
@ -146,7 +155,10 @@ class BaseConfig implements VideoCodecSWConfig {
|
||||||
getSize(videoStream: VideoStreamInfo) {
|
getSize(videoStream: VideoStreamInfo) {
|
||||||
const smaller = this.getTargetResolution(videoStream);
|
const smaller = this.getTargetResolution(videoStream);
|
||||||
const factor = Math.max(videoStream.height, videoStream.width) / Math.min(videoStream.height, videoStream.width);
|
const factor = Math.max(videoStream.height, videoStream.width) / Math.min(videoStream.height, videoStream.width);
|
||||||
const larger = Math.round(smaller * factor);
|
let larger = Math.round(smaller * factor);
|
||||||
|
if (larger % 2 !== 0) {
|
||||||
|
larger -= 1;
|
||||||
|
}
|
||||||
return this.isVideoVertical(videoStream) ? { width: smaller, height: larger } : { width: larger, height: smaller };
|
return this.isVideoVertical(videoStream) ? { width: smaller, height: larger } : { width: larger, height: smaller };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
30
server/test/fixtures/media.stub.ts
vendored
30
server/test/fixtures/media.stub.ts
vendored
|
@ -117,6 +117,36 @@ export const probeStub = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
videoStreamOddHeight: Object.freeze<VideoInfo>({
|
||||||
|
...probeStubDefault,
|
||||||
|
videoStreams: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
height: 355,
|
||||||
|
width: 1586,
|
||||||
|
codecName: 'h264',
|
||||||
|
codecType: 'video',
|
||||||
|
frameCount: 100,
|
||||||
|
rotation: 0,
|
||||||
|
isHDR: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
videoStreamOddWidth: Object.freeze<VideoInfo>({
|
||||||
|
...probeStubDefault,
|
||||||
|
videoStreams: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
height: 1586,
|
||||||
|
width: 355,
|
||||||
|
codecName: 'h264',
|
||||||
|
codecType: 'video',
|
||||||
|
frameCount: 100,
|
||||||
|
rotation: 0,
|
||||||
|
isHDR: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
audioStreamMp3: Object.freeze<VideoInfo>({
|
audioStreamMp3: Object.freeze<VideoInfo>({
|
||||||
...probeStubDefault,
|
...probeStubDefault,
|
||||||
audioStreams: [{ index: 1, codecType: 'audio', codecName: 'aac', frameCount: 100 }],
|
audioStreams: [{ index: 1, codecType: 'audio', codecName: 'aac', frameCount: 100 }],
|
||||||
|
|
Loading…
Reference in a new issue