From f443a74649174b2f9347c158e30f8ac7aa3e958a Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 4 Mar 2022 13:40:02 +0100 Subject: Add latency setting support --- .../edit-custom-config.component.ts | 3 + .../edit-live-configuration.component.html | 12 +++ .../+video-edit/shared/video-edit.component.html | 11 +++ .../+video-edit/shared/video-edit.component.ts | 25 +++++- .../+videos/+video-edit/video-update.component.ts | 4 +- .../+videos/+video-watch/video-watch.component.ts | 34 ++++++-- .../player/peertube-player-options-builder.ts | 99 +++++++++++++++++----- client/src/standalone/videos/embed.ts | 28 ++++-- config/default.yaml | 6 ++ config/production.yaml.example | 6 ++ server/controllers/api/config.ts | 3 + server/controllers/api/videos/live.ts | 9 +- server/helpers/activitypub.ts | 4 + .../custom-validators/activitypub/videos.ts | 4 +- server/helpers/custom-validators/video-lives.ts | 11 +++ server/helpers/ffmpeg/ffmpeg-live.ts | 39 +++++++-- server/initializers/checker-before-init.ts | 4 +- server/initializers/config.ts | 6 +- server/initializers/constants.ts | 10 ++- .../migrations/0690-live-latency-mode.ts | 35 ++++++++ .../videos/shared/object-to-model-attributes.ts | 1 + server/lib/live/live-manager.ts | 7 +- server/lib/live/shared/muxing-session.ts | 9 +- server/lib/server-config-manager.ts | 4 + server/middlewares/validators/videos/video-live.ts | 73 ++++++++++++++-- .../models/video/formatter/video-format-utils.ts | 39 ++++++--- .../sql/video/shared/video-table-attributes.ts | 1 + server/models/video/video-live.ts | 11 ++- server/tests/api/check-params/config.ts | 3 + server/tests/api/check-params/live.ts | 32 ++++++- server/tests/api/live/live.ts | 9 +- server/tests/api/server/config.ts | 5 ++ .../activitypub/objects/video-torrent-object.ts | 3 +- shared/models/server/custom-config.model.ts | 4 + shared/models/server/server-config.model.ts | 6 +- shared/models/videos/live/index.ts | 1 + .../models/videos/live/live-video-create.model.ts | 2 + .../videos/live/live-video-latency-mode.enum.ts | 5 ++ .../models/videos/live/live-video-update.model.ts | 3 + shared/models/videos/live/live-video.model.ts | 4 + shared/server-commands/server/config-command.ts | 3 + support/doc/api/openapi.yaml | 19 ++++- 42 files changed, 516 insertions(+), 81 deletions(-) create mode 100644 server/helpers/custom-validators/video-lives.ts create mode 100644 server/initializers/migrations/0690-live-latency-mode.ts create mode 100644 shared/models/videos/live/live-video-latency-mode.enum.ts 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 + + + +
+
+ +
+ + + +
+ {{ formErrors.latencyMode }} +
+
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: -- cgit v1.2.3