From 1896bca09e088b0da9d5e845407ecebae330618c Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 28 Jan 2021 15:52:44 +0100 Subject: [PATCH] Support transcoding options/encoders by plugins --- config/default.yaml | 14 ++ config/production.yaml.example | 16 +- 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 +- ...n-helpers.ts => plugin-helpers-builder.ts} | 0 server/lib/plugins/plugin-manager.ts | 35 +-- ...r-helpers-store.ts => register-helpers.ts} | 91 ++++++- 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 +- .../main.js | 35 +++ .../package.json | 20 ++ .../main.js | 38 +++ .../package.json | 20 ++ server/tests/plugins/index.ts | 9 +- server/tests/plugins/plugin-transcoding.ts | 226 ++++++++++++++++++ .../plugins/register-server-option.model.ts | 3 + shared/extra-utils/server/config.ts | 2 + shared/models/plugins/index.ts | 1 + .../plugin-transcoding-manager.model.ts | 11 + shared/models/server/custom-config.model.ts | 4 + shared/models/server/server-config.model.ts | 6 + shared/models/videos/index.ts | 1 + .../models/videos/video-transcoding.model.ts | 48 ++++ 32 files changed, 744 insertions(+), 125 deletions(-) rename server/lib/plugins/{plugin-helpers.ts => plugin-helpers-builder.ts} (100%) rename server/lib/plugins/{register-helpers-store.ts => register-helpers.ts} (78%) 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 create mode 100644 shared/models/plugins/plugin-transcoding-manager.model.ts create mode 100644 shared/models/videos/video-transcoding.model.ts diff --git a/config/default.yaml b/config/default.yaml index b9e382fa7..283e0ab93 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -223,11 +223,20 @@ user: # Please, do not disable transcoding since many uploaded videos will not work transcoding: enabled: true + # Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos allow_additional_extensions: true + # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file allow_audio_files: true + threads: 1 + + # Choose the transcoding profile + # New profiles can be added by plugins + # Available in core PeerTube: 'default' + profile: 'default' + resolutions: # Only created if the original video has a higher resolution, uses more storage! 0p: false # audio-only (creates mp4 without video stream, always created when enabled) 240p: false @@ -283,6 +292,11 @@ live: enabled: true threads: 2 + # Choose the transcoding profile + # New profiles can be added by plugins + # Available in core PeerTube: 'default' + profile: 'default' + resolutions: 240p: false 360p: false diff --git a/config/production.yaml.example b/config/production.yaml.example index b616c6ced..66c981dd5 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -236,11 +236,20 @@ user: # Please, do not disable transcoding since many uploaded videos will not work transcoding: enabled: true + # Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos allow_additional_extensions: true + # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file allow_audio_files: true + threads: 1 + + # Choose the transcoding profile + # New profiles can be added by plugins + # Available in core PeerTube: 'default' + profile: 'default' + resolutions: # Only created if the original video has a higher resolution, uses more storage! 0p: false # audio-only (creates mp4 without video stream, always created when enabled) 240p: false @@ -270,7 +279,7 @@ live: enabled: false # Limit lives duration - # Set null to disable duration limit + # -1 == unlimited max_duration: -1 # For example: '5 hours' # Limit max number of live videos created on your instance @@ -296,6 +305,11 @@ live: enabled: true threads: 2 + # Choose the transcoding profile + # New profiles can be added by plugins + # Available in core PeerTube: 'default' + profile: 'default' + resolutions: 240p: false 360p: false 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.ts b/server/lib/plugins/plugin-helpers-builder.ts similarity index 100% rename from server/lib/plugins/plugin-helpers.ts rename to 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' 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.ts similarity index 78% rename from server/lib/plugins/register-helpers-store.ts rename to 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 { RegisterServerOptions } from '@server/types/plugins' import { + EncoderOptionsBuilder, PluginPlaylistPrivacyManager, PluginSettingsManager, PluginStorageManager, @@ -28,7 +29,8 @@ import { RegisterServerSettingOptions } from '@shared/models' import { serverHookObject } from '@shared/models/plugins/server-hook.model' -import { buildPluginHelpers } from './plugin-helpers' +import { VideoTranscodingProfilesManager } from '../video-transcoding-profiles' +import { buildPluginHelpers } from './plugin-helpers-builder' type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' type VideoConstant = { [key in number | string]: string } @@ -40,7 +42,7 @@ type UpdatedVideoConstant = { } } -export class RegisterHelpersStore { +export class RegisterHelpers { private readonly updatedVideoConstants: UpdatedVideoConstant = { playlistPrivacy: { added: [], deleted: [] }, privacy: { added: [], deleted: [] }, @@ -49,6 +51,23 @@ export class RegisterHelpersStore { category: { added: [], deleted: [] } } + private readonly transcodingProfiles: { + [ npmName: string ]: { + type: 'vod' | 'live' + encoder: string + profile: string + }[] + } = {} + + private readonly transcodingEncoders: { + [ npmName: string ]: { + type: 'vod' | 'live' + streamType: 'audio' | 'video' + encoder: string + priority: number + }[] + } = {} + private readonly settings: RegisterServerSettingOptions[] = [] private idAndPassAuths: RegisterServerAuthPassOptions[] = [] @@ -83,6 +102,8 @@ export class RegisterHelpersStore { const videoPrivacyManager = this.buildVideoPrivacyManager() const playlistPrivacyManager = this.buildPlaylistPrivacyManager() + const transcodingManager = this.buildTranscodingManager() + const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth() const registerExternalAuth = this.buildRegisterExternalAuth() const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth() @@ -106,6 +127,8 @@ export class RegisterHelpersStore { videoPrivacyManager, playlistPrivacyManager, + transcodingManager, + registerIdAndPassAuth, registerExternalAuth, unregisterIdAndPassAuth, @@ -141,6 +164,22 @@ export class RegisterHelpersStore { } } + reinitTranscodingProfilesAndEncoders (npmName: string) { + const profiles = this.transcodingProfiles[npmName] + if (Array.isArray(profiles)) { + for (const profile of profiles) { + VideoTranscodingProfilesManager.Instance.removeProfile(profile) + } + } + + const encoders = this.transcodingEncoders[npmName] + if (Array.isArray(encoders)) { + for (const o of encoders) { + VideoTranscodingProfilesManager.Instance.removeEncoderPriority(o.type, o.streamType, o.encoder, o.priority) + } + } + } + getSettings () { return this.settings } @@ -354,4 +393,52 @@ export class RegisterHelpersStore { return true } + + private buildTranscodingManager () { + const self = this + + function addProfile (type: 'live' | 'vod', encoder: string, profile: string, builder: EncoderOptionsBuilder) { + if (profile === 'default') { + logger.error('A plugin cannot add a default live transcoding profile') + return false + } + + VideoTranscodingProfilesManager.Instance.addProfile({ + type, + encoder, + profile, + builder + }) + + if (!self.transcodingProfiles[self.npmName]) self.transcodingProfiles[self.npmName] = [] + self.transcodingProfiles[self.npmName].push({ type, encoder, profile }) + + return true + } + + function addEncoderPriority (type: 'live' | 'vod', streamType: 'audio' | 'video', encoder: string, priority: number) { + VideoTranscodingProfilesManager.Instance.addEncoderPriority(type, streamType, encoder, priority) + + if (!self.transcodingEncoders[self.npmName]) self.transcodingEncoders[self.npmName] = [] + self.transcodingEncoders[self.npmName].push({ type, streamType, encoder, priority }) + } + + return { + addLiveProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder) { + return addProfile('live', encoder, profile, builder) + }, + + addVODProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder) { + return addProfile('vod', encoder, profile, builder) + }, + + addLiveEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number) { + return addEncoderPriority('live', streamType, encoder, priority) + }, + + addVODEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number) { + return addEncoderPriority('vod', streamType, encoder, priority) + } + } + } } 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 diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts index 4e09e0412..8998da8b6 100644 --- a/shared/extra-utils/server/config.ts +++ b/shared/extra-utils/server/config.ts @@ -112,6 +112,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti allowAdditionalExtensions: true, allowAudioFiles: true, threads: 1, + profile: 'default', resolutions: { '0p': false, '240p': false, @@ -138,6 +139,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti transcoding: { enabled: true, threads: 4, + profile: 'default', resolutions: { '240p': true, '360p': true, diff --git a/shared/models/plugins/index.ts b/shared/models/plugins/index.ts index 83ed6f583..96621460a 100644 --- a/shared/models/plugins/index.ts +++ b/shared/models/plugins/index.ts @@ -11,6 +11,7 @@ export * from './plugin-package-json.model' export * from './plugin-playlist-privacy-manager.model' export * from './plugin-settings-manager.model' export * from './plugin-storage-manager.model' +export * from './plugin-transcoding-manager.model' export * from './plugin-translation.model' export * from './plugin-video-category-manager.model' export * from './plugin-video-language-manager.model' diff --git a/shared/models/plugins/plugin-transcoding-manager.model.ts b/shared/models/plugins/plugin-transcoding-manager.model.ts new file mode 100644 index 000000000..ff89687e9 --- /dev/null +++ b/shared/models/plugins/plugin-transcoding-manager.model.ts @@ -0,0 +1,11 @@ +import { EncoderOptionsBuilder } from '../videos/video-transcoding.model' + +export interface PluginTranscodingManager { + addLiveProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder): boolean + + addVODProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder): boolean + + addLiveEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number): void + + addVODEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number): void +} diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts index a57237414..d23b8abef 100644 --- a/shared/models/server/custom-config.model.ts +++ b/shared/models/server/custom-config.model.ts @@ -87,6 +87,9 @@ export interface CustomConfig { allowAudioFiles: boolean threads: number + + profile: string + resolutions: ConfigResolutions & { '0p': boolean } webtorrent: { @@ -110,6 +113,7 @@ export interface CustomConfig { transcoding: { enabled: boolean threads: number + profile: string resolutions: ConfigResolutions } } diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts index 47d0e623b..efde4ad9d 100644 --- a/shared/models/server/server-config.model.ts +++ b/shared/models/server/server-config.model.ts @@ -96,6 +96,9 @@ export interface ServerConfig { } enabledResolutions: number[] + + profile: string + availableProfiles: string[] } live: { @@ -110,6 +113,9 @@ export interface ServerConfig { enabled: boolean enabledResolutions: number[] + + profile: string + availableProfiles: string[] } rtmp: { diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index abf144f23..fac3e0b2f 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts @@ -34,6 +34,7 @@ export * from './video-state.enum' export * from './video-streaming-playlist.model' export * from './video-streaming-playlist.type' +export * from './video-transcoding.model' export * from './video-transcoding-fps.model' export * from './video-update.model' diff --git a/shared/models/videos/video-transcoding.model.ts b/shared/models/videos/video-transcoding.model.ts new file mode 100644 index 000000000..06b555c16 --- /dev/null +++ b/shared/models/videos/video-transcoding.model.ts @@ -0,0 +1,48 @@ +import { VideoResolution } from './video-resolution.enum' + +// Types used by plugins and ffmpeg-utils + +export type EncoderOptionsBuilder = (params: { + input: string + resolution: VideoResolution + fps?: number + streamNum?: number +}) => Promise | EncoderOptions + +export interface EncoderOptions { + copy?: boolean // Copy stream? Default to false + + outputOptions: string[] +} + +// All our encoders + +export interface EncoderProfile { + [ profile: string ]: T + + default: T +} + +export type AvailableEncoders = { + available: { + live: { + [ encoder: string ]: EncoderProfile + } + + vod: { + [ encoder: string ]: EncoderProfile + } + } + + encodersToTry: { + vod: { + video: string[] + audio: string[] + } + + live: { + video: string[] + audio: string[] + } + } +} -- 2.41.0