1
0
Fork 0

Support transcoding options/encoders by plugins

This commit is contained in:
Chocobozzz 2021-01-28 15:52:44 +01:00
parent 529b37527c
commit 1896bca09e
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
32 changed files with 754 additions and 135 deletions

View File

@ -223,11 +223,20 @@ user:
# Please, do not disable transcoding since many uploaded videos will not work
transcoding:
enabled: true
# Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos
allow_additional_extensions: true
# If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file
allow_audio_files: true
threads: 1
# Choose the transcoding profile
# New profiles can be added by plugins
# Available in core PeerTube: 'default'
profile: 'default'
resolutions: # Only created if the original video has a higher resolution, uses more storage!
0p: false # audio-only (creates mp4 without video stream, always created when enabled)
240p: false
@ -283,6 +292,11 @@ live:
enabled: true
threads: 2
# Choose the transcoding profile
# New profiles can be added by plugins
# Available in core PeerTube: 'default'
profile: 'default'
resolutions:
240p: false
360p: false

View File

@ -236,11 +236,20 @@ user:
# Please, do not disable transcoding since many uploaded videos will not work
transcoding:
enabled: true
# Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos
allow_additional_extensions: true
# If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file
allow_audio_files: true
threads: 1
# Choose the transcoding profile
# New profiles can be added by plugins
# Available in core PeerTube: 'default'
profile: 'default'
resolutions: # Only created if the original video has a higher resolution, uses more storage!
0p: false # audio-only (creates mp4 without video stream, always created when enabled)
240p: false
@ -270,7 +279,7 @@ live:
enabled: false
# Limit lives duration
# Set null to disable duration limit
# -1 == unlimited
max_duration: -1 # For example: '5 hours'
# Limit max number of live videos created on your instance
@ -296,6 +305,11 @@ live:
enabled: true
threads: 2
# Choose the transcoding profile
# New profiles can be added by plugins
# Available in core PeerTube: 'default'
profile: 'default'
resolutions:
240p: false
360p: false

View File

@ -18,6 +18,7 @@ import { PluginManager } from '../../lib/plugins/plugin-manager'
import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
import { customConfigUpdateValidator } from '../../middlewares/validators/config'
import { VideoTranscodingProfilesManager } from '@server/lib/video-transcoding-profiles'
const configRouter = express.Router()
@ -114,7 +115,9 @@ async function getConfig (req: express.Request, res: express.Response) {
webtorrent: {
enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
},
enabledResolutions: getEnabledResolutions('vod')
enabledResolutions: getEnabledResolutions('vod'),
profile: CONFIG.TRANSCODING.PROFILE,
availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('vod')
},
live: {
enabled: CONFIG.LIVE.ENABLED,
@ -126,7 +129,9 @@ async function getConfig (req: express.Request, res: express.Response) {
transcoding: {
enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
enabledResolutions: getEnabledResolutions('live')
enabledResolutions: getEnabledResolutions('live'),
profile: CONFIG.LIVE.TRANSCODING.PROFILE,
availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('live')
},
rtmp: {
@ -412,6 +417,7 @@ function customConfig (): CustomConfig {
allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES,
threads: CONFIG.TRANSCODING.THREADS,
profile: CONFIG.TRANSCODING.PROFILE,
resolutions: {
'0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'],
'240p': CONFIG.TRANSCODING.RESOLUTIONS['240p'],
@ -438,6 +444,7 @@ function customConfig (): CustomConfig {
transcoding: {
enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
threads: CONFIG.LIVE.TRANSCODING.THREADS,
profile: CONFIG.LIVE.TRANSCODING.PROFILE,
resolutions: {
'240p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['240p'],
'360p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['360p'],

View File

@ -3,9 +3,9 @@ import * as ffmpeg from 'fluent-ffmpeg'
import { readFile, remove, writeFile } from 'fs-extra'
import { dirname, join } from 'path'
import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants'
import { VideoResolution } from '../../shared/models/videos'
import { checkFFmpegEncoders } from '../initializers/checker-before-init'
import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos'
import { CONFIG } from '../initializers/config'
import { promisify0 } from './core-utils'
import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils'
import { processImage } from './image-utils'
import { logger } from './logger'
@ -21,47 +21,46 @@ import { logger } from './logger'
// Encoder options
// ---------------------------------------------------------------------------
// Options builders
export type EncoderOptionsBuilder = (params: {
input: string
resolution: VideoResolution
fps?: number
streamNum?: number
}) => Promise<EncoderOptions> | EncoderOptions
// Options types
export interface EncoderOptions {
copy?: boolean
outputOptions: string[]
}
// All our encoders
export interface EncoderProfile <T> {
[ profile: string ]: T
default: T
}
export type AvailableEncoders = {
live: {
[ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
}
vod: {
[ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
}
encodersToTry: {
video: string[]
audio: string[]
}
}
type StreamType = 'audio' | 'video'
// ---------------------------------------------------------------------------
// Encoders support
// ---------------------------------------------------------------------------
// Detect supported encoders by ffmpeg
let supportedEncoders: Map<string, boolean>
async function checkFFmpegEncoders (peertubeAvailableEncoders: AvailableEncoders): Promise<Map<string, boolean>> {
if (supportedEncoders !== undefined) {
return supportedEncoders
}
const getAvailableEncodersPromise = promisify0(ffmpeg.getAvailableEncoders)
const availableFFmpegEncoders = await getAvailableEncodersPromise()
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)
}
}
}
supportedEncoders = new Map<string, boolean>()
for (const searchEncoder of searchEncoders) {
supportedEncoders.set(searchEncoder, availableFFmpegEncoders[searchEncoder] !== undefined)
}
logger.info('Built supported ffmpeg encoders.', { supportedEncoders, searchEncoders })
return supportedEncoders
}
function resetSupportedEncoders () {
supportedEncoders = undefined
}
// ---------------------------------------------------------------------------
// Image manipulation
// ---------------------------------------------------------------------------
@ -275,7 +274,7 @@ async function getLiveTranscodingCommand (options: {
addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
logger.debug('Apply ffmpeg live video params from %s.', builderResult.encoder, builderResult)
logger.debug('Apply ffmpeg live video params from %s using %s profile.', builderResult.encoder, profile, builderResult)
command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`)
command.addOutputOptions(builderResult.result.outputOptions)
@ -292,7 +291,7 @@ async function getLiveTranscodingCommand (options: {
addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
logger.debug('Apply ffmpeg live audio params from %s.', builderResult.encoder, builderResult)
logger.debug('Apply ffmpeg live audio params from %s using %s profile.', builderResult.encoder, profile, builderResult)
command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`)
command.addOutputOptions(builderResult.result.outputOptions)
@ -513,11 +512,19 @@ async function getEncoderBuilderResult (options: {
}) {
const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options
const encodersToTry = availableEncoders.encodersToTry[streamType]
const encoders = availableEncoders[videoType]
const encodersToTry = availableEncoders.encodersToTry[videoType][streamType]
const encoders = availableEncoders.available[videoType]
for (const encoder of encodersToTry) {
if (!(await checkFFmpegEncoders()).get(encoder) || !encoders[encoder]) continue
if (!(await checkFFmpegEncoders(availableEncoders)).get(encoder)) {
logger.debug('Encoder %s not available in ffmpeg, skipping.', encoder)
continue
}
if (!encoders[encoder]) {
logger.debug('Encoder %s not available in peertube encoders, skipping.', encoder)
continue
}
// An object containing available profiles for this encoder
const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = encoders[encoder]
@ -567,7 +574,7 @@ async function presetVideo (
if (!parsedAudio.audioStream) {
localCommand = localCommand.noAudio()
streamsToProcess = [ 'audio' ]
streamsToProcess = [ 'video' ]
}
for (const streamType of streamsToProcess) {
@ -587,7 +594,10 @@ async function presetVideo (
throw new Error('No available encoder found for stream ' + streamType)
}
logger.debug('Apply ffmpeg params from %s.', builderResult.encoder, builderResult)
logger.debug(
'Apply ffmpeg params from %s for %s stream of input %s using %s profile.',
builderResult.encoder, streamType, input, profile, builderResult
)
if (streamType === 'video') {
localCommand.videoCodec(builderResult.encoder)
@ -679,6 +689,8 @@ export {
transcode,
runCommand,
resetSupportedEncoders,
// builders
buildx264VODCommand
}

View File

@ -27,6 +27,14 @@ function getLoggerReplacer () {
seen.add(value)
}
if (value instanceof Set) {
return Array.from(value)
}
if (value instanceof Map) {
return Array.from(value.entries())
}
if (value instanceof Error) {
const error = {}

View File

@ -97,34 +97,6 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) {
throw new Error('Unavailable encode codec ' + codec + ' in FFmpeg')
}
}
return checkFFmpegEncoders()
}
// Detect supported encoders by ffmpeg
let supportedEncoders: Map<string, boolean>
async function checkFFmpegEncoders (): Promise<Map<string, boolean>> {
if (supportedEncoders !== undefined) {
return supportedEncoders
}
const Ffmpeg = require('fluent-ffmpeg')
const getAvailableEncodersPromise = promisify0(Ffmpeg.getAvailableEncoders)
const availableEncoders = await getAvailableEncodersPromise()
const searchEncoders = [
'aac',
'libfdk_aac',
'libx264'
]
supportedEncoders = new Map<string, boolean>()
for (const searchEncoder of searchEncoders) {
supportedEncoders.set(searchEncoder, availableEncoders[searchEncoder] !== undefined)
}
return supportedEncoders
}
function checkNodeVersion () {
@ -143,7 +115,6 @@ function checkNodeVersion () {
export {
checkFFmpeg,
checkFFmpegEncoders,
checkMissedConfig,
checkNodeVersion
}

View File

@ -188,6 +188,7 @@ const CONFIG = {
get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') },
get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') },
get THREADS () { return config.get<number>('transcoding.threads') },
get PROFILE () { return config.get<string>('transcoding.profile') },
RESOLUTIONS: {
get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') },
get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') },
@ -221,6 +222,7 @@ const CONFIG = {
TRANSCODING: {
get ENABLED () { return config.get<boolean>('live.transcoding.enabled') },
get THREADS () { return config.get<number>('live.transcoding.threads') },
get PROFILE () { return config.get<string>('live.transcoding.profile') },
RESOLUTIONS: {
get '240p' () { return config.get<boolean>('live.transcoding.resolutions.240p') },

View File

@ -338,7 +338,7 @@ class LiveManager {
resolutions: allResolutions,
fps,
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
profile: 'default'
profile: CONFIG.LIVE.TRANSCODING.PROFILE
})
: getLiveMuxingCommand(rtmpUrl, outPath)

View File

@ -20,7 +20,7 @@ import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants'
import { PluginModel } from '../../models/server/plugin'
import { PluginLibrary, RegisterServerAuthExternalOptions, RegisterServerAuthPassOptions, RegisterServerOptions } from '../../types/plugins'
import { ClientHtml } from '../client-html'
import { RegisterHelpersStore } from './register-helpers-store'
import { RegisterHelpers } from './register-helpers'
import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
export interface RegisteredPlugin {
@ -40,7 +40,7 @@ export interface RegisteredPlugin {
css: string[]
// Only if this is a plugin
registerHelpersStore?: RegisterHelpersStore
registerHelpers?: RegisterHelpers
unregister?: Function
}
@ -109,7 +109,7 @@ export class PluginManager implements ServerHook {
npmName: p.npmName,
name: p.name,
version: p.version,
idAndPassAuths: p.registerHelpersStore.getIdAndPassAuths()
idAndPassAuths: p.registerHelpers.getIdAndPassAuths()
}))
.filter(v => v.idAndPassAuths.length !== 0)
}
@ -120,7 +120,7 @@ export class PluginManager implements ServerHook {
npmName: p.npmName,
name: p.name,
version: p.version,
externalAuths: p.registerHelpersStore.getExternalAuths()
externalAuths: p.registerHelpers.getExternalAuths()
}))
.filter(v => v.externalAuths.length !== 0)
}
@ -129,14 +129,14 @@ export class PluginManager implements ServerHook {
const result = this.getRegisteredPluginOrTheme(npmName)
if (!result || result.type !== PluginType.PLUGIN) return []
return result.registerHelpersStore.getSettings()
return result.registerHelpers.getSettings()
}
getRouter (npmName: string) {
const result = this.getRegisteredPluginOrTheme(npmName)
if (!result || result.type !== PluginType.PLUGIN) return null
return result.registerHelpersStore.getRouter()
return result.registerHelpers.getRouter()
}
getTranslations (locale: string) {
@ -194,7 +194,7 @@ export class PluginManager implements ServerHook {
logger.error('Cannot find plugin %s to call on settings changed.', name)
}
for (const cb of registered.registerHelpersStore.getOnSettingsChangedCallbacks()) {
for (const cb of registered.registerHelpers.getOnSettingsChangedCallbacks()) {
try {
cb(settings)
} catch (err) {
@ -268,8 +268,9 @@ export class PluginManager implements ServerHook {
this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName)
}
const store = plugin.registerHelpersStore
const store = plugin.registerHelpers
store.reinitVideoConstants(plugin.npmName)
store.reinitTranscodingProfilesAndEncoders(plugin.npmName)
logger.info('Regenerating registered plugin CSS to global file.')
await this.regeneratePluginGlobalCSS()
@ -375,11 +376,11 @@ export class PluginManager implements ServerHook {
this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type)
let library: PluginLibrary
let registerHelpersStore: RegisterHelpersStore
let registerHelpers: RegisterHelpers
if (plugin.type === PluginType.PLUGIN) {
const result = await this.registerPlugin(plugin, pluginPath, packageJSON)
library = result.library
registerHelpersStore = result.registerStore
registerHelpers = result.registerStore
}
const clientScripts: { [id: string]: ClientScript } = {}
@ -398,7 +399,7 @@ export class PluginManager implements ServerHook {
staticDirs: packageJSON.staticDirs,
clientScripts,
css: packageJSON.css,
registerHelpersStore: registerHelpersStore || undefined,
registerHelpers: registerHelpers || undefined,
unregister: library ? library.unregister : undefined
}
@ -512,8 +513,8 @@ export class PluginManager implements ServerHook {
const plugin = this.getRegisteredPluginOrTheme(npmName)
if (!plugin || plugin.type !== PluginType.PLUGIN) return null
let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpersStore.getIdAndPassAuths()
auths = auths.concat(plugin.registerHelpersStore.getExternalAuths())
let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpers.getIdAndPassAuths()
auths = auths.concat(plugin.registerHelpers.getExternalAuths())
return auths.find(a => a.authName === authName)
}
@ -538,7 +539,7 @@ export class PluginManager implements ServerHook {
private getRegisterHelpers (
npmName: string,
plugin: PluginModel
): { registerStore: RegisterHelpersStore, registerOptions: RegisterServerOptions } {
): { registerStore: RegisterHelpers, registerOptions: RegisterServerOptions } {
const onHookAdded = (options: RegisterServerHookOptions) => {
if (!this.hooks[options.target]) this.hooks[options.target] = []
@ -550,11 +551,11 @@ export class PluginManager implements ServerHook {
})
}
const registerHelpersStore = new RegisterHelpersStore(npmName, plugin, onHookAdded.bind(this))
const registerHelpers = new RegisterHelpers(npmName, plugin, onHookAdded.bind(this))
return {
registerStore: registerHelpersStore,
registerOptions: registerHelpersStore.buildRegisterHelpers()
registerStore: registerHelpers,
registerOptions: registerHelpers.buildRegisterHelpers()
}
}

View File

@ -17,6 +17,7 @@ import {
RegisterServerOptions
} from '@server/types/plugins'
import {
EncoderOptionsBuilder,
PluginPlaylistPrivacyManager,
PluginSettingsManager,
PluginStorageManager,
@ -28,7 +29,8 @@ import {
RegisterServerSettingOptions
} from '@shared/models'
import { serverHookObject } from '@shared/models/plugins/server-hook.model'
import { buildPluginHelpers } from './plugin-helpers'
import { VideoTranscodingProfilesManager } from '../video-transcoding-profiles'
import { buildPluginHelpers } from './plugin-helpers-builder'
type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
type VideoConstant = { [key in number | string]: string }
@ -40,7 +42,7 @@ type UpdatedVideoConstant = {
}
}
export class RegisterHelpersStore {
export class RegisterHelpers {
private readonly updatedVideoConstants: UpdatedVideoConstant = {
playlistPrivacy: { added: [], deleted: [] },
privacy: { added: [], deleted: [] },
@ -49,6 +51,23 @@ export class RegisterHelpersStore {
category: { added: [], deleted: [] }
}
private readonly transcodingProfiles: {
[ npmName: string ]: {
type: 'vod' | 'live'
encoder: string
profile: string
}[]
} = {}
private readonly transcodingEncoders: {
[ npmName: string ]: {
type: 'vod' | 'live'
streamType: 'audio' | 'video'
encoder: string
priority: number
}[]
} = {}
private readonly settings: RegisterServerSettingOptions[] = []
private idAndPassAuths: RegisterServerAuthPassOptions[] = []
@ -83,6 +102,8 @@ export class RegisterHelpersStore {
const videoPrivacyManager = this.buildVideoPrivacyManager()
const playlistPrivacyManager = this.buildPlaylistPrivacyManager()
const transcodingManager = this.buildTranscodingManager()
const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth()
const registerExternalAuth = this.buildRegisterExternalAuth()
const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth()
@ -106,6 +127,8 @@ export class RegisterHelpersStore {
videoPrivacyManager,
playlistPrivacyManager,
transcodingManager,
registerIdAndPassAuth,
registerExternalAuth,
unregisterIdAndPassAuth,
@ -141,6 +164,22 @@ export class RegisterHelpersStore {
}
}
reinitTranscodingProfilesAndEncoders (npmName: string) {
const profiles = this.transcodingProfiles[npmName]
if (Array.isArray(profiles)) {
for (const profile of profiles) {
VideoTranscodingProfilesManager.Instance.removeProfile(profile)
}
}
const encoders = this.transcodingEncoders[npmName]
if (Array.isArray(encoders)) {
for (const o of encoders) {
VideoTranscodingProfilesManager.Instance.removeEncoderPriority(o.type, o.streamType, o.encoder, o.priority)
}
}
}
getSettings () {
return this.settings
}
@ -354,4 +393,52 @@ export class RegisterHelpersStore {
return true
}
private buildTranscodingManager () {
const self = this
function addProfile (type: 'live' | 'vod', encoder: string, profile: string, builder: EncoderOptionsBuilder) {
if (profile === 'default') {
logger.error('A plugin cannot add a default live transcoding profile')
return false
}
VideoTranscodingProfilesManager.Instance.addProfile({
type,
encoder,
profile,
builder
})
if (!self.transcodingProfiles[self.npmName]) self.transcodingProfiles[self.npmName] = []
self.transcodingProfiles[self.npmName].push({ type, encoder, profile })
return true
}
function addEncoderPriority (type: 'live' | 'vod', streamType: 'audio' | 'video', encoder: string, priority: number) {
VideoTranscodingProfilesManager.Instance.addEncoderPriority(type, streamType, encoder, priority)
if (!self.transcodingEncoders[self.npmName]) self.transcodingEncoders[self.npmName] = []
self.transcodingEncoders[self.npmName].push({ type, streamType, encoder, priority })
}
return {
addLiveProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder) {
return addProfile('live', encoder, profile, builder)
},
addVODProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder) {
return addProfile('vod', encoder, profile, builder)
},
addLiveEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number) {
return addEncoderPriority('live', streamType, encoder, priority)
},
addVODEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number) {
return addEncoderPriority('vod', streamType, encoder, priority)
}
}
}
}

View File

@ -1,6 +1,6 @@
import { logger } from '@server/helpers/logger'
import { getTargetBitrate, VideoResolution } from '../../shared/models/videos'
import { AvailableEncoders, buildStreamSuffix, EncoderOptionsBuilder } from '../helpers/ffmpeg-utils'
import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../shared/models/videos'
import { buildStreamSuffix, resetSupportedEncoders } from '../helpers/ffmpeg-utils'
import {
canDoQuickAudioTranscode,
ffprobePromise,
@ -84,16 +84,8 @@ class VideoTranscodingProfilesManager {
// 1 === less priority
private readonly encodersPriorities = {
video: [
{ name: 'libx264', priority: 100 }
],
// Try the first one, if not available try the second one etc
audio: [
// we favor VBR, if a good AAC encoder is available
{ name: 'libfdk_aac', priority: 200 },
{ name: 'aac', priority: 100 }
]
vod: this.buildDefaultEncodersPriorities(),
live: this.buildDefaultEncodersPriorities()
}
private readonly availableEncoders = {
@ -118,25 +110,77 @@ class VideoTranscodingProfilesManager {
}
}
private constructor () {
private availableProfiles = {
vod: [] as string[],
live: [] as string[]
}
private constructor () {
this.buildAvailableProfiles()
}
getAvailableEncoders (): AvailableEncoders {
const encodersToTry = {
video: this.getEncodersByPriority('video'),
audio: this.getEncodersByPriority('audio')
return {
available: this.availableEncoders,
encodersToTry: {
vod: {
video: this.getEncodersByPriority('vod', 'video'),
audio: this.getEncodersByPriority('vod', 'audio')
},
live: {
video: this.getEncodersByPriority('live', 'video'),
audio: this.getEncodersByPriority('live', 'audio')
}
}
}
return Object.assign({}, this.availableEncoders, { encodersToTry })
}
getAvailableProfiles (type: 'vod' | 'live') {
return this.availableEncoders[type]
return this.availableProfiles[type]
}
private getEncodersByPriority (type: 'video' | 'audio') {
return this.encodersPriorities[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()
}
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())
}

View File

@ -42,7 +42,7 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile:
outputPath: videoTranscodedPath,
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
profile: 'default',
profile: CONFIG.TRANSCODING.PROFILE,
resolution: inputVideoFile.resolution,
@ -96,7 +96,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti
outputPath: videoTranscodedPath,
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
profile: 'default',
profile: CONFIG.TRANSCODING.PROFILE,
resolution,
@ -108,7 +108,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti
outputPath: videoTranscodedPath,
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
profile: 'default',
profile: CONFIG.TRANSCODING.PROFILE,
resolution,
isPortraitMode: isPortrait,
@ -143,7 +143,7 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video
outputPath: videoTranscodedPath,
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
profile: 'default',
profile: CONFIG.TRANSCODING.PROFILE,
audioPath: audioInputPath,
resolution,
@ -284,7 +284,7 @@ async function generateHlsPlaylistCommon (options: {
outputPath,
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
profile: 'default',
profile: CONFIG.TRANSCODING.PROFILE,
resolution,
copyCodecs,

View File

@ -50,9 +50,9 @@ const getExternalAuthValidator = [
if (areValidationErrors(req, res)) return
const plugin = res.locals.registeredPlugin
if (!plugin.registerHelpersStore) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
if (!plugin.registerHelpers) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
const externalAuth = plugin.registerHelpersStore.getExternalAuths().find(a => a.authName === req.params.authName)
const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName)
if (!externalAuth) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
res.locals.externalAuth = externalAuth

View File

@ -96,10 +96,11 @@ import {
MVideoWithRights
} from '../../types/models'
import { MThumbnail } from '../../types/models/video/thumbnail'
import { MVideoFile, MVideoFileRedundanciesOpt, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
import { VideoAbuseModel } from '../abuse/video-abuse'
import { AccountModel } from '../account/account'
import { AccountVideoRateModel } from '../account/account-video-rate'
import { UserModel } from '../account/user'
import { UserVideoHistoryModel } from '../account/user-video-history'
import { ActorModel } from '../activitypub/actor'
import { AvatarModel } from '../avatar/avatar'
@ -129,7 +130,6 @@ import { VideoShareModel } from './video-share'
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
import { VideoTagModel } from './video-tag'
import { VideoViewModel } from './video-view'
import { UserModel } from '../account/user'
export enum ScopeNames {
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',

View File

@ -87,6 +87,7 @@ describe('Test config API validators', function () {
allowAdditionalExtensions: true,
allowAudioFiles: true,
threads: 1,
profile: 'vod_profile',
resolutions: {
'0p': false,
'240p': false,
@ -115,6 +116,7 @@ describe('Test config API validators', function () {
transcoding: {
enabled: true,
threads: 4,
profile: 'live_profile',
resolutions: {
'240p': true,
'360p': true,

View File

@ -70,6 +70,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
expect(data.transcoding.allowAdditionalExtensions).to.be.false
expect(data.transcoding.allowAudioFiles).to.be.false
expect(data.transcoding.threads).to.equal(2)
expect(data.transcoding.profile).to.equal('default')
expect(data.transcoding.resolutions['240p']).to.be.true
expect(data.transcoding.resolutions['360p']).to.be.true
expect(data.transcoding.resolutions['480p']).to.be.true
@ -87,6 +88,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
expect(data.live.maxUserLives).to.equal(3)
expect(data.live.transcoding.enabled).to.be.false
expect(data.live.transcoding.threads).to.equal(2)
expect(data.live.transcoding.profile).to.equal('default')
expect(data.live.transcoding.resolutions['240p']).to.be.false
expect(data.live.transcoding.resolutions['360p']).to.be.false
expect(data.live.transcoding.resolutions['480p']).to.be.false
@ -159,6 +161,7 @@ function checkUpdatedConfig (data: CustomConfig) {
expect(data.transcoding.threads).to.equal(1)
expect(data.transcoding.allowAdditionalExtensions).to.be.true
expect(data.transcoding.allowAudioFiles).to.be.true
expect(data.transcoding.profile).to.equal('vod_profile')
expect(data.transcoding.resolutions['240p']).to.be.false
expect(data.transcoding.resolutions['360p']).to.be.true
expect(data.transcoding.resolutions['480p']).to.be.true
@ -175,6 +178,7 @@ function checkUpdatedConfig (data: CustomConfig) {
expect(data.live.maxUserLives).to.equal(10)
expect(data.live.transcoding.enabled).to.be.true
expect(data.live.transcoding.threads).to.equal(4)
expect(data.live.transcoding.profile).to.equal('live_profile')
expect(data.live.transcoding.resolutions['240p']).to.be.true
expect(data.live.transcoding.resolutions['360p']).to.be.true
expect(data.live.transcoding.resolutions['480p']).to.be.true
@ -319,6 +323,7 @@ describe('Test config', function () {
allowAdditionalExtensions: true,
allowAudioFiles: true,
threads: 1,
profile: 'vod_profile',
resolutions: {
'0p': false,
'240p': false,
@ -345,6 +350,7 @@ describe('Test config', function () {
transcoding: {
enabled: true,
threads: 4,
profile: 'live_profile',
resolutions: {
'240p': true,
'360p': true,

View File

@ -511,7 +511,9 @@ describe('Test video transcoding', function () {
const resolutions = [ 240, 360, 480, 720, 1080 ]
for (const r of resolutions) {
expect(await getServerFileSize(servers[1], `videos/${videoUUID}-${r}.mp4`)).to.be.below(60_000)
const path = `videos/${videoUUID}-${r}.mp4`
const size = await getServerFileSize(servers[1], path)
expect(size, `${path} not below ${60_000}`).to.be.below(60_000)
}
})

View File

@ -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
}

View File

@ -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": {}
}

View File

@ -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
}

View File

@ -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": {}
}

View File

@ -1,10 +1,11 @@
import './action-hooks'
import './html-injection'
import './id-and-pass-auth'
import './external-auth'
import './filter-hooks'
import './translations'
import './video-constants'
import './html-injection'
import './id-and-pass-auth'
import './plugin-helpers'
import './plugin-router'
import './plugin-storage'
import './plugin-transcoding'
import './translations'
import './video-constants'

View File

@ -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 ])
})
})

View File

@ -5,6 +5,7 @@ import {
PluginPlaylistPrivacyManager,
PluginSettingsManager,
PluginStorageManager,
PluginTranscodingManager,
PluginVideoCategoryManager,
PluginVideoLanguageManager,
PluginVideoLicenceManager,
@ -68,6 +69,8 @@ export type RegisterServerOptions = {
videoPrivacyManager: PluginVideoPrivacyManager
playlistPrivacyManager: PluginPlaylistPrivacyManager
transcodingManager: PluginTranscodingManager
registerIdAndPassAuth: (options: RegisterServerAuthPassOptions) => void
registerExternalAuth: (options: RegisterServerAuthExternalOptions) => RegisterServerAuthExternalResult
unregisterIdAndPassAuth: (authName: string) => void

View File

@ -112,6 +112,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
allowAdditionalExtensions: true,
allowAudioFiles: true,
threads: 1,
profile: 'default',
resolutions: {
'0p': false,
'240p': false,
@ -138,6 +139,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
transcoding: {
enabled: true,
threads: 4,
profile: 'default',
resolutions: {
'240p': true,
'360p': true,

View File

@ -11,6 +11,7 @@ export * from './plugin-package-json.model'
export * from './plugin-playlist-privacy-manager.model'
export * from './plugin-settings-manager.model'
export * from './plugin-storage-manager.model'
export * from './plugin-transcoding-manager.model'
export * from './plugin-translation.model'
export * from './plugin-video-category-manager.model'
export * from './plugin-video-language-manager.model'

View File

@ -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
}

View File

@ -87,6 +87,9 @@ export interface CustomConfig {
allowAudioFiles: boolean
threads: number
profile: string
resolutions: ConfigResolutions & { '0p': boolean }
webtorrent: {
@ -110,6 +113,7 @@ export interface CustomConfig {
transcoding: {
enabled: boolean
threads: number
profile: string
resolutions: ConfigResolutions
}
}

View File

@ -96,6 +96,9 @@ export interface ServerConfig {
}
enabledResolutions: number[]
profile: string
availableProfiles: string[]
}
live: {
@ -110,6 +113,9 @@ export interface ServerConfig {
enabled: boolean
enabledResolutions: number[]
profile: string
availableProfiles: string[]
}
rtmp: {

View File

@ -34,6 +34,7 @@ export * from './video-state.enum'
export * from './video-streaming-playlist.model'
export * from './video-streaming-playlist.type'
export * from './video-transcoding.model'
export * from './video-transcoding-fps.model'
export * from './video-update.model'

View File

@ -0,0 +1,48 @@
import { VideoResolution } from './video-resolution.enum'
// Types used by plugins and ffmpeg-utils
export type EncoderOptionsBuilder = (params: {
input: string
resolution: VideoResolution
fps?: number
streamNum?: number
}) => Promise<EncoderOptions> | EncoderOptions
export interface EncoderOptions {
copy?: boolean // Copy stream? Default to false
outputOptions: string[]
}
// All our encoders
export interface EncoderProfile <T> {
[ profile: string ]: T
default: T
}
export type AvailableEncoders = {
available: {
live: {
[ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
}
vod: {
[ encoder: string ]: EncoderProfile<EncoderOptionsBuilder>
}
}
encodersToTry: {
vod: {
video: string[]
audio: string[]
}
live: {
video: string[]
audio: string[]
}
}
}