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
|
# Please, do not disable transcoding since many uploaded videos will not work
|
||||||
transcoding:
|
transcoding:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
# Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos
|
# Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos
|
||||||
allow_additional_extensions: true
|
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
|
# 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
|
allow_audio_files: true
|
||||||
|
|
||||||
threads: 1
|
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!
|
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)
|
0p: false # audio-only (creates mp4 without video stream, always created when enabled)
|
||||||
240p: false
|
240p: false
|
||||||
|
@ -283,6 +292,11 @@ live:
|
||||||
enabled: true
|
enabled: true
|
||||||
threads: 2
|
threads: 2
|
||||||
|
|
||||||
|
# Choose the transcoding profile
|
||||||
|
# New profiles can be added by plugins
|
||||||
|
# Available in core PeerTube: 'default'
|
||||||
|
profile: 'default'
|
||||||
|
|
||||||
resolutions:
|
resolutions:
|
||||||
240p: false
|
240p: false
|
||||||
360p: false
|
360p: false
|
||||||
|
|
|
@ -236,11 +236,20 @@ user:
|
||||||
# Please, do not disable transcoding since many uploaded videos will not work
|
# Please, do not disable transcoding since many uploaded videos will not work
|
||||||
transcoding:
|
transcoding:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
# Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos
|
# Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos
|
||||||
allow_additional_extensions: true
|
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
|
# 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
|
allow_audio_files: true
|
||||||
|
|
||||||
threads: 1
|
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!
|
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)
|
0p: false # audio-only (creates mp4 without video stream, always created when enabled)
|
||||||
240p: false
|
240p: false
|
||||||
|
@ -270,7 +279,7 @@ live:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
# Limit lives duration
|
# Limit lives duration
|
||||||
# Set null to disable duration limit
|
# -1 == unlimited
|
||||||
max_duration: -1 # For example: '5 hours'
|
max_duration: -1 # For example: '5 hours'
|
||||||
|
|
||||||
# Limit max number of live videos created on your instance
|
# Limit max number of live videos created on your instance
|
||||||
|
@ -296,6 +305,11 @@ live:
|
||||||
enabled: true
|
enabled: true
|
||||||
threads: 2
|
threads: 2
|
||||||
|
|
||||||
|
# Choose the transcoding profile
|
||||||
|
# New profiles can be added by plugins
|
||||||
|
# Available in core PeerTube: 'default'
|
||||||
|
profile: 'default'
|
||||||
|
|
||||||
resolutions:
|
resolutions:
|
||||||
240p: false
|
240p: false
|
||||||
360p: false
|
360p: false
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { PluginManager } from '../../lib/plugins/plugin-manager'
|
||||||
import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
|
import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
|
||||||
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
|
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
|
||||||
import { customConfigUpdateValidator } from '../../middlewares/validators/config'
|
import { customConfigUpdateValidator } from '../../middlewares/validators/config'
|
||||||
|
import { VideoTranscodingProfilesManager } from '@server/lib/video-transcoding-profiles'
|
||||||
|
|
||||||
const configRouter = express.Router()
|
const configRouter = express.Router()
|
||||||
|
|
||||||
|
@ -114,7 +115,9 @@ async function getConfig (req: express.Request, res: express.Response) {
|
||||||
webtorrent: {
|
webtorrent: {
|
||||||
enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
|
enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
|
||||||
},
|
},
|
||||||
enabledResolutions: getEnabledResolutions('vod')
|
enabledResolutions: getEnabledResolutions('vod'),
|
||||||
|
profile: CONFIG.TRANSCODING.PROFILE,
|
||||||
|
availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('vod')
|
||||||
},
|
},
|
||||||
live: {
|
live: {
|
||||||
enabled: CONFIG.LIVE.ENABLED,
|
enabled: CONFIG.LIVE.ENABLED,
|
||||||
|
@ -126,7 +129,9 @@ async function getConfig (req: express.Request, res: express.Response) {
|
||||||
|
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
|
enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
|
||||||
enabledResolutions: getEnabledResolutions('live')
|
enabledResolutions: getEnabledResolutions('live'),
|
||||||
|
profile: CONFIG.LIVE.TRANSCODING.PROFILE,
|
||||||
|
availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('live')
|
||||||
},
|
},
|
||||||
|
|
||||||
rtmp: {
|
rtmp: {
|
||||||
|
@ -412,6 +417,7 @@ function customConfig (): CustomConfig {
|
||||||
allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
|
allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
|
||||||
allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES,
|
allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES,
|
||||||
threads: CONFIG.TRANSCODING.THREADS,
|
threads: CONFIG.TRANSCODING.THREADS,
|
||||||
|
profile: CONFIG.TRANSCODING.PROFILE,
|
||||||
resolutions: {
|
resolutions: {
|
||||||
'0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'],
|
'0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'],
|
||||||
'240p': CONFIG.TRANSCODING.RESOLUTIONS['240p'],
|
'240p': CONFIG.TRANSCODING.RESOLUTIONS['240p'],
|
||||||
|
@ -438,6 +444,7 @@ function customConfig (): CustomConfig {
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
|
enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
|
||||||
threads: CONFIG.LIVE.TRANSCODING.THREADS,
|
threads: CONFIG.LIVE.TRANSCODING.THREADS,
|
||||||
|
profile: CONFIG.LIVE.TRANSCODING.PROFILE,
|
||||||
resolutions: {
|
resolutions: {
|
||||||
'240p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['240p'],
|
'240p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['240p'],
|
||||||
'360p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['360p'],
|
'360p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['360p'],
|
||||||
|
|
|
@ -3,9 +3,9 @@ import * as ffmpeg from 'fluent-ffmpeg'
|
||||||
import { readFile, remove, writeFile } from 'fs-extra'
|
import { readFile, remove, writeFile } from 'fs-extra'
|
||||||
import { dirname, join } from 'path'
|
import { dirname, join } from 'path'
|
||||||
import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants'
|
import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants'
|
||||||
import { VideoResolution } from '../../shared/models/videos'
|
import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos'
|
||||||
import { checkFFmpegEncoders } from '../initializers/checker-before-init'
|
|
||||||
import { CONFIG } from '../initializers/config'
|
import { CONFIG } from '../initializers/config'
|
||||||
|
import { promisify0 } from './core-utils'
|
||||||
import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils'
|
import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils'
|
||||||
import { processImage } from './image-utils'
|
import { processImage } from './image-utils'
|
||||||
import { logger } from './logger'
|
import { logger } from './logger'
|
||||||
|
@ -21,47 +21,46 @@ import { logger } from './logger'
|
||||||
// Encoder options
|
// 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'
|
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
|
// Image manipulation
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
@ -275,7 +274,7 @@ async function getLiveTranscodingCommand (options: {
|
||||||
|
|
||||||
addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
|
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.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`)
|
||||||
command.addOutputOptions(builderResult.result.outputOptions)
|
command.addOutputOptions(builderResult.result.outputOptions)
|
||||||
|
@ -292,7 +291,7 @@ async function getLiveTranscodingCommand (options: {
|
||||||
|
|
||||||
addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
|
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.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`)
|
||||||
command.addOutputOptions(builderResult.result.outputOptions)
|
command.addOutputOptions(builderResult.result.outputOptions)
|
||||||
|
@ -513,11 +512,19 @@ async function getEncoderBuilderResult (options: {
|
||||||
}) {
|
}) {
|
||||||
const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options
|
const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options
|
||||||
|
|
||||||
const encodersToTry = availableEncoders.encodersToTry[streamType]
|
const encodersToTry = availableEncoders.encodersToTry[videoType][streamType]
|
||||||
const encoders = availableEncoders[videoType]
|
const encoders = availableEncoders.available[videoType]
|
||||||
|
|
||||||
for (const encoder of encodersToTry) {
|
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
|
// An object containing available profiles for this encoder
|
||||||
const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = encoders[encoder]
|
const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = encoders[encoder]
|
||||||
|
@ -567,7 +574,7 @@ async function presetVideo (
|
||||||
|
|
||||||
if (!parsedAudio.audioStream) {
|
if (!parsedAudio.audioStream) {
|
||||||
localCommand = localCommand.noAudio()
|
localCommand = localCommand.noAudio()
|
||||||
streamsToProcess = [ 'audio' ]
|
streamsToProcess = [ 'video' ]
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const streamType of streamsToProcess) {
|
for (const streamType of streamsToProcess) {
|
||||||
|
@ -587,7 +594,10 @@ async function presetVideo (
|
||||||
throw new Error('No available encoder found for stream ' + streamType)
|
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') {
|
if (streamType === 'video') {
|
||||||
localCommand.videoCodec(builderResult.encoder)
|
localCommand.videoCodec(builderResult.encoder)
|
||||||
|
@ -679,6 +689,8 @@ export {
|
||||||
transcode,
|
transcode,
|
||||||
runCommand,
|
runCommand,
|
||||||
|
|
||||||
|
resetSupportedEncoders,
|
||||||
|
|
||||||
// builders
|
// builders
|
||||||
buildx264VODCommand
|
buildx264VODCommand
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,14 @@ function getLoggerReplacer () {
|
||||||
seen.add(value)
|
seen.add(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value instanceof Set) {
|
||||||
|
return Array.from(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Map) {
|
||||||
|
return Array.from(value.entries())
|
||||||
|
}
|
||||||
|
|
||||||
if (value instanceof Error) {
|
if (value instanceof Error) {
|
||||||
const error = {}
|
const error = {}
|
||||||
|
|
||||||
|
|
|
@ -97,34 +97,6 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) {
|
||||||
throw new Error('Unavailable encode codec ' + codec + ' in FFmpeg')
|
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 () {
|
function checkNodeVersion () {
|
||||||
|
@ -143,7 +115,6 @@ function checkNodeVersion () {
|
||||||
|
|
||||||
export {
|
export {
|
||||||
checkFFmpeg,
|
checkFFmpeg,
|
||||||
checkFFmpegEncoders,
|
|
||||||
checkMissedConfig,
|
checkMissedConfig,
|
||||||
checkNodeVersion
|
checkNodeVersion
|
||||||
}
|
}
|
||||||
|
|
|
@ -188,6 +188,7 @@ const CONFIG = {
|
||||||
get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') },
|
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 ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') },
|
||||||
get THREADS () { return config.get<number>('transcoding.threads') },
|
get THREADS () { return config.get<number>('transcoding.threads') },
|
||||||
|
get PROFILE () { return config.get<string>('transcoding.profile') },
|
||||||
RESOLUTIONS: {
|
RESOLUTIONS: {
|
||||||
get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') },
|
get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') },
|
||||||
get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') },
|
get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') },
|
||||||
|
@ -221,6 +222,7 @@ const CONFIG = {
|
||||||
TRANSCODING: {
|
TRANSCODING: {
|
||||||
get ENABLED () { return config.get<boolean>('live.transcoding.enabled') },
|
get ENABLED () { return config.get<boolean>('live.transcoding.enabled') },
|
||||||
get THREADS () { return config.get<number>('live.transcoding.threads') },
|
get THREADS () { return config.get<number>('live.transcoding.threads') },
|
||||||
|
get PROFILE () { return config.get<string>('live.transcoding.profile') },
|
||||||
|
|
||||||
RESOLUTIONS: {
|
RESOLUTIONS: {
|
||||||
get '240p' () { return config.get<boolean>('live.transcoding.resolutions.240p') },
|
get '240p' () { return config.get<boolean>('live.transcoding.resolutions.240p') },
|
||||||
|
|
|
@ -338,7 +338,7 @@ class LiveManager {
|
||||||
resolutions: allResolutions,
|
resolutions: allResolutions,
|
||||||
fps,
|
fps,
|
||||||
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
||||||
profile: 'default'
|
profile: CONFIG.LIVE.TRANSCODING.PROFILE
|
||||||
})
|
})
|
||||||
: getLiveMuxingCommand(rtmpUrl, outPath)
|
: getLiveMuxingCommand(rtmpUrl, outPath)
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants'
|
||||||
import { PluginModel } from '../../models/server/plugin'
|
import { PluginModel } from '../../models/server/plugin'
|
||||||
import { PluginLibrary, RegisterServerAuthExternalOptions, RegisterServerAuthPassOptions, RegisterServerOptions } from '../../types/plugins'
|
import { PluginLibrary, RegisterServerAuthExternalOptions, RegisterServerAuthPassOptions, RegisterServerOptions } from '../../types/plugins'
|
||||||
import { ClientHtml } from '../client-html'
|
import { ClientHtml } from '../client-html'
|
||||||
import { RegisterHelpersStore } from './register-helpers-store'
|
import { RegisterHelpers } from './register-helpers'
|
||||||
import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
|
import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
|
||||||
|
|
||||||
export interface RegisteredPlugin {
|
export interface RegisteredPlugin {
|
||||||
|
@ -40,7 +40,7 @@ export interface RegisteredPlugin {
|
||||||
css: string[]
|
css: string[]
|
||||||
|
|
||||||
// Only if this is a plugin
|
// Only if this is a plugin
|
||||||
registerHelpersStore?: RegisterHelpersStore
|
registerHelpers?: RegisterHelpers
|
||||||
unregister?: Function
|
unregister?: Function
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ export class PluginManager implements ServerHook {
|
||||||
npmName: p.npmName,
|
npmName: p.npmName,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
version: p.version,
|
version: p.version,
|
||||||
idAndPassAuths: p.registerHelpersStore.getIdAndPassAuths()
|
idAndPassAuths: p.registerHelpers.getIdAndPassAuths()
|
||||||
}))
|
}))
|
||||||
.filter(v => v.idAndPassAuths.length !== 0)
|
.filter(v => v.idAndPassAuths.length !== 0)
|
||||||
}
|
}
|
||||||
|
@ -120,7 +120,7 @@ export class PluginManager implements ServerHook {
|
||||||
npmName: p.npmName,
|
npmName: p.npmName,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
version: p.version,
|
version: p.version,
|
||||||
externalAuths: p.registerHelpersStore.getExternalAuths()
|
externalAuths: p.registerHelpers.getExternalAuths()
|
||||||
}))
|
}))
|
||||||
.filter(v => v.externalAuths.length !== 0)
|
.filter(v => v.externalAuths.length !== 0)
|
||||||
}
|
}
|
||||||
|
@ -129,14 +129,14 @@ export class PluginManager implements ServerHook {
|
||||||
const result = this.getRegisteredPluginOrTheme(npmName)
|
const result = this.getRegisteredPluginOrTheme(npmName)
|
||||||
if (!result || result.type !== PluginType.PLUGIN) return []
|
if (!result || result.type !== PluginType.PLUGIN) return []
|
||||||
|
|
||||||
return result.registerHelpersStore.getSettings()
|
return result.registerHelpers.getSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
getRouter (npmName: string) {
|
getRouter (npmName: string) {
|
||||||
const result = this.getRegisteredPluginOrTheme(npmName)
|
const result = this.getRegisteredPluginOrTheme(npmName)
|
||||||
if (!result || result.type !== PluginType.PLUGIN) return null
|
if (!result || result.type !== PluginType.PLUGIN) return null
|
||||||
|
|
||||||
return result.registerHelpersStore.getRouter()
|
return result.registerHelpers.getRouter()
|
||||||
}
|
}
|
||||||
|
|
||||||
getTranslations (locale: string) {
|
getTranslations (locale: string) {
|
||||||
|
@ -194,7 +194,7 @@ export class PluginManager implements ServerHook {
|
||||||
logger.error('Cannot find plugin %s to call on settings changed.', name)
|
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 {
|
try {
|
||||||
cb(settings)
|
cb(settings)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -268,8 +268,9 @@ export class PluginManager implements ServerHook {
|
||||||
this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName)
|
this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName)
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = plugin.registerHelpersStore
|
const store = plugin.registerHelpers
|
||||||
store.reinitVideoConstants(plugin.npmName)
|
store.reinitVideoConstants(plugin.npmName)
|
||||||
|
store.reinitTranscodingProfilesAndEncoders(plugin.npmName)
|
||||||
|
|
||||||
logger.info('Regenerating registered plugin CSS to global file.')
|
logger.info('Regenerating registered plugin CSS to global file.')
|
||||||
await this.regeneratePluginGlobalCSS()
|
await this.regeneratePluginGlobalCSS()
|
||||||
|
@ -375,11 +376,11 @@ export class PluginManager implements ServerHook {
|
||||||
this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type)
|
this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type)
|
||||||
|
|
||||||
let library: PluginLibrary
|
let library: PluginLibrary
|
||||||
let registerHelpersStore: RegisterHelpersStore
|
let registerHelpers: RegisterHelpers
|
||||||
if (plugin.type === PluginType.PLUGIN) {
|
if (plugin.type === PluginType.PLUGIN) {
|
||||||
const result = await this.registerPlugin(plugin, pluginPath, packageJSON)
|
const result = await this.registerPlugin(plugin, pluginPath, packageJSON)
|
||||||
library = result.library
|
library = result.library
|
||||||
registerHelpersStore = result.registerStore
|
registerHelpers = result.registerStore
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientScripts: { [id: string]: ClientScript } = {}
|
const clientScripts: { [id: string]: ClientScript } = {}
|
||||||
|
@ -398,7 +399,7 @@ export class PluginManager implements ServerHook {
|
||||||
staticDirs: packageJSON.staticDirs,
|
staticDirs: packageJSON.staticDirs,
|
||||||
clientScripts,
|
clientScripts,
|
||||||
css: packageJSON.css,
|
css: packageJSON.css,
|
||||||
registerHelpersStore: registerHelpersStore || undefined,
|
registerHelpers: registerHelpers || undefined,
|
||||||
unregister: library ? library.unregister : undefined
|
unregister: library ? library.unregister : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -512,8 +513,8 @@ export class PluginManager implements ServerHook {
|
||||||
const plugin = this.getRegisteredPluginOrTheme(npmName)
|
const plugin = this.getRegisteredPluginOrTheme(npmName)
|
||||||
if (!plugin || plugin.type !== PluginType.PLUGIN) return null
|
if (!plugin || plugin.type !== PluginType.PLUGIN) return null
|
||||||
|
|
||||||
let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpersStore.getIdAndPassAuths()
|
let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpers.getIdAndPassAuths()
|
||||||
auths = auths.concat(plugin.registerHelpersStore.getExternalAuths())
|
auths = auths.concat(plugin.registerHelpers.getExternalAuths())
|
||||||
|
|
||||||
return auths.find(a => a.authName === authName)
|
return auths.find(a => a.authName === authName)
|
||||||
}
|
}
|
||||||
|
@ -538,7 +539,7 @@ export class PluginManager implements ServerHook {
|
||||||
private getRegisterHelpers (
|
private getRegisterHelpers (
|
||||||
npmName: string,
|
npmName: string,
|
||||||
plugin: PluginModel
|
plugin: PluginModel
|
||||||
): { registerStore: RegisterHelpersStore, registerOptions: RegisterServerOptions } {
|
): { registerStore: RegisterHelpers, registerOptions: RegisterServerOptions } {
|
||||||
const onHookAdded = (options: RegisterServerHookOptions) => {
|
const onHookAdded = (options: RegisterServerHookOptions) => {
|
||||||
if (!this.hooks[options.target]) this.hooks[options.target] = []
|
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 {
|
return {
|
||||||
registerStore: registerHelpersStore,
|
registerStore: registerHelpers,
|
||||||
registerOptions: registerHelpersStore.buildRegisterHelpers()
|
registerOptions: registerHelpers.buildRegisterHelpers()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
RegisterServerOptions
|
RegisterServerOptions
|
||||||
} from '@server/types/plugins'
|
} from '@server/types/plugins'
|
||||||
import {
|
import {
|
||||||
|
EncoderOptionsBuilder,
|
||||||
PluginPlaylistPrivacyManager,
|
PluginPlaylistPrivacyManager,
|
||||||
PluginSettingsManager,
|
PluginSettingsManager,
|
||||||
PluginStorageManager,
|
PluginStorageManager,
|
||||||
|
@ -28,7 +29,8 @@ import {
|
||||||
RegisterServerSettingOptions
|
RegisterServerSettingOptions
|
||||||
} from '@shared/models'
|
} from '@shared/models'
|
||||||
import { serverHookObject } from '@shared/models/plugins/server-hook.model'
|
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 AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
|
||||||
type VideoConstant = { [key in number | string]: string }
|
type VideoConstant = { [key in number | string]: string }
|
||||||
|
@ -40,7 +42,7 @@ type UpdatedVideoConstant = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RegisterHelpersStore {
|
export class RegisterHelpers {
|
||||||
private readonly updatedVideoConstants: UpdatedVideoConstant = {
|
private readonly updatedVideoConstants: UpdatedVideoConstant = {
|
||||||
playlistPrivacy: { added: [], deleted: [] },
|
playlistPrivacy: { added: [], deleted: [] },
|
||||||
privacy: { added: [], deleted: [] },
|
privacy: { added: [], deleted: [] },
|
||||||
|
@ -49,6 +51,23 @@ export class RegisterHelpersStore {
|
||||||
category: { added: [], deleted: [] }
|
category: { added: [], deleted: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly transcodingProfiles: {
|
||||||
|
[ npmName: string ]: {
|
||||||
|
type: 'vod' | 'live'
|
||||||
|
encoder: string
|
||||||
|
profile: string
|
||||||
|
}[]
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
private readonly transcodingEncoders: {
|
||||||
|
[ npmName: string ]: {
|
||||||
|
type: 'vod' | 'live'
|
||||||
|
streamType: 'audio' | 'video'
|
||||||
|
encoder: string
|
||||||
|
priority: number
|
||||||
|
}[]
|
||||||
|
} = {}
|
||||||
|
|
||||||
private readonly settings: RegisterServerSettingOptions[] = []
|
private readonly settings: RegisterServerSettingOptions[] = []
|
||||||
|
|
||||||
private idAndPassAuths: RegisterServerAuthPassOptions[] = []
|
private idAndPassAuths: RegisterServerAuthPassOptions[] = []
|
||||||
|
@ -83,6 +102,8 @@ export class RegisterHelpersStore {
|
||||||
const videoPrivacyManager = this.buildVideoPrivacyManager()
|
const videoPrivacyManager = this.buildVideoPrivacyManager()
|
||||||
const playlistPrivacyManager = this.buildPlaylistPrivacyManager()
|
const playlistPrivacyManager = this.buildPlaylistPrivacyManager()
|
||||||
|
|
||||||
|
const transcodingManager = this.buildTranscodingManager()
|
||||||
|
|
||||||
const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth()
|
const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth()
|
||||||
const registerExternalAuth = this.buildRegisterExternalAuth()
|
const registerExternalAuth = this.buildRegisterExternalAuth()
|
||||||
const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth()
|
const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth()
|
||||||
|
@ -106,6 +127,8 @@ export class RegisterHelpersStore {
|
||||||
videoPrivacyManager,
|
videoPrivacyManager,
|
||||||
playlistPrivacyManager,
|
playlistPrivacyManager,
|
||||||
|
|
||||||
|
transcodingManager,
|
||||||
|
|
||||||
registerIdAndPassAuth,
|
registerIdAndPassAuth,
|
||||||
registerExternalAuth,
|
registerExternalAuth,
|
||||||
unregisterIdAndPassAuth,
|
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 () {
|
getSettings () {
|
||||||
return this.settings
|
return this.settings
|
||||||
}
|
}
|
||||||
|
@ -354,4 +393,52 @@ export class RegisterHelpersStore {
|
||||||
|
|
||||||
return true
|
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 { logger } from '@server/helpers/logger'
|
||||||
import { getTargetBitrate, VideoResolution } from '../../shared/models/videos'
|
import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../shared/models/videos'
|
||||||
import { AvailableEncoders, buildStreamSuffix, EncoderOptionsBuilder } from '../helpers/ffmpeg-utils'
|
import { buildStreamSuffix, resetSupportedEncoders } from '../helpers/ffmpeg-utils'
|
||||||
import {
|
import {
|
||||||
canDoQuickAudioTranscode,
|
canDoQuickAudioTranscode,
|
||||||
ffprobePromise,
|
ffprobePromise,
|
||||||
|
@ -84,16 +84,8 @@ class VideoTranscodingProfilesManager {
|
||||||
|
|
||||||
// 1 === less priority
|
// 1 === less priority
|
||||||
private readonly encodersPriorities = {
|
private readonly encodersPriorities = {
|
||||||
video: [
|
vod: this.buildDefaultEncodersPriorities(),
|
||||||
{ name: 'libx264', priority: 100 }
|
live: this.buildDefaultEncodersPriorities()
|
||||||
],
|
|
||||||
|
|
||||||
// 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 }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly availableEncoders = {
|
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 {
|
getAvailableEncoders (): AvailableEncoders {
|
||||||
const encodersToTry = {
|
return {
|
||||||
video: this.getEncodersByPriority('video'),
|
available: this.availableEncoders,
|
||||||
audio: this.getEncodersByPriority('audio')
|
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') {
|
getAvailableProfiles (type: 'vod' | 'live') {
|
||||||
return this.availableEncoders[type]
|
return this.availableProfiles[type]
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEncodersByPriority (type: 'video' | 'audio') {
|
addProfile (options: {
|
||||||
return this.encodersPriorities[type]
|
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) => {
|
.sort((e1, e2) => {
|
||||||
if (e1.priority > e2.priority) return -1
|
if (e1.priority > e2.priority) return -1
|
||||||
else if (e1.priority === e2.priority) return 0
|
else if (e1.priority === e2.priority) return 0
|
||||||
|
@ -146,6 +190,39 @@ class VideoTranscodingProfilesManager {
|
||||||
.map(e => e.name)
|
.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 () {
|
static get Instance () {
|
||||||
return this.instance || (this.instance = new this())
|
return this.instance || (this.instance = new this())
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile:
|
||||||
outputPath: videoTranscodedPath,
|
outputPath: videoTranscodedPath,
|
||||||
|
|
||||||
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
||||||
profile: 'default',
|
profile: CONFIG.TRANSCODING.PROFILE,
|
||||||
|
|
||||||
resolution: inputVideoFile.resolution,
|
resolution: inputVideoFile.resolution,
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti
|
||||||
outputPath: videoTranscodedPath,
|
outputPath: videoTranscodedPath,
|
||||||
|
|
||||||
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
||||||
profile: 'default',
|
profile: CONFIG.TRANSCODING.PROFILE,
|
||||||
|
|
||||||
resolution,
|
resolution,
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti
|
||||||
outputPath: videoTranscodedPath,
|
outputPath: videoTranscodedPath,
|
||||||
|
|
||||||
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
||||||
profile: 'default',
|
profile: CONFIG.TRANSCODING.PROFILE,
|
||||||
|
|
||||||
resolution,
|
resolution,
|
||||||
isPortraitMode: isPortrait,
|
isPortraitMode: isPortrait,
|
||||||
|
@ -143,7 +143,7 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video
|
||||||
outputPath: videoTranscodedPath,
|
outputPath: videoTranscodedPath,
|
||||||
|
|
||||||
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
||||||
profile: 'default',
|
profile: CONFIG.TRANSCODING.PROFILE,
|
||||||
|
|
||||||
audioPath: audioInputPath,
|
audioPath: audioInputPath,
|
||||||
resolution,
|
resolution,
|
||||||
|
@ -284,7 +284,7 @@ async function generateHlsPlaylistCommon (options: {
|
||||||
outputPath,
|
outputPath,
|
||||||
|
|
||||||
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
||||||
profile: 'default',
|
profile: CONFIG.TRANSCODING.PROFILE,
|
||||||
|
|
||||||
resolution,
|
resolution,
|
||||||
copyCodecs,
|
copyCodecs,
|
||||||
|
|
|
@ -50,9 +50,9 @@ const getExternalAuthValidator = [
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
const plugin = res.locals.registeredPlugin
|
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)
|
if (!externalAuth) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||||
|
|
||||||
res.locals.externalAuth = externalAuth
|
res.locals.externalAuth = externalAuth
|
||||||
|
|
|
@ -96,10 +96,11 @@ import {
|
||||||
MVideoWithRights
|
MVideoWithRights
|
||||||
} from '../../types/models'
|
} from '../../types/models'
|
||||||
import { MThumbnail } from '../../types/models/video/thumbnail'
|
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 { VideoAbuseModel } from '../abuse/video-abuse'
|
||||||
import { AccountModel } from '../account/account'
|
import { AccountModel } from '../account/account'
|
||||||
import { AccountVideoRateModel } from '../account/account-video-rate'
|
import { AccountVideoRateModel } from '../account/account-video-rate'
|
||||||
|
import { UserModel } from '../account/user'
|
||||||
import { UserVideoHistoryModel } from '../account/user-video-history'
|
import { UserVideoHistoryModel } from '../account/user-video-history'
|
||||||
import { ActorModel } from '../activitypub/actor'
|
import { ActorModel } from '../activitypub/actor'
|
||||||
import { AvatarModel } from '../avatar/avatar'
|
import { AvatarModel } from '../avatar/avatar'
|
||||||
|
@ -129,7 +130,6 @@ import { VideoShareModel } from './video-share'
|
||||||
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
|
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
|
||||||
import { VideoTagModel } from './video-tag'
|
import { VideoTagModel } from './video-tag'
|
||||||
import { VideoViewModel } from './video-view'
|
import { VideoViewModel } from './video-view'
|
||||||
import { UserModel } from '../account/user'
|
|
||||||
|
|
||||||
export enum ScopeNames {
|
export enum ScopeNames {
|
||||||
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
|
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
|
||||||
|
|
|
@ -87,6 +87,7 @@ describe('Test config API validators', function () {
|
||||||
allowAdditionalExtensions: true,
|
allowAdditionalExtensions: true,
|
||||||
allowAudioFiles: true,
|
allowAudioFiles: true,
|
||||||
threads: 1,
|
threads: 1,
|
||||||
|
profile: 'vod_profile',
|
||||||
resolutions: {
|
resolutions: {
|
||||||
'0p': false,
|
'0p': false,
|
||||||
'240p': false,
|
'240p': false,
|
||||||
|
@ -115,6 +116,7 @@ describe('Test config API validators', function () {
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
threads: 4,
|
threads: 4,
|
||||||
|
profile: 'live_profile',
|
||||||
resolutions: {
|
resolutions: {
|
||||||
'240p': true,
|
'240p': true,
|
||||||
'360p': true,
|
'360p': true,
|
||||||
|
|
|
@ -70,6 +70,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
|
||||||
expect(data.transcoding.allowAdditionalExtensions).to.be.false
|
expect(data.transcoding.allowAdditionalExtensions).to.be.false
|
||||||
expect(data.transcoding.allowAudioFiles).to.be.false
|
expect(data.transcoding.allowAudioFiles).to.be.false
|
||||||
expect(data.transcoding.threads).to.equal(2)
|
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['240p']).to.be.true
|
||||||
expect(data.transcoding.resolutions['360p']).to.be.true
|
expect(data.transcoding.resolutions['360p']).to.be.true
|
||||||
expect(data.transcoding.resolutions['480p']).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.maxUserLives).to.equal(3)
|
||||||
expect(data.live.transcoding.enabled).to.be.false
|
expect(data.live.transcoding.enabled).to.be.false
|
||||||
expect(data.live.transcoding.threads).to.equal(2)
|
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['240p']).to.be.false
|
||||||
expect(data.live.transcoding.resolutions['360p']).to.be.false
|
expect(data.live.transcoding.resolutions['360p']).to.be.false
|
||||||
expect(data.live.transcoding.resolutions['480p']).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.threads).to.equal(1)
|
||||||
expect(data.transcoding.allowAdditionalExtensions).to.be.true
|
expect(data.transcoding.allowAdditionalExtensions).to.be.true
|
||||||
expect(data.transcoding.allowAudioFiles).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['240p']).to.be.false
|
||||||
expect(data.transcoding.resolutions['360p']).to.be.true
|
expect(data.transcoding.resolutions['360p']).to.be.true
|
||||||
expect(data.transcoding.resolutions['480p']).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.maxUserLives).to.equal(10)
|
||||||
expect(data.live.transcoding.enabled).to.be.true
|
expect(data.live.transcoding.enabled).to.be.true
|
||||||
expect(data.live.transcoding.threads).to.equal(4)
|
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['240p']).to.be.true
|
||||||
expect(data.live.transcoding.resolutions['360p']).to.be.true
|
expect(data.live.transcoding.resolutions['360p']).to.be.true
|
||||||
expect(data.live.transcoding.resolutions['480p']).to.be.true
|
expect(data.live.transcoding.resolutions['480p']).to.be.true
|
||||||
|
@ -319,6 +323,7 @@ describe('Test config', function () {
|
||||||
allowAdditionalExtensions: true,
|
allowAdditionalExtensions: true,
|
||||||
allowAudioFiles: true,
|
allowAudioFiles: true,
|
||||||
threads: 1,
|
threads: 1,
|
||||||
|
profile: 'vod_profile',
|
||||||
resolutions: {
|
resolutions: {
|
||||||
'0p': false,
|
'0p': false,
|
||||||
'240p': false,
|
'240p': false,
|
||||||
|
@ -345,6 +350,7 @@ describe('Test config', function () {
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
threads: 4,
|
threads: 4,
|
||||||
|
profile: 'live_profile',
|
||||||
resolutions: {
|
resolutions: {
|
||||||
'240p': true,
|
'240p': true,
|
||||||
'360p': true,
|
'360p': true,
|
||||||
|
|
|
@ -511,7 +511,9 @@ describe('Test video transcoding', function () {
|
||||||
|
|
||||||
const resolutions = [ 240, 360, 480, 720, 1080 ]
|
const resolutions = [ 240, 360, 480, 720, 1080 ]
|
||||||
for (const r of resolutions) {
|
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 './action-hooks'
|
||||||
import './html-injection'
|
|
||||||
import './id-and-pass-auth'
|
|
||||||
import './external-auth'
|
import './external-auth'
|
||||||
import './filter-hooks'
|
import './filter-hooks'
|
||||||
import './translations'
|
import './html-injection'
|
||||||
import './video-constants'
|
import './id-and-pass-auth'
|
||||||
import './plugin-helpers'
|
import './plugin-helpers'
|
||||||
import './plugin-router'
|
import './plugin-router'
|
||||||
import './plugin-storage'
|
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,
|
PluginPlaylistPrivacyManager,
|
||||||
PluginSettingsManager,
|
PluginSettingsManager,
|
||||||
PluginStorageManager,
|
PluginStorageManager,
|
||||||
|
PluginTranscodingManager,
|
||||||
PluginVideoCategoryManager,
|
PluginVideoCategoryManager,
|
||||||
PluginVideoLanguageManager,
|
PluginVideoLanguageManager,
|
||||||
PluginVideoLicenceManager,
|
PluginVideoLicenceManager,
|
||||||
|
@ -68,6 +69,8 @@ export type RegisterServerOptions = {
|
||||||
videoPrivacyManager: PluginVideoPrivacyManager
|
videoPrivacyManager: PluginVideoPrivacyManager
|
||||||
playlistPrivacyManager: PluginPlaylistPrivacyManager
|
playlistPrivacyManager: PluginPlaylistPrivacyManager
|
||||||
|
|
||||||
|
transcodingManager: PluginTranscodingManager
|
||||||
|
|
||||||
registerIdAndPassAuth: (options: RegisterServerAuthPassOptions) => void
|
registerIdAndPassAuth: (options: RegisterServerAuthPassOptions) => void
|
||||||
registerExternalAuth: (options: RegisterServerAuthExternalOptions) => RegisterServerAuthExternalResult
|
registerExternalAuth: (options: RegisterServerAuthExternalOptions) => RegisterServerAuthExternalResult
|
||||||
unregisterIdAndPassAuth: (authName: string) => void
|
unregisterIdAndPassAuth: (authName: string) => void
|
||||||
|
|
|
@ -112,6 +112,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
|
||||||
allowAdditionalExtensions: true,
|
allowAdditionalExtensions: true,
|
||||||
allowAudioFiles: true,
|
allowAudioFiles: true,
|
||||||
threads: 1,
|
threads: 1,
|
||||||
|
profile: 'default',
|
||||||
resolutions: {
|
resolutions: {
|
||||||
'0p': false,
|
'0p': false,
|
||||||
'240p': false,
|
'240p': false,
|
||||||
|
@ -138,6 +139,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
threads: 4,
|
threads: 4,
|
||||||
|
profile: 'default',
|
||||||
resolutions: {
|
resolutions: {
|
||||||
'240p': true,
|
'240p': true,
|
||||||
'360p': true,
|
'360p': true,
|
||||||
|
|
|
@ -11,6 +11,7 @@ export * from './plugin-package-json.model'
|
||||||
export * from './plugin-playlist-privacy-manager.model'
|
export * from './plugin-playlist-privacy-manager.model'
|
||||||
export * from './plugin-settings-manager.model'
|
export * from './plugin-settings-manager.model'
|
||||||
export * from './plugin-storage-manager.model'
|
export * from './plugin-storage-manager.model'
|
||||||
|
export * from './plugin-transcoding-manager.model'
|
||||||
export * from './plugin-translation.model'
|
export * from './plugin-translation.model'
|
||||||
export * from './plugin-video-category-manager.model'
|
export * from './plugin-video-category-manager.model'
|
||||||
export * from './plugin-video-language-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
|
allowAudioFiles: boolean
|
||||||
|
|
||||||
threads: number
|
threads: number
|
||||||
|
|
||||||
|
profile: string
|
||||||
|
|
||||||
resolutions: ConfigResolutions & { '0p': boolean }
|
resolutions: ConfigResolutions & { '0p': boolean }
|
||||||
|
|
||||||
webtorrent: {
|
webtorrent: {
|
||||||
|
@ -110,6 +113,7 @@ export interface CustomConfig {
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
threads: number
|
threads: number
|
||||||
|
profile: string
|
||||||
resolutions: ConfigResolutions
|
resolutions: ConfigResolutions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,9 @@ export interface ServerConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
enabledResolutions: number[]
|
enabledResolutions: number[]
|
||||||
|
|
||||||
|
profile: string
|
||||||
|
availableProfiles: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
live: {
|
live: {
|
||||||
|
@ -110,6 +113,9 @@ export interface ServerConfig {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
|
||||||
enabledResolutions: number[]
|
enabledResolutions: number[]
|
||||||
|
|
||||||
|
profile: string
|
||||||
|
availableProfiles: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
rtmp: {
|
rtmp: {
|
||||||
|
|
|
@ -34,6 +34,7 @@ export * from './video-state.enum'
|
||||||
export * from './video-streaming-playlist.model'
|
export * from './video-streaming-playlist.model'
|
||||||
export * from './video-streaming-playlist.type'
|
export * from './video-streaming-playlist.type'
|
||||||
|
|
||||||
|
export * from './video-transcoding.model'
|
||||||
export * from './video-transcoding-fps.model'
|
export * from './video-transcoding-fps.model'
|
||||||
|
|
||||||
export * from './video-update.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