X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Flib%2Fjob-queue%2Fhandlers%2Fvideo-studio-edition.ts;h=caf051bfa7d6e532f6313c5ea374342dd5b6e2e0;hb=0c302acb3c358b4d4d8dee45aed1de1108ea37ea;hp=434d0ffe821d1e9430c7020b102ac6488dca12e4;hpb=4fae2b1f300c1f027629569817262f60873a663a;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/lib/job-queue/handlers/video-studio-edition.ts b/server/lib/job-queue/handlers/video-studio-edition.ts index 434d0ffe8..caf051bfa 100644 --- a/server/lib/job-queue/handlers/video-studio-edition.ts +++ b/server/lib/job-queue/handlers/video-studio-edition.ts @@ -1,106 +1,85 @@ -import { Job } from 'bull' -import { move, remove } from 'fs-extra' +import { Job } from 'bullmq' +import { remove } from 'fs-extra' import { join } from 'path' -import { addIntroOutro, addWatermark, cutVideo } from '@server/helpers/ffmpeg' -import { createTorrentAndSetInfoHashFromPath } from '@server/helpers/webtorrent' +import { getFFmpegCommandWrapperOptions } from '@server/helpers/ffmpeg' import { CONFIG } from '@server/initializers/config' -import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' -import { generateWebTorrentVideoFilename } from '@server/lib/paths' import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles' import { isAbleToUploadVideo } from '@server/lib/user' -import { addOptimizeOrMergeAudioJob } from '@server/lib/video' import { VideoPathManager } from '@server/lib/video-path-manager' -import { approximateIntroOutroAdditionalSize } from '@server/lib/video-studio' +import { approximateIntroOutroAdditionalSize, onVideoStudioEnded, safeCleanupStudioTMPFiles } from '@server/lib/video-studio' import { UserModel } from '@server/models/user/user' import { VideoModel } from '@server/models/video/video' -import { VideoFileModel } from '@server/models/video/video-file' -import { MVideo, MVideoFile, MVideoFullLight, MVideoId, MVideoWithAllFiles } from '@server/types/models' -import { getLowercaseExtension, pick } from '@shared/core-utils' -import { - buildFileMetadata, - buildUUID, - ffprobePromise, - getFileSize, - getVideoStreamDimensionsInfo, - getVideoStreamDuration, - getVideoStreamFPS -} from '@shared/extra-utils' +import { MVideo, MVideoFullLight } from '@server/types/models' +import { pick } from '@shared/core-utils' +import { buildUUID } from '@shared/extra-utils' +import { FFmpegEdition } from '@shared/ffmpeg' import { VideoStudioEditionPayload, - VideoStudioTaskPayload, + VideoStudioTask, VideoStudioTaskCutPayload, VideoStudioTaskIntroPayload, VideoStudioTaskOutroPayload, - VideoStudioTaskWatermarkPayload, - VideoStudioTask + VideoStudioTaskPayload, + VideoStudioTaskWatermarkPayload } from '@shared/models' import { logger, loggerTagsFactory } from '../../../helpers/logger' -const lTagsBase = loggerTagsFactory('video-edition') +const lTagsBase = loggerTagsFactory('video-studio') async function processVideoStudioEdition (job: Job) { const payload = job.data as VideoStudioEditionPayload const lTags = lTagsBase(payload.videoUUID) - logger.info('Process video studio edition of %s in job %d.', payload.videoUUID, job.id, lTags) - - const video = await VideoModel.loadFull(payload.videoUUID) - - // No video, maybe deleted? - if (!video) { - logger.info('Can\'t process job %d, video does not exist.', job.id, lTags) - return undefined - } - - await checkUserQuotaOrThrow(video, payload) - - const inputFile = video.getMaxQualityFile() - - const editionResultPath = await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async originalFilePath => { - let tmpInputFilePath: string - let outputPath: string + logger.info('Process video studio edition of %s in job %s.', payload.videoUUID, job.id, lTags) - for (const task of payload.tasks) { - const outputFilename = buildUUID() + inputFile.extname - outputPath = join(CONFIG.STORAGE.TMP_DIR, outputFilename) + try { + const video = await VideoModel.loadFull(payload.videoUUID) - await processTask({ - inputPath: tmpInputFilePath ?? originalFilePath, - video, - outputPath, - task, - lTags - }) + // No video, maybe deleted? + if (!video) { + logger.info('Can\'t process job %d, video does not exist.', job.id, lTags) - if (tmpInputFilePath) await remove(tmpInputFilePath) - - // For the next iteration - tmpInputFilePath = outputPath + await safeCleanupStudioTMPFiles(payload.tasks) + return undefined } - return outputPath - }) + await checkUserQuotaOrThrow(video, payload) - logger.info('Video edition ended for video %s.', video.uuid, lTags) + const inputFile = video.getMaxQualityFile() - const newFile = await buildNewFile(video, editionResultPath) + const editionResultPath = await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async originalFilePath => { + let tmpInputFilePath: string + let outputPath: string - const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newFile) - await move(editionResultPath, outputPath) + for (const task of payload.tasks) { + const outputFilename = buildUUID() + inputFile.extname + outputPath = join(CONFIG.STORAGE.TMP_DIR, outputFilename) - await createTorrentAndSetInfoHashFromPath(video, newFile, outputPath) + await processTask({ + inputPath: tmpInputFilePath ?? originalFilePath, + video, + outputPath, + task, + lTags + }) - await removeAllFiles(video, newFile) + if (tmpInputFilePath) await remove(tmpInputFilePath) - await newFile.save() + // For the next iteration + tmpInputFilePath = outputPath + } - video.duration = await getVideoStreamDuration(outputPath) - await video.save() + return outputPath + }) - await federateVideoIfNeeded(video, false, undefined) + logger.info('Video edition ended for video %s.', video.uuid, lTags) - const user = await UserModel.loadByVideoId(video.id) - await addOptimizeOrMergeAudioJob({ video, videoFile: newFile, user, isNewVideo: false }) + await onVideoStudioEnded({ video, editionResultPath, tasks: payload.tasks }) + } catch (err) { + await safeCleanupStudioTMPFiles(payload.tasks) + + throw err + } } // --------------------------------------------------------------------------- @@ -127,9 +106,9 @@ const taskProcessors: { [id in VideoStudioTask['name']]: (options: TaskProcessor } async function processTask (options: TaskProcessorOptions) { - const { video, task } = options + const { video, task, lTags } = options - logger.info('Processing %s task for video %s.', task.name, video.uuid, { task, ...options.lTags }) + logger.info('Processing %s task for video %s.', task.name, video.uuid, { task, ...lTags }) const processor = taskProcessors[options.task.name] if (!process) throw new Error('Unknown task ' + task.name) @@ -138,25 +117,26 @@ async function processTask (options: TaskProcessorOptions) { } function processAddIntroOutro (options: TaskProcessorOptions) { - const { task } = options + const { task, lTags } = options - return addIntroOutro({ + logger.debug('Will add intro/outro to the video.', { options, ...lTags }) + + return buildFFmpegEdition().addIntroOutro({ ...pick(options, [ 'inputPath', 'outputPath' ]), introOutroPath: task.options.file, type: task.name === 'add-intro' ? 'intro' - : 'outro', - - availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), - profile: CONFIG.TRANSCODING.PROFILE + : 'outro' }) } function processCut (options: TaskProcessorOptions) { - const { task } = options + const { task, lTags } = options - return cutVideo({ + logger.debug('Will cut the video.', { options, ...lTags }) + + return buildFFmpegEdition().cutVideo({ ...pick(options, [ 'inputPath', 'outputPath' ]), start: task.options.start, @@ -165,52 +145,24 @@ function processCut (options: TaskProcessorOptions) { } function processAddWatermark (options: TaskProcessorOptions) { - const { task } = options + const { task, lTags } = options + + logger.debug('Will add watermark to the video.', { options, ...lTags }) - return addWatermark({ + return buildFFmpegEdition().addWatermark({ ...pick(options, [ 'inputPath', 'outputPath' ]), watermarkPath: task.options.file, - availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), - profile: CONFIG.TRANSCODING.PROFILE + videoFilters: { + watermarkSizeRatio: task.options.watermarkSizeRatio, + horitonzalMarginRatio: task.options.horitonzalMarginRatio, + verticalMarginRatio: task.options.verticalMarginRatio + } }) } -async function buildNewFile (video: MVideoId, path: string) { - const videoFile = new VideoFileModel({ - extname: getLowercaseExtension(path), - size: await getFileSize(path), - metadata: await buildFileMetadata(path), - videoStreamingPlaylistId: null, - videoId: video.id - }) - - const probe = await ffprobePromise(path) - - videoFile.fps = await getVideoStreamFPS(path, probe) - videoFile.resolution = (await getVideoStreamDimensionsInfo(path, probe)).resolution - - videoFile.filename = generateWebTorrentVideoFilename(videoFile.resolution, videoFile.extname) - - return videoFile -} - -async function removeAllFiles (video: MVideoWithAllFiles, webTorrentFileException: MVideoFile) { - const hls = video.getHLSPlaylist() - - if (hls) { - await video.removeStreamingPlaylistFiles(hls) - await hls.destroy() - } - - for (const file of video.VideoFiles) { - if (file.id === webTorrentFileException.id) continue - - await video.removeWebTorrentFileAndTorrent(file) - await file.destroy() - } -} +// --------------------------------------------------------------------------- async function checkUserQuotaOrThrow (video: MVideoFullLight, payload: VideoStudioEditionPayload) { const user = await UserModel.loadByVideoId(video.id) @@ -222,3 +174,7 @@ async function checkUserQuotaOrThrow (video: MVideoFullLight, payload: VideoStud throw new Error('Quota exceeded for this user to edit the video') } } + +function buildFFmpegEdition () { + return new FFmpegEdition(getFFmpegCommandWrapperOptions('vod', VideoTranscodingProfilesManager.Instance.getAvailableEncoders())) +}