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 ++++-- 8 files changed, 184 insertions(+), 32 deletions(-) (limited to 'client') 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 -- cgit v1.2.3