</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">
enabled: null,
maxDuration: null,
+ maxInstanceLives: null,
+ maxUserLives: null,
allowReplay: null,
transcoding: {
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({
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)
}
)
}
enabled: false,
allowReplay: true,
maxDuration: null,
+ maxInstanceLives: -1,
+ maxUserLives: -1,
transcoding: {
enabled: false,
enabledResolutions: []
</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>
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()
# 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
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,
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,
'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'
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: {
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'),
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'
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'),
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' })
}
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()
}
])
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 = {
allowReplay: false,
maxDuration: null,
+ maxInstanceLives: -1,
+ maxUserLives: 50,
transcoding: {
enabled: true,
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
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
enabled: true,
allowReplay: false,
maxDuration: 5000,
+ maxInstanceLives: -1,
+ maxUserLives: 10,
transcoding: {
enabled: true,
threads: 4,
enabled: true,
allowReplay: false,
maxDuration: null,
+ maxInstanceLives: -1,
+ maxUserLives: 50,
transcoding: {
enabled: true,
threads: 4,
enabled: boolean
allowReplay: boolean
+
maxDuration: number
+ maxInstanceLives: number
+ maxUserLives: number
transcoding: {
enabled: boolean
enabled: boolean
maxDuration: number
+ maxInstanceLives: number
+ maxUserLives: number
allowReplay: boolean
transcoding: {
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,
}