diff options
Diffstat (limited to 'client')
8 files changed, 184 insertions, 32 deletions
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 { | |||
189 | maxInstanceLives: MAX_INSTANCE_LIVES_VALIDATOR, | 189 | maxInstanceLives: MAX_INSTANCE_LIVES_VALIDATOR, |
190 | maxUserLives: MAX_USER_LIVES_VALIDATOR, | 190 | maxUserLives: MAX_USER_LIVES_VALIDATOR, |
191 | allowReplay: null, | 191 | allowReplay: null, |
192 | latencySetting: { | ||
193 | enabled: null | ||
194 | }, | ||
192 | 195 | ||
193 | transcoding: { | 196 | transcoding: { |
194 | enabled: null, | 197 | 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 @@ | |||
36 | </my-peertube-checkbox> | 36 | </my-peertube-checkbox> |
37 | </div> | 37 | </div> |
38 | 38 | ||
39 | <div class="form-group" formGroupName="latencySetting" [ngClass]="getDisabledLiveClass()"> | ||
40 | <my-peertube-checkbox | ||
41 | inputName="liveLatencySettingEnabled" formControlName="enabled" | ||
42 | i18n-labelText labelText="Allow your users to change live latency" | ||
43 | > | ||
44 | <ng-container ngProjectAs="description" i18n> | ||
45 | Small latency disables P2P and high latency can increase P2P ratio | ||
46 | </ng-container> | ||
47 | |||
48 | </my-peertube-checkbox> | ||
49 | </div> | ||
50 | |||
39 | <div class="form-group" [ngClass]="getDisabledLiveClass()"> | 51 | <div class="form-group" [ngClass]="getDisabledLiveClass()"> |
40 | <label i18n for="liveMaxInstanceLives"> | 52 | <label i18n for="liveMaxInstanceLives"> |
41 | Max simultaneous lives created on your instance <span class="text-muted">(-1 for "unlimited")</span> | 53 | Max simultaneous lives created on your instance <span class="text-muted">(-1 for "unlimited")</span> |
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html index 2281f8631..515daf15f 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html | |||
@@ -289,6 +289,17 @@ | |||
289 | </ng-container> | 289 | </ng-container> |
290 | </my-peertube-checkbox> | 290 | </my-peertube-checkbox> |
291 | </div> | 291 | </div> |
292 | |||
293 | <div class="form-group" *ngIf="isLatencyModeEnabled()"> | ||
294 | <label i18n for="latencyMode">Latency mode</label> | ||
295 | <my-select-options | ||
296 | labelForId="latencyMode" [items]="latencyModes" formControlName="latencyMode" [clearable]="true" | ||
297 | ></my-select-options> | ||
298 | |||
299 | <div *ngIf="formErrors.latencyMode" class="form-error"> | ||
300 | {{ formErrors.latencyMode }} | ||
301 | </div> | ||
302 | </div> | ||
292 | </div> | 303 | </div> |
293 | </div> | 304 | </div> |
294 | </ng-template> | 305 | </ng-template> |
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 @@ | |||
1 | import { forkJoin } from 'rxjs' | 1 | import { forkJoin } from 'rxjs' |
2 | import { map } from 'rxjs/operators' | 2 | import { map } from 'rxjs/operators' |
3 | import { SelectChannelItem } from 'src/types/select-options-item.model' | 3 | import { SelectChannelItem, SelectOptionsItem } from 'src/types/select-options-item.model' |
4 | import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' | 4 | import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' |
5 | import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms' | 5 | import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms' |
6 | import { HooksService, PluginService, ServerService } from '@app/core' | 6 | import { HooksService, PluginService, ServerService } from '@app/core' |
@@ -26,6 +26,7 @@ import { PluginInfo } from '@root-helpers/plugins-manager' | |||
26 | import { | 26 | import { |
27 | HTMLServerConfig, | 27 | HTMLServerConfig, |
28 | LiveVideo, | 28 | LiveVideo, |
29 | LiveVideoLatencyMode, | ||
29 | RegisterClientFormFieldOptions, | 30 | RegisterClientFormFieldOptions, |
30 | RegisterClientVideoFieldOptions, | 31 | RegisterClientVideoFieldOptions, |
31 | VideoConstant, | 32 | VideoConstant, |
@@ -78,6 +79,23 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
78 | videoCategories: VideoConstant<number>[] = [] | 79 | videoCategories: VideoConstant<number>[] = [] |
79 | videoLicences: VideoConstant<number>[] = [] | 80 | videoLicences: VideoConstant<number>[] = [] |
80 | videoLanguages: VideoLanguages[] = [] | 81 | videoLanguages: VideoLanguages[] = [] |
82 | latencyModes: SelectOptionsItem[] = [ | ||
83 | { | ||
84 | id: LiveVideoLatencyMode.SMALL_LATENCY, | ||
85 | label: $localize`Small latency`, | ||
86 | description: $localize`Reduce latency to ~15s disabling P2P` | ||
87 | }, | ||
88 | { | ||
89 | id: LiveVideoLatencyMode.DEFAULT, | ||
90 | label: $localize`Default`, | ||
91 | description: $localize`Average latency of 30s` | ||
92 | }, | ||
93 | { | ||
94 | id: LiveVideoLatencyMode.HIGH_LATENCY, | ||
95 | label: $localize`High latency`, | ||
96 | description: $localize`Average latency of 60s increasing P2P ratio` | ||
97 | } | ||
98 | ] | ||
81 | 99 | ||
82 | pluginDataFormGroup: FormGroup | 100 | pluginDataFormGroup: FormGroup |
83 | 101 | ||
@@ -141,6 +159,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
141 | originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR, | 159 | originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR, |
142 | liveStreamKey: null, | 160 | liveStreamKey: null, |
143 | permanentLive: null, | 161 | permanentLive: null, |
162 | latencyMode: null, | ||
144 | saveReplay: null | 163 | saveReplay: null |
145 | } | 164 | } |
146 | 165 | ||
@@ -273,6 +292,10 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
273 | return this.form.value['permanentLive'] === true | 292 | return this.form.value['permanentLive'] === true |
274 | } | 293 | } |
275 | 294 | ||
295 | isLatencyModeEnabled () { | ||
296 | return this.serverConfig.live.latencySetting.enabled | ||
297 | } | ||
298 | |||
276 | isPluginFieldHidden (pluginField: PluginField) { | 299 | isPluginFieldHidden (pluginField: PluginField) { |
277 | if (typeof pluginField.commonOptions.hidden !== 'function') return false | 300 | if (typeof pluginField.commonOptions.hidden !== 'function') return false |
278 | 301 | ||
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 { | |||
64 | if (this.liveVideo) { | 64 | if (this.liveVideo) { |
65 | this.form.patchValue({ | 65 | this.form.patchValue({ |
66 | saveReplay: this.liveVideo.saveReplay, | 66 | saveReplay: this.liveVideo.saveReplay, |
67 | latencyMode: this.liveVideo.latencyMode, | ||
67 | permanentLive: this.liveVideo.permanentLive | 68 | permanentLive: this.liveVideo.permanentLive |
68 | }) | 69 | }) |
69 | } | 70 | } |
@@ -127,7 +128,8 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
127 | 128 | ||
128 | const liveVideoUpdate: LiveVideoUpdate = { | 129 | const liveVideoUpdate: LiveVideoUpdate = { |
129 | saveReplay: !!this.form.value.saveReplay, | 130 | saveReplay: !!this.form.value.saveReplay, |
130 | permanentLive: !!this.form.value.permanentLive | 131 | permanentLive: !!this.form.value.permanentLive, |
132 | latencyMode: this.form.value.latencyMode | ||
131 | } | 133 | } |
132 | 134 | ||
133 | // Don't update live attributes if they did not change | 135 | // 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 @@ | |||
1 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 1 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
2 | import { forkJoin, Subscription } from 'rxjs' | 2 | import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs' |
3 | import { isP2PEnabled } from 'src/assets/player/utils' | 3 | import { isP2PEnabled } from 'src/assets/player/utils' |
4 | import { PlatformLocation } from '@angular/common' | 4 | import { PlatformLocation } from '@angular/common' |
5 | import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' | 5 | 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' | |||
22 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' | 22 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' |
23 | import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' | 23 | import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' |
24 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' | 24 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' |
25 | import { LiveVideoService } from '@app/shared/shared-video-live' | ||
25 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | 26 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' |
26 | import { timeToInt } from '@shared/core-utils' | 27 | import { timeToInt } from '@shared/core-utils' |
27 | import { | 28 | import { |
28 | HTMLServerConfig, | 29 | HTMLServerConfig, |
29 | HttpStatusCode, | 30 | HttpStatusCode, |
31 | LiveVideo, | ||
30 | PeerTubeProblemDocument, | 32 | PeerTubeProblemDocument, |
31 | ServerErrorCode, | 33 | ServerErrorCode, |
32 | VideoCaption, | 34 | VideoCaption, |
@@ -63,6 +65,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
63 | 65 | ||
64 | video: VideoDetails = null | 66 | video: VideoDetails = null |
65 | videoCaptions: VideoCaption[] = [] | 67 | videoCaptions: VideoCaption[] = [] |
68 | liveVideo: LiveVideo | ||
66 | 69 | ||
67 | playlistPosition: number | 70 | playlistPosition: number |
68 | playlist: VideoPlaylist = null | 71 | playlist: VideoPlaylist = null |
@@ -89,6 +92,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
89 | private router: Router, | 92 | private router: Router, |
90 | private videoService: VideoService, | 93 | private videoService: VideoService, |
91 | private playlistService: VideoPlaylistService, | 94 | private playlistService: VideoPlaylistService, |
95 | private liveVideoService: LiveVideoService, | ||
92 | private confirmService: ConfirmService, | 96 | private confirmService: ConfirmService, |
93 | private metaService: MetaService, | 97 | private metaService: MetaService, |
94 | private authService: AuthService, | 98 | private authService: AuthService, |
@@ -239,12 +243,21 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
239 | 'filter:api.video-watch.video.get.result' | 243 | 'filter:api.video-watch.video.get.result' |
240 | ) | 244 | ) |
241 | 245 | ||
246 | const videoAndLiveObs: Observable<{ video: VideoDetails, live?: LiveVideo }> = videoObs.pipe( | ||
247 | switchMap(video => { | ||
248 | if (!video.isLive) return of({ video }) | ||
249 | |||
250 | return this.liveVideoService.getVideoLive(video.uuid) | ||
251 | .pipe(map(live => ({ live, video }))) | ||
252 | }) | ||
253 | ) | ||
254 | |||
242 | forkJoin([ | 255 | forkJoin([ |
243 | videoObs, | 256 | videoAndLiveObs, |
244 | this.videoCaptionService.listCaptions(videoId), | 257 | this.videoCaptionService.listCaptions(videoId), |
245 | this.userService.getAnonymousOrLoggedUser() | 258 | this.userService.getAnonymousOrLoggedUser() |
246 | ]).subscribe({ | 259 | ]).subscribe({ |
247 | next: ([ video, captionsResult, loggedInOrAnonymousUser ]) => { | 260 | next: ([ { video, live }, captionsResult, loggedInOrAnonymousUser ]) => { |
248 | const queryParams = this.route.snapshot.queryParams | 261 | const queryParams = this.route.snapshot.queryParams |
249 | 262 | ||
250 | const urlOptions = { | 263 | const urlOptions = { |
@@ -261,7 +274,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
261 | peertubeLink: false | 274 | peertubeLink: false |
262 | } | 275 | } |
263 | 276 | ||
264 | this.onVideoFetched({ video, videoCaptions: captionsResult.data, loggedInOrAnonymousUser, urlOptions }) | 277 | this.onVideoFetched({ video, live, videoCaptions: captionsResult.data, loggedInOrAnonymousUser, urlOptions }) |
265 | .catch(err => this.handleGlobalError(err)) | 278 | .catch(err => this.handleGlobalError(err)) |
266 | }, | 279 | }, |
267 | 280 | ||
@@ -330,16 +343,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
330 | 343 | ||
331 | private async onVideoFetched (options: { | 344 | private async onVideoFetched (options: { |
332 | video: VideoDetails | 345 | video: VideoDetails |
346 | live: LiveVideo | ||
333 | videoCaptions: VideoCaption[] | 347 | videoCaptions: VideoCaption[] |
334 | urlOptions: URLOptions | 348 | urlOptions: URLOptions |
335 | loggedInOrAnonymousUser: User | 349 | loggedInOrAnonymousUser: User |
336 | }) { | 350 | }) { |
337 | const { video, videoCaptions, urlOptions, loggedInOrAnonymousUser } = options | 351 | const { video, live, videoCaptions, urlOptions, loggedInOrAnonymousUser } = options |
338 | 352 | ||
339 | this.subscribeToLiveEventsIfNeeded(this.video, video) | 353 | this.subscribeToLiveEventsIfNeeded(this.video, video) |
340 | 354 | ||
341 | this.video = video | 355 | this.video = video |
342 | this.videoCaptions = videoCaptions | 356 | this.videoCaptions = videoCaptions |
357 | this.liveVideo = live | ||
343 | 358 | ||
344 | // Re init attributes | 359 | // Re init attributes |
345 | this.playerPlaceholderImgSrc = undefined | 360 | this.playerPlaceholderImgSrc = undefined |
@@ -387,6 +402,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
387 | const params = { | 402 | const params = { |
388 | video: this.video, | 403 | video: this.video, |
389 | videoCaptions: this.videoCaptions, | 404 | videoCaptions: this.videoCaptions, |
405 | liveVideo: this.liveVideo, | ||
390 | urlOptions, | 406 | urlOptions, |
391 | loggedInOrAnonymousUser, | 407 | loggedInOrAnonymousUser, |
392 | user: this.user | 408 | user: this.user |
@@ -532,12 +548,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
532 | 548 | ||
533 | private buildPlayerManagerOptions (params: { | 549 | private buildPlayerManagerOptions (params: { |
534 | video: VideoDetails | 550 | video: VideoDetails |
551 | liveVideo: LiveVideo | ||
535 | videoCaptions: VideoCaption[] | 552 | videoCaptions: VideoCaption[] |
536 | urlOptions: CustomizationOptions & { playerMode: PlayerMode } | 553 | urlOptions: CustomizationOptions & { playerMode: PlayerMode } |
537 | loggedInOrAnonymousUser: User | 554 | loggedInOrAnonymousUser: User |
538 | user?: AuthUser | 555 | user?: AuthUser |
539 | }) { | 556 | }) { |
540 | const { video, videoCaptions, urlOptions, loggedInOrAnonymousUser, user } = params | 557 | const { video, liveVideo, videoCaptions, urlOptions, loggedInOrAnonymousUser, user } = params |
541 | 558 | ||
542 | const getStartTime = () => { | 559 | const getStartTime = () => { |
543 | const byUrl = urlOptions.startTime !== undefined | 560 | const byUrl = urlOptions.startTime !== undefined |
@@ -562,6 +579,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
562 | src: environment.apiUrl + c.captionPath | 579 | src: environment.apiUrl + c.captionPath |
563 | })) | 580 | })) |
564 | 581 | ||
582 | const liveOptions = video.isLive | ||
583 | ? { latencyMode: liveVideo.latencyMode } | ||
584 | : undefined | ||
585 | |||
565 | const options: PeertubePlayerManagerOptions = { | 586 | const options: PeertubePlayerManagerOptions = { |
566 | common: { | 587 | common: { |
567 | autoplay: this.isAutoplay(), | 588 | autoplay: this.isAutoplay(), |
@@ -597,6 +618,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
597 | embedTitle: video.name, | 618 | embedTitle: video.name, |
598 | 619 | ||
599 | isLive: video.isLive, | 620 | isLive: video.isLive, |
621 | liveOptions, | ||
600 | 622 | ||
601 | language: this.localeId, | 623 | language: this.localeId, |
602 | 624 | ||
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 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core' | ||
2 | import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs' | 3 | import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs' |
3 | import { PluginsManager } from '@root-helpers/plugins-manager' | 4 | import { PluginsManager } from '@root-helpers/plugins-manager' |
4 | import { buildVideoLink, decorateVideoLink } from '@shared/core-utils' | 5 | import { buildVideoLink, decorateVideoLink } from '@shared/core-utils' |
5 | import { isDefaultLocale } from '@shared/core-utils/i18n' | 6 | import { isDefaultLocale } from '@shared/core-utils/i18n' |
6 | import { VideoFile } from '@shared/models' | 7 | import { LiveVideoLatencyMode, VideoFile } from '@shared/models' |
7 | import { copyToClipboard } from '../../root-helpers/utils' | 8 | import { copyToClipboard } from '../../root-helpers/utils' |
8 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' | 9 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' |
9 | import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' | 10 | import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' |
@@ -19,7 +20,6 @@ import { | |||
19 | VideoJSPluginOptions | 20 | VideoJSPluginOptions |
20 | } from './peertube-videojs-typings' | 21 | } from './peertube-videojs-typings' |
21 | import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils' | 22 | import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils' |
22 | import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core' | ||
23 | 23 | ||
24 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' | 24 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' |
25 | 25 | ||
@@ -76,6 +76,9 @@ export interface CommonOptions extends CustomizationOptions { | |||
76 | embedTitle: string | 76 | embedTitle: string |
77 | 77 | ||
78 | isLive: boolean | 78 | isLive: boolean |
79 | liveOptions?: { | ||
80 | latencyMode: LiveVideoLatencyMode | ||
81 | } | ||
79 | 82 | ||
80 | language?: string | 83 | language?: string |
81 | 84 | ||
@@ -250,21 +253,8 @@ export class PeertubePlayerOptionsBuilder { | |||
250 | .filter(t => t.startsWith('ws')) | 253 | .filter(t => t.startsWith('ws')) |
251 | 254 | ||
252 | const specificLiveOrVODOptions = this.options.common.isLive | 255 | const specificLiveOrVODOptions = this.options.common.isLive |
253 | ? { // Live | 256 | ? this.getP2PMediaLoaderLiveOptions() |
254 | requiredSegmentsPriority: 1 | 257 | : this.getP2PMediaLoaderVODOptions() |
255 | } | ||
256 | : { // VOD | ||
257 | requiredSegmentsPriority: 3, | ||
258 | |||
259 | cachedSegmentExpiration: 86400000, | ||
260 | cachedSegmentsCount: 100, | ||
261 | |||
262 | httpDownloadMaxPriority: 9, | ||
263 | httpDownloadProbability: 0.06, | ||
264 | httpDownloadProbabilitySkipIfNoPeers: true, | ||
265 | |||
266 | p2pDownloadMaxPriority: 50 | ||
267 | } | ||
268 | 258 | ||
269 | return { | 259 | return { |
270 | trackerAnnounce, | 260 | trackerAnnounce, |
@@ -283,13 +273,57 @@ export class PeertubePlayerOptionsBuilder { | |||
283 | } | 273 | } |
284 | } | 274 | } |
285 | 275 | ||
276 | private getP2PMediaLoaderLiveOptions (): Partial<HybridLoaderSettings> { | ||
277 | const base = { | ||
278 | requiredSegmentsPriority: 1 | ||
279 | } | ||
280 | |||
281 | const latencyMode = this.options.common.liveOptions.latencyMode | ||
282 | |||
283 | switch (latencyMode) { | ||
284 | case LiveVideoLatencyMode.SMALL_LATENCY: | ||
285 | return { | ||
286 | ...base, | ||
287 | |||
288 | useP2P: false, | ||
289 | httpDownloadProbability: 1 | ||
290 | } | ||
291 | |||
292 | case LiveVideoLatencyMode.HIGH_LATENCY: | ||
293 | return base | ||
294 | |||
295 | default: | ||
296 | return base | ||
297 | } | ||
298 | } | ||
299 | |||
300 | private getP2PMediaLoaderVODOptions (): Partial<HybridLoaderSettings> { | ||
301 | return { | ||
302 | requiredSegmentsPriority: 3, | ||
303 | |||
304 | cachedSegmentExpiration: 86400000, | ||
305 | cachedSegmentsCount: 100, | ||
306 | |||
307 | httpDownloadMaxPriority: 9, | ||
308 | httpDownloadProbability: 0.06, | ||
309 | httpDownloadProbabilitySkipIfNoPeers: true, | ||
310 | |||
311 | p2pDownloadMaxPriority: 50 | ||
312 | } | ||
313 | } | ||
314 | |||
286 | private getHLSOptions (p2pMediaLoaderConfig: HlsJsEngineSettings) { | 315 | private getHLSOptions (p2pMediaLoaderConfig: HlsJsEngineSettings) { |
316 | const specificLiveOrVODOptions = this.options.common.isLive | ||
317 | ? this.getHLSLiveOptions() | ||
318 | : this.getHLSVODOptions() | ||
319 | |||
287 | const base = { | 320 | const base = { |
288 | capLevelToPlayerSize: true, | 321 | capLevelToPlayerSize: true, |
289 | autoStartLoad: false, | 322 | autoStartLoad: false, |
290 | liveSyncDurationCount: 5, | ||
291 | 323 | ||
292 | loader: new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() | 324 | loader: new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass(), |
325 | |||
326 | ...specificLiveOrVODOptions | ||
293 | } | 327 | } |
294 | 328 | ||
295 | const averageBandwidth = getAverageBandwidthInStore() | 329 | const averageBandwidth = getAverageBandwidthInStore() |
@@ -305,6 +339,33 @@ export class PeertubePlayerOptionsBuilder { | |||
305 | } | 339 | } |
306 | } | 340 | } |
307 | 341 | ||
342 | private getHLSLiveOptions () { | ||
343 | const latencyMode = this.options.common.liveOptions.latencyMode | ||
344 | |||
345 | switch (latencyMode) { | ||
346 | case LiveVideoLatencyMode.SMALL_LATENCY: | ||
347 | return { | ||
348 | liveSyncDurationCount: 2 | ||
349 | } | ||
350 | |||
351 | case LiveVideoLatencyMode.HIGH_LATENCY: | ||
352 | return { | ||
353 | liveSyncDurationCount: 10 | ||
354 | } | ||
355 | |||
356 | default: | ||
357 | return { | ||
358 | liveSyncDurationCount: 5 | ||
359 | } | ||
360 | } | ||
361 | } | ||
362 | |||
363 | private getHLSVODOptions () { | ||
364 | return { | ||
365 | liveSyncDurationCount: 5 | ||
366 | } | ||
367 | } | ||
368 | |||
308 | private addWebTorrentOptions (plugins: VideoJSPluginOptions, alreadyPlayed: boolean) { | 369 | private addWebTorrentOptions (plugins: VideoJSPluginOptions, alreadyPlayed: boolean) { |
309 | const commonOptions = this.options.common | 370 | const commonOptions = this.options.common |
310 | const webtorrentOptions = this.options.webtorrent | 371 | 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' | |||
6 | import { | 6 | import { |
7 | HTMLServerConfig, | 7 | HTMLServerConfig, |
8 | HttpStatusCode, | 8 | HttpStatusCode, |
9 | LiveVideo, | ||
9 | OAuth2ErrorCode, | 10 | OAuth2ErrorCode, |
10 | ResultList, | 11 | ResultList, |
11 | UserRefreshToken, | 12 | UserRefreshToken, |
@@ -94,6 +95,10 @@ export class PeerTubeEmbed { | |||
94 | return window.location.origin + '/api/v1/videos/' + id | 95 | return window.location.origin + '/api/v1/videos/' + id |
95 | } | 96 | } |
96 | 97 | ||
98 | getLiveUrl (videoId: string) { | ||
99 | return window.location.origin + '/api/v1/videos/live/' + videoId | ||
100 | } | ||
101 | |||
97 | refreshFetch (url: string, options?: RequestInit) { | 102 | refreshFetch (url: string, options?: RequestInit) { |
98 | return fetch(url, options) | 103 | return fetch(url, options) |
99 | .then((res: Response) => { | 104 | .then((res: Response) => { |
@@ -166,6 +171,12 @@ export class PeerTubeEmbed { | |||
166 | return this.refreshFetch(this.getVideoUrl(videoId) + '/captions', { headers: this.headers }) | 171 | return this.refreshFetch(this.getVideoUrl(videoId) + '/captions', { headers: this.headers }) |
167 | } | 172 | } |
168 | 173 | ||
174 | loadWithLive (video: VideoDetails) { | ||
175 | return this.refreshFetch(this.getLiveUrl(video.uuid), { headers: this.headers }) | ||
176 | .then(res => res.json()) | ||
177 | .then((live: LiveVideo) => ({ video, live })) | ||
178 | } | ||
179 | |||
169 | loadPlaylistInfo (playlistId: string): Promise<Response> { | 180 | loadPlaylistInfo (playlistId: string): Promise<Response> { |
170 | return this.refreshFetch(this.getPlaylistUrl(playlistId), { headers: this.headers }) | 181 | return this.refreshFetch(this.getPlaylistUrl(playlistId), { headers: this.headers }) |
171 | } | 182 | } |
@@ -475,13 +486,15 @@ export class PeerTubeEmbed { | |||
475 | .then(res => res.json()) | 486 | .then(res => res.json()) |
476 | } | 487 | } |
477 | 488 | ||
478 | const videoInfoPromise = videoResponse.json() | 489 | const videoInfoPromise: Promise<{ video: VideoDetails, live?: LiveVideo }> = videoResponse.json() |
479 | .then((videoInfo: VideoDetails) => { | 490 | .then((videoInfo: VideoDetails) => { |
480 | this.loadParams(videoInfo) | 491 | this.loadParams(videoInfo) |
481 | 492 | ||
482 | if (!alreadyHadPlayer && !this.autoplay) this.loadPlaceholder(videoInfo) | 493 | if (!alreadyHadPlayer && !this.autoplay) this.buildPlaceholder(videoInfo) |
483 | 494 | ||
484 | return videoInfo | 495 | if (!videoInfo.isLive) return { video: videoInfo } |
496 | |||
497 | return this.loadWithLive(videoInfo) | ||
485 | }) | 498 | }) |
486 | 499 | ||
487 | const [ videoInfoTmp, serverTranslations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([ | 500 | const [ videoInfoTmp, serverTranslations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([ |
@@ -493,11 +506,15 @@ export class PeerTubeEmbed { | |||
493 | 506 | ||
494 | await this.loadPlugins(serverTranslations) | 507 | await this.loadPlugins(serverTranslations) |
495 | 508 | ||
496 | const videoInfo: VideoDetails = videoInfoTmp | 509 | const { video: videoInfo, live } = videoInfoTmp |
497 | 510 | ||
498 | const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager | 511 | const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager |
499 | const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse) | 512 | const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse) |
500 | 513 | ||
514 | const liveOptions = videoInfo.isLive | ||
515 | ? { latencyMode: live.latencyMode } | ||
516 | : undefined | ||
517 | |||
501 | const playlistPlugin = this.currentPlaylistElement | 518 | const playlistPlugin = this.currentPlaylistElement |
502 | ? { | 519 | ? { |
503 | elements: this.playlistElements, | 520 | elements: this.playlistElements, |
@@ -545,6 +562,7 @@ export class PeerTubeEmbed { | |||
545 | videoUUID: videoInfo.uuid, | 562 | videoUUID: videoInfo.uuid, |
546 | 563 | ||
547 | isLive: videoInfo.isLive, | 564 | isLive: videoInfo.isLive, |
565 | liveOptions, | ||
548 | 566 | ||
549 | playerElement: this.playerElement, | 567 | playerElement: this.playerElement, |
550 | onPlayerElementChange: (element: HTMLVideoElement) => { | 568 | onPlayerElementChange: (element: HTMLVideoElement) => { |
@@ -726,7 +744,7 @@ export class PeerTubeEmbed { | |||
726 | return [] | 744 | return [] |
727 | } | 745 | } |
728 | 746 | ||
729 | private loadPlaceholder (video: VideoDetails) { | 747 | private buildPlaceholder (video: VideoDetails) { |
730 | const placeholder = this.getPlaceholderElement() | 748 | const placeholder = this.getPlaceholderElement() |
731 | 749 | ||
732 | const url = window.location.origin + video.previewPath | 750 | const url = window.location.origin + video.previewPath |