Add latency setting support
This commit is contained in:
parent
01dd04cd5a
commit
f443a74649
|
@ -189,6 +189,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
maxInstanceLives: MAX_INSTANCE_LIVES_VALIDATOR,
|
maxInstanceLives: MAX_INSTANCE_LIVES_VALIDATOR,
|
||||||
maxUserLives: MAX_USER_LIVES_VALIDATOR,
|
maxUserLives: MAX_USER_LIVES_VALIDATOR,
|
||||||
allowReplay: null,
|
allowReplay: null,
|
||||||
|
latencySetting: {
|
||||||
|
enabled: null
|
||||||
|
},
|
||||||
|
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabled: null,
|
enabled: null,
|
||||||
|
|
|
@ -36,6 +36,18 @@
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
</div>
|
</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()">
|
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
||||||
<label i18n for="liveMaxInstanceLives">
|
<label i18n for="liveMaxInstanceLives">
|
||||||
Max simultaneous lives created on your instance <span class="text-muted">(-1 for "unlimited")</span>
|
Max simultaneous lives created on your instance <span class="text-muted">(-1 for "unlimited")</span>
|
||||||
|
|
|
@ -289,6 +289,17 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { forkJoin } from 'rxjs'
|
import { forkJoin } from 'rxjs'
|
||||||
import { map } from 'rxjs/operators'
|
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 { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
|
||||||
import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms'
|
import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms'
|
||||||
import { HooksService, PluginService, ServerService } from '@app/core'
|
import { HooksService, PluginService, ServerService } from '@app/core'
|
||||||
|
@ -26,6 +26,7 @@ import { PluginInfo } from '@root-helpers/plugins-manager'
|
||||||
import {
|
import {
|
||||||
HTMLServerConfig,
|
HTMLServerConfig,
|
||||||
LiveVideo,
|
LiveVideo,
|
||||||
|
LiveVideoLatencyMode,
|
||||||
RegisterClientFormFieldOptions,
|
RegisterClientFormFieldOptions,
|
||||||
RegisterClientVideoFieldOptions,
|
RegisterClientVideoFieldOptions,
|
||||||
VideoConstant,
|
VideoConstant,
|
||||||
|
@ -78,6 +79,23 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
||||||
videoCategories: VideoConstant<number>[] = []
|
videoCategories: VideoConstant<number>[] = []
|
||||||
videoLicences: VideoConstant<number>[] = []
|
videoLicences: VideoConstant<number>[] = []
|
||||||
videoLanguages: VideoLanguages[] = []
|
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
|
pluginDataFormGroup: FormGroup
|
||||||
|
|
||||||
|
@ -141,6 +159,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
||||||
originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR,
|
originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR,
|
||||||
liveStreamKey: null,
|
liveStreamKey: null,
|
||||||
permanentLive: null,
|
permanentLive: null,
|
||||||
|
latencyMode: null,
|
||||||
saveReplay: null
|
saveReplay: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -273,6 +292,10 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
||||||
return this.form.value['permanentLive'] === true
|
return this.form.value['permanentLive'] === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLatencyModeEnabled () {
|
||||||
|
return this.serverConfig.live.latencySetting.enabled
|
||||||
|
}
|
||||||
|
|
||||||
isPluginFieldHidden (pluginField: PluginField) {
|
isPluginFieldHidden (pluginField: PluginField) {
|
||||||
if (typeof pluginField.commonOptions.hidden !== 'function') return false
|
if (typeof pluginField.commonOptions.hidden !== 'function') return false
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
||||||
if (this.liveVideo) {
|
if (this.liveVideo) {
|
||||||
this.form.patchValue({
|
this.form.patchValue({
|
||||||
saveReplay: this.liveVideo.saveReplay,
|
saveReplay: this.liveVideo.saveReplay,
|
||||||
|
latencyMode: this.liveVideo.latencyMode,
|
||||||
permanentLive: this.liveVideo.permanentLive
|
permanentLive: this.liveVideo.permanentLive
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -127,7 +128,8 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
||||||
|
|
||||||
const liveVideoUpdate: LiveVideoUpdate = {
|
const liveVideoUpdate: LiveVideoUpdate = {
|
||||||
saveReplay: !!this.form.value.saveReplay,
|
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
|
// Don't update live attributes if they did not change
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Hotkey, HotkeysService } from 'angular2-hotkeys'
|
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 { isP2PEnabled } from 'src/assets/player/utils'
|
||||||
import { PlatformLocation } from '@angular/common'
|
import { PlatformLocation } from '@angular/common'
|
||||||
import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
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 { isXPercentInViewport, scrollToTop } from '@app/helpers'
|
||||||
import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main'
|
import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main'
|
||||||
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
|
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 { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
|
||||||
import { timeToInt } from '@shared/core-utils'
|
import { timeToInt } from '@shared/core-utils'
|
||||||
import {
|
import {
|
||||||
HTMLServerConfig,
|
HTMLServerConfig,
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
|
LiveVideo,
|
||||||
PeerTubeProblemDocument,
|
PeerTubeProblemDocument,
|
||||||
ServerErrorCode,
|
ServerErrorCode,
|
||||||
VideoCaption,
|
VideoCaption,
|
||||||
|
@ -63,6 +65,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
video: VideoDetails = null
|
video: VideoDetails = null
|
||||||
videoCaptions: VideoCaption[] = []
|
videoCaptions: VideoCaption[] = []
|
||||||
|
liveVideo: LiveVideo
|
||||||
|
|
||||||
playlistPosition: number
|
playlistPosition: number
|
||||||
playlist: VideoPlaylist = null
|
playlist: VideoPlaylist = null
|
||||||
|
@ -89,6 +92,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private videoService: VideoService,
|
private videoService: VideoService,
|
||||||
private playlistService: VideoPlaylistService,
|
private playlistService: VideoPlaylistService,
|
||||||
|
private liveVideoService: LiveVideoService,
|
||||||
private confirmService: ConfirmService,
|
private confirmService: ConfirmService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
@ -239,12 +243,21 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
'filter:api.video-watch.video.get.result'
|
'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([
|
forkJoin([
|
||||||
videoObs,
|
videoAndLiveObs,
|
||||||
this.videoCaptionService.listCaptions(videoId),
|
this.videoCaptionService.listCaptions(videoId),
|
||||||
this.userService.getAnonymousOrLoggedUser()
|
this.userService.getAnonymousOrLoggedUser()
|
||||||
]).subscribe({
|
]).subscribe({
|
||||||
next: ([ video, captionsResult, loggedInOrAnonymousUser ]) => {
|
next: ([ { video, live }, captionsResult, loggedInOrAnonymousUser ]) => {
|
||||||
const queryParams = this.route.snapshot.queryParams
|
const queryParams = this.route.snapshot.queryParams
|
||||||
|
|
||||||
const urlOptions = {
|
const urlOptions = {
|
||||||
|
@ -261,7 +274,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
peertubeLink: false
|
peertubeLink: false
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onVideoFetched({ video, videoCaptions: captionsResult.data, loggedInOrAnonymousUser, urlOptions })
|
this.onVideoFetched({ video, live, videoCaptions: captionsResult.data, loggedInOrAnonymousUser, urlOptions })
|
||||||
.catch(err => this.handleGlobalError(err))
|
.catch(err => this.handleGlobalError(err))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -330,16 +343,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
private async onVideoFetched (options: {
|
private async onVideoFetched (options: {
|
||||||
video: VideoDetails
|
video: VideoDetails
|
||||||
|
live: LiveVideo
|
||||||
videoCaptions: VideoCaption[]
|
videoCaptions: VideoCaption[]
|
||||||
urlOptions: URLOptions
|
urlOptions: URLOptions
|
||||||
loggedInOrAnonymousUser: User
|
loggedInOrAnonymousUser: User
|
||||||
}) {
|
}) {
|
||||||
const { video, videoCaptions, urlOptions, loggedInOrAnonymousUser } = options
|
const { video, live, videoCaptions, urlOptions, loggedInOrAnonymousUser } = options
|
||||||
|
|
||||||
this.subscribeToLiveEventsIfNeeded(this.video, video)
|
this.subscribeToLiveEventsIfNeeded(this.video, video)
|
||||||
|
|
||||||
this.video = video
|
this.video = video
|
||||||
this.videoCaptions = videoCaptions
|
this.videoCaptions = videoCaptions
|
||||||
|
this.liveVideo = live
|
||||||
|
|
||||||
// Re init attributes
|
// Re init attributes
|
||||||
this.playerPlaceholderImgSrc = undefined
|
this.playerPlaceholderImgSrc = undefined
|
||||||
|
@ -387,6 +402,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
const params = {
|
const params = {
|
||||||
video: this.video,
|
video: this.video,
|
||||||
videoCaptions: this.videoCaptions,
|
videoCaptions: this.videoCaptions,
|
||||||
|
liveVideo: this.liveVideo,
|
||||||
urlOptions,
|
urlOptions,
|
||||||
loggedInOrAnonymousUser,
|
loggedInOrAnonymousUser,
|
||||||
user: this.user
|
user: this.user
|
||||||
|
@ -532,12 +548,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
private buildPlayerManagerOptions (params: {
|
private buildPlayerManagerOptions (params: {
|
||||||
video: VideoDetails
|
video: VideoDetails
|
||||||
|
liveVideo: LiveVideo
|
||||||
videoCaptions: VideoCaption[]
|
videoCaptions: VideoCaption[]
|
||||||
urlOptions: CustomizationOptions & { playerMode: PlayerMode }
|
urlOptions: CustomizationOptions & { playerMode: PlayerMode }
|
||||||
loggedInOrAnonymousUser: User
|
loggedInOrAnonymousUser: User
|
||||||
user?: AuthUser
|
user?: AuthUser
|
||||||
}) {
|
}) {
|
||||||
const { video, videoCaptions, urlOptions, loggedInOrAnonymousUser, user } = params
|
const { video, liveVideo, videoCaptions, urlOptions, loggedInOrAnonymousUser, user } = params
|
||||||
|
|
||||||
const getStartTime = () => {
|
const getStartTime = () => {
|
||||||
const byUrl = urlOptions.startTime !== undefined
|
const byUrl = urlOptions.startTime !== undefined
|
||||||
|
@ -562,6 +579,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
src: environment.apiUrl + c.captionPath
|
src: environment.apiUrl + c.captionPath
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const liveOptions = video.isLive
|
||||||
|
? { latencyMode: liveVideo.latencyMode }
|
||||||
|
: undefined
|
||||||
|
|
||||||
const options: PeertubePlayerManagerOptions = {
|
const options: PeertubePlayerManagerOptions = {
|
||||||
common: {
|
common: {
|
||||||
autoplay: this.isAutoplay(),
|
autoplay: this.isAutoplay(),
|
||||||
|
@ -597,6 +618,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
embedTitle: video.name,
|
embedTitle: video.name,
|
||||||
|
|
||||||
isLive: video.isLive,
|
isLive: video.isLive,
|
||||||
|
liveOptions,
|
||||||
|
|
||||||
language: this.localeId,
|
language: this.localeId,
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import videojs from 'video.js'
|
import videojs from 'video.js'
|
||||||
|
import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core'
|
||||||
import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
|
import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
|
||||||
import { PluginsManager } from '@root-helpers/plugins-manager'
|
import { PluginsManager } from '@root-helpers/plugins-manager'
|
||||||
import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
|
import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
|
||||||
import { isDefaultLocale } from '@shared/core-utils/i18n'
|
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 { copyToClipboard } from '../../root-helpers/utils'
|
||||||
import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
|
import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
|
||||||
import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
|
import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
|
||||||
|
@ -19,7 +20,6 @@ import {
|
||||||
VideoJSPluginOptions
|
VideoJSPluginOptions
|
||||||
} from './peertube-videojs-typings'
|
} from './peertube-videojs-typings'
|
||||||
import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils'
|
import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils'
|
||||||
import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core'
|
|
||||||
|
|
||||||
export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
|
export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
|
||||||
|
|
||||||
|
@ -76,6 +76,9 @@ export interface CommonOptions extends CustomizationOptions {
|
||||||
embedTitle: string
|
embedTitle: string
|
||||||
|
|
||||||
isLive: boolean
|
isLive: boolean
|
||||||
|
liveOptions?: {
|
||||||
|
latencyMode: LiveVideoLatencyMode
|
||||||
|
}
|
||||||
|
|
||||||
language?: string
|
language?: string
|
||||||
|
|
||||||
|
@ -250,21 +253,8 @@ export class PeertubePlayerOptionsBuilder {
|
||||||
.filter(t => t.startsWith('ws'))
|
.filter(t => t.startsWith('ws'))
|
||||||
|
|
||||||
const specificLiveOrVODOptions = this.options.common.isLive
|
const specificLiveOrVODOptions = this.options.common.isLive
|
||||||
? { // Live
|
? this.getP2PMediaLoaderLiveOptions()
|
||||||
requiredSegmentsPriority: 1
|
: this.getP2PMediaLoaderVODOptions()
|
||||||
}
|
|
||||||
: { // VOD
|
|
||||||
requiredSegmentsPriority: 3,
|
|
||||||
|
|
||||||
cachedSegmentExpiration: 86400000,
|
|
||||||
cachedSegmentsCount: 100,
|
|
||||||
|
|
||||||
httpDownloadMaxPriority: 9,
|
|
||||||
httpDownloadProbability: 0.06,
|
|
||||||
httpDownloadProbabilitySkipIfNoPeers: true,
|
|
||||||
|
|
||||||
p2pDownloadMaxPriority: 50
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
trackerAnnounce,
|
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) {
|
private getHLSOptions (p2pMediaLoaderConfig: HlsJsEngineSettings) {
|
||||||
|
const specificLiveOrVODOptions = this.options.common.isLive
|
||||||
|
? this.getHLSLiveOptions()
|
||||||
|
: this.getHLSVODOptions()
|
||||||
|
|
||||||
const base = {
|
const base = {
|
||||||
capLevelToPlayerSize: true,
|
capLevelToPlayerSize: true,
|
||||||
autoStartLoad: false,
|
autoStartLoad: false,
|
||||||
liveSyncDurationCount: 5,
|
|
||||||
|
|
||||||
loader: new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass()
|
loader: new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass(),
|
||||||
|
|
||||||
|
...specificLiveOrVODOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
const averageBandwidth = getAverageBandwidthInStore()
|
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) {
|
private addWebTorrentOptions (plugins: VideoJSPluginOptions, alreadyPlayed: boolean) {
|
||||||
const commonOptions = this.options.common
|
const commonOptions = this.options.common
|
||||||
const webtorrentOptions = this.options.webtorrent
|
const webtorrentOptions = this.options.webtorrent
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
|
||||||
import {
|
import {
|
||||||
HTMLServerConfig,
|
HTMLServerConfig,
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
|
LiveVideo,
|
||||||
OAuth2ErrorCode,
|
OAuth2ErrorCode,
|
||||||
ResultList,
|
ResultList,
|
||||||
UserRefreshToken,
|
UserRefreshToken,
|
||||||
|
@ -94,6 +95,10 @@ export class PeerTubeEmbed {
|
||||||
return window.location.origin + '/api/v1/videos/' + id
|
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) {
|
refreshFetch (url: string, options?: RequestInit) {
|
||||||
return fetch(url, options)
|
return fetch(url, options)
|
||||||
.then((res: Response) => {
|
.then((res: Response) => {
|
||||||
|
@ -166,6 +171,12 @@ export class PeerTubeEmbed {
|
||||||
return this.refreshFetch(this.getVideoUrl(videoId) + '/captions', { headers: this.headers })
|
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> {
|
loadPlaylistInfo (playlistId: string): Promise<Response> {
|
||||||
return this.refreshFetch(this.getPlaylistUrl(playlistId), { headers: this.headers })
|
return this.refreshFetch(this.getPlaylistUrl(playlistId), { headers: this.headers })
|
||||||
}
|
}
|
||||||
|
@ -475,13 +486,15 @@ export class PeerTubeEmbed {
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoInfoPromise = videoResponse.json()
|
const videoInfoPromise: Promise<{ video: VideoDetails, live?: LiveVideo }> = videoResponse.json()
|
||||||
.then((videoInfo: VideoDetails) => {
|
.then((videoInfo: VideoDetails) => {
|
||||||
this.loadParams(videoInfo)
|
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([
|
const [ videoInfoTmp, serverTranslations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
|
||||||
|
@ -493,11 +506,15 @@ export class PeerTubeEmbed {
|
||||||
|
|
||||||
await this.loadPlugins(serverTranslations)
|
await this.loadPlugins(serverTranslations)
|
||||||
|
|
||||||
const videoInfo: VideoDetails = videoInfoTmp
|
const { video: videoInfo, live } = videoInfoTmp
|
||||||
|
|
||||||
const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
|
const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
|
||||||
const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse)
|
const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse)
|
||||||
|
|
||||||
|
const liveOptions = videoInfo.isLive
|
||||||
|
? { latencyMode: live.latencyMode }
|
||||||
|
: undefined
|
||||||
|
|
||||||
const playlistPlugin = this.currentPlaylistElement
|
const playlistPlugin = this.currentPlaylistElement
|
||||||
? {
|
? {
|
||||||
elements: this.playlistElements,
|
elements: this.playlistElements,
|
||||||
|
@ -545,6 +562,7 @@ export class PeerTubeEmbed {
|
||||||
videoUUID: videoInfo.uuid,
|
videoUUID: videoInfo.uuid,
|
||||||
|
|
||||||
isLive: videoInfo.isLive,
|
isLive: videoInfo.isLive,
|
||||||
|
liveOptions,
|
||||||
|
|
||||||
playerElement: this.playerElement,
|
playerElement: this.playerElement,
|
||||||
onPlayerElementChange: (element: HTMLVideoElement) => {
|
onPlayerElementChange: (element: HTMLVideoElement) => {
|
||||||
|
@ -726,7 +744,7 @@ export class PeerTubeEmbed {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadPlaceholder (video: VideoDetails) {
|
private buildPlaceholder (video: VideoDetails) {
|
||||||
const placeholder = this.getPlaceholderElement()
|
const placeholder = this.getPlaceholderElement()
|
||||||
|
|
||||||
const url = window.location.origin + video.previewPath
|
const url = window.location.origin + video.previewPath
|
||||||
|
|
|
@ -392,6 +392,12 @@ live:
|
||||||
# /!\ transcoding.enabled (and not live.transcoding.enabled) has to be true to create a replay
|
# /!\ transcoding.enabled (and not live.transcoding.enabled) has to be true to create a replay
|
||||||
allow_replay: true
|
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
|
# Your firewall should accept traffic from this port in TCP if you enable live
|
||||||
rtmp:
|
rtmp:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
|
@ -400,6 +400,12 @@ live:
|
||||||
# /!\ transcoding.enabled (and not live.transcoding.enabled) has to be true to create a replay
|
# /!\ transcoding.enabled (and not live.transcoding.enabled) has to be true to create a replay
|
||||||
allow_replay: true
|
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
|
# Your firewall should accept traffic from this port in TCP if you enable live
|
||||||
rtmp:
|
rtmp:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
|
@ -237,6 +237,9 @@ function customConfig (): CustomConfig {
|
||||||
live: {
|
live: {
|
||||||
enabled: CONFIG.LIVE.ENABLED,
|
enabled: CONFIG.LIVE.ENABLED,
|
||||||
allowReplay: CONFIG.LIVE.ALLOW_REPLAY,
|
allowReplay: CONFIG.LIVE.ALLOW_REPLAY,
|
||||||
|
latencySetting: {
|
||||||
|
enabled: CONFIG.LIVE.LATENCY_SETTING.ENABLED
|
||||||
|
},
|
||||||
maxDuration: CONFIG.LIVE.MAX_DURATION,
|
maxDuration: CONFIG.LIVE.MAX_DURATION,
|
||||||
maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES,
|
maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES,
|
||||||
maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,
|
maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
import { exists } from '@server/helpers/custom-validators/misc'
|
||||||
import { createReqFiles } from '@server/helpers/express-utils'
|
import { createReqFiles } from '@server/helpers/express-utils'
|
||||||
import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants'
|
import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants'
|
||||||
import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
|
import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
|
||||||
|
@ -9,7 +10,7 @@ import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator
|
||||||
import { VideoLiveModel } from '@server/models/video/video-live'
|
import { VideoLiveModel } from '@server/models/video/video-live'
|
||||||
import { MVideoDetails, MVideoFullLight } from '@server/types/models'
|
import { MVideoDetails, MVideoFullLight } from '@server/types/models'
|
||||||
import { buildUUID, uuidToShort } from '@shared/extra-utils'
|
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 { logger } from '../../../helpers/logger'
|
||||||
import { sequelizeTypescript } from '../../../initializers/database'
|
import { sequelizeTypescript } from '../../../initializers/database'
|
||||||
import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail'
|
import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail'
|
||||||
|
@ -60,8 +61,9 @@ async function updateLiveVideo (req: express.Request, res: express.Response) {
|
||||||
const video = res.locals.videoAll
|
const video = res.locals.videoAll
|
||||||
const videoLive = res.locals.videoLive
|
const videoLive = res.locals.videoLive
|
||||||
|
|
||||||
videoLive.saveReplay = body.saveReplay || false
|
if (exists(body.saveReplay)) videoLive.saveReplay = body.saveReplay
|
||||||
videoLive.permanentLive = body.permanentLive || false
|
if (exists(body.permanentLive)) videoLive.permanentLive = body.permanentLive
|
||||||
|
if (exists(body.latencyMode)) videoLive.latencyMode = body.latencyMode
|
||||||
|
|
||||||
video.VideoLive = await videoLive.save()
|
video.VideoLive = await videoLive.save()
|
||||||
|
|
||||||
|
@ -87,6 +89,7 @@ async function addLiveVideo (req: express.Request, res: express.Response) {
|
||||||
const videoLive = new VideoLiveModel()
|
const videoLive = new VideoLiveModel()
|
||||||
videoLive.saveReplay = videoInfo.saveReplay || false
|
videoLive.saveReplay = videoInfo.saveReplay || false
|
||||||
videoLive.permanentLive = videoInfo.permanentLive || false
|
videoLive.permanentLive = videoInfo.permanentLive || false
|
||||||
|
videoLive.latencyMode = videoInfo.latencyMode || LiveVideoLatencyMode.DEFAULT
|
||||||
videoLive.streamKey = buildUUID()
|
videoLive.streamKey = buildUUID()
|
||||||
|
|
||||||
const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
|
const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
|
||||||
|
|
|
@ -50,6 +50,10 @@ function getContextData (type: ContextType) {
|
||||||
'@type': 'sc:Boolean',
|
'@type': 'sc:Boolean',
|
||||||
'@id': 'pt:permanentLive'
|
'@id': 'pt:permanentLive'
|
||||||
},
|
},
|
||||||
|
latencyMode: {
|
||||||
|
'@type': 'sc:Number',
|
||||||
|
'@id': 'pt:latencyMode'
|
||||||
|
},
|
||||||
|
|
||||||
Infohash: 'pt:Infohash',
|
Infohash: 'pt:Infohash',
|
||||||
Playlist: 'pt:Playlist',
|
Playlist: 'pt:Playlist',
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import validator from 'validator'
|
import validator from 'validator'
|
||||||
import { logger } from '@server/helpers/logger'
|
import { logger } from '@server/helpers/logger'
|
||||||
import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@shared/models'
|
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 { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants'
|
||||||
import { peertubeTruncate } from '../../core-utils'
|
import { peertubeTruncate } from '../../core-utils'
|
||||||
import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
|
import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
|
||||||
|
import { isLiveLatencyModeValid } from '../video-lives'
|
||||||
import {
|
import {
|
||||||
isVideoDurationValid,
|
isVideoDurationValid,
|
||||||
isVideoNameValid,
|
isVideoNameValid,
|
||||||
|
@ -65,6 +66,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
|
||||||
if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false
|
if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false
|
||||||
if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false
|
if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false
|
||||||
if (!isBooleanValid(video.permanentLive)) video.permanentLive = false
|
if (!isBooleanValid(video.permanentLive)) video.permanentLive = false
|
||||||
|
if (!isLiveLatencyModeValid(video.latencyMode)) video.latencyMode = LiveVideoLatencyMode.DEFAULT
|
||||||
|
|
||||||
return isActivityPubUrlValid(video.id) &&
|
return isActivityPubUrlValid(video.id) &&
|
||||||
isVideoNameValid(video.name) &&
|
isVideoNameValid(video.name) &&
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { FfmpegCommand, FilterSpecification } from 'fluent-ffmpeg'
|
import { FfmpegCommand, FilterSpecification } from 'fluent-ffmpeg'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { VIDEO_LIVE } from '@server/initializers/constants'
|
import { VIDEO_LIVE } from '@server/initializers/constants'
|
||||||
import { AvailableEncoders } from '@shared/models'
|
import { AvailableEncoders, LiveVideoLatencyMode } from '@shared/models'
|
||||||
import { logger, loggerTagsFactory } from '../logger'
|
import { logger, loggerTagsFactory } from '../logger'
|
||||||
import { buildStreamSuffix, getFFmpeg, getScaleFilter, StreamType } from './ffmpeg-commons'
|
import { buildStreamSuffix, getFFmpeg, getScaleFilter, StreamType } from './ffmpeg-commons'
|
||||||
import { getEncoderBuilderResult } from './ffmpeg-encoders'
|
import { getEncoderBuilderResult } from './ffmpeg-encoders'
|
||||||
|
@ -15,6 +15,7 @@ async function getLiveTranscodingCommand (options: {
|
||||||
|
|
||||||
outPath: string
|
outPath: string
|
||||||
masterPlaylistName: string
|
masterPlaylistName: string
|
||||||
|
latencyMode: LiveVideoLatencyMode
|
||||||
|
|
||||||
resolutions: number[]
|
resolutions: number[]
|
||||||
|
|
||||||
|
@ -26,7 +27,7 @@ async function getLiveTranscodingCommand (options: {
|
||||||
availableEncoders: AvailableEncoders
|
availableEncoders: AvailableEncoders
|
||||||
profile: string
|
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')
|
const command = getFFmpeg(inputUrl, 'live')
|
||||||
|
|
||||||
|
@ -120,14 +121,21 @@ async function getLiveTranscodingCommand (options: {
|
||||||
|
|
||||||
command.complexFilter(complexFilter)
|
command.complexFilter(complexFilter)
|
||||||
|
|
||||||
addDefaultLiveHLSParams(command, outPath, masterPlaylistName)
|
addDefaultLiveHLSParams({ command, outPath, masterPlaylistName, latencyMode })
|
||||||
|
|
||||||
command.outputOption('-var_stream_map', varStreamMap.join(' '))
|
command.outputOption('-var_stream_map', varStreamMap.join(' '))
|
||||||
|
|
||||||
return command
|
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')
|
const command = getFFmpeg(inputUrl, 'live')
|
||||||
|
|
||||||
command.outputOption('-c:v copy')
|
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:a?')
|
||||||
command.outputOption('-map 0:v?')
|
command.outputOption('-map 0:v?')
|
||||||
|
|
||||||
addDefaultLiveHLSParams(command, outPath, masterPlaylistName)
|
addDefaultLiveHLSParams({ command, outPath, masterPlaylistName, latencyMode })
|
||||||
|
|
||||||
return command
|
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 {
|
export {
|
||||||
|
getLiveSegmentTime,
|
||||||
|
|
||||||
getLiveTranscodingCommand,
|
getLiveTranscodingCommand,
|
||||||
getLiveMuxingCommand
|
getLiveMuxingCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function addDefaultLiveHLSParams (command: FfmpegCommand, outPath: string, masterPlaylistName: string) {
|
function addDefaultLiveHLSParams (options: {
|
||||||
command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS)
|
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_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE)
|
||||||
command.outputOption('-hls_flags delete_segments+independent_segments')
|
command.outputOption('-hls_flags delete_segments+independent_segments')
|
||||||
command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`)
|
command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`)
|
||||||
|
|
|
@ -49,8 +49,8 @@ function checkMissedConfig () {
|
||||||
'peertube.check_latest_version.enabled', 'peertube.check_latest_version.url',
|
'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.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',
|
'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.enabled', 'live.allow_replay', 'live.latency_setting.enabled', 'live.max_duration',
|
||||||
'live.rtmp.enabled', 'live.rtmp.port', 'live.rtmp.hostname',
|
'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.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.enabled', 'live.transcoding.threads', 'live.transcoding.profile',
|
||||||
'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p',
|
'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p',
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { dirname, join } from 'path'
|
||||||
import { decacheModule } from '@server/helpers/decache'
|
import { decacheModule } from '@server/helpers/decache'
|
||||||
import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
|
import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
|
||||||
import { BroadcastMessageLevel } from '@shared/models/server'
|
import { BroadcastMessageLevel } from '@shared/models/server'
|
||||||
|
import { buildPath, root } from '../../shared/core-utils'
|
||||||
import { VideoPrivacy, VideosRedundancyStrategy } from '../../shared/models'
|
import { VideoPrivacy, VideosRedundancyStrategy } from '../../shared/models'
|
||||||
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
|
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
|
||||||
import { buildPath, root } from '../../shared/core-utils'
|
|
||||||
import { parseBytes, parseDurationToMs } from '../helpers/core-utils'
|
import { parseBytes, parseDurationToMs } from '../helpers/core-utils'
|
||||||
|
|
||||||
// Use a variable to reload the configuration if we need
|
// 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') },
|
get ALLOW_REPLAY () { return config.get<boolean>('live.allow_replay') },
|
||||||
|
|
||||||
|
LATENCY_SETTING: {
|
||||||
|
get ENABLED () { return config.get<boolean>('live.latency_setting.enabled') }
|
||||||
|
},
|
||||||
|
|
||||||
RTMP: {
|
RTMP: {
|
||||||
get ENABLED () { return config.get<boolean>('live.rtmp.enabled') },
|
get ENABLED () { return config.get<boolean>('live.rtmp.enabled') },
|
||||||
get PORT () { return config.get<number>('live.rtmp.port') },
|
get PORT () { return config.get<number>('live.rtmp.port') },
|
||||||
|
|
|
@ -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 = {
|
const VIDEO_LIVE = {
|
||||||
EXTENSION: '.ts',
|
EXTENSION: '.ts',
|
||||||
CLEANUP_DELAY: 1000 * 60 * 5, // 5 minutes
|
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
|
SEGMENTS_LIST_SIZE: 15, // 15 maximum segments in live playlist
|
||||||
REPLAY_DIRECTORY: 'replay',
|
REPLAY_DIRECTORY: 'replay',
|
||||||
EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION: 4,
|
EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION: 4,
|
||||||
|
@ -842,7 +845,8 @@ if (isTestInstance() === true) {
|
||||||
PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000
|
PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000
|
||||||
|
|
||||||
VIDEO_LIVE.CLEANUP_DELAY = 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
|
VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -151,6 +151,7 @@ function getLiveAttributesFromObject (video: MVideoId, videoObject: VideoObject)
|
||||||
return {
|
return {
|
||||||
saveReplay: videoObject.liveSaveReplay,
|
saveReplay: videoObject.liveSaveReplay,
|
||||||
permanentLive: videoObject.permanentLive,
|
permanentLive: videoObject.permanentLive,
|
||||||
|
latencyMode: videoObject.latencyMode,
|
||||||
videoId: video.id
|
videoId: video.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,10 @@ import { createServer as createServerTLS, Server as ServerTLS } from 'tls'
|
||||||
import {
|
import {
|
||||||
computeLowerResolutionsToTranscode,
|
computeLowerResolutionsToTranscode,
|
||||||
ffprobePromise,
|
ffprobePromise,
|
||||||
|
getLiveSegmentTime,
|
||||||
getVideoStreamBitrate,
|
getVideoStreamBitrate,
|
||||||
getVideoStreamFPS,
|
getVideoStreamDimensionsInfo,
|
||||||
getVideoStreamDimensionsInfo
|
getVideoStreamFPS
|
||||||
} from '@server/helpers/ffmpeg'
|
} from '@server/helpers/ffmpeg'
|
||||||
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
||||||
import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config'
|
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 }))
|
.catch(err => logger.error('Cannot federate live video %s.', video.url, { err, ...localLTags }))
|
||||||
|
|
||||||
PeerTubeSocket.Instance.sendVideoLiveNewState(video)
|
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) {
|
} catch (err) {
|
||||||
logger.error('Cannot save/federate live video %d.', videoId, { err, ...localLTags })
|
logger.error('Cannot save/federate live video %d.', videoId, { err, ...localLTags })
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,6 +125,8 @@ class MuxingSession extends EventEmitter {
|
||||||
outPath,
|
outPath,
|
||||||
masterPlaylistName: this.streamingPlaylist.playlistFilename,
|
masterPlaylistName: this.streamingPlaylist.playlistFilename,
|
||||||
|
|
||||||
|
latencyMode: this.videoLive.latencyMode,
|
||||||
|
|
||||||
resolutions: this.allResolutions,
|
resolutions: this.allResolutions,
|
||||||
fps: this.fps,
|
fps: this.fps,
|
||||||
bitrate: this.bitrate,
|
bitrate: this.bitrate,
|
||||||
|
@ -133,7 +135,12 @@ class MuxingSession extends EventEmitter {
|
||||||
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
||||||
profile: CONFIG.LIVE.TRANSCODING.PROFILE
|
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())
|
logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags())
|
||||||
|
|
||||||
|
|
|
@ -137,6 +137,10 @@ class ServerConfigManager {
|
||||||
enabled: CONFIG.LIVE.ENABLED,
|
enabled: CONFIG.LIVE.ENABLED,
|
||||||
|
|
||||||
allowReplay: CONFIG.LIVE.ALLOW_REPLAY,
|
allowReplay: CONFIG.LIVE.ALLOW_REPLAY,
|
||||||
|
latencySetting: {
|
||||||
|
enabled: CONFIG.LIVE.LATENCY_SETTING.ENABLED
|
||||||
|
},
|
||||||
|
|
||||||
maxDuration: CONFIG.LIVE.MAX_DURATION,
|
maxDuration: CONFIG.LIVE.MAX_DURATION,
|
||||||
maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES,
|
maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES,
|
||||||
maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,
|
maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,
|
||||||
|
|
|
@ -1,12 +1,21 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { body } from 'express-validator'
|
import { body } from 'express-validator'
|
||||||
|
import { isLiveLatencyModeValid } from '@server/helpers/custom-validators/video-lives'
|
||||||
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants'
|
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants'
|
||||||
import { isLocalLiveVideoAccepted } from '@server/lib/moderation'
|
import { isLocalLiveVideoAccepted } from '@server/lib/moderation'
|
||||||
import { Hooks } from '@server/lib/plugins/hooks'
|
import { Hooks } from '@server/lib/plugins/hooks'
|
||||||
import { VideoModel } from '@server/models/video/video'
|
import { VideoModel } from '@server/models/video/video'
|
||||||
import { VideoLiveModel } from '@server/models/video/video-live'
|
import { VideoLiveModel } from '@server/models/video/video-live'
|
||||||
import { HttpStatusCode, ServerErrorCode, UserRight, VideoState } from '@shared/models'
|
import {
|
||||||
import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
|
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 { isVideoNameValid } from '../../../helpers/custom-validators/videos'
|
||||||
import { cleanUpReqFiles } from '../../../helpers/express-utils'
|
import { cleanUpReqFiles } from '../../../helpers/express-utils'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
|
@ -67,6 +76,12 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
|
||||||
.customSanitizer(toBooleanOrNull)
|
.customSanitizer(toBooleanOrNull)
|
||||||
.custom(isBooleanValid).withMessage('Should have a valid permanentLive attribute'),
|
.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) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking videoLiveAddValidator parameters', { parameters: req.body })
|
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)
|
cleanUpReqFiles(req)
|
||||||
|
|
||||||
return res.fail({
|
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)
|
cleanUpReqFiles(req)
|
||||||
|
|
||||||
return res.fail({ message: 'Cannot set this live as permanent while saving its replay' })
|
return res.fail({ message: 'Cannot set this live as permanent while saving its replay' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = res.locals.oauth.token.User
|
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) {
|
if (CONFIG.LIVE.MAX_INSTANCE_LIVES !== -1) {
|
||||||
const totalInstanceLives = await VideoModel.countLocalLives()
|
const totalInstanceLives = await VideoModel.countLocalLives()
|
||||||
|
@ -141,19 +167,34 @@ const videoLiveUpdateValidator = [
|
||||||
.customSanitizer(toBooleanOrNull)
|
.customSanitizer(toBooleanOrNull)
|
||||||
.custom(isBooleanValid).withMessage('Should have a valid saveReplay attribute'),
|
.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) => {
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking videoLiveUpdateValidator parameters', { parameters: req.body })
|
logger.debug('Checking videoLiveUpdateValidator parameters', { parameters: req.body })
|
||||||
|
|
||||||
if (areValidationErrors(req, res)) return
|
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' })
|
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({
|
return res.fail({
|
||||||
status: HttpStatusCode.FORBIDDEN_403,
|
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
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -411,15 +411,6 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
||||||
views: video.views,
|
views: video.views,
|
||||||
sensitive: video.nsfw,
|
sensitive: video.nsfw,
|
||||||
waitTranscoding: video.waitTranscoding,
|
waitTranscoding: video.waitTranscoding,
|
||||||
isLiveBroadcast: video.isLive,
|
|
||||||
|
|
||||||
liveSaveReplay: video.isLive
|
|
||||||
? video.VideoLive.saveReplay
|
|
||||||
: null,
|
|
||||||
|
|
||||||
permanentLive: video.isLive
|
|
||||||
? video.VideoLive.permanentLive
|
|
||||||
: null,
|
|
||||||
|
|
||||||
state: video.state,
|
state: video.state,
|
||||||
commentsEnabled: video.commentsEnabled,
|
commentsEnabled: video.commentsEnabled,
|
||||||
|
@ -431,10 +422,13 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
||||||
: null,
|
: null,
|
||||||
|
|
||||||
updated: video.updatedAt.toISOString(),
|
updated: video.updatedAt.toISOString(),
|
||||||
|
|
||||||
mediaType: 'text/markdown',
|
mediaType: 'text/markdown',
|
||||||
content: video.description,
|
content: video.description,
|
||||||
support: video.support,
|
support: video.support,
|
||||||
|
|
||||||
subtitleLanguage,
|
subtitleLanguage,
|
||||||
|
|
||||||
icon: icons.map(i => ({
|
icon: icons.map(i => ({
|
||||||
type: 'Image',
|
type: 'Image',
|
||||||
url: i.getFileUrl(video),
|
url: i.getFileUrl(video),
|
||||||
|
@ -442,11 +436,14 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
||||||
width: i.width,
|
width: i.width,
|
||||||
height: i.height
|
height: i.height
|
||||||
})),
|
})),
|
||||||
|
|
||||||
url,
|
url,
|
||||||
|
|
||||||
likes: getLocalVideoLikesActivityPubUrl(video),
|
likes: getLocalVideoLikesActivityPubUrl(video),
|
||||||
dislikes: getLocalVideoDislikesActivityPubUrl(video),
|
dislikes: getLocalVideoDislikesActivityPubUrl(video),
|
||||||
shares: getLocalVideoSharesActivityPubUrl(video),
|
shares: getLocalVideoSharesActivityPubUrl(video),
|
||||||
comments: getLocalVideoCommentsActivityPubUrl(video),
|
comments: getLocalVideoCommentsActivityPubUrl(video),
|
||||||
|
|
||||||
attributedTo: [
|
attributedTo: [
|
||||||
{
|
{
|
||||||
type: 'Person',
|
type: 'Person',
|
||||||
|
@ -456,7 +453,9 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
||||||
type: 'Group',
|
type: 'Group',
|
||||||
id: video.VideoChannel.Actor.url
|
id: video.VideoChannel.Actor.url
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
|
||||||
|
...buildLiveAPAttributes(video)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -500,3 +499,23 @@ export {
|
||||||
getPrivacyLabel,
|
getPrivacyLabel,
|
||||||
getStateLabel
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -158,6 +158,7 @@ export class VideoTableAttributes {
|
||||||
'streamKey',
|
'streamKey',
|
||||||
'saveReplay',
|
'saveReplay',
|
||||||
'permanentLive',
|
'permanentLive',
|
||||||
|
'latencyMode',
|
||||||
'videoId',
|
'videoId',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt'
|
'updatedAt'
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
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 { WEBSERVER } from '@server/initializers/constants'
|
||||||
import { MVideoLive, MVideoLiveVideo } from '@server/types/models'
|
import { MVideoLive, MVideoLiveVideo } from '@server/types/models'
|
||||||
|
import { LiveVideo, LiveVideoLatencyMode, VideoState } from '@shared/models'
|
||||||
import { AttributesOnly } from '@shared/typescript-utils'
|
import { AttributesOnly } from '@shared/typescript-utils'
|
||||||
import { LiveVideo, VideoState } from '@shared/models'
|
|
||||||
import { VideoModel } from './video'
|
import { VideoModel } from './video'
|
||||||
import { VideoBlacklistModel } from './video-blacklist'
|
import { VideoBlacklistModel } from './video-blacklist'
|
||||||
import { CONFIG } from '@server/initializers/config'
|
|
||||||
|
|
||||||
@DefaultScope(() => ({
|
@DefaultScope(() => ({
|
||||||
include: [
|
include: [
|
||||||
|
@ -44,6 +44,10 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>
|
||||||
@Column
|
@Column
|
||||||
permanentLive: boolean
|
permanentLive: boolean
|
||||||
|
|
||||||
|
@AllowNull(false)
|
||||||
|
@Column
|
||||||
|
latencyMode: LiveVideoLatencyMode
|
||||||
|
|
||||||
@CreatedAt
|
@CreatedAt
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|
||||||
|
@ -113,7 +117,8 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>
|
||||||
|
|
||||||
streamKey: this.streamKey,
|
streamKey: this.streamKey,
|
||||||
permanentLive: this.permanentLive,
|
permanentLive: this.permanentLive,
|
||||||
saveReplay: this.saveReplay
|
saveReplay: this.saveReplay,
|
||||||
|
latencyMode: this.latencyMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,6 +125,9 @@ describe('Test config API validators', function () {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
||||||
allowReplay: false,
|
allowReplay: false,
|
||||||
|
latencySetting: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
maxDuration: 30,
|
maxDuration: 30,
|
||||||
maxInstanceLives: -1,
|
maxInstanceLives: -1,
|
||||||
maxUserLives: 50,
|
maxUserLives: 50,
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import { omit } from 'lodash'
|
import { omit } from 'lodash'
|
||||||
import { buildAbsoluteFixturePath } from '@shared/core-utils'
|
import { buildAbsoluteFixturePath } from '@shared/core-utils'
|
||||||
import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@shared/models'
|
import { HttpStatusCode, LiveVideoLatencyMode, VideoCreateResult, VideoPrivacy } from '@shared/models'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createSingleServer,
|
createSingleServer,
|
||||||
|
@ -38,6 +38,9 @@ describe('Test video lives API validator', function () {
|
||||||
newConfig: {
|
newConfig: {
|
||||||
live: {
|
live: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
latencySetting: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
maxInstanceLives: 20,
|
maxInstanceLives: 20,
|
||||||
maxUserLives: 20,
|
maxUserLives: 20,
|
||||||
allowReplay: true
|
allowReplay: true
|
||||||
|
@ -81,7 +84,8 @@ describe('Test video lives API validator', function () {
|
||||||
privacy: VideoPrivacy.PUBLIC,
|
privacy: VideoPrivacy.PUBLIC,
|
||||||
channelId,
|
channelId,
|
||||||
saveReplay: false,
|
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 })
|
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 () {
|
it('Should succeed with the correct parameters', async function () {
|
||||||
this.timeout(30000)
|
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 })
|
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 () {
|
it('Should succeed with the correct params', async function () {
|
||||||
await command.update({ videoId: video.id, fields: { saveReplay: false } })
|
await command.update({ videoId: video.id, fields: { saveReplay: false } })
|
||||||
await command.update({ videoId: video.uuid, fields: { saveReplay: false } })
|
await command.update({ videoId: video.uuid, fields: { saveReplay: false } })
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
LiveVideo,
|
LiveVideo,
|
||||||
LiveVideoCreate,
|
LiveVideoCreate,
|
||||||
|
LiveVideoLatencyMode,
|
||||||
VideoDetails,
|
VideoDetails,
|
||||||
VideoPrivacy,
|
VideoPrivacy,
|
||||||
VideoState,
|
VideoState,
|
||||||
|
@ -52,6 +53,9 @@ describe('Test live', function () {
|
||||||
live: {
|
live: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
allowReplay: true,
|
allowReplay: true,
|
||||||
|
latencySetting: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabled: false
|
enabled: false
|
||||||
}
|
}
|
||||||
|
@ -85,6 +89,7 @@ describe('Test live', function () {
|
||||||
commentsEnabled: false,
|
commentsEnabled: false,
|
||||||
downloadEnabled: false,
|
downloadEnabled: false,
|
||||||
saveReplay: true,
|
saveReplay: true,
|
||||||
|
latencyMode: LiveVideoLatencyMode.SMALL_LATENCY,
|
||||||
privacy: VideoPrivacy.PUBLIC,
|
privacy: VideoPrivacy.PUBLIC,
|
||||||
previewfile: 'video_short1-preview.webm.jpg',
|
previewfile: 'video_short1-preview.webm.jpg',
|
||||||
thumbnailfile: 'video_short1.webm.jpg'
|
thumbnailfile: 'video_short1.webm.jpg'
|
||||||
|
@ -131,6 +136,7 @@ describe('Test live', function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(live.saveReplay).to.be.true
|
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 () {
|
it('Should update the live', async function () {
|
||||||
this.timeout(10000)
|
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)
|
await waitJobs(servers)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -192,6 +198,7 @@ describe('Test live', function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(live.saveReplay).to.be.false
|
expect(live.saveReplay).to.be.false
|
||||||
|
expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
|
||||||
|
|
||||||
expect(data.live.enabled).to.be.false
|
expect(data.live.enabled).to.be.false
|
||||||
expect(data.live.allowReplay).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.maxDuration).to.equal(-1)
|
||||||
expect(data.live.maxInstanceLives).to.equal(20)
|
expect(data.live.maxInstanceLives).to.equal(20)
|
||||||
expect(data.live.maxUserLives).to.equal(3)
|
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.enabled).to.be.true
|
||||||
expect(data.live.allowReplay).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.maxDuration).to.equal(5000)
|
||||||
expect(data.live.maxInstanceLives).to.equal(-1)
|
expect(data.live.maxInstanceLives).to.equal(-1)
|
||||||
expect(data.live.maxUserLives).to.equal(10)
|
expect(data.live.maxUserLives).to.equal(10)
|
||||||
|
@ -326,6 +328,9 @@ const newCustomConfig: CustomConfig = {
|
||||||
live: {
|
live: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
allowReplay: true,
|
allowReplay: true,
|
||||||
|
latencySetting: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
maxDuration: 5000,
|
maxDuration: 5000,
|
||||||
maxInstanceLives: -1,
|
maxInstanceLives: -1,
|
||||||
maxUserLives: 10,
|
maxUserLives: 10,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
ActivityTagObject,
|
ActivityTagObject,
|
||||||
ActivityUrlObject
|
ActivityUrlObject
|
||||||
} from './common-objects'
|
} from './common-objects'
|
||||||
import { VideoState } from '../../videos'
|
import { LiveVideoLatencyMode, VideoState } from '../../videos'
|
||||||
|
|
||||||
export interface VideoObject {
|
export interface VideoObject {
|
||||||
type: 'Video'
|
type: 'Video'
|
||||||
|
@ -25,6 +25,7 @@ export interface VideoObject {
|
||||||
isLiveBroadcast: boolean
|
isLiveBroadcast: boolean
|
||||||
liveSaveReplay: boolean
|
liveSaveReplay: boolean
|
||||||
permanentLive: boolean
|
permanentLive: boolean
|
||||||
|
latencyMode: LiveVideoLatencyMode
|
||||||
|
|
||||||
commentsEnabled: boolean
|
commentsEnabled: boolean
|
||||||
downloadEnabled: boolean
|
downloadEnabled: boolean
|
||||||
|
|
|
@ -131,6 +131,10 @@ export interface CustomConfig {
|
||||||
|
|
||||||
allowReplay: boolean
|
allowReplay: boolean
|
||||||
|
|
||||||
|
latencySetting: {
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
maxDuration: number
|
maxDuration: number
|
||||||
maxInstanceLives: number
|
maxInstanceLives: number
|
||||||
maxUserLives: number
|
maxUserLives: number
|
||||||
|
|
|
@ -149,10 +149,14 @@ export interface ServerConfig {
|
||||||
live: {
|
live: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
|
||||||
|
allowReplay: boolean
|
||||||
|
latencySetting: {
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
maxDuration: number
|
maxDuration: number
|
||||||
maxInstanceLives: number
|
maxInstanceLives: number
|
||||||
maxUserLives: number
|
maxUserLives: number
|
||||||
allowReplay: boolean
|
|
||||||
|
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export * from './live-video-create.model'
|
export * from './live-video-create.model'
|
||||||
export * from './live-video-event-payload.model'
|
export * from './live-video-event-payload.model'
|
||||||
export * from './live-video-event.type'
|
export * from './live-video-event.type'
|
||||||
|
export * from './live-video-latency-mode.enum'
|
||||||
export * from './live-video-update.model'
|
export * from './live-video-update.model'
|
||||||
export * from './live-video.model'
|
export * from './live-video.model'
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { LiveVideoLatencyMode } from '.'
|
||||||
import { VideoCreate } from '../video-create.model'
|
import { VideoCreate } from '../video-create.model'
|
||||||
|
|
||||||
export interface LiveVideoCreate extends VideoCreate {
|
export interface LiveVideoCreate extends VideoCreate {
|
||||||
saveReplay?: boolean
|
saveReplay?: boolean
|
||||||
permanentLive?: boolean
|
permanentLive?: boolean
|
||||||
|
latencyMode?: LiveVideoLatencyMode
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
export const enum LiveVideoLatencyMode {
|
||||||
|
DEFAULT = 1,
|
||||||
|
HIGH_LATENCY = 2,
|
||||||
|
SMALL_LATENCY = 3
|
||||||
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
|
import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
|
||||||
|
|
||||||
export interface LiveVideoUpdate {
|
export interface LiveVideoUpdate {
|
||||||
permanentLive?: boolean
|
permanentLive?: boolean
|
||||||
saveReplay?: boolean
|
saveReplay?: boolean
|
||||||
|
latencyMode?: LiveVideoLatencyMode
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
|
import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
|
||||||
|
|
||||||
export interface LiveVideo {
|
export interface LiveVideo {
|
||||||
rtmpUrl: string
|
rtmpUrl: string
|
||||||
rtmpsUrl: string
|
rtmpsUrl: string
|
||||||
|
|
||||||
streamKey: string
|
streamKey: string
|
||||||
|
|
||||||
saveReplay: boolean
|
saveReplay: boolean
|
||||||
permanentLive: boolean
|
permanentLive: boolean
|
||||||
|
latencyMode: LiveVideoLatencyMode
|
||||||
}
|
}
|
||||||
|
|
|
@ -292,6 +292,9 @@ export class ConfigCommand extends AbstractCommand {
|
||||||
live: {
|
live: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
allowReplay: false,
|
allowReplay: false,
|
||||||
|
latencySetting: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
maxDuration: -1,
|
maxDuration: -1,
|
||||||
maxInstanceLives: -1,
|
maxInstanceLives: -1,
|
||||||
maxUserLives: 50,
|
maxUserLives: 50,
|
||||||
|
|
|
@ -2295,6 +2295,9 @@ paths:
|
||||||
permanentLive:
|
permanentLive:
|
||||||
description: User can stream multiple times in a permanent live
|
description: User can stream multiple times in a permanent live
|
||||||
type: boolean
|
type: boolean
|
||||||
|
latencyMode:
|
||||||
|
description: User can select live latency mode if enabled by the instance
|
||||||
|
$ref: '#/components/schemas/LiveVideoLatencyMode'
|
||||||
thumbnailfile:
|
thumbnailfile:
|
||||||
description: Live video/replay thumbnail file
|
description: Live video/replay thumbnail file
|
||||||
type: string
|
type: string
|
||||||
|
@ -5291,6 +5294,14 @@ components:
|
||||||
description: 'Admin flags for the user (None = `0`, Bypass video blocklist = `1`)'
|
description: 'Admin flags for the user (None = `0`, Bypass video blocklist = `1`)'
|
||||||
example: 1
|
example: 1
|
||||||
|
|
||||||
|
LiveVideoLatencyMode:
|
||||||
|
type: integer
|
||||||
|
enum:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
description: 'The live latency mode (Default = `1`, HIght latency = `2`, Small Latency = `3`)'
|
||||||
|
|
||||||
VideoStateConstant:
|
VideoStateConstant:
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
|
@ -7482,6 +7493,9 @@ components:
|
||||||
permanentLive:
|
permanentLive:
|
||||||
description: User can stream multiple times in a permanent live
|
description: User can stream multiple times in a permanent live
|
||||||
type: boolean
|
type: boolean
|
||||||
|
latencyMode:
|
||||||
|
description: User can select live latency mode if enabled by the instance
|
||||||
|
$ref: '#/components/schemas/LiveVideoLatencyMode'
|
||||||
|
|
||||||
LiveVideoResponse:
|
LiveVideoResponse:
|
||||||
properties:
|
properties:
|
||||||
|
@ -7497,8 +7511,9 @@ components:
|
||||||
permanentLive:
|
permanentLive:
|
||||||
description: User can stream multiple times in a permanent live
|
description: User can stream multiple times in a permanent live
|
||||||
type: boolean
|
type: boolean
|
||||||
|
latencyMode:
|
||||||
|
description: User can select live latency mode if enabled by the instance
|
||||||
|
$ref: '#/components/schemas/LiveVideoLatencyMode'
|
||||||
|
|
||||||
callbacks:
|
callbacks:
|
||||||
searchIndex:
|
searchIndex:
|
||||||
|
|
Loading…
Reference in New Issue