]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/lib/transcoding/video-transcoding.ts
Better 413 error handling in cli script
[github/Chocobozzz/PeerTube.git] / server / lib / transcoding / video-transcoding.ts
index 5df192575f1ea6548f8291ca703b133a400d199f..9942a067b08e40d75aea435524e0214e4dd9fe28 100644 (file)
@@ -1,19 +1,26 @@
 import { Job } from 'bull'
 import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
 import { basename, extname as extnameUtil, join } from 'path'
+import { toEven } from '@server/helpers/core-utils'
 import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
 import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
-import { VideoResolution } from '../../../shared/models/videos'
+import { VideoResolution, VideoStorage } from '../../../shared/models/videos'
 import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
 import { transcode, TranscodeOptions, TranscodeOptionsType } from '../../helpers/ffmpeg-utils'
 import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils'
-import { logger } from '../../helpers/logger'
 import { CONFIG } from '../../initializers/config'
-import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../../initializers/constants'
+import { P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants'
 import { VideoFileModel } from '../../models/video/video-file'
 import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
 import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls'
-import { generateVideoFilename, generateVideoStreamingPlaylistName, getVideoFilePath } from '../video-paths'
+import {
+  generateHLSMasterPlaylistFilename,
+  generateHlsSha256SegmentsFilename,
+  generateHLSVideoFilename,
+  generateWebTorrentVideoFilename,
+  getHlsResolutionPlaylistFilename
+} from '../paths'
+import { VideoPathManager } from '../video-path-manager'
 import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
 
 /**
@@ -24,157 +31,155 @@ import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
  */
 
 // Optimize the original video file and replace it. The resolution is not changed.
-async function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) {
+function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) {
   const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
   const newExtname = '.mp4'
 
-  const videoInputPath = getVideoFilePath(video, inputVideoFile)
-  const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
+  return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async videoInputPath => {
+    const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
 
-  const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
-    ? 'quick-transcode'
-    : 'video'
+    const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
+      ? 'quick-transcode'
+      : 'video'
 
-  const transcodeOptions: TranscodeOptions = {
-    type: transcodeType,
+    const resolution = toEven(inputVideoFile.resolution)
 
-    inputPath: videoInputPath,
-    outputPath: videoTranscodedPath,
+    const transcodeOptions: TranscodeOptions = {
+      type: transcodeType,
 
-    availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
-    profile: CONFIG.TRANSCODING.PROFILE,
+      inputPath: videoInputPath,
+      outputPath: videoTranscodedPath,
 
-    resolution: inputVideoFile.resolution,
+      availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
+      profile: CONFIG.TRANSCODING.PROFILE,
 
-    job
-  }
+      resolution,
 
-  // Could be very long!
-  await transcode(transcodeOptions)
+      job
+    }
 
-  try {
-    await remove(videoInputPath)
+    // Could be very long!
+    await transcode(transcodeOptions)
 
     // Important to do this before getVideoFilename() to take in account the new filename
     inputVideoFile.extname = newExtname
-    inputVideoFile.filename = generateVideoFilename(video, false, inputVideoFile.resolution, newExtname)
-
-    const videoOutputPath = getVideoFilePath(video, inputVideoFile)
+    inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
+    inputVideoFile.storage = VideoStorage.FILE_SYSTEM
 
-    await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
+    const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
 
-    return transcodeType
-  } catch (err) {
-    // Auto destruction...
-    video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
+    const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
+    await remove(videoInputPath)
 
-    throw err
-  }
+    return { transcodeType, videoFile }
+  })
 }
 
-// Transcode the original video file to a lower resolution.
-async function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) {
+// Transcode the original video file to a lower resolution
+// We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
+function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) {
   const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
   const extname = '.mp4'
 
-  // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
-  const videoInputPath = getVideoFilePath(video, video.getMaxQualityFile())
+  return VideoPathManager.Instance.makeAvailableVideoFile(video.getMaxQualityFile().withVideoOrPlaylist(video), async videoInputPath => {
+    const newVideoFile = new VideoFileModel({
+      resolution,
+      extname,
+      filename: generateWebTorrentVideoFilename(resolution, extname),
+      size: 0,
+      videoId: video.id
+    })
 
-  const newVideoFile = new VideoFileModel({
-    resolution,
-    extname,
-    filename: generateVideoFilename(video, false, resolution, extname),
-    size: 0,
-    videoId: video.id
-  })
+    const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile)
+    const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename)
 
-  const videoOutputPath = getVideoFilePath(video, newVideoFile)
-  const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename)
+    const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
+      ? {
+        type: 'only-audio' as 'only-audio',
 
-  const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
-    ? {
-      type: 'only-audio' as 'only-audio',
+        inputPath: videoInputPath,
+        outputPath: videoTranscodedPath,
 
-      inputPath: videoInputPath,
-      outputPath: videoTranscodedPath,
+        availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
+        profile: CONFIG.TRANSCODING.PROFILE,
 
-      availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
-      profile: CONFIG.TRANSCODING.PROFILE,
+        resolution,
 
-      resolution,
+        job
+      }
+      : {
+        type: 'video' as 'video',
+        inputPath: videoInputPath,
+        outputPath: videoTranscodedPath,
 
-      job
-    }
-    : {
-      type: 'video' as 'video',
-      inputPath: videoInputPath,
-      outputPath: videoTranscodedPath,
+        availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
+        profile: CONFIG.TRANSCODING.PROFILE,
 
-      availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
-      profile: CONFIG.TRANSCODING.PROFILE,
-
-      resolution,
-      isPortraitMode: isPortrait,
+        resolution,
+        isPortraitMode: isPortrait,
 
-      job
-    }
+        job
+      }
 
-  await transcode(transcodeOptions)
+    await transcode(transcodeOptions)
 
-  return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
+    return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
+  })
 }
 
 // Merge an image with an audio file to create a video
-async function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) {
+function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) {
   const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
   const newExtname = '.mp4'
 
   const inputVideoFile = video.getMinQualityFile()
 
-  const audioInputPath = getVideoFilePath(video, inputVideoFile)
-  const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
+  return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async audioInputPath => {
+    const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
 
-  // If the user updates the video preview during transcoding
-  const previewPath = video.getPreview().getPath()
-  const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
-  await copyFile(previewPath, tmpPreviewPath)
+    // If the user updates the video preview during transcoding
+    const previewPath = video.getPreview().getPath()
+    const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
+    await copyFile(previewPath, tmpPreviewPath)
 
-  const transcodeOptions = {
-    type: 'merge-audio' as 'merge-audio',
+    const transcodeOptions = {
+      type: 'merge-audio' as 'merge-audio',
 
-    inputPath: tmpPreviewPath,
-    outputPath: videoTranscodedPath,
+      inputPath: tmpPreviewPath,
+      outputPath: videoTranscodedPath,
 
-    availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
-    profile: CONFIG.TRANSCODING.PROFILE,
+      availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
+      profile: CONFIG.TRANSCODING.PROFILE,
 
-    audioPath: audioInputPath,
-    resolution,
+      audioPath: audioInputPath,
+      resolution,
 
-    job
-  }
+      job
+    }
 
-  try {
-    await transcode(transcodeOptions)
+    try {
+      await transcode(transcodeOptions)
 
-    await remove(audioInputPath)
-    await remove(tmpPreviewPath)
-  } catch (err) {
-    await remove(tmpPreviewPath)
-    throw err
-  }
+      await remove(audioInputPath)
+      await remove(tmpPreviewPath)
+    } catch (err) {
+      await remove(tmpPreviewPath)
+      throw err
+    }
 
-  // Important to do this before getVideoFilename() to take in account the new file extension
-  inputVideoFile.extname = newExtname
-  inputVideoFile.filename = generateVideoFilename(video, false, inputVideoFile.resolution, newExtname)
+    // Important to do this before getVideoFilename() to take in account the new file extension
+    inputVideoFile.extname = newExtname
+    inputVideoFile.resolution = resolution
+    inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
 
-  const videoOutputPath = getVideoFilePath(video, inputVideoFile)
-  // ffmpeg generated a new video file, so update the video duration
-  // See https://trac.ffmpeg.org/ticket/5456
-  video.duration = await getDurationFromVideoFile(videoTranscodedPath)
-  await video.save()
+    const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
+    // ffmpeg generated a new video file, so update the video duration
+    // See https://trac.ffmpeg.org/ticket/5456
+    video.duration = await getDurationFromVideoFile(videoTranscodedPath)
+    await video.save()
 
-  return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
+    return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
+  })
 }
 
 // Concat TS segments from a live video to a fragmented mp4 HLS playlist
@@ -248,7 +253,7 @@ async function onWebTorrentVideoFileTranscoding (
   await VideoFileModel.customUpsert(videoFile, 'video', undefined)
   video.VideoFiles = await video.$get('VideoFiles')
 
-  return video
+  return { video, videoFile }
 }
 
 async function generateHlsPlaylistCommon (options: {
@@ -268,15 +273,15 @@ async function generateHlsPlaylistCommon (options: {
   const videoTranscodedBasePath = join(transcodeDirectory, type)
   await ensureDir(videoTranscodedBasePath)
 
-  const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution)
-  const playlistFilename = VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)
-  const playlistFileTranscodePath = join(videoTranscodedBasePath, playlistFilename)
+  const videoFilename = generateHLSVideoFilename(resolution)
+  const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
+  const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
 
   const transcodeOptions = {
     type,
 
     inputPath,
-    outputPath: playlistFileTranscodePath,
+    outputPath: resolutionPlaylistFileTranscodePath,
 
     availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
     profile: CONFIG.TRANSCODING.PROFILE,
@@ -296,19 +301,23 @@ async function generateHlsPlaylistCommon (options: {
 
   await transcode(transcodeOptions)
 
-  const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
-
   // Create or update the playlist
-  const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({
-    videoId: video.id,
-    playlistUrl,
-    segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive),
-    p2pMediaLoaderInfohashes: [],
-    p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
+  const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video)
+
+  if (!playlist.playlistFilename) {
+    playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive)
+  }
+
+  if (!playlist.segmentsSha256Filename) {
+    playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive)
+  }
+
+  playlist.p2pMediaLoaderInfohashes = []
+  playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
 
-    type: VideoStreamingPlaylistType.HLS
-  }, { returning: true }) as [ MStreamingPlaylistFilesVideo, boolean ]
-  videoStreamingPlaylist.Video = video
+  playlist.type = VideoStreamingPlaylistType.HLS
+
+  await playlist.save()
 
   // Build the new playlist file
   const extname = extnameUtil(videoFilename)
@@ -316,20 +325,19 @@ async function generateHlsPlaylistCommon (options: {
     resolution,
     extname,
     size: 0,
-    filename: generateVideoFilename(video, true, resolution, extname),
+    filename: videoFilename,
     fps: -1,
-    videoStreamingPlaylistId: videoStreamingPlaylist.id
+    videoStreamingPlaylistId: playlist.id
   })
 
-  const videoFilePath = getVideoFilePath(videoStreamingPlaylist, newVideoFile)
+  const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile)
 
   // Move files from tmp transcoded directory to the appropriate place
-  const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
-  await ensureDir(baseHlsDirectory)
+  await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
 
   // Move playlist file
-  const playlistPath = join(baseHlsDirectory, playlistFilename)
-  await move(playlistFileTranscodePath, playlistPath, { overwrite: true })
+  const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename)
+  await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true })
   // Move video file
   await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
 
@@ -339,20 +347,20 @@ async function generateHlsPlaylistCommon (options: {
   newVideoFile.fps = await getVideoFileFPS(videoFilePath)
   newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
 
-  await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile)
+  await createTorrentAndSetInfoHash(playlist, newVideoFile)
+
+  const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
 
-  await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
-  videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles')
+  const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo
+  playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles')
+  playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles)
 
-  videoStreamingPlaylist.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(
-    playlistUrl, videoStreamingPlaylist.VideoFiles
-  )
-  await videoStreamingPlaylist.save()
+  await playlist.save()
 
-  video.setHLSPlaylist(videoStreamingPlaylist)
+  video.setHLSPlaylist(playlist)
 
-  await updateMasterHLSPlaylist(video)
-  await updateSha256VODSegments(video)
+  await updateMasterHLSPlaylist(video, playlistWithFiles)
+  await updateSha256VODSegments(video, playlistWithFiles)
 
-  return playlistPath
+  return { resolutionPlaylistPath, videoFile: savedVideoFile }
 }