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 { canDoQuickTranscode } from '../../transcoding-quick-transcode'
20 import { computeResolutionsToTranscode } from '../../transcoding-resolutions'
21 import { AbstractJobBuilder } from './abstract-job-builder'
23 export class TranscodingJobQueueBuilder extends AbstractJobBuilder {
25 async createOptimizeOrMergeAudioJobs (options: {
26 video: MVideoFullLight
31 const { video, videoFile, isNewVideo, user } = options
33 let mergeOrOptimizePayload: MergeAudioTranscodingPayload | OptimizeTranscodingPayload
34 let nextTranscodingSequentialJobPayloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[][] = []
36 const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
39 await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), async videoFilePath => {
40 const probe = await ffprobePromise(videoFilePath)
42 const { resolution } = await getVideoStreamDimensionsInfo(videoFilePath, probe)
43 const hasAudio = await hasAudioStream(videoFilePath, probe)
44 const quickTranscode = await canDoQuickTranscode(videoFilePath, probe)
45 const inputFPS = videoFile.isAudio()
46 ? VIDEO_TRANSCODING_FPS.AUDIO_MERGE // The first transcoding job will transcode to this FPS value
47 : await getVideoStreamFPS(videoFilePath, probe)
49 const maxResolution = await isAudioFile(videoFilePath, probe)
50 ? DEFAULT_AUDIO_RESOLUTION
53 if (CONFIG.TRANSCODING.HLS.ENABLED === true) {
54 nextTranscodingSequentialJobPayloads.push([
55 this.buildHLSJobPayload({
56 deleteWebTorrentFiles: CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false,
58 // We had some issues with a web video quick transcoded while producing a HLS version of it
59 copyCodecs: !quickTranscode,
61 resolution: maxResolution,
62 fps: computeOutputFPS({ inputFPS, resolution: maxResolution }),
63 videoUUID: video.uuid,
69 const lowerResolutionJobPayloads = await this.buildLowerResolutionJobPayloads({
71 inputVideoResolution: maxResolution,
72 inputVideoFPS: inputFPS,
77 nextTranscodingSequentialJobPayloads = [ ...nextTranscodingSequentialJobPayloads, ...lowerResolutionJobPayloads ]
79 const hasChildren = nextTranscodingSequentialJobPayloads.length !== 0
80 mergeOrOptimizePayload = videoFile.isAudio()
81 ? this.buildMergeAudioPayload({ videoUUID: video.uuid, isNewVideo, hasChildren })
82 : this.buildOptimizePayload({ videoUUID: video.uuid, isNewVideo, quickTranscode, hasChildren })
88 const nextTranscodingSequentialJobs = await Bluebird.mapSeries(nextTranscodingSequentialJobPayloads, payloads => {
89 return Bluebird.mapSeries(payloads, payload => {
90 return this.buildTranscodingJob({ payload, user })
94 const transcodingJobBuilderJob: CreateJobArgument = {
95 type: 'transcoding-job-builder',
97 videoUUID: video.uuid,
98 sequentialJobs: nextTranscodingSequentialJobs
102 const mergeOrOptimizeJob = await this.buildTranscodingJob({ payload: mergeOrOptimizePayload, user })
104 await JobQueue.Instance.createSequentialJobFlow(...[ mergeOrOptimizeJob, transcodingJobBuilderJob ])
106 await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingTranscode')
109 // ---------------------------------------------------------------------------
111 async createTranscodingJobs (options: {
112 transcodingType: 'hls' | 'webtorrent'
113 video: MVideoFullLight
114 resolutions: number[]
118 const { video, transcodingType, resolutions, isNewVideo } = options
120 const maxResolution = Math.max(...resolutions)
121 const childrenResolutions = resolutions.filter(r => r !== maxResolution)
123 logger.info('Manually creating transcoding jobs for %s.', transcodingType, { childrenResolutions, maxResolution })
125 const { fps: inputFPS } = await video.probeMaxQualityFile()
127 const children = childrenResolutions.map(resolution => {
128 const fps = computeOutputFPS({ inputFPS, resolution })
130 if (transcodingType === 'hls') {
131 return this.buildHLSJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo })
134 if (transcodingType === 'webtorrent') {
135 return this.buildWebTorrentJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo })
138 throw new Error('Unknown transcoding type')
141 const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
143 const parent = transcodingType === 'hls'
144 ? this.buildHLSJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo })
145 : this.buildWebTorrentJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo })
147 // Process the last resolution after the other ones to prevent concurrency issue
148 // Because low resolutions use the biggest one as ffmpeg input
149 await this.createTranscodingJobsWithChildren({ videoUUID: video.uuid, parent, children, user: null })
152 // ---------------------------------------------------------------------------
154 private async createTranscodingJobsWithChildren (options: {
156 parent: (HLSTranscodingPayload | NewWebTorrentResolutionTranscodingPayload)
157 children: (HLSTranscodingPayload | NewWebTorrentResolutionTranscodingPayload)[]
160 const { videoUUID, parent, children, user } = options
162 const parentJob = await this.buildTranscodingJob({ payload: parent, user })
163 const childrenJobs = await Bluebird.mapSeries(children, c => this.buildTranscodingJob({ payload: c, user }))
165 await JobQueue.Instance.createJobWithChildren(parentJob, childrenJobs)
167 await VideoJobInfoModel.increaseOrCreate(videoUUID, 'pendingTranscode', 1 + children.length)
170 private async buildTranscodingJob (options: {
171 payload: VideoTranscodingPayload
172 user: MUserId | null // null means we don't want priority
174 const { user, payload } = options
177 type: 'video-transcoding' as 'video-transcoding',
178 priority: await this.getTranscodingJobPriority({ user, fallback: undefined }),
183 private async buildLowerResolutionJobPayloads (options: {
184 video: MVideoWithFileThumbnail
185 inputVideoResolution: number
186 inputVideoFPS: number
190 const { video, inputVideoResolution, inputVideoFPS, isNewVideo, hasAudio } = options
192 // Create transcoding jobs if there are enabled resolutions
193 const resolutionsEnabled = await Hooks.wrapObject(
194 computeResolutionsToTranscode({ input: inputVideoResolution, type: 'vod', includeInput: false, strictLower: true, hasAudio }),
195 'filter:transcoding.auto.resolutions-to-transcode.result',
199 const sequentialPayloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[][] = []
201 for (const resolution of resolutionsEnabled) {
202 const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution })
204 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) {
205 const payloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[] = [
206 this.buildWebTorrentJobPayload({
207 videoUUID: video.uuid,
214 // Create a subsequent job to create HLS resolution that will just copy web video codecs
215 if (CONFIG.TRANSCODING.HLS.ENABLED) {
217 this.buildHLSJobPayload({
218 videoUUID: video.uuid,
227 sequentialPayloads.push(payloads)
228 } else if (CONFIG.TRANSCODING.HLS.ENABLED) {
229 sequentialPayloads.push([
230 this.buildHLSJobPayload({
231 videoUUID: video.uuid,
241 return sequentialPayloads
244 private buildHLSJobPayload (options: {
249 deleteWebTorrentFiles?: boolean // default false
250 copyCodecs?: boolean // default false
251 }): HLSTranscodingPayload {
252 const { videoUUID, resolution, fps, isNewVideo, deleteWebTorrentFiles = false, copyCodecs = false } = options
255 type: 'new-resolution-to-hls',
261 deleteWebTorrentFiles
265 private buildWebTorrentJobPayload (options: {
270 }): NewWebTorrentResolutionTranscodingPayload {
271 const { videoUUID, resolution, fps, isNewVideo } = options
274 type: 'new-resolution-to-webtorrent',
282 private buildMergeAudioPayload (options: {
286 }): MergeAudioTranscodingPayload {
287 const { videoUUID, isNewVideo, hasChildren } = options
290 type: 'merge-audio-to-webtorrent',
291 resolution: DEFAULT_AUDIO_RESOLUTION,
292 fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE,
299 private buildOptimizePayload (options: {
301 quickTranscode: boolean
304 }): OptimizeTranscodingPayload {
305 const { videoUUID, quickTranscode, isNewVideo, hasChildren } = options
308 type: 'optimize-to-webtorrent',