From f443a74649174b2f9347c158e30f8ac7aa3e958a Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 4 Mar 2022 13:40:02 +0100 Subject: Add latency setting support --- server/controllers/api/config.ts | 3 + server/controllers/api/videos/live.ts | 9 ++- server/helpers/activitypub.ts | 4 ++ .../custom-validators/activitypub/videos.ts | 4 +- server/helpers/custom-validators/video-lives.ts | 11 ++++ server/helpers/ffmpeg/ffmpeg-live.ts | 39 +++++++++--- server/initializers/checker-before-init.ts | 4 +- server/initializers/config.ts | 6 +- server/initializers/constants.ts | 10 ++- .../migrations/0690-live-latency-mode.ts | 35 +++++++++++ .../videos/shared/object-to-model-attributes.ts | 1 + server/lib/live/live-manager.ts | 7 ++- server/lib/live/shared/muxing-session.ts | 9 ++- server/lib/server-config-manager.ts | 4 ++ server/middlewares/validators/videos/video-live.ts | 73 +++++++++++++++++++--- .../models/video/formatter/video-format-utils.ts | 39 +++++++++--- .../sql/video/shared/video-table-attributes.ts | 1 + server/models/video/video-live.ts | 11 +++- server/tests/api/check-params/config.ts | 3 + server/tests/api/check-params/live.ts | 32 +++++++++- server/tests/api/live/live.ts | 9 ++- server/tests/api/server/config.ts | 5 ++ 22 files changed, 274 insertions(+), 45 deletions(-) create mode 100644 server/helpers/custom-validators/video-lives.ts create mode 100644 server/initializers/migrations/0690-live-latency-mode.ts (limited to 'server') 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 { 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, 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 @@ 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({ 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) { '@type': 'sc:Boolean', '@id': 'pt:permanentLive' }, + latencyMode: { + '@type': 'sc:Number', + '@id': 'pt:latencyMode' + }, Infohash: 'pt:Infohash', 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 @@ 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) && 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 @@ +import { LiveVideoLatencyMode } from '@shared/models' + +function isLiveLatencyModeValid (value: any) { + return [ LiveVideoLatencyMode.DEFAULT, LiveVideoLatencyMode.SMALL_LATENCY, LiveVideoLatencyMode.HIGH_LATENCY ].includes(value) +} + +// --------------------------------------------------------------------------- + +export { + isLiveLatencyModeValid +} 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 @@ 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')}`) 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 () { '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', 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' 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('live.allow_replay') }, + LATENCY_SETTING: { + get ENABLED () { return config.get('live.latency_setting.enabled') } + }, + RTMP: { get ENABLED () { return config.get('live.rtmp.enabled') }, get PORT () { return config.get('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' // --------------------------------------------------------------------------- -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 } 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 @@ +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 { + 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 +} 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) return { saveReplay: videoObject.liveSaveReplay, permanentLive: videoObject.permanentLive, + latencyMode: videoObject.latencyMode, videoId: video.id } } 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' 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 }) } 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 { 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()) 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 { 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, 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 @@ 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 +} 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 { 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 + } +} 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 { 'streamKey', 'saveReplay', 'permanentLive', + 'latencyMode', 'videoId', 'createdAt', '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 @@ 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 @Column permanentLive: boolean + @AllowNull(false) + @Column + latencyMode: LiveVideoLatencyMode + @CreatedAt createdAt: Date @@ -113,7 +117,8 @@ export class VideoLiveModel extends Model streamKey: this.streamKey, permanentLive: this.permanentLive, - saveReplay: this.saveReplay + saveReplay: this.saveReplay, + latencyMode: this.latencyMode } } } 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 () { enabled: true, allowReplay: false, + latencySetting: { + enabled: false + }, maxDuration: 30, maxInstanceLives: -1, 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 @@ 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 } }) 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 { 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) } }) 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) { 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, -- cgit v1.2.3