aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/job-queue/handlers
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-02-11 10:51:33 +0100
committerChocobozzz <chocobozzz@cpy.re>2022-02-28 10:42:19 +0100
commitc729caf6cc34630877a0e5a1bda1719384cd0c8a (patch)
tree1d2e13722e518c73d2c9e6f0969615e29d51cf8c /server/lib/job-queue/handlers
parenta24bf4dc659cebb65d887862bf21d7a35e9ec791 (diff)
downloadPeerTube-c729caf6cc34630877a0e5a1bda1719384cd0c8a.tar.gz
PeerTube-c729caf6cc34630877a0e5a1bda1719384cd0c8a.tar.zst
PeerTube-c729caf6cc34630877a0e5a1bda1719384cd0c8a.zip
Add basic video editor support
Diffstat (limited to 'server/lib/job-queue/handlers')
-rw-r--r--server/lib/job-queue/handlers/video-edition.ts229
-rw-r--r--server/lib/job-queue/handlers/video-file-import.ts12
-rw-r--r--server/lib/job-queue/handlers/video-import.ts8
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts8
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts10
5 files changed, 248 insertions, 19 deletions
diff --git a/server/lib/job-queue/handlers/video-edition.ts b/server/lib/job-queue/handlers/video-edition.ts
new file mode 100644
index 000000000..c5ba0452f
--- /dev/null
+++ b/server/lib/job-queue/handlers/video-edition.ts
@@ -0,0 +1,229 @@
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 { addMoveToObjectStorageJob, addOptimizeOrMergeAudioJob } from '@server/lib/video'
12import { approximateIntroOutroAdditionalSize } from '@server/lib/video-editor'
13import { VideoPathManager } from '@server/lib/video-path-manager'
14import { buildNextVideoState } from '@server/lib/video-state'
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 {
30 VideoEditionPayload,
31 VideoEditionTaskPayload,
32 VideoEditorTask,
33 VideoEditorTaskCutPayload,
34 VideoEditorTaskIntroPayload,
35 VideoEditorTaskOutroPayload,
36 VideoEditorTaskWatermarkPayload,
37 VideoState
38} from '@shared/models'
39import { logger, loggerTagsFactory } from '../../../helpers/logger'
40
41const lTagsBase = loggerTagsFactory('video-edition')
42
43async function processVideoEdition (job: Job) {
44 const payload = job.data as VideoEditionPayload
45
46 logger.info('Process video edition of %s in job %d.', payload.videoUUID, job.id)
47
48 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoUUID)
49
50 // No video, maybe deleted?
51 if (!video) {
52 logger.info('Can\'t process job %d, video does not exist.', job.id, lTagsBase(payload.videoUUID))
53 return undefined
54 }
55
56 await checkUserQuotaOrThrow(video, payload)
57
58 const inputFile = video.getMaxQualityFile()
59
60 const editionResultPath = await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async originalFilePath => {
61 let tmpInputFilePath: string
62 let outputPath: string
63
64 for (const task of payload.tasks) {
65 const outputFilename = buildUUID() + inputFile.extname
66 outputPath = join(CONFIG.STORAGE.TMP_DIR, outputFilename)
67
68 await processTask({
69 inputPath: tmpInputFilePath ?? originalFilePath,
70 video,
71 outputPath,
72 task
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)
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.state = buildNextVideoState()
98 video.duration = await getVideoStreamDuration(outputPath)
99 await video.save()
100
101 await federateVideoIfNeeded(video, false, undefined)
102
103 if (video.state === VideoState.TO_TRANSCODE) {
104 const user = await UserModel.loadByVideoId(video.id)
105
106 await addOptimizeOrMergeAudioJob(video, newFile, user, false)
107 } else if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
108 await addMoveToObjectStorageJob(video, false)
109 }
110}
111
112// ---------------------------------------------------------------------------
113
114export {
115 processVideoEdition
116}
117
118// ---------------------------------------------------------------------------
119
120type TaskProcessorOptions <T extends VideoEditionTaskPayload = VideoEditionTaskPayload> = {
121 inputPath: string
122 outputPath: string
123 video: MVideo
124 task: T
125}
126
127const taskProcessors: { [id in VideoEditorTask['name']]: (options: TaskProcessorOptions) => Promise<any> } = {
128 'add-intro': processAddIntroOutro,
129 'add-outro': processAddIntroOutro,
130 'cut': processCut,
131 'add-watermark': processAddWatermark
132}
133
134async function processTask (options: TaskProcessorOptions) {
135 const { video, task } = options
136
137 logger.info('Processing %s task for video %s.', task.name, video.uuid, { task })
138
139 const processor = taskProcessors[options.task.name]
140 if (!process) throw new Error('Unknown task ' + task.name)
141
142 return processor(options)
143}
144
145function processAddIntroOutro (options: TaskProcessorOptions<VideoEditorTaskIntroPayload | VideoEditorTaskOutroPayload>) {
146 const { task } = options
147
148 return addIntroOutro({
149 ...pick(options, [ 'inputPath', 'outputPath' ]),
150
151 introOutroPath: task.options.file,
152 type: task.name === 'add-intro'
153 ? 'intro'
154 : 'outro',
155
156 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
157 profile: CONFIG.TRANSCODING.PROFILE
158 })
159}
160
161function processCut (options: TaskProcessorOptions<VideoEditorTaskCutPayload>) {
162 const { task } = options
163
164 return cutVideo({
165 ...pick(options, [ 'inputPath', 'outputPath' ]),
166
167 start: task.options.start,
168 end: task.options.end
169 })
170}
171
172function processAddWatermark (options: TaskProcessorOptions<VideoEditorTaskWatermarkPayload>) {
173 const { task } = options
174
175 return addWatermark({
176 ...pick(options, [ 'inputPath', 'outputPath' ]),
177
178 watermarkPath: task.options.file,
179
180 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
181 profile: CONFIG.TRANSCODING.PROFILE
182 })
183}
184
185async function buildNewFile (video: MVideoId, path: string) {
186 const videoFile = new VideoFileModel({
187 extname: getLowercaseExtension(path),
188 size: await getFileSize(path),
189 metadata: await buildFileMetadata(path),
190 videoStreamingPlaylistId: null,
191 videoId: video.id
192 })
193
194 const probe = await ffprobePromise(path)
195
196 videoFile.fps = await getVideoStreamFPS(path, probe)
197 videoFile.resolution = (await getVideoStreamDimensionsInfo(path, probe)).resolution
198
199 videoFile.filename = generateWebTorrentVideoFilename(videoFile.resolution, videoFile.extname)
200
201 return videoFile
202}
203
204async function removeAllFiles (video: MVideoWithAllFiles, webTorrentFileException: MVideoFile) {
205 const hls = video.getHLSPlaylist()
206
207 if (hls) {
208 await video.removeStreamingPlaylistFiles(hls)
209 await hls.destroy()
210 }
211
212 for (const file of video.VideoFiles) {
213 if (file.id === webTorrentFileException.id) continue
214
215 await video.removeWebTorrentFileAndTorrent(file)
216 await file.destroy()
217 }
218}
219
220async function checkUserQuotaOrThrow (video: MVideoFullLight, payload: VideoEditionPayload) {
221 const user = await UserModel.loadByVideoId(video.id)
222
223 const filePathFinder = (i: number) => (payload.tasks[i] as VideoEditorTaskIntroPayload | VideoEditorTaskOutroPayload).options.file
224
225 const additionalBytes = await approximateIntroOutroAdditionalSize(video, payload.tasks, filePathFinder)
226 if (await isAbleToUploadVideo(user.id, additionalBytes) === false) {
227 throw new Error('Quota exceeded for this user to edit the video')
228 }
229}
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts
index 0d9e80cb8..6b2d60317 100644
--- a/server/lib/job-queue/handlers/video-file-import.ts
+++ b/server/lib/job-queue/handlers/video-file-import.ts
@@ -1,18 +1,18 @@
1import { Job } from 'bull' 1import { Job } from 'bull'
2import { copy, stat } from 'fs-extra' 2import { copy, stat } from 'fs-extra'
3import { getLowercaseExtension } from '@shared/core-utils'
4import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 3import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
5import { CONFIG } from '@server/initializers/config' 4import { CONFIG } from '@server/initializers/config'
6import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' 5import { federateVideoIfNeeded } from '@server/lib/activitypub/videos'
7import { generateWebTorrentVideoFilename } from '@server/lib/paths' 6import { generateWebTorrentVideoFilename } from '@server/lib/paths'
8import { addMoveToObjectStorageJob } from '@server/lib/video' 7import { addMoveToObjectStorageJob } from '@server/lib/video'
9import { VideoPathManager } from '@server/lib/video-path-manager' 8import { VideoPathManager } from '@server/lib/video-path-manager'
9import { VideoModel } from '@server/models/video/video'
10import { VideoFileModel } from '@server/models/video/video-file'
10import { MVideoFullLight } from '@server/types/models' 11import { MVideoFullLight } from '@server/types/models'
12import { getLowercaseExtension } from '@shared/core-utils'
11import { VideoFileImportPayload, VideoStorage } from '@shared/models' 13import { VideoFileImportPayload, VideoStorage } from '@shared/models'
12import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' 14import { getVideoStreamFPS, getVideoStreamDimensionsInfo } from '../../../helpers/ffmpeg'
13import { logger } from '../../../helpers/logger' 15import { logger } from '../../../helpers/logger'
14import { VideoModel } from '../../../models/video/video'
15import { VideoFileModel } from '../../../models/video/video-file'
16 16
17async function processVideoFileImport (job: Job) { 17async function processVideoFileImport (job: Job) {
18 const payload = job.data as VideoFileImportPayload 18 const payload = job.data as VideoFileImportPayload
@@ -45,9 +45,9 @@ export {
45// --------------------------------------------------------------------------- 45// ---------------------------------------------------------------------------
46 46
47async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) { 47async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
48 const { resolution } = await getVideoFileResolution(inputFilePath) 48 const { resolution } = await getVideoStreamDimensionsInfo(inputFilePath)
49 const { size } = await stat(inputFilePath) 49 const { size } = await stat(inputFilePath)
50 const fps = await getVideoFileFPS(inputFilePath) 50 const fps = await getVideoStreamFPS(inputFilePath)
51 51
52 const fileExt = getLowercaseExtension(inputFilePath) 52 const fileExt = getLowercaseExtension(inputFilePath)
53 53
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index b6e05d8f5..b3ca28c2f 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -25,7 +25,7 @@ import {
25 VideoResolution, 25 VideoResolution,
26 VideoState 26 VideoState
27} from '@shared/models' 27} from '@shared/models'
28import { ffprobePromise, getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' 28import { ffprobePromise, getVideoStreamDuration, getVideoStreamFPS, getVideoStreamDimensionsInfo } from '../../../helpers/ffmpeg'
29import { logger } from '../../../helpers/logger' 29import { logger } from '../../../helpers/logger'
30import { getSecureTorrentName } from '../../../helpers/utils' 30import { getSecureTorrentName } from '../../../helpers/utils'
31import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' 31import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent'
@@ -121,10 +121,10 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
121 121
122 const { resolution } = await isAudioFile(tempVideoPath, probe) 122 const { resolution } = await isAudioFile(tempVideoPath, probe)
123 ? { resolution: VideoResolution.H_NOVIDEO } 123 ? { resolution: VideoResolution.H_NOVIDEO }
124 : await getVideoFileResolution(tempVideoPath) 124 : await getVideoStreamDimensionsInfo(tempVideoPath)
125 125
126 const fps = await getVideoFileFPS(tempVideoPath, probe) 126 const fps = await getVideoStreamFPS(tempVideoPath, probe)
127 const duration = await getDurationFromVideoFile(tempVideoPath, probe) 127 const duration = await getVideoStreamDuration(tempVideoPath, probe)
128 128
129 // Prepare video file object for creation in database 129 // Prepare video file object for creation in database
130 const fileExt = getLowercaseExtension(tempVideoPath) 130 const fileExt = getLowercaseExtension(tempVideoPath)
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts
index a04cfa2c9..497f6612a 100644
--- a/server/lib/job-queue/handlers/video-live-ending.ts
+++ b/server/lib/job-queue/handlers/video-live-ending.ts
@@ -1,12 +1,12 @@
1import { Job } from 'bull' 1import { Job } from 'bull'
2import { pathExists, readdir, remove } from 'fs-extra' 2import { pathExists, readdir, remove } from 'fs-extra'
3import { join } from 'path' 3import { join } from 'path'
4import { ffprobePromise, getAudioStream, getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils' 4import { ffprobePromise, getAudioStream, getVideoStreamDuration, getVideoStreamDimensionsInfo } from '@server/helpers/ffmpeg'
5import { VIDEO_LIVE } from '@server/initializers/constants' 5import { VIDEO_LIVE } from '@server/initializers/constants'
6import { buildConcatenatedName, cleanupLive, LiveSegmentShaStore } from '@server/lib/live' 6import { buildConcatenatedName, cleanupLive, LiveSegmentShaStore } from '@server/lib/live'
7import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveDirectory } from '@server/lib/paths' 7import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveDirectory } from '@server/lib/paths'
8import { generateVideoMiniature } from '@server/lib/thumbnail' 8import { generateVideoMiniature } from '@server/lib/thumbnail'
9import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/video-transcoding' 9import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/transcoding'
10import { VideoPathManager } from '@server/lib/video-path-manager' 10import { VideoPathManager } from '@server/lib/video-path-manager'
11import { moveToNextState } from '@server/lib/video-state' 11import { moveToNextState } from '@server/lib/video-state'
12import { VideoModel } from '@server/models/video/video' 12import { VideoModel } from '@server/models/video/video'
@@ -96,7 +96,7 @@ async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MSt
96 const probe = await ffprobePromise(concatenatedTsFilePath) 96 const probe = await ffprobePromise(concatenatedTsFilePath)
97 const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) 97 const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe)
98 98
99 const { resolution, isPortraitMode } = await getVideoFileResolution(concatenatedTsFilePath, probe) 99 const { resolution, isPortraitMode } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe)
100 100
101 const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({ 101 const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({
102 video: videoWithFiles, 102 video: videoWithFiles,
@@ -107,7 +107,7 @@ async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MSt
107 }) 107 })
108 108
109 if (!durationDone) { 109 if (!durationDone) {
110 videoWithFiles.duration = await getDurationFromVideoFile(outputPath) 110 videoWithFiles.duration = await getVideoStreamDuration(outputPath)
111 await videoWithFiles.save() 111 await videoWithFiles.save()
112 112
113 durationDone = true 113 durationDone = true
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index 5540b791d..512979734 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -1,5 +1,5 @@
1import { Job } from 'bull' 1import { Job } from 'bull'
2import { TranscodeOptionsType } from '@server/helpers/ffmpeg-utils' 2import { TranscodeVODOptionsType } from '@server/helpers/ffmpeg'
3import { addTranscodingJob, getTranscodingJobPriority } from '@server/lib/video' 3import { addTranscodingJob, getTranscodingJobPriority } from '@server/lib/video'
4import { VideoPathManager } from '@server/lib/video-path-manager' 4import { VideoPathManager } from '@server/lib/video-path-manager'
5import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video-state' 5import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video-state'
@@ -16,7 +16,7 @@ import {
16 VideoTranscodingPayload 16 VideoTranscodingPayload
17} from '@shared/models' 17} from '@shared/models'
18import { retryTransactionWrapper } from '../../../helpers/database-utils' 18import { retryTransactionWrapper } from '../../../helpers/database-utils'
19import { computeLowerResolutionsToTranscode } from '../../../helpers/ffprobe-utils' 19import { computeLowerResolutionsToTranscode } from '../../../helpers/ffmpeg'
20import { logger, loggerTagsFactory } from '../../../helpers/logger' 20import { logger, loggerTagsFactory } from '../../../helpers/logger'
21import { CONFIG } from '../../../initializers/config' 21import { CONFIG } from '../../../initializers/config'
22import { VideoModel } from '../../../models/video/video' 22import { VideoModel } from '../../../models/video/video'
@@ -25,7 +25,7 @@ import {
25 mergeAudioVideofile, 25 mergeAudioVideofile,
26 optimizeOriginalVideofile, 26 optimizeOriginalVideofile,
27 transcodeNewWebTorrentResolution 27 transcodeNewWebTorrentResolution
28} from '../../transcoding/video-transcoding' 28} from '../../transcoding/transcoding'
29 29
30type HandlerFunction = (job: Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<void> 30type HandlerFunction = (job: Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<void>
31 31
@@ -174,10 +174,10 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay
174async function onVideoFirstWebTorrentTranscoding ( 174async function onVideoFirstWebTorrentTranscoding (
175 videoArg: MVideoWithFile, 175 videoArg: MVideoWithFile,
176 payload: OptimizeTranscodingPayload | MergeAudioTranscodingPayload, 176 payload: OptimizeTranscodingPayload | MergeAudioTranscodingPayload,
177 transcodeType: TranscodeOptionsType, 177 transcodeType: TranscodeVODOptionsType,
178 user: MUserId 178 user: MUserId
179) { 179) {
180 const { resolution, isPortraitMode, audioStream } = await videoArg.getMaxQualityFileInfo() 180 const { resolution, isPortraitMode, audioStream } = await videoArg.probeMaxQualityFile()
181 181
182 // Maybe the video changed in database, refresh it 182 // Maybe the video changed in database, refresh it
183 const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid) 183 const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid)