]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Use a profile manager for transcoding
authorChocobozzz <me@florianbigard.com>
Thu, 28 Jan 2021 08:37:26 +0000 (09:37 +0100)
committerChocobozzz <me@florianbigard.com>
Thu, 28 Jan 2021 14:55:39 +0000 (15:55 +0100)
scripts/print-transcode-command.ts
server/helpers/ffmpeg-utils.ts
server/initializers/constants.ts
server/lib/live-manager.ts
server/lib/video-transcoding-profiles.ts
server/lib/video-transcoding.ts

index b75b711a4c7087475d984d9d7e1cdd76aed29f7d..f3b169b32e95c85f078bbe4a478d26d9e4952bde 100644 (file)
@@ -3,9 +3,9 @@ registerTSPaths()
 
 import * as program from 'commander'
 import * as ffmpeg from 'fluent-ffmpeg'
-import { availableEncoders } from '@server/lib/video-transcoding-profiles'
 import { buildx264VODCommand, runCommand, TranscodeOptions } from '@server/helpers/ffmpeg-utils'
 import { exit } from 'process'
+import { VideoTranscodingProfilesManager } from '@server/lib/video-transcoding-profiles'
 
 program
   .arguments('<path>')
@@ -31,7 +31,7 @@ async function run (path: string, cmd: any) {
     inputPath: path,
     outputPath: '/dev/null',
 
-    availableEncoders,
+    availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
     profile: 'default',
 
     resolution: +cmd.resolution,
index f85b9f316872bd24102d6b5e6f2a233b202e5266..7d46130ec206fce06a3fe7e2de4f3bb0cc61b7ff 100644 (file)
@@ -2,7 +2,7 @@ import { Job } from 'bull'
 import * as ffmpeg from 'fluent-ffmpeg'
 import { readFile, remove, writeFile } from 'fs-extra'
 import { dirname, join } from 'path'
-import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS } from '@server/initializers/constants'
+import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants'
 import { VideoResolution } from '../../shared/models/videos'
 import { checkFFmpegEncoders } from '../initializers/checker-before-init'
 import { CONFIG } from '../initializers/config'
@@ -46,11 +46,22 @@ export interface EncoderProfile <T> {
 }
 
 export type AvailableEncoders = {
-  [ id in 'live' | 'vod' ]: {
-    [ encoder in 'libx264' | 'aac' | 'libfdk_aac' ]?: EncoderProfile<EncoderOptionsBuilder>
+  live: {
+    [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
+  }
+
+  vod: {
+    [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
+  }
+
+  encodersToTry: {
+    video: string[]
+    audio: string[]
   }
 }
 
+type StreamType = 'audio' | 'video'
+
 // ---------------------------------------------------------------------------
 // Image manipulation
 // ---------------------------------------------------------------------------
@@ -243,8 +254,10 @@ async function getLiveTranscodingCommand (options: {
 
     const baseEncoderBuilderParams = {
       input,
+
       availableEncoders,
       profile,
+
       fps: resolutionFPS,
       resolution,
       streamNum: i,
@@ -252,7 +265,8 @@ async function getLiveTranscodingCommand (options: {
     }
 
     {
-      const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'VIDEO' }))
+      const streamType: StreamType = 'video'
+      const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType }))
       if (!builderResult) {
         throw new Error('No available live video encoder found')
       }
@@ -268,7 +282,8 @@ async function getLiveTranscodingCommand (options: {
     }
 
     {
-      const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'AUDIO' }))
+      const streamType: StreamType = 'audio'
+      const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType }))
       if (!builderResult) {
         throw new Error('No available live audio encoder found')
       }
@@ -480,8 +495,11 @@ function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptio
 // Transcoding presets
 // ---------------------------------------------------------------------------
 
+// Run encoder builder depending on available encoders
+// Try encoders by priority: if the encoder is available, run the chosen profile or fallback to the default one
+// If the default one does not exist, check the next encoder
 async function getEncoderBuilderResult (options: {
-  streamType: string
+  streamType: 'video' | 'audio'
   input: string
 
   availableEncoders: AvailableEncoders
@@ -495,17 +513,24 @@ async function getEncoderBuilderResult (options: {
 }) {
   const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options
 
-  const encodersToTry: string[] = VIDEO_TRANSCODING_ENCODERS[streamType]
+  const encodersToTry = availableEncoders.encodersToTry[streamType]
+  const encoders = availableEncoders[videoType]
 
   for (const encoder of encodersToTry) {
-    if (!(await checkFFmpegEncoders()).get(encoder) || !availableEncoders[videoType][encoder]) continue
+    if (!(await checkFFmpegEncoders()).get(encoder) || !encoders[encoder]) continue
 
-    const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = availableEncoders[videoType][encoder]
+    // An object containing available profiles for this encoder
+    const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = encoders[encoder]
     let builder = builderProfiles[profile]
 
     if (!builder) {
       logger.debug('Profile %s for encoder %s not available. Fallback to default.', profile, encoder)
       builder = builderProfiles.default
+
+      if (!builder) {
+        logger.debug('Default profile for encoder %s not available. Try next available encoder.', encoder)
+        continue
+      }
     }
 
     const result = await builder({ input, resolution: resolution, fps, streamNum })
@@ -538,11 +563,11 @@ async function presetVideo (
   // Audio encoder
   const parsedAudio = await getAudioStream(input)
 
-  let streamsToProcess = [ 'AUDIO', 'VIDEO' ]
+  let streamsToProcess: StreamType[] = [ 'audio', 'video' ]
 
   if (!parsedAudio.audioStream) {
     localCommand = localCommand.noAudio()
-    streamsToProcess = [ 'VIDEO' ]
+    streamsToProcess = [ 'audio' ]
   }
 
   for (const streamType of streamsToProcess) {
@@ -564,9 +589,9 @@ async function presetVideo (
 
     logger.debug('Apply ffmpeg params from %s.', builderResult.encoder, builderResult)
 
-    if (streamType === 'VIDEO') {
+    if (streamType === 'video') {
       localCommand.videoCodec(builderResult.encoder)
-    } else if (streamType === 'AUDIO') {
+    } else if (streamType === 'audio') {
       localCommand.audioCodec(builderResult.encoder)
     }
 
index 89491708e0abf24890cf2473823f723e5e0f4e3a..0fab872a9d29e3882e8c3b7c0b0967192201d5cd 100644 (file)
@@ -341,17 +341,6 @@ const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = {
   KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum)
 }
 
-const VIDEO_TRANSCODING_ENCODERS = {
-  VIDEO: [ 'libx264' ],
-
-  // Try the first one, if not available try the second one etc
-  AUDIO: [
-    // we favor VBR, if a good AAC encoder is available
-    'libfdk_aac',
-    'aac'
-  ]
-}
-
 const DEFAULT_AUDIO_RESOLUTION = VideoResolution.H_480P
 
 const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = {
@@ -828,7 +817,6 @@ export {
   ACTOR_FOLLOW_SCORE,
   PREVIEWS_SIZE,
   REMOTE_SCHEME,
-  VIDEO_TRANSCODING_ENCODERS,
   FOLLOW_STATES,
   DEFAULT_USER_THEME_NAME,
   SERVER_ACTOR_NAME,
index d968f05da4bea0ef299044c5e8cf02b69d547879..c8e5bcb774d7d0fb94acb114634c46456dc1e6a3 100644 (file)
@@ -25,7 +25,7 @@ import { cleanupLive } from './job-queue/handlers/video-live-ending'
 import { PeerTubeSocket } from './peertube-socket'
 import { isAbleToUploadVideo } from './user'
 import { getHLSDirectory } from './video-paths'
-import { availableEncoders } from './video-transcoding-profiles'
+import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
 
 import memoizee = require('memoizee')
 const NodeRtmpSession = require('node-media-server/node_rtmp_session')
@@ -337,7 +337,7 @@ class LiveManager {
         outPath,
         resolutions: allResolutions,
         fps,
-        availableEncoders,
+        availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
         profile: 'default'
       })
       : getLiveMuxingCommand(rtmpUrl, outPath)
index 338f4de4a28e7244595dc481488fe5917f1c6c5d..bbe556e753bd408e8ded70752825cd26f12a756e 100644 (file)
@@ -78,32 +78,83 @@ const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum })
   return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] }
 }
 
-const availableEncoders: AvailableEncoders = {
-  vod: {
-    libx264: {
-      default: defaultX264VODOptionsBuilder
-    },
-    aac: {
-      default: defaultAACOptionsBuilder
+// Used to get and update available encoders
+class VideoTranscodingProfilesManager {
+  private static instance: VideoTranscodingProfilesManager
+
+  // 1 === less priority
+  private readonly encodersPriorities = {
+    video: [
+      { name: 'libx264', priority: 100 }
+    ],
+
+    // Try the first one, if not available try the second one etc
+    audio: [
+      // we favor VBR, if a good AAC encoder is available
+      { name: 'libfdk_aac', priority: 200 },
+      { name: 'aac', priority: 100 }
+    ]
+  }
+
+  private readonly availableEncoders = {
+    vod: {
+      libx264: {
+        default: defaultX264VODOptionsBuilder
+      },
+      aac: {
+        default: defaultAACOptionsBuilder
+      },
+      libfdk_aac: {
+        default: defaultLibFDKAACVODOptionsBuilder
+      }
     },
-    libfdk_aac: {
-      default: defaultLibFDKAACVODOptionsBuilder
+    live: {
+      libx264: {
+        default: defaultX264LiveOptionsBuilder
+      },
+      aac: {
+        default: defaultAACOptionsBuilder
+      }
     }
-  },
-  live: {
-    libx264: {
-      default: defaultX264LiveOptionsBuilder
-    },
-    aac: {
-      default: defaultAACOptionsBuilder
+  }
+
+  private constructor () {
+
+  }
+
+  getAvailableEncoders (): AvailableEncoders {
+    const encodersToTry = {
+      video: this.getEncodersByPriority('video'),
+      audio: this.getEncodersByPriority('audio')
     }
+
+    return Object.assign({}, this.availableEncoders, { encodersToTry })
+  }
+
+  getAvailableProfiles (type: 'vod' | 'live') {
+    return this.availableEncoders[type]
+  }
+
+  private getEncodersByPriority (type: 'video' | 'audio') {
+    return this.encodersPriorities[type]
+      .sort((e1, e2) => {
+        if (e1.priority > e2.priority) return -1
+        else if (e1.priority === e2.priority) return 0
+
+        return 1
+      })
+      .map(e => e.name)
+  }
+
+  static get Instance () {
+    return this.instance || (this.instance = new this())
   }
 }
 
 // ---------------------------------------------------------------------------
 
 export {
-  availableEncoders
+  VideoTranscodingProfilesManager
 }
 
 // ---------------------------------------------------------------------------
index 7af7a481cf83576d34c381e99ade0aca9ed87689..c4b3425d1e86edbac831d5f10362048337f74d98 100644 (file)
@@ -14,7 +14,7 @@ import { VideoFileModel } from '../models/video/video-file'
 import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
 import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls'
 import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths'
-import { availableEncoders } from './video-transcoding-profiles'
+import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
 
 /**
  *
@@ -41,7 +41,7 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile:
     inputPath: videoInputPath,
     outputPath: videoTranscodedPath,
 
-    availableEncoders,
+    availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
     profile: 'default',
 
     resolution: inputVideoFile.resolution,
@@ -95,7 +95,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti
       inputPath: videoInputPath,
       outputPath: videoTranscodedPath,
 
-      availableEncoders,
+      availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
       profile: 'default',
 
       resolution,
@@ -107,7 +107,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti
       inputPath: videoInputPath,
       outputPath: videoTranscodedPath,
 
-      availableEncoders,
+      availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
       profile: 'default',
 
       resolution,
@@ -142,7 +142,7 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video
     inputPath: tmpPreviewPath,
     outputPath: videoTranscodedPath,
 
-    availableEncoders,
+    availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
     profile: 'default',
 
     audioPath: audioInputPath,
@@ -283,7 +283,7 @@ async function generateHlsPlaylistCommon (options: {
     inputPath,
     outputPath,
 
-    availableEncoders,
+    availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
     profile: 'default',
 
     resolution,