From 05a60d85997c108d39bcfb14f1ffd4c74f8b1e93 Mon Sep 17 00:00:00 2001 From: Wicklow <123956049+wickloww@users.noreply.github.com> Date: Fri, 31 Mar 2023 07:12:21 +0000 Subject: Feature/Add replay privacy (#5692) * Add replay settings feature * Fix replay settings behaviour * Fix tests * Fix tests * Fix tests * Update openapi doc and fix tests * Add tests and fix code * Models correction * Add migration and update controller and middleware * Add check params tests * Fix video live middleware * Updated code based on review comments --- server/controllers/api/videos/live.ts | 38 ++- server/initializers/constants.ts | 2 +- server/initializers/database.ts | 2 + .../migrations/0760-video-live-replay-setting.ts | 125 ++++++++++ server/lib/job-queue/handlers/video-live-ending.ts | 15 +- server/lib/live/live-manager.ts | 33 ++- server/middlewares/validators/videos/video-live.ts | 54 ++++- .../sql/video/shared/video-table-attributes.ts | 1 + server/models/video/video-live-replay-setting.ts | 42 ++++ server/models/video/video-live-session.ts | 49 +++- server/models/video/video-live.ts | 57 ++++- server/models/video/video.ts | 1 + server/tests/api/check-params/live.ts | 47 +++- server/tests/api/live/live-constraints.ts | 6 +- server/tests/api/live/live-fast-restream.ts | 1 + server/tests/api/live/live-save-replay.ts | 264 +++++++++++++++------ server/tests/api/live/live.ts | 13 +- .../tests/api/notifications/user-notifications.ts | 2 + server/tests/api/object-storage/live.ts | 1 + .../object-storage/video-static-file-privacy.ts | 12 +- .../tests/api/videos/video-static-file-privacy.ts | 12 +- server/types/express.d.ts | 6 +- server/types/models/video/index.ts | 1 + .../models/video/video-live-replay-setting.ts | 3 + server/types/models/video/video-live-session.ts | 6 +- server/types/models/video/video-live.ts | 9 +- 26 files changed, 690 insertions(+), 112 deletions(-) create mode 100644 server/initializers/migrations/0760-video-live-replay-setting.ts create mode 100644 server/models/video/video-live-replay-setting.ts create mode 100644 server/types/models/video/video-live-replay-setting.ts (limited to 'server') diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts index ec4c073b5..de047d4ec 100644 --- a/server/controllers/api/videos/live.ts +++ b/server/controllers/api/videos/live.ts @@ -16,7 +16,7 @@ import { } from '@server/middlewares/validators/videos/video-live' import { VideoLiveModel } from '@server/models/video/video-live' import { VideoLiveSessionModel } from '@server/models/video/video-live-session' -import { MVideoDetails, MVideoFullLight } from '@server/types/models' +import { MVideoDetails, MVideoFullLight, MVideoLive } from '@server/types/models' import { buildUUID, uuidToShort } from '@shared/extra-utils' import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoState } from '@shared/models' import { logger } from '../../../helpers/logger' @@ -24,6 +24,7 @@ import { sequelizeTypescript } from '../../../initializers/database' import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares' import { VideoModel } from '../../../models/video/video' +import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' const liveRouter = express.Router() @@ -105,7 +106,10 @@ async function updateLiveVideo (req: express.Request, res: express.Response) { const video = res.locals.videoAll const videoLive = res.locals.videoLive - if (exists(body.saveReplay)) videoLive.saveReplay = body.saveReplay + const newReplaySettingModel = await updateReplaySettings(videoLive, body) + if (newReplaySettingModel) videoLive.replaySettingId = newReplaySettingModel.id + else videoLive.replaySettingId = null + if (exists(body.permanentLive)) videoLive.permanentLive = body.permanentLive if (exists(body.latencyMode)) videoLive.latencyMode = body.latencyMode @@ -116,6 +120,27 @@ async function updateLiveVideo (req: express.Request, res: express.Response) { return res.status(HttpStatusCode.NO_CONTENT_204).end() } +async function updateReplaySettings (videoLive: MVideoLive, body: LiveVideoUpdate) { + if (exists(body.saveReplay)) videoLive.saveReplay = body.saveReplay + + // The live replay is not saved anymore, destroy the old model if it existed + if (!videoLive.saveReplay) { + if (videoLive.replaySettingId) { + await VideoLiveReplaySettingModel.removeSettings(videoLive.replaySettingId) + } + + return undefined + } + + const settingModel = videoLive.replaySettingId + ? await VideoLiveReplaySettingModel.load(videoLive.replaySettingId) + : new VideoLiveReplaySettingModel() + + if (exists(body.replaySettings.privacy)) settingModel.privacy = body.replaySettings.privacy + + return settingModel.save() +} + async function addLiveVideo (req: express.Request, res: express.Response) { const videoInfo: LiveVideoCreate = req.body @@ -161,6 +186,15 @@ async function addLiveVideo (req: express.Request, res: express.Response) { // Do not forget to add video channel information to the created video videoCreated.VideoChannel = res.locals.videoChannel + if (videoLive.saveReplay) { + const replaySettings = new VideoLiveReplaySettingModel({ + privacy: videoInfo.replaySettings.privacy + }) + await replaySettings.save(sequelizeOptions) + + videoLive.replaySettingId = replaySettings.id + } + videoLive.videoId = videoCreated.id videoCreated.VideoLive = await videoLive.save(sequelizeOptions) diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 4703e20f2..6cad4eb23 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -26,7 +26,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 755 +const LAST_MIGRATION_VERSION = 760 // --------------------------------------------------------------------------- diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 96145f489..3f31099ed 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -52,6 +52,7 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla import { VideoTagModel } from '../models/video/video-tag' import { VideoViewModel } from '../models/view/video-view' import { CONFIG } from './config' +import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string @@ -141,6 +142,7 @@ async function initDatabaseModels (silent: boolean) { UserVideoHistoryModel, VideoLiveModel, VideoLiveSessionModel, + VideoLiveReplaySettingModel, AccountBlocklistModel, ServerBlocklistModel, UserNotificationModel, diff --git a/server/initializers/migrations/0760-video-live-replay-setting.ts b/server/initializers/migrations/0760-video-live-replay-setting.ts new file mode 100644 index 000000000..7878df3f7 --- /dev/null +++ b/server/initializers/migrations/0760-video-live-replay-setting.ts @@ -0,0 +1,125 @@ +import * as Sequelize from 'sequelize' + +async function up (utils: { + transaction: Sequelize.Transaction + queryInterface: Sequelize.QueryInterface + sequelize: Sequelize.Sequelize +}): Promise { + { + const query = ` + CREATE TABLE IF NOT EXISTS "videoLiveReplaySetting" ( + "id" SERIAL , + "privacy" INTEGER NOT NULL, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, + PRIMARY KEY ("id") + ); + ` + + await utils.sequelize.query(query, { transaction : utils.transaction }) + } + + { + await utils.queryInterface.addColumn('videoLive', 'replaySettingId', { + type: Sequelize.INTEGER, + defaultValue: null, + allowNull: true, + references: { + model: 'videoLiveReplaySetting', + key: 'id' + }, + onDelete: 'SET NULL' + }, { transaction: utils.transaction }) + } + + { + await utils.queryInterface.addColumn('videoLiveSession', 'replaySettingId', { + type: Sequelize.INTEGER, + defaultValue: null, + allowNull: true, + references: { + model: 'videoLiveReplaySetting', + key: 'id' + }, + onDelete: 'SET NULL' + }, { transaction: utils.transaction }) + } + + { + const query = ` + SELECT live."id", v."privacy" + FROM "videoLive" live + INNER JOIN "video" v ON live."videoId" = v."id" + WHERE live."saveReplay" = true + ` + + const videoLives = await utils.sequelize.query<{ id: number, privacy: number }>( + query, + { type: Sequelize.QueryTypes.SELECT, transaction: utils.transaction } + ) + + for (const videoLive of videoLives) { + const query = ` + WITH new_replay_setting AS ( + INSERT INTO "videoLiveReplaySetting" ("privacy", "createdAt", "updatedAt") + VALUES (:privacy, NOW(), NOW()) + RETURNING id + ) + UPDATE "videoLive" SET "replaySettingId" = (SELECT id FROM new_replay_setting) + WHERE "id" = :id + ` + + const options = { + replacements: { privacy: videoLive.privacy, id: videoLive.id }, + type: Sequelize.QueryTypes.UPDATE, + transaction: utils.transaction + } + + await utils.sequelize.query(query, options) + } + } + + { + const query = ` + SELECT session."id", v."privacy" + FROM "videoLiveSession" session + INNER JOIN "video" v ON session."liveVideoId" = v."id" + WHERE session."saveReplay" = true + AND session."liveVideoId" IS NOT NULL; + ` + + const videoLiveSessions = await utils.sequelize.query<{ id: number, privacy: number }>( + query, + { type: Sequelize.QueryTypes.SELECT, transaction: utils.transaction } + ) + + for (const videoLive of videoLiveSessions) { + const query = ` + WITH new_replay_setting AS ( + INSERT INTO "videoLiveReplaySetting" ("privacy", "createdAt", "updatedAt") + VALUES (:privacy, NOW(), NOW()) + RETURNING id + ) + UPDATE "videoLiveSession" SET "replaySettingId" = (SELECT id FROM new_replay_setting) + WHERE "id" = :id + ` + + const options = { + replacements: { privacy: videoLive.privacy, id: videoLive.id }, + type: Sequelize.QueryTypes.UPDATE, + transaction: utils.transaction + } + + await utils.sequelize.query(query, options) + } + } +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index c6263f55a..2f3a971bd 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts @@ -19,6 +19,7 @@ import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@serv import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' import { logger, loggerTagsFactory } from '../../../helpers/logger' import { VideoPathManager } from '@server/lib/video-path-manager' +import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' const lTags = loggerTagsFactory('live', 'job') @@ -60,7 +61,13 @@ async function processVideoLiveEnding (job: Job) { return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId }) } - return replaceLiveByReplay({ video, liveSession, live, permanentLive, replayDirectory: payload.replayDirectory }) + return replaceLiveByReplay({ + video, + liveSession, + live, + permanentLive, + replayDirectory: payload.replayDirectory + }) } // --------------------------------------------------------------------------- @@ -79,6 +86,8 @@ async function saveReplayToExternalVideo (options: { }) { const { liveVideo, liveSession, publishedAt, replayDirectory } = options + const replaySettings = await VideoLiveReplaySettingModel.load(liveSession.replaySettingId) + const replayVideo = new VideoModel({ name: `${liveVideo.name} - ${new Date(publishedAt).toLocaleString()}`, isLive: false, @@ -95,7 +104,7 @@ async function saveReplayToExternalVideo (options: { nsfw: liveVideo.nsfw, description: liveVideo.description, support: liveVideo.support, - privacy: liveVideo.privacy, + privacy: replaySettings.privacy, channelId: liveVideo.channelId }) as MVideoWithAllFiles @@ -142,6 +151,7 @@ async function replaceLiveByReplay (options: { }) { const { video, liveSession, live, permanentLive, replayDirectory } = options + const replaySettings = await VideoLiveReplaySettingModel.load(liveSession.replaySettingId) const videoWithFiles = await VideoModel.loadFull(video.id) const hlsPlaylist = videoWithFiles.getHLSPlaylist() @@ -150,6 +160,7 @@ async function replaceLiveByReplay (options: { await live.destroy() videoWithFiles.isLive = false + videoWithFiles.privacy = replaySettings.privacy videoWithFiles.waitTranscoding = true videoWithFiles.state = VideoState.TO_TRANSCODE diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts index 1d5b8bf14..05274955d 100644 --- a/server/lib/live/live-manager.ts +++ b/server/lib/live/live-manager.ts @@ -19,7 +19,7 @@ import { VideoModel } from '@server/models/video/video' import { VideoLiveModel } from '@server/models/video/video-live' import { VideoLiveSessionModel } from '@server/models/video/video-live-session' import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' -import { MVideo, MVideoLiveSession, MVideoLiveVideo } from '@server/types/models' +import { MVideo, MVideoLiveSession, MVideoLiveVideo, MVideoLiveVideoWithSetting } from '@server/types/models' import { pick, wait } from '@shared/core-utils' import { LiveVideoError, VideoState } from '@shared/models' import { federateVideoIfNeeded } from '../activitypub/videos' @@ -30,6 +30,8 @@ import { Hooks } from '../plugins/hooks' import { LiveQuotaStore } from './live-quota-store' import { cleanupAndDestroyPermanentLive } from './live-utils' import { MuxingSession } from './shared' +import { sequelizeTypescript } from '@server/initializers/database' +import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' const NodeRtmpSession = require('node-media-server/src/node_rtmp_session') const context = require('node-media-server/src/node_core_ctx') @@ -270,7 +272,7 @@ class LiveManager { private async runMuxingSession (options: { sessionId: string - videoLive: MVideoLiveVideo + videoLive: MVideoLiveVideoWithSetting inputUrl: string fps: number @@ -470,15 +472,26 @@ class LiveManager { return resolutionsEnabled } - private saveStartingSession (videoLive: MVideoLiveVideo) { - const liveSession = new VideoLiveSessionModel({ - startDate: new Date(), - liveVideoId: videoLive.videoId, - saveReplay: videoLive.saveReplay, - endingProcessed: false - }) + private async saveStartingSession (videoLive: MVideoLiveVideoWithSetting) { + const replaySettings = videoLive.saveReplay + ? new VideoLiveReplaySettingModel({ + privacy: videoLive.ReplaySetting.privacy + }) + : null - return liveSession.save() + return sequelizeTypescript.transaction(async t => { + if (videoLive.saveReplay) { + await replaySettings.save({ transaction: t }) + } + + return VideoLiveSessionModel.create({ + startDate: new Date(), + liveVideoId: videoLive.videoId, + saveReplay: videoLive.saveReplay, + replaySettingId: videoLive.saveReplay ? replaySettings.id : null, + endingProcessed: false + }, { transaction: t }) + }) } private async saveEndingSession (videoId: number, error: LiveVideoError | null) { diff --git a/server/middlewares/validators/videos/video-live.ts b/server/middlewares/validators/videos/video-live.ts index 328760dde..e80fe1593 100644 --- a/server/middlewares/validators/videos/video-live.ts +++ b/server/middlewares/validators/videos/video-live.ts @@ -17,7 +17,7 @@ import { VideoState } from '@shared/models' import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' -import { isVideoNameValid } from '../../../helpers/custom-validators/videos' +import { isVideoNameValid, isVideoPrivacyValid } from '../../../helpers/custom-validators/videos' import { cleanUpReqFiles } from '../../../helpers/express-utils' import { logger } from '../../../helpers/logger' import { CONFIG } from '../../../initializers/config' @@ -66,6 +66,11 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ .customSanitizer(toBooleanOrNull) .custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'), + body('replaySettings.privacy') + .optional() + .customSanitizer(toIntOrNull) + .custom(isVideoPrivacyValid), + body('permanentLive') .optional() .customSanitizer(toBooleanOrNull) @@ -153,6 +158,11 @@ const videoLiveUpdateValidator = [ .customSanitizer(toBooleanOrNull) .custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'), + body('replaySettings.privacy') + .optional() + .customSanitizer(toIntOrNull) + .custom(isVideoPrivacyValid), + body('latencyMode') .optional() .customSanitizer(toIntOrNull) @@ -177,6 +187,8 @@ const videoLiveUpdateValidator = [ }) } + if (!checkLiveSettingsReplayConsistency({ res, body })) return + if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) { return res.fail({ message: 'Cannot update a live that has already started' }) } @@ -272,3 +284,43 @@ function hasValidLatencyMode (body: LiveVideoUpdate | LiveVideoCreate) { return true } + +function checkLiveSettingsReplayConsistency (options: { + res: express.Response + body: LiveVideoUpdate +}) { + const { res, body } = options + + // We now save replays of this live, so replay settings are mandatory + if (res.locals.videoLive.saveReplay !== true && body.saveReplay === true) { + + if (!exists(body.replaySettings)) { + res.fail({ + status: HttpStatusCode.BAD_REQUEST_400, + message: 'Replay settings are missing now the live replay is saved' + }) + return false + } + + if (!exists(body.replaySettings.privacy)) { + res.fail({ + status: HttpStatusCode.BAD_REQUEST_400, + message: 'Privacy replay setting is missing now the live replay is saved' + }) + return false + } + } + + // Save replay was and is not enabled, so send an error the user if it specified replay settings + if ((!exists(body.saveReplay) && res.locals.videoLive.saveReplay === false) || body.saveReplay === false) { + if (exists(body.replaySettings)) { + res.fail({ + status: HttpStatusCode.BAD_REQUEST_400, + message: 'Cannot save replay settings since live replay is not enabled' + }) + return false + } + } + + return true +} 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 e2c1c0f6d..34967cd20 100644 --- a/server/models/video/sql/video/shared/video-table-attributes.ts +++ b/server/models/video/sql/video/shared/video-table-attributes.ts @@ -160,6 +160,7 @@ export class VideoTableAttributes { 'permanentLive', 'latencyMode', 'videoId', + 'replaySettingId', 'createdAt', 'updatedAt' ] diff --git a/server/models/video/video-live-replay-setting.ts b/server/models/video/video-live-replay-setting.ts new file mode 100644 index 000000000..1c824dfa2 --- /dev/null +++ b/server/models/video/video-live-replay-setting.ts @@ -0,0 +1,42 @@ +import { isVideoPrivacyValid } from '@server/helpers/custom-validators/videos' +import { MLiveReplaySetting } from '@server/types/models/video/video-live-replay-setting' +import { VideoPrivacy } from '@shared/models/videos/video-privacy.enum' +import { Transaction } from 'sequelize' +import { AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { throwIfNotValid } from '../shared/sequelize-helpers' + +@Table({ + tableName: 'videoLiveReplaySetting' +}) +export class VideoLiveReplaySettingModel extends Model { + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @AllowNull(false) + @Is('VideoPrivacy', value => throwIfNotValid(value, isVideoPrivacyValid, 'privacy')) + @Column + privacy: VideoPrivacy + + static load (id: number, transaction?: Transaction): Promise { + return VideoLiveReplaySettingModel.findOne({ + where: { id }, + transaction + }) + } + + static removeSettings (id: number) { + return VideoLiveReplaySettingModel.destroy({ + where: { id } + }) + } + + toFormattedJSON () { + return { + privacy: this.privacy + } + } +} diff --git a/server/models/video/video-live-session.ts b/server/models/video/video-live-session.ts index ed386052b..dcded7872 100644 --- a/server/models/video/video-live-session.ts +++ b/server/models/video/video-live-session.ts @@ -1,10 +1,23 @@ import { FindOptions } from 'sequelize' -import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' +import { + AllowNull, + BeforeDestroy, + BelongsTo, + Column, + CreatedAt, + DataType, + ForeignKey, + Model, + Scopes, + Table, + UpdatedAt +} from 'sequelize-typescript' import { MVideoLiveSession, MVideoLiveSessionReplay } from '@server/types/models' import { uuidToShort } from '@shared/extra-utils' import { LiveVideoError, LiveVideoSession } from '@shared/models' import { AttributesOnly } from '@shared/typescript-utils' import { VideoModel } from './video' +import { VideoLiveReplaySettingModel } from './video-live-replay-setting' export enum ScopeNames { WITH_REPLAY = 'WITH_REPLAY' @@ -17,6 +30,10 @@ export enum ScopeNames { model: VideoModel.unscoped(), as: 'ReplayVideo', required: false + }, + { + model: VideoLiveReplaySettingModel, + required: false } ] } @@ -30,6 +47,10 @@ export enum ScopeNames { }, { fields: [ 'liveVideoId' ] + }, + { + fields: [ 'replaySettingId' ], + unique: true } ] }) @@ -89,6 +110,27 @@ export class VideoLiveSessionModel extends Model VideoLiveReplaySettingModel) + @Column + replaySettingId: number + + @BelongsTo(() => VideoLiveReplaySettingModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'set null' + }) + ReplaySetting: VideoLiveReplaySettingModel + + @BeforeDestroy + static deleteReplaySetting (instance: VideoLiveSessionModel) { + return VideoLiveReplaySettingModel.destroy({ + where: { + id: instance.replaySettingId + } + }) + } + static load (id: number): Promise { return VideoLiveSessionModel.findOne({ where: { id } @@ -146,6 +188,10 @@ export class VideoLiveSessionModel extends Model ({ include: [ @@ -18,6 +31,10 @@ import { VideoBlacklistModel } from './video-blacklist' required: false } ] + }, + { + model: VideoLiveReplaySettingModel, + required: false } ] })) @@ -27,6 +44,10 @@ import { VideoBlacklistModel } from './video-blacklist' { fields: [ 'videoId' ], unique: true + }, + { + fields: [ 'replaySettingId' ], + unique: true } ] }) @@ -66,6 +87,27 @@ export class VideoLiveModel extends Model }) Video: VideoModel + @ForeignKey(() => VideoLiveReplaySettingModel) + @Column + replaySettingId: number + + @BelongsTo(() => VideoLiveReplaySettingModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'set null' + }) + ReplaySetting: VideoLiveReplaySettingModel + + @BeforeDestroy + static deleteReplaySetting (instance: VideoLiveModel) { + return VideoLiveReplaySettingModel.destroy({ + where: { + id: instance.replaySettingId + } + }) + } + static loadByStreamKey (streamKey: string) { const query = { where: { @@ -84,11 +126,15 @@ export class VideoLiveModel extends Model required: false } ] + }, + { + model: VideoLiveReplaySettingModel.unscoped(), + required: false } ] } - return VideoLiveModel.findOne(query) + return VideoLiveModel.findOne(query) } static loadByVideoId (videoId: number) { @@ -120,11 +166,16 @@ export class VideoLiveModel extends Model } } + const replaySettings = this.replaySettingId + ? this.ReplaySetting.toFormattedJSON() + : undefined + return { ...privateInformation, permanentLive: this.permanentLive, saveReplay: this.saveReplay, + replaySettings, latencyMode: this.latencyMode } } diff --git a/server/models/video/video.ts b/server/models/video/video.ts index aa9c62e36..0c5ed64ec 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -706,6 +706,7 @@ export class VideoModel extends Model>> { name: 'videoId', allowNull: false }, + hooks: true, onDelete: 'cascade' }) VideoLive: VideoLiveModel diff --git a/server/tests/api/check-params/live.ts b/server/tests/api/check-params/live.ts index 2eff9414b..81f10ed8e 100644 --- a/server/tests/api/check-params/live.ts +++ b/server/tests/api/check-params/live.ts @@ -83,6 +83,7 @@ describe('Test video lives API validator', function () { privacy: VideoPrivacy.PUBLIC, channelId, saveReplay: false, + replaySettings: undefined, permanentLive: false, latencyMode: LiveVideoLatencyMode.DEFAULT } @@ -141,6 +142,12 @@ describe('Test video lives API validator', function () { await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) }) + it('Should fail with a bad privacy for replay settings', async function () { + const fields = { ...baseCorrectParams, replaySettings: { privacy: 5 } } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + it('Should fail with another user channel', async function () { const user = { username: 'fake', @@ -256,7 +263,7 @@ describe('Test video lives API validator', function () { }) it('Should forbid to save replay if not enabled by the admin', async function () { - const fields = { ...baseCorrectParams, saveReplay: true } + const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } await server.config.updateCustomSubConfig({ newConfig: { @@ -277,7 +284,7 @@ describe('Test video lives API validator', function () { }) it('Should allow to save replay if enabled by the admin', async function () { - const fields = { ...baseCorrectParams, saveReplay: true } + const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } await server.config.updateCustomSubConfig({ newConfig: { @@ -464,6 +471,39 @@ describe('Test video lives API validator', function () { await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) }) + it('Should fail with a bad privacy for replay settings', async function () { + const fields = { saveReplay: true, replaySettings: { privacy: 5 } } + + await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with save replay enabled but without replay settings', async function () { + await server.config.updateCustomSubConfig({ + newConfig: { + live: { + enabled: true, + allowReplay: true + } + } + }) + + const fields = { saveReplay: true } + + await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with save replay disabled and replay settings', async function () { + const fields = { saveReplay: false, replaySettings: { privacy: VideoPrivacy.INTERNAL } } + + await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with only replay settings when save replay is disabled', async function () { + const fields = { replaySettings: { privacy: VideoPrivacy.INTERNAL } } + + 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 } @@ -474,6 +514,9 @@ describe('Test video lives API validator', function () { await command.update({ videoId: video.id, fields: { saveReplay: false } }) await command.update({ videoId: video.uuid, fields: { saveReplay: false } }) await command.update({ videoId: video.shortUUID, fields: { saveReplay: false } }) + + await command.update({ videoId: video.id, fields: { saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } }) + }) it('Should fail to update replay status if replay is not allowed on the instance', async function () { diff --git a/server/tests/api/live/live-constraints.ts b/server/tests/api/live/live-constraints.ts index c82585a9e..fabb8798d 100644 --- a/server/tests/api/live/live-constraints.ts +++ b/server/tests/api/live/live-constraints.ts @@ -24,10 +24,7 @@ describe('Test live constraints', function () { let userAccessToken: string let userChannelId: number - async function createLiveWrapper (options: { - replay: boolean - permanent: boolean - }) { + async function createLiveWrapper (options: { replay: boolean, permanent: boolean }) { const { replay, permanent } = options const liveAttributes = { @@ -35,6 +32,7 @@ describe('Test live constraints', function () { channelId: userChannelId, privacy: VideoPrivacy.PUBLIC, saveReplay: replay, + replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined, permanentLive: permanent } diff --git a/server/tests/api/live/live-fast-restream.ts b/server/tests/api/live/live-fast-restream.ts index 9e6d10dbd..4e30feaef 100644 --- a/server/tests/api/live/live-fast-restream.ts +++ b/server/tests/api/live/live-fast-restream.ts @@ -23,6 +23,7 @@ describe('Fast restream in live', function () { privacy: VideoPrivacy.PUBLIC, name: 'my super live', saveReplay: options.replay, + replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined, permanentLive: options.permanent } diff --git a/server/tests/api/live/live-save-replay.ts b/server/tests/api/live/live-save-replay.ts index 8f17b4566..3a9a84f7e 100644 --- a/server/tests/api/live/live-save-replay.ts +++ b/server/tests/api/live/live-save-replay.ts @@ -27,7 +27,7 @@ describe('Save replay setting', function () { let liveVideoUUID: string let ffmpegCommand: FfmpegCommand - async function createLiveWrapper (options: { permanent: boolean, replay: boolean }) { + async function createLiveWrapper (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacy } }) { if (liveVideoUUID) { try { await servers[0].videos.remove({ id: liveVideoUUID }) @@ -40,6 +40,7 @@ describe('Save replay setting', function () { privacy: VideoPrivacy.PUBLIC, name: 'my super live', saveReplay: options.replay, + replaySettings: options.replaySettings, permanentLive: options.permanent } @@ -47,7 +48,7 @@ describe('Save replay setting', function () { return uuid } - async function publishLive (options: { permanent: boolean, replay: boolean }) { + async function publishLive (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacy } }) { liveVideoUUID = await createLiveWrapper(options) const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) @@ -61,7 +62,7 @@ describe('Save replay setting', function () { return { ffmpegCommand, liveDetails } } - async function publishLiveAndDelete (options: { permanent: boolean, replay: boolean }) { + async function publishLiveAndDelete (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacy } }) { const { ffmpegCommand, liveDetails } = await publishLive(options) await Promise.all([ @@ -76,7 +77,7 @@ describe('Save replay setting', function () { return { liveDetails } } - async function publishLiveAndBlacklist (options: { permanent: boolean, replay: boolean }) { + async function publishLiveAndBlacklist (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacy } }) { const { ffmpegCommand, liveDetails } = await publishLive(options) await Promise.all([ @@ -112,6 +113,13 @@ describe('Save replay setting', function () { } } + async function checkVideoPrivacy (videoId: string, privacy: VideoPrivacy) { + for (const server of servers) { + const video = await server.videos.get({ id: videoId }) + expect(video.privacy.id).to.equal(privacy) + } + } + before(async function () { this.timeout(120000) @@ -247,12 +255,13 @@ describe('Save replay setting', function () { it('Should correctly create and federate the "waiting for stream" live', async function () { this.timeout(20000) - liveVideoUUID = await createLiveWrapper({ permanent: false, replay: true }) + liveVideoUUID = await createLiveWrapper({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } }) await waitJobs(servers) await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) + await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) }) it('Should correctly have updated the live and federated it when streaming in the live', async function () { @@ -265,6 +274,7 @@ describe('Save replay setting', function () { await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) + await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) }) it('Should correctly have saved the live and federated it after the streaming', async function () { @@ -274,6 +284,8 @@ describe('Save replay setting', function () { expect(session.endDate).to.not.exist expect(session.endingProcessed).to.be.false expect(session.saveReplay).to.be.true + expect(session.replaySettings).to.exist + expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED) await stopFfmpeg(ffmpegCommand) @@ -281,8 +293,9 @@ describe('Save replay setting', function () { await waitJobs(servers) // Live has been transcoded - await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) + await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) + await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.UNLISTED) }) it('Should find the replay live session', async function () { @@ -296,6 +309,8 @@ describe('Save replay setting', function () { expect(session.error).to.not.exist expect(session.saveReplay).to.be.true expect(session.endingProcessed).to.be.true + expect(session.replaySettings).to.exist + expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED) expect(session.replayVideo).to.exist expect(session.replayVideo.id).to.exist @@ -306,13 +321,14 @@ describe('Save replay setting', function () { it('Should update the saved live and correctly federate the updated attributes', async function () { this.timeout(30000) - await servers[0].videos.update({ id: liveVideoUUID, attributes: { name: 'video updated' } }) + await servers[0].videos.update({ id: liveVideoUUID, attributes: { name: 'video updated', privacy: VideoPrivacy.PUBLIC } }) await waitJobs(servers) for (const server of servers) { const video = await server.videos.get({ id: liveVideoUUID }) expect(video.name).to.equal('video updated') expect(video.isLive).to.be.false + expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC) } }) @@ -323,7 +339,7 @@ describe('Save replay setting', function () { it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { this.timeout(120000) - await publishLiveAndBlacklist({ permanent: false, replay: true }) + await publishLiveAndBlacklist({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }) await checkVideosExist(liveVideoUUID, false) @@ -338,7 +354,7 @@ describe('Save replay setting', function () { it('Should correctly terminate the stream on delete and delete the video', async function () { this.timeout(40000) - await publishLiveAndDelete({ permanent: false, replay: true }) + await publishLiveAndDelete({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }) await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) @@ -348,103 +364,201 @@ describe('Save replay setting', function () { describe('With save replay enabled on permanent live', function () { let lastReplayUUID: string - it('Should correctly create and federate the "waiting for stream" live', async function () { - this.timeout(20000) + describe('With a first live and its replay', function () { - liveVideoUUID = await createLiveWrapper({ permanent: true, replay: true }) + it('Should correctly create and federate the "waiting for stream" live', async function () { + this.timeout(20000) - await waitJobs(servers) + liveVideoUUID = await createLiveWrapper({ permanent: true, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } }) - await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) - await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) - }) + await waitJobs(servers) - it('Should correctly have updated the live and federated it when streaming in the live', async function () { - this.timeout(20000) + await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) + await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) + await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) + }) - ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) - await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) + it('Should correctly have updated the live and federated it when streaming in the live', async function () { + this.timeout(20000) - await waitJobs(servers) + ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) + await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) - await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) - await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) - }) + await waitJobs(servers) - it('Should correctly have saved the live and federated it after the streaming', async function () { - this.timeout(30000) + await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) + await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) + await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) + }) - const liveDetails = await servers[0].videos.get({ id: liveVideoUUID }) + it('Should correctly have saved the live and federated it after the streaming', async function () { + this.timeout(30000) - await stopFfmpeg(ffmpegCommand) + const liveDetails = await servers[0].videos.get({ id: liveVideoUUID }) - await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID) - await waitJobs(servers) + await stopFfmpeg(ffmpegCommand) - const video = await findExternalSavedVideo(servers[0], liveDetails) - expect(video).to.exist + await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID) + await waitJobs(servers) - for (const server of servers) { - await server.videos.get({ id: video.uuid }) - } + const video = await findExternalSavedVideo(servers[0], liveDetails) + expect(video).to.exist - lastReplayUUID = video.uuid - }) + for (const server of servers) { + await server.videos.get({ id: video.uuid }) + } - it('Should have appropriate ended session and replay live session', async function () { - const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID }) - expect(total).to.equal(1) - expect(data).to.have.lengthOf(1) + lastReplayUUID = video.uuid + }) - const sessionFromLive = data[0] - const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID }) + it('Should have appropriate ended session and replay live session', async function () { + const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID }) + expect(total).to.equal(1) + expect(data).to.have.lengthOf(1) - for (const session of [ sessionFromLive, sessionFromReplay ]) { - expect(session.startDate).to.exist - expect(session.endDate).to.exist + const sessionFromLive = data[0] + const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID }) - expect(session.error).to.not.exist + for (const session of [ sessionFromLive, sessionFromReplay ]) { + expect(session.startDate).to.exist + expect(session.endDate).to.exist - expect(session.replayVideo).to.exist - expect(session.replayVideo.id).to.exist - expect(session.replayVideo.shortUUID).to.exist - expect(session.replayVideo.uuid).to.equal(lastReplayUUID) - } - }) + expect(session.replaySettings).to.exist + expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED) - it('Should have cleaned up the live files', async function () { - await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) + expect(session.error).to.not.exist + + expect(session.replayVideo).to.exist + expect(session.replayVideo.id).to.exist + expect(session.replayVideo.shortUUID).to.exist + expect(session.replayVideo.uuid).to.equal(lastReplayUUID) + } + }) + + it('Should have the first live replay with correct settings', async function () { + await checkVideosExist(lastReplayUUID, false, HttpStatusCode.OK_200) + await checkVideoState(lastReplayUUID, VideoState.PUBLISHED) + await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.UNLISTED) + }) }) - it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { - this.timeout(120000) + describe('With a second live and its replay', function () { + it('Should update the replay settings', async function () { + await servers[0].live.update( + { videoId: liveVideoUUID, fields: { replaySettings: { privacy: VideoPrivacy.PUBLIC } } }) + await waitJobs(servers) + const live = await servers[0].live.get({ videoId: liveVideoUUID }) - await servers[0].videos.remove({ id: lastReplayUUID }) - const { liveDetails } = await publishLiveAndBlacklist({ permanent: true, replay: true }) + expect(live.saveReplay).to.be.true + expect(live.replaySettings).to.exist + expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC) - const replay = await findExternalSavedVideo(servers[0], liveDetails) - expect(replay).to.exist + }) - for (const videoId of [ liveVideoUUID, replay.uuid ]) { - await checkVideosExist(videoId, false) + it('Should correctly have updated the live and federated it when streaming in the live', async function () { + this.timeout(20000) - await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) - await servers[1].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) - } + ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) + await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) - await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) - }) + await waitJobs(servers) - it('Should correctly terminate the stream on delete and not save the video', async function () { - this.timeout(40000) + await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) + await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) + await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) + }) - const { liveDetails } = await publishLiveAndDelete({ permanent: true, replay: true }) + it('Should correctly have saved the live and federated it after the streaming', async function () { + this.timeout(30000) + const liveDetails = await servers[0].videos.get({ id: liveVideoUUID }) - const replay = await findExternalSavedVideo(servers[0], liveDetails) - expect(replay).to.not.exist + await stopFfmpeg(ffmpegCommand) - await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) - await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) + await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID) + await waitJobs(servers) + + const video = await findExternalSavedVideo(servers[0], liveDetails) + expect(video).to.exist + + for (const server of servers) { + await server.videos.get({ id: video.uuid }) + } + + lastReplayUUID = video.uuid + }) + + it('Should have appropriate ended session and replay live session', async function () { + const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID }) + expect(total).to.equal(2) + expect(data).to.have.lengthOf(2) + + const sessionFromLive = data[1] + const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID }) + + for (const session of [ sessionFromLive, sessionFromReplay ]) { + expect(session.startDate).to.exist + expect(session.endDate).to.exist + + expect(session.replaySettings).to.exist + expect(session.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC) + + expect(session.error).to.not.exist + + expect(session.replayVideo).to.exist + expect(session.replayVideo.id).to.exist + expect(session.replayVideo.shortUUID).to.exist + expect(session.replayVideo.uuid).to.equal(lastReplayUUID) + } + }) + + it('Should have the first live replay with correct settings', async function () { + await checkVideosExist(lastReplayUUID, true, HttpStatusCode.OK_200) + await checkVideoState(lastReplayUUID, VideoState.PUBLISHED) + await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.PUBLIC) + }) + + it('Should have cleaned up the live files', async function () { + await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) + }) + + it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { + this.timeout(120000) + + await servers[0].videos.remove({ id: lastReplayUUID }) + const { liveDetails } = await publishLiveAndBlacklist({ + permanent: true, + replay: true, + replaySettings: { privacy: VideoPrivacy.PUBLIC } + }) + + const replay = await findExternalSavedVideo(servers[0], liveDetails) + expect(replay).to.exist + + for (const videoId of [ liveVideoUUID, replay.uuid ]) { + await checkVideosExist(videoId, false) + + await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + await servers[1].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + } + + await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) + }) + + it('Should correctly terminate the stream on delete and not save the video', async function () { + this.timeout(40000) + + const { liveDetails } = await publishLiveAndDelete({ + permanent: true, + replay: true, + replaySettings: { privacy: VideoPrivacy.PUBLIC } + }) + + const replay = await findExternalSavedVideo(servers[0], liveDetails) + expect(replay).to.not.exist + + await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) + await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) + }) }) }) diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index 003cc934f..ceb606af1 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts @@ -87,6 +87,7 @@ describe('Test live', function () { commentsEnabled: false, downloadEnabled: false, saveReplay: true, + replaySettings: { privacy: VideoPrivacy.PUBLIC }, latencyMode: LiveVideoLatencyMode.SMALL_LATENCY, privacy: VideoPrivacy.PUBLIC, previewfile: 'video_short1-preview.webm.jpg', @@ -128,6 +129,9 @@ describe('Test live', function () { if (server.url === servers[0].url) { expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live') expect(live.streamKey).to.not.be.empty + + expect(live.replaySettings).to.exist + expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC) } else { expect(live.rtmpUrl).to.not.exist expect(live.streamKey).to.not.exist @@ -196,6 +200,7 @@ describe('Test live', function () { } expect(live.saveReplay).to.be.false + expect(live.replaySettings).to.not.exist expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT) } }) @@ -366,7 +371,10 @@ describe('Test live', function () { name: 'live video', channelId: servers[0].store.channel.id, privacy: VideoPrivacy.PUBLIC, - saveReplay + saveReplay, + replaySettings: saveReplay + ? { privacy: VideoPrivacy.PUBLIC } + : undefined } const { uuid } = await commands[0].create({ fields: liveAttributes }) @@ -670,6 +678,9 @@ describe('Test live', function () { channelId: servers[0].store.channel.id, privacy: VideoPrivacy.PUBLIC, saveReplay: options.saveReplay, + replaySettings: options.saveReplay + ? { privacy: VideoPrivacy.PUBLIC } + : undefined, permanentLive: options.permanent } diff --git a/server/tests/api/notifications/user-notifications.ts b/server/tests/api/notifications/user-notifications.ts index f945cb6a8..7a8a234c2 100644 --- a/server/tests/api/notifications/user-notifications.ts +++ b/server/tests/api/notifications/user-notifications.ts @@ -342,6 +342,7 @@ describe('Test user notifications', function () { privacy: VideoPrivacy.PUBLIC, channelId: servers[1].store.channel.id, saveReplay: true, + replaySettings: { privacy: VideoPrivacy.PUBLIC }, permanentLive: false } }) @@ -367,6 +368,7 @@ describe('Test user notifications', function () { privacy: VideoPrivacy.PUBLIC, channelId: servers[1].store.channel.id, saveReplay: true, + replaySettings: { privacy: VideoPrivacy.PUBLIC }, permanentLive: true } }) diff --git a/server/tests/api/object-storage/live.ts b/server/tests/api/object-storage/live.ts index 2a3fc4779..588e0a8d7 100644 --- a/server/tests/api/object-storage/live.ts +++ b/server/tests/api/object-storage/live.ts @@ -27,6 +27,7 @@ async function createLive (server: PeerTubeServer, permanent: boolean) { privacy: VideoPrivacy.PUBLIC, name: 'my super live', saveReplay: true, + replaySettings: { privacy: VideoPrivacy.PUBLIC }, permanentLive: permanent } diff --git a/server/tests/api/object-storage/video-static-file-privacy.ts b/server/tests/api/object-storage/video-static-file-privacy.ts index 869d437d5..930c88543 100644 --- a/server/tests/api/object-storage/video-static-file-privacy.ts +++ b/server/tests/api/object-storage/video-static-file-privacy.ts @@ -305,13 +305,21 @@ describe('Object storage for video static file privacy', function () { }) { - const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: false, privacy: VideoPrivacy.PRIVATE }) + const { video, live } = await server.live.quickCreate({ + saveReplay: true, + permanentLive: false, + privacy: VideoPrivacy.PRIVATE + }) normalLiveId = video.uuid normalLive = live } { - const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: true, privacy: VideoPrivacy.PRIVATE }) + const { video, live } = await server.live.quickCreate({ + saveReplay: true, + permanentLive: true, + privacy: VideoPrivacy.PRIVATE + }) permanentLiveId = video.uuid permanentLive = live } diff --git a/server/tests/api/videos/video-static-file-privacy.ts b/server/tests/api/videos/video-static-file-privacy.ts index 16530884e..2dcfbbc57 100644 --- a/server/tests/api/videos/video-static-file-privacy.ts +++ b/server/tests/api/videos/video-static-file-privacy.ts @@ -364,13 +364,21 @@ describe('Test video static file privacy', function () { }) { - const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: false, privacy: VideoPrivacy.PRIVATE }) + const { video, live } = await server.live.quickCreate({ + saveReplay: true, + permanentLive: false, + privacy: VideoPrivacy.PRIVATE + }) normalLiveId = video.uuid normalLive = live } { - const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: true, privacy: VideoPrivacy.PRIVATE }) + const { video, live } = await server.live.quickCreate({ + saveReplay: true, + permanentLive: true, + privacy: VideoPrivacy.PRIVATE + }) permanentLiveId = video.uuid permanentLive = live } diff --git a/server/types/express.d.ts b/server/types/express.d.ts index c1c379b98..a992a9926 100644 --- a/server/types/express.d.ts +++ b/server/types/express.d.ts @@ -1,4 +1,5 @@ import { OutgoingHttpHeaders } from 'http' +import { Writable } from 'stream' import { RegisterServerAuthExternalOptions } from '@server/types' import { MAbuseMessage, @@ -16,7 +17,7 @@ import { MVideoFormattableDetails, MVideoId, MVideoImmutable, - MVideoLive, + MVideoLiveFormattable, MVideoPlaylistFull, MVideoPlaylistFullSummary } from '@server/types/models' @@ -43,7 +44,6 @@ import { MVideoShareActor, MVideoThumbnail } from './models' -import { Writable } from 'stream' import { MVideoSource } from './models/video/video-source' declare module 'express' { @@ -124,7 +124,7 @@ declare module 'express' { onlyVideo?: MVideoThumbnail videoId?: MVideoId - videoLive?: MVideoLive + videoLive?: MVideoLiveFormattable videoLiveSession?: MVideoLiveSession videoShare?: MVideoShareActor diff --git a/server/types/models/video/index.ts b/server/types/models/video/index.ts index 940f0ac0d..6e45fcc79 100644 --- a/server/types/models/video/index.ts +++ b/server/types/models/video/index.ts @@ -13,6 +13,7 @@ export * from './video-channels' export * from './video-comment' export * from './video-file' export * from './video-import' +export * from './video-live-replay-setting' export * from './video-live-session' export * from './video-live' export * from './video-playlist' diff --git a/server/types/models/video/video-live-replay-setting.ts b/server/types/models/video/video-live-replay-setting.ts new file mode 100644 index 000000000..c5a5adf54 --- /dev/null +++ b/server/types/models/video/video-live-replay-setting.ts @@ -0,0 +1,3 @@ +import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' + +export type MLiveReplaySetting = Omit diff --git a/server/types/models/video/video-live-session.ts b/server/types/models/video/video-live-session.ts index 2e5e4b684..852e2c24b 100644 --- a/server/types/models/video/video-live-session.ts +++ b/server/types/models/video/video-live-session.ts @@ -1,15 +1,17 @@ import { VideoLiveSessionModel } from '@server/models/video/video-live-session' import { PickWith } from '@shared/typescript-utils' import { MVideo } from './video' +import { MLiveReplaySetting } from './video-live-replay-setting' type Use = PickWith // ############################################################################ -export type MVideoLiveSession = Omit +export type MVideoLiveSession = Omit // ############################################################################ export type MVideoLiveSessionReplay = MVideoLiveSession & - Use<'ReplayVideo', MVideo> + Use<'ReplayVideo', MVideo> & + Use<'ReplaySetting', MLiveReplaySetting> diff --git a/server/types/models/video/video-live.ts b/server/types/models/video/video-live.ts index 903cea982..a899edfa6 100644 --- a/server/types/models/video/video-live.ts +++ b/server/types/models/video/video-live.ts @@ -1,15 +1,22 @@ import { VideoLiveModel } from '@server/models/video/video-live' import { PickWith } from '@shared/typescript-utils' import { MVideo } from './video' +import { MLiveReplaySetting } from './video-live-replay-setting' type Use = PickWith // ############################################################################ -export type MVideoLive = Omit +export type MVideoLive = Omit // ############################################################################ export type MVideoLiveVideo = MVideoLive & Use<'Video', MVideo> + +// ############################################################################ + +export type MVideoLiveVideoWithSetting = + MVideoLiveVideo & + Use<'ReplaySetting', MLiveReplaySetting> -- cgit v1.2.3