mirror of
https://github.com/KevinMidboe/immich.git
synced 2026-01-08 10:15:49 +00:00
feat(server): wide gamut thumbnails (#3658)
This commit is contained in:
@@ -9,7 +9,6 @@ import { ISystemConfigRepository, SystemConfigFFmpegDto } from '../system-config
|
||||
import { SystemConfigCore } from '../system-config/system-config.core';
|
||||
import { AudioStreamInfo, IMediaRepository, VideoCodecHWConfig, VideoStreamInfo } from './media.repository';
|
||||
import { H264Config, HEVCConfig, NVENCConfig, QSVConfig, ThumbnailConfig, VAAPIConfig, VP9Config } from './media.util';
|
||||
|
||||
@Injectable()
|
||||
export class MediaService {
|
||||
private logger = new Logger(MediaService.name);
|
||||
@@ -21,9 +20,9 @@ export class MediaService {
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(ISystemConfigRepository) systemConfig: ISystemConfigRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
) {
|
||||
this.configCore = new SystemConfigCore(systemConfig);
|
||||
this.configCore = new SystemConfigCore(configRepository);
|
||||
}
|
||||
|
||||
async handleQueueGenerateThumbnails(job: IBaseJob) {
|
||||
@@ -59,38 +58,53 @@ export class MediaService {
|
||||
return false;
|
||||
}
|
||||
|
||||
const resizePath = this.storageCore.getFolderLocation(StorageFolder.THUMBNAILS, asset.ownerId);
|
||||
this.storageRepository.mkdirSync(resizePath);
|
||||
const jpegThumbnailPath = join(resizePath, `${asset.id}.jpeg`);
|
||||
const { thumbnail } = await this.configCore.getConfig();
|
||||
const resizePath = await this.generateThumbnail(asset, 'jpeg');
|
||||
await this.assetRepository.save({ id: asset.id, resizePath });
|
||||
return true;
|
||||
}
|
||||
|
||||
async generateThumbnail(asset: AssetEntity, format: 'jpeg' | 'webp') {
|
||||
let path;
|
||||
switch (asset.type) {
|
||||
case AssetType.IMAGE:
|
||||
await this.mediaRepository.resize(asset.originalPath, jpegThumbnailPath, {
|
||||
size: thumbnail.jpegSize,
|
||||
format: 'jpeg',
|
||||
});
|
||||
this.logger.log(`Successfully generated image thumbnail ${asset.id}`);
|
||||
path = await this.generateImageThumbnail(asset, format);
|
||||
break;
|
||||
case AssetType.VIDEO:
|
||||
const { audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath);
|
||||
const mainVideoStream = this.getMainStream(videoStreams);
|
||||
if (!mainVideoStream) {
|
||||
this.logger.error(`Could not extract thumbnail for asset ${asset.id}: no video streams found`);
|
||||
return false;
|
||||
}
|
||||
const mainAudioStream = this.getMainStream(audioStreams);
|
||||
const { ffmpeg } = await this.configCore.getConfig();
|
||||
const config = { ...ffmpeg, targetResolution: thumbnail.jpegSize.toString(), twoPass: false };
|
||||
const options = new ThumbnailConfig(config).getOptions(mainVideoStream, mainAudioStream);
|
||||
await this.mediaRepository.transcode(asset.originalPath, jpegThumbnailPath, options);
|
||||
this.logger.log(`Successfully generated video thumbnail ${asset.id}`);
|
||||
path = await this.generateVideoThumbnail(asset, format);
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedMediaTypeException(`Unsupported asset type for thumbnail generation: ${asset.type}`);
|
||||
}
|
||||
this.logger.log(
|
||||
`Successfully generated ${format.toUpperCase()} ${asset.type.toLowerCase()} thumbnail for asset ${asset.id}`,
|
||||
);
|
||||
return path;
|
||||
}
|
||||
|
||||
await this.assetRepository.save({ id: asset.id, resizePath: jpegThumbnailPath });
|
||||
async generateImageThumbnail(asset: AssetEntity, format: 'jpeg' | 'webp') {
|
||||
const { thumbnail } = await this.configCore.getConfig();
|
||||
const size = format === 'jpeg' ? thumbnail.jpegSize : thumbnail.webpSize;
|
||||
const thumbnailOptions = { format, size, colorspace: thumbnail.colorspace, quality: thumbnail.quality };
|
||||
const path = this.ensureThumbnailPath(asset, format);
|
||||
await this.mediaRepository.resize(asset.originalPath, path, thumbnailOptions);
|
||||
return path;
|
||||
}
|
||||
|
||||
return true;
|
||||
async generateVideoThumbnail(asset: AssetEntity, format: 'jpeg' | 'webp') {
|
||||
const { ffmpeg, thumbnail } = await this.configCore.getConfig();
|
||||
const size = format === 'jpeg' ? thumbnail.jpegSize : thumbnail.webpSize;
|
||||
const { audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath);
|
||||
const mainVideoStream = this.getMainStream(videoStreams);
|
||||
if (!mainVideoStream) {
|
||||
this.logger.warn(`Skipped thumbnail generation for asset ${asset.id}: no video streams found`);
|
||||
return;
|
||||
}
|
||||
const mainAudioStream = this.getMainStream(audioStreams);
|
||||
const path = this.ensureThumbnailPath(asset, format);
|
||||
const config = { ...ffmpeg, targetResolution: size.toString() };
|
||||
const options = new ThumbnailConfig(config).getOptions(mainVideoStream, mainAudioStream);
|
||||
await this.mediaRepository.transcode(asset.originalPath, path, options);
|
||||
return path;
|
||||
}
|
||||
|
||||
async handleGenerateWebpThumbnail({ id }: IEntityJob) {
|
||||
@@ -99,12 +113,8 @@ export class MediaService {
|
||||
return false;
|
||||
}
|
||||
|
||||
const webpPath = asset.resizePath.replace('jpeg', 'webp').replace('jpg', 'webp');
|
||||
|
||||
const { thumbnail } = await this.configCore.getConfig();
|
||||
await this.mediaRepository.resize(asset.resizePath, webpPath, { size: thumbnail.webpSize, format: 'webp' });
|
||||
const webpPath = await this.generateThumbnail(asset, 'webp');
|
||||
await this.assetRepository.save({ id: asset.id, webpPath });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -289,4 +299,10 @@ export class MediaService {
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
ensureThumbnailPath(asset: AssetEntity, extension: string): string {
|
||||
const folderPath = this.storageCore.getFolderLocation(StorageFolder.THUMBNAILS, asset.ownerId);
|
||||
this.storageRepository.mkdirSync(folderPath);
|
||||
return join(folderPath, `${asset.id}.${extension}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user