import { Job } from 'bull' ; import { TranscodeOptionsType } from '@server/helpers/ffmpeg-utils' ; import { addTranscodingJob , getTranscodingJobPriority } from '@server/lib/video' ; import { VideoPathManager } from '@server/lib/video-path-manager' ; import { moveToFailedTranscodingState , moveToNextState } from '@server/lib/video-state' ; import { UserModel } from '@server/models/user/user' ; import { VideoJobInfoModel } from '@server/models/video/video-job-info' ; import { MUser , MUserId , MVideo , MVideoFullLight , MVideoWithFile } from '@server/types/models' ; import { pick } from '@shared/core-utils' ; import { HLSTranscodingPayload , MergeAudioTranscodingPayload , NewResolutionTranscodingPayload , OptimizeTranscodingPayload , VideoResolution , VideoTranscodingPayload } from '@shared/models' ; import { retryTransactionWrapper } from '../../../helpers/database-utils' ; import { computeLowerResolutionsToTranscode } from '../../../helpers/ffprobe-utils' ; import { logger , loggerTagsFactory } from '../../../helpers/logger' ; import { CONFIG } from '../../../initializers/config' ; import { VideoModel } from '../../../models/video/video' ; import { generateHlsPlaylistResolution , mergeAudioVideofile , optimizeOriginalVideofile , transcodeNewWebTorrentResolution } from '../../transcoding/video-transcoding' ; type HandlerFunction = ( job : Job , payload : VideoTranscodingPayload , video : MVideoFullLight , user : MUser ) => Promise < void > ; const handlers = { 'new-resolution-to-hls' : handleHLSJob , 'new-resolution-to-webtorrent' : handleNewWebTorrentResolutionJob , 'merge-audio-to-webtorrent' : handleWebTorrentMergeAudioJob , 'optimize-to-webtorrent' : handleWebTorrentOptimizeJob } ; const lTags = loggerTagsFactory ( 'transcoding' ) ; async function processVideoTranscoding ( job ) { const payload = job . data as VideoTranscodingPayload ; logger . info ( 'Processing transcoding job %d.' , job . id , lTags ( payload . videoUUID ) ) ; const video = await VideoModel . loadAndPopulateAccountAndServerAndTags ( payload . videoUUID ) ; if ( ! video ) { logger . info ( 'Do not process job %d, video does not exist.' , job . id , lTags ( payload . videoUUID ) ) ; return undefined ; } const user = await UserModel . loadByChannelActorId ( video . VideoChannel . actorId ) ; const handler = handlers [ payload . type ] ; if ( ! handler ) { await moveToFailedTranscodingState ( video ) ; await VideoJobInfoModel . decrease ( video . uuid , 'pendingTranscode' ) ; throw new Error ( 'Cannot find transcoding handler for ' + payload . type ) ; } try { await handler ( job , payload , video , user ) ; } catch ( error ) { await moveToFailedTranscodingState ( video ) ; await VideoJobInfoModel . decrease ( video . uuid , 'pendingTranscode' ) ; throw error ; } return video ; } export { processVideoTranscoding } ; async function handleHLSJob ( job , payload , video , user ) { logger . info ( 'Handling HLS transcoding job for %s.' , video . uuid , lTags ( video . uuid ) ) ; const videoFileInput = payload . copyCodecs ? video . getWebTorrentFile ( payload . resolution ) : video . getMaxQualityFile ( ) ; const videoOrStreamingPlaylist = videoFileInput . getVideoOrStreamingPlaylist ( ) ; await VideoPathManager . Instance . makeAvailableVideoFile ( videoFileInput . withVideoOrPlaylist ( videoOrStreamingPlaylist ) , videoInputPath => { return generateHlsPlaylistResolution ( { video , videoInputPath , resolution : payload . resolution , copyCodecs : payload . copyCodecs , isPortraitMode : payload . isPortraitMode || false , job } ) ; } ) ; logger . info ( 'HLS transcoding job for %s ended.' , video . uuid , lTags ( video . uuid ) ) ; await onHlsPlaylistGeneration ( video , user , payload ) ; } async function handleNewWebTorrentResolutionJob ( job , payload , video , user ) { logger . info ( 'Handling WebTorrent transcoding job for %s.' , video . uuid , lTags ( video . uuid ) ) ; await transcodeNewWebTorrentResolution ( video , payload . resolution , payload . isPortraitMode || false , job ) ; logger . info ( 'WebTorrent transcoding job for %s ended.' , video . uuid , lTags ( video . uuid ) ) ; await onNewWebTorrentFileResolution ( video , user , payload ) ; } async function handleWebTorrentMergeAudioJob ( job , payload , video , user ) { logger . info ( 'Handling merge audio transcoding job for %s.' , video . uuid , lTags ( video . uuid ) ) ; await mergeAudioVideofile ( video , payload . resolution , job ) ; logger . info ( 'Merge audio transcoding job for %s ended.' , video . uuid , lTags ( video . uuid ) ) ; await onVideoFirstWebTorrentTranscoding ( video , payload , 'video' , user ) ; } async function handleWebTorrentOptimizeJob ( job , payload , video , user ) { logger . info ( 'Handling optimize transcoding job for %s.' , video . uuid , lTags ( video . uuid ) ) ; const { transcodeType } = await optimizeOriginalVideofile ( video , video . getMaxQualityFile ( ) , job ) ; logger . info ( 'Optimize transcoding job for %s ended.' , video . uuid , lTags ( video . uuid ) ) ; await onVideoFirstWebTorrentTranscoding ( video , payload , transcodeType , user ) ; } async function onHlsPlaylistGeneration ( video , user , payload ) { if ( payload . isMaxQuality && payload . autoDeleteWebTorrentIfNeeded && CONFIG . TRANSCODING . WEBTORRENT . ENABLED === false ) { for ( const file of video . VideoFiles ) { await video . removeWebTorrentFileAndTorrent ( file ) ; await file . destroy ( ) ; } video . VideoFiles = [ ] ; await createLowerResolutionsJobs ( { video , user , videoFileResolution : payload . resolution , isPortraitMode : payload . isPortraitMode , hasAudio : payload . hasAudio , isNewVideo : payload . isNewVideo ?? true , type : 'hls' } ) ; } await VideoJobInfoModel . decrease ( video . uuid , 'pendingTranscode' ) ; await retryTransactionWrapper ( moveToNextState , video , payload . isNewVideo ) ; } async function onVideoFirstWebTorrentTranscoding ( videoArg , payload , transcodeType , user ) { const { resolution , isPortraitMode , audioStream } = await videoArg . getMaxQualityFileInfo ( ) ; const videoDatabase = await VideoModel . loadAndPopulateAccountAndServerAndTags ( videoArg . uuid ) ; if ( ! videoDatabase ) return undefined ; const originalFileHLSPayload = { ... payload , isPortraitMode , hasAudio : ! ! audioStream , resolution : videoDatabase . getMaxQualityFile ( ) . resolution , copyCodecs : transcodeType !== 'quick-transcode' , isMaxQuality : true } ; const hasHls = await createHlsJobIfEnabled ( user , originalFileHLSPayload ) ; const hasNewResolutions = await createLowerResolutionsJobs ( { video : videoDatabase , user , videoFileResolution : resolution , hasAudio : ! ! audioStream , isPortraitMode , type : 'webtorrent' , isNewVideo : payload . isNewVideo ?? true } ) ; await VideoJobInfoModel . decrease ( videoDatabase . uuid , 'pendingTranscode' ) ; if ( ! hasHls && ! hasNewResolutions ) { await retryTransactionWrapper ( moveToNextState , videoDatabase , payload . isNewVideo ) ; } } async function onNewWebTorrentFileResolution ( video , user , payload ) { await createHlsJobIfEnabled ( user , { hasAudio : true , copyCodecs : true , isMaxQuality : false , ... payload } ) ; await VideoJobInfoModel . decrease ( video . uuid , 'pendingTranscode' ) ; await retryTransactionWrapper ( moveToNextState , video , payload . isNewVideo ) ; } async function createHlsJobIfEnabled ( user , payload ) { if ( ! payload || CONFIG . TRANSCODING . ENABLED !== true || CONFIG . TRANSCODING . HLS . ENABLED !== true ) return false ; const jobOptions = { priority : await getTranscodingJobPriority ( user ) } ; const hlsTranscodingPayload = { type : 'new-resolution-to-hls' , autoDeleteWebTorrentIfNeeded : true , ... pick ( payload , [ 'videoUUID' , 'resolution' , 'isPortraitMode' , 'copyCodecs' , 'isMaxQuality' , 'isNewVideo' , 'hasAudio' ] ) } ; await addTranscodingJob ( hlsTranscodingPayload , jobOptions ) ; return true ; } async function createLowerResolutionsJobs ( options ) { const { video , user , videoFileResolution , isPortraitMode , isNewVideo , hasAudio , type } = options ; const resolutionsEnabled = computeLowerResolutionsToTranscode ( videoFileResolution , 'vod' ) ; const resolutionCreated = [ ] ; for ( const resolution of resolutionsEnabled ) { if ( resolution === VideoResolution . H_NOVIDEO && hasAudio === false ) continue ; let dataInput ; if ( CONFIG . TRANSCODING . WEBTORRENT . ENABLED && type === 'webtorrent' ) { dataInput = { type : 'new-resolution-to-webtorrent' , videoUUID : video . uuid , resolution , isPortraitMode , hasAudio , isNewVideo } ; resolutionCreated . push ( 'webtorrent-' + resolution ) ; } if ( CONFIG . TRANSCODING . HLS . ENABLED && type === 'hls' ) { dataInput = { type : 'new-resolution-to-hls' , videoUUID : video . uuid , resolution , isPortraitMode , hasAudio , copyCodecs : false , isMaxQuality : false , autoDeleteWebTorrentIfNeeded : true , isNewVideo } ; resolutionCreated . push ( 'hls-' + resolution ) ; } if ( ! dataInput ) continue ; const jobOptions = { priority : await getTranscodingJobPriority ( user ) } ; await addTranscodingJob ( dataInput , jobOptions ) ; } if ( resolutionCreated . length === 0 ) { logger . info ( 'No transcoding jobs created for video %s (no resolutions).' , video . uuid , lTags ( video . uuid ) ) ; return false ; } logger . info ( 'New resolutions %s transcoding jobs created for video %s and origin file resolution of %d.' , type , video . uuid , videoFileResolution , { resolutionCreated , ... lTags ( video . uuid ) } ) ; return true ; }