diff options
author | Chocobozzz <me@florianbigard.com> | 2022-03-04 13:40:02 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2022-03-09 09:23:10 +0100 |
commit | f443a74649174b2f9347c158e30f8ac7aa3e958a (patch) | |
tree | e423bc4e2307477bda4341037b7fa04ad10adae6 | |
parent | 01dd04cd5ab7b55d2a9af7d0ebf405bee9579b09 (diff) | |
download | PeerTube-f443a74649174b2f9347c158e30f8ac7aa3e958a.tar.gz PeerTube-f443a74649174b2f9347c158e30f8ac7aa3e958a.tar.zst PeerTube-f443a74649174b2f9347c158e30f8ac7aa3e958a.zip |
Add latency setting support
42 files changed, 516 insertions, 81 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 |
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: | |||
392 | # /!\ transcoding.enabled (and not live.transcoding.enabled) has to be true to create a replay | 392 | # /!\ transcoding.enabled (and not live.transcoding.enabled) has to be true to create a replay |
393 | allow_replay: true | 393 | allow_replay: true |
394 | 394 | ||
395 | # Allow your users to change latency settings (small latency/default/high latency) | ||
396 | # Small latency live streams cannot use P2P | ||
397 | # High latency live streams can increase P2P ratio | ||
398 | latency_setting: | ||
399 | enabled: true | ||
400 | |||
395 | # Your firewall should accept traffic from this port in TCP if you enable live | 401 | # Your firewall should accept traffic from this port in TCP if you enable live |
396 | rtmp: | 402 | rtmp: |
397 | enabled: true | 403 | 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: | |||
400 | # /!\ transcoding.enabled (and not live.transcoding.enabled) has to be true to create a replay | 400 | # /!\ transcoding.enabled (and not live.transcoding.enabled) has to be true to create a replay |
401 | allow_replay: true | 401 | allow_replay: true |
402 | 402 | ||
403 | # Allow your users to change latency settings (small latency/default/high latency) | ||
404 | # Small latency live streams cannot use P2P | ||
405 | # High latency live streams can increase P2P ratio | ||
406 | latency_setting: | ||
407 | enabled: true | ||
408 | |||
403 | # Your firewall should accept traffic from this port in TCP if you enable live | 409 | # Your firewall should accept traffic from this port in TCP if you enable live |
404 | rtmp: | 410 | rtmp: |
405 | enabled: true | 411 | 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 { | |||
237 | live: { | 237 | live: { |
238 | enabled: CONFIG.LIVE.ENABLED, | 238 | enabled: CONFIG.LIVE.ENABLED, |
239 | allowReplay: CONFIG.LIVE.ALLOW_REPLAY, | 239 | allowReplay: CONFIG.LIVE.ALLOW_REPLAY, |
240 | latencySetting: { | ||
241 | enabled: CONFIG.LIVE.LATENCY_SETTING.ENABLED | ||
242 | }, | ||
240 | maxDuration: CONFIG.LIVE.MAX_DURATION, | 243 | maxDuration: CONFIG.LIVE.MAX_DURATION, |
241 | maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES, | 244 | maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES, |
242 | maxUserLives: CONFIG.LIVE.MAX_USER_LIVES, | 245 | 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 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { exists } from '@server/helpers/custom-validators/misc' | ||
2 | import { createReqFiles } from '@server/helpers/express-utils' | 3 | import { createReqFiles } from '@server/helpers/express-utils' |
3 | import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants' | 4 | import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants' |
4 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | 5 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' |
@@ -9,7 +10,7 @@ import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator | |||
9 | import { VideoLiveModel } from '@server/models/video/video-live' | 10 | import { VideoLiveModel } from '@server/models/video/video-live' |
10 | import { MVideoDetails, MVideoFullLight } from '@server/types/models' | 11 | import { MVideoDetails, MVideoFullLight } from '@server/types/models' |
11 | import { buildUUID, uuidToShort } from '@shared/extra-utils' | 12 | import { buildUUID, uuidToShort } from '@shared/extra-utils' |
12 | import { HttpStatusCode, LiveVideoCreate, LiveVideoUpdate, VideoState } from '@shared/models' | 13 | import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, VideoState } from '@shared/models' |
13 | import { logger } from '../../../helpers/logger' | 14 | import { logger } from '../../../helpers/logger' |
14 | import { sequelizeTypescript } from '../../../initializers/database' | 15 | import { sequelizeTypescript } from '../../../initializers/database' |
15 | import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' | 16 | import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' |
@@ -60,8 +61,9 @@ async function updateLiveVideo (req: express.Request, res: express.Response) { | |||
60 | const video = res.locals.videoAll | 61 | const video = res.locals.videoAll |
61 | const videoLive = res.locals.videoLive | 62 | const videoLive = res.locals.videoLive |
62 | 63 | ||
63 | videoLive.saveReplay = body.saveReplay || false | 64 | if (exists(body.saveReplay)) videoLive.saveReplay = body.saveReplay |
64 | videoLive.permanentLive = body.permanentLive || false | 65 | if (exists(body.permanentLive)) videoLive.permanentLive = body.permanentLive |
66 | if (exists(body.latencyMode)) videoLive.latencyMode = body.latencyMode | ||
65 | 67 | ||
66 | video.VideoLive = await videoLive.save() | 68 | video.VideoLive = await videoLive.save() |
67 | 69 | ||
@@ -87,6 +89,7 @@ async function addLiveVideo (req: express.Request, res: express.Response) { | |||
87 | const videoLive = new VideoLiveModel() | 89 | const videoLive = new VideoLiveModel() |
88 | videoLive.saveReplay = videoInfo.saveReplay || false | 90 | videoLive.saveReplay = videoInfo.saveReplay || false |
89 | videoLive.permanentLive = videoInfo.permanentLive || false | 91 | videoLive.permanentLive = videoInfo.permanentLive || false |
92 | videoLive.latencyMode = videoInfo.latencyMode || LiveVideoLatencyMode.DEFAULT | ||
90 | videoLive.streamKey = buildUUID() | 93 | videoLive.streamKey = buildUUID() |
91 | 94 | ||
92 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | 95 | 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) { | |||
50 | '@type': 'sc:Boolean', | 50 | '@type': 'sc:Boolean', |
51 | '@id': 'pt:permanentLive' | 51 | '@id': 'pt:permanentLive' |
52 | }, | 52 | }, |
53 | latencyMode: { | ||
54 | '@type': 'sc:Number', | ||
55 | '@id': 'pt:latencyMode' | ||
56 | }, | ||
53 | 57 | ||
54 | Infohash: 'pt:Infohash', | 58 | Infohash: 'pt:Infohash', |
55 | Playlist: 'pt:Playlist', | 59 | 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 @@ | |||
1 | import validator from 'validator' | 1 | import validator from 'validator' |
2 | import { logger } from '@server/helpers/logger' | 2 | import { logger } from '@server/helpers/logger' |
3 | import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@shared/models' | 3 | import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@shared/models' |
4 | import { VideoState } from '../../../../shared/models/videos' | 4 | import { LiveVideoLatencyMode, VideoState } from '../../../../shared/models/videos' |
5 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants' | 5 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants' |
6 | import { peertubeTruncate } from '../../core-utils' | 6 | import { peertubeTruncate } from '../../core-utils' |
7 | import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc' | 7 | import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc' |
8 | import { isLiveLatencyModeValid } from '../video-lives' | ||
8 | import { | 9 | import { |
9 | isVideoDurationValid, | 10 | isVideoDurationValid, |
10 | isVideoNameValid, | 11 | isVideoNameValid, |
@@ -65,6 +66,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) { | |||
65 | if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false | 66 | if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false |
66 | if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false | 67 | if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false |
67 | if (!isBooleanValid(video.permanentLive)) video.permanentLive = false | 68 | if (!isBooleanValid(video.permanentLive)) video.permanentLive = false |
69 | if (!isLiveLatencyModeValid(video.latencyMode)) video.latencyMode = LiveVideoLatencyMode.DEFAULT | ||
68 | 70 | ||
69 | return isActivityPubUrlValid(video.id) && | 71 | return isActivityPubUrlValid(video.id) && |
70 | isVideoNameValid(video.name) && | 72 | 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 @@ | |||
1 | import { LiveVideoLatencyMode } from '@shared/models' | ||
2 | |||
3 | function isLiveLatencyModeValid (value: any) { | ||
4 | return [ LiveVideoLatencyMode.DEFAULT, LiveVideoLatencyMode.SMALL_LATENCY, LiveVideoLatencyMode.HIGH_LATENCY ].includes(value) | ||
5 | } | ||
6 | |||
7 | // --------------------------------------------------------------------------- | ||
8 | |||
9 | export { | ||
10 | isLiveLatencyModeValid | ||
11 | } | ||
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 @@ | |||
1 | import { FfmpegCommand, FilterSpecification } from 'fluent-ffmpeg' | 1 | import { FfmpegCommand, FilterSpecification } from 'fluent-ffmpeg' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { VIDEO_LIVE } from '@server/initializers/constants' | 3 | import { VIDEO_LIVE } from '@server/initializers/constants' |
4 | import { AvailableEncoders } from '@shared/models' | 4 | import { AvailableEncoders, LiveVideoLatencyMode } from '@shared/models' |
5 | import { logger, loggerTagsFactory } from '../logger' | 5 | import { logger, loggerTagsFactory } from '../logger' |
6 | import { buildStreamSuffix, getFFmpeg, getScaleFilter, StreamType } from './ffmpeg-commons' | 6 | import { buildStreamSuffix, getFFmpeg, getScaleFilter, StreamType } from './ffmpeg-commons' |
7 | import { getEncoderBuilderResult } from './ffmpeg-encoders' | 7 | import { getEncoderBuilderResult } from './ffmpeg-encoders' |
@@ -15,6 +15,7 @@ async function getLiveTranscodingCommand (options: { | |||
15 | 15 | ||
16 | outPath: string | 16 | outPath: string |
17 | masterPlaylistName: string | 17 | masterPlaylistName: string |
18 | latencyMode: LiveVideoLatencyMode | ||
18 | 19 | ||
19 | resolutions: number[] | 20 | resolutions: number[] |
20 | 21 | ||
@@ -26,7 +27,7 @@ async function getLiveTranscodingCommand (options: { | |||
26 | availableEncoders: AvailableEncoders | 27 | availableEncoders: AvailableEncoders |
27 | profile: string | 28 | profile: string |
28 | }) { | 29 | }) { |
29 | const { inputUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName, ratio } = options | 30 | const { inputUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName, ratio, latencyMode } = options |
30 | 31 | ||
31 | const command = getFFmpeg(inputUrl, 'live') | 32 | const command = getFFmpeg(inputUrl, 'live') |
32 | 33 | ||
@@ -120,14 +121,21 @@ async function getLiveTranscodingCommand (options: { | |||
120 | 121 | ||
121 | command.complexFilter(complexFilter) | 122 | command.complexFilter(complexFilter) |
122 | 123 | ||
123 | addDefaultLiveHLSParams(command, outPath, masterPlaylistName) | 124 | addDefaultLiveHLSParams({ command, outPath, masterPlaylistName, latencyMode }) |
124 | 125 | ||
125 | command.outputOption('-var_stream_map', varStreamMap.join(' ')) | 126 | command.outputOption('-var_stream_map', varStreamMap.join(' ')) |
126 | 127 | ||
127 | return command | 128 | return command |
128 | } | 129 | } |
129 | 130 | ||
130 | function getLiveMuxingCommand (inputUrl: string, outPath: string, masterPlaylistName: string) { | 131 | function getLiveMuxingCommand (options: { |
132 | inputUrl: string | ||
133 | outPath: string | ||
134 | masterPlaylistName: string | ||
135 | latencyMode: LiveVideoLatencyMode | ||
136 | }) { | ||
137 | const { inputUrl, outPath, masterPlaylistName, latencyMode } = options | ||
138 | |||
131 | const command = getFFmpeg(inputUrl, 'live') | 139 | const command = getFFmpeg(inputUrl, 'live') |
132 | 140 | ||
133 | command.outputOption('-c:v copy') | 141 | command.outputOption('-c:v copy') |
@@ -135,22 +143,39 @@ function getLiveMuxingCommand (inputUrl: string, outPath: string, masterPlaylist | |||
135 | command.outputOption('-map 0:a?') | 143 | command.outputOption('-map 0:a?') |
136 | command.outputOption('-map 0:v?') | 144 | command.outputOption('-map 0:v?') |
137 | 145 | ||
138 | addDefaultLiveHLSParams(command, outPath, masterPlaylistName) | 146 | addDefaultLiveHLSParams({ command, outPath, masterPlaylistName, latencyMode }) |
139 | 147 | ||
140 | return command | 148 | return command |
141 | } | 149 | } |
142 | 150 | ||
151 | function getLiveSegmentTime (latencyMode: LiveVideoLatencyMode) { | ||
152 | if (latencyMode === LiveVideoLatencyMode.SMALL_LATENCY) { | ||
153 | return VIDEO_LIVE.SEGMENT_TIME_SECONDS.SMALL_LATENCY | ||
154 | } | ||
155 | |||
156 | return VIDEO_LIVE.SEGMENT_TIME_SECONDS.DEFAULT_LATENCY | ||
157 | } | ||
158 | |||
143 | // --------------------------------------------------------------------------- | 159 | // --------------------------------------------------------------------------- |
144 | 160 | ||
145 | export { | 161 | export { |
162 | getLiveSegmentTime, | ||
163 | |||
146 | getLiveTranscodingCommand, | 164 | getLiveTranscodingCommand, |
147 | getLiveMuxingCommand | 165 | getLiveMuxingCommand |
148 | } | 166 | } |
149 | 167 | ||
150 | // --------------------------------------------------------------------------- | 168 | // --------------------------------------------------------------------------- |
151 | 169 | ||
152 | function addDefaultLiveHLSParams (command: FfmpegCommand, outPath: string, masterPlaylistName: string) { | 170 | function addDefaultLiveHLSParams (options: { |
153 | command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS) | 171 | command: FfmpegCommand |
172 | outPath: string | ||
173 | masterPlaylistName: string | ||
174 | latencyMode: LiveVideoLatencyMode | ||
175 | }) { | ||
176 | const { command, outPath, masterPlaylistName, latencyMode } = options | ||
177 | |||
178 | command.outputOption('-hls_time ' + getLiveSegmentTime(latencyMode)) | ||
154 | command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE) | 179 | command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE) |
155 | command.outputOption('-hls_flags delete_segments+independent_segments') | 180 | command.outputOption('-hls_flags delete_segments+independent_segments') |
156 | command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`) | 181 | 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 () { | |||
49 | 'peertube.check_latest_version.enabled', 'peertube.check_latest_version.url', | 49 | 'peertube.check_latest_version.enabled', 'peertube.check_latest_version.url', |
50 | 'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url', | 50 | 'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url', |
51 | 'search.search_index.disable_local_search', 'search.search_index.is_default_search', | 51 | 'search.search_index.disable_local_search', 'search.search_index.is_default_search', |
52 | 'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives', | 52 | 'live.enabled', 'live.allow_replay', 'live.latency_setting.enabled', 'live.max_duration', |
53 | 'live.rtmp.enabled', 'live.rtmp.port', 'live.rtmp.hostname', | 53 | 'live.max_user_lives', 'live.max_instance_lives', 'live.rtmp.enabled', 'live.rtmp.port', 'live.rtmp.hostname', |
54 | 'live.rtmps.enabled', 'live.rtmps.port', 'live.rtmps.hostname', 'live.rtmps.key_file', 'live.rtmps.cert_file', | 54 | 'live.rtmps.enabled', 'live.rtmps.port', 'live.rtmps.hostname', 'live.rtmps.key_file', 'live.rtmps.cert_file', |
55 | 'live.transcoding.enabled', 'live.transcoding.threads', 'live.transcoding.profile', | 55 | 'live.transcoding.enabled', 'live.transcoding.threads', 'live.transcoding.profile', |
56 | 'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p', | 56 | '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' | |||
4 | import { decacheModule } from '@server/helpers/decache' | 4 | import { decacheModule } from '@server/helpers/decache' |
5 | import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' | 5 | import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' |
6 | import { BroadcastMessageLevel } from '@shared/models/server' | 6 | import { BroadcastMessageLevel } from '@shared/models/server' |
7 | import { buildPath, root } from '../../shared/core-utils' | ||
7 | import { VideoPrivacy, VideosRedundancyStrategy } from '../../shared/models' | 8 | import { VideoPrivacy, VideosRedundancyStrategy } from '../../shared/models' |
8 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | 9 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' |
9 | import { buildPath, root } from '../../shared/core-utils' | ||
10 | import { parseBytes, parseDurationToMs } from '../helpers/core-utils' | 10 | import { parseBytes, parseDurationToMs } from '../helpers/core-utils' |
11 | 11 | ||
12 | // Use a variable to reload the configuration if we need | 12 | // Use a variable to reload the configuration if we need |
@@ -296,6 +296,10 @@ const CONFIG = { | |||
296 | 296 | ||
297 | get ALLOW_REPLAY () { return config.get<boolean>('live.allow_replay') }, | 297 | get ALLOW_REPLAY () { return config.get<boolean>('live.allow_replay') }, |
298 | 298 | ||
299 | LATENCY_SETTING: { | ||
300 | get ENABLED () { return config.get<boolean>('live.latency_setting.enabled') } | ||
301 | }, | ||
302 | |||
299 | RTMP: { | 303 | RTMP: { |
300 | get ENABLED () { return config.get<boolean>('live.rtmp.enabled') }, | 304 | get ENABLED () { return config.get<boolean>('live.rtmp.enabled') }, |
301 | get PORT () { return config.get<number>('live.rtmp.port') }, | 305 | get PORT () { return config.get<number>('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' | |||
24 | 24 | ||
25 | // --------------------------------------------------------------------------- | 25 | // --------------------------------------------------------------------------- |
26 | 26 | ||
27 | const LAST_MIGRATION_VERSION = 685 | 27 | const LAST_MIGRATION_VERSION = 690 |
28 | 28 | ||
29 | // --------------------------------------------------------------------------- | 29 | // --------------------------------------------------------------------------- |
30 | 30 | ||
@@ -700,7 +700,10 @@ const RESUMABLE_UPLOAD_SESSION_LIFETIME = SCHEDULER_INTERVALS_MS.REMOVE_DANGLING | |||
700 | const VIDEO_LIVE = { | 700 | const VIDEO_LIVE = { |
701 | EXTENSION: '.ts', | 701 | EXTENSION: '.ts', |
702 | CLEANUP_DELAY: 1000 * 60 * 5, // 5 minutes | 702 | CLEANUP_DELAY: 1000 * 60 * 5, // 5 minutes |
703 | SEGMENT_TIME_SECONDS: 4, // 4 seconds | 703 | SEGMENT_TIME_SECONDS: { |
704 | DEFAULT_LATENCY: 4, // 4 seconds | ||
705 | SMALL_LATENCY: 2 // 2 seconds | ||
706 | }, | ||
704 | SEGMENTS_LIST_SIZE: 15, // 15 maximum segments in live playlist | 707 | SEGMENTS_LIST_SIZE: 15, // 15 maximum segments in live playlist |
705 | REPLAY_DIRECTORY: 'replay', | 708 | REPLAY_DIRECTORY: 'replay', |
706 | EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION: 4, | 709 | EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION: 4, |
@@ -842,7 +845,8 @@ if (isTestInstance() === true) { | |||
842 | PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000 | 845 | PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000 |
843 | 846 | ||
844 | VIDEO_LIVE.CLEANUP_DELAY = 5000 | 847 | VIDEO_LIVE.CLEANUP_DELAY = 5000 |
845 | VIDEO_LIVE.SEGMENT_TIME_SECONDS = 2 | 848 | VIDEO_LIVE.SEGMENT_TIME_SECONDS.DEFAULT_LATENCY = 2 |
849 | VIDEO_LIVE.SEGMENT_TIME_SECONDS.SMALL_LATENCY = 1 | ||
846 | VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION = 1 | 850 | VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION = 1 |
847 | } | 851 | } |
848 | 852 | ||
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 @@ | |||
1 | import { LiveVideoLatencyMode } from '@shared/models' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | |||
4 | async function up (utils: { | ||
5 | transaction: Sequelize.Transaction | ||
6 | queryInterface: Sequelize.QueryInterface | ||
7 | sequelize: Sequelize.Sequelize | ||
8 | db: any | ||
9 | }): Promise<void> { | ||
10 | await utils.queryInterface.addColumn('videoLive', 'latencyMode', { | ||
11 | type: Sequelize.INTEGER, | ||
12 | defaultValue: null, | ||
13 | allowNull: true | ||
14 | }, { transaction: utils.transaction }) | ||
15 | |||
16 | { | ||
17 | const query = `UPDATE "videoLive" SET "latencyMode" = ${LiveVideoLatencyMode.DEFAULT}` | ||
18 | await utils.sequelize.query(query, { type: Sequelize.QueryTypes.UPDATE, transaction: utils.transaction }) | ||
19 | } | ||
20 | |||
21 | await utils.queryInterface.changeColumn('videoLive', 'latencyMode', { | ||
22 | type: Sequelize.INTEGER, | ||
23 | defaultValue: null, | ||
24 | allowNull: false | ||
25 | }, { transaction: utils.transaction }) | ||
26 | } | ||
27 | |||
28 | function down () { | ||
29 | throw new Error('Not implemented.') | ||
30 | } | ||
31 | |||
32 | export { | ||
33 | up, | ||
34 | down | ||
35 | } | ||
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) | |||
151 | return { | 151 | return { |
152 | saveReplay: videoObject.liveSaveReplay, | 152 | saveReplay: videoObject.liveSaveReplay, |
153 | permanentLive: videoObject.permanentLive, | 153 | permanentLive: videoObject.permanentLive, |
154 | latencyMode: videoObject.latencyMode, | ||
154 | videoId: video.id | 155 | videoId: video.id |
155 | } | 156 | } |
156 | } | 157 | } |
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' | |||
5 | import { | 5 | import { |
6 | computeLowerResolutionsToTranscode, | 6 | computeLowerResolutionsToTranscode, |
7 | ffprobePromise, | 7 | ffprobePromise, |
8 | getLiveSegmentTime, | ||
8 | getVideoStreamBitrate, | 9 | getVideoStreamBitrate, |
9 | getVideoStreamFPS, | 10 | getVideoStreamDimensionsInfo, |
10 | getVideoStreamDimensionsInfo | 11 | getVideoStreamFPS |
11 | } from '@server/helpers/ffmpeg' | 12 | } from '@server/helpers/ffmpeg' |
12 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 13 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
13 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' | 14 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' |
@@ -353,7 +354,7 @@ class LiveManager { | |||
353 | .catch(err => logger.error('Cannot federate live video %s.', video.url, { err, ...localLTags })) | 354 | .catch(err => logger.error('Cannot federate live video %s.', video.url, { err, ...localLTags })) |
354 | 355 | ||
355 | PeerTubeSocket.Instance.sendVideoLiveNewState(video) | 356 | PeerTubeSocket.Instance.sendVideoLiveNewState(video) |
356 | }, VIDEO_LIVE.SEGMENT_TIME_SECONDS * 1000 * VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION) | 357 | }, getLiveSegmentTime(live.latencyMode) * 1000 * VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION) |
357 | } catch (err) { | 358 | } catch (err) { |
358 | logger.error('Cannot save/federate live video %d.', videoId, { err, ...localLTags }) | 359 | logger.error('Cannot save/federate live video %d.', videoId, { err, ...localLTags }) |
359 | } | 360 | } |
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 { | |||
125 | outPath, | 125 | outPath, |
126 | masterPlaylistName: this.streamingPlaylist.playlistFilename, | 126 | masterPlaylistName: this.streamingPlaylist.playlistFilename, |
127 | 127 | ||
128 | latencyMode: this.videoLive.latencyMode, | ||
129 | |||
128 | resolutions: this.allResolutions, | 130 | resolutions: this.allResolutions, |
129 | fps: this.fps, | 131 | fps: this.fps, |
130 | bitrate: this.bitrate, | 132 | bitrate: this.bitrate, |
@@ -133,7 +135,12 @@ class MuxingSession extends EventEmitter { | |||
133 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 135 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
134 | profile: CONFIG.LIVE.TRANSCODING.PROFILE | 136 | profile: CONFIG.LIVE.TRANSCODING.PROFILE |
135 | }) | 137 | }) |
136 | : getLiveMuxingCommand(this.inputUrl, outPath, this.streamingPlaylist.playlistFilename) | 138 | : getLiveMuxingCommand({ |
139 | inputUrl: this.inputUrl, | ||
140 | outPath, | ||
141 | masterPlaylistName: this.streamingPlaylist.playlistFilename, | ||
142 | latencyMode: this.videoLive.latencyMode | ||
143 | }) | ||
137 | 144 | ||
138 | logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags()) | 145 | logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags()) |
139 | 146 | ||
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 { | |||
137 | enabled: CONFIG.LIVE.ENABLED, | 137 | enabled: CONFIG.LIVE.ENABLED, |
138 | 138 | ||
139 | allowReplay: CONFIG.LIVE.ALLOW_REPLAY, | 139 | allowReplay: CONFIG.LIVE.ALLOW_REPLAY, |
140 | latencySetting: { | ||
141 | enabled: CONFIG.LIVE.LATENCY_SETTING.ENABLED | ||
142 | }, | ||
143 | |||
140 | maxDuration: CONFIG.LIVE.MAX_DURATION, | 144 | maxDuration: CONFIG.LIVE.MAX_DURATION, |
141 | maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES, | 145 | maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES, |
142 | maxUserLives: CONFIG.LIVE.MAX_USER_LIVES, | 146 | 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 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { body } from 'express-validator' | 2 | import { body } from 'express-validator' |
3 | import { isLiveLatencyModeValid } from '@server/helpers/custom-validators/video-lives' | ||
3 | import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' | 4 | import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' |
4 | import { isLocalLiveVideoAccepted } from '@server/lib/moderation' | 5 | import { isLocalLiveVideoAccepted } from '@server/lib/moderation' |
5 | import { Hooks } from '@server/lib/plugins/hooks' | 6 | import { Hooks } from '@server/lib/plugins/hooks' |
6 | import { VideoModel } from '@server/models/video/video' | 7 | import { VideoModel } from '@server/models/video/video' |
7 | import { VideoLiveModel } from '@server/models/video/video-live' | 8 | import { VideoLiveModel } from '@server/models/video/video-live' |
8 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoState } from '@shared/models' | 9 | import { |
9 | import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' | 10 | HttpStatusCode, |
11 | LiveVideoCreate, | ||
12 | LiveVideoLatencyMode, | ||
13 | LiveVideoUpdate, | ||
14 | ServerErrorCode, | ||
15 | UserRight, | ||
16 | VideoState | ||
17 | } from '@shared/models' | ||
18 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' | ||
10 | import { isVideoNameValid } from '../../../helpers/custom-validators/videos' | 19 | import { isVideoNameValid } from '../../../helpers/custom-validators/videos' |
11 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 20 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
12 | import { logger } from '../../../helpers/logger' | 21 | import { logger } from '../../../helpers/logger' |
@@ -67,6 +76,12 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
67 | .customSanitizer(toBooleanOrNull) | 76 | .customSanitizer(toBooleanOrNull) |
68 | .custom(isBooleanValid).withMessage('Should have a valid permanentLive attribute'), | 77 | .custom(isBooleanValid).withMessage('Should have a valid permanentLive attribute'), |
69 | 78 | ||
79 | body('latencyMode') | ||
80 | .optional() | ||
81 | .customSanitizer(toIntOrNull) | ||
82 | .custom(isLiveLatencyModeValid) | ||
83 | .withMessage('Should have a valid latency mode attribute'), | ||
84 | |||
70 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 85 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
71 | logger.debug('Checking videoLiveAddValidator parameters', { parameters: req.body }) | 86 | logger.debug('Checking videoLiveAddValidator parameters', { parameters: req.body }) |
72 | 87 | ||
@@ -82,7 +97,9 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
82 | }) | 97 | }) |
83 | } | 98 | } |
84 | 99 | ||
85 | if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) { | 100 | const body: LiveVideoCreate = req.body |
101 | |||
102 | if (hasValidSaveReplay(body) !== true) { | ||
86 | cleanUpReqFiles(req) | 103 | cleanUpReqFiles(req) |
87 | 104 | ||
88 | return res.fail({ | 105 | return res.fail({ |
@@ -92,14 +109,23 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
92 | }) | 109 | }) |
93 | } | 110 | } |
94 | 111 | ||
95 | if (req.body.permanentLive && req.body.saveReplay) { | 112 | if (hasValidLatencyMode(body) !== true) { |
113 | cleanUpReqFiles(req) | ||
114 | |||
115 | return res.fail({ | ||
116 | status: HttpStatusCode.FORBIDDEN_403, | ||
117 | message: 'Custom latency mode is not allowed by this instance' | ||
118 | }) | ||
119 | } | ||
120 | |||
121 | if (body.permanentLive && body.saveReplay) { | ||
96 | cleanUpReqFiles(req) | 122 | cleanUpReqFiles(req) |
97 | 123 | ||
98 | return res.fail({ message: 'Cannot set this live as permanent while saving its replay' }) | 124 | return res.fail({ message: 'Cannot set this live as permanent while saving its replay' }) |
99 | } | 125 | } |
100 | 126 | ||
101 | const user = res.locals.oauth.token.User | 127 | const user = res.locals.oauth.token.User |
102 | if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) | 128 | if (!await doesVideoChannelOfAccountExist(body.channelId, user, res)) return cleanUpReqFiles(req) |
103 | 129 | ||
104 | if (CONFIG.LIVE.MAX_INSTANCE_LIVES !== -1) { | 130 | if (CONFIG.LIVE.MAX_INSTANCE_LIVES !== -1) { |
105 | const totalInstanceLives = await VideoModel.countLocalLives() | 131 | const totalInstanceLives = await VideoModel.countLocalLives() |
@@ -141,19 +167,34 @@ const videoLiveUpdateValidator = [ | |||
141 | .customSanitizer(toBooleanOrNull) | 167 | .customSanitizer(toBooleanOrNull) |
142 | .custom(isBooleanValid).withMessage('Should have a valid saveReplay attribute'), | 168 | .custom(isBooleanValid).withMessage('Should have a valid saveReplay attribute'), |
143 | 169 | ||
170 | body('latencyMode') | ||
171 | .optional() | ||
172 | .customSanitizer(toIntOrNull) | ||
173 | .custom(isLiveLatencyModeValid) | ||
174 | .withMessage('Should have a valid latency mode attribute'), | ||
175 | |||
144 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 176 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
145 | logger.debug('Checking videoLiveUpdateValidator parameters', { parameters: req.body }) | 177 | logger.debug('Checking videoLiveUpdateValidator parameters', { parameters: req.body }) |
146 | 178 | ||
147 | if (areValidationErrors(req, res)) return | 179 | if (areValidationErrors(req, res)) return |
148 | 180 | ||
149 | if (req.body.permanentLive && req.body.saveReplay) { | 181 | const body: LiveVideoUpdate = req.body |
182 | |||
183 | if (body.permanentLive && body.saveReplay) { | ||
150 | return res.fail({ message: 'Cannot set this live as permanent while saving its replay' }) | 184 | return res.fail({ message: 'Cannot set this live as permanent while saving its replay' }) |
151 | } | 185 | } |
152 | 186 | ||
153 | if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) { | 187 | if (hasValidSaveReplay(body) !== true) { |
154 | return res.fail({ | 188 | return res.fail({ |
155 | status: HttpStatusCode.FORBIDDEN_403, | 189 | status: HttpStatusCode.FORBIDDEN_403, |
156 | message: 'Saving live replay is not allowed instance' | 190 | message: 'Saving live replay is not allowed by this instance' |
191 | }) | ||
192 | } | ||
193 | |||
194 | if (hasValidLatencyMode(body) !== true) { | ||
195 | return res.fail({ | ||
196 | status: HttpStatusCode.FORBIDDEN_403, | ||
197 | message: 'Custom latency mode is not allowed by this instance' | ||
157 | }) | 198 | }) |
158 | } | 199 | } |
159 | 200 | ||
@@ -203,3 +244,19 @@ async function isLiveVideoAccepted (req: express.Request, res: express.Response) | |||
203 | 244 | ||
204 | return true | 245 | return true |
205 | } | 246 | } |
247 | |||
248 | function hasValidSaveReplay (body: LiveVideoUpdate | LiveVideoCreate) { | ||
249 | if (CONFIG.LIVE.ALLOW_REPLAY !== true && body.saveReplay === true) return false | ||
250 | |||
251 | return true | ||
252 | } | ||
253 | |||
254 | function hasValidLatencyMode (body: LiveVideoUpdate | LiveVideoCreate) { | ||
255 | if ( | ||
256 | CONFIG.LIVE.LATENCY_SETTING.ENABLED !== true && | ||
257 | exists(body.latencyMode) && | ||
258 | body.latencyMode !== LiveVideoLatencyMode.DEFAULT | ||
259 | ) return false | ||
260 | |||
261 | return true | ||
262 | } | ||
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 { | |||
411 | views: video.views, | 411 | views: video.views, |
412 | sensitive: video.nsfw, | 412 | sensitive: video.nsfw, |
413 | waitTranscoding: video.waitTranscoding, | 413 | waitTranscoding: video.waitTranscoding, |
414 | isLiveBroadcast: video.isLive, | ||
415 | |||
416 | liveSaveReplay: video.isLive | ||
417 | ? video.VideoLive.saveReplay | ||
418 | : null, | ||
419 | |||
420 | permanentLive: video.isLive | ||
421 | ? video.VideoLive.permanentLive | ||
422 | : null, | ||
423 | 414 | ||
424 | state: video.state, | 415 | state: video.state, |
425 | commentsEnabled: video.commentsEnabled, | 416 | commentsEnabled: video.commentsEnabled, |
@@ -431,10 +422,13 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
431 | : null, | 422 | : null, |
432 | 423 | ||
433 | updated: video.updatedAt.toISOString(), | 424 | updated: video.updatedAt.toISOString(), |
425 | |||
434 | mediaType: 'text/markdown', | 426 | mediaType: 'text/markdown', |
435 | content: video.description, | 427 | content: video.description, |
436 | support: video.support, | 428 | support: video.support, |
429 | |||
437 | subtitleLanguage, | 430 | subtitleLanguage, |
431 | |||
438 | icon: icons.map(i => ({ | 432 | icon: icons.map(i => ({ |
439 | type: 'Image', | 433 | type: 'Image', |
440 | url: i.getFileUrl(video), | 434 | url: i.getFileUrl(video), |
@@ -442,11 +436,14 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
442 | width: i.width, | 436 | width: i.width, |
443 | height: i.height | 437 | height: i.height |
444 | })), | 438 | })), |
439 | |||
445 | url, | 440 | url, |
441 | |||
446 | likes: getLocalVideoLikesActivityPubUrl(video), | 442 | likes: getLocalVideoLikesActivityPubUrl(video), |
447 | dislikes: getLocalVideoDislikesActivityPubUrl(video), | 443 | dislikes: getLocalVideoDislikesActivityPubUrl(video), |
448 | shares: getLocalVideoSharesActivityPubUrl(video), | 444 | shares: getLocalVideoSharesActivityPubUrl(video), |
449 | comments: getLocalVideoCommentsActivityPubUrl(video), | 445 | comments: getLocalVideoCommentsActivityPubUrl(video), |
446 | |||
450 | attributedTo: [ | 447 | attributedTo: [ |
451 | { | 448 | { |
452 | type: 'Person', | 449 | type: 'Person', |
@@ -456,7 +453,9 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
456 | type: 'Group', | 453 | type: 'Group', |
457 | id: video.VideoChannel.Actor.url | 454 | id: video.VideoChannel.Actor.url |
458 | } | 455 | } |
459 | ] | 456 | ], |
457 | |||
458 | ...buildLiveAPAttributes(video) | ||
460 | } | 459 | } |
461 | } | 460 | } |
462 | 461 | ||
@@ -500,3 +499,23 @@ export { | |||
500 | getPrivacyLabel, | 499 | getPrivacyLabel, |
501 | getStateLabel | 500 | getStateLabel |
502 | } | 501 | } |
502 | |||
503 | // --------------------------------------------------------------------------- | ||
504 | |||
505 | function buildLiveAPAttributes (video: MVideoAP) { | ||
506 | if (!video.isLive) { | ||
507 | return { | ||
508 | isLiveBroadcast: false, | ||
509 | liveSaveReplay: null, | ||
510 | permanentLive: null, | ||
511 | latencyMode: null | ||
512 | } | ||
513 | } | ||
514 | |||
515 | return { | ||
516 | isLiveBroadcast: true, | ||
517 | liveSaveReplay: video.VideoLive.saveReplay, | ||
518 | permanentLive: video.VideoLive.permanentLive, | ||
519 | latencyMode: video.VideoLive.latencyMode | ||
520 | } | ||
521 | } | ||
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 { | |||
158 | 'streamKey', | 158 | 'streamKey', |
159 | 'saveReplay', | 159 | 'saveReplay', |
160 | 'permanentLive', | 160 | 'permanentLive', |
161 | 'latencyMode', | ||
161 | 'videoId', | 162 | 'videoId', |
162 | 'createdAt', | 163 | 'createdAt', |
163 | 'updatedAt' | 164 | '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 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { CONFIG } from '@server/initializers/config' | ||
2 | import { WEBSERVER } from '@server/initializers/constants' | 3 | import { WEBSERVER } from '@server/initializers/constants' |
3 | import { MVideoLive, MVideoLiveVideo } from '@server/types/models' | 4 | import { MVideoLive, MVideoLiveVideo } from '@server/types/models' |
5 | import { LiveVideo, LiveVideoLatencyMode, VideoState } from '@shared/models' | ||
4 | import { AttributesOnly } from '@shared/typescript-utils' | 6 | import { AttributesOnly } from '@shared/typescript-utils' |
5 | import { LiveVideo, VideoState } from '@shared/models' | ||
6 | import { VideoModel } from './video' | 7 | import { VideoModel } from './video' |
7 | import { VideoBlacklistModel } from './video-blacklist' | 8 | import { VideoBlacklistModel } from './video-blacklist' |
8 | import { CONFIG } from '@server/initializers/config' | ||
9 | 9 | ||
10 | @DefaultScope(() => ({ | 10 | @DefaultScope(() => ({ |
11 | include: [ | 11 | include: [ |
@@ -44,6 +44,10 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel> | |||
44 | @Column | 44 | @Column |
45 | permanentLive: boolean | 45 | permanentLive: boolean |
46 | 46 | ||
47 | @AllowNull(false) | ||
48 | @Column | ||
49 | latencyMode: LiveVideoLatencyMode | ||
50 | |||
47 | @CreatedAt | 51 | @CreatedAt |
48 | createdAt: Date | 52 | createdAt: Date |
49 | 53 | ||
@@ -113,7 +117,8 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel> | |||
113 | 117 | ||
114 | streamKey: this.streamKey, | 118 | streamKey: this.streamKey, |
115 | permanentLive: this.permanentLive, | 119 | permanentLive: this.permanentLive, |
116 | saveReplay: this.saveReplay | 120 | saveReplay: this.saveReplay, |
121 | latencyMode: this.latencyMode | ||
117 | } | 122 | } |
118 | } | 123 | } |
119 | } | 124 | } |
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 () { | |||
125 | enabled: true, | 125 | enabled: true, |
126 | 126 | ||
127 | allowReplay: false, | 127 | allowReplay: false, |
128 | latencySetting: { | ||
129 | enabled: false | ||
130 | }, | ||
128 | maxDuration: 30, | 131 | maxDuration: 30, |
129 | maxInstanceLives: -1, | 132 | maxInstanceLives: -1, |
130 | maxUserLives: 50, | 133 | 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 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { omit } from 'lodash' | 4 | import { omit } from 'lodash' |
5 | import { buildAbsoluteFixturePath } from '@shared/core-utils' | 5 | import { buildAbsoluteFixturePath } from '@shared/core-utils' |
6 | import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@shared/models' | 6 | import { HttpStatusCode, LiveVideoLatencyMode, VideoCreateResult, VideoPrivacy } from '@shared/models' |
7 | import { | 7 | import { |
8 | cleanupTests, | 8 | cleanupTests, |
9 | createSingleServer, | 9 | createSingleServer, |
@@ -38,6 +38,9 @@ describe('Test video lives API validator', function () { | |||
38 | newConfig: { | 38 | newConfig: { |
39 | live: { | 39 | live: { |
40 | enabled: true, | 40 | enabled: true, |
41 | latencySetting: { | ||
42 | enabled: false | ||
43 | }, | ||
41 | maxInstanceLives: 20, | 44 | maxInstanceLives: 20, |
42 | maxUserLives: 20, | 45 | maxUserLives: 20, |
43 | allowReplay: true | 46 | allowReplay: true |
@@ -81,7 +84,8 @@ describe('Test video lives API validator', function () { | |||
81 | privacy: VideoPrivacy.PUBLIC, | 84 | privacy: VideoPrivacy.PUBLIC, |
82 | channelId, | 85 | channelId, |
83 | saveReplay: false, | 86 | saveReplay: false, |
84 | permanentLive: false | 87 | permanentLive: false, |
88 | latencyMode: LiveVideoLatencyMode.DEFAULT | ||
85 | } | 89 | } |
86 | }) | 90 | }) |
87 | 91 | ||
@@ -214,6 +218,18 @@ describe('Test video lives API validator', function () { | |||
214 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 218 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
215 | }) | 219 | }) |
216 | 220 | ||
221 | it('Should fail with bad latency setting', async function () { | ||
222 | const fields = { ...baseCorrectParams, latencyMode: 42 } | ||
223 | |||
224 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
225 | }) | ||
226 | |||
227 | it('Should fail to set latency if the server does not allow it', async function () { | ||
228 | const fields = { ...baseCorrectParams, latencyMode: LiveVideoLatencyMode.HIGH_LATENCY } | ||
229 | |||
230 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
231 | }) | ||
232 | |||
217 | it('Should succeed with the correct parameters', async function () { | 233 | it('Should succeed with the correct parameters', async function () { |
218 | this.timeout(30000) | 234 | this.timeout(30000) |
219 | 235 | ||
@@ -393,6 +409,18 @@ describe('Test video lives API validator', function () { | |||
393 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 409 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
394 | }) | 410 | }) |
395 | 411 | ||
412 | it('Should fail with bad latency setting', async function () { | ||
413 | const fields = { latencyMode: 42 } | ||
414 | |||
415 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
416 | }) | ||
417 | |||
418 | it('Should fail to set latency if the server does not allow it', async function () { | ||
419 | const fields = { latencyMode: LiveVideoLatencyMode.HIGH_LATENCY } | ||
420 | |||
421 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
422 | }) | ||
423 | |||
396 | it('Should succeed with the correct params', async function () { | 424 | it('Should succeed with the correct params', async function () { |
397 | await command.update({ videoId: video.id, fields: { saveReplay: false } }) | 425 | await command.update({ videoId: video.id, fields: { saveReplay: false } }) |
398 | await command.update({ videoId: video.uuid, fields: { saveReplay: false } }) | 426 | 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 { | |||
10 | HttpStatusCode, | 10 | HttpStatusCode, |
11 | LiveVideo, | 11 | LiveVideo, |
12 | LiveVideoCreate, | 12 | LiveVideoCreate, |
13 | LiveVideoLatencyMode, | ||
13 | VideoDetails, | 14 | VideoDetails, |
14 | VideoPrivacy, | 15 | VideoPrivacy, |
15 | VideoState, | 16 | VideoState, |
@@ -52,6 +53,9 @@ describe('Test live', function () { | |||
52 | live: { | 53 | live: { |
53 | enabled: true, | 54 | enabled: true, |
54 | allowReplay: true, | 55 | allowReplay: true, |
56 | latencySetting: { | ||
57 | enabled: true | ||
58 | }, | ||
55 | transcoding: { | 59 | transcoding: { |
56 | enabled: false | 60 | enabled: false |
57 | } | 61 | } |
@@ -85,6 +89,7 @@ describe('Test live', function () { | |||
85 | commentsEnabled: false, | 89 | commentsEnabled: false, |
86 | downloadEnabled: false, | 90 | downloadEnabled: false, |
87 | saveReplay: true, | 91 | saveReplay: true, |
92 | latencyMode: LiveVideoLatencyMode.SMALL_LATENCY, | ||
88 | privacy: VideoPrivacy.PUBLIC, | 93 | privacy: VideoPrivacy.PUBLIC, |
89 | previewfile: 'video_short1-preview.webm.jpg', | 94 | previewfile: 'video_short1-preview.webm.jpg', |
90 | thumbnailfile: 'video_short1.webm.jpg' | 95 | thumbnailfile: 'video_short1.webm.jpg' |
@@ -131,6 +136,7 @@ describe('Test live', function () { | |||
131 | } | 136 | } |
132 | 137 | ||
133 | expect(live.saveReplay).to.be.true | 138 | expect(live.saveReplay).to.be.true |
139 | expect(live.latencyMode).to.equal(LiveVideoLatencyMode.SMALL_LATENCY) | ||
134 | } | 140 | } |
135 | }) | 141 | }) |
136 | 142 | ||
@@ -175,7 +181,7 @@ describe('Test live', function () { | |||
175 | it('Should update the live', async function () { | 181 | it('Should update the live', async function () { |
176 | this.timeout(10000) | 182 | this.timeout(10000) |
177 | 183 | ||
178 | await commands[0].update({ videoId: liveVideoUUID, fields: { saveReplay: false } }) | 184 | await commands[0].update({ videoId: liveVideoUUID, fields: { saveReplay: false, latencyMode: LiveVideoLatencyMode.DEFAULT } }) |
179 | await waitJobs(servers) | 185 | await waitJobs(servers) |
180 | }) | 186 | }) |
181 | 187 | ||
@@ -192,6 +198,7 @@ describe('Test live', function () { | |||
192 | } | 198 | } |
193 | 199 | ||
194 | expect(live.saveReplay).to.be.false | 200 | expect(live.saveReplay).to.be.false |
201 | expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT) | ||
195 | } | 202 | } |
196 | }) | 203 | }) |
197 | 204 | ||
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) { | |||
82 | 82 | ||
83 | expect(data.live.enabled).to.be.false | 83 | expect(data.live.enabled).to.be.false |
84 | expect(data.live.allowReplay).to.be.false | 84 | expect(data.live.allowReplay).to.be.false |
85 | expect(data.live.latencySetting.enabled).to.be.true | ||
85 | expect(data.live.maxDuration).to.equal(-1) | 86 | expect(data.live.maxDuration).to.equal(-1) |
86 | expect(data.live.maxInstanceLives).to.equal(20) | 87 | expect(data.live.maxInstanceLives).to.equal(20) |
87 | expect(data.live.maxUserLives).to.equal(3) | 88 | expect(data.live.maxUserLives).to.equal(3) |
@@ -185,6 +186,7 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
185 | 186 | ||
186 | expect(data.live.enabled).to.be.true | 187 | expect(data.live.enabled).to.be.true |
187 | expect(data.live.allowReplay).to.be.true | 188 | expect(data.live.allowReplay).to.be.true |
189 | expect(data.live.latencySetting.enabled).to.be.false | ||
188 | expect(data.live.maxDuration).to.equal(5000) | 190 | expect(data.live.maxDuration).to.equal(5000) |
189 | expect(data.live.maxInstanceLives).to.equal(-1) | 191 | expect(data.live.maxInstanceLives).to.equal(-1) |
190 | expect(data.live.maxUserLives).to.equal(10) | 192 | expect(data.live.maxUserLives).to.equal(10) |
@@ -326,6 +328,9 @@ const newCustomConfig: CustomConfig = { | |||
326 | live: { | 328 | live: { |
327 | enabled: true, | 329 | enabled: true, |
328 | allowReplay: true, | 330 | allowReplay: true, |
331 | latencySetting: { | ||
332 | enabled: false | ||
333 | }, | ||
329 | maxDuration: 5000, | 334 | maxDuration: 5000, |
330 | maxInstanceLives: -1, | 335 | maxInstanceLives: -1, |
331 | maxUserLives: 10, | 336 | 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 { | |||
5 | ActivityTagObject, | 5 | ActivityTagObject, |
6 | ActivityUrlObject | 6 | ActivityUrlObject |
7 | } from './common-objects' | 7 | } from './common-objects' |
8 | import { VideoState } from '../../videos' | 8 | import { LiveVideoLatencyMode, VideoState } from '../../videos' |
9 | 9 | ||
10 | export interface VideoObject { | 10 | export interface VideoObject { |
11 | type: 'Video' | 11 | type: 'Video' |
@@ -25,6 +25,7 @@ export interface VideoObject { | |||
25 | isLiveBroadcast: boolean | 25 | isLiveBroadcast: boolean |
26 | liveSaveReplay: boolean | 26 | liveSaveReplay: boolean |
27 | permanentLive: boolean | 27 | permanentLive: boolean |
28 | latencyMode: LiveVideoLatencyMode | ||
28 | 29 | ||
29 | commentsEnabled: boolean | 30 | commentsEnabled: boolean |
30 | downloadEnabled: boolean | 31 | 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 { | |||
131 | 131 | ||
132 | allowReplay: boolean | 132 | allowReplay: boolean |
133 | 133 | ||
134 | latencySetting: { | ||
135 | enabled: boolean | ||
136 | } | ||
137 | |||
134 | maxDuration: number | 138 | maxDuration: number |
135 | maxInstanceLives: number | 139 | maxInstanceLives: number |
136 | maxUserLives: number | 140 | 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 { | |||
149 | live: { | 149 | live: { |
150 | enabled: boolean | 150 | enabled: boolean |
151 | 151 | ||
152 | allowReplay: boolean | ||
153 | latencySetting: { | ||
154 | enabled: boolean | ||
155 | } | ||
156 | |||
152 | maxDuration: number | 157 | maxDuration: number |
153 | maxInstanceLives: number | 158 | maxInstanceLives: number |
154 | maxUserLives: number | 159 | maxUserLives: number |
155 | allowReplay: boolean | ||
156 | 160 | ||
157 | transcoding: { | 161 | transcoding: { |
158 | enabled: boolean | 162 | 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 @@ | |||
1 | export * from './live-video-create.model' | 1 | export * from './live-video-create.model' |
2 | export * from './live-video-event-payload.model' | 2 | export * from './live-video-event-payload.model' |
3 | export * from './live-video-event.type' | 3 | export * from './live-video-event.type' |
4 | export * from './live-video-latency-mode.enum' | ||
4 | export * from './live-video-update.model' | 5 | export * from './live-video-update.model' |
5 | export * from './live-video.model' | 6 | 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 @@ | |||
1 | import { LiveVideoLatencyMode } from '.' | ||
1 | import { VideoCreate } from '../video-create.model' | 2 | import { VideoCreate } from '../video-create.model' |
2 | 3 | ||
3 | export interface LiveVideoCreate extends VideoCreate { | 4 | export interface LiveVideoCreate extends VideoCreate { |
4 | saveReplay?: boolean | 5 | saveReplay?: boolean |
5 | permanentLive?: boolean | 6 | permanentLive?: boolean |
7 | latencyMode?: LiveVideoLatencyMode | ||
6 | } | 8 | } |
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 @@ | |||
1 | export const enum LiveVideoLatencyMode { | ||
2 | DEFAULT = 1, | ||
3 | HIGH_LATENCY = 2, | ||
4 | SMALL_LATENCY = 3 | ||
5 | } | ||
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 @@ | |||
1 | import { LiveVideoLatencyMode } from './live-video-latency-mode.enum' | ||
2 | |||
1 | export interface LiveVideoUpdate { | 3 | export interface LiveVideoUpdate { |
2 | permanentLive?: boolean | 4 | permanentLive?: boolean |
3 | saveReplay?: boolean | 5 | saveReplay?: boolean |
6 | latencyMode?: LiveVideoLatencyMode | ||
4 | } | 7 | } |
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 @@ | |||
1 | import { LiveVideoLatencyMode } from './live-video-latency-mode.enum' | ||
2 | |||
1 | export interface LiveVideo { | 3 | export interface LiveVideo { |
2 | rtmpUrl: string | 4 | rtmpUrl: string |
3 | rtmpsUrl: string | 5 | rtmpsUrl: string |
4 | 6 | ||
5 | streamKey: string | 7 | streamKey: string |
8 | |||
6 | saveReplay: boolean | 9 | saveReplay: boolean |
7 | permanentLive: boolean | 10 | permanentLive: boolean |
11 | latencyMode: LiveVideoLatencyMode | ||
8 | } | 12 | } |
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 { | |||
292 | live: { | 292 | live: { |
293 | enabled: true, | 293 | enabled: true, |
294 | allowReplay: false, | 294 | allowReplay: false, |
295 | latencySetting: { | ||
296 | enabled: false | ||
297 | }, | ||
295 | maxDuration: -1, | 298 | maxDuration: -1, |
296 | maxInstanceLives: -1, | 299 | maxInstanceLives: -1, |
297 | maxUserLives: 50, | 300 | 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: | |||
2295 | permanentLive: | 2295 | permanentLive: |
2296 | description: User can stream multiple times in a permanent live | 2296 | description: User can stream multiple times in a permanent live |
2297 | type: boolean | 2297 | type: boolean |
2298 | latencyMode: | ||
2299 | description: User can select live latency mode if enabled by the instance | ||
2300 | $ref: '#/components/schemas/LiveVideoLatencyMode' | ||
2298 | thumbnailfile: | 2301 | thumbnailfile: |
2299 | description: Live video/replay thumbnail file | 2302 | description: Live video/replay thumbnail file |
2300 | type: string | 2303 | type: string |
@@ -5291,6 +5294,14 @@ components: | |||
5291 | description: 'Admin flags for the user (None = `0`, Bypass video blocklist = `1`)' | 5294 | description: 'Admin flags for the user (None = `0`, Bypass video blocklist = `1`)' |
5292 | example: 1 | 5295 | example: 1 |
5293 | 5296 | ||
5297 | LiveVideoLatencyMode: | ||
5298 | type: integer | ||
5299 | enum: | ||
5300 | - 1 | ||
5301 | - 2 | ||
5302 | - 3 | ||
5303 | description: 'The live latency mode (Default = `1`, HIght latency = `2`, Small Latency = `3`)' | ||
5304 | |||
5294 | VideoStateConstant: | 5305 | VideoStateConstant: |
5295 | properties: | 5306 | properties: |
5296 | id: | 5307 | id: |
@@ -7482,6 +7493,9 @@ components: | |||
7482 | permanentLive: | 7493 | permanentLive: |
7483 | description: User can stream multiple times in a permanent live | 7494 | description: User can stream multiple times in a permanent live |
7484 | type: boolean | 7495 | type: boolean |
7496 | latencyMode: | ||
7497 | description: User can select live latency mode if enabled by the instance | ||
7498 | $ref: '#/components/schemas/LiveVideoLatencyMode' | ||
7485 | 7499 | ||
7486 | LiveVideoResponse: | 7500 | LiveVideoResponse: |
7487 | properties: | 7501 | properties: |
@@ -7497,8 +7511,9 @@ components: | |||
7497 | permanentLive: | 7511 | permanentLive: |
7498 | description: User can stream multiple times in a permanent live | 7512 | description: User can stream multiple times in a permanent live |
7499 | type: boolean | 7513 | type: boolean |
7500 | 7514 | latencyMode: | |
7501 | 7515 | description: User can select live latency mode if enabled by the instance | |
7516 | $ref: '#/components/schemas/LiveVideoLatencyMode' | ||
7502 | 7517 | ||
7503 | callbacks: | 7518 | callbacks: |
7504 | searchIndex: | 7519 | searchIndex: |