Add max lives limit
This commit is contained in:
parent
d846d99c6c
commit
a056ca4813
|
@ -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">
|
||||
|
|
|
@ -216,6 +216,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
|
|||
enabled: null,
|
||||
|
||||
maxDuration: null,
|
||||
maxInstanceLives: null,
|
||||
maxUserLives: null,
|
||||
allowReplay: null,
|
||||
|
||||
transcoding: {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -78,6 +78,8 @@ export class ServerService {
|
|||
enabled: false,
|
||||
allowReplay: true,
|
||||
maxDuration: null,
|
||||
maxInstanceLives: -1,
|
||||
maxUserLives: -1,
|
||||
transcoding: {
|
||||
enabled: false,
|
||||
enabledResolutions: []
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
])
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -105,6 +105,8 @@ describe('Test config API validators', function () {
|
|||
|
||||
allowReplay: false,
|
||||
maxDuration: null,
|
||||
maxInstanceLives: -1,
|
||||
maxUserLives: 50,
|
||||
|
||||
transcoding: {
|
||||
enabled: true,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -99,7 +99,10 @@ export interface CustomConfig {
|
|||
enabled: boolean
|
||||
|
||||
allowReplay: boolean
|
||||
|
||||
maxDuration: number
|
||||
maxInstanceLives: number
|
||||
maxUserLives: number
|
||||
|
||||
transcoding: {
|
||||
enabled: boolean
|
||||
|
|
|
@ -102,6 +102,8 @@ export interface ServerConfig {
|
|||
enabled: boolean
|
||||
|
||||
maxDuration: number
|
||||
maxInstanceLives: number
|
||||
maxUserLives: number
|
||||
allowReplay: boolean
|
||||
|
||||
transcoding: {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue