Support transcoding options/encoders by plugins
This commit is contained in:
parent
529b37527c
commit
1896bca09e
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 = {}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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') },
|
||||
|
|
|
@ -338,7 +338,7 @@ class LiveManager {
|
|||
resolutions: allResolutions,
|
||||
fps,
|
||||
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
||||
profile: 'default'
|
||||
profile: CONFIG.LIVE.TRANSCODING.PROFILE
|
||||
})
|
||||
: getLiveMuxingCommand(rtmpUrl, outPath)
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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": {}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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": {}
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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 ])
|
||||
})
|
||||
})
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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[]
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue