aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-03-11 11:11:00 +0100
committerChocobozzz <me@florianbigard.com>2022-03-11 11:11:00 +0100
commit4495806f2f5045a0626ab64f43b5fc4deefe2ac0 (patch)
treee83b61b8da313fe1add571b9dd73261828c38cf9 /server
parent052bdb7c120b2ac7a1616a8072f9eedd2ab2f4b6 (diff)
parent58ae266bada2233945c4446220c22ccc4e7c62b5 (diff)
downloadPeerTube-4495806f2f5045a0626ab64f43b5fc4deefe2ac0.tar.gz
PeerTube-4495806f2f5045a0626ab64f43b5fc4deefe2ac0.tar.zst
PeerTube-4495806f2f5045a0626ab64f43b5fc4deefe2ac0.zip
Merge remote-tracking branch 'weblate/develop' into develop
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/config.ts3
-rw-r--r--server/controllers/api/videos/live.ts9
-rw-r--r--server/helpers/activitypub.ts4
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts4
-rw-r--r--server/helpers/custom-validators/video-lives.ts11
-rw-r--r--server/helpers/ffmpeg/ffmpeg-live.ts39
-rw-r--r--server/initializers/checker-before-init.ts4
-rw-r--r--server/initializers/config.ts6
-rw-r--r--server/initializers/constants.ts10
-rw-r--r--server/initializers/migrations/0690-live-latency-mode.ts35
-rw-r--r--server/lib/activitypub/videos/shared/object-to-model-attributes.ts1
-rw-r--r--server/lib/live/live-manager.ts7
-rw-r--r--server/lib/live/shared/muxing-session.ts9
-rw-r--r--server/lib/server-config-manager.ts4
-rw-r--r--server/middlewares/validators/videos/video-live.ts73
-rw-r--r--server/models/video/formatter/video-format-utils.ts39
-rw-r--r--server/models/video/sql/video/shared/video-table-attributes.ts1
-rw-r--r--server/models/video/video-live.ts11
-rw-r--r--server/tests/api/check-params/config.ts3
-rw-r--r--server/tests/api/check-params/live.ts32
-rw-r--r--server/tests/api/live/live.ts9
-rw-r--r--server/tests/api/server/config.ts5
22 files changed, 274 insertions, 45 deletions
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 @@
1import express from 'express' 1import express from 'express'
2import { exists } from '@server/helpers/custom-validators/misc'
2import { createReqFiles } from '@server/helpers/express-utils' 3import { createReqFiles } from '@server/helpers/express-utils'
3import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants' 4import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants'
4import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' 5import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
@@ -9,7 +10,7 @@ import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator
9import { VideoLiveModel } from '@server/models/video/video-live' 10import { VideoLiveModel } from '@server/models/video/video-live'
10import { MVideoDetails, MVideoFullLight } from '@server/types/models' 11import { MVideoDetails, MVideoFullLight } from '@server/types/models'
11import { buildUUID, uuidToShort } from '@shared/extra-utils' 12import { buildUUID, uuidToShort } from '@shared/extra-utils'
12import { HttpStatusCode, LiveVideoCreate, LiveVideoUpdate, VideoState } from '@shared/models' 13import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, VideoState } from '@shared/models'
13import { logger } from '../../../helpers/logger' 14import { logger } from '../../../helpers/logger'
14import { sequelizeTypescript } from '../../../initializers/database' 15import { sequelizeTypescript } from '../../../initializers/database'
15import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' 16import { 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 @@
1import validator from 'validator' 1import validator from 'validator'
2import { logger } from '@server/helpers/logger' 2import { logger } from '@server/helpers/logger'
3import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@shared/models' 3import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@shared/models'
4import { VideoState } from '../../../../shared/models/videos' 4import { LiveVideoLatencyMode, VideoState } from '../../../../shared/models/videos'
5import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants' 5import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants'
6import { peertubeTruncate } from '../../core-utils' 6import { peertubeTruncate } from '../../core-utils'
7import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc' 7import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
8import { isLiveLatencyModeValid } from '../video-lives'
8import { 9import {
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 @@
1import { LiveVideoLatencyMode } from '@shared/models'
2
3function isLiveLatencyModeValid (value: any) {
4 return [ LiveVideoLatencyMode.DEFAULT, LiveVideoLatencyMode.SMALL_LATENCY, LiveVideoLatencyMode.HIGH_LATENCY ].includes(value)
5}
6
7// ---------------------------------------------------------------------------
8
9export {
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 @@
1import { FfmpegCommand, FilterSpecification } from 'fluent-ffmpeg' 1import { FfmpegCommand, FilterSpecification } from 'fluent-ffmpeg'
2import { join } from 'path' 2import { join } from 'path'
3import { VIDEO_LIVE } from '@server/initializers/constants' 3import { VIDEO_LIVE } from '@server/initializers/constants'
4import { AvailableEncoders } from '@shared/models' 4import { AvailableEncoders, LiveVideoLatencyMode } from '@shared/models'
5import { logger, loggerTagsFactory } from '../logger' 5import { logger, loggerTagsFactory } from '../logger'
6import { buildStreamSuffix, getFFmpeg, getScaleFilter, StreamType } from './ffmpeg-commons' 6import { buildStreamSuffix, getFFmpeg, getScaleFilter, StreamType } from './ffmpeg-commons'
7import { getEncoderBuilderResult } from './ffmpeg-encoders' 7import { 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
130function getLiveMuxingCommand (inputUrl: string, outPath: string, masterPlaylistName: string) { 131function 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
151function 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
145export { 161export {
162 getLiveSegmentTime,
163
146 getLiveTranscodingCommand, 164 getLiveTranscodingCommand,
147 getLiveMuxingCommand 165 getLiveMuxingCommand
148} 166}
149 167
150// --------------------------------------------------------------------------- 168// ---------------------------------------------------------------------------
151 169
152function addDefaultLiveHLSParams (command: FfmpegCommand, outPath: string, masterPlaylistName: string) { 170function 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'
4import { decacheModule } from '@server/helpers/decache' 4import { decacheModule } from '@server/helpers/decache'
5import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' 5import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
6import { BroadcastMessageLevel } from '@shared/models/server' 6import { BroadcastMessageLevel } from '@shared/models/server'
7import { buildPath, root } from '../../shared/core-utils'
7import { VideoPrivacy, VideosRedundancyStrategy } from '../../shared/models' 8import { VideoPrivacy, VideosRedundancyStrategy } from '../../shared/models'
8import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' 9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
9import { buildPath, root } from '../../shared/core-utils'
10import { parseBytes, parseDurationToMs } from '../helpers/core-utils' 10import { 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
27const LAST_MIGRATION_VERSION = 685 27const LAST_MIGRATION_VERSION = 690
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
@@ -700,7 +700,10 @@ const RESUMABLE_UPLOAD_SESSION_LIFETIME = SCHEDULER_INTERVALS_MS.REMOVE_DANGLING
700const VIDEO_LIVE = { 700const 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 @@
1import { LiveVideoLatencyMode } from '@shared/models'
2import * as Sequelize from 'sequelize'
3
4async 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
28function down () {
29 throw new Error('Not implemented.')
30}
31
32export {
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'
5import { 5import {
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'
12import { logger, loggerTagsFactory } from '@server/helpers/logger' 13import { logger, loggerTagsFactory } from '@server/helpers/logger'
13import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' 14import { 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 @@
1import express from 'express' 1import express from 'express'
2import { body } from 'express-validator' 2import { body } from 'express-validator'
3import { isLiveLatencyModeValid } from '@server/helpers/custom-validators/video-lives'
3import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' 4import { CONSTRAINTS_FIELDS } from '@server/initializers/constants'
4import { isLocalLiveVideoAccepted } from '@server/lib/moderation' 5import { isLocalLiveVideoAccepted } from '@server/lib/moderation'
5import { Hooks } from '@server/lib/plugins/hooks' 6import { Hooks } from '@server/lib/plugins/hooks'
6import { VideoModel } from '@server/models/video/video' 7import { VideoModel } from '@server/models/video/video'
7import { VideoLiveModel } from '@server/models/video/video-live' 8import { VideoLiveModel } from '@server/models/video/video-live'
8import { HttpStatusCode, ServerErrorCode, UserRight, VideoState } from '@shared/models' 9import {
9import { 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'
18import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
10import { isVideoNameValid } from '../../../helpers/custom-validators/videos' 19import { isVideoNameValid } from '../../../helpers/custom-validators/videos'
11import { cleanUpReqFiles } from '../../../helpers/express-utils' 20import { cleanUpReqFiles } from '../../../helpers/express-utils'
12import { logger } from '../../../helpers/logger' 21import { 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
248function hasValidSaveReplay (body: LiveVideoUpdate | LiveVideoCreate) {
249 if (CONFIG.LIVE.ALLOW_REPLAY !== true && body.saveReplay === true) return false
250
251 return true
252}
253
254function 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
505function 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 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { CONFIG } from '@server/initializers/config'
2import { WEBSERVER } from '@server/initializers/constants' 3import { WEBSERVER } from '@server/initializers/constants'
3import { MVideoLive, MVideoLiveVideo } from '@server/types/models' 4import { MVideoLive, MVideoLiveVideo } from '@server/types/models'
5import { LiveVideo, LiveVideoLatencyMode, VideoState } from '@shared/models'
4import { AttributesOnly } from '@shared/typescript-utils' 6import { AttributesOnly } from '@shared/typescript-utils'
5import { LiveVideo, VideoState } from '@shared/models'
6import { VideoModel } from './video' 7import { VideoModel } from './video'
7import { VideoBlacklistModel } from './video-blacklist' 8import { VideoBlacklistModel } from './video-blacklist'
8import { 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 @@
3import 'mocha' 3import 'mocha'
4import { omit } from 'lodash' 4import { omit } from 'lodash'
5import { buildAbsoluteFixturePath } from '@shared/core-utils' 5import { buildAbsoluteFixturePath } from '@shared/core-utils'
6import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@shared/models' 6import { HttpStatusCode, LiveVideoLatencyMode, VideoCreateResult, VideoPrivacy } from '@shared/models'
7import { 7import {
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,