From daa8c52fae6108f2a0e19e2b2595946689cd2f72 Mon Sep 17 00:00:00 2001 From: Qusai Ismael Date: Wed, 1 Apr 2026 12:42:24 +0300 Subject: [PATCH 1/2] refactor: detect fMP4 via probe metadata tags and integrate container mapping --- server/src/repositories/media.repository.ts | 3 + server/src/services/media.service.spec.ts | 69 +++++++++++++++++++++ server/src/services/media.service.ts | 30 ++++++++- server/src/types.ts | 1 + server/src/utils/media.ts | 7 +++ server/test/fixtures/media.stub.ts | 18 ++++++ 6 files changed, 125 insertions(+), 3 deletions(-) diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index 58e006171a..1ee8e75f0a 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -241,6 +241,9 @@ export class MediaRepository { formatLongName: results.format.format_long_name, duration: this.parseFloat(results.format.duration), bitrate: this.parseInt(results.format.bit_rate), + tags: results.format.tags + ? Object.fromEntries(Object.entries(results.format.tags).map(([key, value]) => [key, String(value)])) + : undefined, }, videoStreams: results.streams .filter((stream) => stream.codec_type === 'video' && !stream.disposition?.attached_pic) diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 51a10a39c2..4c24db63c6 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -2055,6 +2055,75 @@ describe(MediaService.name, () => { expect(mocks.media.transcode).not.toHaveBeenCalled(); }); + it('should remux fragmented MP4 (fMP4) files based on probe metadata', async () => { + mocks.media.probe.mockResolvedValue(probeStub.fragmentedMp4); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); + await sut.handleVideoConversion({ id: 'video-id' }); + expect(mocks.media.transcode).toHaveBeenCalledWith( + '/original/path.ext', + expect.any(String), + expect.objectContaining({ + inputOptions: expect.any(Array), + outputOptions: expect.arrayContaining(['-c:v copy', '-c:a copy', '-movflags faststart']), + twoPass: false, + }), + ); + expect(mocks.asset.upsertFile).toHaveBeenCalledWith( + expect.objectContaining({ + type: AssetFileType.EncodedVideo, + isEdited: false, + }), + ); + }); + + it('should remux fragmented MP4 when only compatible_brands indicates fragmentation', async () => { + mocks.media.probe.mockResolvedValue(probeStub.fragmentedMp4CompatibleBrands); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); + await sut.handleVideoConversion({ id: 'video-id' }); + expect(mocks.media.transcode).toHaveBeenCalledWith( + '/original/path.ext', + expect.any(String), + expect.objectContaining({ + outputOptions: expect.arrayContaining(['-c:v copy', '-c:a copy', '-movflags faststart']), + twoPass: false, + }), + ); + }); + + it('should not include encoding options when remuxing fragmented MP4', async () => { + mocks.media.probe.mockResolvedValue(probeStub.fragmentedMp4); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); + await sut.handleVideoConversion({ id: 'video-id' }); + expect(mocks.media.transcode).toHaveBeenCalledWith( + '/original/path.ext', + expect.any(String), + expect.objectContaining({ + outputOptions: expect.not.arrayContaining([ + expect.stringContaining('-preset'), + expect.stringContaining('-crf'), + expect.stringContaining('scale'), + ]), + }), + ); + }); + + it('should still skip non-MP4 containers that do not need transcoding', async () => { + mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); + mocks.systemMetadata.get.mockResolvedValue({ + ffmpeg: { transcode: TranscodePolicy.Required, acceptedVideoCodecs: [VideoCodec.Vp9], acceptedContainers: ['matroska,webm'] }, + }); + await sut.handleVideoConversion({ id: 'video-id' }); + expect(mocks.media.transcode).not.toHaveBeenCalled(); + }); + + it('should skip remux for standard MP4s', async () => { + mocks.media.probe.mockResolvedValue(probeStub.videoStreamH264); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); + const result = await sut.handleVideoConversion({ id: 'video-id' }); + expect(result).toBe(JobStatus.Skipped); + expect(mocks.media.transcode).not.toHaveBeenCalled(); + }); + it('should not scale resolution if no target resolution', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index ea0b1e9142..1bc94e6888 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -56,6 +56,7 @@ interface UpsertFileOptions { } type ThumbnailAsset = NonNullable>>; +const FRAGMENTED_MP4_BRANDS = new Set(['iso5', 'iso6', 'dash', 'msdh', 'msix', 'cmfc']); @Injectable() export class MediaService extends BaseService { @@ -751,13 +752,36 @@ export class MediaService extends BaseService { } } - private isRemuxRequired(ffmpegConfig: SystemConfigFFmpegDto, { formatName, formatLongName }: VideoFormat): boolean { + private isRemuxRequired(ffmpegConfig: SystemConfigFFmpegDto, { formatName, formatLongName, tags }: VideoFormat): boolean { if (ffmpegConfig.transcode === TranscodePolicy.Disabled) { return false; } - const name = formatLongName === 'QuickTime / MOV' ? VideoContainer.Mov : (formatName as VideoContainer); - return name !== VideoContainer.Mp4 && !ffmpegConfig.acceptedContainers.includes(name); + const formatLongNameMapping: Record = { + 'QuickTime / MOV': VideoContainer.Mov, + 'Matroska / WebM': VideoContainer.Webm, + }; + + const name = (formatLongName ? formatLongNameMapping[formatLongName] : undefined) ?? (formatName as VideoContainer); + if (name !== VideoContainer.Mp4 && !ffmpegConfig.acceptedContainers.includes(name)) { + return true; + } + + if (!formatName?.includes('mp4') && formatLongName !== 'QuickTime / MOV') { + return false; + } + + const majorBrand = tags?.major_brand; + if (majorBrand && FRAGMENTED_MP4_BRANDS.has(majorBrand)) { + return true; + } + + const compatibleBrands = tags?.compatible_brands; + if (!compatibleBrands) { + return false; + } + + return Array.from(FRAGMENTED_MP4_BRANDS).some((brand) => compatibleBrands.includes(brand)); } isSRGB({ diff --git a/server/src/types.ts b/server/src/types.ts index 33174e187e..d09ce23d41 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -106,6 +106,7 @@ export interface VideoFormat { formatLongName?: string; duration: number; bitrate: number; + tags?: Record; } export interface ImageDimensions { diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index ce185305bd..d52695a7c8 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -95,6 +95,13 @@ export class BaseConfig implements VideoCodecSWConfig { twoPass: this.eligibleForTwoPass(), progress: { frameCount: videoStream.frameCount, percentInterval: 5 }, } as TranscodeCommand; + + // Skip two-pass and encoder-specific options when we're only remuxing streams. + if (target === TranscodeTarget.None) { + options.twoPass = false; + return options; + } + if ([TranscodeTarget.All, TranscodeTarget.Video].includes(target)) { const filters = this.getFilterOptions(videoStream); if (filters.length > 0) { diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts index 23617fcaf0..78f38dd649 100644 --- a/server/test/fixtures/media.stub.ts +++ b/server/test/fixtures/media.stub.ts @@ -259,6 +259,24 @@ export const probeStub = { ...probeStubDefault, videoStreams: [{ ...probeStubDefaultVideoStream[0], codecName: 'h264' }], }), + fragmentedMp4: Object.freeze({ + ...probeStubDefault, + videoStreams: [{ ...probeStubDefaultVideoStream[0], codecName: 'h264' }], + format: { + ...probeStubDefaultFormat, + formatName: 'mov,mp4,m4a,3gp,3g2,mj2', + tags: { major_brand: 'iso6', compatible_brands: 'isomiso6dashmp41' }, + }, + }), + fragmentedMp4CompatibleBrands: Object.freeze({ + ...probeStubDefault, + videoStreams: [{ ...probeStubDefaultVideoStream[0], codecName: 'h264' }], + format: { + ...probeStubDefaultFormat, + formatName: 'mov,mp4,m4a,3gp,3g2,mj2', + tags: { major_brand: 'isom', compatible_brands: 'isomiso6dashmp41' }, + }, + }), videoStreamAvi: Object.freeze({ ...probeStubDefault, videoStreams: [{ ...probeStubDefaultVideoStream[0], codecName: 'h264' }], From fa6d9e50ecaed15c59ce6f8899a43861766461ea Mon Sep 17 00:00:00 2001 From: Qusai Ismael Date: Fri, 10 Apr 2026 15:22:11 +0300 Subject: [PATCH 2/2] refactor: optimize fMP4 detection --- server/src/repositories/media.repository.ts | 4 +--- server/src/services/media.service.spec.ts | 19 +------------------ server/src/services/media.service.ts | 6 +++--- 3 files changed, 5 insertions(+), 24 deletions(-) diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index 1ee8e75f0a..3fef1a7e0a 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -241,9 +241,7 @@ export class MediaRepository { formatLongName: results.format.format_long_name, duration: this.parseFloat(results.format.duration), bitrate: this.parseInt(results.format.bit_rate), - tags: results.format.tags - ? Object.fromEntries(Object.entries(results.format.tags).map(([key, value]) => [key, String(value)])) - : undefined, + tags: results.format.tags, }, videoStreams: results.streams .filter((stream) => stream.codec_type === 'video' && !stream.disposition?.attached_pic) diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 4c24db63c6..cc88ad7d4e 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -2090,7 +2090,7 @@ describe(MediaService.name, () => { ); }); - it('should not include encoding options when remuxing fragmented MP4', async () => { + it('should not include encoding options when remuxing', async () => { mocks.media.probe.mockResolvedValue(probeStub.fragmentedMp4); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); await sut.handleVideoConversion({ id: 'video-id' }); @@ -2107,23 +2107,6 @@ describe(MediaService.name, () => { ); }); - it('should still skip non-MP4 containers that do not need transcoding', async () => { - mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); - mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { transcode: TranscodePolicy.Required, acceptedVideoCodecs: [VideoCodec.Vp9], acceptedContainers: ['matroska,webm'] }, - }); - await sut.handleVideoConversion({ id: 'video-id' }); - expect(mocks.media.transcode).not.toHaveBeenCalled(); - }); - - it('should skip remux for standard MP4s', async () => { - mocks.media.probe.mockResolvedValue(probeStub.videoStreamH264); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); - const result = await sut.handleVideoConversion({ id: 'video-id' }); - expect(result).toBe(JobStatus.Skipped); - expect(mocks.media.transcode).not.toHaveBeenCalled(); - }); - it('should not scale resolution if no target resolution', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 1bc94e6888..ef9aaed8fc 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -56,7 +56,7 @@ interface UpsertFileOptions { } type ThumbnailAsset = NonNullable>>; -const FRAGMENTED_MP4_BRANDS = new Set(['iso5', 'iso6', 'dash', 'msdh', 'msix', 'cmfc']); +const FRAGMENTED_MP4_BRANDS = ['iso5', 'iso6', 'dash', 'msdh', 'msix', 'cmfc']; @Injectable() export class MediaService extends BaseService { @@ -772,7 +772,7 @@ export class MediaService extends BaseService { } const majorBrand = tags?.major_brand; - if (majorBrand && FRAGMENTED_MP4_BRANDS.has(majorBrand)) { + if (majorBrand && FRAGMENTED_MP4_BRANDS.includes(majorBrand)) { return true; } @@ -781,7 +781,7 @@ export class MediaService extends BaseService { return false; } - return Array.from(FRAGMENTED_MP4_BRANDS).some((brand) => compatibleBrands.includes(brand)); + return FRAGMENTED_MP4_BRANDS.some((brand) => compatibleBrands.includes(brand)); } isSRGB({