Merge b172a7e9d3 into 963862b1b9
commit
ec0c84db7d
|
|
@ -244,6 +244,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,
|
||||
},
|
||||
videoStreams: results.streams
|
||||
.filter((stream) => stream.codec_type === 'video' && !stream.disposition?.attached_pic)
|
||||
|
|
|
|||
|
|
@ -2092,6 +2092,58 @@ 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', 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 not scale resolution if no target resolution', async () => {
|
||||
mocks.assetJob.getForVideoConversion.mockResolvedValue({ ...asset, ...probeStub.videoStream2160p });
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ interface UpsertFileOptions {
|
|||
}
|
||||
|
||||
type ThumbnailAsset = NonNullable<Awaited<ReturnType<AssetJobRepository['getForGenerateThumbnailJob']>>>;
|
||||
const FRAGMENTED_MP4_BRANDS = ['iso5', 'iso6', 'dash', 'msdh', 'msix', 'cmfc'];
|
||||
|
||||
@Injectable()
|
||||
export class MediaService extends BaseService {
|
||||
|
|
@ -731,7 +732,7 @@ 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;
|
||||
}
|
||||
|
|
@ -742,8 +743,25 @@ export class MediaService extends BaseService {
|
|||
};
|
||||
|
||||
const name = (formatLongName ? formatLongNameMapping[formatLongName] : undefined) ?? (formatName as VideoContainer);
|
||||
if (name !== VideoContainer.Mp4 && !ffmpegConfig.acceptedContainers.includes(name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return name !== VideoContainer.Mp4 && !ffmpegConfig.acceptedContainers.includes(name);
|
||||
if (!formatName?.includes('mp4') && formatLongName !== 'QuickTime / MOV') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const majorBrand = tags?.major_brand;
|
||||
if (majorBrand && FRAGMENTED_MP4_BRANDS.includes(majorBrand)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const compatibleBrands = tags?.compatible_brands;
|
||||
if (!compatibleBrands) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return FRAGMENTED_MP4_BRANDS.some((brand) => compatibleBrands.includes(brand));
|
||||
}
|
||||
|
||||
isSRGB({
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ export interface VideoFormat {
|
|||
formatLongName?: string;
|
||||
duration: number;
|
||||
bitrate: number;
|
||||
tags?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ImageDimensions {
|
||||
|
|
|
|||
|
|
@ -129,6 +129,13 @@ export class BaseConfig implements VideoCodecSWConfig {
|
|||
twoPass: this.eligibleForTwoPass(),
|
||||
progress: { frameCount: video.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(video);
|
||||
if (filters.length > 0) {
|
||||
|
|
|
|||
|
|
@ -380,6 +380,24 @@ export const videoInfoStub = {
|
|||
...probeStubDefault,
|
||||
videoStreams: [{ ...probeStubDefaultVideoStream[0], codecName: 'h264' }],
|
||||
}),
|
||||
fragmentedMp4: Object.freeze<VideoInfo>({
|
||||
...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<VideoInfo>({
|
||||
...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<VideoInfo>({
|
||||
...probeStubDefault,
|
||||
videoStreams: [{ ...probeStubDefaultVideoStream[0], codecName: 'h264' }],
|
||||
|
|
|
|||
Loading…
Reference in New Issue