From 1896bca09e088b0da9d5e845407ecebae330618c Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 28 Jan 2021 15:52:44 +0100 Subject: Support transcoding options/encoders by plugins --- server/controllers/api/config.ts | 11 +- server/helpers/ffmpeg-utils.ts | 88 ++-- server/helpers/logger.ts | 8 + server/initializers/checker-before-init.ts | 29 -- server/initializers/config.ts | 2 + server/lib/live-manager.ts | 2 +- server/lib/plugins/plugin-helpers-builder.ts | 133 ++++++ server/lib/plugins/plugin-helpers.ts | 133 ------ server/lib/plugins/plugin-manager.ts | 35 +- server/lib/plugins/register-helpers-store.ts | 357 ----------------- server/lib/plugins/register-helpers.ts | 444 +++++++++++++++++++++ server/lib/video-transcoding-profiles.ts | 119 +++++- server/lib/video-transcoding.ts | 10 +- server/middlewares/validators/plugins.ts | 4 +- server/models/video/video.ts | 4 +- server/tests/api/check-params/config.ts | 2 + server/tests/api/server/config.ts | 6 + server/tests/api/videos/video-transcoder.ts | 4 +- .../peertube-plugin-test-transcoding-one/main.js | 35 ++ .../package.json | 20 + .../peertube-plugin-test-transcoding-two/main.js | 38 ++ .../package.json | 20 + server/tests/plugins/index.ts | 9 +- server/tests/plugins/plugin-transcoding.ts | 226 +++++++++++ .../types/plugins/register-server-option.model.ts | 3 + 25 files changed, 1130 insertions(+), 612 deletions(-) create mode 100644 server/lib/plugins/plugin-helpers-builder.ts delete mode 100644 server/lib/plugins/plugin-helpers.ts delete mode 100644 server/lib/plugins/register-helpers-store.ts create mode 100644 server/lib/plugins/register-helpers.ts create mode 100644 server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js create mode 100644 server/tests/fixtures/peertube-plugin-test-transcoding-one/package.json create mode 100644 server/tests/fixtures/peertube-plugin-test-transcoding-two/main.js create mode 100644 server/tests/fixtures/peertube-plugin-test-transcoding-two/package.json create mode 100644 server/tests/plugins/plugin-transcoding.ts (limited to 'server') 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' 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'], 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' 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 +type StreamType = 'audio' | 'video' -// Options types +// --------------------------------------------------------------------------- +// Encoders support +// --------------------------------------------------------------------------- -export interface EncoderOptions { - copy?: boolean - outputOptions: string[] -} +// Detect supported encoders by ffmpeg +let supportedEncoders: Map +async function checkFFmpegEncoders (peertubeAvailableEncoders: AvailableEncoders): Promise> { + if (supportedEncoders !== undefined) { + return supportedEncoders + } -// All our encoders + const getAvailableEncodersPromise = promisify0(ffmpeg.getAvailableEncoders) + const availableFFmpegEncoders = await getAvailableEncodersPromise() -export interface EncoderProfile { - [ profile: string ]: T + const searchEncoders = new Set() + 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() -export type AvailableEncoders = { - live: { - [ encoder: string ]: EncoderProfile + for (const searchEncoder of searchEncoders) { + supportedEncoders.set(searchEncoder, availableFFmpegEncoders[searchEncoder] !== undefined) } - vod: { - [ encoder: string ]: EncoderProfile - } + 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 = 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 } 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 () { 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 = {} 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 } }) { throw new Error('Unavailable encode codec ' + codec + ' in FFmpeg') } } - - return checkFFmpegEncoders() -} - -// Detect supported encoders by ffmpeg -let supportedEncoders: Map -async function checkFFmpegEncoders (): Promise> { - 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() - - 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 } 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 = { get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get('transcoding.allow_additional_extensions') }, get ALLOW_AUDIO_FILES () { return config.get('transcoding.allow_audio_files') }, get THREADS () { return config.get('transcoding.threads') }, + get PROFILE () { return config.get('transcoding.profile') }, RESOLUTIONS: { get '0p' () { return config.get('transcoding.resolutions.0p') }, get '240p' () { return config.get('transcoding.resolutions.240p') }, @@ -221,6 +222,7 @@ const CONFIG = { TRANSCODING: { get ENABLED () { return config.get('live.transcoding.enabled') }, get THREADS () { return config.get('live.transcoding.threads') }, + get PROFILE () { return config.get('live.transcoding.profile') }, RESOLUTIONS: { get '240p' () { return config.get('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 { resolutions: allResolutions, fps, availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), - profile: 'default' + profile: CONFIG.LIVE.TRANSCODING.PROFILE }) : getLiveMuxingCommand(rtmpUrl, outPath) diff --git a/server/lib/plugins/plugin-helpers-builder.ts b/server/lib/plugins/plugin-helpers-builder.ts new file mode 100644 index 000000000..39773f693 --- /dev/null +++ b/server/lib/plugins/plugin-helpers-builder.ts @@ -0,0 +1,133 @@ +import { PeerTubeHelpers } from '@server/types/plugins' +import { sequelizeTypescript } from '@server/initializers/database' +import { buildLogger } from '@server/helpers/logger' +import { VideoModel } from '@server/models/video/video' +import { WEBSERVER } from '@server/initializers/constants' +import { ServerModel } from '@server/models/server/server' +import { getServerActor } from '@server/models/application/application' +import { addServerInBlocklist, removeServerFromBlocklist, addAccountInBlocklist, removeAccountFromBlocklist } from '../blocklist' +import { ServerBlocklistModel } from '@server/models/server/server-blocklist' +import { AccountModel } from '@server/models/account/account' +import { VideoBlacklistCreate } from '@shared/models' +import { blacklistVideo, unblacklistVideo } from '../video-blacklist' +import { VideoBlacklistModel } from '@server/models/video/video-blacklist' +import { AccountBlocklistModel } from '@server/models/account/account-blocklist' + +function buildPluginHelpers (npmName: string): PeerTubeHelpers { + const logger = buildPluginLogger(npmName) + + const database = buildDatabaseHelpers() + const videos = buildVideosHelpers() + + const config = buildConfigHelpers() + + const server = buildServerHelpers() + + const moderation = buildModerationHelpers() + + return { + logger, + database, + videos, + config, + moderation, + server + } +} + +export { + buildPluginHelpers +} + +// --------------------------------------------------------------------------- + +function buildPluginLogger (npmName: string) { + return buildLogger(npmName) +} + +function buildDatabaseHelpers () { + return { + query: sequelizeTypescript.query.bind(sequelizeTypescript) + } +} + +function buildServerHelpers () { + return { + getServerActor: () => getServerActor() + } +} + +function buildVideosHelpers () { + return { + loadByUrl: (url: string) => { + return VideoModel.loadByUrl(url) + }, + + removeVideo: (id: number) => { + return sequelizeTypescript.transaction(async t => { + const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(id, t) + + await video.destroy({ transaction: t }) + }) + } + } +} + +function buildModerationHelpers () { + return { + blockServer: async (options: { byAccountId: number, hostToBlock: string }) => { + const serverToBlock = await ServerModel.loadOrCreateByHost(options.hostToBlock) + + await addServerInBlocklist(options.byAccountId, serverToBlock.id) + }, + + unblockServer: async (options: { byAccountId: number, hostToUnblock: string }) => { + const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(options.byAccountId, options.hostToUnblock) + if (!serverBlock) return + + await removeServerFromBlocklist(serverBlock) + }, + + blockAccount: async (options: { byAccountId: number, handleToBlock: string }) => { + const accountToBlock = await AccountModel.loadByNameWithHost(options.handleToBlock) + if (!accountToBlock) return + + await addAccountInBlocklist(options.byAccountId, accountToBlock.id) + }, + + unblockAccount: async (options: { byAccountId: number, handleToUnblock: string }) => { + const targetAccount = await AccountModel.loadByNameWithHost(options.handleToUnblock) + if (!targetAccount) return + + const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(options.byAccountId, targetAccount.id) + if (!accountBlock) return + + await removeAccountFromBlocklist(accountBlock) + }, + + blacklistVideo: async (options: { videoIdOrUUID: number | string, createOptions: VideoBlacklistCreate }) => { + const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(options.videoIdOrUUID) + if (!video) return + + await blacklistVideo(video, options.createOptions) + }, + + unblacklistVideo: async (options: { videoIdOrUUID: number | string }) => { + const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(options.videoIdOrUUID) + if (!video) return + + const videoBlacklist = await VideoBlacklistModel.loadByVideoId(video.id) + if (!videoBlacklist) return + + await unblacklistVideo(videoBlacklist, video) + } + } +} + +function buildConfigHelpers () { + return { + getWebserverUrl () { + return WEBSERVER.URL + } + } +} diff --git a/server/lib/plugins/plugin-helpers.ts b/server/lib/plugins/plugin-helpers.ts deleted file mode 100644 index 39773f693..000000000 --- a/server/lib/plugins/plugin-helpers.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { PeerTubeHelpers } from '@server/types/plugins' -import { sequelizeTypescript } from '@server/initializers/database' -import { buildLogger } from '@server/helpers/logger' -import { VideoModel } from '@server/models/video/video' -import { WEBSERVER } from '@server/initializers/constants' -import { ServerModel } from '@server/models/server/server' -import { getServerActor } from '@server/models/application/application' -import { addServerInBlocklist, removeServerFromBlocklist, addAccountInBlocklist, removeAccountFromBlocklist } from '../blocklist' -import { ServerBlocklistModel } from '@server/models/server/server-blocklist' -import { AccountModel } from '@server/models/account/account' -import { VideoBlacklistCreate } from '@shared/models' -import { blacklistVideo, unblacklistVideo } from '../video-blacklist' -import { VideoBlacklistModel } from '@server/models/video/video-blacklist' -import { AccountBlocklistModel } from '@server/models/account/account-blocklist' - -function buildPluginHelpers (npmName: string): PeerTubeHelpers { - const logger = buildPluginLogger(npmName) - - const database = buildDatabaseHelpers() - const videos = buildVideosHelpers() - - const config = buildConfigHelpers() - - const server = buildServerHelpers() - - const moderation = buildModerationHelpers() - - return { - logger, - database, - videos, - config, - moderation, - server - } -} - -export { - buildPluginHelpers -} - -// --------------------------------------------------------------------------- - -function buildPluginLogger (npmName: string) { - return buildLogger(npmName) -} - -function buildDatabaseHelpers () { - return { - query: sequelizeTypescript.query.bind(sequelizeTypescript) - } -} - -function buildServerHelpers () { - return { - getServerActor: () => getServerActor() - } -} - -function buildVideosHelpers () { - return { - loadByUrl: (url: string) => { - return VideoModel.loadByUrl(url) - }, - - removeVideo: (id: number) => { - return sequelizeTypescript.transaction(async t => { - const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(id, t) - - await video.destroy({ transaction: t }) - }) - } - } -} - -function buildModerationHelpers () { - return { - blockServer: async (options: { byAccountId: number, hostToBlock: string }) => { - const serverToBlock = await ServerModel.loadOrCreateByHost(options.hostToBlock) - - await addServerInBlocklist(options.byAccountId, serverToBlock.id) - }, - - unblockServer: async (options: { byAccountId: number, hostToUnblock: string }) => { - const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(options.byAccountId, options.hostToUnblock) - if (!serverBlock) return - - await removeServerFromBlocklist(serverBlock) - }, - - blockAccount: async (options: { byAccountId: number, handleToBlock: string }) => { - const accountToBlock = await AccountModel.loadByNameWithHost(options.handleToBlock) - if (!accountToBlock) return - - await addAccountInBlocklist(options.byAccountId, accountToBlock.id) - }, - - unblockAccount: async (options: { byAccountId: number, handleToUnblock: string }) => { - const targetAccount = await AccountModel.loadByNameWithHost(options.handleToUnblock) - if (!targetAccount) return - - const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(options.byAccountId, targetAccount.id) - if (!accountBlock) return - - await removeAccountFromBlocklist(accountBlock) - }, - - blacklistVideo: async (options: { videoIdOrUUID: number | string, createOptions: VideoBlacklistCreate }) => { - const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(options.videoIdOrUUID) - if (!video) return - - await blacklistVideo(video, options.createOptions) - }, - - unblacklistVideo: async (options: { videoIdOrUUID: number | string }) => { - const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(options.videoIdOrUUID) - if (!video) return - - const videoBlacklist = await VideoBlacklistModel.loadByVideoId(video.id) - if (!videoBlacklist) return - - await unblacklistVideo(videoBlacklist, video) - } - } -} - -function buildConfigHelpers () { - return { - getWebserverUrl () { - return WEBSERVER.URL - } - } -} 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' 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() } } diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers-store.ts deleted file mode 100644 index c73079302..000000000 --- a/server/lib/plugins/register-helpers-store.ts +++ /dev/null @@ -1,357 +0,0 @@ -import * as express from 'express' -import { logger } from '@server/helpers/logger' -import { - VIDEO_CATEGORIES, - VIDEO_LANGUAGES, - VIDEO_LICENCES, - VIDEO_PLAYLIST_PRIVACIES, - VIDEO_PRIVACIES -} from '@server/initializers/constants' -import { onExternalUserAuthenticated } from '@server/lib/auth' -import { PluginModel } from '@server/models/server/plugin' -import { - RegisterServerAuthExternalOptions, - RegisterServerAuthExternalResult, - RegisterServerAuthPassOptions, - RegisterServerExternalAuthenticatedResult, - RegisterServerOptions -} from '@server/types/plugins' -import { - PluginPlaylistPrivacyManager, - PluginSettingsManager, - PluginStorageManager, - PluginVideoCategoryManager, - PluginVideoLanguageManager, - PluginVideoLicenceManager, - PluginVideoPrivacyManager, - RegisterServerHookOptions, - RegisterServerSettingOptions -} from '@shared/models' -import { serverHookObject } from '@shared/models/plugins/server-hook.model' -import { buildPluginHelpers } from './plugin-helpers' - -type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' -type VideoConstant = { [key in number | string]: string } - -type UpdatedVideoConstant = { - [name in AlterableVideoConstant]: { - added: { key: number | string, label: string }[] - deleted: { key: number | string, label: string }[] - } -} - -export class RegisterHelpersStore { - private readonly updatedVideoConstants: UpdatedVideoConstant = { - playlistPrivacy: { added: [], deleted: [] }, - privacy: { added: [], deleted: [] }, - language: { added: [], deleted: [] }, - licence: { added: [], deleted: [] }, - category: { added: [], deleted: [] } - } - - private readonly settings: RegisterServerSettingOptions[] = [] - - private idAndPassAuths: RegisterServerAuthPassOptions[] = [] - private externalAuths: RegisterServerAuthExternalOptions[] = [] - - private readonly onSettingsChangeCallbacks: ((settings: any) => void)[] = [] - - private readonly router: express.Router - - constructor ( - private readonly npmName: string, - private readonly plugin: PluginModel, - private readonly onHookAdded: (options: RegisterServerHookOptions) => void - ) { - this.router = express.Router() - } - - buildRegisterHelpers (): RegisterServerOptions { - const registerHook = this.buildRegisterHook() - const registerSetting = this.buildRegisterSetting() - - const getRouter = this.buildGetRouter() - - const settingsManager = this.buildSettingsManager() - const storageManager = this.buildStorageManager() - - const videoLanguageManager = this.buildVideoLanguageManager() - - const videoLicenceManager = this.buildVideoLicenceManager() - const videoCategoryManager = this.buildVideoCategoryManager() - - const videoPrivacyManager = this.buildVideoPrivacyManager() - const playlistPrivacyManager = this.buildPlaylistPrivacyManager() - - const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth() - const registerExternalAuth = this.buildRegisterExternalAuth() - const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth() - const unregisterExternalAuth = this.buildUnregisterExternalAuth() - - const peertubeHelpers = buildPluginHelpers(this.npmName) - - return { - registerHook, - registerSetting, - - getRouter, - - settingsManager, - storageManager, - - videoLanguageManager, - videoCategoryManager, - videoLicenceManager, - - videoPrivacyManager, - playlistPrivacyManager, - - registerIdAndPassAuth, - registerExternalAuth, - unregisterIdAndPassAuth, - unregisterExternalAuth, - - peertubeHelpers - } - } - - reinitVideoConstants (npmName: string) { - const hash = { - language: VIDEO_LANGUAGES, - licence: VIDEO_LICENCES, - category: VIDEO_CATEGORIES, - privacy: VIDEO_PRIVACIES, - playlistPrivacy: VIDEO_PLAYLIST_PRIVACIES - } - const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category', 'privacy', 'playlistPrivacy' ] - - for (const type of types) { - const updatedConstants = this.updatedVideoConstants[type][npmName] - if (!updatedConstants) continue - - for (const added of updatedConstants.added) { - delete hash[type][added.key] - } - - for (const deleted of updatedConstants.deleted) { - hash[type][deleted.key] = deleted.label - } - - delete this.updatedVideoConstants[type][npmName] - } - } - - getSettings () { - return this.settings - } - - getRouter () { - return this.router - } - - getIdAndPassAuths () { - return this.idAndPassAuths - } - - getExternalAuths () { - return this.externalAuths - } - - getOnSettingsChangedCallbacks () { - return this.onSettingsChangeCallbacks - } - - private buildGetRouter () { - return () => this.router - } - - private buildRegisterSetting () { - return (options: RegisterServerSettingOptions) => { - this.settings.push(options) - } - } - - private buildRegisterHook () { - return (options: RegisterServerHookOptions) => { - if (serverHookObject[options.target] !== true) { - logger.warn('Unknown hook %s of plugin %s. Skipping.', options.target, this.npmName) - return - } - - return this.onHookAdded(options) - } - } - - private buildRegisterIdAndPassAuth () { - return (options: RegisterServerAuthPassOptions) => { - if (!options.authName || typeof options.getWeight !== 'function' || typeof options.login !== 'function') { - logger.error('Cannot register auth plugin %s: authName, getWeight or login are not valid.', this.npmName, { options }) - return - } - - this.idAndPassAuths.push(options) - } - } - - private buildRegisterExternalAuth () { - const self = this - - return (options: RegisterServerAuthExternalOptions) => { - if (!options.authName || typeof options.authDisplayName !== 'function' || typeof options.onAuthRequest !== 'function') { - logger.error('Cannot register auth plugin %s: authName, authDisplayName or onAuthRequest are not valid.', this.npmName, { options }) - return - } - - this.externalAuths.push(options) - - return { - userAuthenticated (result: RegisterServerExternalAuthenticatedResult): void { - onExternalUserAuthenticated({ - npmName: self.npmName, - authName: options.authName, - authResult: result - }).catch(err => { - logger.error('Cannot execute onExternalUserAuthenticated.', { npmName: self.npmName, authName: options.authName, err }) - }) - } - } as RegisterServerAuthExternalResult - } - } - - private buildUnregisterExternalAuth () { - return (authName: string) => { - this.externalAuths = this.externalAuths.filter(a => a.authName !== authName) - } - } - - private buildUnregisterIdAndPassAuth () { - return (authName: string) => { - this.idAndPassAuths = this.idAndPassAuths.filter(a => a.authName !== authName) - } - } - - private buildSettingsManager (): PluginSettingsManager { - return { - getSetting: (name: string) => PluginModel.getSetting(this.plugin.name, this.plugin.type, name, this.settings), - - getSettings: (names: string[]) => PluginModel.getSettings(this.plugin.name, this.plugin.type, names, this.settings), - - setSetting: (name: string, value: string) => PluginModel.setSetting(this.plugin.name, this.plugin.type, name, value), - - onSettingsChange: (cb: (settings: any) => void) => this.onSettingsChangeCallbacks.push(cb) - } - } - - private buildStorageManager (): PluginStorageManager { - return { - getData: (key: string) => PluginModel.getData(this.plugin.name, this.plugin.type, key), - - storeData: (key: string, data: any) => PluginModel.storeData(this.plugin.name, this.plugin.type, key, data) - } - } - - private buildVideoLanguageManager (): PluginVideoLanguageManager { - return { - addLanguage: (key: string, label: string) => { - return this.addConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key, label }) - }, - - deleteLanguage: (key: string) => { - return this.deleteConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key }) - } - } - } - - private buildVideoCategoryManager (): PluginVideoCategoryManager { - return { - addCategory: (key: number, label: string) => { - return this.addConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label }) - }, - - deleteCategory: (key: number) => { - return this.deleteConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key }) - } - } - } - - private buildVideoPrivacyManager (): PluginVideoPrivacyManager { - return { - deletePrivacy: (key: number) => { - return this.deleteConstant({ npmName: this.npmName, type: 'privacy', obj: VIDEO_PRIVACIES, key }) - } - } - } - - private buildPlaylistPrivacyManager (): PluginPlaylistPrivacyManager { - return { - deletePlaylistPrivacy: (key: number) => { - return this.deleteConstant({ npmName: this.npmName, type: 'playlistPrivacy', obj: VIDEO_PLAYLIST_PRIVACIES, key }) - } - } - } - - private buildVideoLicenceManager (): PluginVideoLicenceManager { - return { - addLicence: (key: number, label: string) => { - return this.addConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key, label }) - }, - - deleteLicence: (key: number) => { - return this.deleteConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key }) - } - } - } - - private addConstant (parameters: { - npmName: string - type: AlterableVideoConstant - obj: VideoConstant - key: T - label: string - }) { - const { npmName, type, obj, key, label } = parameters - - if (obj[key]) { - logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key) - return false - } - - if (!this.updatedVideoConstants[type][npmName]) { - this.updatedVideoConstants[type][npmName] = { - added: [], - deleted: [] - } - } - - this.updatedVideoConstants[type][npmName].added.push({ key, label }) - obj[key] = label - - return true - } - - private deleteConstant (parameters: { - npmName: string - type: AlterableVideoConstant - obj: VideoConstant - key: T - }) { - const { npmName, type, obj, key } = parameters - - if (!obj[key]) { - logger.warn('Cannot delete %s %s by plugin %s: key does not exist.', type, npmName, key) - return false - } - - if (!this.updatedVideoConstants[type][npmName]) { - this.updatedVideoConstants[type][npmName] = { - added: [], - deleted: [] - } - } - - this.updatedVideoConstants[type][npmName].deleted.push({ key, label: obj[key] }) - delete obj[key] - - return true - } -} diff --git a/server/lib/plugins/register-helpers.ts b/server/lib/plugins/register-helpers.ts new file mode 100644 index 000000000..3a38a4835 --- /dev/null +++ b/server/lib/plugins/register-helpers.ts @@ -0,0 +1,444 @@ +import * as express from 'express' +import { logger } from '@server/helpers/logger' +import { + VIDEO_CATEGORIES, + VIDEO_LANGUAGES, + VIDEO_LICENCES, + VIDEO_PLAYLIST_PRIVACIES, + VIDEO_PRIVACIES +} from '@server/initializers/constants' +import { onExternalUserAuthenticated } from '@server/lib/auth' +import { PluginModel } from '@server/models/server/plugin' +import { + RegisterServerAuthExternalOptions, + RegisterServerAuthExternalResult, + RegisterServerAuthPassOptions, + RegisterServerExternalAuthenticatedResult, + RegisterServerOptions +} from '@server/types/plugins' +import { + EncoderOptionsBuilder, + PluginPlaylistPrivacyManager, + PluginSettingsManager, + PluginStorageManager, + PluginVideoCategoryManager, + PluginVideoLanguageManager, + PluginVideoLicenceManager, + PluginVideoPrivacyManager, + RegisterServerHookOptions, + RegisterServerSettingOptions +} from '@shared/models' +import { serverHookObject } from '@shared/models/plugins/server-hook.model' +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 } + +type UpdatedVideoConstant = { + [name in AlterableVideoConstant]: { + added: { key: number | string, label: string }[] + deleted: { key: number | string, label: string }[] + } +} + +export class RegisterHelpers { + private readonly updatedVideoConstants: UpdatedVideoConstant = { + playlistPrivacy: { added: [], deleted: [] }, + privacy: { added: [], deleted: [] }, + language: { added: [], deleted: [] }, + licence: { added: [], deleted: [] }, + 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[] = [] + private externalAuths: RegisterServerAuthExternalOptions[] = [] + + private readonly onSettingsChangeCallbacks: ((settings: any) => void)[] = [] + + private readonly router: express.Router + + constructor ( + private readonly npmName: string, + private readonly plugin: PluginModel, + private readonly onHookAdded: (options: RegisterServerHookOptions) => void + ) { + this.router = express.Router() + } + + buildRegisterHelpers (): RegisterServerOptions { + const registerHook = this.buildRegisterHook() + const registerSetting = this.buildRegisterSetting() + + const getRouter = this.buildGetRouter() + + const settingsManager = this.buildSettingsManager() + const storageManager = this.buildStorageManager() + + const videoLanguageManager = this.buildVideoLanguageManager() + + const videoLicenceManager = this.buildVideoLicenceManager() + const videoCategoryManager = this.buildVideoCategoryManager() + + const videoPrivacyManager = this.buildVideoPrivacyManager() + const playlistPrivacyManager = this.buildPlaylistPrivacyManager() + + const transcodingManager = this.buildTranscodingManager() + + const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth() + const registerExternalAuth = this.buildRegisterExternalAuth() + const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth() + const unregisterExternalAuth = this.buildUnregisterExternalAuth() + + const peertubeHelpers = buildPluginHelpers(this.npmName) + + return { + registerHook, + registerSetting, + + getRouter, + + settingsManager, + storageManager, + + videoLanguageManager, + videoCategoryManager, + videoLicenceManager, + + videoPrivacyManager, + playlistPrivacyManager, + + transcodingManager, + + registerIdAndPassAuth, + registerExternalAuth, + unregisterIdAndPassAuth, + unregisterExternalAuth, + + peertubeHelpers + } + } + + reinitVideoConstants (npmName: string) { + const hash = { + language: VIDEO_LANGUAGES, + licence: VIDEO_LICENCES, + category: VIDEO_CATEGORIES, + privacy: VIDEO_PRIVACIES, + playlistPrivacy: VIDEO_PLAYLIST_PRIVACIES + } + const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category', 'privacy', 'playlistPrivacy' ] + + for (const type of types) { + const updatedConstants = this.updatedVideoConstants[type][npmName] + if (!updatedConstants) continue + + for (const added of updatedConstants.added) { + delete hash[type][added.key] + } + + for (const deleted of updatedConstants.deleted) { + hash[type][deleted.key] = deleted.label + } + + delete this.updatedVideoConstants[type][npmName] + } + } + + 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 + } + + getRouter () { + return this.router + } + + getIdAndPassAuths () { + return this.idAndPassAuths + } + + getExternalAuths () { + return this.externalAuths + } + + getOnSettingsChangedCallbacks () { + return this.onSettingsChangeCallbacks + } + + private buildGetRouter () { + return () => this.router + } + + private buildRegisterSetting () { + return (options: RegisterServerSettingOptions) => { + this.settings.push(options) + } + } + + private buildRegisterHook () { + return (options: RegisterServerHookOptions) => { + if (serverHookObject[options.target] !== true) { + logger.warn('Unknown hook %s of plugin %s. Skipping.', options.target, this.npmName) + return + } + + return this.onHookAdded(options) + } + } + + private buildRegisterIdAndPassAuth () { + return (options: RegisterServerAuthPassOptions) => { + if (!options.authName || typeof options.getWeight !== 'function' || typeof options.login !== 'function') { + logger.error('Cannot register auth plugin %s: authName, getWeight or login are not valid.', this.npmName, { options }) + return + } + + this.idAndPassAuths.push(options) + } + } + + private buildRegisterExternalAuth () { + const self = this + + return (options: RegisterServerAuthExternalOptions) => { + if (!options.authName || typeof options.authDisplayName !== 'function' || typeof options.onAuthRequest !== 'function') { + logger.error('Cannot register auth plugin %s: authName, authDisplayName or onAuthRequest are not valid.', this.npmName, { options }) + return + } + + this.externalAuths.push(options) + + return { + userAuthenticated (result: RegisterServerExternalAuthenticatedResult): void { + onExternalUserAuthenticated({ + npmName: self.npmName, + authName: options.authName, + authResult: result + }).catch(err => { + logger.error('Cannot execute onExternalUserAuthenticated.', { npmName: self.npmName, authName: options.authName, err }) + }) + } + } as RegisterServerAuthExternalResult + } + } + + private buildUnregisterExternalAuth () { + return (authName: string) => { + this.externalAuths = this.externalAuths.filter(a => a.authName !== authName) + } + } + + private buildUnregisterIdAndPassAuth () { + return (authName: string) => { + this.idAndPassAuths = this.idAndPassAuths.filter(a => a.authName !== authName) + } + } + + private buildSettingsManager (): PluginSettingsManager { + return { + getSetting: (name: string) => PluginModel.getSetting(this.plugin.name, this.plugin.type, name, this.settings), + + getSettings: (names: string[]) => PluginModel.getSettings(this.plugin.name, this.plugin.type, names, this.settings), + + setSetting: (name: string, value: string) => PluginModel.setSetting(this.plugin.name, this.plugin.type, name, value), + + onSettingsChange: (cb: (settings: any) => void) => this.onSettingsChangeCallbacks.push(cb) + } + } + + private buildStorageManager (): PluginStorageManager { + return { + getData: (key: string) => PluginModel.getData(this.plugin.name, this.plugin.type, key), + + storeData: (key: string, data: any) => PluginModel.storeData(this.plugin.name, this.plugin.type, key, data) + } + } + + private buildVideoLanguageManager (): PluginVideoLanguageManager { + return { + addLanguage: (key: string, label: string) => { + return this.addConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key, label }) + }, + + deleteLanguage: (key: string) => { + return this.deleteConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key }) + } + } + } + + private buildVideoCategoryManager (): PluginVideoCategoryManager { + return { + addCategory: (key: number, label: string) => { + return this.addConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label }) + }, + + deleteCategory: (key: number) => { + return this.deleteConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key }) + } + } + } + + private buildVideoPrivacyManager (): PluginVideoPrivacyManager { + return { + deletePrivacy: (key: number) => { + return this.deleteConstant({ npmName: this.npmName, type: 'privacy', obj: VIDEO_PRIVACIES, key }) + } + } + } + + private buildPlaylistPrivacyManager (): PluginPlaylistPrivacyManager { + return { + deletePlaylistPrivacy: (key: number) => { + return this.deleteConstant({ npmName: this.npmName, type: 'playlistPrivacy', obj: VIDEO_PLAYLIST_PRIVACIES, key }) + } + } + } + + private buildVideoLicenceManager (): PluginVideoLicenceManager { + return { + addLicence: (key: number, label: string) => { + return this.addConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key, label }) + }, + + deleteLicence: (key: number) => { + return this.deleteConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key }) + } + } + } + + private addConstant (parameters: { + npmName: string + type: AlterableVideoConstant + obj: VideoConstant + key: T + label: string + }) { + const { npmName, type, obj, key, label } = parameters + + if (obj[key]) { + logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key) + return false + } + + if (!this.updatedVideoConstants[type][npmName]) { + this.updatedVideoConstants[type][npmName] = { + added: [], + deleted: [] + } + } + + this.updatedVideoConstants[type][npmName].added.push({ key, label }) + obj[key] = label + + return true + } + + private deleteConstant (parameters: { + npmName: string + type: AlterableVideoConstant + obj: VideoConstant + key: T + }) { + const { npmName, type, obj, key } = parameters + + if (!obj[key]) { + logger.warn('Cannot delete %s %s by plugin %s: key does not exist.', type, npmName, key) + return false + } + + if (!this.updatedVideoConstants[type][npmName]) { + this.updatedVideoConstants[type][npmName] = { + added: [], + deleted: [] + } + } + + this.updatedVideoConstants[type][npmName].deleted.push({ key, label: obj[key] }) + delete obj[key] + + 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) + } + } + } +} 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 @@ 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()) } 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: 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, 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 = [ 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 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 { 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', 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 () { 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, 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) { 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, 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 () { 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 index 000000000..5990ce1ce --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js @@ -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 index 000000000..bedbfa051 --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-transcoding-one/package.json @@ -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 index 000000000..a914bce49 --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-transcoding-two/main.js @@ -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 index 000000000..34be0454b --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-transcoding-two/package.json @@ -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": {} +} 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 @@ 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 index 000000000..96ff4c2fe --- /dev/null +++ b/server/tests/plugins/plugin-transcoding.ts @@ -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 ]) + }) +}) 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 { 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 -- cgit v1.2.3