]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Support transcoding options/encoders by plugins
authorChocobozzz <me@florianbigard.com>
Thu, 28 Jan 2021 14:52:44 +0000 (15:52 +0100)
committerChocobozzz <me@florianbigard.com>
Thu, 28 Jan 2021 14:55:39 +0000 (15:55 +0100)
32 files changed:
config/default.yaml
config/production.yaml.example
server/controllers/api/config.ts
server/helpers/ffmpeg-utils.ts
server/helpers/logger.ts
server/initializers/checker-before-init.ts
server/initializers/config.ts
server/lib/live-manager.ts
server/lib/plugins/plugin-helpers-builder.ts [moved from server/lib/plugins/plugin-helpers.ts with 100% similarity]
server/lib/plugins/plugin-manager.ts
server/lib/plugins/register-helpers.ts [moved from server/lib/plugins/register-helpers-store.ts with 78% similarity]
server/lib/video-transcoding-profiles.ts
server/lib/video-transcoding.ts
server/middlewares/validators/plugins.ts
server/models/video/video.ts
server/tests/api/check-params/config.ts
server/tests/api/server/config.ts
server/tests/api/videos/video-transcoder.ts
server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js [new file with mode: 0644]
server/tests/fixtures/peertube-plugin-test-transcoding-one/package.json [new file with mode: 0644]
server/tests/fixtures/peertube-plugin-test-transcoding-two/main.js [new file with mode: 0644]
server/tests/fixtures/peertube-plugin-test-transcoding-two/package.json [new file with mode: 0644]
server/tests/plugins/index.ts
server/tests/plugins/plugin-transcoding.ts [new file with mode: 0644]
server/types/plugins/register-server-option.model.ts
shared/extra-utils/server/config.ts
shared/models/plugins/index.ts
shared/models/plugins/plugin-transcoding-manager.model.ts [new file with mode: 0644]
shared/models/server/custom-config.model.ts
shared/models/server/server-config.model.ts
shared/models/videos/index.ts
shared/models/videos/video-transcoding.model.ts [new file with mode: 0644]

index b9e382fa7d2595aa384784adc9e56b42e1191094..283e0ab936225504120e698e0b7f3b61b065db99 100644 (file)
@@ -223,11 +223,20 @@ user:
 # Please, do not disable transcoding since many uploaded videos will not work
 transcoding:
   enabled: true
+
   # Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos
   allow_additional_extensions: true
+
   # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file
   allow_audio_files: true
+
   threads: 1
+
+  # Choose the transcoding profile
+  # New profiles can be added by plugins
+  # Available in core PeerTube: 'default'
+  profile: 'default'
+
   resolutions: # Only created if the original video has a higher resolution, uses more storage!
     0p: false # audio-only (creates mp4 without video stream, always created when enabled)
     240p: false
@@ -283,6 +292,11 @@ live:
     enabled: true
     threads: 2
 
+    # Choose the transcoding profile
+    # New profiles can be added by plugins
+    # Available in core PeerTube: 'default'
+    profile: 'default'
+
     resolutions:
       240p: false
       360p: false
index b616c6ced0789b3b1be28ec3300242025f8824e2..66c981dd544e4a16cdc835a6125682584029433f 100644 (file)
@@ -236,11 +236,20 @@ user:
 # Please, do not disable transcoding since many uploaded videos will not work
 transcoding:
   enabled: true
+
   # Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos
   allow_additional_extensions: true
+
   # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file
   allow_audio_files: true
+
   threads: 1
+
+  # Choose the transcoding profile
+  # New profiles can be added by plugins
+  # Available in core PeerTube: 'default'
+  profile: 'default'
+
   resolutions: # Only created if the original video has a higher resolution, uses more storage!
     0p: false # audio-only (creates mp4 without video stream, always created when enabled)
     240p: false
@@ -270,7 +279,7 @@ live:
   enabled: false
 
   # Limit lives duration
-  # Set null to disable duration limit
+  # -1 == unlimited
   max_duration: -1 # For example: '5 hours'
 
   # Limit max number of live videos created on your instance
@@ -296,6 +305,11 @@ live:
     enabled: true
     threads: 2
 
+    # Choose the transcoding profile
+    # New profiles can be added by plugins
+    # Available in core PeerTube: 'default'
+    profile: 'default'
+
     resolutions:
       240p: false
       360p: false
index 45c03be243411d9e5f38ade27b85048743a4eeb1..7fda06a87329f565fcb06f8e5a5b3ad286af0f22 100644 (file)
@@ -18,6 +18,7 @@ import { PluginManager } from '../../lib/plugins/plugin-manager'
 import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
 import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
 import { customConfigUpdateValidator } from '../../middlewares/validators/config'
+import { VideoTranscodingProfilesManager } from '@server/lib/video-transcoding-profiles'
 
 const configRouter = express.Router()
 
@@ -114,7 +115,9 @@ async function getConfig (req: express.Request, res: express.Response) {
       webtorrent: {
         enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
       },
-      enabledResolutions: getEnabledResolutions('vod')
+      enabledResolutions: getEnabledResolutions('vod'),
+      profile: CONFIG.TRANSCODING.PROFILE,
+      availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('vod')
     },
     live: {
       enabled: CONFIG.LIVE.ENABLED,
@@ -126,7 +129,9 @@ async function getConfig (req: express.Request, res: express.Response) {
 
       transcoding: {
         enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
-        enabledResolutions: getEnabledResolutions('live')
+        enabledResolutions: getEnabledResolutions('live'),
+        profile: CONFIG.LIVE.TRANSCODING.PROFILE,
+        availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('live')
       },
 
       rtmp: {
@@ -412,6 +417,7 @@ function customConfig (): CustomConfig {
       allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
       allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES,
       threads: CONFIG.TRANSCODING.THREADS,
+      profile: CONFIG.TRANSCODING.PROFILE,
       resolutions: {
         '0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'],
         '240p': CONFIG.TRANSCODING.RESOLUTIONS['240p'],
@@ -438,6 +444,7 @@ function customConfig (): CustomConfig {
       transcoding: {
         enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
         threads: CONFIG.LIVE.TRANSCODING.THREADS,
+        profile: CONFIG.LIVE.TRANSCODING.PROFILE,
         resolutions: {
           '240p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['240p'],
           '360p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['360p'],
index 7d46130ec206fce06a3fe7e2de4f3bb0cc61b7ff..33c625c9ef659a310fd6b6b1c99cd486ba0e45b4 100644 (file)
@@ -3,9 +3,9 @@ import * as ffmpeg from 'fluent-ffmpeg'
 import { readFile, remove, writeFile } from 'fs-extra'
 import { dirname, join } from 'path'
 import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants'
-import { VideoResolution } from '../../shared/models/videos'
-import { checkFFmpegEncoders } from '../initializers/checker-before-init'
+import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos'
 import { CONFIG } from '../initializers/config'
+import { promisify0 } from './core-utils'
 import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils'
 import { processImage } from './image-utils'
 import { logger } from './logger'
@@ -21,46 +21,45 @@ import { logger } from './logger'
 // Encoder options
 // ---------------------------------------------------------------------------
 
-// Options builders
-
-export type EncoderOptionsBuilder = (params: {
-  input: string
-  resolution: VideoResolution
-  fps?: number
-  streamNum?: number
-}) => Promise<EncoderOptions> | EncoderOptions
+type StreamType = 'audio' | 'video'
 
-// Options types
+// ---------------------------------------------------------------------------
+// Encoders support
+// ---------------------------------------------------------------------------
 
-export interface EncoderOptions {
-  copy?: boolean
-  outputOptions: string[]
-}
+// Detect supported encoders by ffmpeg
+let supportedEncoders: Map<string, boolean>
+async function checkFFmpegEncoders (peertubeAvailableEncoders: AvailableEncoders): Promise<Map<string, boolean>> {
+  if (supportedEncoders !== undefined) {
+    return supportedEncoders
+  }
 
-// All our encoders
+  const getAvailableEncodersPromise = promisify0(ffmpeg.getAvailableEncoders)
+  const availableFFmpegEncoders = await getAvailableEncodersPromise()
 
-export interface EncoderProfile <T> {
-  [ profile: string ]: T
+  const searchEncoders = new Set<string>()
+  for (const type of [ 'live', 'vod' ]) {
+    for (const streamType of [ 'audio', 'video' ]) {
+      for (const encoder of peertubeAvailableEncoders.encodersToTry[type][streamType]) {
+        searchEncoders.add(encoder)
+      }
+    }
+  }
 
-  default: T
-}
+  supportedEncoders = new Map<string, boolean>()
 
-export type AvailableEncoders = {
-  live: {
-    [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
+  for (const searchEncoder of searchEncoders) {
+    supportedEncoders.set(searchEncoder, availableFFmpegEncoders[searchEncoder] !== undefined)
   }
 
-  vod: {
-    [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
-  }
+  logger.info('Built supported ffmpeg encoders.', { supportedEncoders, searchEncoders })
 
-  encodersToTry: {
-    video: string[]
-    audio: string[]
-  }
+  return supportedEncoders
 }
 
-type StreamType = 'audio' | 'video'
+function resetSupportedEncoders () {
+  supportedEncoders = undefined
+}
 
 // ---------------------------------------------------------------------------
 // Image manipulation
@@ -275,7 +274,7 @@ async function getLiveTranscodingCommand (options: {
 
       addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
 
-      logger.debug('Apply ffmpeg live video params from %s.', builderResult.encoder, builderResult)
+      logger.debug('Apply ffmpeg live video params from %s using %s profile.', builderResult.encoder, profile, builderResult)
 
       command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`)
       command.addOutputOptions(builderResult.result.outputOptions)
@@ -292,7 +291,7 @@ async function getLiveTranscodingCommand (options: {
 
       addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
 
-      logger.debug('Apply ffmpeg live audio params from %s.', builderResult.encoder, builderResult)
+      logger.debug('Apply ffmpeg live audio params from %s using %s profile.', builderResult.encoder, profile, builderResult)
 
       command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`)
       command.addOutputOptions(builderResult.result.outputOptions)
@@ -513,11 +512,19 @@ async function getEncoderBuilderResult (options: {
 }) {
   const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options
 
-  const encodersToTry = availableEncoders.encodersToTry[streamType]
-  const encoders = availableEncoders[videoType]
+  const encodersToTry = availableEncoders.encodersToTry[videoType][streamType]
+  const encoders = availableEncoders.available[videoType]
 
   for (const encoder of encodersToTry) {
-    if (!(await checkFFmpegEncoders()).get(encoder) || !encoders[encoder]) continue
+    if (!(await checkFFmpegEncoders(availableEncoders)).get(encoder)) {
+      logger.debug('Encoder %s not available in ffmpeg, skipping.', encoder)
+      continue
+    }
+
+    if (!encoders[encoder]) {
+      logger.debug('Encoder %s not available in peertube encoders, skipping.', encoder)
+      continue
+    }
 
     // An object containing available profiles for this encoder
     const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = encoders[encoder]
@@ -567,7 +574,7 @@ async function presetVideo (
 
   if (!parsedAudio.audioStream) {
     localCommand = localCommand.noAudio()
-    streamsToProcess = [ 'audio' ]
+    streamsToProcess = [ 'video' ]
   }
 
   for (const streamType of streamsToProcess) {
@@ -587,7 +594,10 @@ async function presetVideo (
       throw new Error('No available encoder found for stream ' + streamType)
     }
 
-    logger.debug('Apply ffmpeg params from %s.', builderResult.encoder, builderResult)
+    logger.debug(
+      'Apply ffmpeg params from %s for %s stream of input %s using %s profile.',
+      builderResult.encoder, streamType, input, profile, builderResult
+    )
 
     if (streamType === 'video') {
       localCommand.videoCodec(builderResult.encoder)
@@ -679,6 +689,8 @@ export {
   transcode,
   runCommand,
 
+  resetSupportedEncoders,
+
   // builders
   buildx264VODCommand
 }
index 05ec4a6b9ac4a34e301ddeb6647fa6ec29bbdbca..746b2e0a6d0a05f3eec6b563ee7a669c94e70a2e 100644 (file)
@@ -27,6 +27,14 @@ function getLoggerReplacer () {
       seen.add(value)
     }
 
+    if (value instanceof Set) {
+      return Array.from(value)
+    }
+
+    if (value instanceof Map) {
+      return Array.from(value.entries())
+    }
+
     if (value instanceof Error) {
       const error = {}
 
index 7945e85863fec6230da58ce09d03235dc96d33f3..9c4e0048a0c0166ddaa163150cc8cd1460430641 100644 (file)
@@ -97,34 +97,6 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) {
       throw new Error('Unavailable encode codec ' + codec + ' in FFmpeg')
     }
   }
-
-  return checkFFmpegEncoders()
-}
-
-// Detect supported encoders by ffmpeg
-let supportedEncoders: Map<string, boolean>
-async function checkFFmpegEncoders (): Promise<Map<string, boolean>> {
-  if (supportedEncoders !== undefined) {
-    return supportedEncoders
-  }
-
-  const Ffmpeg = require('fluent-ffmpeg')
-  const getAvailableEncodersPromise = promisify0(Ffmpeg.getAvailableEncoders)
-  const availableEncoders = await getAvailableEncodersPromise()
-
-  const searchEncoders = [
-    'aac',
-    'libfdk_aac',
-    'libx264'
-  ]
-
-  supportedEncoders = new Map<string, boolean>()
-
-  for (const searchEncoder of searchEncoders) {
-    supportedEncoders.set(searchEncoder, availableEncoders[searchEncoder] !== undefined)
-  }
-
-  return supportedEncoders
 }
 
 function checkNodeVersion () {
@@ -143,7 +115,6 @@ function checkNodeVersion () {
 
 export {
   checkFFmpeg,
-  checkFFmpegEncoders,
   checkMissedConfig,
   checkNodeVersion
 }
index fc4a8b709a4ebc013d3924a4d699a7cb139b5ec4..7322b89e22cc6626aff2fc0d0595a2035a900e40 100644 (file)
@@ -188,6 +188,7 @@ const CONFIG = {
     get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') },
     get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') },
     get THREADS () { return config.get<number>('transcoding.threads') },
+    get PROFILE () { return config.get<string>('transcoding.profile') },
     RESOLUTIONS: {
       get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') },
       get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') },
@@ -221,6 +222,7 @@ const CONFIG = {
     TRANSCODING: {
       get ENABLED () { return config.get<boolean>('live.transcoding.enabled') },
       get THREADS () { return config.get<number>('live.transcoding.threads') },
+      get PROFILE () { return config.get<string>('live.transcoding.profile') },
 
       RESOLUTIONS: {
         get '240p' () { return config.get<boolean>('live.transcoding.resolutions.240p') },
index c8e5bcb774d7d0fb94acb114634c46456dc1e6a3..9f17b8820e8c790c7b9e065b04596039ab836cb5 100644 (file)
@@ -338,7 +338,7 @@ class LiveManager {
         resolutions: allResolutions,
         fps,
         availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
-        profile: 'default'
+        profile: CONFIG.LIVE.TRANSCODING.PROFILE
       })
       : getLiveMuxingCommand(rtmpUrl, outPath)
 
index 8e74912570e1ab7c9bd3d682f5c4863507a9510e..c19b4013512c0735dcac883103ad542da84a2622 100644 (file)
@@ -20,7 +20,7 @@ import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants'
 import { PluginModel } from '../../models/server/plugin'
 import { PluginLibrary, RegisterServerAuthExternalOptions, RegisterServerAuthPassOptions, RegisterServerOptions } from '../../types/plugins'
 import { ClientHtml } from '../client-html'
-import { RegisterHelpersStore } from './register-helpers-store'
+import { RegisterHelpers } from './register-helpers'
 import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
 
 export interface RegisteredPlugin {
@@ -40,7 +40,7 @@ export interface RegisteredPlugin {
   css: string[]
 
   // Only if this is a plugin
-  registerHelpersStore?: RegisterHelpersStore
+  registerHelpers?: RegisterHelpers
   unregister?: Function
 }
 
@@ -109,7 +109,7 @@ export class PluginManager implements ServerHook {
         npmName: p.npmName,
         name: p.name,
         version: p.version,
-        idAndPassAuths: p.registerHelpersStore.getIdAndPassAuths()
+        idAndPassAuths: p.registerHelpers.getIdAndPassAuths()
       }))
       .filter(v => v.idAndPassAuths.length !== 0)
   }
@@ -120,7 +120,7 @@ export class PluginManager implements ServerHook {
         npmName: p.npmName,
         name: p.name,
         version: p.version,
-        externalAuths: p.registerHelpersStore.getExternalAuths()
+        externalAuths: p.registerHelpers.getExternalAuths()
       }))
       .filter(v => v.externalAuths.length !== 0)
   }
@@ -129,14 +129,14 @@ export class PluginManager implements ServerHook {
     const result = this.getRegisteredPluginOrTheme(npmName)
     if (!result || result.type !== PluginType.PLUGIN) return []
 
-    return result.registerHelpersStore.getSettings()
+    return result.registerHelpers.getSettings()
   }
 
   getRouter (npmName: string) {
     const result = this.getRegisteredPluginOrTheme(npmName)
     if (!result || result.type !== PluginType.PLUGIN) return null
 
-    return result.registerHelpersStore.getRouter()
+    return result.registerHelpers.getRouter()
   }
 
   getTranslations (locale: string) {
@@ -194,7 +194,7 @@ export class PluginManager implements ServerHook {
       logger.error('Cannot find plugin %s to call on settings changed.', name)
     }
 
-    for (const cb of registered.registerHelpersStore.getOnSettingsChangedCallbacks()) {
+    for (const cb of registered.registerHelpers.getOnSettingsChangedCallbacks()) {
       try {
         cb(settings)
       } catch (err) {
@@ -268,8 +268,9 @@ export class PluginManager implements ServerHook {
         this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName)
       }
 
-      const store = plugin.registerHelpersStore
+      const store = plugin.registerHelpers
       store.reinitVideoConstants(plugin.npmName)
+      store.reinitTranscodingProfilesAndEncoders(plugin.npmName)
 
       logger.info('Regenerating registered plugin CSS to global file.')
       await this.regeneratePluginGlobalCSS()
@@ -375,11 +376,11 @@ export class PluginManager implements ServerHook {
     this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type)
 
     let library: PluginLibrary
-    let registerHelpersStore: RegisterHelpersStore
+    let registerHelpers: RegisterHelpers
     if (plugin.type === PluginType.PLUGIN) {
       const result = await this.registerPlugin(plugin, pluginPath, packageJSON)
       library = result.library
-      registerHelpersStore = result.registerStore
+      registerHelpers = result.registerStore
     }
 
     const clientScripts: { [id: string]: ClientScript } = {}
@@ -398,7 +399,7 @@ export class PluginManager implements ServerHook {
       staticDirs: packageJSON.staticDirs,
       clientScripts,
       css: packageJSON.css,
-      registerHelpersStore: registerHelpersStore || undefined,
+      registerHelpers: registerHelpers || undefined,
       unregister: library ? library.unregister : undefined
     }
 
@@ -512,8 +513,8 @@ export class PluginManager implements ServerHook {
     const plugin = this.getRegisteredPluginOrTheme(npmName)
     if (!plugin || plugin.type !== PluginType.PLUGIN) return null
 
-    let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpersStore.getIdAndPassAuths()
-    auths = auths.concat(plugin.registerHelpersStore.getExternalAuths())
+    let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpers.getIdAndPassAuths()
+    auths = auths.concat(plugin.registerHelpers.getExternalAuths())
 
     return auths.find(a => a.authName === authName)
   }
@@ -538,7 +539,7 @@ export class PluginManager implements ServerHook {
   private getRegisterHelpers (
     npmName: string,
     plugin: PluginModel
-  ): { registerStore: RegisterHelpersStore, registerOptions: RegisterServerOptions } {
+  ): { registerStore: RegisterHelpers, registerOptions: RegisterServerOptions } {
     const onHookAdded = (options: RegisterServerHookOptions) => {
       if (!this.hooks[options.target]) this.hooks[options.target] = []
 
@@ -550,11 +551,11 @@ export class PluginManager implements ServerHook {
       })
     }
 
-    const registerHelpersStore = new RegisterHelpersStore(npmName, plugin, onHookAdded.bind(this))
+    const registerHelpers = new RegisterHelpers(npmName, plugin, onHookAdded.bind(this))
 
     return {
-      registerStore: registerHelpersStore,
-      registerOptions: registerHelpersStore.buildRegisterHelpers()
+      registerStore: registerHelpers,
+      registerOptions: registerHelpers.buildRegisterHelpers()
     }
   }
 
similarity index 78%
rename from server/lib/plugins/register-helpers-store.ts
rename to server/lib/plugins/register-helpers.ts
index c73079302157c573aa982d4c1109d9e239c67fd4..3a38a4835626070519ef1ba03a8b3b87b2f40f8b 100644 (file)
@@ -17,6 +17,7 @@ import {
   RegisterServerOptions
 } from '@server/types/plugins'
 import {
+  EncoderOptionsBuilder,
   PluginPlaylistPrivacyManager,
   PluginSettingsManager,
   PluginStorageManager,
@@ -28,7 +29,8 @@ import {
   RegisterServerSettingOptions
 } from '@shared/models'
 import { serverHookObject } from '@shared/models/plugins/server-hook.model'
-import { buildPluginHelpers } from './plugin-helpers'
+import { VideoTranscodingProfilesManager } from '../video-transcoding-profiles'
+import { buildPluginHelpers } from './plugin-helpers-builder'
 
 type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
 type VideoConstant = { [key in number | string]: string }
@@ -40,7 +42,7 @@ type UpdatedVideoConstant = {
   }
 }
 
-export class RegisterHelpersStore {
+export class RegisterHelpers {
   private readonly updatedVideoConstants: UpdatedVideoConstant = {
     playlistPrivacy: { added: [], deleted: [] },
     privacy: { added: [], deleted: [] },
@@ -49,6 +51,23 @@ export class RegisterHelpersStore {
     category: { added: [], deleted: [] }
   }
 
+  private readonly transcodingProfiles: {
+    [ npmName: string ]: {
+      type: 'vod' | 'live'
+      encoder: string
+      profile: string
+    }[]
+  } = {}
+
+  private readonly transcodingEncoders: {
+    [ npmName: string ]: {
+      type: 'vod' | 'live'
+      streamType: 'audio' | 'video'
+      encoder: string
+      priority: number
+    }[]
+  } = {}
+
   private readonly settings: RegisterServerSettingOptions[] = []
 
   private idAndPassAuths: RegisterServerAuthPassOptions[] = []
@@ -83,6 +102,8 @@ export class RegisterHelpersStore {
     const videoPrivacyManager = this.buildVideoPrivacyManager()
     const playlistPrivacyManager = this.buildPlaylistPrivacyManager()
 
+    const transcodingManager = this.buildTranscodingManager()
+
     const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth()
     const registerExternalAuth = this.buildRegisterExternalAuth()
     const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth()
@@ -106,6 +127,8 @@ export class RegisterHelpersStore {
       videoPrivacyManager,
       playlistPrivacyManager,
 
+      transcodingManager,
+
       registerIdAndPassAuth,
       registerExternalAuth,
       unregisterIdAndPassAuth,
@@ -141,6 +164,22 @@ export class RegisterHelpersStore {
     }
   }
 
+  reinitTranscodingProfilesAndEncoders (npmName: string) {
+    const profiles = this.transcodingProfiles[npmName]
+    if (Array.isArray(profiles)) {
+      for (const profile of profiles) {
+        VideoTranscodingProfilesManager.Instance.removeProfile(profile)
+      }
+    }
+
+    const encoders = this.transcodingEncoders[npmName]
+    if (Array.isArray(encoders)) {
+      for (const o of encoders) {
+        VideoTranscodingProfilesManager.Instance.removeEncoderPriority(o.type, o.streamType, o.encoder, o.priority)
+      }
+    }
+  }
+
   getSettings () {
     return this.settings
   }
@@ -354,4 +393,52 @@ export class RegisterHelpersStore {
 
     return true
   }
+
+  private buildTranscodingManager () {
+    const self = this
+
+    function addProfile (type: 'live' | 'vod', encoder: string, profile: string, builder: EncoderOptionsBuilder) {
+      if (profile === 'default') {
+        logger.error('A plugin cannot add a default live transcoding profile')
+        return false
+      }
+
+      VideoTranscodingProfilesManager.Instance.addProfile({
+        type,
+        encoder,
+        profile,
+        builder
+      })
+
+      if (!self.transcodingProfiles[self.npmName]) self.transcodingProfiles[self.npmName] = []
+      self.transcodingProfiles[self.npmName].push({ type, encoder, profile })
+
+      return true
+    }
+
+    function addEncoderPriority (type: 'live' | 'vod', streamType: 'audio' | 'video', encoder: string, priority: number) {
+      VideoTranscodingProfilesManager.Instance.addEncoderPriority(type, streamType, encoder, priority)
+
+      if (!self.transcodingEncoders[self.npmName]) self.transcodingEncoders[self.npmName] = []
+      self.transcodingEncoders[self.npmName].push({ type, streamType, encoder, priority })
+    }
+
+    return {
+      addLiveProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder) {
+        return addProfile('live', encoder, profile, builder)
+      },
+
+      addVODProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder) {
+        return addProfile('vod', encoder, profile, builder)
+      },
+
+      addLiveEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number) {
+        return addEncoderPriority('live', streamType, encoder, priority)
+      },
+
+      addVODEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number) {
+        return addEncoderPriority('vod', streamType, encoder, priority)
+      }
+    }
+  }
 }
index bbe556e753bd408e8ded70752825cd26f12a756e..76d38b6cae8f0c710cecd9cf8f43f05941456240 100644 (file)
@@ -1,6 +1,6 @@
 import { logger } from '@server/helpers/logger'
-import { getTargetBitrate, VideoResolution } from '../../shared/models/videos'
-import { AvailableEncoders, buildStreamSuffix, EncoderOptionsBuilder } from '../helpers/ffmpeg-utils'
+import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../shared/models/videos'
+import { buildStreamSuffix, resetSupportedEncoders } from '../helpers/ffmpeg-utils'
 import {
   canDoQuickAudioTranscode,
   ffprobePromise,
@@ -84,16 +84,8 @@ class 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 }
-    ]
+    vod: this.buildDefaultEncodersPriorities(),
+    live: this.buildDefaultEncodersPriorities()
   }
 
   private readonly availableEncoders = {
@@ -118,25 +110,77 @@ class VideoTranscodingProfilesManager {
     }
   }
 
-  private constructor () {
+  private availableProfiles = {
+    vod: [] as string[],
+    live: [] as string[]
+  }
 
+  private constructor () {
+    this.buildAvailableProfiles()
   }
 
   getAvailableEncoders (): AvailableEncoders {
-    const encodersToTry = {
-      video: this.getEncodersByPriority('video'),
-      audio: this.getEncodersByPriority('audio')
+    return {
+      available: this.availableEncoders,
+      encodersToTry: {
+        vod: {
+          video: this.getEncodersByPriority('vod', 'video'),
+          audio: this.getEncodersByPriority('vod', 'audio')
+        },
+        live: {
+          video: this.getEncodersByPriority('live', 'video'),
+          audio: this.getEncodersByPriority('live', 'audio')
+        }
+      }
     }
-
-    return Object.assign({}, this.availableEncoders, { encodersToTry })
   }
 
   getAvailableProfiles (type: 'vod' | 'live') {
-    return this.availableEncoders[type]
+    return this.availableProfiles[type]
+  }
+
+  addProfile (options: {
+    type: 'vod' | 'live'
+    encoder: string
+    profile: string
+    builder: EncoderOptionsBuilder
+  }) {
+    const { type, encoder, profile, builder } = options
+
+    const encoders = this.availableEncoders[type]
+
+    if (!encoders[encoder]) encoders[encoder] = {}
+    encoders[encoder][profile] = builder
+
+    this.buildAvailableProfiles()
+  }
+
+  removeProfile (options: {
+    type: 'vod' | 'live'
+    encoder: string
+    profile: string
+  }) {
+    const { type, encoder, profile } = options
+
+    delete this.availableEncoders[type][encoder][profile]
+    this.buildAvailableProfiles()
   }
 
-  private getEncodersByPriority (type: 'video' | 'audio') {
-    return this.encodersPriorities[type]
+  addEncoderPriority (type: 'vod' | 'live', streamType: 'audio' | 'video', encoder: string, priority: number) {
+    this.encodersPriorities[type][streamType].push({ name: encoder, priority })
+
+    resetSupportedEncoders()
+  }
+
+  removeEncoderPriority (type: 'vod' | 'live', streamType: 'audio' | 'video', encoder: string, priority: number) {
+    this.encodersPriorities[type][streamType] = this.encodersPriorities[type][streamType]
+                                                    .filter(o => o.name !== encoder && o.priority !== priority)
+
+    resetSupportedEncoders()
+  }
+
+  private getEncodersByPriority (type: 'vod' | 'live', streamType: 'audio' | 'video') {
+    return this.encodersPriorities[type][streamType]
       .sort((e1, e2) => {
         if (e1.priority > e2.priority) return -1
         else if (e1.priority === e2.priority) return 0
@@ -146,6 +190,39 @@ class VideoTranscodingProfilesManager {
       .map(e => e.name)
   }
 
+  private buildAvailableProfiles () {
+    for (const type of [ 'vod', 'live' ]) {
+      const result = new Set()
+
+      const encoders = this.availableEncoders[type]
+
+      for (const encoderName of Object.keys(encoders)) {
+        for (const profile of Object.keys(encoders[encoderName])) {
+          result.add(profile)
+        }
+      }
+
+      this.availableProfiles[type] = Array.from(result)
+    }
+
+    logger.debug('Available transcoding profiles built.', { availableProfiles: this.availableProfiles })
+  }
+
+  private buildDefaultEncodersPriorities () {
+    return {
+      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 }
+      ]
+    }
+  }
+
   static get Instance () {
     return this.instance || (this.instance = new this())
   }
index c4b3425d1e86edbac831d5f10362048337f74d98..37a4f3019b0ab6499bee27ba4d29c6ffeb5395ad 100644 (file)
@@ -42,7 +42,7 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile:
     outputPath: videoTranscodedPath,
 
     availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
-    profile: 'default',
+    profile: CONFIG.TRANSCODING.PROFILE,
 
     resolution: inputVideoFile.resolution,
 
@@ -96,7 +96,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti
       outputPath: videoTranscodedPath,
 
       availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
-      profile: 'default',
+      profile: CONFIG.TRANSCODING.PROFILE,
 
       resolution,
 
@@ -108,7 +108,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti
       outputPath: videoTranscodedPath,
 
       availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
-      profile: 'default',
+      profile: CONFIG.TRANSCODING.PROFILE,
 
       resolution,
       isPortraitMode: isPortrait,
@@ -143,7 +143,7 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video
     outputPath: videoTranscodedPath,
 
     availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
-    profile: 'default',
+    profile: CONFIG.TRANSCODING.PROFILE,
 
     audioPath: audioInputPath,
     resolution,
@@ -284,7 +284,7 @@ async function generateHlsPlaylistCommon (options: {
     outputPath,
 
     availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
-    profile: 'default',
+    profile: CONFIG.TRANSCODING.PROFILE,
 
     resolution,
     copyCodecs,
index bbac55a50e08cecaaf5f69d9264da8cfe8bb7bca..1083e0afae83677c9bbd9122f674d6038e3ac687 100644 (file)
@@ -50,9 +50,9 @@ const getExternalAuthValidator = [
     if (areValidationErrors(req, res)) return
 
     const plugin = res.locals.registeredPlugin
-    if (!plugin.registerHelpersStore) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
+    if (!plugin.registerHelpers) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
 
-    const externalAuth = plugin.registerHelpersStore.getExternalAuths().find(a => a.authName === req.params.authName)
+    const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName)
     if (!externalAuth) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
 
     res.locals.externalAuth = externalAuth
index c56fbfbf2aae8312f7e9f311587b738be90b0a1e..ea6c9d44ba080ddadbe2012bacfaaeb3a6cbb1c6 100644 (file)
@@ -96,10 +96,11 @@ import {
   MVideoWithRights
 } from '../../types/models'
 import { MThumbnail } from '../../types/models/video/thumbnail'
-import { MVideoFile, MVideoFileRedundanciesOpt, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
+import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
 import { VideoAbuseModel } from '../abuse/video-abuse'
 import { AccountModel } from '../account/account'
 import { AccountVideoRateModel } from '../account/account-video-rate'
+import { UserModel } from '../account/user'
 import { UserVideoHistoryModel } from '../account/user-video-history'
 import { ActorModel } from '../activitypub/actor'
 import { AvatarModel } from '../avatar/avatar'
@@ -129,7 +130,6 @@ import { VideoShareModel } from './video-share'
 import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
 import { VideoTagModel } from './video-tag'
 import { VideoViewModel } from './video-view'
-import { UserModel } from '../account/user'
 
 export enum ScopeNames {
   AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
index d3ae5fe0acc8d6b8d0446709af925cf18ba69899..e6309b5f7eb386dc92652b476b024c4043a328d1 100644 (file)
@@ -87,6 +87,7 @@ describe('Test config API validators', function () {
       allowAdditionalExtensions: true,
       allowAudioFiles: true,
       threads: 1,
+      profile: 'vod_profile',
       resolutions: {
         '0p': false,
         '240p': false,
@@ -115,6 +116,7 @@ describe('Test config API validators', function () {
       transcoding: {
         enabled: true,
         threads: 4,
+        profile: 'live_profile',
         resolutions: {
           '240p': true,
           '360p': true,
index e0575bdfdc8e711ba7cd1f70d2994ea17fbf6e3d..e5bab0b7721bd332e319865d7826bc01bd4d221e 100644 (file)
@@ -70,6 +70,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
   expect(data.transcoding.allowAdditionalExtensions).to.be.false
   expect(data.transcoding.allowAudioFiles).to.be.false
   expect(data.transcoding.threads).to.equal(2)
+  expect(data.transcoding.profile).to.equal('default')
   expect(data.transcoding.resolutions['240p']).to.be.true
   expect(data.transcoding.resolutions['360p']).to.be.true
   expect(data.transcoding.resolutions['480p']).to.be.true
@@ -87,6 +88,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
   expect(data.live.maxUserLives).to.equal(3)
   expect(data.live.transcoding.enabled).to.be.false
   expect(data.live.transcoding.threads).to.equal(2)
+  expect(data.live.transcoding.profile).to.equal('default')
   expect(data.live.transcoding.resolutions['240p']).to.be.false
   expect(data.live.transcoding.resolutions['360p']).to.be.false
   expect(data.live.transcoding.resolutions['480p']).to.be.false
@@ -159,6 +161,7 @@ function checkUpdatedConfig (data: CustomConfig) {
   expect(data.transcoding.threads).to.equal(1)
   expect(data.transcoding.allowAdditionalExtensions).to.be.true
   expect(data.transcoding.allowAudioFiles).to.be.true
+  expect(data.transcoding.profile).to.equal('vod_profile')
   expect(data.transcoding.resolutions['240p']).to.be.false
   expect(data.transcoding.resolutions['360p']).to.be.true
   expect(data.transcoding.resolutions['480p']).to.be.true
@@ -175,6 +178,7 @@ function checkUpdatedConfig (data: CustomConfig) {
   expect(data.live.maxUserLives).to.equal(10)
   expect(data.live.transcoding.enabled).to.be.true
   expect(data.live.transcoding.threads).to.equal(4)
+  expect(data.live.transcoding.profile).to.equal('live_profile')
   expect(data.live.transcoding.resolutions['240p']).to.be.true
   expect(data.live.transcoding.resolutions['360p']).to.be.true
   expect(data.live.transcoding.resolutions['480p']).to.be.true
@@ -319,6 +323,7 @@ describe('Test config', function () {
         allowAdditionalExtensions: true,
         allowAudioFiles: true,
         threads: 1,
+        profile: 'vod_profile',
         resolutions: {
           '0p': false,
           '240p': false,
@@ -345,6 +350,7 @@ describe('Test config', function () {
         transcoding: {
           enabled: true,
           threads: 4,
+          profile: 'live_profile',
           resolutions: {
             '240p': true,
             '360p': true,
index 631230f26f012d2725cb8cf82ce86b101a8fed23..5ad02df2fc786c95c6d9eca312313eedf8402ebe 100644 (file)
@@ -511,7 +511,9 @@ describe('Test video transcoding', function () {
 
     const resolutions = [ 240, 360, 480, 720, 1080 ]
     for (const r of resolutions) {
-      expect(await getServerFileSize(servers[1], `videos/${videoUUID}-${r}.mp4`)).to.be.below(60_000)
+      const path = `videos/${videoUUID}-${r}.mp4`
+      const size = await getServerFileSize(servers[1], path)
+      expect(size, `${path} not below ${60_000}`).to.be.below(60_000)
     }
   })
 
diff --git a/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js b/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js
new file mode 100644 (file)
index 0000000..5990ce1
--- /dev/null
@@ -0,0 +1,35 @@
+async function register ({ transcodingManager }) {
+
+  {
+    const builder = () => {
+      return {
+        outputOptions: [
+          '-r 10'
+        ]
+      }
+    }
+
+    transcodingManager.addVODProfile('libx264', 'low-vod', builder)
+  }
+
+  {
+    const builder = (options) => {
+      return {
+        outputOptions: [
+          '-r:' + options.streamNum + ' 5'
+        ]
+      }
+    }
+
+    transcodingManager.addLiveProfile('libx264', 'low-live', builder)
+  }
+}
+
+async function unregister () {
+  return
+}
+
+module.exports = {
+  register,
+  unregister
+}
diff --git a/server/tests/fixtures/peertube-plugin-test-transcoding-one/package.json b/server/tests/fixtures/peertube-plugin-test-transcoding-one/package.json
new file mode 100644 (file)
index 0000000..bedbfa0
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "name": "peertube-plugin-test-transcoding-one",
+  "version": "0.0.1",
+  "description": "Plugin test transcoding 1",
+  "engine": {
+    "peertube": ">=1.3.0"
+  },
+  "keywords": [
+    "peertube",
+    "plugin"
+  ],
+  "homepage": "https://github.com/Chocobozzz/PeerTube",
+  "author": "Chocobozzz",
+  "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
+  "library": "./main.js",
+  "staticDirs": {},
+  "css": [],
+  "clientScripts": [],
+  "translations": {}
+}
diff --git a/server/tests/fixtures/peertube-plugin-test-transcoding-two/main.js b/server/tests/fixtures/peertube-plugin-test-transcoding-two/main.js
new file mode 100644 (file)
index 0000000..a914bce
--- /dev/null
@@ -0,0 +1,38 @@
+async function register ({ transcodingManager }) {
+
+  {
+    const builder = () => {
+      return {
+        outputOptions: []
+      }
+    }
+
+    transcodingManager.addVODProfile('libopus', 'test-vod-profile', builder)
+    transcodingManager.addVODProfile('libvpx-vp9', 'test-vod-profile', builder)
+
+    transcodingManager.addVODEncoderPriority('audio', 'libopus', 1000)
+    transcodingManager.addVODEncoderPriority('video', 'libvpx-vp9', 1000)
+  }
+
+  {
+    const builder = (options) => {
+      return {
+        outputOptions: [
+          '-b:' + options.streamNum + ' 10K'
+        ]
+      }
+    }
+
+    transcodingManager.addLiveProfile('libopus', 'test-live-profile', builder)
+    transcodingManager.addLiveEncoderPriority('audio', 'libopus', 1000)
+  }
+}
+
+async function unregister () {
+  return
+}
+
+module.exports = {
+  register,
+  unregister
+}
diff --git a/server/tests/fixtures/peertube-plugin-test-transcoding-two/package.json b/server/tests/fixtures/peertube-plugin-test-transcoding-two/package.json
new file mode 100644 (file)
index 0000000..34be045
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "name": "peertube-plugin-test-transcoding-two",
+  "version": "0.0.1",
+  "description": "Plugin test transcoding 2",
+  "engine": {
+    "peertube": ">=1.3.0"
+  },
+  "keywords": [
+    "peertube",
+    "plugin"
+  ],
+  "homepage": "https://github.com/Chocobozzz/PeerTube",
+  "author": "Chocobozzz",
+  "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
+  "library": "./main.js",
+  "staticDirs": {},
+  "css": [],
+  "clientScripts": [],
+  "translations": {}
+}
index b870a4055fb1ea1d54dac5b6626a0d1e58982c34..fd7116efdc180899f7baf3dc9fdf9a8914741597 100644 (file)
@@ -1,10 +1,11 @@
 import './action-hooks'
-import './html-injection'
-import './id-and-pass-auth'
 import './external-auth'
 import './filter-hooks'
-import './translations'
-import './video-constants'
+import './html-injection'
+import './id-and-pass-auth'
 import './plugin-helpers'
 import './plugin-router'
 import './plugin-storage'
+import './plugin-transcoding'
+import './translations'
+import './video-constants'
diff --git a/server/tests/plugins/plugin-transcoding.ts b/server/tests/plugins/plugin-transcoding.ts
new file mode 100644 (file)
index 0000000..96ff4c2
--- /dev/null
@@ -0,0 +1,226 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import 'mocha'
+import { expect } from 'chai'
+import { join } from 'path'
+import { getAudioStream, getVideoFileFPS, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils'
+import { ServerConfig, VideoDetails, VideoPrivacy } from '@shared/models'
+import {
+  buildServerDirectory,
+  createLive,
+  getConfig,
+  getPluginTestPath,
+  getVideo,
+  installPlugin,
+  sendRTMPStreamInVideo,
+  setAccessTokensToServers,
+  setDefaultVideoChannel,
+  uninstallPlugin,
+  updateCustomSubConfig,
+  uploadVideoAndGetId,
+  waitJobs,
+  waitUntilLivePublished
+} from '../../../shared/extra-utils'
+import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
+
+async function createLiveWrapper (server: ServerInfo) {
+  const liveAttributes = {
+    name: 'live video',
+    channelId: server.videoChannel.id,
+    privacy: VideoPrivacy.PUBLIC
+  }
+
+  const res = await createLive(server.url, server.accessToken, liveAttributes)
+  return res.body.video.uuid
+}
+
+function updateConf (server: ServerInfo, vodProfile: string, liveProfile: string) {
+  return updateCustomSubConfig(server.url, server.accessToken, {
+    transcoding: {
+      enabled: true,
+      profile: vodProfile,
+      hls: {
+        enabled: true
+      },
+      webtorrent: {
+        enabled: true
+      }
+    },
+    live: {
+      transcoding: {
+        profile: liveProfile,
+        enabled: true
+      }
+    }
+  })
+}
+
+describe('Test transcoding plugins', function () {
+  let server: ServerInfo
+
+  before(async function () {
+    this.timeout(60000)
+
+    server = await flushAndRunServer(1)
+    await setAccessTokensToServers([ server ])
+    await setDefaultVideoChannel([ server ])
+
+    await updateConf(server, 'default', 'default')
+  })
+
+  describe('When using a plugin adding profiles to existing encoders', function () {
+
+    async function checkVideoFPS (uuid: string, type: 'above' | 'below', fps: number) {
+      const res = await getVideo(server.url, uuid)
+      const video = res.body as VideoDetails
+      const files = video.files.concat(...video.streamingPlaylists.map(p => p.files))
+
+      for (const file of files) {
+        if (type === 'above') {
+          expect(file.fps).to.be.above(fps)
+        } else {
+          expect(file.fps).to.be.below(fps)
+        }
+      }
+    }
+
+    async function checkLiveFPS (uuid: string, type: 'above' | 'below', fps: number) {
+      const playlistUrl = `${server.url}/static/streaming-playlists/hls/${uuid}/0.m3u8`
+      const videoFPS = await getVideoFileFPS(playlistUrl)
+
+      if (type === 'above') {
+        expect(videoFPS).to.be.above(fps)
+      } else {
+        expect(videoFPS).to.be.below(fps)
+      }
+    }
+
+    before(async function () {
+      await installPlugin({
+        url: server.url,
+        accessToken: server.accessToken,
+        path: getPluginTestPath('-transcoding-one')
+      })
+    })
+
+    it('Should have the appropriate available profiles', async function () {
+      const res = await getConfig(server.url)
+      const config = res.body as ServerConfig
+
+      expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod' ])
+      expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'low-live' ])
+    })
+
+    it('Should not use the plugin profile if not chosen by the admin', async function () {
+      this.timeout(120000)
+
+      const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
+      await waitJobs([ server ])
+
+      await checkVideoFPS(videoUUID, 'above', 20)
+    })
+
+    it('Should use the vod profile', async function () {
+      this.timeout(120000)
+
+      await updateConf(server, 'low-vod', 'default')
+
+      const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
+      await waitJobs([ server ])
+
+      await checkVideoFPS(videoUUID, 'below', 12)
+    })
+
+    it('Should not use the plugin profile if not chosen by the admin', async function () {
+      this.timeout(120000)
+
+      const liveVideoId = await createLiveWrapper(server)
+
+      await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm')
+      await waitUntilLivePublished(server.url, server.accessToken, liveVideoId)
+      await waitJobs([ server ])
+
+      await checkLiveFPS(liveVideoId, 'above', 20)
+    })
+
+    it('Should use the live profile', async function () {
+      this.timeout(120000)
+
+      await updateConf(server, 'low-vod', 'low-live')
+
+      const liveVideoId = await createLiveWrapper(server)
+
+      await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm')
+      await waitUntilLivePublished(server.url, server.accessToken, liveVideoId)
+      await waitJobs([ server ])
+
+      await checkLiveFPS(liveVideoId, 'below', 12)
+    })
+
+    it('Should default to the default profile if the specified profile does not exist', async function () {
+      this.timeout(120000)
+
+      await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-transcoding-one' })
+
+      const res = await getConfig(server.url)
+      const config = res.body as ServerConfig
+
+      expect(config.transcoding.availableProfiles).to.deep.equal([ 'default' ])
+      expect(config.live.transcoding.availableProfiles).to.deep.equal([ 'default' ])
+
+      const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
+      await waitJobs([ server ])
+
+      await checkVideoFPS(videoUUID, 'above', 20)
+    })
+
+  })
+
+  describe('When using a plugin adding new encoders', function () {
+
+    before(async function () {
+      await installPlugin({
+        url: server.url,
+        accessToken: server.accessToken,
+        path: getPluginTestPath('-transcoding-two')
+      })
+
+      await updateConf(server, 'test-vod-profile', 'test-live-profile')
+    })
+
+    it('Should use the new vod encoders', async function () {
+      this.timeout(240000)
+
+      const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
+      await waitJobs([ server ])
+
+      const path = buildServerDirectory(server, join('videos', videoUUID + '-720.mp4'))
+      const audioProbe = await getAudioStream(path)
+      expect(audioProbe.audioStream.codec_name).to.equal('opus')
+
+      const videoProbe = await getVideoStreamFromFile(path)
+      expect(videoProbe.codec_name).to.equal('vp9')
+    })
+
+    it('Should use the new live encoders', async function () {
+      this.timeout(120000)
+
+      const liveVideoId = await createLiveWrapper(server)
+
+      await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm')
+      await waitUntilLivePublished(server.url, server.accessToken, liveVideoId)
+      await waitJobs([ server ])
+
+      const playlistUrl = `${server.url}/static/streaming-playlists/hls/${liveVideoId}/0.m3u8`
+      const audioProbe = await getAudioStream(playlistUrl)
+      expect(audioProbe.audioStream.codec_name).to.equal('opus')
+
+      const videoProbe = await getVideoStreamFromFile(playlistUrl)
+      expect(videoProbe.codec_name).to.equal('h264')
+    })
+  })
+
+  after(async function () {
+    await cleanupTests([ server ])
+  })
+})
index 2e52d1efdc77a6fe5de192cd0be91acbe60a6f98..ccd5a060d341548cd79546a1b867f09c8330c1ec 100644 (file)
@@ -5,6 +5,7 @@ import {
   PluginPlaylistPrivacyManager,
   PluginSettingsManager,
   PluginStorageManager,
+  PluginTranscodingManager,
   PluginVideoCategoryManager,
   PluginVideoLanguageManager,
   PluginVideoLicenceManager,
@@ -68,6 +69,8 @@ export type RegisterServerOptions = {
   videoPrivacyManager: PluginVideoPrivacyManager
   playlistPrivacyManager: PluginPlaylistPrivacyManager
 
+  transcodingManager: PluginTranscodingManager
+
   registerIdAndPassAuth: (options: RegisterServerAuthPassOptions) => void
   registerExternalAuth: (options: RegisterServerAuthExternalOptions) => RegisterServerAuthExternalResult
   unregisterIdAndPassAuth: (authName: string) => void
index 4e09e041233c6923dff840422995bab3fa3163cf..8998da8b60000960b7dbe9c62dcc4db72c175f39 100644 (file)
@@ -112,6 +112,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
       allowAdditionalExtensions: true,
       allowAudioFiles: true,
       threads: 1,
+      profile: 'default',
       resolutions: {
         '0p': false,
         '240p': false,
@@ -138,6 +139,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
       transcoding: {
         enabled: true,
         threads: 4,
+        profile: 'default',
         resolutions: {
           '240p': true,
           '360p': true,
index 83ed6f583be5c4a78e1014d6d19e68689a165075..96621460aac25bbc40506bf0552462bd11225f25 100644 (file)
@@ -11,6 +11,7 @@ export * from './plugin-package-json.model'
 export * from './plugin-playlist-privacy-manager.model'
 export * from './plugin-settings-manager.model'
 export * from './plugin-storage-manager.model'
+export * from './plugin-transcoding-manager.model'
 export * from './plugin-translation.model'
 export * from './plugin-video-category-manager.model'
 export * from './plugin-video-language-manager.model'
diff --git a/shared/models/plugins/plugin-transcoding-manager.model.ts b/shared/models/plugins/plugin-transcoding-manager.model.ts
new file mode 100644 (file)
index 0000000..ff89687
--- /dev/null
@@ -0,0 +1,11 @@
+import { EncoderOptionsBuilder } from '../videos/video-transcoding.model'
+
+export interface PluginTranscodingManager {
+  addLiveProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder): boolean
+
+  addVODProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder): boolean
+
+  addLiveEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number): void
+
+  addVODEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number): void
+}
index a57237414dd1d2688b7d30cbb881ef06c0c49212..d23b8abef052640fd26275d6d62be0c6716051f0 100644 (file)
@@ -87,6 +87,9 @@ export interface CustomConfig {
     allowAudioFiles: boolean
 
     threads: number
+
+    profile: string
+
     resolutions: ConfigResolutions & { '0p': boolean }
 
     webtorrent: {
@@ -110,6 +113,7 @@ export interface CustomConfig {
     transcoding: {
       enabled: boolean
       threads: number
+      profile: string
       resolutions: ConfigResolutions
     }
   }
index 47d0e623bc467fb156c8a3f8a27105d9f0c4f5cf..efde4ad9dc664bb8488196a60121f2d0a9496dc2 100644 (file)
@@ -96,6 +96,9 @@ export interface ServerConfig {
     }
 
     enabledResolutions: number[]
+
+    profile: string
+    availableProfiles: string[]
   }
 
   live: {
@@ -110,6 +113,9 @@ export interface ServerConfig {
       enabled: boolean
 
       enabledResolutions: number[]
+
+      profile: string
+      availableProfiles: string[]
     }
 
     rtmp: {
index abf144f23f3ebaacf3ae32287e44f32998c417b0..fac3e0b2f803f02eed42826b182662f73617f793 100644 (file)
@@ -34,6 +34,7 @@ export * from './video-state.enum'
 export * from './video-streaming-playlist.model'
 export * from './video-streaming-playlist.type'
 
+export * from './video-transcoding.model'
 export * from './video-transcoding-fps.model'
 
 export * from './video-update.model'
diff --git a/shared/models/videos/video-transcoding.model.ts b/shared/models/videos/video-transcoding.model.ts
new file mode 100644 (file)
index 0000000..06b555c
--- /dev/null
@@ -0,0 +1,48 @@
+import { VideoResolution } from './video-resolution.enum'
+
+// Types used by plugins and ffmpeg-utils
+
+export type EncoderOptionsBuilder = (params: {
+  input: string
+  resolution: VideoResolution
+  fps?: number
+  streamNum?: number
+}) => Promise<EncoderOptions> | EncoderOptions
+
+export interface EncoderOptions {
+  copy?: boolean // Copy stream? Default to false
+
+  outputOptions: string[]
+}
+
+// All our encoders
+
+export interface EncoderProfile <T> {
+  [ profile: string ]: T
+
+  default: T
+}
+
+export type AvailableEncoders = {
+  available: {
+    live: {
+      [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
+    }
+
+    vod: {
+      [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
+    }
+  }
+
+  encodersToTry: {
+    vod: {
+      video: string[]
+      audio: string[]
+    }
+
+    live: {
+      video: string[]
+      audio: string[]
+    }
+  }
+}