PeerTube/server/core/lib/job-queue/handlers/video-studio-edition.ts

181 lines
5.9 KiB
TypeScript
Raw Normal View History

2022-08-08 03:42:08 -05:00
import { Job } from 'bullmq'
import { remove } from 'fs-extra/esm'
2022-02-11 03:51:33 -06:00
import { join } from 'path'
import { getFFmpegCommandWrapperOptions } from '@server/helpers/ffmpeg/index.js'
import { CONFIG } from '@server/initializers/config.js'
import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles.js'
import { isAbleToUploadVideo } from '@server/lib/user.js'
import { VideoPathManager } from '@server/lib/video-path-manager.js'
import { approximateIntroOutroAdditionalSize, onVideoStudioEnded, safeCleanupStudioTMPFiles } from '@server/lib/video-studio.js'
import { UserModel } from '@server/models/user/user.js'
import { VideoModel } from '@server/models/video/video.js'
import { MVideo, MVideoFullLight } from '@server/types/models/index.js'
import { pick } from '@peertube/peertube-core-utils'
import { buildUUID } from '@peertube/peertube-node-utils'
import { FFmpegEdition } from '@peertube/peertube-ffmpeg'
2022-02-11 03:51:33 -06:00
import {
2022-03-22 10:58:49 -05:00
VideoStudioEditionPayload,
VideoStudioTask,
2022-03-22 10:58:49 -05:00
VideoStudioTaskCutPayload,
VideoStudioTaskIntroPayload,
VideoStudioTaskOutroPayload,
VideoStudioTaskPayload,
VideoStudioTaskWatermarkPayload
} from '@peertube/peertube-models'
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
2022-02-11 03:51:33 -06:00
const lTagsBase = loggerTagsFactory('video-studio')
2022-02-11 03:51:33 -06:00
2022-03-22 10:58:49 -05:00
async function processVideoStudioEdition (job: Job) {
const payload = job.data as VideoStudioEditionPayload
const lTags = lTagsBase(payload.videoUUID)
2022-02-11 03:51:33 -06:00
2022-08-08 08:48:17 -05:00
logger.info('Process video studio edition of %s in job %s.', payload.videoUUID, job.id, lTags)
2022-02-11 03:51:33 -06:00
try {
const video = await VideoModel.loadFull(payload.videoUUID)
2022-02-11 03:51:33 -06:00
// No video, maybe deleted?
if (!video) {
logger.info('Can\'t process job %d, video does not exist.', job.id, lTags)
await safeCleanupStudioTMPFiles(payload.tasks)
return undefined
}
2022-02-11 03:51:33 -06:00
await checkUserQuotaOrThrow(video, payload)
2022-02-11 03:51:33 -06:00
const inputFile = video.getMaxQualityFile()
2022-02-11 03:51:33 -06:00
const editionResultPath = await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async originalFilePath => {
let tmpInputFilePath: string
let outputPath: string
2022-02-11 03:51:33 -06:00
for (const task of payload.tasks) {
const outputFilename = buildUUID() + inputFile.extname
outputPath = join(CONFIG.STORAGE.TMP_DIR, outputFilename)
2022-02-11 03:51:33 -06:00
await processTask({
inputPath: tmpInputFilePath ?? originalFilePath,
video,
outputPath,
task,
lTags
})
2022-02-11 03:51:33 -06:00
if (tmpInputFilePath) await remove(tmpInputFilePath)
2022-02-11 03:51:33 -06:00
// For the next iteration
tmpInputFilePath = outputPath
}
2022-02-11 03:51:33 -06:00
return outputPath
})
2022-02-11 03:51:33 -06:00
logger.info('Video edition ended for video %s.', video.uuid, lTags)
2022-02-11 03:51:33 -06:00
await onVideoStudioEnded({ video, editionResultPath, tasks: payload.tasks })
} catch (err) {
await safeCleanupStudioTMPFiles(payload.tasks)
throw err
}
2022-02-11 03:51:33 -06:00
}
// ---------------------------------------------------------------------------
export {
2022-03-22 10:58:49 -05:00
processVideoStudioEdition
2022-02-11 03:51:33 -06:00
}
// ---------------------------------------------------------------------------
2022-03-22 10:58:49 -05:00
type TaskProcessorOptions <T extends VideoStudioTaskPayload = VideoStudioTaskPayload> = {
2022-02-11 03:51:33 -06:00
inputPath: string
outputPath: string
video: MVideo
task: T
lTags: { tags: string[] }
2022-02-11 03:51:33 -06:00
}
2022-03-22 10:58:49 -05:00
const taskProcessors: { [id in VideoStudioTask['name']]: (options: TaskProcessorOptions) => Promise<any> } = {
2022-02-11 03:51:33 -06:00
'add-intro': processAddIntroOutro,
'add-outro': processAddIntroOutro,
'cut': processCut,
'add-watermark': processAddWatermark
}
async function processTask (options: TaskProcessorOptions) {
const { video, task, lTags } = options
2022-02-11 03:51:33 -06:00
logger.info('Processing %s task for video %s.', task.name, video.uuid, { task, ...lTags })
2022-02-11 03:51:33 -06:00
const processor = taskProcessors[options.task.name]
if (!process) throw new Error('Unknown task ' + task.name)
return processor(options)
}
2022-03-22 10:58:49 -05:00
function processAddIntroOutro (options: TaskProcessorOptions<VideoStudioTaskIntroPayload | VideoStudioTaskOutroPayload>) {
const { task, lTags } = options
logger.debug('Will add intro/outro to the video.', { options, ...lTags })
2022-02-11 03:51:33 -06:00
return buildFFmpegEdition().addIntroOutro({
2022-02-11 03:51:33 -06:00
...pick(options, [ 'inputPath', 'outputPath' ]),
introOutroPath: task.options.file,
type: task.name === 'add-intro'
? 'intro'
: 'outro'
2022-02-11 03:51:33 -06:00
})
}
2022-03-22 10:58:49 -05:00
function processCut (options: TaskProcessorOptions<VideoStudioTaskCutPayload>) {
const { task, lTags } = options
2022-02-11 03:51:33 -06:00
logger.debug('Will cut the video.', { options, ...lTags })
return buildFFmpegEdition().cutVideo({
2022-02-11 03:51:33 -06:00
...pick(options, [ 'inputPath', 'outputPath' ]),
start: task.options.start,
end: task.options.end
2022-02-11 03:51:33 -06:00
})
}
2022-03-22 10:58:49 -05:00
function processAddWatermark (options: TaskProcessorOptions<VideoStudioTaskWatermarkPayload>) {
const { task, lTags } = options
logger.debug('Will add watermark to the video.', { options, ...lTags })
2022-02-11 03:51:33 -06:00
return buildFFmpegEdition().addWatermark({
2022-02-11 03:51:33 -06:00
...pick(options, [ 'inputPath', 'outputPath' ]),
watermarkPath: task.options.file,
videoFilters: {
watermarkSizeRatio: task.options.watermarkSizeRatio,
horitonzalMarginRatio: task.options.horitonzalMarginRatio,
verticalMarginRatio: task.options.verticalMarginRatio
}
2022-02-11 03:51:33 -06:00
})
}
// ---------------------------------------------------------------------------
2022-03-22 10:58:49 -05:00
async function checkUserQuotaOrThrow (video: MVideoFullLight, payload: VideoStudioEditionPayload) {
2022-02-11 03:51:33 -06:00
const user = await UserModel.loadByVideoId(video.id)
2022-03-22 10:58:49 -05:00
const filePathFinder = (i: number) => (payload.tasks[i] as VideoStudioTaskIntroPayload | VideoStudioTaskOutroPayload).options.file
2022-02-11 03:51:33 -06:00
const additionalBytes = await approximateIntroOutroAdditionalSize(video, payload.tasks, filePathFinder)
if (await isAbleToUploadVideo(user.id, additionalBytes) === false) {
throw new Error('Quota exceeded for this user to edit the video')
}
}
function buildFFmpegEdition () {
return new FFmpegEdition(getFFmpegCommandWrapperOptions('vod', VideoTranscodingProfilesManager.Instance.getAvailableEncoders()))
}