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