]>
Commit | Line | Data |
---|---|---|
5a921e7b | 1 | import { Job } from 'bullmq' |
5e47f6ab | 2 | import { remove } from 'fs-extra' |
c729caf6 | 3 | import { join } from 'path' |
0c9668f7 | 4 | import { getFFmpegCommandWrapperOptions } from '@server/helpers/ffmpeg' |
c729caf6 | 5 | import { CONFIG } from '@server/initializers/config' |
c729caf6 C |
6 | import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles' |
7 | import { isAbleToUploadVideo } from '@server/lib/user' | |
c729caf6 | 8 | import { VideoPathManager } from '@server/lib/video-path-manager' |
ab14f0e0 | 9 | import { approximateIntroOutroAdditionalSize, onVideoStudioEnded, safeCleanupStudioTMPFiles } from '@server/lib/video-studio' |
c729caf6 C |
10 | import { UserModel } from '@server/models/user/user' |
11 | import { VideoModel } from '@server/models/video/video' | |
5e47f6ab C |
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' | |
c729caf6 | 16 | import { |
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' |
25 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | |
26 | ||
ab14f0e0 | 27 | const lTagsBase = loggerTagsFactory('video-studio') |
c729caf6 | 28 | |
92e66e04 C |
29 | async 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 | ||
87 | export { | |
92e66e04 | 88 | processVideoStudioEdition |
c729caf6 C |
89 | } |
90 | ||
91 | // --------------------------------------------------------------------------- | |
92 | ||
92e66e04 | 93 | type 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 | 101 | const 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 | ||
108 | async 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 | 119 | function 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 | 134 | function 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 | 147 | function 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 | 167 | async 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 | |
178 | function buildFFmpegEdition () { | |
179 | return new FFmpegEdition(getFFmpegCommandWrapperOptions('vod', VideoTranscodingProfilesManager.Instance.getAvailableEncoders())) | |
180 | } |