Add latency setting support

pull/4857/head
Chocobozzz 2022-03-04 13:40:02 +01:00 committed by Chocobozzz
parent 01dd04cd5a
commit f443a74649
42 changed files with 516 additions and 81 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

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: