]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/lib/job-queue/handlers/video-studio-edition.ts
Bumped to version v5.2.1
[github/Chocobozzz/PeerTube.git] / server / lib / job-queue / handlers / video-studio-edition.ts
index 735150d57fecd0b26b13b95233882e2886f52a3f..caf051bfa7d6e532f6313c5ea374342dd5b6e2e0 100644 (file)
@@ -1,31 +1,18 @@
-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 { removeHLSPlaylist, removeWebTorrentFile } from '@server/lib/video-file'
 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,
   VideoStudioTask,
@@ -37,70 +24,62 @@ import {
 } 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)
+  logger.info('Process video studio edition of %s in job %s.', payload.videoUUID, job.id, lTags)
 
-  const inputFile = video.getMaxQualityFile()
+  try {
+    const video = await VideoModel.loadFull(payload.videoUUID)
 
-  const editionResultPath = await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async originalFilePath => {
-    let tmpInputFilePath: string
-    let outputPath: string
+    // No video, maybe deleted?
+    if (!video) {
+      logger.info('Can\'t process job %d, video does not exist.', job.id, lTags)
 
-    for (const task of payload.tasks) {
-      const outputFilename = buildUUID() + inputFile.extname
-      outputPath = join(CONFIG.STORAGE.TMP_DIR, outputFilename)
+      await safeCleanupStudioTMPFiles(payload.tasks)
+      return undefined
+    }
 
-      await processTask({
-        inputPath: tmpInputFilePath ?? originalFilePath,
-        video,
-        outputPath,
-        task,
-        lTags
-      })
+    await checkUserQuotaOrThrow(video, payload)
 
-      if (tmpInputFilePath) await remove(tmpInputFilePath)
+    const inputFile = video.getMaxQualityFile()
 
-      // For the next iteration
-      tmpInputFilePath = outputPath
-    }
+    const editionResultPath = await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async originalFilePath => {
+      let tmpInputFilePath: string
+      let outputPath: string
 
-    return outputPath
-  })
+      for (const task of payload.tasks) {
+        const outputFilename = buildUUID() + inputFile.extname
+        outputPath = join(CONFIG.STORAGE.TMP_DIR, outputFilename)
 
-  logger.info('Video edition ended for video %s.', video.uuid, lTags)
+        await processTask({
+          inputPath: tmpInputFilePath ?? originalFilePath,
+          video,
+          outputPath,
+          task,
+          lTags
+        })
 
-  const newFile = await buildNewFile(video, editionResultPath)
+        if (tmpInputFilePath) await remove(tmpInputFilePath)
 
-  const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newFile)
-  await move(editionResultPath, outputPath)
+        // For the next iteration
+        tmpInputFilePath = outputPath
+      }
 
-  await createTorrentAndSetInfoHashFromPath(video, newFile, outputPath)
-  await removeAllFiles(video, newFile)
+      return outputPath
+    })
 
-  await newFile.save()
+    logger.info('Video edition ended for video %s.', video.uuid, lTags)
 
-  video.duration = await getVideoStreamDuration(outputPath)
-  await video.save()
+    await onVideoStudioEnded({ video, editionResultPath, tasks: payload.tasks })
+  } catch (err) {
+    await safeCleanupStudioTMPFiles(payload.tasks)
 
-  await federateVideoIfNeeded(video, false, undefined)
-
-  const user = await UserModel.loadByVideoId(video.id)
-  await addOptimizeOrMergeAudioJob({ video, videoFile: newFile, user, isNewVideo: false })
+    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<VideoStudioTaskIntroPayload | VideoStudioTaskOutroPayload>) {
-  const { task } = options
+  const { task, lTags } = options
+
+  logger.debug('Will add intro/outro to the video.', { options, ...lTags })
 
-  return addIntroOutro({
+  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<VideoStudioTaskCutPayload>) {
-  const { task } = options
+  const { task, lTags } = options
+
+  logger.debug('Will cut the video.', { options, ...lTags })
 
-  return cutVideo({
+  return buildFFmpegEdition().cutVideo({
     ...pick(options, [ 'inputPath', 'outputPath' ]),
 
     start: task.options.start,
@@ -165,46 +145,24 @@ function processCut (options: TaskProcessorOptions<VideoStudioTaskCutPayload>) {
 }
 
 function processAddWatermark (options: TaskProcessorOptions<VideoStudioTaskWatermarkPayload>) {
-  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
-  })
-}
-
-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
+    videoFilters: {
+      watermarkSizeRatio: task.options.watermarkSizeRatio,
+      horitonzalMarginRatio: task.options.horitonzalMarginRatio,
+      verticalMarginRatio: task.options.verticalMarginRatio
+    }
   })
-
-  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) {
-  await removeHLSPlaylist(video)
-
-  for (const file of video.VideoFiles) {
-    if (file.id === webTorrentFileException.id) continue
-
-    await removeWebTorrentFile(video, file.id)
-  }
-}
+// ---------------------------------------------------------------------------
 
 async function checkUserQuotaOrThrow (video: MVideoFullLight, payload: VideoStudioEditionPayload) {
   const user = await UserModel.loadByVideoId(video.id)
@@ -216,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()))
+}