Merge remote-tracking branch 'weblate/develop' into develop
This commit is contained in:
commit
4495806f2f
|
@ -189,6 +189,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
maxInstanceLives: MAX_INSTANCE_LIVES_VALIDATOR,
|
||||
maxUserLives: MAX_USER_LIVES_VALIDATOR,
|
||||
allowReplay: null,
|
||||
latencySetting: {
|
||||
enabled: null
|
||||
},
|
||||
|
||||
transcoding: {
|
||||
enabled: null,
|
||||
|
|
|
@ -36,6 +36,18 @@
|
|||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="latencySetting" [ngClass]="getDisabledLiveClass()">
|
||||
<my-peertube-checkbox
|
||||
inputName="liveLatencySettingEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow your users to change live latency"
|
||||
>
|
||||
<ng-container ngProjectAs="description" i18n>
|
||||
Small latency disables P2P and high latency can increase P2P ratio
|
||||
</ng-container>
|
||||
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
||||
<label i18n for="liveMaxInstanceLives">
|
||||
Max simultaneous lives created on your instance <span class="text-muted">(-1 for "unlimited")</span>
|
||||
|
|
|
@ -289,6 +289,17 @@
|
|||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group" *ngIf="isLatencyModeEnabled()">
|
||||
<label i18n for="latencyMode">Latency mode</label>
|
||||
<my-select-options
|
||||
labelForId="latencyMode" [items]="latencyModes" formControlName="latencyMode" [clearable]="true"
|
||||
></my-select-options>
|
||||
|
||||
<div *ngIf="formErrors.latencyMode" class="form-error">
|
||||
{{ formErrors.latencyMode }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { forkJoin } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { SelectChannelItem } from 'src/types/select-options-item.model'
|
||||
import { SelectChannelItem, SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
|
||||
import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms'
|
||||
import { HooksService, PluginService, ServerService } from '@app/core'
|
||||
|
@ -26,6 +26,7 @@ import { PluginInfo } from '@root-helpers/plugins-manager'
|
|||
import {
|
||||
HTMLServerConfig,
|
||||
LiveVideo,
|
||||
LiveVideoLatencyMode,
|
||||
RegisterClientFormFieldOptions,
|
||||
RegisterClientVideoFieldOptions,
|
||||
VideoConstant,
|
||||
|
@ -78,6 +79,23 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
|||
videoCategories: VideoConstant<number>[] = []
|
||||
videoLicences: VideoConstant<number>[] = []
|
||||
videoLanguages: VideoLanguages[] = []
|
||||
latencyModes: SelectOptionsItem[] = [
|
||||
{
|
||||
id: LiveVideoLatencyMode.SMALL_LATENCY,
|
||||
label: $localize`Small latency`,
|
||||
description: $localize`Reduce latency to ~15s disabling P2P`
|
||||
},
|
||||
{
|
||||
id: LiveVideoLatencyMode.DEFAULT,
|
||||
label: $localize`Default`,
|
||||
description: $localize`Average latency of 30s`
|
||||
},
|
||||
{
|
||||
id: LiveVideoLatencyMode.HIGH_LATENCY,
|
||||
label: $localize`High latency`,
|
||||
description: $localize`Average latency of 60s increasing P2P ratio`
|
||||
}
|
||||
]
|
||||
|
||||
pluginDataFormGroup: FormGroup
|
||||
|
||||
|
@ -141,6 +159,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
|||
originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR,
|
||||
liveStreamKey: null,
|
||||
permanentLive: null,
|
||||
latencyMode: null,
|
||||
saveReplay: null
|
||||
}
|
||||
|
||||
|
@ -273,6 +292,10 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
|||
return this.form.value['permanentLive'] === true
|
||||
}
|
||||
|
||||
isLatencyModeEnabled () {
|
||||
return this.serverConfig.live.latencySetting.enabled
|
||||
}
|
||||
|
||||
isPluginFieldHidden (pluginField: PluginField) {
|
||||
if (typeof pluginField.commonOptions.hidden !== 'function') return false
|
||||
|
||||
|
|
|
@ -64,6 +64,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||
if (this.liveVideo) {
|
||||
this.form.patchValue({
|
||||
saveReplay: this.liveVideo.saveReplay,
|
||||
latencyMode: this.liveVideo.latencyMode,
|
||||
permanentLive: this.liveVideo.permanentLive
|
||||
})
|
||||
}
|
||||
|
@ -127,7 +128,8 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||
|
||||
const liveVideoUpdate: LiveVideoUpdate = {
|
||||
saveReplay: !!this.form.value.saveReplay,
|
||||
permanentLive: !!this.form.value.permanentLive
|
||||
permanentLive: !!this.form.value.permanentLive,
|
||||
latencyMode: this.form.value.latencyMode
|
||||
}
|
||||
|
||||
// Don't update live attributes if they did not change
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Hotkey, HotkeysService } from 'angular2-hotkeys'
|
||||
import { forkJoin, Subscription } from 'rxjs'
|
||||
import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs'
|
||||
import { isP2PEnabled } from 'src/assets/player/utils'
|
||||
import { PlatformLocation } from '@angular/common'
|
||||
import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||
|
@ -22,11 +22,13 @@ import { HooksService } from '@app/core/plugins/hooks.service'
|
|||
import { isXPercentInViewport, scrollToTop } from '@app/helpers'
|
||||
import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main'
|
||||
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live'
|
||||
import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
|
||||
import { timeToInt } from '@shared/core-utils'
|
||||
import {
|
||||
HTMLServerConfig,
|
||||
HttpStatusCode,
|
||||
LiveVideo,
|
||||
PeerTubeProblemDocument,
|
||||
ServerErrorCode,
|
||||
VideoCaption,
|
||||
|
@ -63,6 +65,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
|
||||
video: VideoDetails = null
|
||||
videoCaptions: VideoCaption[] = []
|
||||
liveVideo: LiveVideo
|
||||
|
||||
playlistPosition: number
|
||||
playlist: VideoPlaylist = null
|
||||
|
@ -89,6 +92,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
private router: Router,
|
||||
private videoService: VideoService,
|
||||
private playlistService: VideoPlaylistService,
|
||||
private liveVideoService: LiveVideoService,
|
||||
private confirmService: ConfirmService,
|
||||
private metaService: MetaService,
|
||||
private authService: AuthService,
|
||||
|
@ -239,12 +243,21 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
'filter:api.video-watch.video.get.result'
|
||||
)
|
||||
|
||||
const videoAndLiveObs: Observable<{ video: VideoDetails, live?: LiveVideo }> = videoObs.pipe(
|
||||
switchMap(video => {
|
||||
if (!video.isLive) return of({ video })
|
||||
|
||||
return this.liveVideoService.getVideoLive(video.uuid)
|
||||
.pipe(map(live => ({ live, video })))
|
||||
})
|
||||
)
|
||||
|
||||
forkJoin([
|
||||
videoObs,
|
||||
videoAndLiveObs,
|
||||
this.videoCaptionService.listCaptions(videoId),
|
||||
this.userService.getAnonymousOrLoggedUser()
|
||||
]).subscribe({
|
||||
next: ([ video, captionsResult, loggedInOrAnonymousUser ]) => {
|
||||
next: ([ { video, live }, captionsResult, loggedInOrAnonymousUser ]) => {
|
||||
const queryParams = this.route.snapshot.queryParams
|
||||
|
||||
const urlOptions = {
|
||||
|
@ -261,7 +274,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
peertubeLink: false
|
||||
}
|
||||
|
||||
this.onVideoFetched({ video, videoCaptions: captionsResult.data, loggedInOrAnonymousUser, urlOptions })
|
||||
this.onVideoFetched({ video, live, videoCaptions: captionsResult.data, loggedInOrAnonymousUser, urlOptions })
|
||||
.catch(err => this.handleGlobalError(err))
|
||||
},
|
||||
|
||||
|
@ -330,16 +343,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
|
||||
private async onVideoFetched (options: {
|
||||
video: VideoDetails
|
||||
live: LiveVideo
|
||||
videoCaptions: VideoCaption[]
|
||||
urlOptions: URLOptions
|
||||
loggedInOrAnonymousUser: User
|
||||
}) {
|
||||
const { video, videoCaptions, urlOptions, loggedInOrAnonymousUser } = options
|
||||
const { video, live, videoCaptions, urlOptions, loggedInOrAnonymousUser } = options
|
||||
|
||||
this.subscribeToLiveEventsIfNeeded(this.video, video)
|
||||
|
||||
this.video = video
|
||||
this.videoCaptions = videoCaptions
|
||||
this.liveVideo = live
|
||||
|
||||
// Re init attributes
|
||||
this.playerPlaceholderImgSrc = undefined
|
||||
|
@ -387,6 +402,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
const params = {
|
||||
video: this.video,
|
||||
videoCaptions: this.videoCaptions,
|
||||
liveVideo: this.liveVideo,
|
||||
urlOptions,
|
||||
loggedInOrAnonymousUser,
|
||||
user: this.user
|
||||
|
@ -532,12 +548,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
|
||||
private buildPlayerManagerOptions (params: {
|
||||
video: VideoDetails
|
||||
liveVideo: LiveVideo
|
||||
videoCaptions: VideoCaption[]
|
||||
urlOptions: CustomizationOptions & { playerMode: PlayerMode }
|
||||
loggedInOrAnonymousUser: User
|
||||
user?: AuthUser
|
||||
}) {
|
||||
const { video, videoCaptions, urlOptions, loggedInOrAnonymousUser, user } = params
|
||||
const { video, liveVideo, videoCaptions, urlOptions, loggedInOrAnonymousUser, user } = params
|
||||
|
||||
const getStartTime = () => {
|
||||
const byUrl = urlOptions.startTime !== undefined
|
||||
|
@ -562,6 +579,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
src: environment.apiUrl + c.captionPath
|
||||
}))
|
||||
|
||||
const liveOptions = video.isLive
|
||||
? { latencyMode: liveVideo.latencyMode }
|
||||
: undefined
|
||||
|
||||
const options: PeertubePlayerManagerOptions = {
|
||||
common: {
|
||||
autoplay: this.isAutoplay(),
|
||||
|
@ -597,6 +618,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
embedTitle: video.name,
|
||||
|
||||
isLive: video.isLive,
|
||||
liveOptions,
|
||||
|
||||
language: this.localeId,
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import videojs from 'video.js'
|
||||
import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core'
|
||||
import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
|
||||
import { PluginsManager } from '@root-helpers/plugins-manager'
|
||||
import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
|
||||
import { isDefaultLocale } from '@shared/core-utils/i18n'
|
||||
import { VideoFile } from '@shared/models'
|
||||
import { LiveVideoLatencyMode, VideoFile } from '@shared/models'
|
||||
import { copyToClipboard } from '../../root-helpers/utils'
|
||||
import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
|
||||
import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
|
||||
|
@ -19,7 +20,6 @@ import {
|
|||
VideoJSPluginOptions
|
||||
} from './peertube-videojs-typings'
|
||||
import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils'
|
||||
import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core'
|
||||
|
||||
export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
|
||||
|
||||
|
@ -76,6 +76,9 @@ export interface CommonOptions extends CustomizationOptions {
|
|||
embedTitle: string
|
||||
|
||||
isLive: boolean
|
||||
liveOptions?: {
|
||||
latencyMode: LiveVideoLatencyMode
|
||||
}
|
||||
|
||||
language?: string
|
||||
|
||||
|
@ -250,21 +253,8 @@ export class PeertubePlayerOptionsBuilder {
|
|||
.filter(t => t.startsWith('ws'))
|
||||
|
||||
const specificLiveOrVODOptions = this.options.common.isLive
|
||||
? { // Live
|
||||
requiredSegmentsPriority: 1
|
||||
}
|
||||
: { // VOD
|
||||
requiredSegmentsPriority: 3,
|
||||
|
||||
cachedSegmentExpiration: 86400000,
|
||||
cachedSegmentsCount: 100,
|
||||
|
||||
httpDownloadMaxPriority: 9,
|
||||
httpDownloadProbability: 0.06,
|
||||
httpDownloadProbabilitySkipIfNoPeers: true,
|
||||
|
||||
p2pDownloadMaxPriority: 50
|
||||
}
|
||||
? this.getP2PMediaLoaderLiveOptions()
|
||||
: this.getP2PMediaLoaderVODOptions()
|
||||
|
||||
return {
|
||||
trackerAnnounce,
|
||||
|
@ -283,13 +273,57 @@ export class PeertubePlayerOptionsBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
private getP2PMediaLoaderLiveOptions (): Partial<HybridLoaderSettings> {
|
||||
const base = {
|
||||
requiredSegmentsPriority: 1
|
||||
}
|
||||
|
||||
const latencyMode = this.options.common.liveOptions.latencyMode
|
||||
|
||||
switch (latencyMode) {
|
||||
case LiveVideoLatencyMode.SMALL_LATENCY:
|
||||
return {
|
||||
...base,
|
||||
|
||||
useP2P: false,
|
||||
httpDownloadProbability: 1
|
||||
}
|
||||
|
||||
case LiveVideoLatencyMode.HIGH_LATENCY:
|
||||
return base
|
||||
|
||||
default:
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
private getP2PMediaLoaderVODOptions (): Partial<HybridLoaderSettings> {
|
||||
return {
|
||||
requiredSegmentsPriority: 3,
|
||||
|
||||
cachedSegmentExpiration: 86400000,
|
||||
cachedSegmentsCount: 100,
|
||||
|
||||
httpDownloadMaxPriority: 9,
|
||||
httpDownloadProbability: 0.06,
|
||||
httpDownloadProbabilitySkipIfNoPeers: true,
|
||||
|
||||
p2pDownloadMaxPriority: 50
|
||||
}
|
||||
}
|
||||
|
||||
private getHLSOptions (p2pMediaLoaderConfig: HlsJsEngineSettings) {
|
||||
const specificLiveOrVODOptions = this.options.common.isLive
|
||||
? this.getHLSLiveOptions()
|
||||
: this.getHLSVODOptions()
|
||||
|
||||
const base = {
|
||||
capLevelToPlayerSize: true,
|
||||
autoStartLoad: false,
|
||||
liveSyncDurationCount: 5,
|
||||
|
||||
loader: new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass()
|
||||
loader: new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass(),
|
||||
|
||||
...specificLiveOrVODOptions
|
||||
}
|
||||
|
||||
const averageBandwidth = getAverageBandwidthInStore()
|
||||
|
@ -305,6 +339,33 @@ export class PeertubePlayerOptionsBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
private getHLSLiveOptions () {
|
||||
const latencyMode = this.options.common.liveOptions.latencyMode
|
||||
|
||||
switch (latencyMode) {
|
||||
case LiveVideoLatencyMode.SMALL_LATENCY:
|
||||
return {
|
||||
liveSyncDurationCount: 2
|
||||
}
|
||||
|
||||
case LiveVideoLatencyMode.HIGH_LATENCY:
|
||||
return {
|
||||
liveSyncDurationCount: 10
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
liveSyncDurationCount: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getHLSVODOptions () {
|
||||
return {
|
||||
liveSyncDurationCount: 5
|
||||
}
|
||||
}
|
||||
|
||||
private addWebTorrentOptions (plugins: VideoJSPluginOptions, alreadyPlayed: boolean) {
|
||||
const commonOptions = this.options.common
|
||||
const webtorrentOptions = this.options.webtorrent
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -6,6 +6,7 @@ import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
|
|||
import {
|
||||
HTMLServerConfig,
|
||||
HttpStatusCode,
|
||||
LiveVideo,
|
||||
OAuth2ErrorCode,
|
||||
ResultList,
|
||||
UserRefreshToken,
|
||||
|
@ -94,6 +95,10 @@ export class PeerTubeEmbed {
|
|||
return window.location.origin + '/api/v1/videos/' + id
|
||||
}
|
||||
|
||||
getLiveUrl (videoId: string) {
|
||||
return window.location.origin + '/api/v1/videos/live/' + videoId
|
||||
}
|
||||
|
||||
refreshFetch (url: string, options?: RequestInit) {
|
||||
return fetch(url, options)
|
||||
.then((res: Response) => {
|
||||
|
@ -166,6 +171,12 @@ export class PeerTubeEmbed {
|
|||
return this.refreshFetch(this.getVideoUrl(videoId) + '/captions', { headers: this.headers })
|
||||
}
|
||||
|
||||
loadWithLive (video: VideoDetails) {
|
||||
return this.refreshFetch(this.getLiveUrl(video.uuid), { headers: this.headers })
|
||||
.then(res => res.json())
|
||||
.then((live: LiveVideo) => ({ video, live }))
|
||||
}
|
||||
|
||||
loadPlaylistInfo (playlistId: string): Promise<Response> {
|
||||
return this.refreshFetch(this.getPlaylistUrl(playlistId), { headers: this.headers })
|
||||
}
|
||||
|
@ -475,13 +486,15 @@ export class PeerTubeEmbed {
|
|||
.then(res => res.json())
|
||||
}
|
||||
|
||||
const videoInfoPromise = videoResponse.json()
|
||||
const videoInfoPromise: Promise<{ video: VideoDetails, live?: LiveVideo }> = videoResponse.json()
|
||||
.then((videoInfo: VideoDetails) => {
|
||||
this.loadParams(videoInfo)
|
||||
|
||||
if (!alreadyHadPlayer && !this.autoplay) this.loadPlaceholder(videoInfo)
|
||||
if (!alreadyHadPlayer && !this.autoplay) this.buildPlaceholder(videoInfo)
|
||||
|
||||
return videoInfo
|
||||
if (!videoInfo.isLive) return { video: videoInfo }
|
||||
|
||||
return this.loadWithLive(videoInfo)
|
||||
})
|
||||
|
||||
const [ videoInfoTmp, serverTranslations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
|
||||
|
@ -493,11 +506,15 @@ export class PeerTubeEmbed {
|
|||
|
||||
await this.loadPlugins(serverTranslations)
|
||||
|
||||
const videoInfo: VideoDetails = videoInfoTmp
|
||||
const { video: videoInfo, live } = videoInfoTmp
|
||||
|
||||
const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
|
||||
const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse)
|
||||
|
||||
const liveOptions = videoInfo.isLive
|
||||
? { latencyMode: live.latencyMode }
|
||||
: undefined
|
||||
|
||||
const playlistPlugin = this.currentPlaylistElement
|
||||
? {
|
||||
elements: this.playlistElements,
|
||||
|
@ -545,6 +562,7 @@ export class PeerTubeEmbed {
|
|||
videoUUID: videoInfo.uuid,
|
||||
|
||||
isLive: videoInfo.isLive,
|
||||
liveOptions,
|
||||
|
||||
playerElement: this.playerElement,
|
||||
onPlayerElementChange: (element: HTMLVideoElement) => {
|
||||
|
@ -726,7 +744,7 @@ export class PeerTubeEmbed {
|
|||
return []
|
||||
}
|
||||
|
||||
private loadPlaceholder (video: VideoDetails) {
|
||||
private buildPlaceholder (video: VideoDetails) {
|
||||
const placeholder = this.getPlaceholderElement()
|
||||
|
||||
const url = window.location.origin + video.previewPath
|
||||
|
|
|
@ -392,6 +392,12 @@ live:
|
|||
# /!\ transcoding.enabled (and not live.transcoding.enabled) has to be true to create a replay
|
||||
allow_replay: true
|
||||
|
||||
# Allow your users to change latency settings (small latency/default/high latency)
|
||||
# Small latency live streams cannot use P2P
|
||||
# High latency live streams can increase P2P ratio
|
||||
latency_setting:
|
||||
enabled: true
|
||||
|
||||
# Your firewall should accept traffic from this port in TCP if you enable live
|
||||
rtmp:
|
||||
enabled: true
|
||||
|
|
|
@ -400,6 +400,12 @@ live:
|
|||
# /!\ transcoding.enabled (and not live.transcoding.enabled) has to be true to create a replay
|
||||
allow_replay: true
|
||||
|
||||
# Allow your users to change latency settings (small latency/default/high latency)
|
||||
# Small latency live streams cannot use P2P
|
||||
# High latency live streams can increase P2P ratio
|
||||
latency_setting:
|
||||
enabled: true
|
||||
|
||||
# Your firewall should accept traffic from this port in TCP if you enable live
|
||||
rtmp:
|
||||
enabled: true
|
||||
|
|
|
@ -237,6 +237,9 @@ function customConfig (): CustomConfig {
|
|||
live: {
|
||||
enabled: CONFIG.LIVE.ENABLED,
|
||||
allowReplay: CONFIG.LIVE.ALLOW_REPLAY,
|
||||
latencySetting: {
|
||||
enabled: CONFIG.LIVE.LATENCY_SETTING.ENABLED
|
||||
},
|
||||
maxDuration: CONFIG.LIVE.MAX_DURATION,
|
||||
maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES,
|
||||
maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import express from 'express'
|
||||
import { exists } from '@server/helpers/custom-validators/misc'
|
||||
import { createReqFiles } from '@server/helpers/express-utils'
|
||||
import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants'
|
||||
import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
|
||||
|
@ -9,7 +10,7 @@ import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator
|
|||
import { VideoLiveModel } from '@server/models/video/video-live'
|
||||
import { MVideoDetails, MVideoFullLight } from '@server/types/models'
|
||||
import { buildUUID, uuidToShort } from '@shared/extra-utils'
|
||||
import { HttpStatusCode, LiveVideoCreate, LiveVideoUpdate, VideoState } from '@shared/models'
|
||||
import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, VideoState } from '@shared/models'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { sequelizeTypescript } from '../../../initializers/database'
|
||||
import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail'
|
||||
|
@ -60,8 +61,9 @@ async function updateLiveVideo (req: express.Request, res: express.Response) {
|
|||
const video = res.locals.videoAll
|
||||
const videoLive = res.locals.videoLive
|
||||
|
||||
videoLive.saveReplay = body.saveReplay || false
|
||||
videoLive.permanentLive = body.permanentLive || false
|
||||
if (exists(body.saveReplay)) videoLive.saveReplay = body.saveReplay
|
||||
if (exists(body.permanentLive)) videoLive.permanentLive = body.permanentLive
|
||||
if (exists(body.latencyMode)) videoLive.latencyMode = body.latencyMode
|
||||
|
||||
video.VideoLive = await videoLive.save()
|
||||
|
||||
|
@ -87,6 +89,7 @@ async function addLiveVideo (req: express.Request, res: express.Response) {
|
|||
const videoLive = new VideoLiveModel()
|
||||
videoLive.saveReplay = videoInfo.saveReplay || false
|
||||
videoLive.permanentLive = videoInfo.permanentLive || false
|
||||
videoLive.latencyMode = videoInfo.latencyMode || LiveVideoLatencyMode.DEFAULT
|
||||
videoLive.streamKey = buildUUID()
|
||||
|
||||
const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
|
||||
|
|
|
@ -50,6 +50,10 @@ function getContextData (type: ContextType) {
|
|||
'@type': 'sc:Boolean',
|
||||
'@id': 'pt:permanentLive'
|
||||
},
|
||||
latencyMode: {
|
||||
'@type': 'sc:Number',
|
||||
'@id': 'pt:latencyMode'
|
||||
},
|
||||
|
||||
Infohash: 'pt:Infohash',
|
||||
Playlist: 'pt:Playlist',
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import validator from 'validator'
|
||||
import { logger } from '@server/helpers/logger'
|
||||
import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@shared/models'
|
||||
import { VideoState } from '../../../../shared/models/videos'
|
||||
import { LiveVideoLatencyMode, VideoState } from '../../../../shared/models/videos'
|
||||
import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants'
|
||||
import { peertubeTruncate } from '../../core-utils'
|
||||
import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
|
||||
import { isLiveLatencyModeValid } from '../video-lives'
|
||||
import {
|
||||
isVideoDurationValid,
|
||||
isVideoNameValid,
|
||||
|
@ -65,6 +66,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
|
|||
if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false
|
||||
if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false
|
||||
if (!isBooleanValid(video.permanentLive)) video.permanentLive = false
|
||||
if (!isLiveLatencyModeValid(video.latencyMode)) video.latencyMode = LiveVideoLatencyMode.DEFAULT
|
||||
|
||||
return isActivityPubUrlValid(video.id) &&
|
||||
isVideoNameValid(video.name) &&
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { LiveVideoLatencyMode } from '@shared/models'
|
||||
|
||||
function isLiveLatencyModeValid (value: any) {
|
||||
return [ LiveVideoLatencyMode.DEFAULT, LiveVideoLatencyMode.SMALL_LATENCY, LiveVideoLatencyMode.HIGH_LATENCY ].includes(value)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isLiveLatencyModeValid
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { FfmpegCommand, FilterSpecification } from 'fluent-ffmpeg'
|
||||
import { join } from 'path'
|
||||
import { VIDEO_LIVE } from '@server/initializers/constants'
|
||||
import { AvailableEncoders } from '@shared/models'
|
||||
import { AvailableEncoders, LiveVideoLatencyMode } from '@shared/models'
|
||||
import { logger, loggerTagsFactory } from '../logger'
|
||||
import { buildStreamSuffix, getFFmpeg, getScaleFilter, StreamType } from './ffmpeg-commons'
|
||||
import { getEncoderBuilderResult } from './ffmpeg-encoders'
|
||||
|
@ -15,6 +15,7 @@ async function getLiveTranscodingCommand (options: {
|
|||
|
||||
outPath: string
|
||||
masterPlaylistName: string
|
||||
latencyMode: LiveVideoLatencyMode
|
||||
|
||||
resolutions: number[]
|
||||
|
||||
|
@ -26,7 +27,7 @@ async function getLiveTranscodingCommand (options: {
|
|||
availableEncoders: AvailableEncoders
|
||||
profile: string
|
||||
}) {
|
||||
const { inputUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName, ratio } = options
|
||||
const { inputUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName, ratio, latencyMode } = options
|
||||
|
||||
const command = getFFmpeg(inputUrl, 'live')
|
||||
|
||||
|
@ -120,14 +121,21 @@ async function getLiveTranscodingCommand (options: {
|
|||
|
||||
command.complexFilter(complexFilter)
|
||||
|
||||
addDefaultLiveHLSParams(command, outPath, masterPlaylistName)
|
||||
addDefaultLiveHLSParams({ command, outPath, masterPlaylistName, latencyMode })
|
||||
|
||||
command.outputOption('-var_stream_map', varStreamMap.join(' '))
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
function getLiveMuxingCommand (inputUrl: string, outPath: string, masterPlaylistName: string) {
|
||||
function getLiveMuxingCommand (options: {
|
||||
inputUrl: string
|
||||
outPath: string
|
||||
masterPlaylistName: string
|
||||
latencyMode: LiveVideoLatencyMode
|
||||
}) {
|
||||
const { inputUrl, outPath, masterPlaylistName, latencyMode } = options
|
||||
|
||||
const command = getFFmpeg(inputUrl, 'live')
|
||||
|
||||
command.outputOption('-c:v copy')
|
||||
|
@ -135,22 +143,39 @@ function getLiveMuxingCommand (inputUrl: string, outPath: string, masterPlaylist
|
|||
command.outputOption('-map 0:a?')
|
||||
command.outputOption('-map 0:v?')
|
||||
|
||||
addDefaultLiveHLSParams(command, outPath, masterPlaylistName)
|
||||
addDefaultLiveHLSParams({ command, outPath, masterPlaylistName, latencyMode })
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
function getLiveSegmentTime (latencyMode: LiveVideoLatencyMode) {
|
||||
if (latencyMode === LiveVideoLatencyMode.SMALL_LATENCY) {
|
||||
return VIDEO_LIVE.SEGMENT_TIME_SECONDS.SMALL_LATENCY
|
||||
}
|
||||
|
||||
return VIDEO_LIVE.SEGMENT_TIME_SECONDS.DEFAULT_LATENCY
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
getLiveSegmentTime,
|
||||
|
||||
getLiveTranscodingCommand,
|
||||
getLiveMuxingCommand
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function addDefaultLiveHLSParams (command: FfmpegCommand, outPath: string, masterPlaylistName: string) {
|
||||
command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS)
|
||||
function addDefaultLiveHLSParams (options: {
|
||||
command: FfmpegCommand
|
||||
outPath: string
|
||||
masterPlaylistName: string
|
||||
latencyMode: LiveVideoLatencyMode
|
||||
}) {
|
||||
const { command, outPath, masterPlaylistName, latencyMode } = options
|
||||
|
||||
command.outputOption('-hls_time ' + getLiveSegmentTime(latencyMode))
|
||||
command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE)
|
||||
command.outputOption('-hls_flags delete_segments+independent_segments')
|
||||
command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`)
|
||||
|
|
|
@ -49,8 +49,8 @@ function checkMissedConfig () {
|
|||
'peertube.check_latest_version.enabled', 'peertube.check_latest_version.url',
|
||||
'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url',
|
||||
'search.search_index.disable_local_search', 'search.search_index.is_default_search',
|
||||
'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives',
|
||||
'live.rtmp.enabled', 'live.rtmp.port', 'live.rtmp.hostname',
|
||||
'live.enabled', 'live.allow_replay', 'live.latency_setting.enabled', 'live.max_duration',
|
||||
'live.max_user_lives', 'live.max_instance_lives', 'live.rtmp.enabled', 'live.rtmp.port', 'live.rtmp.hostname',
|
||||
'live.rtmps.enabled', 'live.rtmps.port', 'live.rtmps.hostname', 'live.rtmps.key_file', 'live.rtmps.cert_file',
|
||||
'live.transcoding.enabled', 'live.transcoding.threads', 'live.transcoding.profile',
|
||||
'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p',
|
||||
|
|
|
@ -4,9 +4,9 @@ import { dirname, join } from 'path'
|
|||
import { decacheModule } from '@server/helpers/decache'
|
||||
import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
|
||||
import { BroadcastMessageLevel } from '@shared/models/server'
|
||||
import { buildPath, root } from '../../shared/core-utils'
|
||||
import { VideoPrivacy, VideosRedundancyStrategy } from '../../shared/models'
|
||||
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
|
||||
import { buildPath, root } from '../../shared/core-utils'
|
||||
import { parseBytes, parseDurationToMs } from '../helpers/core-utils'
|
||||
|
||||
// Use a variable to reload the configuration if we need
|
||||
|
@ -296,6 +296,10 @@ const CONFIG = {
|
|||
|
||||
get ALLOW_REPLAY () { return config.get<boolean>('live.allow_replay') },
|
||||
|
||||
LATENCY_SETTING: {
|
||||
get ENABLED () { return config.get<boolean>('live.latency_setting.enabled') }
|
||||
},
|
||||
|
||||
RTMP: {
|
||||
get ENABLED () { return config.get<boolean>('live.rtmp.enabled') },
|
||||
get PORT () { return config.get<number>('live.rtmp.port') },
|
||||
|
|
|
@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LAST_MIGRATION_VERSION = 685
|
||||
const LAST_MIGRATION_VERSION = 690
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
@ -700,7 +700,10 @@ const RESUMABLE_UPLOAD_SESSION_LIFETIME = SCHEDULER_INTERVALS_MS.REMOVE_DANGLING
|
|||
const VIDEO_LIVE = {
|
||||
EXTENSION: '.ts',
|
||||
CLEANUP_DELAY: 1000 * 60 * 5, // 5 minutes
|
||||
SEGMENT_TIME_SECONDS: 4, // 4 seconds
|
||||
SEGMENT_TIME_SECONDS: {
|
||||
DEFAULT_LATENCY: 4, // 4 seconds
|
||||
SMALL_LATENCY: 2 // 2 seconds
|
||||
},
|
||||
SEGMENTS_LIST_SIZE: 15, // 15 maximum segments in live playlist
|
||||
REPLAY_DIRECTORY: 'replay',
|
||||
EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION: 4,
|
||||
|
@ -842,7 +845,8 @@ if (isTestInstance() === true) {
|
|||
PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000
|
||||
|
||||
VIDEO_LIVE.CLEANUP_DELAY = 5000
|
||||
VIDEO_LIVE.SEGMENT_TIME_SECONDS = 2
|
||||
VIDEO_LIVE.SEGMENT_TIME_SECONDS.DEFAULT_LATENCY = 2
|
||||
VIDEO_LIVE.SEGMENT_TIME_SECONDS.SMALL_LATENCY = 1
|
||||
VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION = 1
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import { LiveVideoLatencyMode } from '@shared/models'
|
||||
import * as Sequelize from 'sequelize'
|
||||
|
||||
async function up (utils: {
|
||||
transaction: Sequelize.Transaction
|
||||
queryInterface: Sequelize.QueryInterface
|
||||
sequelize: Sequelize.Sequelize
|
||||
db: any
|
||||
}): Promise<void> {
|
||||
await utils.queryInterface.addColumn('videoLive', 'latencyMode', {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: null,
|
||||
allowNull: true
|
||||
}, { transaction: utils.transaction })
|
||||
|
||||
{
|
||||
const query = `UPDATE "videoLive" SET "latencyMode" = ${LiveVideoLatencyMode.DEFAULT}`
|
||||
await utils.sequelize.query(query, { type: Sequelize.QueryTypes.UPDATE, transaction: utils.transaction })
|
||||
}
|
||||
|
||||
await utils.queryInterface.changeColumn('videoLive', 'latencyMode', {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: null,
|
||||
allowNull: false
|
||||
}, { transaction: utils.transaction })
|
||||
}
|
||||
|
||||
function down () {
|
||||
throw new Error('Not implemented.')
|
||||
}
|
||||
|
||||
export {
|
||||
up,
|
||||
down
|
||||
}
|
|
@ -151,6 +151,7 @@ function getLiveAttributesFromObject (video: MVideoId, videoObject: VideoObject)
|
|||
return {
|
||||
saveReplay: videoObject.liveSaveReplay,
|
||||
permanentLive: videoObject.permanentLive,
|
||||
latencyMode: videoObject.latencyMode,
|
||||
videoId: video.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,10 @@ import { createServer as createServerTLS, Server as ServerTLS } from 'tls'
|
|||
import {
|
||||
computeLowerResolutionsToTranscode,
|
||||
ffprobePromise,
|
||||
getLiveSegmentTime,
|
||||
getVideoStreamBitrate,
|
||||
getVideoStreamFPS,
|
||||
getVideoStreamDimensionsInfo
|
||||
getVideoStreamDimensionsInfo,
|
||||
getVideoStreamFPS
|
||||
} from '@server/helpers/ffmpeg'
|
||||
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
||||
import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config'
|
||||
|
@ -353,7 +354,7 @@ class LiveManager {
|
|||
.catch(err => logger.error('Cannot federate live video %s.', video.url, { err, ...localLTags }))
|
||||
|
||||
PeerTubeSocket.Instance.sendVideoLiveNewState(video)
|
||||
}, VIDEO_LIVE.SEGMENT_TIME_SECONDS * 1000 * VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION)
|
||||
}, getLiveSegmentTime(live.latencyMode) * 1000 * VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION)
|
||||
} catch (err) {
|
||||
logger.error('Cannot save/federate live video %d.', videoId, { err, ...localLTags })
|
||||
}
|
||||
|
|
|
@ -125,6 +125,8 @@ class MuxingSession extends EventEmitter {
|
|||
outPath,
|
||||
masterPlaylistName: this.streamingPlaylist.playlistFilename,
|
||||
|
||||
latencyMode: this.videoLive.latencyMode,
|
||||
|
||||
resolutions: this.allResolutions,
|
||||
fps: this.fps,
|
||||
bitrate: this.bitrate,
|
||||
|
@ -133,7 +135,12 @@ class MuxingSession extends EventEmitter {
|
|||
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
||||
profile: CONFIG.LIVE.TRANSCODING.PROFILE
|
||||
})
|
||||
: getLiveMuxingCommand(this.inputUrl, outPath, this.streamingPlaylist.playlistFilename)
|
||||
: getLiveMuxingCommand({
|
||||
inputUrl: this.inputUrl,
|
||||
outPath,
|
||||
masterPlaylistName: this.streamingPlaylist.playlistFilename,
|
||||
latencyMode: this.videoLive.latencyMode
|
||||
})
|
||||
|
||||
logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags())
|
||||
|
||||
|
|
|
@ -137,6 +137,10 @@ class ServerConfigManager {
|
|||
enabled: CONFIG.LIVE.ENABLED,
|
||||
|
||||
allowReplay: CONFIG.LIVE.ALLOW_REPLAY,
|
||||
latencySetting: {
|
||||
enabled: CONFIG.LIVE.LATENCY_SETTING.ENABLED
|
||||
},
|
||||
|
||||
maxDuration: CONFIG.LIVE.MAX_DURATION,
|
||||
maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES,
|
||||
maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { isLiveLatencyModeValid } from '@server/helpers/custom-validators/video-lives'
|
||||
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants'
|
||||
import { isLocalLiveVideoAccepted } from '@server/lib/moderation'
|
||||
import { Hooks } from '@server/lib/plugins/hooks'
|
||||
import { VideoModel } from '@server/models/video/video'
|
||||
import { VideoLiveModel } from '@server/models/video/video-live'
|
||||
import { HttpStatusCode, ServerErrorCode, UserRight, VideoState } from '@shared/models'
|
||||
import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
LiveVideoCreate,
|
||||
LiveVideoLatencyMode,
|
||||
LiveVideoUpdate,
|
||||
ServerErrorCode,
|
||||
UserRight,
|
||||
VideoState
|
||||
} from '@shared/models'
|
||||
import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
|
||||
import { isVideoNameValid } from '../../../helpers/custom-validators/videos'
|
||||
import { cleanUpReqFiles } from '../../../helpers/express-utils'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
|
@ -67,6 +76,12 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
|
|||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid permanentLive attribute'),
|
||||
|
||||
body('latencyMode')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isLiveLatencyModeValid)
|
||||
.withMessage('Should have a valid latency mode attribute'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking videoLiveAddValidator parameters', { parameters: req.body })
|
||||
|
||||
|
@ -82,7 +97,9 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
|
|||
})
|
||||
}
|
||||
|
||||
if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) {
|
||||
const body: LiveVideoCreate = req.body
|
||||
|
||||
if (hasValidSaveReplay(body) !== true) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
|
@ -92,14 +109,23 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
|
|||
})
|
||||
}
|
||||
|
||||
if (req.body.permanentLive && req.body.saveReplay) {
|
||||
if (hasValidLatencyMode(body) !== true) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Custom latency mode is not allowed by this instance'
|
||||
})
|
||||
}
|
||||
|
||||
if (body.permanentLive && body.saveReplay) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({ message: 'Cannot set this live as permanent while saving its replay' })
|
||||
}
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
|
||||
if (!await doesVideoChannelOfAccountExist(body.channelId, user, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (CONFIG.LIVE.MAX_INSTANCE_LIVES !== -1) {
|
||||
const totalInstanceLives = await VideoModel.countLocalLives()
|
||||
|
@ -141,19 +167,34 @@ const videoLiveUpdateValidator = [
|
|||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid saveReplay attribute'),
|
||||
|
||||
body('latencyMode')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isLiveLatencyModeValid)
|
||||
.withMessage('Should have a valid latency mode attribute'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking videoLiveUpdateValidator parameters', { parameters: req.body })
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (req.body.permanentLive && req.body.saveReplay) {
|
||||
const body: LiveVideoUpdate = req.body
|
||||
|
||||
if (body.permanentLive && body.saveReplay) {
|
||||
return res.fail({ message: 'Cannot set this live as permanent while saving its replay' })
|
||||
}
|
||||
|
||||
if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) {
|
||||
if (hasValidSaveReplay(body) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Saving live replay is not allowed instance'
|
||||
message: 'Saving live replay is not allowed by this instance'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasValidLatencyMode(body) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Custom latency mode is not allowed by this instance'
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -203,3 +244,19 @@ async function isLiveVideoAccepted (req: express.Request, res: express.Response)
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
function hasValidSaveReplay (body: LiveVideoUpdate | LiveVideoCreate) {
|
||||
if (CONFIG.LIVE.ALLOW_REPLAY !== true && body.saveReplay === true) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function hasValidLatencyMode (body: LiveVideoUpdate | LiveVideoCreate) {
|
||||
if (
|
||||
CONFIG.LIVE.LATENCY_SETTING.ENABLED !== true &&
|
||||
exists(body.latencyMode) &&
|
||||
body.latencyMode !== LiveVideoLatencyMode.DEFAULT
|
||||
) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -411,15 +411,6 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
|||
views: video.views,
|
||||
sensitive: video.nsfw,
|
||||
waitTranscoding: video.waitTranscoding,
|
||||
isLiveBroadcast: video.isLive,
|
||||
|
||||
liveSaveReplay: video.isLive
|
||||
? video.VideoLive.saveReplay
|
||||
: null,
|
||||
|
||||
permanentLive: video.isLive
|
||||
? video.VideoLive.permanentLive
|
||||
: null,
|
||||
|
||||
state: video.state,
|
||||
commentsEnabled: video.commentsEnabled,
|
||||
|
@ -431,10 +422,13 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
|||
: null,
|
||||
|
||||
updated: video.updatedAt.toISOString(),
|
||||
|
||||
mediaType: 'text/markdown',
|
||||
content: video.description,
|
||||
support: video.support,
|
||||
|
||||
subtitleLanguage,
|
||||
|
||||
icon: icons.map(i => ({
|
||||
type: 'Image',
|
||||
url: i.getFileUrl(video),
|
||||
|
@ -442,11 +436,14 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
|||
width: i.width,
|
||||
height: i.height
|
||||
})),
|
||||
|
||||
url,
|
||||
|
||||
likes: getLocalVideoLikesActivityPubUrl(video),
|
||||
dislikes: getLocalVideoDislikesActivityPubUrl(video),
|
||||
shares: getLocalVideoSharesActivityPubUrl(video),
|
||||
comments: getLocalVideoCommentsActivityPubUrl(video),
|
||||
|
||||
attributedTo: [
|
||||
{
|
||||
type: 'Person',
|
||||
|
@ -456,7 +453,9 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
|||
type: 'Group',
|
||||
id: video.VideoChannel.Actor.url
|
||||
}
|
||||
]
|
||||
],
|
||||
|
||||
...buildLiveAPAttributes(video)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -500,3 +499,23 @@ export {
|
|||
getPrivacyLabel,
|
||||
getStateLabel
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildLiveAPAttributes (video: MVideoAP) {
|
||||
if (!video.isLive) {
|
||||
return {
|
||||
isLiveBroadcast: false,
|
||||
liveSaveReplay: null,
|
||||
permanentLive: null,
|
||||
latencyMode: null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isLiveBroadcast: true,
|
||||
liveSaveReplay: video.VideoLive.saveReplay,
|
||||
permanentLive: video.VideoLive.permanentLive,
|
||||
latencyMode: video.VideoLive.latencyMode
|
||||
}
|
||||
}
|
||||
|
|
|
@ -158,6 +158,7 @@ export class VideoTableAttributes {
|
|||
'streamKey',
|
||||
'saveReplay',
|
||||
'permanentLive',
|
||||
'latencyMode',
|
||||
'videoId',
|
||||
'createdAt',
|
||||
'updatedAt'
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { CONFIG } from '@server/initializers/config'
|
||||
import { WEBSERVER } from '@server/initializers/constants'
|
||||
import { MVideoLive, MVideoLiveVideo } from '@server/types/models'
|
||||
import { LiveVideo, LiveVideoLatencyMode, VideoState } from '@shared/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
import { LiveVideo, VideoState } from '@shared/models'
|
||||
import { VideoModel } from './video'
|
||||
import { VideoBlacklistModel } from './video-blacklist'
|
||||
import { CONFIG } from '@server/initializers/config'
|
||||
|
||||
@DefaultScope(() => ({
|
||||
include: [
|
||||
|
@ -44,6 +44,10 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>
|
|||
@Column
|
||||
permanentLive: boolean
|
||||
|
||||
@AllowNull(false)
|
||||
@Column
|
||||
latencyMode: LiveVideoLatencyMode
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date
|
||||
|
||||
|
@ -113,7 +117,8 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>
|
|||
|
||||
streamKey: this.streamKey,
|
||||
permanentLive: this.permanentLive,
|
||||
saveReplay: this.saveReplay
|
||||
saveReplay: this.saveReplay,
|
||||
latencyMode: this.latencyMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,6 +125,9 @@ describe('Test config API validators', function () {
|
|||
enabled: true,
|
||||
|
||||
allowReplay: false,
|
||||
latencySetting: {
|
||||
enabled: false
|
||||
},
|
||||
maxDuration: 30,
|
||||
maxInstanceLives: -1,
|
||||
maxUserLives: 50,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import 'mocha'
|
||||
import { omit } from 'lodash'
|
||||
import { buildAbsoluteFixturePath } from '@shared/core-utils'
|
||||
import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@shared/models'
|
||||
import { HttpStatusCode, LiveVideoLatencyMode, VideoCreateResult, VideoPrivacy } from '@shared/models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
|
@ -38,6 +38,9 @@ describe('Test video lives API validator', function () {
|
|||
newConfig: {
|
||||
live: {
|
||||
enabled: true,
|
||||
latencySetting: {
|
||||
enabled: false
|
||||
},
|
||||
maxInstanceLives: 20,
|
||||
maxUserLives: 20,
|
||||
allowReplay: true
|
||||
|
@ -81,7 +84,8 @@ describe('Test video lives API validator', function () {
|
|||
privacy: VideoPrivacy.PUBLIC,
|
||||
channelId,
|
||||
saveReplay: false,
|
||||
permanentLive: false
|
||||
permanentLive: false,
|
||||
latencyMode: LiveVideoLatencyMode.DEFAULT
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -214,6 +218,18 @@ describe('Test video lives API validator', function () {
|
|||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with bad latency setting', async function () {
|
||||
const fields = { ...baseCorrectParams, latencyMode: 42 }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail to set latency if the server does not allow it', async function () {
|
||||
const fields = { ...baseCorrectParams, latencyMode: LiveVideoLatencyMode.HIGH_LATENCY }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
|
@ -393,6 +409,18 @@ describe('Test video lives API validator', function () {
|
|||
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with bad latency setting', async function () {
|
||||
const fields = { latencyMode: 42 }
|
||||
|
||||
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail to set latency if the server does not allow it', async function () {
|
||||
const fields = { latencyMode: LiveVideoLatencyMode.HIGH_LATENCY }
|
||||
|
||||
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await command.update({ videoId: video.id, fields: { saveReplay: false } })
|
||||
await command.update({ videoId: video.uuid, fields: { saveReplay: false } })
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
HttpStatusCode,
|
||||
LiveVideo,
|
||||
LiveVideoCreate,
|
||||
LiveVideoLatencyMode,
|
||||
VideoDetails,
|
||||
VideoPrivacy,
|
||||
VideoState,
|
||||
|
@ -52,6 +53,9 @@ describe('Test live', function () {
|
|||
live: {
|
||||
enabled: true,
|
||||
allowReplay: true,
|
||||
latencySetting: {
|
||||
enabled: true
|
||||
},
|
||||
transcoding: {
|
||||
enabled: false
|
||||
}
|
||||
|
@ -85,6 +89,7 @@ describe('Test live', function () {
|
|||
commentsEnabled: false,
|
||||
downloadEnabled: false,
|
||||
saveReplay: true,
|
||||
latencyMode: LiveVideoLatencyMode.SMALL_LATENCY,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
previewfile: 'video_short1-preview.webm.jpg',
|
||||
thumbnailfile: 'video_short1.webm.jpg'
|
||||
|
@ -131,6 +136,7 @@ describe('Test live', function () {
|
|||
}
|
||||
|
||||
expect(live.saveReplay).to.be.true
|
||||
expect(live.latencyMode).to.equal(LiveVideoLatencyMode.SMALL_LATENCY)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -175,7 +181,7 @@ describe('Test live', function () {
|
|||
it('Should update the live', async function () {
|
||||
this.timeout(10000)
|
||||
|
||||
await commands[0].update({ videoId: liveVideoUUID, fields: { saveReplay: false } })
|
||||
await commands[0].update({ videoId: liveVideoUUID, fields: { saveReplay: false, latencyMode: LiveVideoLatencyMode.DEFAULT } })
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
|
@ -192,6 +198,7 @@ describe('Test live', function () {
|
|||
}
|
||||
|
||||
expect(live.saveReplay).to.be.false
|
||||
expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -82,6 +82,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
|
|||
|
||||
expect(data.live.enabled).to.be.false
|
||||
expect(data.live.allowReplay).to.be.false
|
||||
expect(data.live.latencySetting.enabled).to.be.true
|
||||
expect(data.live.maxDuration).to.equal(-1)
|
||||
expect(data.live.maxInstanceLives).to.equal(20)
|
||||
expect(data.live.maxUserLives).to.equal(3)
|
||||
|
@ -185,6 +186,7 @@ function checkUpdatedConfig (data: CustomConfig) {
|
|||
|
||||
expect(data.live.enabled).to.be.true
|
||||
expect(data.live.allowReplay).to.be.true
|
||||
expect(data.live.latencySetting.enabled).to.be.false
|
||||
expect(data.live.maxDuration).to.equal(5000)
|
||||
expect(data.live.maxInstanceLives).to.equal(-1)
|
||||
expect(data.live.maxUserLives).to.equal(10)
|
||||
|
@ -326,6 +328,9 @@ const newCustomConfig: CustomConfig = {
|
|||
live: {
|
||||
enabled: true,
|
||||
allowReplay: true,
|
||||
latencySetting: {
|
||||
enabled: false
|
||||
},
|
||||
maxDuration: 5000,
|
||||
maxInstanceLives: -1,
|
||||
maxUserLives: 10,
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
ActivityTagObject,
|
||||
ActivityUrlObject
|
||||
} from './common-objects'
|
||||
import { VideoState } from '../../videos'
|
||||
import { LiveVideoLatencyMode, VideoState } from '../../videos'
|
||||
|
||||
export interface VideoObject {
|
||||
type: 'Video'
|
||||
|
@ -25,6 +25,7 @@ export interface VideoObject {
|
|||
isLiveBroadcast: boolean
|
||||
liveSaveReplay: boolean
|
||||
permanentLive: boolean
|
||||
latencyMode: LiveVideoLatencyMode
|
||||
|
||||
commentsEnabled: boolean
|
||||
downloadEnabled: boolean
|
||||
|
|
|
@ -131,6 +131,10 @@ export interface CustomConfig {
|
|||
|
||||
allowReplay: boolean
|
||||
|
||||
latencySetting: {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
maxDuration: number
|
||||
maxInstanceLives: number
|
||||
maxUserLives: number
|
||||
|
|
|
@ -149,10 +149,14 @@ export interface ServerConfig {
|
|||
live: {
|
||||
enabled: boolean
|
||||
|
||||
allowReplay: boolean
|
||||
latencySetting: {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
maxDuration: number
|
||||
maxInstanceLives: number
|
||||
maxUserLives: number
|
||||
allowReplay: boolean
|
||||
|
||||
transcoding: {
|
||||
enabled: boolean
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export * from './live-video-create.model'
|
||||
export * from './live-video-event-payload.model'
|
||||
export * from './live-video-event.type'
|
||||
export * from './live-video-latency-mode.enum'
|
||||
export * from './live-video-update.model'
|
||||
export * from './live-video.model'
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { LiveVideoLatencyMode } from '.'
|
||||
import { VideoCreate } from '../video-create.model'
|
||||
|
||||
export interface LiveVideoCreate extends VideoCreate {
|
||||
saveReplay?: boolean
|
||||
permanentLive?: boolean
|
||||
latencyMode?: LiveVideoLatencyMode
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export const enum LiveVideoLatencyMode {
|
||||
DEFAULT = 1,
|
||||
HIGH_LATENCY = 2,
|
||||
SMALL_LATENCY = 3
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
|
||||
|
||||
export interface LiveVideoUpdate {
|
||||
permanentLive?: boolean
|
||||
saveReplay?: boolean
|
||||
latencyMode?: LiveVideoLatencyMode
|
||||
}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
|
||||
|
||||
export interface LiveVideo {
|
||||
rtmpUrl: string
|
||||
rtmpsUrl: string
|
||||
|
||||
streamKey: string
|
||||
|
||||
saveReplay: boolean
|
||||
permanentLive: boolean
|
||||
latencyMode: LiveVideoLatencyMode
|
||||
}
|
||||
|
|
|
@ -292,6 +292,9 @@ export class ConfigCommand extends AbstractCommand {
|
|||
live: {
|
||||
enabled: true,
|
||||
allowReplay: false,
|
||||
latencySetting: {
|
||||
enabled: false
|
||||
},
|
||||
maxDuration: -1,
|
||||
maxInstanceLives: -1,
|
||||
maxUserLives: 50,
|
||||
|
|
|
@ -2295,6 +2295,9 @@ paths:
|
|||
permanentLive:
|
||||
description: User can stream multiple times in a permanent live
|
||||
type: boolean
|
||||
latencyMode:
|
||||
description: User can select live latency mode if enabled by the instance
|
||||
$ref: '#/components/schemas/LiveVideoLatencyMode'
|
||||
thumbnailfile:
|
||||
description: Live video/replay thumbnail file
|
||||
type: string
|
||||
|
@ -5291,6 +5294,14 @@ components:
|
|||
description: 'Admin flags for the user (None = `0`, Bypass video blocklist = `1`)'
|
||||
example: 1
|
||||
|
||||
LiveVideoLatencyMode:
|
||||
type: integer
|
||||
enum:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
description: 'The live latency mode (Default = `1`, HIght latency = `2`, Small Latency = `3`)'
|
||||
|
||||
VideoStateConstant:
|
||||
properties:
|
||||
id:
|
||||
|
@ -7482,6 +7493,9 @@ components:
|
|||
permanentLive:
|
||||
description: User can stream multiple times in a permanent live
|
||||
type: boolean
|
||||
latencyMode:
|
||||
description: User can select live latency mode if enabled by the instance
|
||||
$ref: '#/components/schemas/LiveVideoLatencyMode'
|
||||
|
||||
LiveVideoResponse:
|
||||
properties:
|
||||
|
@ -7497,8 +7511,9 @@ components:
|
|||
permanentLive:
|
||||
description: User can stream multiple times in a permanent live
|
||||
type: boolean
|
||||
|
||||
|
||||
latencyMode:
|
||||
description: User can select live latency mode if enabled by the instance
|
||||
$ref: '#/components/schemas/LiveVideoLatencyMode'
|
||||
|
||||
callbacks:
|
||||
searchIndex:
|
||||
|
|
Loading…
Reference in New Issue