]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/job-queue/handlers/video-transcoding.ts
0f6b3f753071d63cae69dbfa3efc6b344935ae12
[github/Chocobozzz/PeerTube.git] / server / lib / job-queue / handlers / video-transcoding.ts
1 import * as Bull from 'bull'
2 import { TranscodeOptionsType } from '@server/helpers/ffmpeg-utils'
3 import { publishAndFederateIfNeeded } from '@server/lib/video'
4 import { getVideoFilePath } from '@server/lib/video-paths'
5 import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models'
6 import {
7 HLSTranscodingPayload,
8 MergeAudioTranscodingPayload,
9 NewResolutionTranscodingPayload,
10 OptimizeTranscodingPayload,
11 VideoTranscodingPayload
12 } from '../../../../shared'
13 import { retryTransactionWrapper } from '../../../helpers/database-utils'
14 import { computeResolutionsToTranscode } from '../../../helpers/ffprobe-utils'
15 import { logger } from '../../../helpers/logger'
16 import { CONFIG } from '../../../initializers/config'
17 import { sequelizeTypescript } from '../../../initializers/database'
18 import { VideoModel } from '../../../models/video/video'
19 import { federateVideoIfNeeded } from '../../activitypub/videos'
20 import { Notifier } from '../../notifier'
21 import {
22 generateHlsPlaylistResolution,
23 mergeAudioVideofile,
24 optimizeOriginalVideofile,
25 transcodeNewWebTorrentResolution
26 } from '../../video-transcoding'
27 import { JobQueue } from '../job-queue'
28
29 const handlers: { [ id: string ]: (job: Bull.Job, payload: VideoTranscodingPayload, video: MVideoFullLight) => Promise<any> } = {
30 // Deprecated, introduced in 3.1
31 'hls': handleHLSJob,
32 'new-resolution-to-hls': handleHLSJob,
33
34 // Deprecated, introduced in 3.1
35 'new-resolution': handleNewWebTorrentResolutionJob,
36 'new-resolution-to-webtorrent': handleNewWebTorrentResolutionJob,
37
38 // Deprecated, introduced in 3.1
39 'merge-audio': handleWebTorrentMergeAudioJob,
40 'merge-audio-to-webtorrent': handleWebTorrentMergeAudioJob,
41
42 // Deprecated, introduced in 3.1
43 'optimize': handleWebTorrentOptimizeJob,
44 'optimize-to-webtorrent': handleWebTorrentOptimizeJob
45 }
46
47 async function processVideoTranscoding (job: Bull.Job) {
48 const payload = job.data as VideoTranscodingPayload
49 logger.info('Processing video file in job %d.', job.id)
50
51 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoUUID)
52 // No video, maybe deleted?
53 if (!video) {
54 logger.info('Do not process job %d, video does not exist.', job.id)
55 return undefined
56 }
57
58 const handler = handlers[payload.type]
59
60 if (!handler) {
61 throw new Error('Cannot find transcoding handler for ' + payload.type)
62 }
63
64 await handler(job, payload, video)
65
66 return video
67 }
68
69 // ---------------------------------------------------------------------------
70 // Job handlers
71 // ---------------------------------------------------------------------------
72
73 async function handleHLSJob (job: Bull.Job, payload: HLSTranscodingPayload, video: MVideoFullLight) {
74 const videoFileInput = payload.copyCodecs
75 ? video.getWebTorrentFile(payload.resolution)
76 : video.getMaxQualityFile()
77
78 const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist()
79 const videoInputPath = getVideoFilePath(videoOrStreamingPlaylist, videoFileInput)
80
81 await generateHlsPlaylistResolution({
82 video,
83 videoInputPath,
84 resolution: payload.resolution,
85 copyCodecs: payload.copyCodecs,
86 isPortraitMode: payload.isPortraitMode || false,
87 job
88 })
89
90 await retryTransactionWrapper(onHlsPlaylistGeneration, video)
91 }
92
93 async function handleNewWebTorrentResolutionJob (job: Bull.Job, payload: NewResolutionTranscodingPayload, video: MVideoFullLight) {
94 await transcodeNewWebTorrentResolution(video, payload.resolution, payload.isPortraitMode || false, job)
95
96 await retryTransactionWrapper(onNewWebTorrentFileResolution, video, payload)
97 }
98
99 async function handleWebTorrentMergeAudioJob (job: Bull.Job, payload: MergeAudioTranscodingPayload, video: MVideoFullLight) {
100 await mergeAudioVideofile(video, payload.resolution, job)
101
102 await retryTransactionWrapper(onNewWebTorrentFileResolution, video, payload)
103 }
104
105 async function handleWebTorrentOptimizeJob (job: Bull.Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight) {
106 const transcodeType = await optimizeOriginalVideofile(video, video.getMaxQualityFile(), job)
107
108 await retryTransactionWrapper(onVideoFileOptimizer, video, payload, transcodeType)
109 }
110
111 // ---------------------------------------------------------------------------
112
113 async function onHlsPlaylistGeneration (video: MVideoFullLight) {
114 if (video === undefined) return undefined
115
116 // We generated the HLS playlist, we don't need the webtorrent files anymore if the admin disabled it
117 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) {
118 for (const file of video.VideoFiles) {
119 await video.removeFile(file)
120 await file.destroy()
121 }
122
123 video.VideoFiles = []
124 }
125
126 return publishAndFederateIfNeeded(video)
127 }
128
129 async function onVideoFileOptimizer (
130 videoArg: MVideoWithFile,
131 payload: OptimizeTranscodingPayload,
132 transcodeType: TranscodeOptionsType
133 ) {
134 if (videoArg === undefined) return undefined
135
136 // Outside the transaction (IO on disk)
137 const { videoFileResolution, isPortraitMode } = await videoArg.getMaxQualityResolution()
138
139 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
140 // Maybe the video changed in database, refresh it
141 const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t)
142 // Video does not exist anymore
143 if (!videoDatabase) return undefined
144
145 // Create transcoding jobs if there are enabled resolutions
146 const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution, 'vod')
147 logger.info(
148 'Resolutions computed for video %s and origin file resolution of %d.', videoDatabase.uuid, videoFileResolution,
149 { resolutions: resolutionsEnabled }
150 )
151
152 let videoPublished = false
153
154 // Generate HLS version of the original file
155 const originalFileHLSPayload = Object.assign({}, payload, {
156 isPortraitMode,
157 resolution: videoDatabase.getMaxQualityFile().resolution,
158 // If we quick transcoded original file, force transcoding for HLS to avoid some weird playback issues
159 copyCodecs: transcodeType !== 'quick-transcode'
160 })
161 createHlsJobIfEnabled(originalFileHLSPayload)
162
163 const hasNewResolutions = createLowerResolutionsJobs(videoDatabase, videoFileResolution, isPortraitMode)
164
165 if (!hasNewResolutions) {
166 // No transcoding to do, it's now published
167 videoPublished = await videoDatabase.publishIfNeededAndSave(t)
168 }
169
170 await federateVideoIfNeeded(videoDatabase, payload.isNewVideo, t)
171
172 return { videoDatabase, videoPublished }
173 })
174
175 if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase)
176 if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
177 }
178
179 async function onNewWebTorrentFileResolution (
180 video: MVideoUUID,
181 payload?: NewResolutionTranscodingPayload | MergeAudioTranscodingPayload
182 ) {
183 await publishAndFederateIfNeeded(video)
184
185 createHlsJobIfEnabled(Object.assign({}, payload, { copyCodecs: true }))
186 }
187
188 // ---------------------------------------------------------------------------
189
190 export {
191 processVideoTranscoding,
192 onNewWebTorrentFileResolution
193 }
194
195 // ---------------------------------------------------------------------------
196
197 function createHlsJobIfEnabled (payload: { videoUUID: string, resolution: number, isPortraitMode?: boolean, copyCodecs: boolean }) {
198 // Generate HLS playlist?
199 if (payload && CONFIG.TRANSCODING.HLS.ENABLED) {
200 const hlsTranscodingPayload: HLSTranscodingPayload = {
201 type: 'new-resolution-to-hls',
202 videoUUID: payload.videoUUID,
203 resolution: payload.resolution,
204 isPortraitMode: payload.isPortraitMode,
205 copyCodecs: payload.copyCodecs
206 }
207
208 return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload })
209 }
210 }
211
212 function createLowerResolutionsJobs (video: MVideoFullLight, videoFileResolution: number, isPortraitMode: boolean) {
213 // Create transcoding jobs if there are enabled resolutions
214 const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution, 'vod')
215 logger.info(
216 'Resolutions computed for video %s and origin file resolution of %d.', video.uuid, videoFileResolution,
217 { resolutions: resolutionsEnabled }
218 )
219
220 if (resolutionsEnabled.length === 0) {
221 logger.info('No transcoding jobs created for video %s (no resolutions).', video.uuid)
222
223 return false
224 }
225
226 for (const resolution of resolutionsEnabled) {
227 let dataInput: VideoTranscodingPayload
228
229 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) {
230 // WebTorrent will create subsequent HLS job
231 dataInput = {
232 type: 'new-resolution-to-webtorrent',
233 videoUUID: video.uuid,
234 resolution,
235 isPortraitMode
236 }
237 } else if (CONFIG.TRANSCODING.HLS.ENABLED) {
238 dataInput = {
239 type: 'new-resolution-to-hls',
240 videoUUID: video.uuid,
241 resolution,
242 isPortraitMode,
243 copyCodecs: false
244 }
245 }
246
247 JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
248 }
249
250 logger.info('Transcoding jobs created for uuid %s.', video.uuid, { resolutionsEnabled })
251
252 return true
253 }