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