]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/transcoding/shared/job-builders/transcoding-job-queue-builder.ts
Support studio transcoding in peertube runner
[github/Chocobozzz/PeerTube.git] / server / lib / transcoding / shared / job-builders / transcoding-job-queue-builder.ts
CommitLineData
0c9668f7
C
1import Bluebird from 'bluebird'
2import { computeOutputFPS } from '@server/helpers/ffmpeg'
3import { logger } from '@server/helpers/logger'
4import { CONFIG } from '@server/initializers/config'
5import { DEFAULT_AUDIO_RESOLUTION, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
6import { CreateJobArgument, JobQueue } from '@server/lib/job-queue'
7import { Hooks } from '@server/lib/plugins/hooks'
8import { VideoPathManager } from '@server/lib/video-path-manager'
9import { VideoJobInfoModel } from '@server/models/video/video-job-info'
10import { MUserId, MVideoFile, MVideoFullLight, MVideoWithFileThumbnail } from '@server/types/models'
11import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream, isAudioFile } from '@shared/ffmpeg'
12import {
13 HLSTranscodingPayload,
14 MergeAudioTranscodingPayload,
15 NewWebTorrentResolutionTranscodingPayload,
16 OptimizeTranscodingPayload,
17 VideoTranscodingPayload
18} from '@shared/models'
5e47f6ab 19import { getTranscodingJobPriority } from '../../transcoding-priority'
0c9668f7
C
20import { canDoQuickTranscode } from '../../transcoding-quick-transcode'
21import { computeResolutionsToTranscode } from '../../transcoding-resolutions'
22import { AbstractJobBuilder } from './abstract-job-builder'
23
24export class TranscodingJobQueueBuilder extends AbstractJobBuilder {
25
26 async createOptimizeOrMergeAudioJobs (options: {
27 video: MVideoFullLight
28 videoFile: MVideoFile
29 isNewVideo: boolean
30 user: MUserId
9a3db678 31 videoFileAlreadyLocked: boolean
0c9668f7 32 }) {
9a3db678 33 const { video, videoFile, isNewVideo, user, videoFileAlreadyLocked } = options
0c9668f7
C
34
35 let mergeOrOptimizePayload: MergeAudioTranscodingPayload | OptimizeTranscodingPayload
36 let nextTranscodingSequentialJobPayloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[][] = []
37
9a3db678
C
38 const mutexReleaser = videoFileAlreadyLocked
39 ? () => {}
40 : await VideoPathManager.Instance.lockFiles(video.uuid)
0c9668f7
C
41
42 try {
43 await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), async videoFilePath => {
44 const probe = await ffprobePromise(videoFilePath)
45
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)
52
53 const maxResolution = await isAudioFile(videoFilePath, probe)
54 ? DEFAULT_AUDIO_RESOLUTION
55 : resolution
56
57 if (CONFIG.TRANSCODING.HLS.ENABLED === true) {
58 nextTranscodingSequentialJobPayloads.push([
59 this.buildHLSJobPayload({
60 deleteWebTorrentFiles: CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false,
61
62 // We had some issues with a web video quick transcoded while producing a HLS version of it
63 copyCodecs: !quickTranscode,
64
65 resolution: maxResolution,
66 fps: computeOutputFPS({ inputFPS, resolution: maxResolution }),
67 videoUUID: video.uuid,
68 isNewVideo
69 })
70 ])
71 }
72
73 const lowerResolutionJobPayloads = await this.buildLowerResolutionJobPayloads({
74 video,
75 inputVideoResolution: maxResolution,
76 inputVideoFPS: inputFPS,
77 hasAudio,
78 isNewVideo
79 })
80
81 nextTranscodingSequentialJobPayloads = [ ...nextTranscodingSequentialJobPayloads, ...lowerResolutionJobPayloads ]
82
cc2abbc3 83 const hasChildren = nextTranscodingSequentialJobPayloads.length !== 0
0c9668f7 84 mergeOrOptimizePayload = videoFile.isAudio()
cc2abbc3
C
85 ? this.buildMergeAudioPayload({ videoUUID: video.uuid, isNewVideo, hasChildren })
86 : this.buildOptimizePayload({ videoUUID: video.uuid, isNewVideo, quickTranscode, hasChildren })
0c9668f7
C
87 })
88 } finally {
89 mutexReleaser()
90 }
91
92 const nextTranscodingSequentialJobs = await Bluebird.mapSeries(nextTranscodingSequentialJobPayloads, payloads => {
93 return Bluebird.mapSeries(payloads, payload => {
94 return this.buildTranscodingJob({ payload, user })
95 })
96 })
97
98 const transcodingJobBuilderJob: CreateJobArgument = {
99 type: 'transcoding-job-builder',
100 payload: {
101 videoUUID: video.uuid,
102 sequentialJobs: nextTranscodingSequentialJobs
103 }
104 }
105
106 const mergeOrOptimizeJob = await this.buildTranscodingJob({ payload: mergeOrOptimizePayload, user })
107
cc2abbc3
C
108 await JobQueue.Instance.createSequentialJobFlow(...[ mergeOrOptimizeJob, transcodingJobBuilderJob ])
109
110 await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingTranscode')
0c9668f7
C
111 }
112
113 // ---------------------------------------------------------------------------
114
115 async createTranscodingJobs (options: {
116 transcodingType: 'hls' | 'webtorrent'
117 video: MVideoFullLight
118 resolutions: number[]
119 isNewVideo: boolean
120 user: MUserId | null
121 }) {
122 const { video, transcodingType, resolutions, isNewVideo } = options
123
124 const maxResolution = Math.max(...resolutions)
125 const childrenResolutions = resolutions.filter(r => r !== maxResolution)
126
127 logger.info('Manually creating transcoding jobs for %s.', transcodingType, { childrenResolutions, maxResolution })
128
129 const { fps: inputFPS } = await video.probeMaxQualityFile()
130
131 const children = childrenResolutions.map(resolution => {
132 const fps = computeOutputFPS({ inputFPS, resolution })
133
134 if (transcodingType === 'hls') {
135 return this.buildHLSJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo })
136 }
137
138 if (transcodingType === 'webtorrent') {
139 return this.buildWebTorrentJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo })
140 }
141
142 throw new Error('Unknown transcoding type')
143 })
144
145 const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
146
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 })
150
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 })
154 }
155
156 // ---------------------------------------------------------------------------
157
158 private async createTranscodingJobsWithChildren (options: {
159 videoUUID: string
160 parent: (HLSTranscodingPayload | NewWebTorrentResolutionTranscodingPayload)
161 children: (HLSTranscodingPayload | NewWebTorrentResolutionTranscodingPayload)[]
162 user: MUserId | null
163 }) {
164 const { videoUUID, parent, children, user } = options
165
166 const parentJob = await this.buildTranscodingJob({ payload: parent, user })
167 const childrenJobs = await Bluebird.mapSeries(children, c => this.buildTranscodingJob({ payload: c, user }))
168
169 await JobQueue.Instance.createJobWithChildren(parentJob, childrenJobs)
170
171 await VideoJobInfoModel.increaseOrCreate(videoUUID, 'pendingTranscode', 1 + children.length)
172 }
173
174 private async buildTranscodingJob (options: {
175 payload: VideoTranscodingPayload
176 user: MUserId | null // null means we don't want priority
177 }) {
178 const { user, payload } = options
179
180 return {
181 type: 'video-transcoding' as 'video-transcoding',
5e47f6ab 182 priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: undefined }),
0c9668f7
C
183 payload
184 }
185 }
186
187 private async buildLowerResolutionJobPayloads (options: {
188 video: MVideoWithFileThumbnail
189 inputVideoResolution: number
190 inputVideoFPS: number
191 hasAudio: boolean
192 isNewVideo: boolean
193 }) {
194 const { video, inputVideoResolution, inputVideoFPS, isNewVideo, hasAudio } = options
195
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',
200 options
201 )
202
203 const sequentialPayloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[][] = []
204
205 for (const resolution of resolutionsEnabled) {
206 const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution })
207
208 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) {
209 const payloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[] = [
210 this.buildWebTorrentJobPayload({
211 videoUUID: video.uuid,
212 resolution,
213 fps,
214 isNewVideo
215 })
216 ]
217
218 // Create a subsequent job to create HLS resolution that will just copy web video codecs
219 if (CONFIG.TRANSCODING.HLS.ENABLED) {
220 payloads.push(
221 this.buildHLSJobPayload({
222 videoUUID: video.uuid,
223 resolution,
224 fps,
225 isNewVideo,
226 copyCodecs: true
227 })
228 )
229 }
230
231 sequentialPayloads.push(payloads)
232 } else if (CONFIG.TRANSCODING.HLS.ENABLED) {
233 sequentialPayloads.push([
234 this.buildHLSJobPayload({
235 videoUUID: video.uuid,
236 resolution,
237 fps,
238 copyCodecs: false,
239 isNewVideo
240 })
241 ])
242 }
243 }
244
245 return sequentialPayloads
246 }
247
248 private buildHLSJobPayload (options: {
249 videoUUID: string
250 resolution: number
251 fps: number
252 isNewVideo: boolean
253 deleteWebTorrentFiles?: boolean // default false
254 copyCodecs?: boolean // default false
255 }): HLSTranscodingPayload {
256 const { videoUUID, resolution, fps, isNewVideo, deleteWebTorrentFiles = false, copyCodecs = false } = options
257
258 return {
259 type: 'new-resolution-to-hls',
260 videoUUID,
261 resolution,
262 fps,
263 copyCodecs,
264 isNewVideo,
265 deleteWebTorrentFiles
266 }
267 }
268
269 private buildWebTorrentJobPayload (options: {
270 videoUUID: string
271 resolution: number
272 fps: number
273 isNewVideo: boolean
274 }): NewWebTorrentResolutionTranscodingPayload {
275 const { videoUUID, resolution, fps, isNewVideo } = options
276
277 return {
278 type: 'new-resolution-to-webtorrent',
279 videoUUID,
280 isNewVideo,
281 resolution,
282 fps
283 }
284 }
285
286 private buildMergeAudioPayload (options: {
287 videoUUID: string
288 isNewVideo: boolean
cc2abbc3 289 hasChildren: boolean
0c9668f7 290 }): MergeAudioTranscodingPayload {
cc2abbc3 291 const { videoUUID, isNewVideo, hasChildren } = options
0c9668f7
C
292
293 return {
294 type: 'merge-audio-to-webtorrent',
295 resolution: DEFAULT_AUDIO_RESOLUTION,
296 fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE,
297 videoUUID,
cc2abbc3
C
298 isNewVideo,
299 hasChildren
0c9668f7
C
300 }
301 }
302
303 private buildOptimizePayload (options: {
304 videoUUID: string
305 quickTranscode: boolean
306 isNewVideo: boolean
cc2abbc3 307 hasChildren: boolean
0c9668f7 308 }): OptimizeTranscodingPayload {
cc2abbc3 309 const { videoUUID, quickTranscode, isNewVideo, hasChildren } = options
0c9668f7
C
310
311 return {
312 type: 'optimize-to-webtorrent',
313 videoUUID,
314 isNewVideo,
cc2abbc3 315 hasChildren,
0c9668f7
C
316 quickTranscode
317 }
318 }
319}