Compare commits

...

1 Commits

Author SHA1 Message Date
Alex Tran
03e8f98c2c feat: consider DAR when extracting video dimension 2026-01-16 02:47:57 +00:00
5 changed files with 82 additions and 5 deletions

View File

@@ -258,6 +258,7 @@ export class MediaRepository {
colorPrimaries: stream.color_primaries,
colorSpace: stream.color_space,
colorTransfer: stream.color_transfer,
displayAspectRatio: stream.display_aspect_ratio,
})),
audioStreams: results.streams
.filter((stream) => stream.codec_type === 'audio')

View File

@@ -599,6 +599,21 @@ describe(MetadataService.name, () => {
);
});
it('should apply Display Aspect Ratio (DAR) for anamorphic video', async () => {
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
mocks.media.probe.mockResolvedValue(probeStub.videoStreamAnamorphic);
mockReadTags({});
await sut.handleMetadataExtraction({ id: assetStub.video.id });
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id);
// Anamorphic video: 1440x1080 with DAR 16:9 should display as 1920x1080
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
expect.objectContaining({ exifImageWidth: 1920, exifImageHeight: 1080 }),
{ lockedPropertiesBehavior: 'skip' },
);
});
it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => {
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
...assetStub.livePhotoWithOriginalFileName,

View File

@@ -989,12 +989,15 @@ export class MetadataService extends BaseService {
const tags: Pick<ImmichTags, 'Duration' | 'Orientation' | 'ImageWidth' | 'ImageHeight'> = {};
if (videoStreams[0]) {
// Set video dimensions
if (videoStreams[0].width) {
tags.ImageWidth = videoStreams[0].width;
// Set video dimensions, considering Display Aspect Ratio (DAR) for anamorphic videos
const { width, height, displayAspectRatio } = videoStreams[0];
const displayDimensions = this.applyDisplayAspectRatio(width, height, displayAspectRatio);
if (displayDimensions.width) {
tags.ImageWidth = displayDimensions.width;
}
if (videoStreams[0].height) {
tags.ImageHeight = videoStreams[0].height;
if (displayDimensions.height) {
tags.ImageHeight = displayDimensions.height;
}
switch (videoStreams[0].rotation) {
@@ -1023,4 +1026,44 @@ export class MetadataService extends BaseService {
return tags;
}
/**
* Calculates the display dimensions of a video based on its Display Aspect Ratio (DAR).
* DAR accounts for anamorphic videos where the stored pixel dimensions differ from the intended display dimensions.
* For example, a 1440x1080 video with DAR 16:9 should display as 1920x1080 (1440 * 16/9 / (1440/1080) = 1920).
*/
private applyDisplayAspectRatio(
width: number | undefined,
height: number | undefined,
displayAspectRatio: string | undefined,
): { width?: number; height?: number } {
if (!width || !height || !displayAspectRatio) {
return { width, height };
}
// Parse DAR string (e.g., "16:9" or "4:3")
const darMatch = displayAspectRatio.match(/^(\d+):(\d+)$/);
if (!darMatch) {
return { width, height };
}
const darWidth = Number.parseInt(darMatch[1], 10);
const darHeight = Number.parseInt(darMatch[2], 10);
if (!darWidth || !darHeight) {
return { width, height };
}
const dar = darWidth / darHeight;
const storedAspectRatio = width / height;
// If DAR is effectively the same as stored aspect ratio (within a small tolerance), no adjustment needed
if (Math.abs(dar - storedAspectRatio) < 0.01) {
return { width, height };
}
// Apply DAR by adjusting width while keeping height constant
// This matches how video players typically handle anamorphic content
const displayWidth = Math.round(height * dar);
return { width: displayWidth, height };
}
}

View File

@@ -85,6 +85,7 @@ export interface VideoStreamInfo {
colorPrimaries?: string;
colorSpace?: string;
colorTransfer?: string;
displayAspectRatio?: string;
}
export interface AudioStreamInfo {

View File

@@ -272,4 +272,21 @@ export const probeStub = {
},
],
}),
videoStreamAnamorphic: Object.freeze<VideoInfo>({
...probeStubDefault,
videoStreams: [
{
index: 0,
height: 1080,
width: 1440,
codecName: 'h264',
frameCount: 100,
rotation: 0,
isHDR: false,
bitrate: 0,
pixelFormat: 'yuv420p',
displayAspectRatio: '16:9',
},
],
}),
};