diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index e3b6f8305..94f1021bf 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -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,
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html
index 10d5278c1..8d6a4ce19 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html
@@ -36,6 +36,18 @@
+
+
+
+ Small latency disables P2P and high latency can increase P2P ratio
+
+
+
+
+
+
+
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
index 2801fc519..a2399eafb 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
@@ -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[] = []
videoLicences: VideoConstant[] = []
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
diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts
index d9e8344fc..9c4998f2e 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.ts
+++ b/client/src/app/+videos/+video-edit/video-update.component.ts
@@ -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
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts
index 1f45c4d26..067d3bc84 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -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,
diff --git a/client/src/assets/player/peertube-player-options-builder.ts b/client/src/assets/player/peertube-player-options-builder.ts
index 7a82b128d..c9cbbbf4d 100644
--- a/client/src/assets/player/peertube-player-options-builder.ts
+++ b/client/src/assets/player/peertube-player-options-builder.ts
@@ -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 {
+ 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 {
+ 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
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index 38ff39890..9e4d87911 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -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 {
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
diff --git a/config/default.yaml b/config/default.yaml
index d76894b52..898395705 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -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
diff --git a/config/production.yaml.example b/config/production.yaml.example
index 45d26190a..03afe5841 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -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
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 821ed4ad3..376143cb8 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -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,
diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts
index 49cabb6f3..c6f038079 100644
--- a/server/controllers/api/videos/live.ts
+++ b/server/controllers/api/videos/live.ts
@@ -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({
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index cbba2f51c..d0bcc6785 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -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',
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index a41d37810..80a321117 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -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) &&
diff --git a/server/helpers/custom-validators/video-lives.ts b/server/helpers/custom-validators/video-lives.ts
new file mode 100644
index 000000000..69d08ae68
--- /dev/null
+++ b/server/helpers/custom-validators/video-lives.ts
@@ -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
+}
diff --git a/server/helpers/ffmpeg/ffmpeg-live.ts b/server/helpers/ffmpeg/ffmpeg-live.ts
index ff571626c..fd20971eb 100644
--- a/server/helpers/ffmpeg/ffmpeg-live.ts
+++ b/server/helpers/ffmpeg/ffmpeg-live.ts
@@ -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')}`)
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 10dd98f43..fa311f708 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -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',
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index 7a13a1368..6dcca9b67 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -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('live.allow_replay') },
+ LATENCY_SETTING: {
+ get ENABLED () { return config.get('live.latency_setting.enabled') }
+ },
+
RTMP: {
get ENABLED () { return config.get('live.rtmp.enabled') },
get PORT () { return config.get('live.rtmp.port') },
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 7bc2877aa..1c849b561 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -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
}
diff --git a/server/initializers/migrations/0690-live-latency-mode.ts b/server/initializers/migrations/0690-live-latency-mode.ts
new file mode 100644
index 000000000..c31a61364
--- /dev/null
+++ b/server/initializers/migrations/0690-live-latency-mode.ts
@@ -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 {
+ 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
+}
diff --git a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
index 1e1479869..c97217669 100644
--- a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
+++ b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
@@ -151,6 +151,7 @@ function getLiveAttributesFromObject (video: MVideoId, videoObject: VideoObject)
return {
saveReplay: videoObject.liveSaveReplay,
permanentLive: videoObject.permanentLive,
+ latencyMode: videoObject.latencyMode,
videoId: video.id
}
}
diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts
index 21c34a9a4..920d3a5ec 100644
--- a/server/lib/live/live-manager.ts
+++ b/server/lib/live/live-manager.ts
@@ -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 })
}
diff --git a/server/lib/live/shared/muxing-session.ts b/server/lib/live/shared/muxing-session.ts
index f5f473039..a703f5b5f 100644
--- a/server/lib/live/shared/muxing-session.ts
+++ b/server/lib/live/shared/muxing-session.ts
@@ -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())
diff --git a/server/lib/server-config-manager.ts b/server/lib/server-config-manager.ts
index 43ca2332b..744186cfc 100644
--- a/server/lib/server-config-manager.ts
+++ b/server/lib/server-config-manager.ts
@@ -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,
diff --git a/server/middlewares/validators/videos/video-live.ts b/server/middlewares/validators/videos/video-live.ts
index 6c7601e05..8e52c953f 100644
--- a/server/middlewares/validators/videos/video-live.ts
+++ b/server/middlewares/validators/videos/video-live.ts
@@ -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
+}
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts
index 7456f37c5..611edf0b9 100644
--- a/server/models/video/formatter/video-format-utils.ts
+++ b/server/models/video/formatter/video-format-utils.ts
@@ -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
+ }
+}
diff --git a/server/models/video/sql/video/shared/video-table-attributes.ts b/server/models/video/sql/video/shared/video-table-attributes.ts
index f4d9e99fd..e2c1c0f6d 100644
--- a/server/models/video/sql/video/shared/video-table-attributes.ts
+++ b/server/models/video/sql/video/shared/video-table-attributes.ts
@@ -158,6 +158,7 @@ export class VideoTableAttributes {
'streamKey',
'saveReplay',
'permanentLive',
+ 'latencyMode',
'videoId',
'createdAt',
'updatedAt'
diff --git a/server/models/video/video-live.ts b/server/models/video/video-live.ts
index e3fdcc0ba..904f712b4 100644
--- a/server/models/video/video-live.ts
+++ b/server/models/video/video-live.ts
@@ -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
@Column
permanentLive: boolean
+ @AllowNull(false)
+ @Column
+ latencyMode: LiveVideoLatencyMode
+
@CreatedAt
createdAt: Date
@@ -113,7 +117,8 @@ export class VideoLiveModel extends Model
streamKey: this.streamKey,
permanentLive: this.permanentLive,
- saveReplay: this.saveReplay
+ saveReplay: this.saveReplay,
+ latencyMode: this.latencyMode
}
}
}
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index ce067a892..900f642c2 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -125,6 +125,9 @@ describe('Test config API validators', function () {
enabled: true,
allowReplay: false,
+ latencySetting: {
+ enabled: false
+ },
maxDuration: 30,
maxInstanceLives: -1,
maxUserLives: 50,
diff --git a/server/tests/api/check-params/live.ts b/server/tests/api/check-params/live.ts
index 8aee6164c..b253f5e20 100644
--- a/server/tests/api/check-params/live.ts
+++ b/server/tests/api/check-params/live.ts
@@ -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 } })
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts
index d756a02c1..aeb039696 100644
--- a/server/tests/api/live/live.ts
+++ b/server/tests/api/live/live.ts
@@ -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)
}
})
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index 565b2953a..5028b65e6 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -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,
diff --git a/shared/models/activitypub/objects/video-torrent-object.ts b/shared/models/activitypub/objects/video-torrent-object.ts
index 9faa3bb87..23d54bdbd 100644
--- a/shared/models/activitypub/objects/video-torrent-object.ts
+++ b/shared/models/activitypub/objects/video-torrent-object.ts
@@ -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
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index c9e7654de..5df606566 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -131,6 +131,10 @@ export interface CustomConfig {
allowReplay: boolean
+ latencySetting: {
+ enabled: boolean
+ }
+
maxDuration: number
maxInstanceLives: number
maxUserLives: number
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index b06019bb8..d7fbed13c 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -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
diff --git a/shared/models/videos/live/index.ts b/shared/models/videos/live/index.ts
index a36f42a7d..68f32092a 100644
--- a/shared/models/videos/live/index.ts
+++ b/shared/models/videos/live/index.ts
@@ -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'
diff --git a/shared/models/videos/live/live-video-create.model.ts b/shared/models/videos/live/live-video-create.model.ts
index caa7acc17..49ccaf45b 100644
--- a/shared/models/videos/live/live-video-create.model.ts
+++ b/shared/models/videos/live/live-video-create.model.ts
@@ -1,6 +1,8 @@
+import { LiveVideoLatencyMode } from '.'
import { VideoCreate } from '../video-create.model'
export interface LiveVideoCreate extends VideoCreate {
saveReplay?: boolean
permanentLive?: boolean
+ latencyMode?: LiveVideoLatencyMode
}
diff --git a/shared/models/videos/live/live-video-latency-mode.enum.ts b/shared/models/videos/live/live-video-latency-mode.enum.ts
new file mode 100644
index 000000000..4285e1d41
--- /dev/null
+++ b/shared/models/videos/live/live-video-latency-mode.enum.ts
@@ -0,0 +1,5 @@
+export const enum LiveVideoLatencyMode {
+ DEFAULT = 1,
+ HIGH_LATENCY = 2,
+ SMALL_LATENCY = 3
+}
diff --git a/shared/models/videos/live/live-video-update.model.ts b/shared/models/videos/live/live-video-update.model.ts
index a39c44797..93bb4d30d 100644
--- a/shared/models/videos/live/live-video-update.model.ts
+++ b/shared/models/videos/live/live-video-update.model.ts
@@ -1,4 +1,7 @@
+import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
+
export interface LiveVideoUpdate {
permanentLive?: boolean
saveReplay?: boolean
+ latencyMode?: LiveVideoLatencyMode
}
diff --git a/shared/models/videos/live/live-video.model.ts b/shared/models/videos/live/live-video.model.ts
index 815a93804..2d3169941 100644
--- a/shared/models/videos/live/live-video.model.ts
+++ b/shared/models/videos/live/live-video.model.ts
@@ -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
}
diff --git a/shared/server-commands/server/config-command.ts b/shared/server-commands/server/config-command.ts
index c0042060b..e47a0d346 100644
--- a/shared/server-commands/server/config-command.ts
+++ b/shared/server-commands/server/config-command.ts
@@ -292,6 +292,9 @@ export class ConfigCommand extends AbstractCommand {
live: {
enabled: true,
allowReplay: false,
+ latencySetting: {
+ enabled: false
+ },
maxDuration: -1,
maxInstanceLives: -1,
maxUserLives: 50,
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 70f2d97f5..5ce1f228a 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -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: