]>
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' | |
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 | ||
cc2abbc3 | 79 | const hasChildren = nextTranscodingSequentialJobPayloads.length !== 0 |
0c9668f7 | 80 | mergeOrOptimizePayload = videoFile.isAudio() |
cc2abbc3 C |
81 | ? this.buildMergeAudioPayload({ videoUUID: video.uuid, isNewVideo, hasChildren }) |
82 | : this.buildOptimizePayload({ videoUUID: video.uuid, isNewVideo, quickTranscode, hasChildren }) | |
0c9668f7 C |
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 | ||
cc2abbc3 C |
104 | await JobQueue.Instance.createSequentialJobFlow(...[ mergeOrOptimizeJob, transcodingJobBuilderJob ]) |
105 | ||
106 | await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingTranscode') | |
0c9668f7 C |
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 | |
cc2abbc3 | 285 | hasChildren: boolean |
0c9668f7 | 286 | }): MergeAudioTranscodingPayload { |
cc2abbc3 | 287 | const { videoUUID, isNewVideo, hasChildren } = options |
0c9668f7 C |
288 | |
289 | return { | |
290 | type: 'merge-audio-to-webtorrent', | |
291 | resolution: DEFAULT_AUDIO_RESOLUTION, | |
292 | fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE, | |
293 | videoUUID, | |
cc2abbc3 C |
294 | isNewVideo, |
295 | hasChildren | |
0c9668f7 C |
296 | } |
297 | } | |
298 | ||
299 | private buildOptimizePayload (options: { | |
300 | videoUUID: string | |
301 | quickTranscode: boolean | |
302 | isNewVideo: boolean | |
cc2abbc3 | 303 | hasChildren: boolean |
0c9668f7 | 304 | }): OptimizeTranscodingPayload { |
cc2abbc3 | 305 | const { videoUUID, quickTranscode, isNewVideo, hasChildren } = options |
0c9668f7 C |
306 | |
307 | return { | |
308 | type: 'optimize-to-webtorrent', | |
309 | videoUUID, | |
310 | isNewVideo, | |
cc2abbc3 | 311 | hasChildren, |
0c9668f7 C |
312 | quickTranscode |
313 | } | |
314 | } | |
315 | } |