# 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
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
# 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
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
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
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()
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,
transcoding: {
enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
- enabledResolutions: getEnabledResolutions('live')
+ enabledResolutions: getEnabledResolutions('live'),
+ profile: CONFIG.LIVE.TRANSCODING.PROFILE,
+ availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('live')
},
rtmp: {
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'],
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'],
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'
// Encoder options
// ---------------------------------------------------------------------------
-// Options builders
-
-export type EncoderOptionsBuilder = (params: {
- input: string
- resolution: VideoResolution
- fps?: number
- streamNum?: number
-}) => Promise<EncoderOptions> | EncoderOptions
+type StreamType = 'audio' | 'video'
-// Options types
+// ---------------------------------------------------------------------------
+// Encoders support
+// ---------------------------------------------------------------------------
-export interface EncoderOptions {
- copy?: boolean
- outputOptions: string[]
-}
+// Detect supported encoders by ffmpeg
+let supportedEncoders: Map<string, boolean>
+async function checkFFmpegEncoders (peertubeAvailableEncoders: AvailableEncoders): Promise<Map<string, boolean>> {
+ if (supportedEncoders !== undefined) {
+ return supportedEncoders
+ }
-// All our encoders
+ const getAvailableEncodersPromise = promisify0(ffmpeg.getAvailableEncoders)
+ const availableFFmpegEncoders = await getAvailableEncodersPromise()
-export interface EncoderProfile <T> {
- [ profile: string ]: T
+ const searchEncoders = new Set<string>()
+ for (const type of [ 'live', 'vod' ]) {
+ for (const streamType of [ 'audio', 'video' ]) {
+ for (const encoder of peertubeAvailableEncoders.encodersToTry[type][streamType]) {
+ searchEncoders.add(encoder)
+ }
+ }
+ }
- default: T
-}
+ supportedEncoders = new Map<string, boolean>()
-export type AvailableEncoders = {
- live: {
- [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
+ for (const searchEncoder of searchEncoders) {
+ supportedEncoders.set(searchEncoder, availableFFmpegEncoders[searchEncoder] !== undefined)
}
- vod: {
- [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
- }
+ logger.info('Built supported ffmpeg encoders.', { supportedEncoders, searchEncoders })
- encodersToTry: {
- video: string[]
- audio: string[]
- }
+ return supportedEncoders
}
-type StreamType = 'audio' | 'video'
+function resetSupportedEncoders () {
+ supportedEncoders = undefined
+}
// ---------------------------------------------------------------------------
// Image manipulation
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)
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)
}) {
const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options
- const encodersToTry = availableEncoders.encodersToTry[streamType]
- const encoders = availableEncoders[videoType]
+ const encodersToTry = availableEncoders.encodersToTry[videoType][streamType]
+ const encoders = availableEncoders.available[videoType]
for (const encoder of encodersToTry) {
- if (!(await checkFFmpegEncoders()).get(encoder) || !encoders[encoder]) continue
+ if (!(await checkFFmpegEncoders(availableEncoders)).get(encoder)) {
+ logger.debug('Encoder %s not available in ffmpeg, skipping.', encoder)
+ continue
+ }
+
+ if (!encoders[encoder]) {
+ logger.debug('Encoder %s not available in peertube encoders, skipping.', encoder)
+ continue
+ }
// An object containing available profiles for this encoder
const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = encoders[encoder]
if (!parsedAudio.audioStream) {
localCommand = localCommand.noAudio()
- streamsToProcess = [ 'audio' ]
+ streamsToProcess = [ 'video' ]
}
for (const streamType of streamsToProcess) {
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)
transcode,
runCommand,
+ resetSupportedEncoders,
+
// builders
buildx264VODCommand
}
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 = {}
throw new Error('Unavailable encode codec ' + codec + ' in FFmpeg')
}
}
-
- return checkFFmpegEncoders()
-}
-
-// Detect supported encoders by ffmpeg
-let supportedEncoders: Map<string, boolean>
-async function checkFFmpegEncoders (): Promise<Map<string, boolean>> {
- if (supportedEncoders !== undefined) {
- return supportedEncoders
- }
-
- const Ffmpeg = require('fluent-ffmpeg')
- const getAvailableEncodersPromise = promisify0(Ffmpeg.getAvailableEncoders)
- const availableEncoders = await getAvailableEncodersPromise()
-
- const searchEncoders = [
- 'aac',
- 'libfdk_aac',
- 'libx264'
- ]
-
- supportedEncoders = new Map<string, boolean>()
-
- for (const searchEncoder of searchEncoders) {
- supportedEncoders.set(searchEncoder, availableEncoders[searchEncoder] !== undefined)
- }
-
- return supportedEncoders
}
function checkNodeVersion () {
export {
checkFFmpeg,
- checkFFmpegEncoders,
checkMissedConfig,
checkNodeVersion
}
get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') },
get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') },
get THREADS () { return config.get<number>('transcoding.threads') },
+ get PROFILE () { return config.get<string>('transcoding.profile') },
RESOLUTIONS: {
get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') },
get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') },
TRANSCODING: {
get ENABLED () { return config.get<boolean>('live.transcoding.enabled') },
get THREADS () { return config.get<number>('live.transcoding.threads') },
+ get PROFILE () { return config.get<string>('live.transcoding.profile') },
RESOLUTIONS: {
get '240p' () { return config.get<boolean>('live.transcoding.resolutions.240p') },
resolutions: allResolutions,
fps,
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
- profile: 'default'
+ profile: CONFIG.LIVE.TRANSCODING.PROFILE
})
: getLiveMuxingCommand(rtmpUrl, outPath)
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 {
css: string[]
// Only if this is a plugin
- registerHelpersStore?: RegisterHelpersStore
+ registerHelpers?: RegisterHelpers
unregister?: Function
}
npmName: p.npmName,
name: p.name,
version: p.version,
- idAndPassAuths: p.registerHelpersStore.getIdAndPassAuths()
+ idAndPassAuths: p.registerHelpers.getIdAndPassAuths()
}))
.filter(v => v.idAndPassAuths.length !== 0)
}
npmName: p.npmName,
name: p.name,
version: p.version,
- externalAuths: p.registerHelpersStore.getExternalAuths()
+ externalAuths: p.registerHelpers.getExternalAuths()
}))
.filter(v => v.externalAuths.length !== 0)
}
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) {
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) {
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()
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 } = {}
staticDirs: packageJSON.staticDirs,
clientScripts,
css: packageJSON.css,
- registerHelpersStore: registerHelpersStore || undefined,
+ registerHelpers: registerHelpers || undefined,
unregister: library ? library.unregister : undefined
}
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)
}
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] = []
})
}
- 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()
}
}
RegisterServerOptions
} from '@server/types/plugins'
import {
+ EncoderOptionsBuilder,
PluginPlaylistPrivacyManager,
PluginSettingsManager,
PluginStorageManager,
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 }
}
}
-export class RegisterHelpersStore {
+export class RegisterHelpers {
private readonly updatedVideoConstants: UpdatedVideoConstant = {
playlistPrivacy: { added: [], deleted: [] },
privacy: { 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[] = []
const videoPrivacyManager = this.buildVideoPrivacyManager()
const playlistPrivacyManager = this.buildPlaylistPrivacyManager()
+ const transcodingManager = this.buildTranscodingManager()
+
const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth()
const registerExternalAuth = this.buildRegisterExternalAuth()
const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth()
videoPrivacyManager,
playlistPrivacyManager,
+ transcodingManager,
+
registerIdAndPassAuth,
registerExternalAuth,
unregisterIdAndPassAuth,
}
}
+ 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
}
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)
+ }
+ }
+ }
}
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,
// 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 = {
}
}
- 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
.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())
}
outputPath: videoTranscodedPath,
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
- profile: 'default',
+ profile: CONFIG.TRANSCODING.PROFILE,
resolution: inputVideoFile.resolution,
outputPath: videoTranscodedPath,
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
- profile: 'default',
+ profile: CONFIG.TRANSCODING.PROFILE,
resolution,
outputPath: videoTranscodedPath,
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
- profile: 'default',
+ profile: CONFIG.TRANSCODING.PROFILE,
resolution,
isPortraitMode: isPortrait,
outputPath: videoTranscodedPath,
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
- profile: 'default',
+ profile: CONFIG.TRANSCODING.PROFILE,
audioPath: audioInputPath,
resolution,
outputPath,
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
- profile: 'default',
+ profile: CONFIG.TRANSCODING.PROFILE,
resolution,
copyCodecs,
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
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'
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',
allowAdditionalExtensions: true,
allowAudioFiles: true,
threads: 1,
+ profile: 'vod_profile',
resolutions: {
'0p': false,
'240p': false,
transcoding: {
enabled: true,
threads: 4,
+ profile: 'live_profile',
resolutions: {
'240p': true,
'360p': true,
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
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
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
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
allowAdditionalExtensions: true,
allowAudioFiles: true,
threads: 1,
+ profile: 'vod_profile',
resolutions: {
'0p': false,
'240p': false,
transcoding: {
enabled: true,
threads: 4,
+ profile: 'live_profile',
resolutions: {
'240p': true,
'360p': true,
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)
}
})
--- /dev/null
+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
+}
--- /dev/null
+{
+ "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": {}
+}
--- /dev/null
+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
+}
--- /dev/null
+{
+ "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": {}
+}
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'
--- /dev/null
+/* 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 ])
+ })
+})
PluginPlaylistPrivacyManager,
PluginSettingsManager,
PluginStorageManager,
+ PluginTranscodingManager,
PluginVideoCategoryManager,
PluginVideoLanguageManager,
PluginVideoLicenceManager,
videoPrivacyManager: PluginVideoPrivacyManager
playlistPrivacyManager: PluginPlaylistPrivacyManager
+ transcodingManager: PluginTranscodingManager
+
registerIdAndPassAuth: (options: RegisterServerAuthPassOptions) => void
registerExternalAuth: (options: RegisterServerAuthExternalOptions) => RegisterServerAuthExternalResult
unregisterIdAndPassAuth: (authName: string) => void
allowAdditionalExtensions: true,
allowAudioFiles: true,
threads: 1,
+ profile: 'default',
resolutions: {
'0p': false,
'240p': false,
transcoding: {
enabled: true,
threads: 4,
+ profile: 'default',
resolutions: {
'240p': true,
'360p': true,
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'
--- /dev/null
+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
+}
allowAudioFiles: boolean
threads: number
+
+ profile: string
+
resolutions: ConfigResolutions & { '0p': boolean }
webtorrent: {
transcoding: {
enabled: boolean
threads: number
+ profile: string
resolutions: ConfigResolutions
}
}
}
enabledResolutions: number[]
+
+ profile: string
+ availableProfiles: string[]
}
live: {
enabled: boolean
enabledResolutions: number[]
+
+ profile: string
+ availableProfiles: string[]
}
rtmp: {
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'
--- /dev/null
+import { VideoResolution } from './video-resolution.enum'
+
+// Types used by plugins and ffmpeg-utils
+
+export type EncoderOptionsBuilder = (params: {
+ input: string
+ resolution: VideoResolution
+ fps?: number
+ streamNum?: number
+}) => Promise<EncoderOptions> | EncoderOptions
+
+export interface EncoderOptions {
+ copy?: boolean // Copy stream? Default to false
+
+ outputOptions: string[]
+}
+
+// All our encoders
+
+export interface EncoderProfile <T> {
+ [ profile: string ]: T
+
+ default: T
+}
+
+export type AvailableEncoders = {
+ available: {
+ live: {
+ [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
+ }
+
+ vod: {
+ [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
+ }
+ }
+
+ encodersToTry: {
+ vod: {
+ video: string[]
+ audio: string[]
+ }
+
+ live: {
+ video: string[]
+ audio: string[]
+ }
+ }
+}