2023-12-14 17:55:40 +01:00
import { Inject , Injectable , UnsupportedMediaTypeException } from '@nestjs/common' ;
2024-03-20 21:20:38 +01:00
import { StorageCore , StorageFolder } from 'src/cores/storage.core' ;
import { SystemConfigCore } from 'src/cores/system-config.core' ;
2024-03-20 19:32:04 +01:00
import { JOBS_ASSET_PAGINATION_SIZE , JobName , QueueName } from 'src/domain/job/job.constants' ;
import { IBaseJob , IEntityJob } from 'src/domain/job/job.interface' ;
2023-10-30 15:39:37 +01:00
import {
H264Config ,
HEVCConfig ,
NVENCConfig ,
QSVConfig ,
RKMPPConfig ,
ThumbnailConfig ,
VAAPIConfig ,
VP9Config ,
2024-03-20 19:32:04 +01:00
} from 'src/domain/media/media.util' ;
2024-03-20 23:53:07 +01:00
import { SystemConfigFFmpegDto } from 'src/dtos/system-config-ffmpeg.dto' ;
2024-03-20 22:02:51 +01:00
import { AssetEntity , AssetType } from 'src/entities/asset.entity' ;
import { AssetPathType } from 'src/entities/move.entity' ;
2024-03-20 19:32:04 +01:00
import {
AudioCodec ,
Colorspace ,
TranscodeHWAccel ,
TranscodePolicy ,
TranscodeTarget ,
VideoCodec ,
2024-03-20 22:02:51 +01:00
} from 'src/entities/system-config.entity' ;
2024-03-20 19:32:04 +01:00
import { ImmichLogger } from 'src/infra/logger' ;
2024-03-20 21:42:58 +01:00
import { IAssetRepository , WithoutProperty } from 'src/interfaces/asset.repository' ;
import { ICryptoRepository } from 'src/interfaces/crypto.repository' ;
import { IJobRepository , JobItem , JobStatus } from 'src/interfaces/job.repository' ;
import {
AudioStreamInfo ,
IMediaRepository ,
VideoCodecHWConfig ,
VideoStreamInfo ,
} from 'src/interfaces/media.repository' ;
import { IMoveRepository } from 'src/interfaces/move.repository' ;
import { IPersonRepository } from 'src/interfaces/person.repository' ;
import { IStorageRepository } from 'src/interfaces/storage.repository' ;
import { ISystemConfigRepository } from 'src/interfaces/system-config.repository' ;
2024-03-20 21:04:03 +01:00
import { usePagination } from 'src/utils' ;
2023-09-08 08:49:43 +02:00
2023-02-25 15:12:03 +01:00
@Injectable ( )
export class MediaService {
2023-12-14 17:55:40 +01:00
private logger = new ImmichLogger ( MediaService . name ) ;
2023-04-04 16:48:02 +02:00
private configCore : SystemConfigCore ;
2023-09-25 17:07:21 +02:00
private storageCore : StorageCore ;
2024-03-09 03:17:26 +01:00
private hasOpenCL? : boolean = undefined ;
2023-02-25 15:12:03 +01:00
constructor (
@Inject ( IAssetRepository ) private assetRepository : IAssetRepository ,
2023-09-08 08:49:43 +02:00
@Inject ( IPersonRepository ) private personRepository : IPersonRepository ,
2023-02-25 15:12:03 +01:00
@Inject ( IJobRepository ) private jobRepository : IJobRepository ,
@Inject ( IMediaRepository ) private mediaRepository : IMediaRepository ,
@Inject ( IStorageRepository ) private storageRepository : IStorageRepository ,
2023-09-03 08:21:51 +02:00
@Inject ( ISystemConfigRepository ) configRepository : ISystemConfigRepository ,
2023-10-11 04:14:44 +02:00
@Inject ( IMoveRepository ) moveRepository : IMoveRepository ,
2024-03-20 19:32:04 +01:00
@Inject ( ICryptoRepository ) cryptoRepository : ICryptoRepository ,
2023-04-04 16:48:02 +02:00
) {
2023-10-09 02:51:03 +02:00
this . configCore = SystemConfigCore . create ( configRepository ) ;
2023-12-29 19:41:33 +01:00
this . storageCore = StorageCore . create (
assetRepository ,
moveRepository ,
personRepository ,
cryptoRepository ,
configRepository ,
storageRepository ,
) ;
2023-04-04 16:48:02 +02:00
}
2023-02-25 15:12:03 +01:00
2024-03-15 14:16:54 +01:00
async handleQueueGenerateThumbnails ( { force } : IBaseJob ) : Promise < JobStatus > {
2023-05-26 21:43:24 +02:00
const assetPagination = usePagination ( JOBS_ASSET_PAGINATION_SIZE , ( pagination ) = > {
return force
? this . assetRepository . getAll ( pagination )
: this . assetRepository . getWithout ( pagination , WithoutProperty . THUMBNAIL ) ;
} ) ;
2023-03-20 16:55:28 +01:00
2023-05-26 21:43:24 +02:00
for await ( const assets of assetPagination ) {
2024-01-01 21:45:42 +01:00
const jobs : JobItem [ ] = [ ] ;
2023-05-26 21:43:24 +02:00
for ( const asset of assets ) {
2023-06-18 05:22:31 +02:00
if ( ! asset . resizePath || force ) {
2024-01-01 21:45:42 +01:00
jobs . push ( { name : JobName.GENERATE_JPEG_THUMBNAIL , data : { id : asset.id } } ) ;
2023-06-18 05:22:31 +02:00
continue ;
}
if ( ! asset . webpPath ) {
2024-01-01 21:45:42 +01:00
jobs . push ( { name : JobName.GENERATE_WEBP_THUMBNAIL , data : { id : asset.id } } ) ;
2023-06-18 05:22:31 +02:00
}
if ( ! asset . thumbhash ) {
2024-01-01 21:45:42 +01:00
jobs . push ( { name : JobName.GENERATE_THUMBHASH_THUMBNAIL , data : { id : asset.id } } ) ;
2023-06-18 05:22:31 +02:00
}
2023-03-20 16:55:28 +01:00
}
2024-01-01 21:45:42 +01:00
await this . jobRepository . queueAll ( jobs ) ;
2023-03-20 16:55:28 +01:00
}
2024-01-01 21:45:42 +01:00
const jobs : JobItem [ ] = [ ] ;
2024-01-18 06:08:48 +01:00
const personPagination = usePagination ( JOBS_ASSET_PAGINATION_SIZE , ( pagination ) = >
this . personRepository . getAll ( pagination , { where : force ? undefined : { thumbnailPath : '' } } ) ,
) ;
for await ( const people of personPagination ) {
for ( const person of people ) {
if ( ! person . faceAssetId ) {
const face = await this . personRepository . getRandomFace ( person . id ) ;
if ( ! face ) {
continue ;
}
await this . personRepository . update ( { id : person.id , faceAssetId : face.assetId } ) ;
2023-09-26 09:03:22 +02:00
}
2024-01-18 06:08:48 +01:00
jobs . push ( { name : JobName.GENERATE_PERSON_THUMBNAIL , data : { id : person.id } } ) ;
2023-09-08 08:49:43 +02:00
}
}
2024-01-01 21:45:42 +01:00
await this . jobRepository . queueAll ( jobs ) ;
2024-03-15 14:16:54 +01:00
return JobStatus . SUCCESS ;
2023-05-26 21:43:24 +02:00
}
2023-04-12 03:28:25 +02:00
2024-03-15 14:16:54 +01:00
async handleQueueMigration ( ) : Promise < JobStatus > {
2023-09-25 17:07:21 +02:00
const assetPagination = usePagination ( JOBS_ASSET_PAGINATION_SIZE , ( pagination ) = >
this . assetRepository . getAll ( pagination ) ,
) ;
const { active , waiting } = await this . jobRepository . getJobCounts ( QueueName . MIGRATION ) ;
if ( active === 1 && waiting === 0 ) {
await this . storageCore . removeEmptyDirs ( StorageFolder . THUMBNAILS ) ;
await this . storageCore . removeEmptyDirs ( StorageFolder . ENCODED_VIDEO ) ;
}
for await ( const assets of assetPagination ) {
2024-01-01 21:45:42 +01:00
await this . jobRepository . queueAll (
assets . map ( ( asset ) = > ( { name : JobName.MIGRATE_ASSET , data : { id : asset.id } } ) ) ,
) ;
2023-09-25 17:07:21 +02:00
}
2024-01-18 06:08:48 +01:00
const personPagination = usePagination ( JOBS_ASSET_PAGINATION_SIZE , ( pagination ) = >
this . personRepository . getAll ( pagination ) ,
2024-01-01 21:45:42 +01:00
) ;
2023-09-25 17:07:21 +02:00
2024-01-18 06:08:48 +01:00
for await ( const people of personPagination ) {
await this . jobRepository . queueAll (
people . map ( ( person ) = > ( { name : JobName.MIGRATE_PERSON , data : { id : person.id } } ) ) ,
) ;
}
2024-03-15 14:16:54 +01:00
return JobStatus . SUCCESS ;
2023-09-25 17:07:21 +02:00
}
2024-03-15 14:16:54 +01:00
async handleAssetMigration ( { id } : IEntityJob ) : Promise < JobStatus > {
2023-09-25 17:07:21 +02:00
const [ asset ] = await this . assetRepository . getByIds ( [ id ] ) ;
if ( ! asset ) {
2024-03-15 14:16:54 +01:00
return JobStatus . FAILED ;
2023-09-25 17:07:21 +02:00
}
2023-10-11 04:14:44 +02:00
await this . storageCore . moveAssetFile ( asset , AssetPathType . JPEG_THUMBNAIL ) ;
await this . storageCore . moveAssetFile ( asset , AssetPathType . WEBP_THUMBNAIL ) ;
await this . storageCore . moveAssetFile ( asset , AssetPathType . ENCODED_VIDEO ) ;
2023-09-25 17:07:21 +02:00
2024-03-15 14:16:54 +01:00
return JobStatus . SUCCESS ;
2023-09-25 17:07:21 +02:00
}
2024-03-15 14:16:54 +01:00
async handleGenerateJpegThumbnail ( { id } : IEntityJob ) : Promise < JobStatus > {
2024-03-14 06:58:09 +01:00
const [ asset ] = await this . assetRepository . getByIds ( [ id ] , { exifInfo : true } ) ;
2023-04-12 03:28:25 +02:00
if ( ! asset ) {
2024-03-15 14:16:54 +01:00
return JobStatus . FAILED ;
2023-04-12 03:28:25 +02:00
}
2023-02-25 15:12:03 +01:00
2023-09-03 08:21:51 +02:00
const resizePath = await this . generateThumbnail ( asset , 'jpeg' ) ;
2024-03-20 03:42:10 +01:00
await this . assetRepository . update ( { id : asset.id , resizePath } ) ;
2024-03-15 14:16:54 +01:00
return JobStatus . SUCCESS ;
2023-09-03 08:21:51 +02:00
}
2023-05-26 21:43:24 +02:00
2023-10-11 04:14:44 +02:00
private async generateThumbnail ( asset : AssetEntity , format : 'jpeg' | 'webp' ) {
const { thumbnail , ffmpeg } = await this . configCore . getConfig ( ) ;
const size = format === 'jpeg' ? thumbnail.jpegSize : thumbnail.webpSize ;
const path =
2023-10-23 17:52:21 +02:00
format === 'jpeg' ? StorageCore . getLargeThumbnailPath ( asset ) : StorageCore . getSmallThumbnailPath ( asset ) ;
2023-10-11 04:14:44 +02:00
this . storageCore . ensureFolders ( path ) ;
2023-06-15 05:42:35 +02:00
switch ( asset . type ) {
2024-02-02 04:18:00 +01:00
case AssetType . IMAGE : {
2023-10-11 04:14:44 +02:00
const colorspace = this . isSRGB ( asset ) ? Colorspace.SRGB : thumbnail.colorspace ;
const thumbnailOptions = { format , size , colorspace , quality : thumbnail.quality } ;
await this . mediaRepository . resize ( asset . originalPath , path , thumbnailOptions ) ;
2023-06-15 05:42:35 +02:00
break ;
2024-02-02 04:18:00 +01:00
}
2023-10-11 04:14:44 +02:00
2024-02-02 04:18:00 +01:00
case AssetType . VIDEO : {
2023-10-11 04:14:44 +02:00
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 config = { . . . ffmpeg , targetResolution : size.toString ( ) } ;
2024-02-14 17:24:39 +01:00
const options = new ThumbnailConfig ( config ) . getOptions ( TranscodeTarget . VIDEO , mainVideoStream , mainAudioStream ) ;
2023-10-11 04:14:44 +02:00
await this . mediaRepository . transcode ( asset . originalPath , path , options ) ;
2023-06-15 05:42:35 +02:00
break ;
2024-02-02 04:18:00 +01:00
}
2023-10-11 04:14:44 +02:00
2024-02-02 04:18:00 +01:00
default : {
2023-09-03 08:21:51 +02:00
throw new UnsupportedMediaTypeException ( ` Unsupported asset type for thumbnail generation: ${ asset . type } ` ) ;
2024-02-02 04:18:00 +01:00
}
2023-05-26 21:43:24 +02:00
}
2023-09-03 08:21:51 +02:00
this . logger . log (
` Successfully generated ${ format . toUpperCase ( ) } ${ asset . type . toLowerCase ( ) } thumbnail for asset ${ asset . id } ` ,
) ;
return path ;
}
2023-02-25 15:12:03 +01:00
2024-03-15 14:16:54 +01:00
async handleGenerateWebpThumbnail ( { id } : IEntityJob ) : Promise < JobStatus > {
2024-03-14 06:58:09 +01:00
const [ asset ] = await this . assetRepository . getByIds ( [ id ] , { exifInfo : true } ) ;
2023-09-05 01:24:55 +02:00
if ( ! asset ) {
2024-03-15 14:16:54 +01:00
return JobStatus . FAILED ;
2023-02-25 15:12:03 +01:00
}
2023-09-03 08:21:51 +02:00
const webpPath = await this . generateThumbnail ( asset , 'webp' ) ;
2024-03-20 03:42:10 +01:00
await this . assetRepository . update ( { id : asset.id , webpPath } ) ;
2024-03-15 14:16:54 +01:00
return JobStatus . SUCCESS ;
2023-02-25 15:12:03 +01:00
}
2023-04-04 16:48:02 +02:00
2024-03-15 14:16:54 +01:00
async handleGenerateThumbhashThumbnail ( { id } : IEntityJob ) : Promise < JobStatus > {
2023-06-18 05:22:31 +02:00
const [ asset ] = await this . assetRepository . getByIds ( [ id ] ) ;
if ( ! asset ? . resizePath ) {
2024-03-15 14:16:54 +01:00
return JobStatus . FAILED ;
2023-06-18 05:22:31 +02:00
}
const thumbhash = await this . mediaRepository . generateThumbhash ( asset . resizePath ) ;
2024-03-20 03:42:10 +01:00
await this . assetRepository . update ( { id : asset.id , thumbhash } ) ;
2023-06-18 05:22:31 +02:00
2024-03-15 14:16:54 +01:00
return JobStatus . SUCCESS ;
2023-06-18 05:22:31 +02:00
}
2024-03-15 14:16:54 +01:00
async handleQueueVideoConversion ( job : IBaseJob ) : Promise < JobStatus > {
2023-04-04 16:48:02 +02:00
const { force } = job ;
2023-05-26 21:43:24 +02:00
const assetPagination = usePagination ( JOBS_ASSET_PAGINATION_SIZE , ( pagination ) = > {
return force
? this . assetRepository . getAll ( pagination , { type : AssetType . VIDEO } )
: this . assetRepository . getWithout ( pagination , WithoutProperty . ENCODED_VIDEO ) ;
} ) ;
for await ( const assets of assetPagination ) {
2024-01-01 21:45:42 +01:00
await this . jobRepository . queueAll (
assets . map ( ( asset ) = > ( { name : JobName.VIDEO_CONVERSION , data : { id : asset.id } } ) ) ,
) ;
2023-04-04 16:48:02 +02:00
}
2024-03-15 14:16:54 +01:00
return JobStatus . SUCCESS ;
2023-05-26 21:43:24 +02:00
}
2023-04-11 15:56:52 +02:00
2024-03-15 14:16:54 +01:00
async handleVideoConversion ( { id } : IEntityJob ) : Promise < JobStatus > {
2023-05-26 21:43:24 +02:00
const [ asset ] = await this . assetRepository . getByIds ( [ id ] ) ;
2023-07-05 07:36:16 +02:00
if ( ! asset || asset . type !== AssetType . VIDEO ) {
2024-03-15 14:16:54 +01:00
return JobStatus . FAILED ;
2023-04-11 15:56:52 +02:00
}
2023-04-04 16:48:02 +02:00
2023-05-26 21:43:24 +02:00
const input = asset . originalPath ;
2023-10-23 17:52:21 +02:00
const output = StorageCore . getEncodedVideoPath ( asset ) ;
2023-10-11 04:14:44 +02:00
this . storageCore . ensureFolders ( output ) ;
2023-05-26 21:43:24 +02:00
const { videoStreams , audioStreams , format } = await this . mediaRepository . probe ( input ) ;
2023-08-29 11:01:42 +02:00
const mainVideoStream = this . getMainStream ( videoStreams ) ;
const mainAudioStream = this . getMainStream ( audioStreams ) ;
2023-05-26 21:43:24 +02:00
const containerExtension = format . formatName ;
2023-07-09 22:15:34 +02:00
if ( ! mainVideoStream || ! containerExtension ) {
2024-03-15 14:16:54 +01:00
return JobStatus . FAILED ;
2023-05-26 21:43:24 +02:00
}
2023-04-04 16:48:02 +02:00
2023-12-28 06:34:00 +01:00
if ( ! mainVideoStream . height || ! mainVideoStream . width ) {
this . logger . warn ( ` Skipped transcoding for asset ${ asset . id } : no video streams found ` ) ;
2024-03-15 14:16:54 +01:00
return JobStatus . FAILED ;
2023-12-28 06:34:00 +01:00
}
2023-05-26 21:43:24 +02:00
const { ffmpeg : config } = await this . configCore . getConfig ( ) ;
2023-04-04 16:48:02 +02:00
2024-02-14 17:24:39 +01:00
const target = this . getTranscodeTarget ( config , mainVideoStream , mainAudioStream ) ;
if ( target === TranscodeTarget . NONE ) {
2023-12-28 06:34:00 +01:00
if ( asset . encodedVideoPath ) {
this . logger . log ( ` Transcoded video exists for asset ${ asset . id } , but is no longer required. Deleting... ` ) ;
await this . jobRepository . queue ( { name : JobName.DELETE_FILES , data : { files : [ asset . encodedVideoPath ] } } ) ;
2024-03-20 03:42:10 +01:00
await this . assetRepository . update ( { id : asset.id , encodedVideoPath : null } ) ;
2023-12-28 06:34:00 +01:00
}
2024-03-15 14:16:54 +01:00
return JobStatus . SKIPPED ;
2023-05-26 21:43:24 +02:00
}
2023-04-04 16:48:02 +02:00
2023-07-09 04:43:11 +02:00
let transcodeOptions ;
try {
2024-02-14 17:24:39 +01:00
transcodeOptions = await this . getCodecConfig ( config ) . then ( ( c ) = >
c . getOptions ( target , mainVideoStream , mainAudioStream ) ,
) ;
2024-02-02 04:18:00 +01:00
} catch ( error ) {
this . logger . error ( ` An error occurred while configuring transcoding options: ${ error } ` ) ;
2024-03-15 14:16:54 +01:00
return JobStatus . FAILED ;
2023-07-09 04:43:11 +02:00
}
2023-04-06 05:32:59 +02:00
2024-02-14 17:24:39 +01:00
this . logger . log ( ` Started encoding video ${ asset . id } ${ JSON . stringify ( transcodeOptions ) } ` ) ;
2023-08-02 03:56:10 +02:00
try {
await this . mediaRepository . transcode ( input , output , transcodeOptions ) ;
2024-02-02 04:18:00 +01:00
} catch ( error ) {
this . logger . error ( error ) ;
2023-08-29 11:01:42 +02:00
if ( config . accel !== TranscodeHWAccel . DISABLED ) {
2023-08-02 03:56:10 +02:00
this . logger . error (
` Error occurred during transcoding. Retrying with ${ config . accel . toUpperCase ( ) } acceleration disabled. ` ,
) ;
}
config . accel = TranscodeHWAccel . DISABLED ;
2024-02-14 17:24:39 +01:00
transcodeOptions = await this . getCodecConfig ( config ) . then ( ( c ) = >
c . getOptions ( target , mainVideoStream , mainAudioStream ) ,
) ;
2023-08-02 03:56:10 +02:00
await this . mediaRepository . transcode ( input , output , transcodeOptions ) ;
}
2023-04-04 16:48:02 +02:00
2024-02-14 17:24:39 +01:00
this . logger . log ( ` Successfully encoded ${ asset . id } ` ) ;
2023-04-04 16:48:02 +02:00
2024-03-20 03:42:10 +01:00
await this . assetRepository . update ( { id : asset.id , encodedVideoPath : output } ) ;
2023-05-26 21:43:24 +02:00
2024-03-15 14:16:54 +01:00
return JobStatus . SUCCESS ;
2023-04-04 16:48:02 +02:00
}
2023-08-29 11:01:42 +02:00
private getMainStream < T extends VideoStreamInfo | AudioStreamInfo > ( streams : T [ ] ) : T {
2023-04-06 05:32:59 +02:00
return streams . sort ( ( stream1 , stream2 ) = > stream2 . frameCount - stream1 . frameCount ) [ 0 ] ;
}
2024-02-14 17:24:39 +01:00
private getTranscodeTarget (
config : SystemConfigFFmpegDto ,
videoStream : VideoStreamInfo | null ,
2023-07-09 22:15:34 +02:00
audioStream : AudioStreamInfo | null ,
2024-02-14 17:24:39 +01:00
) : TranscodeTarget {
if ( videoStream == null && audioStream == null ) {
return TranscodeTarget . NONE ;
}
const isAudioTranscodeRequired = this . isAudioTranscodeRequired ( config , audioStream ) ;
const isVideoTranscodeRequired = this . isVideoTranscodeRequired ( config , videoStream ) ;
if ( isAudioTranscodeRequired && isVideoTranscodeRequired ) {
return TranscodeTarget . ALL ;
}
if ( isAudioTranscodeRequired ) {
return TranscodeTarget . AUDIO ;
}
if ( isVideoTranscodeRequired ) {
return TranscodeTarget . VIDEO ;
}
return TranscodeTarget . NONE ;
}
private isAudioTranscodeRequired ( ffmpegConfig : SystemConfigFFmpegDto , stream : AudioStreamInfo | null ) : boolean {
if ( stream == null ) {
return false ;
}
switch ( ffmpegConfig . transcode ) {
case TranscodePolicy . DISABLED : {
return false ;
}
case TranscodePolicy . ALL : {
return true ;
}
case TranscodePolicy . REQUIRED :
case TranscodePolicy . OPTIMAL :
case TranscodePolicy . BITRATE : {
return ! ffmpegConfig . acceptedAudioCodecs . includes ( stream . codecName as AudioCodec ) ;
}
default : {
throw new Error ( ` Unsupported transcode policy: ${ ffmpegConfig . transcode } ` ) ;
}
}
}
private isVideoTranscodeRequired ( ffmpegConfig : SystemConfigFFmpegDto , stream : VideoStreamInfo | null ) : boolean {
if ( stream == null ) {
return false ;
}
2023-04-06 05:32:59 +02:00
2023-06-10 06:15:12 +02:00
const scalingEnabled = ffmpegConfig . targetResolution !== 'original' ;
const targetRes = Number . parseInt ( ffmpegConfig . targetResolution ) ;
2024-02-14 17:24:39 +01:00
const isLargerThanTargetRes = scalingEnabled && Math . min ( stream . height , stream . width ) > targetRes ;
const isLargerThanTargetBitrate = stream . bitrate > this . parseBitrateToBps ( ffmpegConfig . maxBitrate ) ;
const isTargetVideoCodec = ffmpegConfig . acceptedVideoCodecs . includes ( stream . codecName as VideoCodec ) ;
const isRequired = ! isTargetVideoCodec || stream . isHDR ;
2023-04-04 16:48:02 +02:00
switch ( ffmpegConfig . transcode ) {
2024-02-02 04:18:00 +01:00
case TranscodePolicy . DISABLED : {
2023-04-06 05:32:59 +02:00
return false ;
2024-02-02 04:18:00 +01:00
}
case TranscodePolicy . ALL : {
2023-04-04 16:48:02 +02:00
return true ;
2024-02-02 04:18:00 +01:00
}
case TranscodePolicy . REQUIRED : {
2024-02-14 17:24:39 +01:00
return isRequired ;
2024-02-02 04:18:00 +01:00
}
case TranscodePolicy . OPTIMAL : {
2024-02-14 17:24:39 +01:00
return isRequired || isLargerThanTargetRes ;
2024-02-02 04:18:00 +01:00
}
case TranscodePolicy . BITRATE : {
2024-02-14 17:24:39 +01:00
return isRequired || isLargerThanTargetBitrate ;
2024-02-02 04:18:00 +01:00
}
default : {
2024-02-14 17:24:39 +01:00
throw new Error ( ` Unsupported transcode policy: ${ ffmpegConfig . transcode } ` ) ;
2024-02-02 04:18:00 +01:00
}
2023-04-04 16:48:02 +02:00
}
}
2023-08-02 03:56:10 +02:00
async getCodecConfig ( config : SystemConfigFFmpegDto ) {
if ( config . accel === TranscodeHWAccel . DISABLED ) {
return this . getSWCodecConfig ( config ) ;
}
return this . getHWCodecConfig ( config ) ;
}
private getSWCodecConfig ( config : SystemConfigFFmpegDto ) {
2023-07-09 04:43:11 +02:00
switch ( config . targetVideoCodec ) {
2024-02-02 04:18:00 +01:00
case VideoCodec . H264 : {
2023-07-09 04:43:11 +02:00
return new H264Config ( config ) ;
2024-02-02 04:18:00 +01:00
}
case VideoCodec . HEVC : {
2023-07-09 04:43:11 +02:00
return new HEVCConfig ( config ) ;
2024-02-02 04:18:00 +01:00
}
case VideoCodec . VP9 : {
2023-07-09 04:43:11 +02:00
return new VP9Config ( config ) ;
2024-02-02 04:18:00 +01:00
}
default : {
2023-07-09 04:43:11 +02:00
throw new UnsupportedMediaTypeException ( ` Codec ' ${ config . targetVideoCodec } ' is unsupported ` ) ;
2024-02-02 04:18:00 +01:00
}
2023-05-22 20:07:43 +02:00
}
}
2023-08-02 03:56:10 +02:00
private async getHWCodecConfig ( config : SystemConfigFFmpegDto ) {
let handler : VideoCodecHWConfig ;
let devices : string [ ] ;
switch ( config . accel ) {
2024-02-02 04:18:00 +01:00
case TranscodeHWAccel . NVENC : {
2023-08-02 03:56:10 +02:00
handler = new NVENCConfig ( config ) ;
break ;
2024-02-02 04:18:00 +01:00
}
case TranscodeHWAccel . QSV : {
2023-08-02 03:56:10 +02:00
devices = await this . storageRepository . readdir ( '/dev/dri' ) ;
handler = new QSVConfig ( config , devices ) ;
break ;
2024-02-02 04:18:00 +01:00
}
case TranscodeHWAccel . VAAPI : {
2023-08-02 03:56:10 +02:00
devices = await this . storageRepository . readdir ( '/dev/dri' ) ;
handler = new VAAPIConfig ( config , devices ) ;
break ;
2024-02-02 04:18:00 +01:00
}
case TranscodeHWAccel . RKMPP : {
2024-03-09 03:17:26 +01:00
if ( this . hasOpenCL === undefined ) {
try {
const maliIcdStat = await this . storageRepository . stat ( '/etc/OpenCL/vendors/mali.icd' ) ;
const maliDeviceStat = await this . storageRepository . stat ( '/dev/mali0' ) ;
this . hasOpenCL = maliIcdStat . isFile ( ) && maliDeviceStat . isCharacterDevice ( ) ;
} catch {
this . logger . warn ( 'OpenCL not available for transcoding, using CPU instead.' ) ;
this . hasOpenCL = false ;
}
}
2023-10-30 15:39:37 +01:00
devices = await this . storageRepository . readdir ( '/dev/dri' ) ;
2024-03-09 03:17:26 +01:00
handler = new RKMPPConfig ( config , devices , this . hasOpenCL ) ;
2023-10-30 15:39:37 +01:00
break ;
2024-02-02 04:18:00 +01:00
}
default : {
2023-08-02 03:56:10 +02:00
throw new UnsupportedMediaTypeException ( ` ${ config . accel . toUpperCase ( ) } acceleration is unsupported ` ) ;
2024-02-02 04:18:00 +01:00
}
2023-08-02 03:56:10 +02:00
}
if ( ! handler . getSupportedCodecs ( ) . includes ( config . targetVideoCodec ) ) {
throw new UnsupportedMediaTypeException (
` ${ config . accel . toUpperCase ( ) } acceleration does not support codec ' ${ config . targetVideoCodec . toUpperCase ( ) } '. Supported codecs: ${ handler . getSupportedCodecs ( ) } ` ,
) ;
}
return handler ;
}
2023-09-03 08:21:51 +02:00
2023-09-26 01:18:47 +02:00
isSRGB ( asset : AssetEntity ) : boolean {
const { colorspace , profileDescription , bitsPerSample } = asset . exifInfo ? ? { } ;
if ( colorspace || profileDescription ) {
return [ colorspace , profileDescription ] . some ( ( s ) = > s ? . toLowerCase ( ) . includes ( 'srgb' ) ) ;
} else if ( bitsPerSample ) {
// assume sRGB for 8-bit images with no color profile or colorspace metadata
return bitsPerSample === 8 ;
} else {
// assume sRGB for images with no relevant metadata
return true ;
}
}
2024-01-31 02:25:07 +01:00
parseBitrateToBps ( bitrateString : string ) {
const bitrateValue = Number . parseInt ( bitrateString ) ;
2024-02-02 04:18:00 +01:00
if ( Number . isNaN ( bitrateValue ) ) {
2024-01-31 02:25:07 +01:00
return 0 ;
}
if ( bitrateString . toLowerCase ( ) . endsWith ( 'k' ) ) {
return bitrateValue * 1000 ; // Kilobits per second to bits per second
} else if ( bitrateString . toLowerCase ( ) . endsWith ( 'm' ) ) {
2024-02-02 04:18:00 +01:00
return bitrateValue * 1 _000_000 ; // Megabits per second to bits per second
2024-01-31 02:25:07 +01:00
} else {
return bitrateValue ;
}
}
2023-02-25 15:12:03 +01:00
}