add client hook filter:videojs.options

closes #4086
This commit is contained in:
kontrollanten 2021-05-15 06:30:24 +02:00 committed by Chocobozzz
parent 520bf885c5
commit 72f611ca15
9 changed files with 339 additions and 320 deletions

View File

@ -12,6 +12,7 @@ import {
MetaService, MetaService,
Notifier, Notifier,
PeerTubeSocket, PeerTubeSocket,
PluginService,
RestExtractor, RestExtractor,
ScreenService, ScreenService,
ServerService, ServerService,
@ -146,6 +147,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private videoCaptionService: VideoCaptionService, private videoCaptionService: VideoCaptionService,
private hotkeysService: HotkeysService, private hotkeysService: HotkeysService,
private hooks: HooksService, private hooks: HooksService,
private pluginService: PluginService,
private peertubeSocket: PeerTubeSocket, private peertubeSocket: PeerTubeSocket,
private screenService: ScreenService, private screenService: ScreenService,
private location: PlatformLocation, private location: PlatformLocation,
@ -859,7 +861,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
webtorrent: { webtorrent: {
videoFiles: video.files videoFiles: video.files
} },
pluginsManager: this.pluginService.getPluginsManager()
} }
// Only set this if we're in a playlist // Only set this if we're in a playlist

View File

@ -1,6 +1,5 @@
import * as debug from 'debug' import { Observable, of } from 'rxjs'
import { Observable, of, ReplaySubject } from 'rxjs' import { catchError, map, shareReplay } from 'rxjs/operators'
import { catchError, first, map, shareReplay } from 'rxjs/operators'
import { HttpClient } from '@angular/common/http' import { HttpClient } from '@angular/common/http'
import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core' import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core'
import { VideoEditType } from '@app/+videos/+video-edit/shared/video-edit.type' import { VideoEditType } from '@app/+videos/+video-edit/shared/video-edit.type'
@ -11,7 +10,7 @@ import { RestExtractor } from '@app/core/rest'
import { ServerService } from '@app/core/server/server.service' import { ServerService } from '@app/core/server/server.service'
import { getDevLocale, isOnDevLocale } from '@app/helpers' import { getDevLocale, isOnDevLocale } from '@app/helpers'
import { CustomModalComponent } from '@app/modal/custom-modal.component' import { CustomModalComponent } from '@app/modal/custom-modal.component'
import { FormFields, Hooks, loadPlugin, PluginInfo, runHook } from '@root-helpers/plugins' import { PluginInfo, PluginsManager } from '@root-helpers/plugins-manager'
import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n' import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n'
import { import {
ClientHook, ClientHook,
@ -20,49 +19,39 @@ import {
PluginTranslation, PluginTranslation,
PluginType, PluginType,
PublicServerSetting, PublicServerSetting,
RegisterClientFormFieldOptions,
RegisterClientSettingsScript, RegisterClientSettingsScript,
RegisterClientVideoFieldOptions,
ServerConfigPlugin ServerConfigPlugin
} from '@shared/models' } from '@shared/models'
import { environment } from '../../../environments/environment' import { environment } from '../../../environments/environment'
import { RegisterClientHelpers } from '../../../types/register-client-option.model' import { RegisterClientHelpers } from '../../../types/register-client-option.model'
const logger = debug('peertube:plugins') type FormFields = {
video: {
commonOptions: RegisterClientFormFieldOptions
videoFormOptions: RegisterClientVideoFieldOptions
}[]
}
@Injectable() @Injectable()
export class PluginService implements ClientHook { export class PluginService implements ClientHook {
private static BASE_PLUGIN_API_URL = environment.apiUrl + '/api/v1/plugins' private static BASE_PLUGIN_API_URL = environment.apiUrl + '/api/v1/plugins'
private static BASE_PLUGIN_URL = environment.apiUrl + '/plugins' private static BASE_PLUGIN_URL = environment.apiUrl + '/plugins'
pluginsLoaded: { [ scope in PluginClientScope ]: ReplaySubject<boolean> } = {
common: new ReplaySubject<boolean>(1),
'admin-plugin': new ReplaySubject<boolean>(1),
search: new ReplaySubject<boolean>(1),
'video-watch': new ReplaySubject<boolean>(1),
signup: new ReplaySubject<boolean>(1),
login: new ReplaySubject<boolean>(1),
'video-edit': new ReplaySubject<boolean>(1),
embed: new ReplaySubject<boolean>(1)
}
translationsObservable: Observable<PluginTranslation> translationsObservable: Observable<PluginTranslation>
customModal: CustomModalComponent customModal: CustomModalComponent
private plugins: ServerConfigPlugin[] = []
private helpers: { [ npmName: string ]: RegisterClientHelpers } = {} private helpers: { [ npmName: string ]: RegisterClientHelpers } = {}
private scopes: { [ scopeName: string ]: PluginInfo[] } = {}
private loadedScripts: { [ script: string ]: boolean } = {}
private loadedScopes: PluginClientScope[] = []
private loadingScopes: { [id in PluginClientScope]?: boolean } = {}
private hooks: Hooks = {}
private formFields: FormFields = { private formFields: FormFields = {
video: [] video: []
} }
private settingsScripts: { [ npmName: string ]: RegisterClientSettingsScript } = {} private settingsScripts: { [ npmName: string ]: RegisterClientSettingsScript } = {}
private pluginsManager: PluginsManager
constructor ( constructor (
private authService: AuthService, private authService: AuthService,
private notifier: Notifier, private notifier: Notifier,
@ -74,111 +63,48 @@ export class PluginService implements ClientHook {
@Inject(LOCALE_ID) private localeId: string @Inject(LOCALE_ID) private localeId: string
) { ) {
this.loadTranslations() this.loadTranslations()
this.pluginsManager = new PluginsManager({
peertubeHelpersFactory: this.buildPeerTubeHelpers.bind(this),
onFormFields: this.onFormFields.bind(this),
onSettingsScripts: this.onSettingsScripts.bind(this)
})
} }
initializePlugins () { initializePlugins () {
const config = this.server.getHTMLConfig() this.pluginsManager.loadPluginsList(this.server.getHTMLConfig())
this.plugins = config.plugin.registered
this.buildScopeStruct() this.pluginsManager.ensurePluginsAreLoaded('common')
this.ensurePluginsAreLoaded('common')
} }
initializeCustomModal (customModal: CustomModalComponent) { initializeCustomModal (customModal: CustomModalComponent) {
this.customModal = customModal this.customModal = customModal
} }
ensurePluginsAreLoaded (scope: PluginClientScope) { runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> {
this.loadPluginsByScope(scope) return this.zone.runOutsideAngular(() => {
return this.pluginsManager.runHook(hookName, result, params)
})
}
return this.pluginsLoaded[scope].asObservable() ensurePluginsAreLoaded (scope: PluginClientScope) {
.pipe(first(), shareReplay()) return this.pluginsManager.ensurePluginsAreLoaded(scope)
.toPromise() }
reloadLoadedScopes () {
return this.pluginsManager.reloadLoadedScopes()
}
getPluginsManager () {
return this.pluginsManager
} }
addPlugin (plugin: ServerConfigPlugin, isTheme = false) { addPlugin (plugin: ServerConfigPlugin, isTheme = false) {
const pathPrefix = this.getPluginPathPrefix(isTheme) return this.pluginsManager.addPlugin(plugin, isTheme)
for (const key of Object.keys(plugin.clientScripts)) {
const clientScript = plugin.clientScripts[key]
for (const scope of clientScript.scopes) {
if (!this.scopes[scope]) this.scopes[scope] = []
this.scopes[scope].push({
plugin,
clientScript: {
script: `${pathPrefix}/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`,
scopes: clientScript.scopes
},
pluginType: isTheme ? PluginType.THEME : PluginType.PLUGIN,
isTheme
})
this.loadedScripts[clientScript.script] = false
}
}
} }
removePlugin (plugin: ServerConfigPlugin) { removePlugin (plugin: ServerConfigPlugin) {
for (const key of Object.keys(this.scopes)) { return this.pluginsManager.removePlugin(plugin)
this.scopes[key] = this.scopes[key].filter(o => o.plugin.name !== plugin.name)
}
}
async reloadLoadedScopes () {
for (const scope of this.loadedScopes) {
await this.loadPluginsByScope(scope, true)
}
}
async loadPluginsByScope (scope: PluginClientScope, isReload = false) {
if (this.loadingScopes[scope]) return
if (!isReload && this.loadedScopes.includes(scope)) return
this.loadingScopes[scope] = true
logger('Loading scope %s', scope)
try {
if (!isReload) this.loadedScopes.push(scope)
const toLoad = this.scopes[ scope ]
if (!Array.isArray(toLoad)) {
this.loadingScopes[scope] = false
this.pluginsLoaded[scope].next(true)
logger('Nothing to load for scope %s', scope)
return
}
const promises: Promise<any>[] = []
for (const pluginInfo of toLoad) {
const clientScript = pluginInfo.clientScript
if (this.loadedScripts[ clientScript.script ]) continue
promises.push(this.loadPlugin(pluginInfo))
this.loadedScripts[ clientScript.script ] = true
}
await Promise.all(promises)
this.pluginsLoaded[scope].next(true)
this.loadingScopes[scope] = false
logger('Scope %s loaded', scope)
} catch (err) {
console.error('Cannot load plugins by scope %s.', scope, err)
}
}
runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> {
return this.zone.runOutsideAngular(() => {
return runHook(this.hooks, hookName, result, params)
})
} }
nameToNpmName (name: string, type: PluginType) { nameToNpmName (name: string, type: PluginType) {
@ -189,12 +115,6 @@ export class PluginService implements ClientHook {
return prefix + name return prefix + name
} }
pluginTypeFromNpmName (npmName: string) {
return npmName.startsWith('peertube-plugin-')
? PluginType.PLUGIN
: PluginType.THEME
}
getRegisteredVideoFormFields (type: VideoEditType) { getRegisteredVideoFormFields (type: VideoEditType) {
return this.formFields.video.filter(f => f.videoFormOptions.type === type) return this.formFields.video.filter(f => f.videoFormOptions.type === type)
} }
@ -213,27 +133,17 @@ export class PluginService implements ClientHook {
return helpers.translate(toTranslate) return helpers.translate(toTranslate)
} }
private loadPlugin (pluginInfo: PluginInfo) { private onFormFields (commonOptions: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) {
return this.zone.runOutsideAngular(() => { this.formFields.video.push({
const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType) commonOptions,
videoFormOptions
const helpers = this.buildPeerTubeHelpers(pluginInfo)
this.helpers[npmName] = helpers
return loadPlugin({
hooks: this.hooks,
formFields: this.formFields,
onSettingsScripts: options => this.settingsScripts[npmName] = options,
pluginInfo,
peertubeHelpersFactory: () => helpers
})
}) })
} }
private buildScopeStruct () { private onSettingsScripts (pluginInfo: PluginInfo, options: RegisterClientSettingsScript) {
for (const plugin of this.plugins) { const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType)
this.addPlugin(plugin)
} this.settingsScripts[npmName] = options
} }
private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers { private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers {
@ -242,12 +152,12 @@ export class PluginService implements ClientHook {
return { return {
getBaseStaticRoute: () => { getBaseStaticRoute: () => {
const pathPrefix = this.getPluginPathPrefix(pluginInfo.isTheme) const pathPrefix = PluginsManager.getPluginPathPrefix(pluginInfo.isTheme)
return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/static` return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/static`
}, },
getBaseRouterRoute: () => { getBaseRouterRoute: () => {
const pathPrefix = this.getPluginPathPrefix(pluginInfo.isTheme) const pathPrefix = PluginsManager.getPluginPathPrefix(pluginInfo.isTheme)
return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/router` return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/router`
}, },
@ -324,8 +234,4 @@ export class PluginService implements ClientHook {
.get<PluginTranslation>(PluginService.BASE_PLUGIN_URL + '/translations/' + completeLocale + '.json') .get<PluginTranslation>(PluginService.BASE_PLUGIN_URL + '/translations/' + completeLocale + '.json')
.pipe(shareReplay()) .pipe(shareReplay())
} }
private getPluginPathPrefix (isTheme: boolean) {
return isTheme ? '/themes' : '/plugins'
}
} }

View File

@ -114,6 +114,7 @@ export class ThemeService {
const theme = this.getTheme(currentTheme) const theme = this.getTheme(currentTheme)
if (theme) { if (theme) {
console.log('Adding scripts of theme %s.', currentTheme) console.log('Adding scripts of theme %s.', currentTheme)
this.pluginService.addPlugin(theme, true) this.pluginService.addPlugin(theme, true)
this.pluginService.reloadLoadedScopes() this.pluginService.reloadLoadedScopes()

View File

@ -22,8 +22,10 @@ import './videojs-components/settings-panel-child'
import './videojs-components/theater-button' import './videojs-components/theater-button'
import './playlist/playlist-plugin' import './playlist/playlist-plugin'
import videojs from 'video.js' import videojs from 'video.js'
import { PluginsManager } from '@root-helpers/plugins-manager'
import { isDefaultLocale } from '@shared/core-utils/i18n' import { isDefaultLocale } from '@shared/core-utils/i18n'
import { VideoFile } from '@shared/models' import { VideoFile } from '@shared/models'
import { copyToClipboard } from '../../root-helpers/utils'
import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
@ -37,8 +39,7 @@ import {
VideoJSPluginOptions VideoJSPluginOptions
} from './peertube-videojs-typings' } from './peertube-videojs-typings'
import { TranslationsManager } from './translations-manager' import { TranslationsManager } from './translations-manager'
import { buildVideoOrPlaylistEmbed, buildVideoLink, getRtcConfig, isSafari, isIOS } from './utils' import { buildVideoLink, buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils'
import { copyToClipboard } from '../../root-helpers/utils'
// Change 'Playback Rate' to 'Speed' (smaller for our settings menu) // Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
(videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed' (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed'
@ -116,21 +117,26 @@ export interface CommonOptions extends CustomizationOptions {
} }
export type PeertubePlayerManagerOptions = { export type PeertubePlayerManagerOptions = {
common: CommonOptions, common: CommonOptions
webtorrent: WebtorrentOptions, webtorrent: WebtorrentOptions
p2pMediaLoader?: P2PMediaLoaderOptions p2pMediaLoader?: P2PMediaLoaderOptions
pluginsManager: PluginsManager
} }
export class PeertubePlayerManager { export class PeertubePlayerManager {
private static playerElementClassName: string private static playerElementClassName: string
private static onPlayerChange: (player: videojs.Player) => void private static onPlayerChange: (player: videojs.Player) => void
private static alreadyPlayed = false private static alreadyPlayed = false
private static pluginsManager: PluginsManager
static initState () { static initState () {
PeertubePlayerManager.alreadyPlayed = false PeertubePlayerManager.alreadyPlayed = false
} }
static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: videojs.Player) => void) { static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: videojs.Player) => void) {
this.pluginsManager = options.pluginsManager
let p2pMediaLoader: any let p2pMediaLoader: any
this.onPlayerChange = onPlayerChange this.onPlayerChange = onPlayerChange
@ -144,7 +150,7 @@ export class PeertubePlayerManager {
]) ])
} }
const videojsOptions = this.getVideojsOptions(mode, options, p2pMediaLoader) const videojsOptions = await this.getVideojsOptions(mode, options, p2pMediaLoader)
await TranslationsManager.loadLocaleInVideoJS(options.common.serverUrl, options.common.language, videojs) await TranslationsManager.loadLocaleInVideoJS(options.common.serverUrl, options.common.language, videojs)
@ -206,7 +212,7 @@ export class PeertubePlayerManager {
await import('./webtorrent/webtorrent-plugin') await import('./webtorrent/webtorrent-plugin')
const mode = 'webtorrent' const mode = 'webtorrent'
const videojsOptions = this.getVideojsOptions(mode, options) const videojsOptions = await this.getVideojsOptions(mode, options)
const self = this const self = this
videojs(newVideoElement, videojsOptions, function (this: videojs.Player) { videojs(newVideoElement, videojsOptions, function (this: videojs.Player) {
@ -218,16 +224,16 @@ export class PeertubePlayerManager {
}) })
} }
private static getVideojsOptions ( private static async getVideojsOptions (
mode: PlayerMode, mode: PlayerMode,
options: PeertubePlayerManagerOptions, options: PeertubePlayerManagerOptions,
p2pMediaLoaderModule?: any p2pMediaLoaderModule?: any
): videojs.PlayerOptions { ): Promise<videojs.PlayerOptions> {
const commonOptions = options.common const commonOptions = options.common
const isHLS = mode === 'p2p-media-loader' const isHLS = mode === 'p2p-media-loader'
let autoplay = this.getAutoPlayValue(commonOptions.autoplay) let autoplay = this.getAutoPlayValue(commonOptions.autoplay)
let html5 = { const html5 = {
preloadTextTracks: false preloadTextTracks: false
} }
@ -306,7 +312,7 @@ export class PeertubePlayerManager {
Object.assign(videojsOptions, { language: commonOptions.language }) Object.assign(videojsOptions, { language: commonOptions.language })
} }
return videojsOptions return this.pluginsManager.runHook('filter:internal.player.videojs.options.result', videojsOptions)
} }
private static addP2PMediaLoaderOptions ( private static addP2PMediaLoaderOptions (

View File

@ -2,3 +2,4 @@ export * from './users'
export * from './bytes' export * from './bytes'
export * from './peertube-web-storage' export * from './peertube-web-storage'
export * from './utils' export * from './utils'
export * from './plugins-manager'

View File

@ -0,0 +1,251 @@
import * as debug from 'debug'
import { ReplaySubject } from 'rxjs'
import { first, shareReplay } from 'rxjs/operators'
import { RegisterClientHelpers } from 'src/types/register-client-option.model'
import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
import {
ClientHookName,
clientHookObject,
ClientScript,
HTMLServerConfig,
PluginClientScope,
PluginType,
RegisterClientFormFieldOptions,
RegisterClientHookOptions,
RegisterClientSettingsScript,
RegisterClientVideoFieldOptions,
ServerConfigPlugin
} from '../../../shared/models'
import { environment } from '../environments/environment'
import { ClientScript as ClientScriptModule } from '../types/client-script.model'
interface HookStructValue extends RegisterClientHookOptions {
plugin: ServerConfigPlugin
clientScript: ClientScript
}
type Hooks = { [ name: string ]: HookStructValue[] }
type PluginInfo = {
plugin: ServerConfigPlugin
clientScript: ClientScript
pluginType: PluginType
isTheme: boolean
}
type PeertubeHelpersFactory = (pluginInfo: PluginInfo) => RegisterClientHelpers
type OnFormFields = (options: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => void
type OnSettingsScripts = (pluginInfo: PluginInfo, options: RegisterClientSettingsScript) => void
const logger = debug('peertube:plugins')
class PluginsManager {
private hooks: Hooks = {}
private scopes: { [ scopeName: string ]: PluginInfo[] } = {}
private loadedScripts: { [ script: string ]: boolean } = {}
private loadedScopes: PluginClientScope[] = []
private loadingScopes: { [id in PluginClientScope]?: boolean } = {}
private pluginsLoaded: { [ scope in PluginClientScope ]: ReplaySubject<boolean> } = {
common: new ReplaySubject<boolean>(1),
'admin-plugin': new ReplaySubject<boolean>(1),
search: new ReplaySubject<boolean>(1),
'video-watch': new ReplaySubject<boolean>(1),
signup: new ReplaySubject<boolean>(1),
login: new ReplaySubject<boolean>(1),
'video-edit': new ReplaySubject<boolean>(1),
embed: new ReplaySubject<boolean>(1)
}
private readonly peertubeHelpersFactory: PeertubeHelpersFactory
private readonly onFormFields: OnFormFields
private readonly onSettingsScripts: OnSettingsScripts
constructor (options: {
peertubeHelpersFactory: PeertubeHelpersFactory
onFormFields?: OnFormFields
onSettingsScripts?: OnSettingsScripts
}) {
this.peertubeHelpersFactory = options.peertubeHelpersFactory
this.onFormFields = options.onFormFields
this.onSettingsScripts = options.onSettingsScripts
}
static getPluginPathPrefix (isTheme: boolean) {
return isTheme ? '/themes' : '/plugins'
}
loadPluginsList (config: HTMLServerConfig) {
for (const plugin of config.plugin.registered) {
this.addPlugin(plugin)
}
}
async runHook<T> (hookName: ClientHookName, result?: T, params?: any) {
if (!this.hooks[hookName]) return result
const hookType = getHookType(hookName)
for (const hook of this.hooks[hookName]) {
console.log('Running hook %s of plugin %s.', hookName, hook.plugin.name)
result = await internalRunHook(hook.handler, hookType, result, params, err => {
console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.clientScript.script, hook.plugin.name, err)
})
}
return result
}
ensurePluginsAreLoaded (scope: PluginClientScope) {
this.loadPluginsByScope(scope)
return this.pluginsLoaded[scope].asObservable()
.pipe(first(), shareReplay())
.toPromise()
}
async reloadLoadedScopes () {
for (const scope of this.loadedScopes) {
await this.loadPluginsByScope(scope, true)
}
}
addPlugin (plugin: ServerConfigPlugin, isTheme = false) {
const pathPrefix = PluginsManager.getPluginPathPrefix(isTheme)
for (const key of Object.keys(plugin.clientScripts)) {
const clientScript = plugin.clientScripts[key]
for (const scope of clientScript.scopes) {
if (!this.scopes[scope]) this.scopes[scope] = []
this.scopes[scope].push({
plugin,
clientScript: {
script: `${pathPrefix}/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`,
scopes: clientScript.scopes
},
pluginType: isTheme ? PluginType.THEME : PluginType.PLUGIN,
isTheme
})
this.loadedScripts[clientScript.script] = false
}
}
}
removePlugin (plugin: ServerConfigPlugin) {
for (const key of Object.keys(this.scopes)) {
this.scopes[key] = this.scopes[key].filter(o => o.plugin.name !== plugin.name)
}
}
async loadPluginsByScope (scope: PluginClientScope, isReload = false) {
if (this.loadingScopes[scope]) return
if (!isReload && this.loadedScopes.includes(scope)) return
this.loadingScopes[scope] = true
logger('Loading scope %s', scope)
try {
if (!isReload) this.loadedScopes.push(scope)
const toLoad = this.scopes[ scope ]
if (!Array.isArray(toLoad)) {
this.loadingScopes[scope] = false
this.pluginsLoaded[scope].next(true)
logger('Nothing to load for scope %s', scope)
return
}
const promises: Promise<any>[] = []
for (const pluginInfo of toLoad) {
const clientScript = pluginInfo.clientScript
if (this.loadedScripts[ clientScript.script ]) continue
promises.push(this.loadPlugin(pluginInfo))
this.loadedScripts[ clientScript.script ] = true
}
await Promise.all(promises)
this.pluginsLoaded[scope].next(true)
this.loadingScopes[scope] = false
logger('Scope %s loaded', scope)
} catch (err) {
console.error('Cannot load plugins by scope %s.', scope, err)
}
}
private loadPlugin (pluginInfo: PluginInfo) {
const { plugin, clientScript } = pluginInfo
const registerHook = (options: RegisterClientHookOptions) => {
if (clientHookObject[options.target] !== true) {
console.error('Unknown hook %s of plugin %s. Skipping.', options.target, plugin.name)
return
}
if (!this.hooks[options.target]) this.hooks[options.target] = []
this.hooks[options.target].push({
plugin,
clientScript,
target: options.target,
handler: options.handler,
priority: options.priority || 0
})
}
const registerVideoField = (commonOptions: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => {
if (!this.onFormFields) {
throw new Error('Video field registration is not supported')
}
return this.onFormFields(commonOptions, videoFormOptions)
}
const registerSettingsScript = (options: RegisterClientSettingsScript) => {
if (!this.onSettingsScripts) {
throw new Error('Registering settings script is not supported')
}
return this.onSettingsScripts(pluginInfo, options)
}
const peertubeHelpers = this.peertubeHelpersFactory(pluginInfo)
console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name)
const absURL = (environment.apiUrl || window.location.origin) + clientScript.script
return import(/* webpackIgnore: true */ absURL)
.then((script: ClientScriptModule) => script.register({ registerHook, registerVideoField, registerSettingsScript, peertubeHelpers }))
.then(() => this.sortHooksByPriority())
.catch(err => console.error('Cannot import or register plugin %s.', pluginInfo.plugin.name, err))
}
private sortHooksByPriority () {
for (const hookName of Object.keys(this.hooks)) {
this.hooks[hookName].sort((a, b) => {
return b.priority - a.priority
})
}
}
}
export {
PluginsManager,
PluginInfo,
PeertubeHelpersFactory,
OnFormFields,
OnSettingsScripts
}

View File

@ -1,126 +0,0 @@
import { RegisterClientHelpers } from 'src/types/register-client-option.model'
import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
import {
ClientHookName,
clientHookObject,
ClientScript,
PluginType,
RegisterClientFormFieldOptions,
RegisterClientHookOptions,
RegisterClientSettingsScript,
RegisterClientVideoFieldOptions,
ServerConfigPlugin
} from '../../../shared/models'
import { environment } from '../environments/environment'
import { ClientScript as ClientScriptModule } from '../types/client-script.model'
interface HookStructValue extends RegisterClientHookOptions {
plugin: ServerConfigPlugin
clientScript: ClientScript
}
type Hooks = { [ name: string ]: HookStructValue[] }
type PluginInfo = {
plugin: ServerConfigPlugin
clientScript: ClientScript
pluginType: PluginType
isTheme: boolean
}
type FormFields = {
video: {
commonOptions: RegisterClientFormFieldOptions
videoFormOptions: RegisterClientVideoFieldOptions
}[]
}
async function runHook<T> (hooks: Hooks, hookName: ClientHookName, result?: T, params?: any) {
if (!hooks[hookName]) return result
const hookType = getHookType(hookName)
for (const hook of hooks[hookName]) {
console.log('Running hook %s of plugin %s.', hookName, hook.plugin.name)
result = await internalRunHook(hook.handler, hookType, result, params, err => {
console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.clientScript.script, hook.plugin.name, err)
})
}
return result
}
function loadPlugin (options: {
hooks: Hooks
pluginInfo: PluginInfo
peertubeHelpersFactory: (pluginInfo: PluginInfo) => RegisterClientHelpers
formFields?: FormFields
onSettingsScripts?: (options: RegisterClientSettingsScript) => void
}) {
const { hooks, pluginInfo, peertubeHelpersFactory, formFields, onSettingsScripts } = options
const { plugin, clientScript } = pluginInfo
const registerHook = (options: RegisterClientHookOptions) => {
if (clientHookObject[options.target] !== true) {
console.error('Unknown hook %s of plugin %s. Skipping.', options.target, plugin.name)
return
}
if (!hooks[options.target]) hooks[options.target] = []
hooks[options.target].push({
plugin,
clientScript,
target: options.target,
handler: options.handler,
priority: options.priority || 0
})
}
const registerVideoField = (commonOptions: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => {
if (!formFields) {
throw new Error('Video field registration is not supported')
}
formFields.video.push({
commonOptions,
videoFormOptions
})
}
const registerSettingsScript = (options: RegisterClientSettingsScript) => {
if (!onSettingsScripts) {
throw new Error('Registering settings script is not supported')
}
return onSettingsScripts(options)
}
const peertubeHelpers = peertubeHelpersFactory(pluginInfo)
console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name)
const absURL = (environment.apiUrl || window.location.origin) + clientScript.script
return import(/* webpackIgnore: true */ absURL)
.then((script: ClientScriptModule) => script.register({ registerHook, registerVideoField, registerSettingsScript, peertubeHelpers }))
.then(() => sortHooksByPriority(hooks))
.catch(err => console.error('Cannot import or register plugin %s.', pluginInfo.plugin.name, err))
}
export {
HookStructValue,
Hooks,
PluginInfo,
FormFields,
loadPlugin,
runHook
}
function sortHooksByPriority (hooks: Hooks) {
for (const hookName of Object.keys(hooks)) {
hooks[hookName].sort((a, b) => {
return b.priority - a.priority
})
}
}

View File

@ -3,7 +3,6 @@ import videojs from 'video.js'
import { peertubeTranslate } from '../../../../shared/core-utils/i18n' import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import { import {
ClientHookName,
HTMLServerConfig, HTMLServerConfig,
OAuth2ErrorCode, OAuth2ErrorCode,
PluginType, PluginType,
@ -19,7 +18,7 @@ import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from
import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
import { TranslationsManager } from '../../assets/player/translations-manager' import { TranslationsManager } from '../../assets/player/translations-manager'
import { peertubeLocalStorage } from '../../root-helpers/peertube-web-storage' import { peertubeLocalStorage } from '../../root-helpers/peertube-web-storage'
import { Hooks, loadPlugin, runHook } from '../../root-helpers/plugins' import { PluginsManager } from '../../root-helpers/plugins-manager'
import { Tokens } from '../../root-helpers/users' import { Tokens } from '../../root-helpers/users'
import { objectToUrlEncoded } from '../../root-helpers/utils' import { objectToUrlEncoded } from '../../root-helpers/utils'
import { RegisterClientHelpers } from '../../types/register-client-option.model' import { RegisterClientHelpers } from '../../types/register-client-option.model'
@ -68,8 +67,7 @@ export class PeerTubeEmbed {
private wrapperElement: HTMLElement private wrapperElement: HTMLElement
private peertubeHooks: Hooks = {} private pluginsManager: PluginsManager
private loadedScripts = new Set<string>()
static async main () { static async main () {
const videoContainerId = 'video-wrapper' const videoContainerId = 'video-wrapper'
@ -489,7 +487,7 @@ export class PeerTubeEmbed {
this.PeertubePlayerManagerModulePromise this.PeertubePlayerManagerModulePromise
]) ])
await this.ensurePluginsAreLoaded(serverTranslations) await this.loadPlugins(serverTranslations)
const videoInfo: VideoDetails = videoInfoTmp const videoInfo: VideoDetails = videoInfoTmp
@ -560,7 +558,9 @@ export class PeerTubeEmbed {
webtorrent: { webtorrent: {
videoFiles: videoInfo.files videoFiles: videoInfo.files
} },
pluginsManager: this.pluginsManager
} }
if (this.mode === 'p2p-media-loader') { if (this.mode === 'p2p-media-loader') {
@ -600,7 +600,7 @@ export class PeerTubeEmbed {
}) })
} }
this.runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video: videoInfo }) this.pluginsManager.runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video: videoInfo })
} }
private async initCore () { private async initCore () {
@ -740,37 +740,14 @@ export class PeerTubeEmbed {
return window.location.pathname.split('/')[1] === 'video-playlists' return window.location.pathname.split('/')[1] === 'video-playlists'
} }
private async ensurePluginsAreLoaded (translations?: { [ id: string ]: string }) { private loadPlugins (translations?: { [ id: string ]: string }) {
if (this.config.plugin.registered.length === 0) return this.pluginsManager = new PluginsManager({
peertubeHelpersFactory: _ => this.buildPeerTubeHelpers(translations)
})
for (const plugin of this.config.plugin.registered) { this.pluginsManager.loadPluginsList(this.config)
for (const key of Object.keys(plugin.clientScripts)) {
const clientScript = plugin.clientScripts[key]
if (clientScript.scopes.includes('embed') === false) continue return this.pluginsManager.ensurePluginsAreLoaded('embed')
const script = `/plugins/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`
if (this.loadedScripts.has(script)) continue
const pluginInfo = {
plugin,
clientScript: {
script,
scopes: clientScript.scopes
},
pluginType: PluginType.PLUGIN,
isTheme: false
}
await loadPlugin({
hooks: this.peertubeHooks,
pluginInfo,
onSettingsScripts: () => undefined,
peertubeHelpersFactory: _ => this.buildPeerTubeHelpers(translations)
})
}
}
} }
private buildPeerTubeHelpers (translations?: { [ id: string ]: string }): RegisterClientHelpers { private buildPeerTubeHelpers (translations?: { [ id: string ]: string }): RegisterClientHelpers {
@ -808,10 +785,6 @@ export class PeerTubeEmbed {
} }
} }
} }
private runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> {
return runHook(this.peertubeHooks, hookName, result, params)
}
} }
PeerTubeEmbed.main() PeerTubeEmbed.main()

View File

@ -53,7 +53,10 @@ export const clientFilterHookObject = {
'filter:internal.common.svg-icons.get-content.result': true, 'filter:internal.common.svg-icons.get-content.result': true,
// Filter left menu links // Filter left menu links
'filter:left-menu.links.create.result': true 'filter:left-menu.links.create.result': true,
// Filter videojs options built for PeerTube player
'filter:internal.player.videojs.options.result': true
} }
export type ClientFilterHookName = keyof typeof clientFilterHookObject export type ClientFilterHookName = keyof typeof clientFilterHookObject