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