|
@@ -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}`);
|
|
|
+ }
|
|
|
}
|