aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/config.ts11
-rw-r--r--server/helpers/ffmpeg-utils.ts88
-rw-r--r--server/helpers/logger.ts8
-rw-r--r--server/initializers/checker-before-init.ts29
-rw-r--r--server/initializers/config.ts2
-rw-r--r--server/lib/live-manager.ts2
-rw-r--r--server/lib/plugins/plugin-helpers-builder.ts (renamed from server/lib/plugins/plugin-helpers.ts)0
-rw-r--r--server/lib/plugins/plugin-manager.ts35
-rw-r--r--server/lib/plugins/register-helpers.ts (renamed from server/lib/plugins/register-helpers-store.ts)91
-rw-r--r--server/lib/video-transcoding-profiles.ts119
-rw-r--r--server/lib/video-transcoding.ts10
-rw-r--r--server/middlewares/validators/plugins.ts4
-rw-r--r--server/models/video/video.ts4
-rw-r--r--server/tests/api/check-params/config.ts2
-rw-r--r--server/tests/api/server/config.ts6
-rw-r--r--server/tests/api/videos/video-transcoder.ts4
-rw-r--r--server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js35
-rw-r--r--server/tests/fixtures/peertube-plugin-test-transcoding-one/package.json20
-rw-r--r--server/tests/fixtures/peertube-plugin-test-transcoding-two/main.js38
-rw-r--r--server/tests/fixtures/peertube-plugin-test-transcoding-two/package.json20
-rw-r--r--server/tests/plugins/index.ts9
-rw-r--r--server/tests/plugins/plugin-transcoding.ts226
-rw-r--r--server/types/plugins/register-server-option.model.ts3
23 files changed, 642 insertions, 124 deletions
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 45c03be24..7fda06a87 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -18,6 +18,7 @@ import { PluginManager } from '../../lib/plugins/plugin-manager'
18import { getThemeOrDefault } from '../../lib/plugins/theme-utils' 18import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
19import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' 19import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
20import { customConfigUpdateValidator } from '../../middlewares/validators/config' 20import { customConfigUpdateValidator } from '../../middlewares/validators/config'
21import { VideoTranscodingProfilesManager } from '@server/lib/video-transcoding-profiles'
21 22
22const configRouter = express.Router() 23const configRouter = express.Router()
23 24
@@ -114,7 +115,9 @@ async function getConfig (req: express.Request, res: express.Response) {
114 webtorrent: { 115 webtorrent: {
115 enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED 116 enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
116 }, 117 },
117 enabledResolutions: getEnabledResolutions('vod') 118 enabledResolutions: getEnabledResolutions('vod'),
119 profile: CONFIG.TRANSCODING.PROFILE,
120 availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('vod')
118 }, 121 },
119 live: { 122 live: {
120 enabled: CONFIG.LIVE.ENABLED, 123 enabled: CONFIG.LIVE.ENABLED,
@@ -126,7 +129,9 @@ async function getConfig (req: express.Request, res: express.Response) {
126 129
127 transcoding: { 130 transcoding: {
128 enabled: CONFIG.LIVE.TRANSCODING.ENABLED, 131 enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
129 enabledResolutions: getEnabledResolutions('live') 132 enabledResolutions: getEnabledResolutions('live'),
133 profile: CONFIG.LIVE.TRANSCODING.PROFILE,
134 availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('live')
130 }, 135 },
131 136
132 rtmp: { 137 rtmp: {
@@ -412,6 +417,7 @@ function customConfig (): CustomConfig {
412 allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS, 417 allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
413 allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES, 418 allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES,
414 threads: CONFIG.TRANSCODING.THREADS, 419 threads: CONFIG.TRANSCODING.THREADS,
420 profile: CONFIG.TRANSCODING.PROFILE,
415 resolutions: { 421 resolutions: {
416 '0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'], 422 '0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'],
417 '240p': CONFIG.TRANSCODING.RESOLUTIONS['240p'], 423 '240p': CONFIG.TRANSCODING.RESOLUTIONS['240p'],
@@ -438,6 +444,7 @@ function customConfig (): CustomConfig {
438 transcoding: { 444 transcoding: {
439 enabled: CONFIG.LIVE.TRANSCODING.ENABLED, 445 enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
440 threads: CONFIG.LIVE.TRANSCODING.THREADS, 446 threads: CONFIG.LIVE.TRANSCODING.THREADS,
447 profile: CONFIG.LIVE.TRANSCODING.PROFILE,
441 resolutions: { 448 resolutions: {
442 '240p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['240p'], 449 '240p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['240p'],
443 '360p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['360p'], 450 '360p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['360p'],
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 7d46130ec..33c625c9e 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -3,9 +3,9 @@ import * as ffmpeg from 'fluent-ffmpeg'
3import { readFile, remove, writeFile } from 'fs-extra' 3import { readFile, remove, writeFile } from 'fs-extra'
4import { dirname, join } from 'path' 4import { dirname, join } from 'path'
5import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' 5import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants'
6import { VideoResolution } from '../../shared/models/videos' 6import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos'
7import { checkFFmpegEncoders } from '../initializers/checker-before-init'
8import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
8import { promisify0 } from './core-utils'
9import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' 9import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils'
10import { processImage } from './image-utils' 10import { processImage } from './image-utils'
11import { logger } from './logger' 11import { logger } from './logger'
@@ -21,46 +21,45 @@ import { logger } from './logger'
21// Encoder options 21// Encoder options
22// --------------------------------------------------------------------------- 22// ---------------------------------------------------------------------------
23 23
24// Options builders 24type StreamType = 'audio' | 'video'
25
26export type EncoderOptionsBuilder = (params: {
27 input: string
28 resolution: VideoResolution
29 fps?: number
30 streamNum?: number
31}) => Promise<EncoderOptions> | EncoderOptions
32 25
33// Options types 26// ---------------------------------------------------------------------------
27// Encoders support
28// ---------------------------------------------------------------------------
34 29
35export interface EncoderOptions { 30// Detect supported encoders by ffmpeg
36 copy?: boolean 31let supportedEncoders: Map<string, boolean>
37 outputOptions: string[] 32async function checkFFmpegEncoders (peertubeAvailableEncoders: AvailableEncoders): Promise<Map<string, boolean>> {
38} 33 if (supportedEncoders !== undefined) {
34 return supportedEncoders
35 }
39 36
40// All our encoders 37 const getAvailableEncodersPromise = promisify0(ffmpeg.getAvailableEncoders)
38 const availableFFmpegEncoders = await getAvailableEncodersPromise()
41 39
42export interface EncoderProfile <T> { 40 const searchEncoders = new Set<string>()
43 [ profile: string ]: T 41 for (const type of [ 'live', 'vod' ]) {
42 for (const streamType of [ 'audio', 'video' ]) {
43 for (const encoder of peertubeAvailableEncoders.encodersToTry[type][streamType]) {
44 searchEncoders.add(encoder)
45 }
46 }
47 }
44 48
45 default: T 49 supportedEncoders = new Map<string, boolean>()
46}
47 50
48export type AvailableEncoders = { 51 for (const searchEncoder of searchEncoders) {
49 live: { 52 supportedEncoders.set(searchEncoder, availableFFmpegEncoders[searchEncoder] !== undefined)
50 [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
51 } 53 }
52 54
53 vod: { 55 logger.info('Built supported ffmpeg encoders.', { supportedEncoders, searchEncoders })
54 [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
55 }
56 56
57 encodersToTry: { 57 return supportedEncoders
58 video: string[]
59 audio: string[]
60 }
61} 58}
62 59
63type StreamType = 'audio' | 'video' 60function resetSupportedEncoders () {
61 supportedEncoders = undefined
62}
64 63
65// --------------------------------------------------------------------------- 64// ---------------------------------------------------------------------------
66// Image manipulation 65// Image manipulation
@@ -275,7 +274,7 @@ async function getLiveTranscodingCommand (options: {
275 274
276 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i }) 275 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
277 276
278 logger.debug('Apply ffmpeg live video params from %s.', builderResult.encoder, builderResult) 277 logger.debug('Apply ffmpeg live video params from %s using %s profile.', builderResult.encoder, profile, builderResult)
279 278
280 command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`) 279 command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`)
281 command.addOutputOptions(builderResult.result.outputOptions) 280 command.addOutputOptions(builderResult.result.outputOptions)
@@ -292,7 +291,7 @@ async function getLiveTranscodingCommand (options: {
292 291
293 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i }) 292 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
294 293
295 logger.debug('Apply ffmpeg live audio params from %s.', builderResult.encoder, builderResult) 294 logger.debug('Apply ffmpeg live audio params from %s using %s profile.', builderResult.encoder, profile, builderResult)
296 295
297 command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`) 296 command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`)
298 command.addOutputOptions(builderResult.result.outputOptions) 297 command.addOutputOptions(builderResult.result.outputOptions)
@@ -513,11 +512,19 @@ async function getEncoderBuilderResult (options: {
513}) { 512}) {
514 const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options 513 const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options
515 514
516 const encodersToTry = availableEncoders.encodersToTry[streamType] 515 const encodersToTry = availableEncoders.encodersToTry[videoType][streamType]
517 const encoders = availableEncoders[videoType] 516 const encoders = availableEncoders.available[videoType]
518 517
519 for (const encoder of encodersToTry) { 518 for (const encoder of encodersToTry) {
520 if (!(await checkFFmpegEncoders()).get(encoder) || !encoders[encoder]) continue 519 if (!(await checkFFmpegEncoders(availableEncoders)).get(encoder)) {
520 logger.debug('Encoder %s not available in ffmpeg, skipping.', encoder)
521 continue
522 }
523
524 if (!encoders[encoder]) {
525 logger.debug('Encoder %s not available in peertube encoders, skipping.', encoder)
526 continue
527 }
521 528
522 // An object containing available profiles for this encoder 529 // An object containing available profiles for this encoder
523 const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = encoders[encoder] 530 const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = encoders[encoder]
@@ -567,7 +574,7 @@ async function presetVideo (
567 574
568 if (!parsedAudio.audioStream) { 575 if (!parsedAudio.audioStream) {
569 localCommand = localCommand.noAudio() 576 localCommand = localCommand.noAudio()
570 streamsToProcess = [ 'audio' ] 577 streamsToProcess = [ 'video' ]
571 } 578 }
572 579
573 for (const streamType of streamsToProcess) { 580 for (const streamType of streamsToProcess) {
@@ -587,7 +594,10 @@ async function presetVideo (
587 throw new Error('No available encoder found for stream ' + streamType) 594 throw new Error('No available encoder found for stream ' + streamType)
588 } 595 }
589 596
590 logger.debug('Apply ffmpeg params from %s.', builderResult.encoder, builderResult) 597 logger.debug(
598 'Apply ffmpeg params from %s for %s stream of input %s using %s profile.',
599 builderResult.encoder, streamType, input, profile, builderResult
600 )
591 601
592 if (streamType === 'video') { 602 if (streamType === 'video') {
593 localCommand.videoCodec(builderResult.encoder) 603 localCommand.videoCodec(builderResult.encoder)
@@ -679,6 +689,8 @@ export {
679 transcode, 689 transcode,
680 runCommand, 690 runCommand,
681 691
692 resetSupportedEncoders,
693
682 // builders 694 // builders
683 buildx264VODCommand 695 buildx264VODCommand
684} 696}
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts
index 05ec4a6b9..746b2e0a6 100644
--- a/server/helpers/logger.ts
+++ b/server/helpers/logger.ts
@@ -27,6 +27,14 @@ function getLoggerReplacer () {
27 seen.add(value) 27 seen.add(value)
28 } 28 }
29 29
30 if (value instanceof Set) {
31 return Array.from(value)
32 }
33
34 if (value instanceof Map) {
35 return Array.from(value.entries())
36 }
37
30 if (value instanceof Error) { 38 if (value instanceof Error) {
31 const error = {} 39 const error = {}
32 40
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 7945e8586..9c4e0048a 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -97,34 +97,6 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) {
97 throw new Error('Unavailable encode codec ' + codec + ' in FFmpeg') 97 throw new Error('Unavailable encode codec ' + codec + ' in FFmpeg')
98 } 98 }
99 } 99 }
100
101 return checkFFmpegEncoders()
102}
103
104// Detect supported encoders by ffmpeg
105let supportedEncoders: Map<string, boolean>
106async function checkFFmpegEncoders (): Promise<Map<string, boolean>> {
107 if (supportedEncoders !== undefined) {
108 return supportedEncoders
109 }
110
111 const Ffmpeg = require('fluent-ffmpeg')
112 const getAvailableEncodersPromise = promisify0(Ffmpeg.getAvailableEncoders)
113 const availableEncoders = await getAvailableEncodersPromise()
114
115 const searchEncoders = [
116 'aac',
117 'libfdk_aac',
118 'libx264'
119 ]
120
121 supportedEncoders = new Map<string, boolean>()
122
123 for (const searchEncoder of searchEncoders) {
124 supportedEncoders.set(searchEncoder, availableEncoders[searchEncoder] !== undefined)
125 }
126
127 return supportedEncoders
128} 100}
129 101
130function checkNodeVersion () { 102function checkNodeVersion () {
@@ -143,7 +115,6 @@ function checkNodeVersion () {
143 115
144export { 116export {
145 checkFFmpeg, 117 checkFFmpeg,
146 checkFFmpegEncoders,
147 checkMissedConfig, 118 checkMissedConfig,
148 checkNodeVersion 119 checkNodeVersion
149} 120}
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index fc4a8b709..7322b89e2 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -188,6 +188,7 @@ const CONFIG = {
188 get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') }, 188 get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') },
189 get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') }, 189 get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') },
190 get THREADS () { return config.get<number>('transcoding.threads') }, 190 get THREADS () { return config.get<number>('transcoding.threads') },
191 get PROFILE () { return config.get<string>('transcoding.profile') },
191 RESOLUTIONS: { 192 RESOLUTIONS: {
192 get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') }, 193 get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') },
193 get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, 194 get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') },
@@ -221,6 +222,7 @@ const CONFIG = {
221 TRANSCODING: { 222 TRANSCODING: {
222 get ENABLED () { return config.get<boolean>('live.transcoding.enabled') }, 223 get ENABLED () { return config.get<boolean>('live.transcoding.enabled') },
223 get THREADS () { return config.get<number>('live.transcoding.threads') }, 224 get THREADS () { return config.get<number>('live.transcoding.threads') },
225 get PROFILE () { return config.get<string>('live.transcoding.profile') },
224 226
225 RESOLUTIONS: { 227 RESOLUTIONS: {
226 get '240p' () { return config.get<boolean>('live.transcoding.resolutions.240p') }, 228 get '240p' () { return config.get<boolean>('live.transcoding.resolutions.240p') },
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts
index c8e5bcb77..9f17b8820 100644
--- a/server/lib/live-manager.ts
+++ b/server/lib/live-manager.ts
@@ -338,7 +338,7 @@ class LiveManager {
338 resolutions: allResolutions, 338 resolutions: allResolutions,
339 fps, 339 fps,
340 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), 340 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
341 profile: 'default' 341 profile: CONFIG.LIVE.TRANSCODING.PROFILE
342 }) 342 })
343 : getLiveMuxingCommand(rtmpUrl, outPath) 343 : getLiveMuxingCommand(rtmpUrl, outPath)
344 344
diff --git a/server/lib/plugins/plugin-helpers.ts b/server/lib/plugins/plugin-helpers-builder.ts
index 39773f693..39773f693 100644
--- a/server/lib/plugins/plugin-helpers.ts
+++ b/server/lib/plugins/plugin-helpers-builder.ts
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index 8e7491257..c19b40135 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -20,7 +20,7 @@ import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants'
20import { PluginModel } from '../../models/server/plugin' 20import { PluginModel } from '../../models/server/plugin'
21import { PluginLibrary, RegisterServerAuthExternalOptions, RegisterServerAuthPassOptions, RegisterServerOptions } from '../../types/plugins' 21import { PluginLibrary, RegisterServerAuthExternalOptions, RegisterServerAuthPassOptions, RegisterServerOptions } from '../../types/plugins'
22import { ClientHtml } from '../client-html' 22import { ClientHtml } from '../client-html'
23import { RegisterHelpersStore } from './register-helpers-store' 23import { RegisterHelpers } from './register-helpers'
24import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' 24import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
25 25
26export interface RegisteredPlugin { 26export interface RegisteredPlugin {
@@ -40,7 +40,7 @@ export interface RegisteredPlugin {
40 css: string[] 40 css: string[]
41 41
42 // Only if this is a plugin 42 // Only if this is a plugin
43 registerHelpersStore?: RegisterHelpersStore 43 registerHelpers?: RegisterHelpers
44 unregister?: Function 44 unregister?: Function
45} 45}
46 46
@@ -109,7 +109,7 @@ export class PluginManager implements ServerHook {
109 npmName: p.npmName, 109 npmName: p.npmName,
110 name: p.name, 110 name: p.name,
111 version: p.version, 111 version: p.version,
112 idAndPassAuths: p.registerHelpersStore.getIdAndPassAuths() 112 idAndPassAuths: p.registerHelpers.getIdAndPassAuths()
113 })) 113 }))
114 .filter(v => v.idAndPassAuths.length !== 0) 114 .filter(v => v.idAndPassAuths.length !== 0)
115 } 115 }
@@ -120,7 +120,7 @@ export class PluginManager implements ServerHook {
120 npmName: p.npmName, 120 npmName: p.npmName,
121 name: p.name, 121 name: p.name,
122 version: p.version, 122 version: p.version,
123 externalAuths: p.registerHelpersStore.getExternalAuths() 123 externalAuths: p.registerHelpers.getExternalAuths()
124 })) 124 }))
125 .filter(v => v.externalAuths.length !== 0) 125 .filter(v => v.externalAuths.length !== 0)
126 } 126 }
@@ -129,14 +129,14 @@ export class PluginManager implements ServerHook {
129 const result = this.getRegisteredPluginOrTheme(npmName) 129 const result = this.getRegisteredPluginOrTheme(npmName)
130 if (!result || result.type !== PluginType.PLUGIN) return [] 130 if (!result || result.type !== PluginType.PLUGIN) return []
131 131
132 return result.registerHelpersStore.getSettings() 132 return result.registerHelpers.getSettings()
133 } 133 }
134 134
135 getRouter (npmName: string) { 135 getRouter (npmName: string) {
136 const result = this.getRegisteredPluginOrTheme(npmName) 136 const result = this.getRegisteredPluginOrTheme(npmName)
137 if (!result || result.type !== PluginType.PLUGIN) return null 137 if (!result || result.type !== PluginType.PLUGIN) return null
138 138
139 return result.registerHelpersStore.getRouter() 139 return result.registerHelpers.getRouter()
140 } 140 }
141 141
142 getTranslations (locale: string) { 142 getTranslations (locale: string) {
@@ -194,7 +194,7 @@ export class PluginManager implements ServerHook {
194 logger.error('Cannot find plugin %s to call on settings changed.', name) 194 logger.error('Cannot find plugin %s to call on settings changed.', name)
195 } 195 }
196 196
197 for (const cb of registered.registerHelpersStore.getOnSettingsChangedCallbacks()) { 197 for (const cb of registered.registerHelpers.getOnSettingsChangedCallbacks()) {
198 try { 198 try {
199 cb(settings) 199 cb(settings)
200 } catch (err) { 200 } catch (err) {
@@ -268,8 +268,9 @@ export class PluginManager implements ServerHook {
268 this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName) 268 this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName)
269 } 269 }
270 270
271 const store = plugin.registerHelpersStore 271 const store = plugin.registerHelpers
272 store.reinitVideoConstants(plugin.npmName) 272 store.reinitVideoConstants(plugin.npmName)
273 store.reinitTranscodingProfilesAndEncoders(plugin.npmName)
273 274
274 logger.info('Regenerating registered plugin CSS to global file.') 275 logger.info('Regenerating registered plugin CSS to global file.')
275 await this.regeneratePluginGlobalCSS() 276 await this.regeneratePluginGlobalCSS()
@@ -375,11 +376,11 @@ export class PluginManager implements ServerHook {
375 this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type) 376 this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type)
376 377
377 let library: PluginLibrary 378 let library: PluginLibrary
378 let registerHelpersStore: RegisterHelpersStore 379 let registerHelpers: RegisterHelpers
379 if (plugin.type === PluginType.PLUGIN) { 380 if (plugin.type === PluginType.PLUGIN) {
380 const result = await this.registerPlugin(plugin, pluginPath, packageJSON) 381 const result = await this.registerPlugin(plugin, pluginPath, packageJSON)
381 library = result.library 382 library = result.library
382 registerHelpersStore = result.registerStore 383 registerHelpers = result.registerStore
383 } 384 }
384 385
385 const clientScripts: { [id: string]: ClientScript } = {} 386 const clientScripts: { [id: string]: ClientScript } = {}
@@ -398,7 +399,7 @@ export class PluginManager implements ServerHook {
398 staticDirs: packageJSON.staticDirs, 399 staticDirs: packageJSON.staticDirs,
399 clientScripts, 400 clientScripts,
400 css: packageJSON.css, 401 css: packageJSON.css,
401 registerHelpersStore: registerHelpersStore || undefined, 402 registerHelpers: registerHelpers || undefined,
402 unregister: library ? library.unregister : undefined 403 unregister: library ? library.unregister : undefined
403 } 404 }
404 405
@@ -512,8 +513,8 @@ export class PluginManager implements ServerHook {
512 const plugin = this.getRegisteredPluginOrTheme(npmName) 513 const plugin = this.getRegisteredPluginOrTheme(npmName)
513 if (!plugin || plugin.type !== PluginType.PLUGIN) return null 514 if (!plugin || plugin.type !== PluginType.PLUGIN) return null
514 515
515 let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpersStore.getIdAndPassAuths() 516 let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpers.getIdAndPassAuths()
516 auths = auths.concat(plugin.registerHelpersStore.getExternalAuths()) 517 auths = auths.concat(plugin.registerHelpers.getExternalAuths())
517 518
518 return auths.find(a => a.authName === authName) 519 return auths.find(a => a.authName === authName)
519 } 520 }
@@ -538,7 +539,7 @@ export class PluginManager implements ServerHook {
538 private getRegisterHelpers ( 539 private getRegisterHelpers (
539 npmName: string, 540 npmName: string,
540 plugin: PluginModel 541 plugin: PluginModel
541 ): { registerStore: RegisterHelpersStore, registerOptions: RegisterServerOptions } { 542 ): { registerStore: RegisterHelpers, registerOptions: RegisterServerOptions } {
542 const onHookAdded = (options: RegisterServerHookOptions) => { 543 const onHookAdded = (options: RegisterServerHookOptions) => {
543 if (!this.hooks[options.target]) this.hooks[options.target] = [] 544 if (!this.hooks[options.target]) this.hooks[options.target] = []
544 545
@@ -550,11 +551,11 @@ export class PluginManager implements ServerHook {
550 }) 551 })
551 } 552 }
552 553
553 const registerHelpersStore = new RegisterHelpersStore(npmName, plugin, onHookAdded.bind(this)) 554 const registerHelpers = new RegisterHelpers(npmName, plugin, onHookAdded.bind(this))
554 555
555 return { 556 return {
556 registerStore: registerHelpersStore, 557 registerStore: registerHelpers,
557 registerOptions: registerHelpersStore.buildRegisterHelpers() 558 registerOptions: registerHelpers.buildRegisterHelpers()
558 } 559 }
559 } 560 }
560 561
diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers.ts
index c73079302..3a38a4835 100644
--- a/server/lib/plugins/register-helpers-store.ts
+++ b/server/lib/plugins/register-helpers.ts
@@ -17,6 +17,7 @@ import {
17 RegisterServerOptions 17 RegisterServerOptions
18} from '@server/types/plugins' 18} from '@server/types/plugins'
19import { 19import {
20 EncoderOptionsBuilder,
20 PluginPlaylistPrivacyManager, 21 PluginPlaylistPrivacyManager,
21 PluginSettingsManager, 22 PluginSettingsManager,
22 PluginStorageManager, 23 PluginStorageManager,
@@ -28,7 +29,8 @@ import {
28 RegisterServerSettingOptions 29 RegisterServerSettingOptions
29} from '@shared/models' 30} from '@shared/models'
30import { serverHookObject } from '@shared/models/plugins/server-hook.model' 31import { serverHookObject } from '@shared/models/plugins/server-hook.model'
31import { buildPluginHelpers } from './plugin-helpers' 32import { VideoTranscodingProfilesManager } from '../video-transcoding-profiles'
33import { buildPluginHelpers } from './plugin-helpers-builder'
32 34
33type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' 35type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
34type VideoConstant = { [key in number | string]: string } 36type VideoConstant = { [key in number | string]: string }
@@ -40,7 +42,7 @@ type UpdatedVideoConstant = {
40 } 42 }
41} 43}
42 44
43export class RegisterHelpersStore { 45export class RegisterHelpers {
44 private readonly updatedVideoConstants: UpdatedVideoConstant = { 46 private readonly updatedVideoConstants: UpdatedVideoConstant = {
45 playlistPrivacy: { added: [], deleted: [] }, 47 playlistPrivacy: { added: [], deleted: [] },
46 privacy: { added: [], deleted: [] }, 48 privacy: { added: [], deleted: [] },
@@ -49,6 +51,23 @@ export class RegisterHelpersStore {
49 category: { added: [], deleted: [] } 51 category: { added: [], deleted: [] }
50 } 52 }
51 53
54 private readonly transcodingProfiles: {
55 [ npmName: string ]: {
56 type: 'vod' | 'live'
57 encoder: string
58 profile: string
59 }[]
60 } = {}
61
62 private readonly transcodingEncoders: {
63 [ npmName: string ]: {
64 type: 'vod' | 'live'
65 streamType: 'audio' | 'video'
66 encoder: string
67 priority: number
68 }[]
69 } = {}
70
52 private readonly settings: RegisterServerSettingOptions[] = [] 71 private readonly settings: RegisterServerSettingOptions[] = []
53 72
54 private idAndPassAuths: RegisterServerAuthPassOptions[] = [] 73 private idAndPassAuths: RegisterServerAuthPassOptions[] = []
@@ -83,6 +102,8 @@ export class RegisterHelpersStore {
83 const videoPrivacyManager = this.buildVideoPrivacyManager() 102 const videoPrivacyManager = this.buildVideoPrivacyManager()
84 const playlistPrivacyManager = this.buildPlaylistPrivacyManager() 103 const playlistPrivacyManager = this.buildPlaylistPrivacyManager()
85 104
105 const transcodingManager = this.buildTranscodingManager()
106
86 const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth() 107 const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth()
87 const registerExternalAuth = this.buildRegisterExternalAuth() 108 const registerExternalAuth = this.buildRegisterExternalAuth()
88 const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth() 109 const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth()
@@ -106,6 +127,8 @@ export class RegisterHelpersStore {
106 videoPrivacyManager, 127 videoPrivacyManager,
107 playlistPrivacyManager, 128 playlistPrivacyManager,
108 129
130 transcodingManager,
131
109 registerIdAndPassAuth, 132 registerIdAndPassAuth,
110 registerExternalAuth, 133 registerExternalAuth,
111 unregisterIdAndPassAuth, 134 unregisterIdAndPassAuth,
@@ -141,6 +164,22 @@ export class RegisterHelpersStore {
141 } 164 }
142 } 165 }
143 166
167 reinitTranscodingProfilesAndEncoders (npmName: string) {
168 const profiles = this.transcodingProfiles[npmName]
169 if (Array.isArray(profiles)) {
170 for (const profile of profiles) {
171 VideoTranscodingProfilesManager.Instance.removeProfile(profile)
172 }
173 }
174
175 const encoders = this.transcodingEncoders[npmName]
176 if (Array.isArray(encoders)) {
177 for (const o of encoders) {
178 VideoTranscodingProfilesManager.Instance.removeEncoderPriority(o.type, o.streamType, o.encoder, o.priority)
179 }
180 }
181 }
182
144 getSettings () { 183 getSettings () {
145 return this.settings 184 return this.settings
146 } 185 }
@@ -354,4 +393,52 @@ export class RegisterHelpersStore {
354 393
355 return true 394 return true
356 } 395 }
396
397 private buildTranscodingManager () {
398 const self = this
399
400 function addProfile (type: 'live' | 'vod', encoder: string, profile: string, builder: EncoderOptionsBuilder) {
401 if (profile === 'default') {
402 logger.error('A plugin cannot add a default live transcoding profile')
403 return false
404 }
405
406 VideoTranscodingProfilesManager.Instance.addProfile({
407 type,
408 encoder,
409 profile,
410 builder
411 })
412
413 if (!self.transcodingProfiles[self.npmName]) self.transcodingProfiles[self.npmName] = []
414 self.transcodingProfiles[self.npmName].push({ type, encoder, profile })
415
416 return true
417 }
418
419 function addEncoderPriority (type: 'live' | 'vod', streamType: 'audio' | 'video', encoder: string, priority: number) {
420 VideoTranscodingProfilesManager.Instance.addEncoderPriority(type, streamType, encoder, priority)
421
422 if (!self.transcodingEncoders[self.npmName]) self.transcodingEncoders[self.npmName] = []
423 self.transcodingEncoders[self.npmName].push({ type, streamType, encoder, priority })
424 }
425
426 return {
427 addLiveProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder) {
428 return addProfile('live', encoder, profile, builder)
429 },
430
431 addVODProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder) {
432 return addProfile('vod', encoder, profile, builder)
433 },
434
435 addLiveEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number) {
436 return addEncoderPriority('live', streamType, encoder, priority)
437 },
438
439 addVODEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number) {
440 return addEncoderPriority('vod', streamType, encoder, priority)
441 }
442 }
443 }
357} 444}
diff --git a/server/lib/video-transcoding-profiles.ts b/server/lib/video-transcoding-profiles.ts
index bbe556e75..76d38b6ca 100644
--- a/server/lib/video-transcoding-profiles.ts
+++ b/server/lib/video-transcoding-profiles.ts
@@ -1,6 +1,6 @@
1import { logger } from '@server/helpers/logger' 1import { logger } from '@server/helpers/logger'
2import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' 2import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../shared/models/videos'
3import { AvailableEncoders, buildStreamSuffix, EncoderOptionsBuilder } from '../helpers/ffmpeg-utils' 3import { buildStreamSuffix, resetSupportedEncoders } from '../helpers/ffmpeg-utils'
4import { 4import {
5 canDoQuickAudioTranscode, 5 canDoQuickAudioTranscode,
6 ffprobePromise, 6 ffprobePromise,
@@ -84,16 +84,8 @@ class VideoTranscodingProfilesManager {
84 84
85 // 1 === less priority 85 // 1 === less priority
86 private readonly encodersPriorities = { 86 private readonly encodersPriorities = {
87 video: [ 87 vod: this.buildDefaultEncodersPriorities(),
88 { name: 'libx264', priority: 100 } 88 live: this.buildDefaultEncodersPriorities()
89 ],
90
91 // Try the first one, if not available try the second one etc
92 audio: [
93 // we favor VBR, if a good AAC encoder is available
94 { name: 'libfdk_aac', priority: 200 },
95 { name: 'aac', priority: 100 }
96 ]
97 } 89 }
98 90
99 private readonly availableEncoders = { 91 private readonly availableEncoders = {
@@ -118,25 +110,77 @@ class VideoTranscodingProfilesManager {
118 } 110 }
119 } 111 }
120 112
121 private constructor () { 113 private availableProfiles = {
114 vod: [] as string[],
115 live: [] as string[]
116 }
122 117
118 private constructor () {
119 this.buildAvailableProfiles()
123 } 120 }
124 121
125 getAvailableEncoders (): AvailableEncoders { 122 getAvailableEncoders (): AvailableEncoders {
126 const encodersToTry = { 123 return {
127 video: this.getEncodersByPriority('video'), 124 available: this.availableEncoders,
128 audio: this.getEncodersByPriority('audio') 125 encodersToTry: {
126 vod: {
127 video: this.getEncodersByPriority('vod', 'video'),
128 audio: this.getEncodersByPriority('vod', 'audio')
129 },
130 live: {
131 video: this.getEncodersByPriority('live', 'video'),
132 audio: this.getEncodersByPriority('live', 'audio')
133 }
134 }
129 } 135 }
130
131 return Object.assign({}, this.availableEncoders, { encodersToTry })
132 } 136 }
133 137
134 getAvailableProfiles (type: 'vod' | 'live') { 138 getAvailableProfiles (type: 'vod' | 'live') {
135 return this.availableEncoders[type] 139 return this.availableProfiles[type]
140 }
141
142 addProfile (options: {
143 type: 'vod' | 'live'
144 encoder: string
145 profile: string
146 builder: EncoderOptionsBuilder
147 }) {
148 const { type, encoder, profile, builder } = options
149
150 const encoders = this.availableEncoders[type]
151
152 if (!encoders[encoder]) encoders[encoder] = {}
153 encoders[encoder][profile] = builder
154
155 this.buildAvailableProfiles()
156 }
157
158 removeProfile (options: {
159 type: 'vod' | 'live'
160 encoder: string
161 profile: string
162 }) {
163 const { type, encoder, profile } = options
164
165 delete this.availableEncoders[type][encoder][profile]
166 this.buildAvailableProfiles()
136 } 167 }
137 168
138 private getEncodersByPriority (type: 'video' | 'audio') { 169 addEncoderPriority (type: 'vod' | 'live', streamType: 'audio' | 'video', encoder: string, priority: number) {
139 return this.encodersPriorities[type] 170 this.encodersPriorities[type][streamType].push({ name: encoder, priority })
171
172 resetSupportedEncoders()
173 }
174
175 removeEncoderPriority (type: 'vod' | 'live', streamType: 'audio' | 'video', encoder: string, priority: number) {
176 this.encodersPriorities[type][streamType] = this.encodersPriorities[type][streamType]
177 .filter(o => o.name !== encoder && o.priority !== priority)
178
179 resetSupportedEncoders()
180 }
181
182 private getEncodersByPriority (type: 'vod' | 'live', streamType: 'audio' | 'video') {
183 return this.encodersPriorities[type][streamType]
140 .sort((e1, e2) => { 184 .sort((e1, e2) => {
141 if (e1.priority > e2.priority) return -1 185 if (e1.priority > e2.priority) return -1
142 else if (e1.priority === e2.priority) return 0 186 else if (e1.priority === e2.priority) return 0
@@ -146,6 +190,39 @@ class VideoTranscodingProfilesManager {
146 .map(e => e.name) 190 .map(e => e.name)
147 } 191 }
148 192
193 private buildAvailableProfiles () {
194 for (const type of [ 'vod', 'live' ]) {
195 const result = new Set()
196
197 const encoders = this.availableEncoders[type]
198
199 for (const encoderName of Object.keys(encoders)) {
200 for (const profile of Object.keys(encoders[encoderName])) {
201 result.add(profile)
202 }
203 }
204
205 this.availableProfiles[type] = Array.from(result)
206 }
207
208 logger.debug('Available transcoding profiles built.', { availableProfiles: this.availableProfiles })
209 }
210
211 private buildDefaultEncodersPriorities () {
212 return {
213 video: [
214 { name: 'libx264', priority: 100 }
215 ],
216
217 // Try the first one, if not available try the second one etc
218 audio: [
219 // we favor VBR, if a good AAC encoder is available
220 { name: 'libfdk_aac', priority: 200 },
221 { name: 'aac', priority: 100 }
222 ]
223 }
224 }
225
149 static get Instance () { 226 static get Instance () {
150 return this.instance || (this.instance = new this()) 227 return this.instance || (this.instance = new this())
151 } 228 }
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index c4b3425d1..37a4f3019 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -42,7 +42,7 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile:
42 outputPath: videoTranscodedPath, 42 outputPath: videoTranscodedPath,
43 43
44 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), 44 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
45 profile: 'default', 45 profile: CONFIG.TRANSCODING.PROFILE,
46 46
47 resolution: inputVideoFile.resolution, 47 resolution: inputVideoFile.resolution,
48 48
@@ -96,7 +96,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti
96 outputPath: videoTranscodedPath, 96 outputPath: videoTranscodedPath,
97 97
98 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), 98 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
99 profile: 'default', 99 profile: CONFIG.TRANSCODING.PROFILE,
100 100
101 resolution, 101 resolution,
102 102
@@ -108,7 +108,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti
108 outputPath: videoTranscodedPath, 108 outputPath: videoTranscodedPath,
109 109
110 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), 110 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
111 profile: 'default', 111 profile: CONFIG.TRANSCODING.PROFILE,
112 112
113 resolution, 113 resolution,
114 isPortraitMode: isPortrait, 114 isPortraitMode: isPortrait,
@@ -143,7 +143,7 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video
143 outputPath: videoTranscodedPath, 143 outputPath: videoTranscodedPath,
144 144
145 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), 145 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
146 profile: 'default', 146 profile: CONFIG.TRANSCODING.PROFILE,
147 147
148 audioPath: audioInputPath, 148 audioPath: audioInputPath,
149 resolution, 149 resolution,
@@ -284,7 +284,7 @@ async function generateHlsPlaylistCommon (options: {
284 outputPath, 284 outputPath,
285 285
286 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), 286 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
287 profile: 'default', 287 profile: CONFIG.TRANSCODING.PROFILE,
288 288
289 resolution, 289 resolution,
290 copyCodecs, 290 copyCodecs,
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts
index bbac55a50..1083e0afa 100644
--- a/server/middlewares/validators/plugins.ts
+++ b/server/middlewares/validators/plugins.ts
@@ -50,9 +50,9 @@ const getExternalAuthValidator = [
50 if (areValidationErrors(req, res)) return 50 if (areValidationErrors(req, res)) return
51 51
52 const plugin = res.locals.registeredPlugin 52 const plugin = res.locals.registeredPlugin
53 if (!plugin.registerHelpersStore) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 53 if (!plugin.registerHelpers) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
54 54
55 const externalAuth = plugin.registerHelpersStore.getExternalAuths().find(a => a.authName === req.params.authName) 55 const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName)
56 if (!externalAuth) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 56 if (!externalAuth) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
57 57
58 res.locals.externalAuth = externalAuth 58 res.locals.externalAuth = externalAuth
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index c56fbfbf2..ea6c9d44b 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -96,10 +96,11 @@ import {
96 MVideoWithRights 96 MVideoWithRights
97} from '../../types/models' 97} from '../../types/models'
98import { MThumbnail } from '../../types/models/video/thumbnail' 98import { MThumbnail } from '../../types/models/video/thumbnail'
99import { MVideoFile, MVideoFileRedundanciesOpt, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file' 99import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
100import { VideoAbuseModel } from '../abuse/video-abuse' 100import { VideoAbuseModel } from '../abuse/video-abuse'
101import { AccountModel } from '../account/account' 101import { AccountModel } from '../account/account'
102import { AccountVideoRateModel } from '../account/account-video-rate' 102import { AccountVideoRateModel } from '../account/account-video-rate'
103import { UserModel } from '../account/user'
103import { UserVideoHistoryModel } from '../account/user-video-history' 104import { UserVideoHistoryModel } from '../account/user-video-history'
104import { ActorModel } from '../activitypub/actor' 105import { ActorModel } from '../activitypub/actor'
105import { AvatarModel } from '../avatar/avatar' 106import { AvatarModel } from '../avatar/avatar'
@@ -129,7 +130,6 @@ import { VideoShareModel } from './video-share'
129import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 130import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
130import { VideoTagModel } from './video-tag' 131import { VideoTagModel } from './video-tag'
131import { VideoViewModel } from './video-view' 132import { VideoViewModel } from './video-view'
132import { UserModel } from '../account/user'
133 133
134export enum ScopeNames { 134export enum ScopeNames {
135 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', 135 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index d3ae5fe0a..e6309b5f7 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -87,6 +87,7 @@ describe('Test config API validators', function () {
87 allowAdditionalExtensions: true, 87 allowAdditionalExtensions: true,
88 allowAudioFiles: true, 88 allowAudioFiles: true,
89 threads: 1, 89 threads: 1,
90 profile: 'vod_profile',
90 resolutions: { 91 resolutions: {
91 '0p': false, 92 '0p': false,
92 '240p': false, 93 '240p': false,
@@ -115,6 +116,7 @@ describe('Test config API validators', function () {
115 transcoding: { 116 transcoding: {
116 enabled: true, 117 enabled: true,
117 threads: 4, 118 threads: 4,
119 profile: 'live_profile',
118 resolutions: { 120 resolutions: {
119 '240p': true, 121 '240p': true,
120 '360p': true, 122 '360p': true,
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index e0575bdfd..e5bab0b77 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -70,6 +70,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
70 expect(data.transcoding.allowAdditionalExtensions).to.be.false 70 expect(data.transcoding.allowAdditionalExtensions).to.be.false
71 expect(data.transcoding.allowAudioFiles).to.be.false 71 expect(data.transcoding.allowAudioFiles).to.be.false
72 expect(data.transcoding.threads).to.equal(2) 72 expect(data.transcoding.threads).to.equal(2)
73 expect(data.transcoding.profile).to.equal('default')
73 expect(data.transcoding.resolutions['240p']).to.be.true 74 expect(data.transcoding.resolutions['240p']).to.be.true
74 expect(data.transcoding.resolutions['360p']).to.be.true 75 expect(data.transcoding.resolutions['360p']).to.be.true
75 expect(data.transcoding.resolutions['480p']).to.be.true 76 expect(data.transcoding.resolutions['480p']).to.be.true
@@ -87,6 +88,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
87 expect(data.live.maxUserLives).to.equal(3) 88 expect(data.live.maxUserLives).to.equal(3)
88 expect(data.live.transcoding.enabled).to.be.false 89 expect(data.live.transcoding.enabled).to.be.false
89 expect(data.live.transcoding.threads).to.equal(2) 90 expect(data.live.transcoding.threads).to.equal(2)
91 expect(data.live.transcoding.profile).to.equal('default')
90 expect(data.live.transcoding.resolutions['240p']).to.be.false 92 expect(data.live.transcoding.resolutions['240p']).to.be.false
91 expect(data.live.transcoding.resolutions['360p']).to.be.false 93 expect(data.live.transcoding.resolutions['360p']).to.be.false
92 expect(data.live.transcoding.resolutions['480p']).to.be.false 94 expect(data.live.transcoding.resolutions['480p']).to.be.false
@@ -159,6 +161,7 @@ function checkUpdatedConfig (data: CustomConfig) {
159 expect(data.transcoding.threads).to.equal(1) 161 expect(data.transcoding.threads).to.equal(1)
160 expect(data.transcoding.allowAdditionalExtensions).to.be.true 162 expect(data.transcoding.allowAdditionalExtensions).to.be.true
161 expect(data.transcoding.allowAudioFiles).to.be.true 163 expect(data.transcoding.allowAudioFiles).to.be.true
164 expect(data.transcoding.profile).to.equal('vod_profile')
162 expect(data.transcoding.resolutions['240p']).to.be.false 165 expect(data.transcoding.resolutions['240p']).to.be.false
163 expect(data.transcoding.resolutions['360p']).to.be.true 166 expect(data.transcoding.resolutions['360p']).to.be.true
164 expect(data.transcoding.resolutions['480p']).to.be.true 167 expect(data.transcoding.resolutions['480p']).to.be.true
@@ -175,6 +178,7 @@ function checkUpdatedConfig (data: CustomConfig) {
175 expect(data.live.maxUserLives).to.equal(10) 178 expect(data.live.maxUserLives).to.equal(10)
176 expect(data.live.transcoding.enabled).to.be.true 179 expect(data.live.transcoding.enabled).to.be.true
177 expect(data.live.transcoding.threads).to.equal(4) 180 expect(data.live.transcoding.threads).to.equal(4)
181 expect(data.live.transcoding.profile).to.equal('live_profile')
178 expect(data.live.transcoding.resolutions['240p']).to.be.true 182 expect(data.live.transcoding.resolutions['240p']).to.be.true
179 expect(data.live.transcoding.resolutions['360p']).to.be.true 183 expect(data.live.transcoding.resolutions['360p']).to.be.true
180 expect(data.live.transcoding.resolutions['480p']).to.be.true 184 expect(data.live.transcoding.resolutions['480p']).to.be.true
@@ -319,6 +323,7 @@ describe('Test config', function () {
319 allowAdditionalExtensions: true, 323 allowAdditionalExtensions: true,
320 allowAudioFiles: true, 324 allowAudioFiles: true,
321 threads: 1, 325 threads: 1,
326 profile: 'vod_profile',
322 resolutions: { 327 resolutions: {
323 '0p': false, 328 '0p': false,
324 '240p': false, 329 '240p': false,
@@ -345,6 +350,7 @@ describe('Test config', function () {
345 transcoding: { 350 transcoding: {
346 enabled: true, 351 enabled: true,
347 threads: 4, 352 threads: 4,
353 profile: 'live_profile',
348 resolutions: { 354 resolutions: {
349 '240p': true, 355 '240p': true,
350 '360p': true, 356 '360p': true,
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts
index 631230f26..5ad02df2f 100644
--- a/server/tests/api/videos/video-transcoder.ts
+++ b/server/tests/api/videos/video-transcoder.ts
@@ -511,7 +511,9 @@ describe('Test video transcoding', function () {
511 511
512 const resolutions = [ 240, 360, 480, 720, 1080 ] 512 const resolutions = [ 240, 360, 480, 720, 1080 ]
513 for (const r of resolutions) { 513 for (const r of resolutions) {
514 expect(await getServerFileSize(servers[1], `videos/${videoUUID}-${r}.mp4`)).to.be.below(60_000) 514 const path = `videos/${videoUUID}-${r}.mp4`
515 const size = await getServerFileSize(servers[1], path)
516 expect(size, `${path} not below ${60_000}`).to.be.below(60_000)
515 } 517 }
516 }) 518 })
517 519
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
index 000000000..5990ce1ce
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js
@@ -0,0 +1,35 @@
1async function register ({ transcodingManager }) {
2
3 {
4 const builder = () => {
5 return {
6 outputOptions: [
7 '-r 10'
8 ]
9 }
10 }
11
12 transcodingManager.addVODProfile('libx264', 'low-vod', builder)
13 }
14
15 {
16 const builder = (options) => {
17 return {
18 outputOptions: [
19 '-r:' + options.streamNum + ' 5'
20 ]
21 }
22 }
23
24 transcodingManager.addLiveProfile('libx264', 'low-live', builder)
25 }
26}
27
28async function unregister () {
29 return
30}
31
32module.exports = {
33 register,
34 unregister
35}
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
index 000000000..bedbfa051
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-transcoding-one/package.json
@@ -0,0 +1,20 @@
1{
2 "name": "peertube-plugin-test-transcoding-one",
3 "version": "0.0.1",
4 "description": "Plugin test transcoding 1",
5 "engine": {
6 "peertube": ">=1.3.0"
7 },
8 "keywords": [
9 "peertube",
10 "plugin"
11 ],
12 "homepage": "https://github.com/Chocobozzz/PeerTube",
13 "author": "Chocobozzz",
14 "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
15 "library": "./main.js",
16 "staticDirs": {},
17 "css": [],
18 "clientScripts": [],
19 "translations": {}
20}
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
index 000000000..a914bce49
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-transcoding-two/main.js
@@ -0,0 +1,38 @@
1async function register ({ transcodingManager }) {
2
3 {
4 const builder = () => {
5 return {
6 outputOptions: []
7 }
8 }
9
10 transcodingManager.addVODProfile('libopus', 'test-vod-profile', builder)
11 transcodingManager.addVODProfile('libvpx-vp9', 'test-vod-profile', builder)
12
13 transcodingManager.addVODEncoderPriority('audio', 'libopus', 1000)
14 transcodingManager.addVODEncoderPriority('video', 'libvpx-vp9', 1000)
15 }
16
17 {
18 const builder = (options) => {
19 return {
20 outputOptions: [
21 '-b:' + options.streamNum + ' 10K'
22 ]
23 }
24 }
25
26 transcodingManager.addLiveProfile('libopus', 'test-live-profile', builder)
27 transcodingManager.addLiveEncoderPriority('audio', 'libopus', 1000)
28 }
29}
30
31async function unregister () {
32 return
33}
34
35module.exports = {
36 register,
37 unregister
38}
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
index 000000000..34be0454b
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-transcoding-two/package.json
@@ -0,0 +1,20 @@
1{
2 "name": "peertube-plugin-test-transcoding-two",
3 "version": "0.0.1",
4 "description": "Plugin test transcoding 2",
5 "engine": {
6 "peertube": ">=1.3.0"
7 },
8 "keywords": [
9 "peertube",
10 "plugin"
11 ],
12 "homepage": "https://github.com/Chocobozzz/PeerTube",
13 "author": "Chocobozzz",
14 "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
15 "library": "./main.js",
16 "staticDirs": {},
17 "css": [],
18 "clientScripts": [],
19 "translations": {}
20}
diff --git a/server/tests/plugins/index.ts b/server/tests/plugins/index.ts
index b870a4055..fd7116efd 100644
--- a/server/tests/plugins/index.ts
+++ b/server/tests/plugins/index.ts
@@ -1,10 +1,11 @@
1import './action-hooks' 1import './action-hooks'
2import './html-injection'
3import './id-and-pass-auth'
4import './external-auth' 2import './external-auth'
5import './filter-hooks' 3import './filter-hooks'
6import './translations' 4import './html-injection'
7import './video-constants' 5import './id-and-pass-auth'
8import './plugin-helpers' 6import './plugin-helpers'
9import './plugin-router' 7import './plugin-router'
10import './plugin-storage' 8import './plugin-storage'
9import './plugin-transcoding'
10import './translations'
11import './video-constants'
diff --git a/server/tests/plugins/plugin-transcoding.ts b/server/tests/plugins/plugin-transcoding.ts
new file mode 100644
index 000000000..96ff4c2fe
--- /dev/null
+++ b/server/tests/plugins/plugin-transcoding.ts
@@ -0,0 +1,226 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { expect } from 'chai'
5import { join } from 'path'
6import { getAudioStream, getVideoFileFPS, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils'
7import { ServerConfig, VideoDetails, VideoPrivacy } from '@shared/models'
8import {
9 buildServerDirectory,
10 createLive,
11 getConfig,
12 getPluginTestPath,
13 getVideo,
14 installPlugin,
15 sendRTMPStreamInVideo,
16 setAccessTokensToServers,
17 setDefaultVideoChannel,
18 uninstallPlugin,
19 updateCustomSubConfig,
20 uploadVideoAndGetId,
21 waitJobs,
22 waitUntilLivePublished
23} from '../../../shared/extra-utils'
24import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
25
26async function createLiveWrapper (server: ServerInfo) {
27 const liveAttributes = {
28 name: 'live video',
29 channelId: server.videoChannel.id,
30 privacy: VideoPrivacy.PUBLIC
31 }
32
33 const res = await createLive(server.url, server.accessToken, liveAttributes)
34 return res.body.video.uuid
35}
36
37function updateConf (server: ServerInfo, vodProfile: string, liveProfile: string) {
38 return updateCustomSubConfig(server.url, server.accessToken, {
39 transcoding: {
40 enabled: true,
41 profile: vodProfile,
42 hls: {
43 enabled: true
44 },
45 webtorrent: {
46 enabled: true
47 }
48 },
49 live: {
50 transcoding: {
51 profile: liveProfile,
52 enabled: true
53 }
54 }
55 })
56}
57
58describe('Test transcoding plugins', function () {
59 let server: ServerInfo
60
61 before(async function () {
62 this.timeout(60000)
63
64 server = await flushAndRunServer(1)
65 await setAccessTokensToServers([ server ])
66 await setDefaultVideoChannel([ server ])
67
68 await updateConf(server, 'default', 'default')
69 })
70
71 describe('When using a plugin adding profiles to existing encoders', function () {
72
73 async function checkVideoFPS (uuid: string, type: 'above' | 'below', fps: number) {
74 const res = await getVideo(server.url, uuid)
75 const video = res.body as VideoDetails
76 const files = video.files.concat(...video.streamingPlaylists.map(p => p.files))
77
78 for (const file of files) {
79 if (type === 'above') {
80 expect(file.fps).to.be.above(fps)
81 } else {
82 expect(file.fps).to.be.below(fps)
83 }
84 }
85 }
86
87 async function checkLiveFPS (uuid: string, type: 'above' | 'below', fps: number) {
88 const playlistUrl = `${server.url}/static/streaming-playlists/hls/${uuid}/0.m3u8`
89 const videoFPS = await getVideoFileFPS(playlistUrl)
90
91 if (type === 'above') {
92 expect(videoFPS).to.be.above(fps)
93 } else {
94 expect(videoFPS).to.be.below(fps)
95 }
96 }
97
98 before(async function () {
99 await installPlugin({
100 url: server.url,
101 accessToken: server.accessToken,
102 path: getPluginTestPath('-transcoding-one')
103 })
104 })
105
106 it('Should have the appropriate available profiles', async function () {
107 const res = await getConfig(server.url)
108 const config = res.body as ServerConfig
109
110 expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod' ])
111 expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'low-live' ])
112 })
113
114 it('Should not use the plugin profile if not chosen by the admin', async function () {
115 this.timeout(120000)
116
117 const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
118 await waitJobs([ server ])
119
120 await checkVideoFPS(videoUUID, 'above', 20)
121 })
122
123 it('Should use the vod profile', async function () {
124 this.timeout(120000)
125
126 await updateConf(server, 'low-vod', 'default')
127
128 const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
129 await waitJobs([ server ])
130
131 await checkVideoFPS(videoUUID, 'below', 12)
132 })
133
134 it('Should not use the plugin profile if not chosen by the admin', async function () {
135 this.timeout(120000)
136
137 const liveVideoId = await createLiveWrapper(server)
138
139 await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm')
140 await waitUntilLivePublished(server.url, server.accessToken, liveVideoId)
141 await waitJobs([ server ])
142
143 await checkLiveFPS(liveVideoId, 'above', 20)
144 })
145
146 it('Should use the live profile', async function () {
147 this.timeout(120000)
148
149 await updateConf(server, 'low-vod', 'low-live')
150
151 const liveVideoId = await createLiveWrapper(server)
152
153 await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm')
154 await waitUntilLivePublished(server.url, server.accessToken, liveVideoId)
155 await waitJobs([ server ])
156
157 await checkLiveFPS(liveVideoId, 'below', 12)
158 })
159
160 it('Should default to the default profile if the specified profile does not exist', async function () {
161 this.timeout(120000)
162
163 await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-transcoding-one' })
164
165 const res = await getConfig(server.url)
166 const config = res.body as ServerConfig
167
168 expect(config.transcoding.availableProfiles).to.deep.equal([ 'default' ])
169 expect(config.live.transcoding.availableProfiles).to.deep.equal([ 'default' ])
170
171 const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
172 await waitJobs([ server ])
173
174 await checkVideoFPS(videoUUID, 'above', 20)
175 })
176
177 })
178
179 describe('When using a plugin adding new encoders', function () {
180
181 before(async function () {
182 await installPlugin({
183 url: server.url,
184 accessToken: server.accessToken,
185 path: getPluginTestPath('-transcoding-two')
186 })
187
188 await updateConf(server, 'test-vod-profile', 'test-live-profile')
189 })
190
191 it('Should use the new vod encoders', async function () {
192 this.timeout(240000)
193
194 const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
195 await waitJobs([ server ])
196
197 const path = buildServerDirectory(server, join('videos', videoUUID + '-720.mp4'))
198 const audioProbe = await getAudioStream(path)
199 expect(audioProbe.audioStream.codec_name).to.equal('opus')
200
201 const videoProbe = await getVideoStreamFromFile(path)
202 expect(videoProbe.codec_name).to.equal('vp9')
203 })
204
205 it('Should use the new live encoders', async function () {
206 this.timeout(120000)
207
208 const liveVideoId = await createLiveWrapper(server)
209
210 await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm')
211 await waitUntilLivePublished(server.url, server.accessToken, liveVideoId)
212 await waitJobs([ server ])
213
214 const playlistUrl = `${server.url}/static/streaming-playlists/hls/${liveVideoId}/0.m3u8`
215 const audioProbe = await getAudioStream(playlistUrl)
216 expect(audioProbe.audioStream.codec_name).to.equal('opus')
217
218 const videoProbe = await getVideoStreamFromFile(playlistUrl)
219 expect(videoProbe.codec_name).to.equal('h264')
220 })
221 })
222
223 after(async function () {
224 await cleanupTests([ server ])
225 })
226})
diff --git a/server/types/plugins/register-server-option.model.ts b/server/types/plugins/register-server-option.model.ts
index 2e52d1efd..ccd5a060d 100644
--- a/server/types/plugins/register-server-option.model.ts
+++ b/server/types/plugins/register-server-option.model.ts
@@ -5,6 +5,7 @@ import {
5 PluginPlaylistPrivacyManager, 5 PluginPlaylistPrivacyManager,
6 PluginSettingsManager, 6 PluginSettingsManager,
7 PluginStorageManager, 7 PluginStorageManager,
8 PluginTranscodingManager,
8 PluginVideoCategoryManager, 9 PluginVideoCategoryManager,
9 PluginVideoLanguageManager, 10 PluginVideoLanguageManager,
10 PluginVideoLicenceManager, 11 PluginVideoLicenceManager,
@@ -68,6 +69,8 @@ export type RegisterServerOptions = {
68 videoPrivacyManager: PluginVideoPrivacyManager 69 videoPrivacyManager: PluginVideoPrivacyManager
69 playlistPrivacyManager: PluginPlaylistPrivacyManager 70 playlistPrivacyManager: PluginPlaylistPrivacyManager
70 71
72 transcodingManager: PluginTranscodingManager
73
71 registerIdAndPassAuth: (options: RegisterServerAuthPassOptions) => void 74 registerIdAndPassAuth: (options: RegisterServerAuthPassOptions) => void
72 registerExternalAuth: (options: RegisterServerAuthExternalOptions) => RegisterServerAuthExternalResult 75 registerExternalAuth: (options: RegisterServerAuthExternalOptions) => RegisterServerAuthExternalResult
73 unregisterIdAndPassAuth: (authName: string) => void 76 unregisterIdAndPassAuth: (authName: string) => void