]>
Commit | Line | Data |
---|---|---|
5a921e7b | 1 | import { Job } from 'bullmq' |
c729caf6 C |
2 | import { move, remove } from 'fs-extra' |
3 | import { join } from 'path' | |
0c9668f7 | 4 | import { getFFmpegCommandWrapperOptions } from '@server/helpers/ffmpeg' |
c729caf6 C |
5 | import { createTorrentAndSetInfoHashFromPath } from '@server/helpers/webtorrent' |
6 | import { CONFIG } from '@server/initializers/config' | |
0c9668f7 | 7 | import { VIDEO_FILTERS } from '@server/initializers/constants' |
c729caf6 C |
8 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' |
9 | import { generateWebTorrentVideoFilename } from '@server/lib/paths' | |
0c9668f7 | 10 | import { createOptimizeOrMergeAudioJobs } from '@server/lib/transcoding/create-transcoding-job' |
c729caf6 C |
11 | import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles' |
12 | import { isAbleToUploadVideo } from '@server/lib/user' | |
0c9668f7 | 13 | import { buildFileMetadata, removeHLSPlaylist, removeWebTorrentFile } from '@server/lib/video-file' |
c729caf6 | 14 | import { VideoPathManager } from '@server/lib/video-path-manager' |
92e66e04 | 15 | import { approximateIntroOutroAdditionalSize } from '@server/lib/video-studio' |
c729caf6 C |
16 | import { UserModel } from '@server/models/user/user' |
17 | import { VideoModel } from '@server/models/video/video' | |
18 | import { VideoFileModel } from '@server/models/video/video-file' | |
19 | import { MVideo, MVideoFile, MVideoFullLight, MVideoId, MVideoWithAllFiles } from '@server/types/models' | |
20 | import { getLowercaseExtension, pick } from '@shared/core-utils' | |
0c9668f7 C |
21 | import { buildUUID, getFileSize } from '@shared/extra-utils' |
22 | import { FFmpegEdition, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamDuration, getVideoStreamFPS } from '@shared/ffmpeg' | |
c729caf6 | 23 | import { |
92e66e04 | 24 | VideoStudioEditionPayload, |
1bb4c9ab | 25 | VideoStudioTask, |
92e66e04 C |
26 | VideoStudioTaskCutPayload, |
27 | VideoStudioTaskIntroPayload, | |
28 | VideoStudioTaskOutroPayload, | |
1bb4c9ab C |
29 | VideoStudioTaskPayload, |
30 | VideoStudioTaskWatermarkPayload | |
c729caf6 C |
31 | } from '@shared/models' |
32 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | |
33 | ||
34 | const lTagsBase = loggerTagsFactory('video-edition') | |
35 | ||
92e66e04 C |
36 | async function processVideoStudioEdition (job: Job) { |
37 | const payload = job.data as VideoStudioEditionPayload | |
1808a1f8 | 38 | const lTags = lTagsBase(payload.videoUUID) |
c729caf6 | 39 | |
bd911b54 | 40 | logger.info('Process video studio edition of %s in job %s.', payload.videoUUID, job.id, lTags) |
c729caf6 | 41 | |
4fae2b1f | 42 | const video = await VideoModel.loadFull(payload.videoUUID) |
c729caf6 C |
43 | |
44 | // No video, maybe deleted? | |
45 | if (!video) { | |
1808a1f8 | 46 | logger.info('Can\'t process job %d, video does not exist.', job.id, lTags) |
c729caf6 C |
47 | return undefined |
48 | } | |
49 | ||
50 | await checkUserQuotaOrThrow(video, payload) | |
51 | ||
52 | const inputFile = video.getMaxQualityFile() | |
53 | ||
54 | const editionResultPath = await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async originalFilePath => { | |
55 | let tmpInputFilePath: string | |
56 | let outputPath: string | |
57 | ||
58 | for (const task of payload.tasks) { | |
59 | const outputFilename = buildUUID() + inputFile.extname | |
60 | outputPath = join(CONFIG.STORAGE.TMP_DIR, outputFilename) | |
61 | ||
62 | await processTask({ | |
63 | inputPath: tmpInputFilePath ?? originalFilePath, | |
64 | video, | |
65 | outputPath, | |
1808a1f8 C |
66 | task, |
67 | lTags | |
c729caf6 C |
68 | }) |
69 | ||
70 | if (tmpInputFilePath) await remove(tmpInputFilePath) | |
71 | ||
72 | // For the next iteration | |
73 | tmpInputFilePath = outputPath | |
74 | } | |
75 | ||
76 | return outputPath | |
77 | }) | |
78 | ||
1808a1f8 | 79 | logger.info('Video edition ended for video %s.', video.uuid, lTags) |
c729caf6 C |
80 | |
81 | const newFile = await buildNewFile(video, editionResultPath) | |
82 | ||
83 | const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newFile) | |
84 | await move(editionResultPath, outputPath) | |
85 | ||
86 | await createTorrentAndSetInfoHashFromPath(video, newFile, outputPath) | |
c729caf6 C |
87 | await removeAllFiles(video, newFile) |
88 | ||
89 | await newFile.save() | |
90 | ||
c729caf6 C |
91 | video.duration = await getVideoStreamDuration(outputPath) |
92 | await video.save() | |
93 | ||
94 | await federateVideoIfNeeded(video, false, undefined) | |
95 | ||
1808a1f8 | 96 | const user = await UserModel.loadByVideoId(video.id) |
bd911b54 | 97 | |
0c9668f7 | 98 | await createOptimizeOrMergeAudioJobs({ video, videoFile: newFile, isNewVideo: false, user }) |
c729caf6 C |
99 | } |
100 | ||
101 | // --------------------------------------------------------------------------- | |
102 | ||
103 | export { | |
92e66e04 | 104 | processVideoStudioEdition |
c729caf6 C |
105 | } |
106 | ||
107 | // --------------------------------------------------------------------------- | |
108 | ||
92e66e04 | 109 | type TaskProcessorOptions <T extends VideoStudioTaskPayload = VideoStudioTaskPayload> = { |
c729caf6 C |
110 | inputPath: string |
111 | outputPath: string | |
112 | video: MVideo | |
113 | task: T | |
1808a1f8 | 114 | lTags: { tags: string[] } |
c729caf6 C |
115 | } |
116 | ||
92e66e04 | 117 | const taskProcessors: { [id in VideoStudioTask['name']]: (options: TaskProcessorOptions) => Promise<any> } = { |
c729caf6 C |
118 | 'add-intro': processAddIntroOutro, |
119 | 'add-outro': processAddIntroOutro, | |
120 | 'cut': processCut, | |
121 | 'add-watermark': processAddWatermark | |
122 | } | |
123 | ||
124 | async function processTask (options: TaskProcessorOptions) { | |
0c9668f7 | 125 | const { video, task, lTags } = options |
c729caf6 | 126 | |
0c9668f7 | 127 | logger.info('Processing %s task for video %s.', task.name, video.uuid, { task, ...lTags }) |
c729caf6 C |
128 | |
129 | const processor = taskProcessors[options.task.name] | |
130 | if (!process) throw new Error('Unknown task ' + task.name) | |
131 | ||
132 | return processor(options) | |
133 | } | |
134 | ||
92e66e04 | 135 | function processAddIntroOutro (options: TaskProcessorOptions<VideoStudioTaskIntroPayload | VideoStudioTaskOutroPayload>) { |
0c9668f7 C |
136 | const { task, lTags } = options |
137 | ||
138 | logger.debug('Will add intro/outro to the video.', { options, ...lTags }) | |
c729caf6 | 139 | |
0c9668f7 | 140 | return buildFFmpegEdition().addIntroOutro({ |
c729caf6 C |
141 | ...pick(options, [ 'inputPath', 'outputPath' ]), |
142 | ||
143 | introOutroPath: task.options.file, | |
144 | type: task.name === 'add-intro' | |
145 | ? 'intro' | |
0c9668f7 | 146 | : 'outro' |
c729caf6 C |
147 | }) |
148 | } | |
149 | ||
92e66e04 | 150 | function processCut (options: TaskProcessorOptions<VideoStudioTaskCutPayload>) { |
0c9668f7 | 151 | const { task, lTags } = options |
c729caf6 | 152 | |
0c9668f7 C |
153 | logger.debug('Will cut the video.', { options, ...lTags }) |
154 | ||
155 | return buildFFmpegEdition().cutVideo({ | |
c729caf6 C |
156 | ...pick(options, [ 'inputPath', 'outputPath' ]), |
157 | ||
158 | start: task.options.start, | |
0c9668f7 | 159 | end: task.options.end |
c729caf6 C |
160 | }) |
161 | } | |
162 | ||
92e66e04 | 163 | function processAddWatermark (options: TaskProcessorOptions<VideoStudioTaskWatermarkPayload>) { |
0c9668f7 C |
164 | const { task, lTags } = options |
165 | ||
166 | logger.debug('Will add watermark to the video.', { options, ...lTags }) | |
c729caf6 | 167 | |
0c9668f7 | 168 | return buildFFmpegEdition().addWatermark({ |
c729caf6 C |
169 | ...pick(options, [ 'inputPath', 'outputPath' ]), |
170 | ||
171 | watermarkPath: task.options.file, | |
172 | ||
0c9668f7 C |
173 | videoFilters: { |
174 | watermarkSizeRatio: VIDEO_FILTERS.WATERMARK.SIZE_RATIO, | |
175 | horitonzalMarginRatio: VIDEO_FILTERS.WATERMARK.HORIZONTAL_MARGIN_RATIO, | |
176 | verticalMarginRatio: VIDEO_FILTERS.WATERMARK.VERTICAL_MARGIN_RATIO | |
177 | } | |
c729caf6 C |
178 | }) |
179 | } | |
180 | ||
0c9668f7 C |
181 | // --------------------------------------------------------------------------- |
182 | ||
c729caf6 C |
183 | async function buildNewFile (video: MVideoId, path: string) { |
184 | const videoFile = new VideoFileModel({ | |
185 | extname: getLowercaseExtension(path), | |
186 | size: await getFileSize(path), | |
187 | metadata: await buildFileMetadata(path), | |
188 | videoStreamingPlaylistId: null, | |
189 | videoId: video.id | |
190 | }) | |
191 | ||
192 | const probe = await ffprobePromise(path) | |
193 | ||
194 | videoFile.fps = await getVideoStreamFPS(path, probe) | |
195 | videoFile.resolution = (await getVideoStreamDimensionsInfo(path, probe)).resolution | |
196 | ||
197 | videoFile.filename = generateWebTorrentVideoFilename(videoFile.resolution, videoFile.extname) | |
198 | ||
199 | return videoFile | |
200 | } | |
201 | ||
202 | async function removeAllFiles (video: MVideoWithAllFiles, webTorrentFileException: MVideoFile) { | |
1bb4c9ab | 203 | await removeHLSPlaylist(video) |
c729caf6 C |
204 | |
205 | for (const file of video.VideoFiles) { | |
206 | if (file.id === webTorrentFileException.id) continue | |
207 | ||
1bb4c9ab | 208 | await removeWebTorrentFile(video, file.id) |
c729caf6 C |
209 | } |
210 | } | |
211 | ||
92e66e04 | 212 | async function checkUserQuotaOrThrow (video: MVideoFullLight, payload: VideoStudioEditionPayload) { |
c729caf6 C |
213 | const user = await UserModel.loadByVideoId(video.id) |
214 | ||
92e66e04 | 215 | const filePathFinder = (i: number) => (payload.tasks[i] as VideoStudioTaskIntroPayload | VideoStudioTaskOutroPayload).options.file |
c729caf6 C |
216 | |
217 | const additionalBytes = await approximateIntroOutroAdditionalSize(video, payload.tasks, filePathFinder) | |
218 | if (await isAbleToUploadVideo(user.id, additionalBytes) === false) { | |
219 | throw new Error('Quota exceeded for this user to edit the video') | |
220 | } | |
221 | } | |
0c9668f7 C |
222 | |
223 | function buildFFmpegEdition () { | |
224 | return new FFmpegEdition(getFFmpegCommandWrapperOptions('vod', VideoTranscodingProfilesManager.Instance.getAvailableEncoders())) | |
225 | } |