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