2023-12-14 17:55:40 +01:00
import { Inject , Injectable , UnsupportedMediaTypeException } from '@nestjs/common' ;
2024-04-19 17:50:13 +02:00
import { dirname } from 'node:path' ;
2024-04-02 06:56:56 +02:00
import { GeneratedImageType , StorageCore , StorageFolder } from 'src/cores/storage.core' ;
2024-03-20 21:20:38 +01:00
import { SystemConfigCore } from 'src/cores/system-config.core' ;
2024-03-22 21:36:20 +01:00
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.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 ,
2024-04-02 06:56:56 +02:00
ImageFormat ,
2024-03-20 19:32:04 +01:00
TranscodeHWAccel ,
TranscodePolicy ,
TranscodeTarget ,
VideoCodec ,
2024-03-20 22:02:51 +01:00
} from 'src/entities/system-config.entity' ;
2024-03-21 12:59:49 +01:00
import { IAssetRepository , WithoutProperty } from 'src/interfaces/asset.interface' ;
import { ICryptoRepository } from 'src/interfaces/crypto.interface' ;
2024-03-21 04:15:09 +01:00
import {
IBaseJob ,
IEntityJob ,
IJobRepository ,
JOBS_ASSET_PAGINATION_SIZE ,
JobItem ,
JobName ,
JobStatus ,
QueueName ,
2024-03-21 12:59:49 +01:00
} from 'src/interfaces/job.interface' ;
2024-04-16 23:30:31 +02:00
import { ILoggerRepository } from 'src/interfaces/logger.interface' ;
2024-03-21 12:59:49 +01:00
import { AudioStreamInfo , IMediaRepository , VideoCodecHWConfig , VideoStreamInfo } from 'src/interfaces/media.interface' ;
import { IMoveRepository } from 'src/interfaces/move.interface' ;
import { IPersonRepository } from 'src/interfaces/person.interface' ;
import { IStorageRepository } from 'src/interfaces/storage.interface' ;
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface' ;
2024-03-21 04:15:09 +01:00
import {
2024-04-11 07:26:27 +02:00
AV1Config ,
2024-03-21 04:15:09 +01:00
H264Config ,
HEVCConfig ,
NVENCConfig ,
QSVConfig ,
RKMPPConfig ,
ThumbnailConfig ,
VAAPIConfig ,
VP9Config ,
} from 'src/utils/media' ;
2024-04-19 17:50:13 +02:00
import { mimeTypes } from 'src/utils/mime-types' ;
2024-03-21 04:15:09 +01:00
import { usePagination } from 'src/utils/pagination' ;
2023-09-08 08:49:43 +02:00
2023-02-25 15:12:03 +01:00
@Injectable ( )
export class MediaService {
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 ,
2024-04-16 23:30:31 +02:00
@Inject ( ILoggerRepository ) private logger : ILoggerRepository ,
2023-04-04 16:48:02 +02:00
) {
2024-04-16 23:30:31 +02:00
this . logger . setContext ( MediaService . name ) ;
this . configCore = SystemConfigCore . create ( configRepository , this . logger ) ;
2023-12-29 19:41:33 +01:00
this . storageCore = StorageCore . create (
assetRepository ,
2024-04-16 23:30:31 +02:00
cryptoRepository ,
2023-12-29 19:41:33 +01:00
moveRepository ,
personRepository ,
storageRepository ,
2024-04-16 23:30:31 +02:00
configRepository ,
this . logger ,
2023-12-29 19:41:33 +01:00
) ;
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
2024-04-19 03:37:55 +02:00
? this . assetRepository . getAll ( pagination , { isVisible : true } )
2023-05-26 21:43:24 +02:00
: 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 ) {
2024-04-02 06:56:56 +02:00
if ( ! asset . previewPath || force ) {
jobs . push ( { name : JobName.GENERATE_PREVIEW , data : { id : asset.id } } ) ;
2023-06-18 05:22:31 +02:00
continue ;
}
2024-04-02 06:56:56 +02:00
if ( ! asset . thumbnailPath ) {
jobs . push ( { name : JobName.GENERATE_THUMBNAIL , data : { id : asset.id } } ) ;
2023-06-18 05:22:31 +02:00
}
if ( ! asset . thumbhash ) {
2024-04-02 06:56:56 +02:00
jobs . push ( { name : JobName.GENERATE_THUMBHASH , 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 ;
}
2024-05-02 01:10:02 +02:00
await this . personRepository . update ( { id : person.id , faceAssetId : face.id } ) ;
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 > {
2024-04-02 06:56:56 +02:00
const { image } = await this . configCore . getConfig ( ) ;
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
}
2024-04-02 06:56:56 +02:00
await this . storageCore . moveAssetImage ( asset , AssetPathType . PREVIEW , image . previewFormat ) ;
await this . storageCore . moveAssetImage ( asset , AssetPathType . THUMBNAIL , image . thumbnailFormat ) ;
await this . storageCore . moveAssetVideo ( asset ) ;
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-04-02 06:56:56 +02:00
async handleGeneratePreview ( { id } : IEntityJob ) : Promise < JobStatus > {
2024-04-07 18:44:34 +02:00
const [ { image } , [ asset ] ] = await Promise . all ( [
this . configCore . getConfig ( ) ,
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
2024-04-19 03:37:55 +02:00
if ( ! asset . isVisible ) {
return JobStatus . SKIPPED ;
}
2024-04-07 18:44:34 +02:00
const previewPath = await this . generateThumbnail ( asset , AssetPathType . PREVIEW , image . previewFormat ) ;
2024-04-28 00:43:05 +02:00
if ( asset . previewPath && asset . previewPath !== previewPath ) {
this . logger . debug ( ` Deleting old preview for asset ${ asset . id } ` ) ;
await this . storageRepository . unlink ( asset . previewPath ) ;
}
2024-04-02 06:56:56 +02:00
await this . assetRepository . update ( { id : asset.id , previewPath } ) ;
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
2024-04-02 06:56:56 +02:00
private async generateThumbnail ( asset : AssetEntity , type : GeneratedImageType , format : ImageFormat ) {
const { image , ffmpeg } = await this . configCore . getConfig ( ) ;
const size = type === AssetPathType . PREVIEW ? image.previewSize : image.thumbnailSize ;
const path = StorageCore . getImagePath ( asset , type , format ) ;
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 : {
2024-04-19 17:50:13 +02:00
const shouldExtract = image . extractEmbedded && mimeTypes . isRaw ( asset . originalPath ) ;
const extractedPath = StorageCore . getTempPathInDir ( dirname ( path ) ) ;
const didExtract = shouldExtract && ( await this . mediaRepository . extract ( asset . originalPath , extractedPath ) ) ;
try {
const useExtracted = didExtract && ( await this . shouldUseExtractedImage ( extractedPath , image . previewSize ) ) ;
const colorspace = this . isSRGB ( asset ) ? Colorspace.SRGB : image.colorspace ;
const imageOptions = { format , size , colorspace , quality : image.quality } ;
2024-05-08 15:09:34 +02:00
const outputPath = useExtracted ? extractedPath : asset.originalPath ;
await this . mediaRepository . generateThumbnail ( outputPath , path , imageOptions ) ;
2024-04-19 17:50:13 +02:00
} finally {
if ( didExtract ) {
await this . storageRepository . unlink ( extractedPath ) ;
}
}
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 (
2024-04-07 18:44:34 +02:00
` Successfully generated ${ format . toUpperCase ( ) } ${ asset . type . toLowerCase ( ) } ${ type } for asset ${ asset . id } ` ,
2023-09-03 08:21:51 +02:00
) ;
return path ;
}
2023-02-25 15:12:03 +01:00
2024-04-02 06:56:56 +02:00
async handleGenerateThumbnail ( { id } : IEntityJob ) : Promise < JobStatus > {
2024-04-07 18:44:34 +02:00
const [ { image } , [ asset ] ] = await Promise . all ( [
this . configCore . getConfig ( ) ,
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
}
2024-04-19 03:37:55 +02:00
if ( ! asset . isVisible ) {
return JobStatus . SKIPPED ;
}
2024-04-07 18:44:34 +02:00
const thumbnailPath = await this . generateThumbnail ( asset , AssetPathType . THUMBNAIL , image . thumbnailFormat ) ;
2024-04-28 00:43:05 +02:00
if ( asset . thumbnailPath && asset . thumbnailPath !== thumbnailPath ) {
this . logger . debug ( ` Deleting old thumbnail for asset ${ asset . id } ` ) ;
await this . storageRepository . unlink ( asset . thumbnailPath ) ;
}
2024-04-02 06:56:56 +02:00
await this . assetRepository . update ( { id : asset.id , thumbnailPath } ) ;
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-04-02 06:56:56 +02:00
async handleGenerateThumbhash ( { id } : IEntityJob ) : Promise < JobStatus > {
2023-06-18 05:22:31 +02:00
const [ asset ] = await this . assetRepository . getByIds ( [ id ] ) ;
2024-04-19 03:37:55 +02:00
if ( ! asset ) {
return JobStatus . FAILED ;
}
if ( ! asset . isVisible ) {
return JobStatus . SKIPPED ;
}
if ( ! asset . previewPath ) {
2024-03-15 14:16:54 +01:00
return JobStatus . FAILED ;
2023-06-18 05:22:31 +02:00
}
2024-04-02 06:56:56 +02:00
const thumbhash = await this . mediaRepository . generateThumbhash ( asset . previewPath ) ;
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
}
2024-04-11 07:26:27 +02:00
case VideoCodec . AV1 : {
return new AV1Config ( 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
2024-04-19 17:50:13 +02:00
private parseBitrateToBps ( bitrateString : string ) {
2024-01-31 02:25:07 +01:00
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 ;
}
}
2024-04-19 17:50:13 +02:00
private async shouldUseExtractedImage ( extractedPath : string , targetSize : number ) {
const { width , height } = await this . mediaRepository . getImageDimensions ( extractedPath ) ;
const extractedSize = Math . min ( width , height ) ;
return extractedSize >= targetSize ;
}
2023-02-25 15:12:03 +01:00
}