]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/job-queue/handlers/video-studio-edition.ts
Fix unregister default value
[github/Chocobozzz/PeerTube.git] / server / lib / job-queue / handlers / video-studio-edition.ts
CommitLineData
5a921e7b 1import { Job } from 'bullmq'
c729caf6
C
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'
bd911b54 11import { buildOptimizeOrMergeAudioJob } from '@server/lib/video'
1bb4c9ab 12import { removeHLSPlaylist, removeWebTorrentFile } from '@server/lib/video-file'
c729caf6 13import { VideoPathManager } from '@server/lib/video-path-manager'
92e66e04 14import { approximateIntroOutroAdditionalSize } from '@server/lib/video-studio'
c729caf6
C
15import { UserModel } from '@server/models/user/user'
16import { VideoModel } from '@server/models/video/video'
17import { VideoFileModel } from '@server/models/video/video-file'
18import { MVideo, MVideoFile, MVideoFullLight, MVideoId, MVideoWithAllFiles } from '@server/types/models'
19import { getLowercaseExtension, pick } from '@shared/core-utils'
20import {
21 buildFileMetadata,
22 buildUUID,
23 ffprobePromise,
24 getFileSize,
25 getVideoStreamDimensionsInfo,
26 getVideoStreamDuration,
27 getVideoStreamFPS
28} from '@shared/extra-utils'
29import {
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'
38import { logger, loggerTagsFactory } from '../../../helpers/logger'
bd911b54 39import { JobQueue } from '../job-queue'
c729caf6
C
40
41const lTagsBase = loggerTagsFactory('video-edition')
42
92e66e04
C
43async 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
112export {
92e66e04 113 processVideoStudioEdition
c729caf6
C
114}
115
116// ---------------------------------------------------------------------------
117
92e66e04 118type 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 126const 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
133async 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 144function 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 160function 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 174function 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
187async 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
206async 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 216async 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}