]>
Commit | Line | Data |
---|---|---|
0c9668f7 C |
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' | |
5e47f6ab | 19 | import { getTranscodingJobPriority } from '../../transcoding-priority' |
0c9668f7 C |
20 | import { canDoQuickTranscode } from '../../transcoding-quick-transcode' |
21 | import { computeResolutionsToTranscode } from '../../transcoding-resolutions' | |
22 | import { AbstractJobBuilder } from './abstract-job-builder' | |
23 | ||
24 | export 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 | } |