mirror of
https://github.com/immich-app/immich.git
synced 2025-06-23 15:30:51 -04:00
fix(server): Some MTS videos fail to generate thumbnail (#14134)
* Stop skipping of all frames in MTS video * Only skip flag for mts videos * Fix lint checks * Adds test * Add comment for why flag is removed
This commit is contained in:
parent
11403abfbc
commit
9203a61709
@ -114,7 +114,12 @@ export interface ImageBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoCodecSWConfig {
|
export interface VideoCodecSWConfig {
|
||||||
getCommand(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream: AudioStreamInfo): TranscodeCommand;
|
getCommand(
|
||||||
|
target: TranscodeTarget,
|
||||||
|
videoStream: VideoStreamInfo,
|
||||||
|
audioStream: AudioStreamInfo,
|
||||||
|
format?: VideoFormat,
|
||||||
|
): TranscodeCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoCodecHWConfig extends VideoCodecSWConfig {
|
export interface VideoCodecHWConfig extends VideoCodecSWConfig {
|
||||||
|
@ -487,6 +487,22 @@ describe(MediaService.name, () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
it('should not skip intra frames for MTS file', async () => {
|
||||||
|
mediaMock.probe.mockResolvedValue(probeStub.videoStreamMTS);
|
||||||
|
assetMock.getById.mockResolvedValue(assetStub.video);
|
||||||
|
await sut.handleGenerateThumbnails({ id: assetStub.video.id });
|
||||||
|
|
||||||
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
|
'/original/path.ext',
|
||||||
|
'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
|
||||||
|
expect.objectContaining({
|
||||||
|
inputOptions: ['-sws_flags accurate_rnd+full_chroma_int'],
|
||||||
|
outputOptions: expect.any(Array),
|
||||||
|
progress: expect.any(Object),
|
||||||
|
twoPass: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should use scaling divisible by 2 even when using quick sync', async () => {
|
it('should use scaling divisible by 2 even when using quick sync', async () => {
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
|
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||||
|
@ -239,7 +239,7 @@ export class MediaService extends BaseService {
|
|||||||
const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
|
const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
|
||||||
this.storageCore.ensureFolders(previewPath);
|
this.storageCore.ensureFolders(previewPath);
|
||||||
|
|
||||||
const { audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath);
|
const { format, audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath);
|
||||||
const mainVideoStream = this.getMainStream(videoStreams);
|
const mainVideoStream = this.getMainStream(videoStreams);
|
||||||
if (!mainVideoStream) {
|
if (!mainVideoStream) {
|
||||||
throw new Error(`No video streams found for asset ${asset.id}`);
|
throw new Error(`No video streams found for asset ${asset.id}`);
|
||||||
@ -248,9 +248,14 @@ export class MediaService extends BaseService {
|
|||||||
|
|
||||||
const previewConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.preview.size.toString() });
|
const previewConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.preview.size.toString() });
|
||||||
const thumbnailConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.thumbnail.size.toString() });
|
const thumbnailConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.thumbnail.size.toString() });
|
||||||
|
const previewOptions = previewConfig.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream, format);
|
||||||
const previewOptions = previewConfig.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream);
|
const thumbnailOptions = thumbnailConfig.getCommand(
|
||||||
const thumbnailOptions = thumbnailConfig.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream);
|
TranscodeTarget.VIDEO,
|
||||||
|
mainVideoStream,
|
||||||
|
mainAudioStream,
|
||||||
|
format,
|
||||||
|
);
|
||||||
|
this.logger.error(format.formatName);
|
||||||
await this.mediaRepository.transcode(asset.originalPath, previewPath, previewOptions);
|
await this.mediaRepository.transcode(asset.originalPath, previewPath, previewOptions);
|
||||||
await this.mediaRepository.transcode(asset.originalPath, thumbnailPath, thumbnailOptions);
|
await this.mediaRepository.transcode(asset.originalPath, thumbnailPath, thumbnailOptions);
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
TranscodeCommand,
|
TranscodeCommand,
|
||||||
VideoCodecHWConfig,
|
VideoCodecHWConfig,
|
||||||
VideoCodecSWConfig,
|
VideoCodecSWConfig,
|
||||||
|
VideoFormat,
|
||||||
VideoStreamInfo,
|
VideoStreamInfo,
|
||||||
} from 'src/interfaces/media.interface';
|
} from 'src/interfaces/media.interface';
|
||||||
|
|
||||||
@ -77,9 +78,14 @@ export class BaseConfig implements VideoCodecSWConfig {
|
|||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCommand(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
|
getCommand(
|
||||||
|
target: TranscodeTarget,
|
||||||
|
videoStream: VideoStreamInfo,
|
||||||
|
audioStream?: AudioStreamInfo,
|
||||||
|
format?: VideoFormat,
|
||||||
|
) {
|
||||||
const options = {
|
const options = {
|
||||||
inputOptions: this.getBaseInputOptions(videoStream),
|
inputOptions: this.getBaseInputOptions(videoStream, format),
|
||||||
outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'],
|
outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'],
|
||||||
twoPass: this.eligibleForTwoPass(),
|
twoPass: this.eligibleForTwoPass(),
|
||||||
progress: { frameCount: videoStream.frameCount, percentInterval: 5 },
|
progress: { frameCount: videoStream.frameCount, percentInterval: 5 },
|
||||||
@ -101,7 +107,7 @@ export class BaseConfig implements VideoCodecSWConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
getBaseInputOptions(videoStream: VideoStreamInfo): string[] {
|
getBaseInputOptions(videoStream: VideoStreamInfo, format?: VideoFormat): string[] {
|
||||||
return this.getInputThreadOptions();
|
return this.getInputThreadOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,8 +383,11 @@ export class ThumbnailConfig extends BaseConfig {
|
|||||||
return new ThumbnailConfig(config);
|
return new ThumbnailConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBaseInputOptions(): string[] {
|
getBaseInputOptions(videoStream: VideoStreamInfo, format?: VideoFormat): string[] {
|
||||||
return ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'];
|
// skip_frame nointra skips all frames for some MPEG-TS files. Look at ffmpeg tickets 7950 and 7895 for more details.
|
||||||
|
return format?.formatName === 'mpegts'
|
||||||
|
? ['-sws_flags accurate_rnd+full_chroma_int']
|
||||||
|
: ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'];
|
||||||
}
|
}
|
||||||
|
|
||||||
getBaseOutputOptions() {
|
getBaseOutputOptions() {
|
||||||
|
7
server/test/fixtures/media.stub.ts
vendored
7
server/test/fixtures/media.stub.ts
vendored
@ -95,6 +95,13 @@ export const probeStub = {
|
|||||||
...probeStubDefault,
|
...probeStubDefault,
|
||||||
videoStreams: [{ ...probeStubDefaultVideoStream[0], bitrate: 40_000_000 }],
|
videoStreams: [{ ...probeStubDefaultVideoStream[0], bitrate: 40_000_000 }],
|
||||||
}),
|
}),
|
||||||
|
videoStreamMTS: Object.freeze<VideoInfo>({
|
||||||
|
...probeStubDefault,
|
||||||
|
format: {
|
||||||
|
...probeStubDefaultFormat,
|
||||||
|
formatName: 'mpegts',
|
||||||
|
},
|
||||||
|
}),
|
||||||
videoStreamHDR: Object.freeze<VideoInfo>({
|
videoStreamHDR: Object.freeze<VideoInfo>({
|
||||||
...probeStubDefault,
|
...probeStubDefault,
|
||||||
videoStreams: [
|
videoStreams: [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user