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'
17 VideoStudioEditionPayload,
19 VideoStudioTaskCutPayload,
20 VideoStudioTaskIntroPayload,
21 VideoStudioTaskOutroPayload,
22 VideoStudioTaskPayload,
23 VideoStudioTaskWatermarkPayload
24 } from '@shared/models'
25 import { logger, loggerTagsFactory } from '../../../helpers/logger'
27 const lTagsBase = loggerTagsFactory('video-studio')
29 async function processVideoStudioEdition (job: Job) {
30 const payload = job.data as VideoStudioEditionPayload
31 const lTags = lTagsBase(payload.videoUUID)
33 logger.info('Process video studio edition of %s in job %s.', payload.videoUUID, job.id, lTags)
36 const video = await VideoModel.loadFull(payload.videoUUID)
38 // No video, maybe deleted?
40 logger.info('Can\'t process job %d, video does not exist.', job.id, lTags)
42 await safeCleanupStudioTMPFiles(payload.tasks)
46 await checkUserQuotaOrThrow(video, payload)
48 const inputFile = video.getMaxQualityFile()
50 const editionResultPath = await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async originalFilePath => {
51 let tmpInputFilePath: string
52 let outputPath: string
54 for (const task of payload.tasks) {
55 const outputFilename = buildUUID() + inputFile.extname
56 outputPath = join(CONFIG.STORAGE.TMP_DIR, outputFilename)
59 inputPath: tmpInputFilePath ?? originalFilePath,
66 if (tmpInputFilePath) await remove(tmpInputFilePath)
68 // For the next iteration
69 tmpInputFilePath = outputPath
75 logger.info('Video edition ended for video %s.', video.uuid, lTags)
77 await onVideoStudioEnded({ video, editionResultPath, tasks: payload.tasks })
79 await safeCleanupStudioTMPFiles(payload.tasks)
85 // ---------------------------------------------------------------------------
88 processVideoStudioEdition
91 // ---------------------------------------------------------------------------
93 type TaskProcessorOptions <T extends VideoStudioTaskPayload = VideoStudioTaskPayload> = {
98 lTags: { tags: string[] }
101 const taskProcessors: { [id in VideoStudioTask['name']]: (options: TaskProcessorOptions) => Promise<any> } = {
102 'add-intro': processAddIntroOutro,
103 'add-outro': processAddIntroOutro,
105 'add-watermark': processAddWatermark
108 async function processTask (options: TaskProcessorOptions) {
109 const { video, task, lTags } = options
111 logger.info('Processing %s task for video %s.', task.name, video.uuid, { task, ...lTags })
113 const processor = taskProcessors[options.task.name]
114 if (!process) throw new Error('Unknown task ' + task.name)
116 return processor(options)
119 function processAddIntroOutro (options: TaskProcessorOptions<VideoStudioTaskIntroPayload | VideoStudioTaskOutroPayload>) {
120 const { task, lTags } = options
122 logger.debug('Will add intro/outro to the video.', { options, ...lTags })
124 return buildFFmpegEdition().addIntroOutro({
125 ...pick(options, [ 'inputPath', 'outputPath' ]),
127 introOutroPath: task.options.file,
128 type: task.name === 'add-intro'
134 function processCut (options: TaskProcessorOptions<VideoStudioTaskCutPayload>) {
135 const { task, lTags } = options
137 logger.debug('Will cut the video.', { options, ...lTags })
139 return buildFFmpegEdition().cutVideo({
140 ...pick(options, [ 'inputPath', 'outputPath' ]),
142 start: task.options.start,
143 end: task.options.end
147 function processAddWatermark (options: TaskProcessorOptions<VideoStudioTaskWatermarkPayload>) {
148 const { task, lTags } = options
150 logger.debug('Will add watermark to the video.', { options, ...lTags })
152 return buildFFmpegEdition().addWatermark({
153 ...pick(options, [ 'inputPath', 'outputPath' ]),
155 watermarkPath: task.options.file,
158 watermarkSizeRatio: task.options.watermarkSizeRatio,
159 horitonzalMarginRatio: task.options.horitonzalMarginRatio,
160 verticalMarginRatio: task.options.verticalMarginRatio
165 // ---------------------------------------------------------------------------
167 async function checkUserQuotaOrThrow (video: MVideoFullLight, payload: VideoStudioEditionPayload) {
168 const user = await UserModel.loadByVideoId(video.id)
170 const filePathFinder = (i: number) => (payload.tasks[i] as VideoStudioTaskIntroPayload | VideoStudioTaskOutroPayload).options.file
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')
178 function buildFFmpegEdition () {
179 return new FFmpegEdition(getFFmpegCommandWrapperOptions('vod', VideoTranscodingProfilesManager.Instance.getAvailableEncoders()))