diff --git a/e2e/test-assets b/e2e/test-assets index 0eac5a3738..9470c807b5 160000 --- a/e2e/test-assets +++ b/e2e/test-assets @@ -1 +1 @@ -Subproject commit 0eac5a37384c151be88381b41f9e28d8d59a4466 +Subproject commit 9470c807b517ec645b08166d0263d15904acaaec diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 6f8ccafc53..8e3372011a 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -35,6 +35,7 @@ import { JobRepository } from 'src/repositories/job.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { MachineLearningRepository } from 'src/repositories/machine-learning.repository'; import { MapRepository } from 'src/repositories/map.repository'; +import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; import { MetadataRepository } from 'src/repositories/metadata.repository'; import { NotificationRepository } from 'src/repositories/notification.repository'; @@ -362,7 +363,14 @@ export class ExifTestContext extends MediumTestContext { constructor(database: Kysely) { super(MetadataService, { database, - real: [AssetRepository, AssetJobRepository, MetadataRepository, SystemMetadataRepository, TagRepository], + real: [ + AssetRepository, + AssetJobRepository, + MediaRepository, + MetadataRepository, + SystemMetadataRepository, + TagRepository, + ], mock: [ConfigRepository, EventRepository, LoggingRepository, MapRepository, StorageRepository], }); @@ -445,6 +453,7 @@ const newRealRepository = (key: ClassConstructor, db: Kysely): T => { return new key(LoggingRepository.create()); } + case MediaRepository: case MetadataRepository: { return new key(LoggingRepository.create()); } diff --git a/server/test/medium/specs/exif/audio-video.spec.ts b/server/test/medium/specs/exif/audio-video.spec.ts new file mode 100644 index 0000000000..430e7826f9 --- /dev/null +++ b/server/test/medium/specs/exif/audio-video.spec.ts @@ -0,0 +1,142 @@ +import { Kysely } from 'kysely'; +import { resolve } from 'node:path'; +import { + AacProfile, + AssetType, + ColorMatrix, + ColorPrimaries, + ColorTransfer, + DvProfile, + DvSignalCompatibility, + H264Profile, + HevcProfile, +} from 'src/enum'; +import { DB } from 'src/schema'; +import { ExifTestContext, testAssetsDir } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let database: Kysely; + +beforeAll(async () => { + database = await getKyselyDB(); +}); + +const fixtures = [ + { + file: 'eiffel-tower.mp4', + video: { + codecName: 'h264', + formatName: 'mov,mp4,m4a,3gp,3g2,mj2', + formatLongName: 'QuickTime / MOV', + pixelFormat: 'yuv420p', + bitrate: 5_128_622, + frameCount: 557, + timeBase: 90_000, + index: 0, + profile: H264Profile.High, + level: 40, + colorPrimaries: ColorPrimaries.Smpte170M, + colorTransfer: ColorTransfer.Smpte170M, + colorMatrix: ColorMatrix.Smpte170M, + dvProfile: null, + dvLevel: null, + dvBlSignalCompatibilityId: null, + }, + audio: { codecName: 'aac', bitrate: 125_629, index: 1, profile: AacProfile.Lc }, + keyframes: { + totalDuration: 2_012_441, + packetCount: 557, + outputFrames: 557, + pts: [0, 462_502, 925_004, 1_210_454, 1_387_506, 1_542_878, 1_850_008], + accDuration: [3613, 466_077, 928_541, 1_213_968, 1_391_005, 1_546_364, 1_853_469], + ownDuration: [3613, 3613, 3613, 3613, 3613, 3613, 3613], + }, + }, + { + file: 'waterfall.mp4', + video: { + codecName: 'hevc', + formatName: 'mov,mp4,m4a,3gp,3g2,mj2', + formatLongName: 'QuickTime / MOV', + pixelFormat: 'yuvj420p', + bitrate: 43_363_499, + frameCount: 309, + timeBase: 90_000, + index: 2, + profile: HevcProfile.Main, + level: 156, + colorPrimaries: ColorPrimaries.Bt709, + colorTransfer: ColorTransfer.Bt709, + colorMatrix: ColorMatrix.Bt709, + dvProfile: null, + dvLevel: null, + dvBlSignalCompatibilityId: null, + }, + audio: { codecName: 'aac', bitrate: 191_878, index: 1, profile: null }, + keyframes: { + totalDuration: 932_286, + packetCount: 309, + outputFrames: 309, + pts: [0, 89_987, 179_974, 269_961, 359_948, 449_936, 539_923, 629_910, 725_166, 815_273, 905_295], + accDuration: [2999, 92_987, 182_974, 272_961, 362_948, 452_934, 542_922, 632_909, 728_175, 818_274, 908_296], + ownDuration: [2999, 3000, 3000, 3000, 3000, 2998, 2999, 2999, 3009, 3001, 3001], + }, + }, + { + file: 'train.mov', + video: { + codecName: 'hevc', + formatName: 'mov,mp4,m4a,3gp,3g2,mj2', + formatLongName: 'QuickTime / MOV', + pixelFormat: 'yuv420p10le', + bitrate: 12_595_191, + frameCount: 1229, + timeBase: 600, + index: 0, + profile: HevcProfile.Main10, + level: 123, + colorPrimaries: ColorPrimaries.Bt2020, + colorTransfer: ColorTransfer.AribStdB67, + colorMatrix: ColorMatrix.Bt2020Nc, + dvProfile: DvProfile.Dvhe08, + dvLevel: 5, + dvBlSignalCompatibilityId: DvSignalCompatibility.Hlg, + }, + audio: { codecName: 'aac', bitrate: 175_477, index: 1, profile: AacProfile.Lc }, + keyframes: { + totalDuration: 12_290, + packetCount: 1229, + outputFrames: 1303, + pts: [ + 0, 601, 1201, 1802, 2402, 3003, 3604, 4204, 4805, 5405, 6006, 6607, 7207, 7808, 8408, 9009, 9609, 10_210, + 10_811, 11_411, 12_062, 12_703, + ], + accDuration: [ + 10, 580, 1180, 1780, 2380, 2980, 3580, 4180, 4780, 5380, 5980, 6580, 7180, 7780, 8380, 8980, 9580, 10_180, + 10_780, 11_380, 11_780, 12_100, + ], + ownDuration: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10], + }, + }, +]; + +const isExpected = (name: T, id: string, expected: Omit) => { + const { table, ref } = database.dynamic; + const res = database.selectFrom(table(name).as('t')).selectAll().where(ref('assetId'), '=', id).executeTakeFirst(); + return expect(res).resolves.toEqual({ ...expected, assetId: id }); +}; + +describe('video metadata extraction', () => { + it.each(fixtures)('$file', async ({ file, video, audio, keyframes }) => { + const ctx = new ExifTestContext(database); + const { user } = await ctx.newUser(); + const originalPath = resolve(testAssetsDir, 'videos', file); + const { asset } = await ctx.newAsset({ ownerId: user.id, originalPath, type: AssetType.Video }); + + await ctx.sut.handleMetadataExtraction({ id: asset.id }); + + await isExpected('asset_audio', asset.id, audio); + await isExpected('asset_video', asset.id, video); + await isExpected('asset_keyframe', asset.id, keyframes); + }); +});