]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/helpers/ffmpeg-utils.ts
Move config in its own file
[github/Chocobozzz/PeerTube.git] / server / helpers / ffmpeg-utils.ts
index b59e7e40e692e99ef19bb5e49b6d4745ae412a35..d818c459c66d5752d0da68fbb1d7709cdcca0da2 100644 (file)
@@ -1,11 +1,12 @@
 import * as ffmpeg from 'fluent-ffmpeg'
-import { join } from 'path'
+import { dirname, join } from 'path'
 import { getTargetBitrate, VideoResolution } from '../../shared/models/videos'
-import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
+import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
 import { processImage } from './image-utils'
 import { logger } from './logger'
 import { checkFFmpegEncoders } from '../initializers/checker-before-init'
-import { remove } from 'fs-extra'
+import { readFile, remove, writeFile } from 'fs-extra'
+import { CONFIG } from '../initializers/config'
 
 function computeResolutionsToTranscode (videoFileHeight: number) {
   const resolutionsEnabled: number[] = []
@@ -29,19 +30,28 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
   return resolutionsEnabled
 }
 
-async function getVideoFileResolution (path: string) {
+async function getVideoFileSize (path: string) {
   const videoStream = await getVideoFileStream(path)
 
   return {
-    videoFileResolution: Math.min(videoStream.height, videoStream.width),
-    isPortraitMode: videoStream.height > videoStream.width
+    width: videoStream.width,
+    height: videoStream.height
+  }
+}
+
+async function getVideoFileResolution (path: string) {
+  const size = await getVideoFileSize(path)
+
+  return {
+    videoFileResolution: Math.min(size.height, size.width),
+    isPortraitMode: size.height > size.width
   }
 }
 
 async function getVideoFileFPS (path: string) {
   const videoStream = await getVideoFileStream(path)
 
-  for (const key of [ 'r_frame_rate' , 'avg_frame_rate' ]) {
+  for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
     const valuesText: string = videoStream[key]
     if (!valuesText) continue
 
@@ -110,52 +120,41 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
 type TranscodeOptions = {
   inputPath: string
   outputPath: string
-  resolution?: VideoResolution
+  resolution: VideoResolution
   isPortraitMode?: boolean
+
+  hlsPlaylist?: {
+    videoFilename: string
+  }
 }
 
 function transcode (options: TranscodeOptions) {
   return new Promise<void>(async (res, rej) => {
     try {
-      let fps = await getVideoFileFPS(options.inputPath)
-      // On small/medium resolutions, limit FPS
-      if (
-        options.resolution !== undefined &&
-        options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
-        fps > VIDEO_TRANSCODING_FPS.AVERAGE
-      ) {
-        fps = VIDEO_TRANSCODING_FPS.AVERAGE
-      }
-
       let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
         .output(options.outputPath)
-      command = await presetH264(command, options.resolution, fps)
+
+      if (options.hlsPlaylist) {
+        command = await buildHLSCommand(command, options)
+      } else {
+        command = await buildx264Command(command, options)
+      }
 
       if (CONFIG.TRANSCODING.THREADS > 0) {
         // if we don't set any threads ffmpeg will chose automatically
         command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
       }
 
-      if (options.resolution !== undefined) {
-        // '?x720' or '720x?' for example
-        const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
-        command = command.size(size)
-      }
-
-      if (fps) {
-        // Hard FPS limits
-        if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX
-        else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
-
-        command = command.withFPS(fps)
-      }
-
       command
         .on('error', (err, stdout, stderr) => {
           logger.error('Error in transcoding job.', { stdout, stderr })
           return rej(err)
         })
-        .on('end', res)
+        .on('end', () => {
+          return onTranscodingSuccess(options)
+            .then(() => res())
+            .catch(err => rej(err))
+        })
         .run()
     } catch (err) {
       return rej(err)
@@ -166,6 +165,7 @@ function transcode (options: TranscodeOptions) {
 // ---------------------------------------------------------------------------
 
 export {
+  getVideoFileSize,
   getVideoFileResolution,
   getDurationFromVideoFile,
   generateImageFromVideoFile,
@@ -178,13 +178,78 @@ export {
 
 // ---------------------------------------------------------------------------
 
+async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
+  let fps = await getVideoFileFPS(options.inputPath)
+  // On small/medium resolutions, limit FPS
+  if (
+    options.resolution !== undefined &&
+    options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
+    fps > VIDEO_TRANSCODING_FPS.AVERAGE
+  ) {
+    fps = VIDEO_TRANSCODING_FPS.AVERAGE
+  }
+
+  command = await presetH264(command, options.resolution, fps)
+
+  if (options.resolution !== undefined) {
+    // '?x720' or '720x?' for example
+    const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
+    command = command.size(size)
+  }
+
+  if (fps) {
+    // Hard FPS limits
+    if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX
+    else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
+
+    command = command.withFPS(fps)
+  }
+
+  return command
+}
+
+async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
+  const videoPath = getHLSVideoPath(options)
+
+  command = await presetCopy(command)
+
+  command = command.outputOption('-hls_time 4')
+                   .outputOption('-hls_list_size 0')
+                   .outputOption('-hls_playlist_type vod')
+                   .outputOption('-hls_segment_filename ' + videoPath)
+                   .outputOption('-hls_segment_type fmp4')
+                   .outputOption('-f hls')
+                   .outputOption('-hls_flags single_file')
+
+  return command
+}
+
+function getHLSVideoPath (options: TranscodeOptions) {
+  return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
+}
+
+async function onTranscodingSuccess (options: TranscodeOptions) {
+  if (!options.hlsPlaylist) return
+
+  // Fix wrong mapping with some ffmpeg versions
+  const fileContent = await readFile(options.outputPath)
+
+  const videoFileName = options.hlsPlaylist.videoFilename
+  const videoFilePath = getHLSVideoPath(options)
+
+  const newContent = fileContent.toString()
+                                .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
+
+  await writeFile(options.outputPath, newContent)
+}
+
 function getVideoFileStream (path: string) {
   return new Promise<any>((res, rej) => {
     ffmpeg.ffprobe(path, (err, metadata) => {
       if (err) return rej(err)
 
       const videoStream = metadata.streams.find(s => s.codec_type === 'video')
-      if (!videoStream) throw new Error('Cannot find video stream of ' + path)
+      if (!videoStream) return rej(new Error('Cannot find video stream of ' + path))
 
       return res(videoStream)
     })
@@ -328,10 +393,10 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol
     const audioCodecName = parsedAudio.audioStream[ 'codec_name' ]
     let bitrate: number
     if (audio.bitrate[ audioCodecName ]) {
-      bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ])
+      localCommand = localCommand.audioCodec('aac')
 
-      if (bitrate === -1) localCommand = localCommand.audioCodec('copy')
-      else if (bitrate !== undefined) localCommand = localCommand.audioBitrate(bitrate)
+      bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ])
+      if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate)
     }
   }
 
@@ -348,3 +413,10 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol
 
   return localCommand
 }
+
+async function presetCopy (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> {
+  return command
+    .format('mp4')
+    .videoCodec('copy')
+    .audioCodec('copy')
+}