2023-12-14 11:55:40 -05:00
import { Inject , Injectable , UnsupportedMediaTypeException } from '@nestjs/common' ;
2024-04-19 11:50:13 -04:00
import { dirname } from 'node:path' ;
2024-03-20 19:32:04 +01:00
import {
AudioCodec ,
Colorspace ,
2024-04-02 00:56:56 -04:00
ImageFormat ,
2024-03-20 19:32:04 +01:00
TranscodeHWAccel ,
TranscodePolicy ,
TranscodeTarget ,
VideoCodec ,
2024-05-14 14:43:49 -04:00
} from 'src/config' ;
import { GeneratedImageType , StorageCore , StorageFolder } from 'src/cores/storage.core' ;
import { SystemConfigCore } from 'src/cores/system-config.core' ;
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto' ;
import { AssetEntity , AssetType } from 'src/entities/asset.entity' ;
import { AssetPathType } from 'src/entities/move.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-20 22:15:09 -05: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-17 03:00:31 +05:30
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' ;
2024-05-15 18:58:23 -04:00
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface' ;
2024-03-20 22:15:09 -05:00
import {
2024-04-11 07:26:27 +02:00
AV1Config ,
2024-03-20 22:15:09 -05:00
H264Config ,
HEVCConfig ,
2024-05-16 13:30:26 -04:00
NvencHwDecodeConfig ,
NvencSwDecodeConfig ,
2024-05-22 23:58:29 -04:00
QsvHwDecodeConfig ,
QsvSwDecodeConfig ,
2024-05-16 13:30:26 -04:00
RkmppHwDecodeConfig ,
RkmppSwDecodeConfig ,
2024-03-20 22:15:09 -05:00
ThumbnailConfig ,
VAAPIConfig ,
VP9Config ,
} from 'src/utils/media' ;
2024-04-19 11:50:13 -04:00
import { mimeTypes } from 'src/utils/mime-types' ;
2024-03-20 22:15:09 -05:00
import { usePagination } from 'src/utils/pagination' ;
2023-09-08 08:49:43 +02:00
2023-02-25 09:12:03 -05:00
@Injectable ( )
export class MediaService {
2023-04-04 10:48:02 -04:00
private configCore : SystemConfigCore ;
2023-09-25 17:07:21 +02:00
private storageCore : StorageCore ;
2024-05-10 15:03:47 -04:00
private openCL : boolean | null = null ;
private devices : string [ ] | null = null ;
2023-02-25 09:12:03 -05:00
constructor (
@Inject ( IAssetRepository ) private assetRepository : IAssetRepository ,
2023-09-08 08:49:43 +02:00
@Inject ( IPersonRepository ) private personRepository : IPersonRepository ,
2023-02-25 09:12:03 -05:00
@Inject ( IJobRepository ) private jobRepository : IJobRepository ,
@Inject ( IMediaRepository ) private mediaRepository : IMediaRepository ,
@Inject ( IStorageRepository ) private storageRepository : IStorageRepository ,
2024-05-15 18:58:23 -04:00
@Inject ( ISystemMetadataRepository ) systemMetadataRepository : ISystemMetadataRepository ,
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-17 03:00:31 +05:30
@Inject ( ILoggerRepository ) private logger : ILoggerRepository ,
2023-04-04 10:48:02 -04:00
) {
2024-04-17 03:00:31 +05:30
this . logger . setContext ( MediaService . name ) ;
2024-05-15 18:58:23 -04:00
this . configCore = SystemConfigCore . create ( systemMetadataRepository , this . logger ) ;
2023-12-29 18:41:33 +00:00
this . storageCore = StorageCore . create (
assetRepository ,
2024-04-17 03:00:31 +05:30
cryptoRepository ,
2023-12-29 18:41:33 +00:00
moveRepository ,
personRepository ,
storageRepository ,
2024-05-15 18:58:23 -04:00
systemMetadataRepository ,
2024-04-17 03:00:31 +05:30
this . logger ,
2023-12-29 18:41:33 +00:00
) ;
2023-04-04 10:48:02 -04:00
}
2023-02-25 09:12:03 -05:00
2024-03-15 14:16:54 +01:00
async handleQueueGenerateThumbnails ( { force } : IBaseJob ) : Promise < JobStatus > {
2023-05-26 15:43:24 -04:00
const assetPagination = usePagination ( JOBS_ASSET_PAGINATION_SIZE , ( pagination ) = > {
return force
2024-04-18 21:37:55 -04:00
? this . assetRepository . getAll ( pagination , { isVisible : true } )
2023-05-26 15:43:24 -04:00
: this . assetRepository . getWithout ( pagination , WithoutProperty . THUMBNAIL ) ;
} ) ;
2023-03-20 11:55:28 -04:00
2023-05-26 15:43:24 -04:00
for await ( const assets of assetPagination ) {
2024-01-01 15:45:42 -05:00
const jobs : JobItem [ ] = [ ] ;
2023-05-26 15:43:24 -04:00
for ( const asset of assets ) {
2024-04-02 00:56:56 -04:00
if ( ! asset . previewPath || force ) {
jobs . push ( { name : JobName.GENERATE_PREVIEW , data : { id : asset.id } } ) ;
2023-06-17 23:22:31 -04:00
continue ;
}
2024-04-02 00:56:56 -04:00
if ( ! asset . thumbnailPath ) {
jobs . push ( { name : JobName.GENERATE_THUMBNAIL , data : { id : asset.id } } ) ;
2023-06-17 23:22:31 -04:00
}
if ( ! asset . thumbhash ) {
2024-04-02 00:56:56 -04:00
jobs . push ( { name : JobName.GENERATE_THUMBHASH , data : { id : asset.id } } ) ;
2023-06-17 23:22:31 -04:00
}
2023-03-20 11:55:28 -04:00
}
2024-01-01 15:45:42 -05:00
await this . jobRepository . queueAll ( jobs ) ;
2023-03-20 11:55:28 -04:00
}
2024-01-01 15:45:42 -05:00
const jobs : JobItem [ ] = [ ] ;
2024-01-18 00:08:48 -05: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-01 19:10:02 -04:00
await this . personRepository . update ( { id : person.id , faceAssetId : face.id } ) ;
2023-09-26 03:03:22 -04:00
}
2024-01-18 00:08:48 -05:00
jobs . push ( { name : JobName.GENERATE_PERSON_THUMBNAIL , data : { id : person.id } } ) ;
2023-09-08 08:49:43 +02:00
}
}
2024-01-01 15:45:42 -05:00
await this . jobRepository . queueAll ( jobs ) ;
2024-03-15 14:16:54 +01:00
return JobStatus . SUCCESS ;
2023-05-26 15:43:24 -04:00
}
2023-04-11 20:28:25 -05: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 15:45:42 -05: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 00:08:48 -05:00
const personPagination = usePagination ( JOBS_ASSET_PAGINATION_SIZE , ( pagination ) = >
this . personRepository . getAll ( pagination ) ,
2024-01-01 15:45:42 -05:00
) ;
2023-09-25 17:07:21 +02:00
2024-01-18 00:08:48 -05: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 00:56:56 -04: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 00:56:56 -04: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 00:56:56 -04:00
async handleGeneratePreview ( { id } : IEntityJob ) : Promise < JobStatus > {
2024-04-07 12:44:34 -04:00
const [ { image } , [ asset ] ] = await Promise . all ( [
this . configCore . getConfig ( ) ,
this . assetRepository . getByIds ( [ id ] , { exifInfo : true } ) ,
] ) ;
2023-04-11 20:28:25 -05:00
if ( ! asset ) {
2024-03-15 14:16:54 +01:00
return JobStatus . FAILED ;
2023-04-11 20:28:25 -05:00
}
2023-02-25 09:12:03 -05:00
2024-04-18 21:37:55 -04:00
if ( ! asset . isVisible ) {
return JobStatus . SKIPPED ;
}
2024-04-07 12:44:34 -04:00
const previewPath = await this . generateThumbnail ( asset , AssetPathType . PREVIEW , image . previewFormat ) ;
2024-04-27 18:43:05 -04: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 00:56:56 -04:00
await this . assetRepository . update ( { id : asset.id , previewPath } ) ;
2024-03-15 14:16:54 +01:00
return JobStatus . SUCCESS ;
2023-09-03 02:21:51 -04:00
}
2023-05-26 15:43:24 -04:00
2024-04-02 00:56:56 -04: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 04:42:35 +01:00
switch ( asset . type ) {
2024-02-02 04:18:00 +01:00
case AssetType . IMAGE : {
2024-04-19 11:50:13 -04: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 09:09:34 -04:00
const outputPath = useExtracted ? extractedPath : asset.originalPath ;
await this . mediaRepository . generateThumbnail ( outputPath , path , imageOptions ) ;
2024-04-19 11:50:13 -04:00
} finally {
if ( didExtract ) {
await this . storageRepository . unlink ( extractedPath ) ;
}
}
2023-06-15 04:42:35 +01: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 11:24:39 -05: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 04:42:35 +01: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 02:21:51 -04:00
throw new UnsupportedMediaTypeException ( ` Unsupported asset type for thumbnail generation: ${ asset . type } ` ) ;
2024-02-02 04:18:00 +01:00
}
2023-05-26 15:43:24 -04:00
}
2023-09-03 02:21:51 -04:00
this . logger . log (
2024-04-07 12:44:34 -04:00
` Successfully generated ${ format . toUpperCase ( ) } ${ asset . type . toLowerCase ( ) } ${ type } for asset ${ asset . id } ` ,
2023-09-03 02:21:51 -04:00
) ;
return path ;
}
2023-02-25 09:12:03 -05:00
2024-04-02 00:56:56 -04:00
async handleGenerateThumbnail ( { id } : IEntityJob ) : Promise < JobStatus > {
2024-04-07 12:44:34 -04:00
const [ { image } , [ asset ] ] = await Promise . all ( [
this . configCore . getConfig ( ) ,
this . assetRepository . getByIds ( [ id ] , { exifInfo : true } ) ,
] ) ;
2023-09-04 19:24:55 -04:00
if ( ! asset ) {
2024-03-15 14:16:54 +01:00
return JobStatus . FAILED ;
2023-02-25 09:12:03 -05:00
}
2024-04-18 21:37:55 -04:00
if ( ! asset . isVisible ) {
return JobStatus . SKIPPED ;
}
2024-04-07 12:44:34 -04:00
const thumbnailPath = await this . generateThumbnail ( asset , AssetPathType . THUMBNAIL , image . thumbnailFormat ) ;
2024-04-27 18:43:05 -04: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 00:56:56 -04:00
await this . assetRepository . update ( { id : asset.id , thumbnailPath } ) ;
2024-03-15 14:16:54 +01:00
return JobStatus . SUCCESS ;
2023-02-25 09:12:03 -05:00
}
2023-04-04 10:48:02 -04:00
2024-04-02 00:56:56 -04:00
async handleGenerateThumbhash ( { id } : IEntityJob ) : Promise < JobStatus > {
2023-06-17 23:22:31 -04:00
const [ asset ] = await this . assetRepository . getByIds ( [ id ] ) ;
2024-04-18 21:37:55 -04: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-17 23:22:31 -04:00
}
2024-04-02 00:56:56 -04:00
const thumbhash = await this . mediaRepository . generateThumbhash ( asset . previewPath ) ;
2024-03-19 22:42:10 -04:00
await this . assetRepository . update ( { id : asset.id , thumbhash } ) ;
2023-06-17 23:22:31 -04:00
2024-03-15 14:16:54 +01:00
return JobStatus . SUCCESS ;
2023-06-17 23:22:31 -04:00
}
2024-03-15 14:16:54 +01:00
async handleQueueVideoConversion ( job : IBaseJob ) : Promise < JobStatus > {
2023-04-04 10:48:02 -04:00
const { force } = job ;
2023-05-26 15:43:24 -04: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 15:45:42 -05:00
await this . jobRepository . queueAll (
assets . map ( ( asset ) = > ( { name : JobName.VIDEO_CONVERSION , data : { id : asset.id } } ) ) ,
) ;
2023-04-04 10:48:02 -04:00
}
2024-03-15 14:16:54 +01:00
return JobStatus . SUCCESS ;
2023-05-26 15:43:24 -04:00
}
2023-04-11 08:56:52 -05:00
2024-03-15 14:16:54 +01:00
async handleVideoConversion ( { id } : IEntityJob ) : Promise < JobStatus > {
2023-05-26 15:43:24 -04:00
const [ asset ] = await this . assetRepository . getByIds ( [ id ] ) ;
2023-07-05 01:36:16 -04:00
if ( ! asset || asset . type !== AssetType . VIDEO ) {
2024-03-15 14:16:54 +01:00
return JobStatus . FAILED ;
2023-04-11 08:56:52 -05:00
}
2023-04-04 10:48:02 -04:00
2023-05-26 15:43:24 -04: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 15:43:24 -04:00
const { videoStreams , audioStreams , format } = await this . mediaRepository . probe ( input ) ;
2023-08-29 05:01:42 -04:00
const mainVideoStream = this . getMainStream ( videoStreams ) ;
const mainAudioStream = this . getMainStream ( audioStreams ) ;
2023-05-26 15:43:24 -04: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 15:43:24 -04:00
}
2023-04-04 10:48:02 -04:00
2023-12-28 00:34:00 -05: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 00:34:00 -05:00
}
2023-05-26 15:43:24 -04:00
const { ffmpeg : config } = await this . configCore . getConfig ( ) ;
2024-02-14 11:24:39 -05:00
const target = this . getTranscodeTarget ( config , mainVideoStream , mainAudioStream ) ;
if ( target === TranscodeTarget . NONE ) {
2023-12-28 00:34:00 -05: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-19 22:42:10 -04:00
await this . assetRepository . update ( { id : asset.id , encodedVideoPath : null } ) ;
2023-12-28 00:34:00 -05:00
}
2024-03-15 14:16:54 +01:00
return JobStatus . SKIPPED ;
2023-05-26 15:43:24 -04:00
}
2023-04-04 10:48:02 -04:00
2023-07-08 22:43:11 -04:00
let transcodeOptions ;
try {
2024-02-14 11:24:39 -05: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-08 22:43:11 -04:00
}
2023-04-06 04:32:59 +01:00
2024-02-14 11:24:39 -05:00
this . logger . log ( ` Started encoding video ${ asset . id } ${ JSON . stringify ( transcodeOptions ) } ` ) ;
2023-08-01 21:56:10 -04: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 05:01:42 -04:00
if ( config . accel !== TranscodeHWAccel . DISABLED ) {
2023-08-01 21:56:10 -04:00
this . logger . error (
` Error occurred during transcoding. Retrying with ${ config . accel . toUpperCase ( ) } acceleration disabled. ` ,
) ;
}
2024-05-16 13:30:26 -04:00
transcodeOptions = await this . getCodecConfig ( { . . . config , accel : TranscodeHWAccel.DISABLED } ) . then ( ( c ) = >
2024-02-14 11:24:39 -05:00
c . getOptions ( target , mainVideoStream , mainAudioStream ) ,
) ;
2023-08-01 21:56:10 -04:00
await this . mediaRepository . transcode ( input , output , transcodeOptions ) ;
}
2023-04-04 10:48:02 -04:00
2024-02-14 11:24:39 -05:00
this . logger . log ( ` Successfully encoded ${ asset . id } ` ) ;
2023-04-04 10:48:02 -04:00
2024-03-19 22:42:10 -04:00
await this . assetRepository . update ( { id : asset.id , encodedVideoPath : output } ) ;
2023-05-26 15:43:24 -04:00
2024-03-15 14:16:54 +01:00
return JobStatus . SUCCESS ;
2023-04-04 10:48:02 -04:00
}
2023-08-29 05:01:42 -04:00
private getMainStream < T extends VideoStreamInfo | AudioStreamInfo > ( streams : T [ ] ) : T {
2023-04-06 04:32:59 +01:00
return streams . sort ( ( stream1 , stream2 ) = > stream2 . frameCount - stream1 . frameCount ) [ 0 ] ;
}
2024-02-14 11:24:39 -05:00
private getTranscodeTarget (
config : SystemConfigFFmpegDto ,
videoStream : VideoStreamInfo | null ,
2023-07-09 22:15:34 +02:00
audioStream : AudioStreamInfo | null ,
2024-02-14 11:24:39 -05: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 04:32:59 +01:00
2023-06-10 00:15:12 -04:00
const scalingEnabled = ffmpegConfig . targetResolution !== 'original' ;
const targetRes = Number . parseInt ( ffmpegConfig . targetResolution ) ;
2024-02-14 11:24:39 -05: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 10:48:02 -04:00
switch ( ffmpegConfig . transcode ) {
2024-02-02 04:18:00 +01:00
case TranscodePolicy . DISABLED : {
2023-04-06 04:32:59 +01:00
return false ;
2024-02-02 04:18:00 +01:00
}
case TranscodePolicy . ALL : {
2023-04-04 10:48:02 -04:00
return true ;
2024-02-02 04:18:00 +01:00
}
case TranscodePolicy . REQUIRED : {
2024-02-14 11:24:39 -05:00
return isRequired ;
2024-02-02 04:18:00 +01:00
}
case TranscodePolicy . OPTIMAL : {
2024-02-14 11:24:39 -05:00
return isRequired || isLargerThanTargetRes ;
2024-02-02 04:18:00 +01:00
}
case TranscodePolicy . BITRATE : {
2024-02-14 11:24:39 -05:00
return isRequired || isLargerThanTargetBitrate ;
2024-02-02 04:18:00 +01:00
}
default : {
2024-02-14 11:24:39 -05:00
throw new Error ( ` Unsupported transcode policy: ${ ffmpegConfig . transcode } ` ) ;
2024-02-02 04:18:00 +01:00
}
2023-04-04 10:48:02 -04:00
}
}
2023-08-01 21:56:10 -04:00
async getCodecConfig ( config : SystemConfigFFmpegDto ) {
if ( config . accel === TranscodeHWAccel . DISABLED ) {
return this . getSWCodecConfig ( config ) ;
}
return this . getHWCodecConfig ( config ) ;
}
private getSWCodecConfig ( config : SystemConfigFFmpegDto ) {
2023-07-08 22:43:11 -04:00
switch ( config . targetVideoCodec ) {
2024-02-02 04:18:00 +01:00
case VideoCodec . H264 : {
2023-07-08 22:43:11 -04:00
return new H264Config ( config ) ;
2024-02-02 04:18:00 +01:00
}
case VideoCodec . HEVC : {
2023-07-08 22:43:11 -04:00
return new HEVCConfig ( config ) ;
2024-02-02 04:18:00 +01:00
}
case VideoCodec . VP9 : {
2023-07-08 22:43:11 -04: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-08 22:43:11 -04:00
throw new UnsupportedMediaTypeException ( ` Codec ' ${ config . targetVideoCodec } ' is unsupported ` ) ;
2024-02-02 04:18:00 +01:00
}
2023-05-22 14:07:43 -04:00
}
}
2023-08-01 21:56:10 -04:00
private async getHWCodecConfig ( config : SystemConfigFFmpegDto ) {
let handler : VideoCodecHWConfig ;
switch ( config . accel ) {
2024-02-02 04:18:00 +01:00
case TranscodeHWAccel . NVENC : {
2024-05-16 13:30:26 -04:00
handler = config . accelDecode ? new NvencHwDecodeConfig ( config ) : new NvencSwDecodeConfig ( config ) ;
2023-08-01 21:56:10 -04:00
break ;
2024-02-02 04:18:00 +01:00
}
case TranscodeHWAccel . QSV : {
2024-05-22 23:58:29 -04:00
handler = config . accelDecode
? new QsvHwDecodeConfig ( config , await this . getDevices ( ) )
: new QsvSwDecodeConfig ( config , await this . getDevices ( ) ) ;
2023-08-01 21:56:10 -04:00
break ;
2024-02-02 04:18:00 +01:00
}
case TranscodeHWAccel . VAAPI : {
2024-05-10 15:03:47 -04:00
handler = new VAAPIConfig ( config , await this . getDevices ( ) ) ;
2023-08-01 21:56:10 -04:00
break ;
2024-02-02 04:18:00 +01:00
}
case TranscodeHWAccel . RKMPP : {
2024-05-16 13:30:26 -04:00
handler =
config . accelDecode && ( await this . hasOpenCL ( ) )
? new RkmppHwDecodeConfig ( config , await this . getDevices ( ) )
: new RkmppSwDecodeConfig ( config , await this . getDevices ( ) ) ;
2023-10-30 15:39:37 +01:00
break ;
2024-02-02 04:18:00 +01:00
}
default : {
2023-08-01 21:56:10 -04:00
throw new UnsupportedMediaTypeException ( ` ${ config . accel . toUpperCase ( ) } acceleration is unsupported ` ) ;
2024-02-02 04:18:00 +01:00
}
2023-08-01 21:56:10 -04: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 02:21:51 -04:00
2023-09-25 19:18:47 -04: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 11:50:13 -04: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 11:50:13 -04: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 ;
}
2024-05-10 15:03:47 -04:00
private async getDevices() {
if ( ! this . devices ) {
this . devices = await this . storageRepository . readdir ( '/dev/dri' ) ;
}
return this . devices ;
}
private async hasOpenCL() {
if ( this . openCL === null ) {
try {
const maliIcdStat = await this . storageRepository . stat ( '/etc/OpenCL/vendors/mali.icd' ) ;
const maliDeviceStat = await this . storageRepository . stat ( '/dev/mali0' ) ;
this . openCL = maliIcdStat . isFile ( ) && maliDeviceStat . isCharacterDevice ( ) ;
} catch {
this . logger . warn ( 'OpenCL not available for transcoding, using CPU instead.' ) ;
this . openCL = false ;
}
}
return this . openCL ;
}
2023-02-25 09:12:03 -05:00
}