Add max lives limit

This commit is contained in:
Chocobozzz 2020-10-28 15:24:40 +01:00 committed by Chocobozzz
parent d846d99c6c
commit a056ca4813
19 changed files with 147 additions and 5 deletions

View File

@ -739,6 +739,16 @@
</my-peertube-checkbox>
</div>
<div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
<label i18n for="liveMaxInstanceLives">Max lives created on your instance (-1 for "unlimited")</label>
<input type="number" name="liveMaxInstanceLives" formControlName="maxInstanceLives" />
</div>
<div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
<label i18n for="liveMaxUserLives">Max lives created per user (-1 for "unlimited")</label>
<input type="number" name="liveMaxUserLives" formControlName="maxUserLives" />
</div>
<div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
<label i18n for="liveMaxDuration">Max live duration</label>
<div class="peertube-select-container">

View File

@ -216,6 +216,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
enabled: null,
maxDuration: null,
maxInstanceLives: null,
maxUserLives: null,
allowReplay: null,
transcoding: {

View File

@ -7,7 +7,7 @@ import { scrollToTop } from '@app/helpers'
import { FormValidatorService } from '@app/shared/shared-forms'
import { LiveVideoService, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
import { LoadingBarService } from '@ngx-loading-bar/core'
import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoPrivacy } from '@shared/models'
import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, ServerErrorCode, VideoPrivacy } from '@shared/models'
import { VideoSend } from './video-send'
@Component({
@ -81,7 +81,16 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon
err => {
this.firstStepError.emit()
this.notifier.error(err.message)
let message = err.message
if (err.body?.code === ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED) {
message = $localize`Cannot create live because this instance have too many created lives`
} else if (err.body?.code) {
message = $localize`Cannot create live because you created too many lives`
}
this.notifier.error(message)
}
)
}

View File

@ -78,6 +78,8 @@ export class ServerService {
enabled: false,
allowReplay: true,
maxDuration: null,
maxInstanceLives: -1,
maxUserLives: -1,
transcoding: {
enabled: false,
enabledResolutions: []

View File

@ -81,6 +81,13 @@
</td>
</tr>
<tr>
<th i18n class="sub-label" scope="row">Max parallel lives</th>
<td i18n>
{{ maxUserLives }} per user / {{ maxInstanceLives }} per instance
</td>
</tr>
<tr>
<th i18n class="label" colspan="2">Import</th>
</tr>

View File

@ -21,6 +21,20 @@ export class InstanceFeaturesTableComponent implements OnInit {
return Math.min(this.initialUserVideoQuota, this.serverConfig.user.videoQuotaDaily)
}
get maxInstanceLives () {
const value = this.serverConfig.live.maxInstanceLives
if (value === -1) return $localize`Unlimited`
return value
}
get maxUserLives () {
const value = this.serverConfig.live.maxUserLives
if (value === -1) return $localize`Unlimited`
return value
}
ngOnInit () {
this.serverConfig = this.serverService.getTmpConfig()
this.serverService.getConfig()

View File

@ -250,6 +250,14 @@ live:
# Set null to disable duration limit
max_duration: 5 hours
# Limit max number of live videos created on your instance
# -1 == unlimited
max_instance_lives: 20
# Limit max number of live videos created by a user on your instance
# -1 == unlimited
max_user_lives: 3
# Allow your users to save a replay of their live
# PeerTube will transcode segments in a video file
# If the user daily/total quota is reached, PeerTube will stop the live

View File

@ -120,6 +120,8 @@ async function getConfig (req: express.Request, res: express.Response) {
allowReplay: CONFIG.LIVE.ALLOW_REPLAY,
maxDuration: CONFIG.LIVE.MAX_DURATION,
maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES,
maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,
transcoding: {
enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
@ -430,6 +432,8 @@ function customConfig (): CustomConfig {
enabled: CONFIG.LIVE.ENABLED,
allowReplay: CONFIG.LIVE.ALLOW_REPLAY,
maxDuration: CONFIG.LIVE.MAX_DURATION,
maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES,
maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,
transcoding: {
enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
threads: CONFIG.LIVE.TRANSCODING.THREADS,

View File

@ -38,7 +38,7 @@ function checkMissedConfig () {
'federation.videos.federate_unlisted',
'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url',
'search.search_index.disable_local_search', 'search.search_index.is_default_search',
'live.enabled', 'live.allow_replay', 'live.max_duration',
'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives',
'live.transcoding.enabled', 'live.transcoding.threads',
'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p', 'live.transcoding.resolutions.480p',
'live.transcoding.resolutions.720p', 'live.transcoding.resolutions.1080p', 'live.transcoding.resolutions.2160p'

View File

@ -202,6 +202,9 @@ const CONFIG = {
get ENABLED () { return config.get<boolean>('live.enabled') },
get MAX_DURATION () { return parseDurationToMs(config.get<string>('live.max_duration')) },
get MAX_INSTANCE_LIVES () { return config.get<number>('live.max_instance_lives') },
get MAX_USER_LIVES () { return config.get<number>('live.max_user_lives') },
get ALLOW_REPLAY () { return config.get<boolean>('live.allow_replay') },
RTMP: {

View File

@ -65,6 +65,8 @@ const customConfigUpdateValidator = [
body('live.enabled').isBoolean().withMessage('Should have a valid live enabled boolean'),
body('live.allowReplay').isBoolean().withMessage('Should have a valid live allow replay boolean'),
body('live.maxDuration').custom(isIntOrNull).withMessage('Should have a valid live max duration'),
body('live.maxInstanceLives').custom(isIntOrNull).withMessage('Should have a valid max instance lives'),
body('live.maxUserLives').custom(isIntOrNull).withMessage('Should have a valid max user lives'),
body('live.transcoding.enabled').isBoolean().withMessage('Should have a valid live transcoding enabled boolean'),
body('live.transcoding.threads').isInt().withMessage('Should have a valid live transcoding threads'),
body('live.transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'),

View File

@ -2,7 +2,7 @@ import * as express from 'express'
import { body, param } from 'express-validator'
import { checkUserCanManageVideo, doesVideoChannelOfAccountExist, doesVideoExist } from '@server/helpers/middlewares/videos'
import { VideoLiveModel } from '@server/models/video/video-live'
import { UserRight, VideoState } from '@shared/models'
import { ServerErrorCode, UserRight, VideoState } from '@shared/models'
import { isBooleanValid, isIdOrUUIDValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
import { isVideoNameValid } from '../../../helpers/custom-validators/videos'
import { cleanUpReqFiles } from '../../../helpers/express-utils'
@ -10,6 +10,7 @@ import { logger } from '../../../helpers/logger'
import { CONFIG } from '../../../initializers/config'
import { areValidationErrors } from '../utils'
import { getCommonVideoEditAttributes } from './videos'
import { VideoModel } from '@server/models/video/video'
const videoLiveGetValidator = [
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
@ -50,11 +51,15 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
logger.debug('Checking videoLiveAddValidator parameters', { parameters: req.body })
if (CONFIG.LIVE.ENABLED !== true) {
cleanUpReqFiles(req)
return res.status(403)
.json({ error: 'Live is not enabled on this instance' })
}
if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) {
cleanUpReqFiles(req)
return res.status(403)
.json({ error: 'Saving live replay is not allowed instance' })
}
@ -64,6 +69,34 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
const user = res.locals.oauth.token.User
if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
if (CONFIG.LIVE.MAX_INSTANCE_LIVES !== -1) {
const totalInstanceLives = await VideoModel.countLocalLives()
if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) {
cleanUpReqFiles(req)
return res.status(403)
.json({
code: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED,
error: 'Cannot create this live because the max instance lives limit is reached.'
})
}
}
if (CONFIG.LIVE.MAX_USER_LIVES !== -1) {
const totalUserLives = await VideoModel.countLivesOfAccount(user.Account.id)
if (totalUserLives >= CONFIG.LIVE.MAX_USER_LIVES) {
cleanUpReqFiles(req)
return res.status(403)
.json({
code: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED,
error: 'Cannot create this live because the max user lives limit is reached.'
})
}
}
return next()
}
])

View File

@ -1142,6 +1142,37 @@ export class VideoModel extends Model<VideoModel> {
return VideoModel.getAvailableForApi(queryOptions)
}
static countLocalLives () {
const options = {
where: {
remote: false,
isLive: true
}
}
return VideoModel.count(options)
}
static countLivesOfAccount (accountId: number) {
const options = {
where: {
remote: false,
isLive: true
},
include: [
{
required: true,
model: VideoChannelModel.unscoped(),
where: {
accountId
}
}
]
}
return VideoModel.count(options)
}
static load (id: number | string, t?: Transaction): Bluebird<MVideoThumbnail> {
const where = buildWhereIdOrUUID(id)
const options = {

View File

@ -105,6 +105,8 @@ describe('Test config API validators', function () {
allowReplay: false,
maxDuration: null,
maxInstanceLives: -1,
maxUserLives: 50,
transcoding: {
enabled: true,

View File

@ -81,6 +81,8 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
expect(data.live.enabled).to.be.false
expect(data.live.allowReplay).to.be.true
expect(data.live.maxDuration).to.equal(1000 * 3600 * 5)
expect(data.live.maxInstanceLives).to.equal(20)
expect(data.live.maxUserLives).to.equal(3)
expect(data.live.transcoding.enabled).to.be.false
expect(data.live.transcoding.threads).to.equal(2)
expect(data.live.transcoding.resolutions['240p']).to.be.false
@ -166,6 +168,8 @@ function checkUpdatedConfig (data: CustomConfig) {
expect(data.live.enabled).to.be.true
expect(data.live.allowReplay).to.be.false
expect(data.live.maxDuration).to.equal(5000)
expect(data.live.maxInstanceLives).to.equal(-1)
expect(data.live.maxUserLives).to.equal(10)
expect(data.live.transcoding.enabled).to.be.true
expect(data.live.transcoding.threads).to.equal(4)
expect(data.live.transcoding.resolutions['240p']).to.be.true
@ -330,6 +334,8 @@ describe('Test config', function () {
enabled: true,
allowReplay: false,
maxDuration: 5000,
maxInstanceLives: -1,
maxUserLives: 10,
transcoding: {
enabled: true,
threads: 4,

View File

@ -130,6 +130,8 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
enabled: true,
allowReplay: false,
maxDuration: null,
maxInstanceLives: -1,
maxUserLives: 50,
transcoding: {
enabled: true,
threads: 4,

View File

@ -99,7 +99,10 @@ export interface CustomConfig {
enabled: boolean
allowReplay: boolean
maxDuration: number
maxInstanceLives: number
maxUserLives: number
transcoding: {
enabled: boolean

View File

@ -102,6 +102,8 @@ export interface ServerConfig {
enabled: boolean
maxDuration: number
maxInstanceLives: number
maxUserLives: number
allowReplay: boolean
transcoding: {

View File

@ -1,3 +1,5 @@
export const enum ServerErrorCode {
DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS = 1
DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS = 1,
MAX_INSTANCE_LIVES_LIMIT_REACHED = 2,
MAX_USER_LIVES_LIMIT_REACHED = 3,
}