]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/job-queue/handlers/video-studio-edition.ts
caf051bfa7d6e532f6313c5ea374342dd5b6e2e0
[github/Chocobozzz/PeerTube.git] / server / lib / job-queue / handlers / video-studio-edition.ts
1 import { Job } from 'bullmq'
2 import { remove } from 'fs-extra'
3 import { join } from 'path'
4 import { getFFmpegCommandWrapperOptions } from '@server/helpers/ffmpeg'
5 import { CONFIG } from '@server/initializers/config'
6 import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles'
7 import { isAbleToUploadVideo } from '@server/lib/user'
8 import { VideoPathManager } from '@server/lib/video-path-manager'
9 import { approximateIntroOutroAdditionalSize, onVideoStudioEnded, safeCleanupStudioTMPFiles } from '@server/lib/video-studio'
10 import { UserModel } from '@server/models/user/user'
11 import { VideoModel } from '@server/models/video/video'
12 import { MVideo, MVideoFullLight } from '@server/types/models'
13 import { pick } from '@shared/core-utils'
14 import { buildUUID } from '@shared/extra-utils'
15 import { FFmpegEdition } from '@shared/ffmpeg'
16 import {
17 VideoStudioEditionPayload,
18 VideoStudioTask,
19 VideoStudioTaskCutPayload,
20 VideoStudioTaskIntroPayload,
21 VideoStudioTaskOutroPayload,
22 VideoStudioTaskPayload,
23 VideoStudioTaskWatermarkPayload
24 } from '@shared/models'
25 import { logger, loggerTagsFactory } from '../../../helpers/logger'
26
27 const lTagsBase = loggerTagsFactory('video-studio')
28
29 async function processVideoStudioEdition (job: Job) {
30 const payload = job.data as VideoStudioEditionPayload
31 const lTags = lTagsBase(payload.videoUUID)
32
33 logger.info('Process video studio edition of %s in job %s.', payload.videoUUID, job.id, lTags)
34
35 try {
36 const video = await VideoModel.loadFull(payload.videoUUID)
37
38 // No video, maybe deleted?
39 if (!video) {
40 logger.info('Can\'t process job %d, video does not exist.', job.id, lTags)
41
42 await safeCleanupStudioTMPFiles(payload.tasks)
43 return undefined
44 }
45
46 await checkUserQuotaOrThrow(video, payload)
47
48 const inputFile = video.getMaxQualityFile()
49
50 const editionResultPath = await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async originalFilePath => {
51 let tmpInputFilePath: string
52 let outputPath: string
53
54 for (const task of payload.tasks) {
55 const outputFilename = buildUUID() + inputFile.extname
56 outputPath = join(CONFIG.STORAGE.TMP_DIR, outputFilename)
57
58 await processTask({
59 inputPath: tmpInputFilePath ?? originalFilePath,
60 video,
61 outputPath,
62 task,
63 lTags
64 })
65
66 if (tmpInputFilePath) await remove(tmpInputFilePath)
67
68 // For the next iteration
69 tmpInputFilePath = outputPath
70 }
71
72 return outputPath
73 })
74
75 logger.info('Video edition ended for video %s.', video.uuid, lTags)
76
77 await onVideoStudioEnded({ video, editionResultPath, tasks: payload.tasks })
78 } catch (err) {
79 await safeCleanupStudioTMPFiles(payload.tasks)
80
81 throw err
82 }
83 }
84
85 // ---------------------------------------------------------------------------
86
87 export {
88 processVideoStudioEdition
89 }
90
91 // ---------------------------------------------------------------------------
92
93 type TaskProcessorOptions <T extends VideoStudioTaskPayload = VideoStudioTaskPayload> = {
94 inputPath: string
95 outputPath: string
96 video: MVideo
97 task: T
98 lTags: { tags: string[] }
99 }
100
101 const taskProcessors: { [id in VideoStudioTask['name']]: (options: TaskProcessorOptions) => Promise<any> } = {
102 'add-intro': processAddIntroOutro,
103 'add-outro': processAddIntroOutro,
104 'cut': processCut,
105 'add-watermark': processAddWatermark
106 }
107
108 async function processTask (options: TaskProcessorOptions) {
109 const { video, task, lTags } = options
110
111 logger.info('Processing %s task for video %s.', task.name, video.uuid, { task, ...lTags })
112
113 const processor = taskProcessors[options.task.name]
114 if (!process) throw new Error('Unknown task ' + task.name)
115
116 return processor(options)
117 }
118
119 function processAddIntroOutro (options: TaskProcessorOptions<VideoStudioTaskIntroPayload | VideoStudioTaskOutroPayload>) {
120 const { task, lTags } = options
121
122 logger.debug('Will add intro/outro to the video.', { options, ...lTags })
123
124 return buildFFmpegEdition().addIntroOutro({
125 ...pick(options, [ 'inputPath', 'outputPath' ]),
126
127 introOutroPath: task.options.file,
128 type: task.name === 'add-intro'
129 ? 'intro'
130 : 'outro'
131 })
132 }
133
134 function processCut (options: TaskProcessorOptions<VideoStudioTaskCutPayload>) {
135 const { task, lTags } = options
136
137 logger.debug('Will cut the video.', { options, ...lTags })
138
139 return buildFFmpegEdition().cutVideo({
140 ...pick(options, [ 'inputPath', 'outputPath' ]),
141
142 start: task.options.start,
143 end: task.options.end
144 })
145 }
146
147 function processAddWatermark (options: TaskProcessorOptions<VideoStudioTaskWatermarkPayload>) {
148 const { task, lTags } = options
149
150 logger.debug('Will add watermark to the video.', { options, ...lTags })
151
152 return buildFFmpegEdition().addWatermark({
153 ...pick(options, [ 'inputPath', 'outputPath' ]),
154
155 watermarkPath: task.options.file,
156
157 videoFilters: {
158 watermarkSizeRatio: task.options.watermarkSizeRatio,
159 horitonzalMarginRatio: task.options.horitonzalMarginRatio,
160 verticalMarginRatio: task.options.verticalMarginRatio
161 }
162 })
163 }
164
165 // ---------------------------------------------------------------------------
166
167 async function checkUserQuotaOrThrow (video: MVideoFullLight, payload: VideoStudioEditionPayload) {
168 const user = await UserModel.loadByVideoId(video.id)
169
170 const filePathFinder = (i: number) => (payload.tasks[i] as VideoStudioTaskIntroPayload | VideoStudioTaskOutroPayload).options.file
171
172 const additionalBytes = await approximateIntroOutroAdditionalSize(video, payload.tasks, filePathFinder)
173 if (await isAbleToUploadVideo(user.id, additionalBytes) === false) {
174 throw new Error('Quota exceeded for this user to edit the video')
175 }
176 }
177
178 function buildFFmpegEdition () {
179 return new FFmpegEdition(getFFmpegCommandWrapperOptions('vod', VideoTranscodingProfilesManager.Instance.getAvailableEncoders()))
180 }