Merge remote-tracking branch 'weblate/develop' into develop

This commit is contained in:
Chocobozzz 2022-03-11 11:11:00 +01:00
commit 4495806f2f
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
47 changed files with 8191 additions and 7373 deletions

View File

@ -189,6 +189,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
maxInstanceLives: MAX_INSTANCE_LIVES_VALIDATOR,
maxUserLives: MAX_USER_LIVES_VALIDATOR,
allowReplay: null,
latencySetting: {
enabled: null
},
transcoding: {
enabled: null,

View File

@ -36,6 +36,18 @@
</my-peertube-checkbox>
</div>
<div class="form-group" formGroupName="latencySetting" [ngClass]="getDisabledLiveClass()">
<my-peertube-checkbox
inputName="liveLatencySettingEnabled" formControlName="enabled"
i18n-labelText labelText="Allow your users to change live latency"
>
<ng-container ngProjectAs="description" i18n>
Small latency disables P2P and high latency can increase P2P ratio
</ng-container>
</my-peertube-checkbox>
</div>
<div class="form-group" [ngClass]="getDisabledLiveClass()">
<label i18n for="liveMaxInstanceLives">
Max simultaneous lives created on your instance <span class="text-muted">(-1 for "unlimited")</span>

View File

@ -289,6 +289,17 @@
</ng-container>
</my-peertube-checkbox>
</div>
<div class="form-group" *ngIf="isLatencyModeEnabled()">
<label i18n for="latencyMode">Latency mode</label>
<my-select-options
labelForId="latencyMode" [items]="latencyModes" formControlName="latencyMode" [clearable]="true"
></my-select-options>
<div *ngIf="formErrors.latencyMode" class="form-error">
{{ formErrors.latencyMode }}
</div>
</div>
</div>
</div>
</ng-template>

View File

@ -1,6 +1,6 @@
import { forkJoin } from 'rxjs'
import { map } from 'rxjs/operators'
import { SelectChannelItem } from 'src/types/select-options-item.model'
import { SelectChannelItem, SelectOptionsItem } from 'src/types/select-options-item.model'
import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms'
import { HooksService, PluginService, ServerService } from '@app/core'
@ -26,6 +26,7 @@ import { PluginInfo } from '@root-helpers/plugins-manager'
import {
HTMLServerConfig,
LiveVideo,
LiveVideoLatencyMode,
RegisterClientFormFieldOptions,
RegisterClientVideoFieldOptions,
VideoConstant,
@ -78,6 +79,23 @@ export class VideoEditComponent implements OnInit, OnDestroy {
videoCategories: VideoConstant<number>[] = []
videoLicences: VideoConstant<number>[] = []
videoLanguages: VideoLanguages[] = []
latencyModes: SelectOptionsItem[] = [
{
id: LiveVideoLatencyMode.SMALL_LATENCY,
label: $localize`Small latency`,
description: $localize`Reduce latency to ~15s disabling P2P`
},
{
id: LiveVideoLatencyMode.DEFAULT,
label: $localize`Default`,
description: $localize`Average latency of 30s`
},
{
id: LiveVideoLatencyMode.HIGH_LATENCY,
label: $localize`High latency`,
description: $localize`Average latency of 60s increasing P2P ratio`
}
]
pluginDataFormGroup: FormGroup
@ -141,6 +159,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR,
liveStreamKey: null,
permanentLive: null,
latencyMode: null,
saveReplay: null
}
@ -273,6 +292,10 @@ export class VideoEditComponent implements OnInit, OnDestroy {
return this.form.value['permanentLive'] === true
}
isLatencyModeEnabled () {
return this.serverConfig.live.latencySetting.enabled
}
isPluginFieldHidden (pluginField: PluginField) {
if (typeof pluginField.commonOptions.hidden !== 'function') return false

View File

@ -64,6 +64,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
if (this.liveVideo) {
this.form.patchValue({
saveReplay: this.liveVideo.saveReplay,
latencyMode: this.liveVideo.latencyMode,
permanentLive: this.liveVideo.permanentLive
})
}
@ -127,7 +128,8 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
const liveVideoUpdate: LiveVideoUpdate = {
saveReplay: !!this.form.value.saveReplay,
permanentLive: !!this.form.value.permanentLive
permanentLive: !!this.form.value.permanentLive,
latencyMode: this.form.value.latencyMode
}
// Don't update live attributes if they did not change

View File

@ -1,5 +1,5 @@
import { Hotkey, HotkeysService } from 'angular2-hotkeys'
import { forkJoin, Subscription } from 'rxjs'
import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs'
import { isP2PEnabled } from 'src/assets/player/utils'
import { PlatformLocation } from '@angular/common'
import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
@ -22,11 +22,13 @@ import { HooksService } from '@app/core/plugins/hooks.service'
import { isXPercentInViewport, scrollToTop } from '@app/helpers'
import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main'
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
import { LiveVideoService } from '@app/shared/shared-video-live'
import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
import { timeToInt } from '@shared/core-utils'
import {
HTMLServerConfig,
HttpStatusCode,
LiveVideo,
PeerTubeProblemDocument,
ServerErrorCode,
VideoCaption,
@ -63,6 +65,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
video: VideoDetails = null
videoCaptions: VideoCaption[] = []
liveVideo: LiveVideo
playlistPosition: number
playlist: VideoPlaylist = null
@ -89,6 +92,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private router: Router,
private videoService: VideoService,
private playlistService: VideoPlaylistService,
private liveVideoService: LiveVideoService,
private confirmService: ConfirmService,
private metaService: MetaService,
private authService: AuthService,
@ -239,12 +243,21 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
'filter:api.video-watch.video.get.result'
)
const videoAndLiveObs: Observable<{ video: VideoDetails, live?: LiveVideo }> = videoObs.pipe(
switchMap(video => {
if (!video.isLive) return of({ video })
return this.liveVideoService.getVideoLive(video.uuid)
.pipe(map(live => ({ live, video })))
})
)
forkJoin([
videoObs,
videoAndLiveObs,
this.videoCaptionService.listCaptions(videoId),
this.userService.getAnonymousOrLoggedUser()
]).subscribe({
next: ([ video, captionsResult, loggedInOrAnonymousUser ]) => {
next: ([ { video, live }, captionsResult, loggedInOrAnonymousUser ]) => {
const queryParams = this.route.snapshot.queryParams
const urlOptions = {
@ -261,7 +274,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
peertubeLink: false
}
this.onVideoFetched({ video, videoCaptions: captionsResult.data, loggedInOrAnonymousUser, urlOptions })
this.onVideoFetched({ video, live, videoCaptions: captionsResult.data, loggedInOrAnonymousUser, urlOptions })
.catch(err => this.handleGlobalError(err))
},
@ -330,16 +343,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private async onVideoFetched (options: {
video: VideoDetails
live: LiveVideo
videoCaptions: VideoCaption[]
urlOptions: URLOptions
loggedInOrAnonymousUser: User
}) {
const { video, videoCaptions, urlOptions, loggedInOrAnonymousUser } = options
const { video, live, videoCaptions, urlOptions, loggedInOrAnonymousUser } = options
this.subscribeToLiveEventsIfNeeded(this.video, video)
this.video = video
this.videoCaptions = videoCaptions
this.liveVideo = live
// Re init attributes
this.playerPlaceholderImgSrc = undefined
@ -387,6 +402,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
const params = {
video: this.video,
videoCaptions: this.videoCaptions,
liveVideo: this.liveVideo,
urlOptions,
loggedInOrAnonymousUser,
user: this.user
@ -532,12 +548,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private buildPlayerManagerOptions (params: {
video: VideoDetails
liveVideo: LiveVideo
videoCaptions: VideoCaption[]
urlOptions: CustomizationOptions & { playerMode: PlayerMode }
loggedInOrAnonymousUser: User
user?: AuthUser
}) {
const { video, videoCaptions, urlOptions, loggedInOrAnonymousUser, user } = params
const { video, liveVideo, videoCaptions, urlOptions, loggedInOrAnonymousUser, user } = params
const getStartTime = () => {
const byUrl = urlOptions.startTime !== undefined
@ -562,6 +579,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
src: environment.apiUrl + c.captionPath
}))
const liveOptions = video.isLive
? { latencyMode: liveVideo.latencyMode }
: undefined
const options: PeertubePlayerManagerOptions = {
common: {
autoplay: this.isAutoplay(),
@ -597,6 +618,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
embedTitle: video.name,
isLive: video.isLive,
liveOptions,
language: this.localeId,

View File

@ -1,9 +1,10 @@
import videojs from 'video.js'
import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core'
import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
import { PluginsManager } from '@root-helpers/plugins-manager'
import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
import { isDefaultLocale } from '@shared/core-utils/i18n'
import { VideoFile } from '@shared/models'
import { LiveVideoLatencyMode, VideoFile } from '@shared/models'
import { copyToClipboard } from '../../root-helpers/utils'
import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
@ -19,7 +20,6 @@ import {
VideoJSPluginOptions
} from './peertube-videojs-typings'
import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils'
import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core'
export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
@ -76,6 +76,9 @@ export interface CommonOptions extends CustomizationOptions {
embedTitle: string
isLive: boolean
liveOptions?: {
latencyMode: LiveVideoLatencyMode
}
language?: string
@ -250,21 +253,8 @@ export class PeertubePlayerOptionsBuilder {
.filter(t => t.startsWith('ws'))
const specificLiveOrVODOptions = this.options.common.isLive
? { // Live
requiredSegmentsPriority: 1
}
: { // VOD
requiredSegmentsPriority: 3,
cachedSegmentExpiration: 86400000,
cachedSegmentsCount: 100,
httpDownloadMaxPriority: 9,
httpDownloadProbability: 0.06,
httpDownloadProbabilitySkipIfNoPeers: true,
p2pDownloadMaxPriority: 50
}
? this.getP2PMediaLoaderLiveOptions()
: this.getP2PMediaLoaderVODOptions()
return {
trackerAnnounce,
@ -283,13 +273,57 @@ export class PeertubePlayerOptionsBuilder {
}
}
private getP2PMediaLoaderLiveOptions (): Partial<HybridLoaderSettings> {
const base = {
requiredSegmentsPriority: 1
}
const latencyMode = this.options.common.liveOptions.latencyMode
switch (latencyMode) {
case LiveVideoLatencyMode.SMALL_LATENCY:
return {
...base,
useP2P: false,
httpDownloadProbability: 1
}
case LiveVideoLatencyMode.HIGH_LATENCY:
return base
default:
return base
}
}
private getP2PMediaLoaderVODOptions (): Partial<HybridLoaderSettings> {
return {
requiredSegmentsPriority: 3,
cachedSegmentExpiration: 86400000,
cachedSegmentsCount: 100,
httpDownloadMaxPriority: 9,
httpDownloadProbability: 0.06,
httpDownloadProbabilitySkipIfNoPeers: true,
p2pDownloadMaxPriority: 50
}
}
private getHLSOptions (p2pMediaLoaderConfig: HlsJsEngineSettings) {
const specificLiveOrVODOptions = this.options.common.isLive
? this.getHLSLiveOptions()
: this.getHLSVODOptions()
const base = {
capLevelToPlayerSize: true,
autoStartLoad: false,
liveSyncDurationCount: 5,
loader: new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass()
loader: new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass(),
...specificLiveOrVODOptions
}
const averageBandwidth = getAverageBandwidthInStore()
@ -305,6 +339,33 @@ export class PeertubePlayerOptionsBuilder {
}
}
private getHLSLiveOptions () {
const latencyMode = this.options.common.liveOptions.latencyMode
switch (latencyMode) {
case LiveVideoLatencyMode.SMALL_LATENCY:
return {
liveSyncDurationCount: 2
}
case LiveVideoLatencyMode.HIGH_LATENCY:
return {
liveSyncDurationCount: 10
}
default:
return {
liveSyncDurationCount: 5
}
}
}
private getHLSVODOptions () {
return {
liveSyncDurationCount: 5
}
}
private addWebTorrentOptions (plugins: VideoJSPluginOptions, alreadyPlayed: boolean) {
const commonOptions = this.options.common
const webtorrentOptions = this.options.webtorrent

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,6 +6,7 @@ import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
import {
HTMLServerConfig,
HttpStatusCode,
LiveVideo,
OAuth2ErrorCode,
ResultList,
UserRefreshToken,
@ -94,6 +95,10 @@ export class PeerTubeEmbed {
return window.location.origin + '/api/v1/videos/' + id
}
getLiveUrl (videoId: string) {
return window.location.origin + '/api/v1/videos/live/' + videoId
}
refreshFetch (url: string, options?: RequestInit) {
return fetch(url, options)
.then((res: Response) => {
@ -166,6 +171,12 @@ export class PeerTubeEmbed {
return this.refreshFetch(this.getVideoUrl(videoId) + '/captions', { headers: this.headers })
}
loadWithLive (video: VideoDetails) {
return this.refreshFetch(this.getLiveUrl(video.uuid), { headers: this.headers })
.then(res => res.json())
.then((live: LiveVideo) => ({ video, live }))
}
loadPlaylistInfo (playlistId: string): Promise<Response> {
return this.refreshFetch(this.getPlaylistUrl(playlistId), { headers: this.headers })
}
@ -475,13 +486,15 @@ export class PeerTubeEmbed {
.then(res => res.json())
}
const videoInfoPromise = videoResponse.json()
const videoInfoPromise: Promise<{ video: VideoDetails, live?: LiveVideo }> = videoResponse.json()
.then((videoInfo: VideoDetails) => {
this.loadParams(videoInfo)
if (!alreadyHadPlayer && !this.autoplay) this.loadPlaceholder(videoInfo)
if (!alreadyHadPlayer && !this.autoplay) this.buildPlaceholder(videoInfo)
return videoInfo
if (!videoInfo.isLive) return { video: videoInfo }
return this.loadWithLive(videoInfo)
})
const [ videoInfoTmp, serverTranslations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
@ -493,11 +506,15 @@ export class PeerTubeEmbed {
await this.loadPlugins(serverTranslations)
const videoInfo: VideoDetails = videoInfoTmp
const { video: videoInfo, live } = videoInfoTmp
const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse)
const liveOptions = videoInfo.isLive
? { latencyMode: live.latencyMode }
: undefined
const playlistPlugin = this.currentPlaylistElement
? {
elements: this.playlistElements,
@ -545,6 +562,7 @@ export class PeerTubeEmbed {
videoUUID: videoInfo.uuid,
isLive: videoInfo.isLive,
liveOptions,
playerElement: this.playerElement,
onPlayerElementChange: (element: HTMLVideoElement) => {
@ -726,7 +744,7 @@ export class PeerTubeEmbed {
return []
}
private loadPlaceholder (video: VideoDetails) {
private buildPlaceholder (video: VideoDetails) {
const placeholder = this.getPlaceholderElement()
const url = window.location.origin + video.previewPath

View File

@ -392,6 +392,12 @@ live:
# /!\ transcoding.enabled (and not live.transcoding.enabled) has to be true to create a replay
allow_replay: true
# Allow your users to change latency settings (small latency/default/high latency)
# Small latency live streams cannot use P2P
# High latency live streams can increase P2P ratio
latency_setting:
enabled: true
# Your firewall should accept traffic from this port in TCP if you enable live
rtmp:
enabled: true

View File

@ -400,6 +400,12 @@ live:
# /!\ transcoding.enabled (and not live.transcoding.enabled) has to be true to create a replay
allow_replay: true
# Allow your users to change latency settings (small latency/default/high latency)
# Small latency live streams cannot use P2P
# High latency live streams can increase P2P ratio
latency_setting:
enabled: true
# Your firewall should accept traffic from this port in TCP if you enable live
rtmp:
enabled: true

View File

@ -237,6 +237,9 @@ function customConfig (): CustomConfig {
live: {
enabled: CONFIG.LIVE.ENABLED,
allowReplay: CONFIG.LIVE.ALLOW_REPLAY,
latencySetting: {
enabled: CONFIG.LIVE.LATENCY_SETTING.ENABLED
},
maxDuration: CONFIG.LIVE.MAX_DURATION,
maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES,
maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,

View File

@ -1,4 +1,5 @@
import express from 'express'
import { exists } from '@server/helpers/custom-validators/misc'
import { createReqFiles } from '@server/helpers/express-utils'
import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants'
import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
@ -9,7 +10,7 @@ import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator
import { VideoLiveModel } from '@server/models/video/video-live'
import { MVideoDetails, MVideoFullLight } from '@server/types/models'
import { buildUUID, uuidToShort } from '@shared/extra-utils'
import { HttpStatusCode, LiveVideoCreate, LiveVideoUpdate, VideoState } from '@shared/models'
import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, VideoState } from '@shared/models'
import { logger } from '../../../helpers/logger'
import { sequelizeTypescript } from '../../../initializers/database'
import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail'
@ -60,8 +61,9 @@ async function updateLiveVideo (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
const videoLive = res.locals.videoLive
videoLive.saveReplay = body.saveReplay || false
videoLive.permanentLive = body.permanentLive || false
if (exists(body.saveReplay)) videoLive.saveReplay = body.saveReplay
if (exists(body.permanentLive)) videoLive.permanentLive = body.permanentLive
if (exists(body.latencyMode)) videoLive.latencyMode = body.latencyMode
video.VideoLive = await videoLive.save()
@ -87,6 +89,7 @@ async function addLiveVideo (req: express.Request, res: express.Response) {
const videoLive = new VideoLiveModel()
videoLive.saveReplay = videoInfo.saveReplay || false
videoLive.permanentLive = videoInfo.permanentLive || false
videoLive.latencyMode = videoInfo.latencyMode || LiveVideoLatencyMode.DEFAULT
videoLive.streamKey = buildUUID()
const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({

View File

@ -50,6 +50,10 @@ function getContextData (type: ContextType) {
'@type': 'sc:Boolean',
'@id': 'pt:permanentLive'
},
latencyMode: {
'@type': 'sc:Number',
'@id': 'pt:latencyMode'
},
Infohash: 'pt:Infohash',
Playlist: 'pt:Playlist',

View File

@ -1,10 +1,11 @@
import validator from 'validator'
import { logger } from '@server/helpers/logger'
import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@shared/models'
import { VideoState } from '../../../../shared/models/videos'
import { LiveVideoLatencyMode, VideoState } from '../../../../shared/models/videos'
import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants'
import { peertubeTruncate } from '../../core-utils'
import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
import { isLiveLatencyModeValid } from '../video-lives'
import {
isVideoDurationValid,
isVideoNameValid,
@ -65,6 +66,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false
if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false
if (!isBooleanValid(video.permanentLive)) video.permanentLive = false
if (!isLiveLatencyModeValid(video.latencyMode)) video.latencyMode = LiveVideoLatencyMode.DEFAULT
return isActivityPubUrlValid(video.id) &&
isVideoNameValid(video.name) &&

View File

@ -0,0 +1,11 @@
import { LiveVideoLatencyMode } from '@shared/models'
function isLiveLatencyModeValid (value: any) {
return [ LiveVideoLatencyMode.DEFAULT, LiveVideoLatencyMode.SMALL_LATENCY, LiveVideoLatencyMode.HIGH_LATENCY ].includes(value)
}
// ---------------------------------------------------------------------------
export {
isLiveLatencyModeValid
}

View File

@ -1,7 +1,7 @@
import { FfmpegCommand, FilterSpecification } from 'fluent-ffmpeg'
import { join } from 'path'
import { VIDEO_LIVE } from '@server/initializers/constants'
import { AvailableEncoders } from '@shared/models'
import { AvailableEncoders, LiveVideoLatencyMode } from '@shared/models'
import { logger, loggerTagsFactory } from '../logger'
import { buildStreamSuffix, getFFmpeg, getScaleFilter, StreamType } from './ffmpeg-commons'
import { getEncoderBuilderResult } from './ffmpeg-encoders'
@ -15,6 +15,7 @@ async function getLiveTranscodingCommand (options: {
outPath: string
masterPlaylistName: string
latencyMode: LiveVideoLatencyMode
resolutions: number[]
@ -26,7 +27,7 @@ async function getLiveTranscodingCommand (options: {
availableEncoders: AvailableEncoders
profile: string
}) {
const { inputUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName, ratio } = options
const { inputUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName, ratio, latencyMode } = options
const command = getFFmpeg(inputUrl, 'live')
@ -120,14 +121,21 @@ async function getLiveTranscodingCommand (options: {
command.complexFilter(complexFilter)
addDefaultLiveHLSParams(command, outPath, masterPlaylistName)
addDefaultLiveHLSParams({ command, outPath, masterPlaylistName, latencyMode })
command.outputOption('-var_stream_map', varStreamMap.join(' '))
return command
}
function getLiveMuxingCommand (inputUrl: string, outPath: string, masterPlaylistName: string) {
function getLiveMuxingCommand (options: {
inputUrl: string
outPath: string
masterPlaylistName: string
latencyMode: LiveVideoLatencyMode
}) {
const { inputUrl, outPath, masterPlaylistName, latencyMode } = options
const command = getFFmpeg(inputUrl, 'live')
command.outputOption('-c:v copy')
@ -135,22 +143,39 @@ function getLiveMuxingCommand (inputUrl: string, outPath: string, masterPlaylist
command.outputOption('-map 0:a?')
command.outputOption('-map 0:v?')
addDefaultLiveHLSParams(command, outPath, masterPlaylistName)
addDefaultLiveHLSParams({ command, outPath, masterPlaylistName, latencyMode })
return command
}
function getLiveSegmentTime (latencyMode: LiveVideoLatencyMode) {
if (latencyMode === LiveVideoLatencyMode.SMALL_LATENCY) {
return VIDEO_LIVE.SEGMENT_TIME_SECONDS.SMALL_LATENCY
}
return VIDEO_LIVE.SEGMENT_TIME_SECONDS.DEFAULT_LATENCY
}
// ---------------------------------------------------------------------------
export {
getLiveSegmentTime,
getLiveTranscodingCommand,
getLiveMuxingCommand
}
// ---------------------------------------------------------------------------
function addDefaultLiveHLSParams (command: FfmpegCommand, outPath: string, masterPlaylistName: string) {
command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS)
function addDefaultLiveHLSParams (options: {
command: FfmpegCommand
outPath: string
masterPlaylistName: string
latencyMode: LiveVideoLatencyMode
}) {
const { command, outPath, masterPlaylistName, latencyMode } = options
command.outputOption('-hls_time ' + getLiveSegmentTime(latencyMode))
command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE)
command.outputOption('-hls_flags delete_segments+independent_segments')
command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`)

View File

@ -49,8 +49,8 @@ function checkMissedConfig () {
'peertube.check_latest_version.enabled', 'peertube.check_latest_version.url',
'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.max_user_lives', 'live.max_instance_lives',
'live.rtmp.enabled', 'live.rtmp.port', 'live.rtmp.hostname',
'live.enabled', 'live.allow_replay', 'live.latency_setting.enabled', 'live.max_duration',
'live.max_user_lives', 'live.max_instance_lives', 'live.rtmp.enabled', 'live.rtmp.port', 'live.rtmp.hostname',
'live.rtmps.enabled', 'live.rtmps.port', 'live.rtmps.hostname', 'live.rtmps.key_file', 'live.rtmps.cert_file',
'live.transcoding.enabled', 'live.transcoding.threads', 'live.transcoding.profile',
'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p',

View File

@ -4,9 +4,9 @@ import { dirname, join } from 'path'
import { decacheModule } from '@server/helpers/decache'
import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
import { BroadcastMessageLevel } from '@shared/models/server'
import { buildPath, root } from '../../shared/core-utils'
import { VideoPrivacy, VideosRedundancyStrategy } from '../../shared/models'
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
import { buildPath, root } from '../../shared/core-utils'
import { parseBytes, parseDurationToMs } from '../helpers/core-utils'
// Use a variable to reload the configuration if we need
@ -296,6 +296,10 @@ const CONFIG = {
get ALLOW_REPLAY () { return config.get<boolean>('live.allow_replay') },
LATENCY_SETTING: {
get ENABLED () { return config.get<boolean>('live.latency_setting.enabled') }
},
RTMP: {
get ENABLED () { return config.get<boolean>('live.rtmp.enabled') },
get PORT () { return config.get<number>('live.rtmp.port') },

View File

@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 685
const LAST_MIGRATION_VERSION = 690
// ---------------------------------------------------------------------------
@ -700,7 +700,10 @@ const RESUMABLE_UPLOAD_SESSION_LIFETIME = SCHEDULER_INTERVALS_MS.REMOVE_DANGLING
const VIDEO_LIVE = {
EXTENSION: '.ts',
CLEANUP_DELAY: 1000 * 60 * 5, // 5 minutes
SEGMENT_TIME_SECONDS: 4, // 4 seconds
SEGMENT_TIME_SECONDS: {
DEFAULT_LATENCY: 4, // 4 seconds
SMALL_LATENCY: 2 // 2 seconds
},
SEGMENTS_LIST_SIZE: 15, // 15 maximum segments in live playlist
REPLAY_DIRECTORY: 'replay',
EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION: 4,
@ -842,7 +845,8 @@ if (isTestInstance() === true) {
PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000
VIDEO_LIVE.CLEANUP_DELAY = 5000
VIDEO_LIVE.SEGMENT_TIME_SECONDS = 2
VIDEO_LIVE.SEGMENT_TIME_SECONDS.DEFAULT_LATENCY = 2
VIDEO_LIVE.SEGMENT_TIME_SECONDS.SMALL_LATENCY = 1
VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION = 1
}

View File

@ -0,0 +1,35 @@
import { LiveVideoLatencyMode } from '@shared/models'
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
db: any
}): Promise<void> {
await utils.queryInterface.addColumn('videoLive', 'latencyMode', {
type: Sequelize.INTEGER,
defaultValue: null,
allowNull: true
}, { transaction: utils.transaction })
{
const query = `UPDATE "videoLive" SET "latencyMode" = ${LiveVideoLatencyMode.DEFAULT}`
await utils.sequelize.query(query, { type: Sequelize.QueryTypes.UPDATE, transaction: utils.transaction })
}
await utils.queryInterface.changeColumn('videoLive', 'latencyMode', {
type: Sequelize.INTEGER,
defaultValue: null,
allowNull: false
}, { transaction: utils.transaction })
}
function down () {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -151,6 +151,7 @@ function getLiveAttributesFromObject (video: MVideoId, videoObject: VideoObject)
return {
saveReplay: videoObject.liveSaveReplay,
permanentLive: videoObject.permanentLive,
latencyMode: videoObject.latencyMode,
videoId: video.id
}
}

View File

@ -5,9 +5,10 @@ import { createServer as createServerTLS, Server as ServerTLS } from 'tls'
import {
computeLowerResolutionsToTranscode,
ffprobePromise,
getLiveSegmentTime,
getVideoStreamBitrate,
getVideoStreamFPS,
getVideoStreamDimensionsInfo
getVideoStreamDimensionsInfo,
getVideoStreamFPS
} from '@server/helpers/ffmpeg'
import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config'
@ -353,7 +354,7 @@ class LiveManager {
.catch(err => logger.error('Cannot federate live video %s.', video.url, { err, ...localLTags }))
PeerTubeSocket.Instance.sendVideoLiveNewState(video)
}, VIDEO_LIVE.SEGMENT_TIME_SECONDS * 1000 * VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION)
}, getLiveSegmentTime(live.latencyMode) * 1000 * VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION)
} catch (err) {
logger.error('Cannot save/federate live video %d.', videoId, { err, ...localLTags })
}

View File

@ -125,6 +125,8 @@ class MuxingSession extends EventEmitter {
outPath,
masterPlaylistName: this.streamingPlaylist.playlistFilename,
latencyMode: this.videoLive.latencyMode,
resolutions: this.allResolutions,
fps: this.fps,
bitrate: this.bitrate,
@ -133,7 +135,12 @@ class MuxingSession extends EventEmitter {
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
profile: CONFIG.LIVE.TRANSCODING.PROFILE
})
: getLiveMuxingCommand(this.inputUrl, outPath, this.streamingPlaylist.playlistFilename)
: getLiveMuxingCommand({
inputUrl: this.inputUrl,
outPath,
masterPlaylistName: this.streamingPlaylist.playlistFilename,
latencyMode: this.videoLive.latencyMode
})
logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags())

View File

@ -137,6 +137,10 @@ class ServerConfigManager {
enabled: CONFIG.LIVE.ENABLED,
allowReplay: CONFIG.LIVE.ALLOW_REPLAY,
latencySetting: {
enabled: CONFIG.LIVE.LATENCY_SETTING.ENABLED
},
maxDuration: CONFIG.LIVE.MAX_DURATION,
maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES,
maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,

View File

@ -1,12 +1,21 @@
import express from 'express'
import { body } from 'express-validator'
import { isLiveLatencyModeValid } from '@server/helpers/custom-validators/video-lives'
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants'
import { isLocalLiveVideoAccepted } from '@server/lib/moderation'
import { Hooks } from '@server/lib/plugins/hooks'
import { VideoModel } from '@server/models/video/video'
import { VideoLiveModel } from '@server/models/video/video-live'
import { HttpStatusCode, ServerErrorCode, UserRight, VideoState } from '@shared/models'
import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
import {
HttpStatusCode,
LiveVideoCreate,
LiveVideoLatencyMode,
LiveVideoUpdate,
ServerErrorCode,
UserRight,
VideoState
} from '@shared/models'
import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
import { isVideoNameValid } from '../../../helpers/custom-validators/videos'
import { cleanUpReqFiles } from '../../../helpers/express-utils'
import { logger } from '../../../helpers/logger'
@ -67,6 +76,12 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have a valid permanentLive attribute'),
body('latencyMode')
.optional()
.customSanitizer(toIntOrNull)
.custom(isLiveLatencyModeValid)
.withMessage('Should have a valid latency mode attribute'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoLiveAddValidator parameters', { parameters: req.body })
@ -82,7 +97,9 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
})
}
if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) {
const body: LiveVideoCreate = req.body
if (hasValidSaveReplay(body) !== true) {
cleanUpReqFiles(req)
return res.fail({
@ -92,14 +109,23 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
})
}
if (req.body.permanentLive && req.body.saveReplay) {
if (hasValidLatencyMode(body) !== true) {
cleanUpReqFiles(req)
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Custom latency mode is not allowed by this instance'
})
}
if (body.permanentLive && body.saveReplay) {
cleanUpReqFiles(req)
return res.fail({ message: 'Cannot set this live as permanent while saving its replay' })
}
const user = res.locals.oauth.token.User
if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
if (!await doesVideoChannelOfAccountExist(body.channelId, user, res)) return cleanUpReqFiles(req)
if (CONFIG.LIVE.MAX_INSTANCE_LIVES !== -1) {
const totalInstanceLives = await VideoModel.countLocalLives()
@ -141,19 +167,34 @@ const videoLiveUpdateValidator = [
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have a valid saveReplay attribute'),
body('latencyMode')
.optional()
.customSanitizer(toIntOrNull)
.custom(isLiveLatencyModeValid)
.withMessage('Should have a valid latency mode attribute'),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoLiveUpdateValidator parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
if (req.body.permanentLive && req.body.saveReplay) {
const body: LiveVideoUpdate = req.body
if (body.permanentLive && body.saveReplay) {
return res.fail({ message: 'Cannot set this live as permanent while saving its replay' })
}
if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) {
if (hasValidSaveReplay(body) !== true) {
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Saving live replay is not allowed instance'
message: 'Saving live replay is not allowed by this instance'
})
}
if (hasValidLatencyMode(body) !== true) {
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Custom latency mode is not allowed by this instance'
})
}
@ -203,3 +244,19 @@ async function isLiveVideoAccepted (req: express.Request, res: express.Response)
return true
}
function hasValidSaveReplay (body: LiveVideoUpdate | LiveVideoCreate) {
if (CONFIG.LIVE.ALLOW_REPLAY !== true && body.saveReplay === true) return false
return true
}
function hasValidLatencyMode (body: LiveVideoUpdate | LiveVideoCreate) {
if (
CONFIG.LIVE.LATENCY_SETTING.ENABLED !== true &&
exists(body.latencyMode) &&
body.latencyMode !== LiveVideoLatencyMode.DEFAULT
) return false
return true
}

View File

@ -411,15 +411,6 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
views: video.views,
sensitive: video.nsfw,
waitTranscoding: video.waitTranscoding,
isLiveBroadcast: video.isLive,
liveSaveReplay: video.isLive
? video.VideoLive.saveReplay
: null,
permanentLive: video.isLive
? video.VideoLive.permanentLive
: null,
state: video.state,
commentsEnabled: video.commentsEnabled,
@ -431,10 +422,13 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
: null,
updated: video.updatedAt.toISOString(),
mediaType: 'text/markdown',
content: video.description,
support: video.support,
subtitleLanguage,
icon: icons.map(i => ({
type: 'Image',
url: i.getFileUrl(video),
@ -442,11 +436,14 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
width: i.width,
height: i.height
})),
url,
likes: getLocalVideoLikesActivityPubUrl(video),
dislikes: getLocalVideoDislikesActivityPubUrl(video),
shares: getLocalVideoSharesActivityPubUrl(video),
comments: getLocalVideoCommentsActivityPubUrl(video),
attributedTo: [
{
type: 'Person',
@ -456,7 +453,9 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
type: 'Group',
id: video.VideoChannel.Actor.url
}
]
],
...buildLiveAPAttributes(video)
}
}
@ -500,3 +499,23 @@ export {
getPrivacyLabel,
getStateLabel
}
// ---------------------------------------------------------------------------
function buildLiveAPAttributes (video: MVideoAP) {
if (!video.isLive) {
return {
isLiveBroadcast: false,
liveSaveReplay: null,
permanentLive: null,
latencyMode: null
}
}
return {
isLiveBroadcast: true,
liveSaveReplay: video.VideoLive.saveReplay,
permanentLive: video.VideoLive.permanentLive,
latencyMode: video.VideoLive.latencyMode
}
}

View File

@ -158,6 +158,7 @@ export class VideoTableAttributes {
'streamKey',
'saveReplay',
'permanentLive',
'latencyMode',
'videoId',
'createdAt',
'updatedAt'

View File

@ -1,11 +1,11 @@
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { CONFIG } from '@server/initializers/config'
import { WEBSERVER } from '@server/initializers/constants'
import { MVideoLive, MVideoLiveVideo } from '@server/types/models'
import { LiveVideo, LiveVideoLatencyMode, VideoState } from '@shared/models'
import { AttributesOnly } from '@shared/typescript-utils'
import { LiveVideo, VideoState } from '@shared/models'
import { VideoModel } from './video'
import { VideoBlacklistModel } from './video-blacklist'
import { CONFIG } from '@server/initializers/config'
@DefaultScope(() => ({
include: [
@ -44,6 +44,10 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>
@Column
permanentLive: boolean
@AllowNull(false)
@Column
latencyMode: LiveVideoLatencyMode
@CreatedAt
createdAt: Date
@ -113,7 +117,8 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>
streamKey: this.streamKey,
permanentLive: this.permanentLive,
saveReplay: this.saveReplay
saveReplay: this.saveReplay,
latencyMode: this.latencyMode
}
}
}

View File

@ -125,6 +125,9 @@ describe('Test config API validators', function () {
enabled: true,
allowReplay: false,
latencySetting: {
enabled: false
},
maxDuration: 30,
maxInstanceLives: -1,
maxUserLives: 50,

View File

@ -3,7 +3,7 @@
import 'mocha'
import { omit } from 'lodash'
import { buildAbsoluteFixturePath } from '@shared/core-utils'
import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@shared/models'
import { HttpStatusCode, LiveVideoLatencyMode, VideoCreateResult, VideoPrivacy } from '@shared/models'
import {
cleanupTests,
createSingleServer,
@ -38,6 +38,9 @@ describe('Test video lives API validator', function () {
newConfig: {
live: {
enabled: true,
latencySetting: {
enabled: false
},
maxInstanceLives: 20,
maxUserLives: 20,
allowReplay: true
@ -81,7 +84,8 @@ describe('Test video lives API validator', function () {
privacy: VideoPrivacy.PUBLIC,
channelId,
saveReplay: false,
permanentLive: false
permanentLive: false,
latencyMode: LiveVideoLatencyMode.DEFAULT
}
})
@ -214,6 +218,18 @@ describe('Test video lives API validator', function () {
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
})
it('Should fail with bad latency setting', async function () {
const fields = { ...baseCorrectParams, latencyMode: 42 }
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
})
it('Should fail to set latency if the server does not allow it', async function () {
const fields = { ...baseCorrectParams, latencyMode: LiveVideoLatencyMode.HIGH_LATENCY }
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
})
it('Should succeed with the correct parameters', async function () {
this.timeout(30000)
@ -393,6 +409,18 @@ describe('Test video lives API validator', function () {
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
})
it('Should fail with bad latency setting', async function () {
const fields = { latencyMode: 42 }
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
})
it('Should fail to set latency if the server does not allow it', async function () {
const fields = { latencyMode: LiveVideoLatencyMode.HIGH_LATENCY }
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
})
it('Should succeed with the correct params', async function () {
await command.update({ videoId: video.id, fields: { saveReplay: false } })
await command.update({ videoId: video.uuid, fields: { saveReplay: false } })

View File

@ -10,6 +10,7 @@ import {
HttpStatusCode,
LiveVideo,
LiveVideoCreate,
LiveVideoLatencyMode,
VideoDetails,
VideoPrivacy,
VideoState,
@ -52,6 +53,9 @@ describe('Test live', function () {
live: {
enabled: true,
allowReplay: true,
latencySetting: {
enabled: true
},
transcoding: {
enabled: false
}
@ -85,6 +89,7 @@ describe('Test live', function () {
commentsEnabled: false,
downloadEnabled: false,
saveReplay: true,
latencyMode: LiveVideoLatencyMode.SMALL_LATENCY,
privacy: VideoPrivacy.PUBLIC,
previewfile: 'video_short1-preview.webm.jpg',
thumbnailfile: 'video_short1.webm.jpg'
@ -131,6 +136,7 @@ describe('Test live', function () {
}
expect(live.saveReplay).to.be.true
expect(live.latencyMode).to.equal(LiveVideoLatencyMode.SMALL_LATENCY)
}
})
@ -175,7 +181,7 @@ describe('Test live', function () {
it('Should update the live', async function () {
this.timeout(10000)
await commands[0].update({ videoId: liveVideoUUID, fields: { saveReplay: false } })
await commands[0].update({ videoId: liveVideoUUID, fields: { saveReplay: false, latencyMode: LiveVideoLatencyMode.DEFAULT } })
await waitJobs(servers)
})
@ -192,6 +198,7 @@ describe('Test live', function () {
}
expect(live.saveReplay).to.be.false
expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT)
}
})

View File

@ -82,6 +82,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
expect(data.live.enabled).to.be.false
expect(data.live.allowReplay).to.be.false
expect(data.live.latencySetting.enabled).to.be.true
expect(data.live.maxDuration).to.equal(-1)
expect(data.live.maxInstanceLives).to.equal(20)
expect(data.live.maxUserLives).to.equal(3)
@ -185,6 +186,7 @@ function checkUpdatedConfig (data: CustomConfig) {
expect(data.live.enabled).to.be.true
expect(data.live.allowReplay).to.be.true
expect(data.live.latencySetting.enabled).to.be.false
expect(data.live.maxDuration).to.equal(5000)
expect(data.live.maxInstanceLives).to.equal(-1)
expect(data.live.maxUserLives).to.equal(10)
@ -326,6 +328,9 @@ const newCustomConfig: CustomConfig = {
live: {
enabled: true,
allowReplay: true,
latencySetting: {
enabled: false
},
maxDuration: 5000,
maxInstanceLives: -1,
maxUserLives: 10,

View File

@ -5,7 +5,7 @@ import {
ActivityTagObject,
ActivityUrlObject
} from './common-objects'
import { VideoState } from '../../videos'
import { LiveVideoLatencyMode, VideoState } from '../../videos'
export interface VideoObject {
type: 'Video'
@ -25,6 +25,7 @@ export interface VideoObject {
isLiveBroadcast: boolean
liveSaveReplay: boolean
permanentLive: boolean
latencyMode: LiveVideoLatencyMode
commentsEnabled: boolean
downloadEnabled: boolean

View File

@ -131,6 +131,10 @@ export interface CustomConfig {
allowReplay: boolean
latencySetting: {
enabled: boolean
}
maxDuration: number
maxInstanceLives: number
maxUserLives: number

View File

@ -149,10 +149,14 @@ export interface ServerConfig {
live: {
enabled: boolean
allowReplay: boolean
latencySetting: {
enabled: boolean
}
maxDuration: number
maxInstanceLives: number
maxUserLives: number
allowReplay: boolean
transcoding: {
enabled: boolean

View File

@ -1,5 +1,6 @@
export * from './live-video-create.model'
export * from './live-video-event-payload.model'
export * from './live-video-event.type'
export * from './live-video-latency-mode.enum'
export * from './live-video-update.model'
export * from './live-video.model'

View File

@ -1,6 +1,8 @@
import { LiveVideoLatencyMode } from '.'
import { VideoCreate } from '../video-create.model'
export interface LiveVideoCreate extends VideoCreate {
saveReplay?: boolean
permanentLive?: boolean
latencyMode?: LiveVideoLatencyMode
}

View File

@ -0,0 +1,5 @@
export const enum LiveVideoLatencyMode {
DEFAULT = 1,
HIGH_LATENCY = 2,
SMALL_LATENCY = 3
}

View File

@ -1,4 +1,7 @@
import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
export interface LiveVideoUpdate {
permanentLive?: boolean
saveReplay?: boolean
latencyMode?: LiveVideoLatencyMode
}

View File

@ -1,8 +1,12 @@
import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
export interface LiveVideo {
rtmpUrl: string
rtmpsUrl: string
streamKey: string
saveReplay: boolean
permanentLive: boolean
latencyMode: LiveVideoLatencyMode
}

View File

@ -292,6 +292,9 @@ export class ConfigCommand extends AbstractCommand {
live: {
enabled: true,
allowReplay: false,
latencySetting: {
enabled: false
},
maxDuration: -1,
maxInstanceLives: -1,
maxUserLives: 50,

View File

@ -2295,6 +2295,9 @@ paths:
permanentLive:
description: User can stream multiple times in a permanent live
type: boolean
latencyMode:
description: User can select live latency mode if enabled by the instance
$ref: '#/components/schemas/LiveVideoLatencyMode'
thumbnailfile:
description: Live video/replay thumbnail file
type: string
@ -5291,6 +5294,14 @@ components:
description: 'Admin flags for the user (None = `0`, Bypass video blocklist = `1`)'
example: 1
LiveVideoLatencyMode:
type: integer
enum:
- 1
- 2
- 3
description: 'The live latency mode (Default = `1`, HIght latency = `2`, Small Latency = `3`)'
VideoStateConstant:
properties:
id:
@ -7482,6 +7493,9 @@ components:
permanentLive:
description: User can stream multiple times in a permanent live
type: boolean
latencyMode:
description: User can select live latency mode if enabled by the instance
$ref: '#/components/schemas/LiveVideoLatencyMode'
LiveVideoResponse:
properties:
@ -7497,8 +7511,9 @@ components:
permanentLive:
description: User can stream multiple times in a permanent live
type: boolean
latencyMode:
description: User can select live latency mode if enabled by the instance
$ref: '#/components/schemas/LiveVideoLatencyMode'
callbacks:
searchIndex: