1 import Bluebird from 'bluebird'
2 import { computeOutputFPS } from '@server/helpers/ffmpeg'
3 import { logger } from '@server/helpers/logger'
4 import { CONFIG } from '@server/initializers/config'
5 import { DEFAULT_AUDIO_RESOLUTION, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
6 import { CreateJobArgument, JobQueue } from '@server/lib/job-queue'
7 import { Hooks } from '@server/lib/plugins/hooks'
8 import { VideoPathManager } from '@server/lib/video-path-manager'
9 import { VideoJobInfoModel } from '@server/models/video/video-job-info'
10 import { MUserId, MVideoFile, MVideoFullLight, MVideoWithFileThumbnail } from '@server/types/models'
11 import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream, isAudioFile } from '@shared/ffmpeg'
13 HLSTranscodingPayload,
14 MergeAudioTranscodingPayload,
15 NewWebTorrentResolutionTranscodingPayload,
16 OptimizeTranscodingPayload,
17 VideoTranscodingPayload
18 } from '@shared/models'
19 import { getTranscodingJobPriority } from '../../transcoding-priority'
20 import { canDoQuickTranscode } from '../../transcoding-quick-transcode'
21 import { computeResolutionsToTranscode } from '../../transcoding-resolutions'
22 import { AbstractJobBuilder } from './abstract-job-builder'
24 export class TranscodingJobQueueBuilder extends AbstractJobBuilder {
26 async createOptimizeOrMergeAudioJobs (options: {
27 video: MVideoFullLight
31 videoFileAlreadyLocked: boolean
33 const { video, videoFile, isNewVideo, user, videoFileAlreadyLocked } = options
35 let mergeOrOptimizePayload: MergeAudioTranscodingPayload | OptimizeTranscodingPayload
36 let nextTranscodingSequentialJobPayloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[][] = []
38 const mutexReleaser = videoFileAlreadyLocked
40 : await VideoPathManager.Instance.lockFiles(video.uuid)
43 await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), async videoFilePath => {
44 const probe = await ffprobePromise(videoFilePath)
46 const { resolution } = await getVideoStreamDimensionsInfo(videoFilePath, probe)
47 const hasAudio = await hasAudioStream(videoFilePath, probe)
48 const quickTranscode = await canDoQuickTranscode(videoFilePath, probe)
49 const inputFPS = videoFile.isAudio()
50 ? VIDEO_TRANSCODING_FPS.AUDIO_MERGE // The first transcoding job will transcode to this FPS value
51 : await getVideoStreamFPS(videoFilePath, probe)
53 const maxResolution = await isAudioFile(videoFilePath, probe)
54 ? DEFAULT_AUDIO_RESOLUTION
57 if (CONFIG.TRANSCODING.HLS.ENABLED === true) {
58 nextTranscodingSequentialJobPayloads.push([
59 this.buildHLSJobPayload({
60 deleteWebTorrentFiles: CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false,
62 // We had some issues with a web video quick transcoded while producing a HLS version of it
63 copyCodecs: !quickTranscode,
65 resolution: maxResolution,
66 fps: computeOutputFPS({ inputFPS, resolution: maxResolution }),
67 videoUUID: video.uuid,
73 const lowerResolutionJobPayloads = await this.buildLowerResolutionJobPayloads({
75 inputVideoResolution: maxResolution,
76 inputVideoFPS: inputFPS,
81 nextTranscodingSequentialJobPayloads = [ ...nextTranscodingSequentialJobPayloads, ...lowerResolutionJobPayloads ]
83 const hasChildren = nextTranscodingSequentialJobPayloads.length !== 0
84 mergeOrOptimizePayload = videoFile.isAudio()
85 ? this.buildMergeAudioPayload({ videoUUID: video.uuid, isNewVideo, hasChildren })
86 : this.buildOptimizePayload({ videoUUID: video.uuid, isNewVideo, quickTranscode, hasChildren })
92 const nextTranscodingSequentialJobs = await Bluebird.mapSeries(nextTranscodingSequentialJobPayloads, payloads => {
93 return Bluebird.mapSeries(payloads, payload => {
94 return this.buildTranscodingJob({ payload, user })
98 const transcodingJobBuilderJob: CreateJobArgument = {
99 type: 'transcoding-job-builder',
101 videoUUID: video.uuid,
102 sequentialJobs: nextTranscodingSequentialJobs
106 const mergeOrOptimizeJob = await this.buildTranscodingJob({ payload: mergeOrOptimizePayload, user })
108 await JobQueue.Instance.createSequentialJobFlow(...[ mergeOrOptimizeJob, transcodingJobBuilderJob ])
110 await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingTranscode')
113 // ---------------------------------------------------------------------------
115 async createTranscodingJobs (options: {
116 transcodingType: 'hls' | 'webtorrent'
117 video: MVideoFullLight
118 resolutions: number[]
122 const { video, transcodingType, resolutions, isNewVideo } = options
124 const maxResolution = Math.max(...resolutions)
125 const childrenResolutions = resolutions.filter(r => r !== maxResolution)
127 logger.info('Manually creating transcoding jobs for %s.', transcodingType, { childrenResolutions, maxResolution })
129 const { fps: inputFPS } = await video.probeMaxQualityFile()
131 const children = childrenResolutions.map(resolution => {
132 const fps = computeOutputFPS({ inputFPS, resolution })
134 if (transcodingType === 'hls') {
135 return this.buildHLSJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo })
138 if (transcodingType === 'webtorrent') {
139 return this.buildWebTorrentJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo })
142 throw new Error('Unknown transcoding type')
145 const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
147 const parent = transcodingType === 'hls'
148 ? this.buildHLSJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo })
149 : this.buildWebTorrentJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo })
151 // Process the last resolution after the other ones to prevent concurrency issue
152 // Because low resolutions use the biggest one as ffmpeg input
153 await this.createTranscodingJobsWithChildren({ videoUUID: video.uuid, parent, children, user: null })
156 // ---------------------------------------------------------------------------
158 private async createTranscodingJobsWithChildren (options: {
160 parent: (HLSTranscodingPayload | NewWebTorrentResolutionTranscodingPayload)
161 children: (HLSTranscodingPayload | NewWebTorrentResolutionTranscodingPayload)[]
164 const { videoUUID, parent, children, user } = options
166 const parentJob = await this.buildTranscodingJob({ payload: parent, user })
167 const childrenJobs = await Bluebird.mapSeries(children, c => this.buildTranscodingJob({ payload: c, user }))
169 await JobQueue.Instance.createJobWithChildren(parentJob, childrenJobs)
171 await VideoJobInfoModel.increaseOrCreate(videoUUID, 'pendingTranscode', 1 + children.length)
174 private async buildTranscodingJob (options: {
175 payload: VideoTranscodingPayload
176 user: MUserId | null // null means we don't want priority
178 const { user, payload } = options
181 type: 'video-transcoding' as 'video-transcoding',
182 priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: undefined }),
187 private async buildLowerResolutionJobPayloads (options: {
188 video: MVideoWithFileThumbnail
189 inputVideoResolution: number
190 inputVideoFPS: number
194 const { video, inputVideoResolution, inputVideoFPS, isNewVideo, hasAudio } = options
196 // Create transcoding jobs if there are enabled resolutions
197 const resolutionsEnabled = await Hooks.wrapObject(
198 computeResolutionsToTranscode({ input: inputVideoResolution, type: 'vod', includeInput: false, strictLower: true, hasAudio }),
199 'filter:transcoding.auto.resolutions-to-transcode.result',
203 const sequentialPayloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[][] = []
205 for (const resolution of resolutionsEnabled) {
206 const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution })
208 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) {
209 const payloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[] = [
210 this.buildWebTorrentJobPayload({
211 videoUUID: video.uuid,
218 // Create a subsequent job to create HLS resolution that will just copy web video codecs
219 if (CONFIG.TRANSCODING.HLS.ENABLED) {
221 this.buildHLSJobPayload({
222 videoUUID: video.uuid,
231 sequentialPayloads.push(payloads)
232 } else if (CONFIG.TRANSCODING.HLS.ENABLED) {
233 sequentialPayloads.push([
234 this.buildHLSJobPayload({
235 videoUUID: video.uuid,
245 return sequentialPayloads
248 private buildHLSJobPayload (options: {
253 deleteWebTorrentFiles?: boolean // default false
254 copyCodecs?: boolean // default false
255 }): HLSTranscodingPayload {
256 const { videoUUID, resolution, fps, isNewVideo, deleteWebTorrentFiles = false, copyCodecs = false } = options
259 type: 'new-resolution-to-hls',
265 deleteWebTorrentFiles
269 private buildWebTorrentJobPayload (options: {
274 }): NewWebTorrentResolutionTranscodingPayload {
275 const { videoUUID, resolution, fps, isNewVideo } = options
278 type: 'new-resolution-to-webtorrent',
286 private buildMergeAudioPayload (options: {
290 }): MergeAudioTranscodingPayload {
291 const { videoUUID, isNewVideo, hasChildren } = options
294 type: 'merge-audio-to-webtorrent',
295 resolution: DEFAULT_AUDIO_RESOLUTION,
296 fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE,
303 private buildOptimizePayload (options: {
305 quickTranscode: boolean
308 }): OptimizeTranscodingPayload {
309 const { videoUUID, quickTranscode, isNewVideo, hasChildren } = options
312 type: 'optimize-to-webtorrent',