mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 20:25:32 -04:00
fix(server): video orientation (#5455)
* fix: video orientation * pr feedback
This commit is contained in:
parent
6d3421a505
commit
dfd6846deb
@ -5,11 +5,13 @@ import {
|
|||||||
newAssetRepositoryMock,
|
newAssetRepositoryMock,
|
||||||
newCryptoRepositoryMock,
|
newCryptoRepositoryMock,
|
||||||
newJobRepositoryMock,
|
newJobRepositoryMock,
|
||||||
|
newMediaRepositoryMock,
|
||||||
newMetadataRepositoryMock,
|
newMetadataRepositoryMock,
|
||||||
newMoveRepositoryMock,
|
newMoveRepositoryMock,
|
||||||
newPersonRepositoryMock,
|
newPersonRepositoryMock,
|
||||||
newStorageRepositoryMock,
|
newStorageRepositoryMock,
|
||||||
newSystemConfigRepositoryMock,
|
newSystemConfigRepositoryMock,
|
||||||
|
probeStub,
|
||||||
} from '@test';
|
} from '@test';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { Stats } from 'fs';
|
import { Stats } from 'fs';
|
||||||
@ -21,6 +23,7 @@ import {
|
|||||||
IAssetRepository,
|
IAssetRepository,
|
||||||
ICryptoRepository,
|
ICryptoRepository,
|
||||||
IJobRepository,
|
IJobRepository,
|
||||||
|
IMediaRepository,
|
||||||
IMetadataRepository,
|
IMetadataRepository,
|
||||||
IMoveRepository,
|
IMoveRepository,
|
||||||
IPersonRepository,
|
IPersonRepository,
|
||||||
@ -30,7 +33,7 @@ import {
|
|||||||
WithProperty,
|
WithProperty,
|
||||||
WithoutProperty,
|
WithoutProperty,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { MetadataService } from './metadata.service';
|
import { MetadataService, Orientation } from './metadata.service';
|
||||||
|
|
||||||
describe(MetadataService.name, () => {
|
describe(MetadataService.name, () => {
|
||||||
let albumMock: jest.Mocked<IAlbumRepository>;
|
let albumMock: jest.Mocked<IAlbumRepository>;
|
||||||
@ -40,6 +43,7 @@ describe(MetadataService.name, () => {
|
|||||||
let jobMock: jest.Mocked<IJobRepository>;
|
let jobMock: jest.Mocked<IJobRepository>;
|
||||||
let metadataMock: jest.Mocked<IMetadataRepository>;
|
let metadataMock: jest.Mocked<IMetadataRepository>;
|
||||||
let moveMock: jest.Mocked<IMoveRepository>;
|
let moveMock: jest.Mocked<IMoveRepository>;
|
||||||
|
let mediaMock: jest.Mocked<IMediaRepository>;
|
||||||
let personMock: jest.Mocked<IPersonRepository>;
|
let personMock: jest.Mocked<IPersonRepository>;
|
||||||
let storageMock: jest.Mocked<IStorageRepository>;
|
let storageMock: jest.Mocked<IStorageRepository>;
|
||||||
let sut: MetadataService;
|
let sut: MetadataService;
|
||||||
@ -54,6 +58,7 @@ describe(MetadataService.name, () => {
|
|||||||
moveMock = newMoveRepositoryMock();
|
moveMock = newMoveRepositoryMock();
|
||||||
personMock = newPersonRepositoryMock();
|
personMock = newPersonRepositoryMock();
|
||||||
storageMock = newStorageRepositoryMock();
|
storageMock = newStorageRepositoryMock();
|
||||||
|
mediaMock = newMediaRepositoryMock();
|
||||||
|
|
||||||
sut = new MetadataService(
|
sut = new MetadataService(
|
||||||
albumMock,
|
albumMock,
|
||||||
@ -63,6 +68,7 @@ describe(MetadataService.name, () => {
|
|||||||
metadataMock,
|
metadataMock,
|
||||||
storageMock,
|
storageMock,
|
||||||
configMock,
|
configMock,
|
||||||
|
mediaMock,
|
||||||
moveMock,
|
moveMock,
|
||||||
personMock,
|
personMock,
|
||||||
);
|
);
|
||||||
@ -277,6 +283,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should not apply motion photos if asset is video', async () => {
|
it('should not apply motion photos if asset is video', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoMotionAsset, isVisible: true }]);
|
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoMotionAsset, isVisible: true }]);
|
||||||
|
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id });
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id });
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]);
|
||||||
@ -287,6 +294,19 @@ describe(MetadataService.name, () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should extract the correct video orientation', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
|
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
||||||
|
metadataMock.readTags.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
||||||
|
|
||||||
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]);
|
||||||
|
expect(assetMock.upsertExif).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ orientation: Orientation.Rotate270CW }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should apply motion photos', async () => {
|
it('should apply motion photos', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
|
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
|
||||||
metadataMock.readTags.mockResolvedValue({
|
metadataMock.readTags.mockResolvedValue({
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
IAssetRepository,
|
IAssetRepository,
|
||||||
ICryptoRepository,
|
ICryptoRepository,
|
||||||
IJobRepository,
|
IJobRepository,
|
||||||
|
IMediaRepository,
|
||||||
IMetadataRepository,
|
IMetadataRepository,
|
||||||
IMoveRepository,
|
IMoveRepository,
|
||||||
IPersonRepository,
|
IPersonRepository,
|
||||||
@ -49,6 +50,17 @@ interface DirectoryEntry {
|
|||||||
Item: DirectoryItem;
|
Item: DirectoryItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Orientation {
|
||||||
|
Horizontal = '1',
|
||||||
|
MirrorHorizontal = '2',
|
||||||
|
Rotate180 = '3',
|
||||||
|
MirrorVertical = '4',
|
||||||
|
MirrorHorizontalRotate270CW = '5',
|
||||||
|
Rotate90CW = '6',
|
||||||
|
MirrorHorizontalRotate90CW = '7',
|
||||||
|
Rotate270CW = '8',
|
||||||
|
}
|
||||||
|
|
||||||
type ExifEntityWithoutGeocodeAndTypeOrm = Omit<
|
type ExifEntityWithoutGeocodeAndTypeOrm = Omit<
|
||||||
ExifEntity,
|
ExifEntity,
|
||||||
'city' | 'state' | 'country' | 'description' | 'exifTextSearchableColumn'
|
'city' | 'state' | 'country' | 'description' | 'exifTextSearchableColumn'
|
||||||
@ -90,6 +102,7 @@ export class MetadataService {
|
|||||||
@Inject(IMetadataRepository) private repository: IMetadataRepository,
|
@Inject(IMetadataRepository) private repository: IMetadataRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||||
|
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
||||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||||
@Inject(IPersonRepository) personRepository: IPersonRepository,
|
@Inject(IPersonRepository) personRepository: IPersonRepository,
|
||||||
) {
|
) {
|
||||||
@ -182,6 +195,27 @@ export class MetadataService {
|
|||||||
|
|
||||||
const { exifData, tags } = await this.exifData(asset);
|
const { exifData, tags } = await this.exifData(asset);
|
||||||
|
|
||||||
|
if (asset.type === AssetType.VIDEO) {
|
||||||
|
const { videoStreams } = await this.mediaRepository.probe(asset.originalPath);
|
||||||
|
|
||||||
|
if (videoStreams[0]) {
|
||||||
|
switch (videoStreams[0].rotation) {
|
||||||
|
case -90:
|
||||||
|
exifData.orientation = Orientation.Rotate90CW;
|
||||||
|
break;
|
||||||
|
case 0:
|
||||||
|
exifData.orientation = Orientation.Horizontal;
|
||||||
|
break;
|
||||||
|
case 90:
|
||||||
|
exifData.orientation = Orientation.Rotate270CW;
|
||||||
|
break;
|
||||||
|
case 180:
|
||||||
|
exifData.orientation = Orientation.Rotate180;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.applyMotionPhotos(asset, tags);
|
await this.applyMotionPhotos(asset, tags);
|
||||||
await this.applyReverseGeocoding(asset, exifData);
|
await this.applyReverseGeocoding(asset, exifData);
|
||||||
await this.assetRepository.upsertExif(exifData);
|
await this.assetRepository.upsertExif(exifData);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user