Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(server): Some MTS videos fail to generate thumbnail #14134

Merged
merged 5 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion server/src/interfaces/media.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,12 @@ export interface ImageBuffer {
}

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 {
Expand Down
16 changes: 16 additions & 0 deletions server/src/services/media.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
Expand Down
13 changes: 9 additions & 4 deletions server/src/services/media.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export class MediaService extends BaseService {
const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
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);
if (!mainVideoStream) {
throw new Error(`No video streams found for asset ${asset.id}`);
Expand All @@ -248,9 +248,14 @@ export class MediaService extends BaseService {

const previewConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.preview.size.toString() });
const thumbnailConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.thumbnail.size.toString() });

const previewOptions = previewConfig.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream);
const thumbnailOptions = thumbnailConfig.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream);
const previewOptions = previewConfig.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream, format);
const thumbnailOptions = thumbnailConfig.getCommand(
TranscodeTarget.VIDEO,
mainVideoStream,
mainAudioStream,
format,
);
this.logger.error(format.formatName);
await this.mediaRepository.transcode(asset.originalPath, previewPath, previewOptions);
await this.mediaRepository.transcode(asset.originalPath, thumbnailPath, thumbnailOptions);

Expand Down
19 changes: 14 additions & 5 deletions server/src/utils/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
TranscodeCommand,
VideoCodecHWConfig,
VideoCodecSWConfig,
VideoFormat,
VideoStreamInfo,
} from 'src/interfaces/media.interface';

Expand Down Expand Up @@ -77,9 +78,14 @@ export class BaseConfig implements VideoCodecSWConfig {
return handler;
}

getCommand(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
getCommand(
target: TranscodeTarget,
videoStream: VideoStreamInfo,
audioStream?: AudioStreamInfo,
format?: VideoFormat,
) {
const options = {
inputOptions: this.getBaseInputOptions(videoStream),
inputOptions: this.getBaseInputOptions(videoStream, format),
outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'],
twoPass: this.eligibleForTwoPass(),
progress: { frameCount: videoStream.frameCount, percentInterval: 5 },
Expand All @@ -101,7 +107,7 @@ export class BaseConfig implements VideoCodecSWConfig {
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
getBaseInputOptions(videoStream: VideoStreamInfo): string[] {
getBaseInputOptions(videoStream: VideoStreamInfo, format?: VideoFormat): string[] {
return this.getInputThreadOptions();
}

Expand Down Expand Up @@ -377,8 +383,11 @@ export class ThumbnailConfig extends BaseConfig {
return new ThumbnailConfig(config);
}

getBaseInputOptions(): string[] {
return ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'];
getBaseInputOptions(videoStream: VideoStreamInfo, format?: VideoFormat): string[] {
// 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'
Lukasdotcom marked this conversation as resolved.
Show resolved Hide resolved
? ['-sws_flags accurate_rnd+full_chroma_int']
: ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'];
}

getBaseOutputOptions() {
Expand Down
7 changes: 7 additions & 0 deletions server/test/fixtures/media.stub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ export const probeStub = {
...probeStubDefault,
videoStreams: [{ ...probeStubDefaultVideoStream[0], bitrate: 40_000_000 }],
}),
videoStreamMTS: Object.freeze<VideoInfo>({
...probeStubDefault,
format: {
...probeStubDefaultFormat,
formatName: 'mpegts',
},
}),
videoStreamHDR: Object.freeze<VideoInfo>({
...probeStubDefault,
videoStreams: [
Expand Down
Loading