diff options
author | Wicklow <123956049+wickloww@users.noreply.github.com> | 2023-03-31 07:12:21 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-03-31 09:12:21 +0200 |
commit | 05a60d85997c108d39bcfb14f1ffd4c74f8b1e93 (patch) | |
tree | 5041a95ef945620a17f25ba934064b41f6bb00b7 /server | |
parent | ebd61437c1ec92bea9772924c7051cb00d71f778 (diff) | |
download | PeerTube-05a60d85997c108d39bcfb14f1ffd4c74f8b1e93.tar.gz PeerTube-05a60d85997c108d39bcfb14f1ffd4c74f8b1e93.tar.zst PeerTube-05a60d85997c108d39bcfb14f1ffd4c74f8b1e93.zip |
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
Diffstat (limited to 'server')
26 files changed, 690 insertions, 112 deletions
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 { | |||
16 | } from '@server/middlewares/validators/videos/video-live' | 16 | } from '@server/middlewares/validators/videos/video-live' |
17 | import { VideoLiveModel } from '@server/models/video/video-live' | 17 | import { VideoLiveModel } from '@server/models/video/video-live' |
18 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' | 18 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' |
19 | import { MVideoDetails, MVideoFullLight } from '@server/types/models' | 19 | import { MVideoDetails, MVideoFullLight, MVideoLive } from '@server/types/models' |
20 | import { buildUUID, uuidToShort } from '@shared/extra-utils' | 20 | import { buildUUID, uuidToShort } from '@shared/extra-utils' |
21 | import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoState } from '@shared/models' | 21 | import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoState } from '@shared/models' |
22 | import { logger } from '../../../helpers/logger' | 22 | import { logger } from '../../../helpers/logger' |
@@ -24,6 +24,7 @@ import { sequelizeTypescript } from '../../../initializers/database' | |||
24 | import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' | 24 | import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' |
25 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares' | 25 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares' |
26 | import { VideoModel } from '../../../models/video/video' | 26 | import { VideoModel } from '../../../models/video/video' |
27 | import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' | ||
27 | 28 | ||
28 | const liveRouter = express.Router() | 29 | const liveRouter = express.Router() |
29 | 30 | ||
@@ -105,7 +106,10 @@ async function updateLiveVideo (req: express.Request, res: express.Response) { | |||
105 | const video = res.locals.videoAll | 106 | const video = res.locals.videoAll |
106 | const videoLive = res.locals.videoLive | 107 | const videoLive = res.locals.videoLive |
107 | 108 | ||
108 | if (exists(body.saveReplay)) videoLive.saveReplay = body.saveReplay | 109 | const newReplaySettingModel = await updateReplaySettings(videoLive, body) |
110 | if (newReplaySettingModel) videoLive.replaySettingId = newReplaySettingModel.id | ||
111 | else videoLive.replaySettingId = null | ||
112 | |||
109 | if (exists(body.permanentLive)) videoLive.permanentLive = body.permanentLive | 113 | if (exists(body.permanentLive)) videoLive.permanentLive = body.permanentLive |
110 | if (exists(body.latencyMode)) videoLive.latencyMode = body.latencyMode | 114 | if (exists(body.latencyMode)) videoLive.latencyMode = body.latencyMode |
111 | 115 | ||
@@ -116,6 +120,27 @@ async function updateLiveVideo (req: express.Request, res: express.Response) { | |||
116 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | 120 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
117 | } | 121 | } |
118 | 122 | ||
123 | async function updateReplaySettings (videoLive: MVideoLive, body: LiveVideoUpdate) { | ||
124 | if (exists(body.saveReplay)) videoLive.saveReplay = body.saveReplay | ||
125 | |||
126 | // The live replay is not saved anymore, destroy the old model if it existed | ||
127 | if (!videoLive.saveReplay) { | ||
128 | if (videoLive.replaySettingId) { | ||
129 | await VideoLiveReplaySettingModel.removeSettings(videoLive.replaySettingId) | ||
130 | } | ||
131 | |||
132 | return undefined | ||
133 | } | ||
134 | |||
135 | const settingModel = videoLive.replaySettingId | ||
136 | ? await VideoLiveReplaySettingModel.load(videoLive.replaySettingId) | ||
137 | : new VideoLiveReplaySettingModel() | ||
138 | |||
139 | if (exists(body.replaySettings.privacy)) settingModel.privacy = body.replaySettings.privacy | ||
140 | |||
141 | return settingModel.save() | ||
142 | } | ||
143 | |||
119 | async function addLiveVideo (req: express.Request, res: express.Response) { | 144 | async function addLiveVideo (req: express.Request, res: express.Response) { |
120 | const videoInfo: LiveVideoCreate = req.body | 145 | const videoInfo: LiveVideoCreate = req.body |
121 | 146 | ||
@@ -161,6 +186,15 @@ async function addLiveVideo (req: express.Request, res: express.Response) { | |||
161 | // Do not forget to add video channel information to the created video | 186 | // Do not forget to add video channel information to the created video |
162 | videoCreated.VideoChannel = res.locals.videoChannel | 187 | videoCreated.VideoChannel = res.locals.videoChannel |
163 | 188 | ||
189 | if (videoLive.saveReplay) { | ||
190 | const replaySettings = new VideoLiveReplaySettingModel({ | ||
191 | privacy: videoInfo.replaySettings.privacy | ||
192 | }) | ||
193 | await replaySettings.save(sequelizeOptions) | ||
194 | |||
195 | videoLive.replaySettingId = replaySettings.id | ||
196 | } | ||
197 | |||
164 | videoLive.videoId = videoCreated.id | 198 | videoLive.videoId = videoCreated.id |
165 | videoCreated.VideoLive = await videoLive.save(sequelizeOptions) | 199 | videoCreated.VideoLive = await videoLive.save(sequelizeOptions) |
166 | 200 | ||
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' | |||
26 | 26 | ||
27 | // --------------------------------------------------------------------------- | 27 | // --------------------------------------------------------------------------- |
28 | 28 | ||
29 | const LAST_MIGRATION_VERSION = 755 | 29 | const LAST_MIGRATION_VERSION = 760 |
30 | 30 | ||
31 | // --------------------------------------------------------------------------- | 31 | // --------------------------------------------------------------------------- |
32 | 32 | ||
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 | |||
52 | import { VideoTagModel } from '../models/video/video-tag' | 52 | import { VideoTagModel } from '../models/video/video-tag' |
53 | import { VideoViewModel } from '../models/view/video-view' | 53 | import { VideoViewModel } from '../models/view/video-view' |
54 | import { CONFIG } from './config' | 54 | import { CONFIG } from './config' |
55 | import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' | ||
55 | 56 | ||
56 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 57 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
57 | 58 | ||
@@ -141,6 +142,7 @@ async function initDatabaseModels (silent: boolean) { | |||
141 | UserVideoHistoryModel, | 142 | UserVideoHistoryModel, |
142 | VideoLiveModel, | 143 | VideoLiveModel, |
143 | VideoLiveSessionModel, | 144 | VideoLiveSessionModel, |
145 | VideoLiveReplaySettingModel, | ||
144 | AccountBlocklistModel, | 146 | AccountBlocklistModel, |
145 | ServerBlocklistModel, | 147 | ServerBlocklistModel, |
146 | UserNotificationModel, | 148 | 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | { | ||
9 | const query = ` | ||
10 | CREATE TABLE IF NOT EXISTS "videoLiveReplaySetting" ( | ||
11 | "id" SERIAL , | ||
12 | "privacy" INTEGER NOT NULL, | ||
13 | "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
14 | "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
15 | PRIMARY KEY ("id") | ||
16 | ); | ||
17 | ` | ||
18 | |||
19 | await utils.sequelize.query(query, { transaction : utils.transaction }) | ||
20 | } | ||
21 | |||
22 | { | ||
23 | await utils.queryInterface.addColumn('videoLive', 'replaySettingId', { | ||
24 | type: Sequelize.INTEGER, | ||
25 | defaultValue: null, | ||
26 | allowNull: true, | ||
27 | references: { | ||
28 | model: 'videoLiveReplaySetting', | ||
29 | key: 'id' | ||
30 | }, | ||
31 | onDelete: 'SET NULL' | ||
32 | }, { transaction: utils.transaction }) | ||
33 | } | ||
34 | |||
35 | { | ||
36 | await utils.queryInterface.addColumn('videoLiveSession', 'replaySettingId', { | ||
37 | type: Sequelize.INTEGER, | ||
38 | defaultValue: null, | ||
39 | allowNull: true, | ||
40 | references: { | ||
41 | model: 'videoLiveReplaySetting', | ||
42 | key: 'id' | ||
43 | }, | ||
44 | onDelete: 'SET NULL' | ||
45 | }, { transaction: utils.transaction }) | ||
46 | } | ||
47 | |||
48 | { | ||
49 | const query = ` | ||
50 | SELECT live."id", v."privacy" | ||
51 | FROM "videoLive" live | ||
52 | INNER JOIN "video" v ON live."videoId" = v."id" | ||
53 | WHERE live."saveReplay" = true | ||
54 | ` | ||
55 | |||
56 | const videoLives = await utils.sequelize.query<{ id: number, privacy: number }>( | ||
57 | query, | ||
58 | { type: Sequelize.QueryTypes.SELECT, transaction: utils.transaction } | ||
59 | ) | ||
60 | |||
61 | for (const videoLive of videoLives) { | ||
62 | const query = ` | ||
63 | WITH new_replay_setting AS ( | ||
64 | INSERT INTO "videoLiveReplaySetting" ("privacy", "createdAt", "updatedAt") | ||
65 | VALUES (:privacy, NOW(), NOW()) | ||
66 | RETURNING id | ||
67 | ) | ||
68 | UPDATE "videoLive" SET "replaySettingId" = (SELECT id FROM new_replay_setting) | ||
69 | WHERE "id" = :id | ||
70 | ` | ||
71 | |||
72 | const options = { | ||
73 | replacements: { privacy: videoLive.privacy, id: videoLive.id }, | ||
74 | type: Sequelize.QueryTypes.UPDATE, | ||
75 | transaction: utils.transaction | ||
76 | } | ||
77 | |||
78 | await utils.sequelize.query(query, options) | ||
79 | } | ||
80 | } | ||
81 | |||
82 | { | ||
83 | const query = ` | ||
84 | SELECT session."id", v."privacy" | ||
85 | FROM "videoLiveSession" session | ||
86 | INNER JOIN "video" v ON session."liveVideoId" = v."id" | ||
87 | WHERE session."saveReplay" = true | ||
88 | AND session."liveVideoId" IS NOT NULL; | ||
89 | ` | ||
90 | |||
91 | const videoLiveSessions = await utils.sequelize.query<{ id: number, privacy: number }>( | ||
92 | query, | ||
93 | { type: Sequelize.QueryTypes.SELECT, transaction: utils.transaction } | ||
94 | ) | ||
95 | |||
96 | for (const videoLive of videoLiveSessions) { | ||
97 | const query = ` | ||
98 | WITH new_replay_setting AS ( | ||
99 | INSERT INTO "videoLiveReplaySetting" ("privacy", "createdAt", "updatedAt") | ||
100 | VALUES (:privacy, NOW(), NOW()) | ||
101 | RETURNING id | ||
102 | ) | ||
103 | UPDATE "videoLiveSession" SET "replaySettingId" = (SELECT id FROM new_replay_setting) | ||
104 | WHERE "id" = :id | ||
105 | ` | ||
106 | |||
107 | const options = { | ||
108 | replacements: { privacy: videoLive.privacy, id: videoLive.id }, | ||
109 | type: Sequelize.QueryTypes.UPDATE, | ||
110 | transaction: utils.transaction | ||
111 | } | ||
112 | |||
113 | await utils.sequelize.query(query, options) | ||
114 | } | ||
115 | } | ||
116 | } | ||
117 | |||
118 | function down (options) { | ||
119 | throw new Error('Not implemented.') | ||
120 | } | ||
121 | |||
122 | export { | ||
123 | up, | ||
124 | down | ||
125 | } | ||
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 | |||
19 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' | 19 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' |
20 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | 20 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
21 | import { VideoPathManager } from '@server/lib/video-path-manager' | 21 | import { VideoPathManager } from '@server/lib/video-path-manager' |
22 | import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' | ||
22 | 23 | ||
23 | const lTags = loggerTagsFactory('live', 'job') | 24 | const lTags = loggerTagsFactory('live', 'job') |
24 | 25 | ||
@@ -60,7 +61,13 @@ async function processVideoLiveEnding (job: Job) { | |||
60 | return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId }) | 61 | return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId }) |
61 | } | 62 | } |
62 | 63 | ||
63 | return replaceLiveByReplay({ video, liveSession, live, permanentLive, replayDirectory: payload.replayDirectory }) | 64 | return replaceLiveByReplay({ |
65 | video, | ||
66 | liveSession, | ||
67 | live, | ||
68 | permanentLive, | ||
69 | replayDirectory: payload.replayDirectory | ||
70 | }) | ||
64 | } | 71 | } |
65 | 72 | ||
66 | // --------------------------------------------------------------------------- | 73 | // --------------------------------------------------------------------------- |
@@ -79,6 +86,8 @@ async function saveReplayToExternalVideo (options: { | |||
79 | }) { | 86 | }) { |
80 | const { liveVideo, liveSession, publishedAt, replayDirectory } = options | 87 | const { liveVideo, liveSession, publishedAt, replayDirectory } = options |
81 | 88 | ||
89 | const replaySettings = await VideoLiveReplaySettingModel.load(liveSession.replaySettingId) | ||
90 | |||
82 | const replayVideo = new VideoModel({ | 91 | const replayVideo = new VideoModel({ |
83 | name: `${liveVideo.name} - ${new Date(publishedAt).toLocaleString()}`, | 92 | name: `${liveVideo.name} - ${new Date(publishedAt).toLocaleString()}`, |
84 | isLive: false, | 93 | isLive: false, |
@@ -95,7 +104,7 @@ async function saveReplayToExternalVideo (options: { | |||
95 | nsfw: liveVideo.nsfw, | 104 | nsfw: liveVideo.nsfw, |
96 | description: liveVideo.description, | 105 | description: liveVideo.description, |
97 | support: liveVideo.support, | 106 | support: liveVideo.support, |
98 | privacy: liveVideo.privacy, | 107 | privacy: replaySettings.privacy, |
99 | channelId: liveVideo.channelId | 108 | channelId: liveVideo.channelId |
100 | }) as MVideoWithAllFiles | 109 | }) as MVideoWithAllFiles |
101 | 110 | ||
@@ -142,6 +151,7 @@ async function replaceLiveByReplay (options: { | |||
142 | }) { | 151 | }) { |
143 | const { video, liveSession, live, permanentLive, replayDirectory } = options | 152 | const { video, liveSession, live, permanentLive, replayDirectory } = options |
144 | 153 | ||
154 | const replaySettings = await VideoLiveReplaySettingModel.load(liveSession.replaySettingId) | ||
145 | const videoWithFiles = await VideoModel.loadFull(video.id) | 155 | const videoWithFiles = await VideoModel.loadFull(video.id) |
146 | const hlsPlaylist = videoWithFiles.getHLSPlaylist() | 156 | const hlsPlaylist = videoWithFiles.getHLSPlaylist() |
147 | 157 | ||
@@ -150,6 +160,7 @@ async function replaceLiveByReplay (options: { | |||
150 | await live.destroy() | 160 | await live.destroy() |
151 | 161 | ||
152 | videoWithFiles.isLive = false | 162 | videoWithFiles.isLive = false |
163 | videoWithFiles.privacy = replaySettings.privacy | ||
153 | videoWithFiles.waitTranscoding = true | 164 | videoWithFiles.waitTranscoding = true |
154 | videoWithFiles.state = VideoState.TO_TRANSCODE | 165 | videoWithFiles.state = VideoState.TO_TRANSCODE |
155 | 166 | ||
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' | |||
19 | import { VideoLiveModel } from '@server/models/video/video-live' | 19 | import { VideoLiveModel } from '@server/models/video/video-live' |
20 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' | 20 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' |
21 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 21 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
22 | import { MVideo, MVideoLiveSession, MVideoLiveVideo } from '@server/types/models' | 22 | import { MVideo, MVideoLiveSession, MVideoLiveVideo, MVideoLiveVideoWithSetting } from '@server/types/models' |
23 | import { pick, wait } from '@shared/core-utils' | 23 | import { pick, wait } from '@shared/core-utils' |
24 | import { LiveVideoError, VideoState } from '@shared/models' | 24 | import { LiveVideoError, VideoState } from '@shared/models' |
25 | import { federateVideoIfNeeded } from '../activitypub/videos' | 25 | import { federateVideoIfNeeded } from '../activitypub/videos' |
@@ -30,6 +30,8 @@ import { Hooks } from '../plugins/hooks' | |||
30 | import { LiveQuotaStore } from './live-quota-store' | 30 | import { LiveQuotaStore } from './live-quota-store' |
31 | import { cleanupAndDestroyPermanentLive } from './live-utils' | 31 | import { cleanupAndDestroyPermanentLive } from './live-utils' |
32 | import { MuxingSession } from './shared' | 32 | import { MuxingSession } from './shared' |
33 | import { sequelizeTypescript } from '@server/initializers/database' | ||
34 | import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' | ||
33 | 35 | ||
34 | const NodeRtmpSession = require('node-media-server/src/node_rtmp_session') | 36 | const NodeRtmpSession = require('node-media-server/src/node_rtmp_session') |
35 | const context = require('node-media-server/src/node_core_ctx') | 37 | const context = require('node-media-server/src/node_core_ctx') |
@@ -270,7 +272,7 @@ class LiveManager { | |||
270 | 272 | ||
271 | private async runMuxingSession (options: { | 273 | private async runMuxingSession (options: { |
272 | sessionId: string | 274 | sessionId: string |
273 | videoLive: MVideoLiveVideo | 275 | videoLive: MVideoLiveVideoWithSetting |
274 | 276 | ||
275 | inputUrl: string | 277 | inputUrl: string |
276 | fps: number | 278 | fps: number |
@@ -470,15 +472,26 @@ class LiveManager { | |||
470 | return resolutionsEnabled | 472 | return resolutionsEnabled |
471 | } | 473 | } |
472 | 474 | ||
473 | private saveStartingSession (videoLive: MVideoLiveVideo) { | 475 | private async saveStartingSession (videoLive: MVideoLiveVideoWithSetting) { |
474 | const liveSession = new VideoLiveSessionModel({ | 476 | const replaySettings = videoLive.saveReplay |
475 | startDate: new Date(), | 477 | ? new VideoLiveReplaySettingModel({ |
476 | liveVideoId: videoLive.videoId, | 478 | privacy: videoLive.ReplaySetting.privacy |
477 | saveReplay: videoLive.saveReplay, | 479 | }) |
478 | endingProcessed: false | 480 | : null |
479 | }) | ||
480 | 481 | ||
481 | return liveSession.save() | 482 | return sequelizeTypescript.transaction(async t => { |
483 | if (videoLive.saveReplay) { | ||
484 | await replaySettings.save({ transaction: t }) | ||
485 | } | ||
486 | |||
487 | return VideoLiveSessionModel.create({ | ||
488 | startDate: new Date(), | ||
489 | liveVideoId: videoLive.videoId, | ||
490 | saveReplay: videoLive.saveReplay, | ||
491 | replaySettingId: videoLive.saveReplay ? replaySettings.id : null, | ||
492 | endingProcessed: false | ||
493 | }, { transaction: t }) | ||
494 | }) | ||
482 | } | 495 | } |
483 | 496 | ||
484 | private async saveEndingSession (videoId: number, error: LiveVideoError | null) { | 497 | 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 { | |||
17 | VideoState | 17 | VideoState |
18 | } from '@shared/models' | 18 | } from '@shared/models' |
19 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' | 19 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' |
20 | import { isVideoNameValid } from '../../../helpers/custom-validators/videos' | 20 | import { isVideoNameValid, isVideoPrivacyValid } from '../../../helpers/custom-validators/videos' |
21 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 21 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
22 | import { logger } from '../../../helpers/logger' | 22 | import { logger } from '../../../helpers/logger' |
23 | import { CONFIG } from '../../../initializers/config' | 23 | import { CONFIG } from '../../../initializers/config' |
@@ -66,6 +66,11 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
66 | .customSanitizer(toBooleanOrNull) | 66 | .customSanitizer(toBooleanOrNull) |
67 | .custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'), | 67 | .custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'), |
68 | 68 | ||
69 | body('replaySettings.privacy') | ||
70 | .optional() | ||
71 | .customSanitizer(toIntOrNull) | ||
72 | .custom(isVideoPrivacyValid), | ||
73 | |||
69 | body('permanentLive') | 74 | body('permanentLive') |
70 | .optional() | 75 | .optional() |
71 | .customSanitizer(toBooleanOrNull) | 76 | .customSanitizer(toBooleanOrNull) |
@@ -153,6 +158,11 @@ const videoLiveUpdateValidator = [ | |||
153 | .customSanitizer(toBooleanOrNull) | 158 | .customSanitizer(toBooleanOrNull) |
154 | .custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'), | 159 | .custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'), |
155 | 160 | ||
161 | body('replaySettings.privacy') | ||
162 | .optional() | ||
163 | .customSanitizer(toIntOrNull) | ||
164 | .custom(isVideoPrivacyValid), | ||
165 | |||
156 | body('latencyMode') | 166 | body('latencyMode') |
157 | .optional() | 167 | .optional() |
158 | .customSanitizer(toIntOrNull) | 168 | .customSanitizer(toIntOrNull) |
@@ -177,6 +187,8 @@ const videoLiveUpdateValidator = [ | |||
177 | }) | 187 | }) |
178 | } | 188 | } |
179 | 189 | ||
190 | if (!checkLiveSettingsReplayConsistency({ res, body })) return | ||
191 | |||
180 | if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) { | 192 | if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) { |
181 | return res.fail({ message: 'Cannot update a live that has already started' }) | 193 | return res.fail({ message: 'Cannot update a live that has already started' }) |
182 | } | 194 | } |
@@ -272,3 +284,43 @@ function hasValidLatencyMode (body: LiveVideoUpdate | LiveVideoCreate) { | |||
272 | 284 | ||
273 | return true | 285 | return true |
274 | } | 286 | } |
287 | |||
288 | function checkLiveSettingsReplayConsistency (options: { | ||
289 | res: express.Response | ||
290 | body: LiveVideoUpdate | ||
291 | }) { | ||
292 | const { res, body } = options | ||
293 | |||
294 | // We now save replays of this live, so replay settings are mandatory | ||
295 | if (res.locals.videoLive.saveReplay !== true && body.saveReplay === true) { | ||
296 | |||
297 | if (!exists(body.replaySettings)) { | ||
298 | res.fail({ | ||
299 | status: HttpStatusCode.BAD_REQUEST_400, | ||
300 | message: 'Replay settings are missing now the live replay is saved' | ||
301 | }) | ||
302 | return false | ||
303 | } | ||
304 | |||
305 | if (!exists(body.replaySettings.privacy)) { | ||
306 | res.fail({ | ||
307 | status: HttpStatusCode.BAD_REQUEST_400, | ||
308 | message: 'Privacy replay setting is missing now the live replay is saved' | ||
309 | }) | ||
310 | return false | ||
311 | } | ||
312 | } | ||
313 | |||
314 | // Save replay was and is not enabled, so send an error the user if it specified replay settings | ||
315 | if ((!exists(body.saveReplay) && res.locals.videoLive.saveReplay === false) || body.saveReplay === false) { | ||
316 | if (exists(body.replaySettings)) { | ||
317 | res.fail({ | ||
318 | status: HttpStatusCode.BAD_REQUEST_400, | ||
319 | message: 'Cannot save replay settings since live replay is not enabled' | ||
320 | }) | ||
321 | return false | ||
322 | } | ||
323 | } | ||
324 | |||
325 | return true | ||
326 | } | ||
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 { | |||
160 | 'permanentLive', | 160 | 'permanentLive', |
161 | 'latencyMode', | 161 | 'latencyMode', |
162 | 'videoId', | 162 | 'videoId', |
163 | 'replaySettingId', | ||
163 | 'createdAt', | 164 | 'createdAt', |
164 | 'updatedAt' | 165 | 'updatedAt' |
165 | ] | 166 | ] |
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 @@ | |||
1 | import { isVideoPrivacyValid } from '@server/helpers/custom-validators/videos' | ||
2 | import { MLiveReplaySetting } from '@server/types/models/video/video-live-replay-setting' | ||
3 | import { VideoPrivacy } from '@shared/models/videos/video-privacy.enum' | ||
4 | import { Transaction } from 'sequelize' | ||
5 | import { AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
6 | import { throwIfNotValid } from '../shared/sequelize-helpers' | ||
7 | |||
8 | @Table({ | ||
9 | tableName: 'videoLiveReplaySetting' | ||
10 | }) | ||
11 | export class VideoLiveReplaySettingModel extends Model<VideoLiveReplaySettingModel> { | ||
12 | |||
13 | @CreatedAt | ||
14 | createdAt: Date | ||
15 | |||
16 | @UpdatedAt | ||
17 | updatedAt: Date | ||
18 | |||
19 | @AllowNull(false) | ||
20 | @Is('VideoPrivacy', value => throwIfNotValid(value, isVideoPrivacyValid, 'privacy')) | ||
21 | @Column | ||
22 | privacy: VideoPrivacy | ||
23 | |||
24 | static load (id: number, transaction?: Transaction): Promise<MLiveReplaySetting> { | ||
25 | return VideoLiveReplaySettingModel.findOne({ | ||
26 | where: { id }, | ||
27 | transaction | ||
28 | }) | ||
29 | } | ||
30 | |||
31 | static removeSettings (id: number) { | ||
32 | return VideoLiveReplaySettingModel.destroy({ | ||
33 | where: { id } | ||
34 | }) | ||
35 | } | ||
36 | |||
37 | toFormattedJSON () { | ||
38 | return { | ||
39 | privacy: this.privacy | ||
40 | } | ||
41 | } | ||
42 | } | ||
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 @@ | |||
1 | import { FindOptions } from 'sequelize' | 1 | import { FindOptions } from 'sequelize' |
2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { |
3 | AllowNull, | ||
4 | BeforeDestroy, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | DataType, | ||
9 | ForeignKey, | ||
10 | Model, | ||
11 | Scopes, | ||
12 | Table, | ||
13 | UpdatedAt | ||
14 | } from 'sequelize-typescript' | ||
3 | import { MVideoLiveSession, MVideoLiveSessionReplay } from '@server/types/models' | 15 | import { MVideoLiveSession, MVideoLiveSessionReplay } from '@server/types/models' |
4 | import { uuidToShort } from '@shared/extra-utils' | 16 | import { uuidToShort } from '@shared/extra-utils' |
5 | import { LiveVideoError, LiveVideoSession } from '@shared/models' | 17 | import { LiveVideoError, LiveVideoSession } from '@shared/models' |
6 | import { AttributesOnly } from '@shared/typescript-utils' | 18 | import { AttributesOnly } from '@shared/typescript-utils' |
7 | import { VideoModel } from './video' | 19 | import { VideoModel } from './video' |
20 | import { VideoLiveReplaySettingModel } from './video-live-replay-setting' | ||
8 | 21 | ||
9 | export enum ScopeNames { | 22 | export enum ScopeNames { |
10 | WITH_REPLAY = 'WITH_REPLAY' | 23 | WITH_REPLAY = 'WITH_REPLAY' |
@@ -17,6 +30,10 @@ export enum ScopeNames { | |||
17 | model: VideoModel.unscoped(), | 30 | model: VideoModel.unscoped(), |
18 | as: 'ReplayVideo', | 31 | as: 'ReplayVideo', |
19 | required: false | 32 | required: false |
33 | }, | ||
34 | { | ||
35 | model: VideoLiveReplaySettingModel, | ||
36 | required: false | ||
20 | } | 37 | } |
21 | ] | 38 | ] |
22 | } | 39 | } |
@@ -30,6 +47,10 @@ export enum ScopeNames { | |||
30 | }, | 47 | }, |
31 | { | 48 | { |
32 | fields: [ 'liveVideoId' ] | 49 | fields: [ 'liveVideoId' ] |
50 | }, | ||
51 | { | ||
52 | fields: [ 'replaySettingId' ], | ||
53 | unique: true | ||
33 | } | 54 | } |
34 | ] | 55 | ] |
35 | }) | 56 | }) |
@@ -89,6 +110,27 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv | |||
89 | }) | 110 | }) |
90 | LiveVideo: VideoModel | 111 | LiveVideo: VideoModel |
91 | 112 | ||
113 | @ForeignKey(() => VideoLiveReplaySettingModel) | ||
114 | @Column | ||
115 | replaySettingId: number | ||
116 | |||
117 | @BelongsTo(() => VideoLiveReplaySettingModel, { | ||
118 | foreignKey: { | ||
119 | allowNull: true | ||
120 | }, | ||
121 | onDelete: 'set null' | ||
122 | }) | ||
123 | ReplaySetting: VideoLiveReplaySettingModel | ||
124 | |||
125 | @BeforeDestroy | ||
126 | static deleteReplaySetting (instance: VideoLiveSessionModel) { | ||
127 | return VideoLiveReplaySettingModel.destroy({ | ||
128 | where: { | ||
129 | id: instance.replaySettingId | ||
130 | } | ||
131 | }) | ||
132 | } | ||
133 | |||
92 | static load (id: number): Promise<MVideoLiveSession> { | 134 | static load (id: number): Promise<MVideoLiveSession> { |
93 | return VideoLiveSessionModel.findOne({ | 135 | return VideoLiveSessionModel.findOne({ |
94 | where: { id } | 136 | where: { id } |
@@ -146,6 +188,10 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv | |||
146 | } | 188 | } |
147 | : undefined | 189 | : undefined |
148 | 190 | ||
191 | const replaySettings = this.replaySettingId | ||
192 | ? this.ReplaySetting.toFormattedJSON() | ||
193 | : undefined | ||
194 | |||
149 | return { | 195 | return { |
150 | id: this.id, | 196 | id: this.id, |
151 | startDate: this.startDate.toISOString(), | 197 | startDate: this.startDate.toISOString(), |
@@ -154,6 +200,7 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv | |||
154 | : null, | 200 | : null, |
155 | endingProcessed: this.endingProcessed, | 201 | endingProcessed: this.endingProcessed, |
156 | saveReplay: this.saveReplay, | 202 | saveReplay: this.saveReplay, |
203 | replaySettings, | ||
157 | replayVideo, | 204 | replayVideo, |
158 | error: this.error | 205 | error: this.error |
159 | } | 206 | } |
diff --git a/server/models/video/video-live.ts b/server/models/video/video-live.ts index d2788ef4f..290e1dda7 100644 --- a/server/models/video/video-live.ts +++ b/server/models/video/video-live.ts | |||
@@ -1,11 +1,24 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { |
2 | BeforeDestroy, | ||
3 | AllowNull, | ||
4 | BelongsTo, | ||
5 | Column, | ||
6 | CreatedAt, | ||
7 | DataType, | ||
8 | DefaultScope, | ||
9 | ForeignKey, | ||
10 | Model, | ||
11 | Table, | ||
12 | UpdatedAt | ||
13 | } from 'sequelize-typescript' | ||
2 | import { CONFIG } from '@server/initializers/config' | 14 | import { CONFIG } from '@server/initializers/config' |
3 | import { WEBSERVER } from '@server/initializers/constants' | 15 | import { WEBSERVER } from '@server/initializers/constants' |
4 | import { MVideoLive, MVideoLiveVideo } from '@server/types/models' | 16 | import { MVideoLive, MVideoLiveVideoWithSetting } from '@server/types/models' |
5 | import { LiveVideo, LiveVideoLatencyMode, VideoState } from '@shared/models' | 17 | import { LiveVideo, LiveVideoLatencyMode, VideoState } from '@shared/models' |
6 | import { AttributesOnly } from '@shared/typescript-utils' | 18 | import { AttributesOnly } from '@shared/typescript-utils' |
7 | import { VideoModel } from './video' | 19 | import { VideoModel } from './video' |
8 | import { VideoBlacklistModel } from './video-blacklist' | 20 | import { VideoBlacklistModel } from './video-blacklist' |
21 | import { VideoLiveReplaySettingModel } from './video-live-replay-setting' | ||
9 | 22 | ||
10 | @DefaultScope(() => ({ | 23 | @DefaultScope(() => ({ |
11 | include: [ | 24 | include: [ |
@@ -18,6 +31,10 @@ import { VideoBlacklistModel } from './video-blacklist' | |||
18 | required: false | 31 | required: false |
19 | } | 32 | } |
20 | ] | 33 | ] |
34 | }, | ||
35 | { | ||
36 | model: VideoLiveReplaySettingModel, | ||
37 | required: false | ||
21 | } | 38 | } |
22 | ] | 39 | ] |
23 | })) | 40 | })) |
@@ -27,6 +44,10 @@ import { VideoBlacklistModel } from './video-blacklist' | |||
27 | { | 44 | { |
28 | fields: [ 'videoId' ], | 45 | fields: [ 'videoId' ], |
29 | unique: true | 46 | unique: true |
47 | }, | ||
48 | { | ||
49 | fields: [ 'replaySettingId' ], | ||
50 | unique: true | ||
30 | } | 51 | } |
31 | ] | 52 | ] |
32 | }) | 53 | }) |
@@ -66,6 +87,27 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel> | |||
66 | }) | 87 | }) |
67 | Video: VideoModel | 88 | Video: VideoModel |
68 | 89 | ||
90 | @ForeignKey(() => VideoLiveReplaySettingModel) | ||
91 | @Column | ||
92 | replaySettingId: number | ||
93 | |||
94 | @BelongsTo(() => VideoLiveReplaySettingModel, { | ||
95 | foreignKey: { | ||
96 | allowNull: true | ||
97 | }, | ||
98 | onDelete: 'set null' | ||
99 | }) | ||
100 | ReplaySetting: VideoLiveReplaySettingModel | ||
101 | |||
102 | @BeforeDestroy | ||
103 | static deleteReplaySetting (instance: VideoLiveModel) { | ||
104 | return VideoLiveReplaySettingModel.destroy({ | ||
105 | where: { | ||
106 | id: instance.replaySettingId | ||
107 | } | ||
108 | }) | ||
109 | } | ||
110 | |||
69 | static loadByStreamKey (streamKey: string) { | 111 | static loadByStreamKey (streamKey: string) { |
70 | const query = { | 112 | const query = { |
71 | where: { | 113 | where: { |
@@ -84,11 +126,15 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel> | |||
84 | required: false | 126 | required: false |
85 | } | 127 | } |
86 | ] | 128 | ] |
129 | }, | ||
130 | { | ||
131 | model: VideoLiveReplaySettingModel.unscoped(), | ||
132 | required: false | ||
87 | } | 133 | } |
88 | ] | 134 | ] |
89 | } | 135 | } |
90 | 136 | ||
91 | return VideoLiveModel.findOne<MVideoLiveVideo>(query) | 137 | return VideoLiveModel.findOne<MVideoLiveVideoWithSetting>(query) |
92 | } | 138 | } |
93 | 139 | ||
94 | static loadByVideoId (videoId: number) { | 140 | static loadByVideoId (videoId: number) { |
@@ -120,11 +166,16 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel> | |||
120 | } | 166 | } |
121 | } | 167 | } |
122 | 168 | ||
169 | const replaySettings = this.replaySettingId | ||
170 | ? this.ReplaySetting.toFormattedJSON() | ||
171 | : undefined | ||
172 | |||
123 | return { | 173 | return { |
124 | ...privateInformation, | 174 | ...privateInformation, |
125 | 175 | ||
126 | permanentLive: this.permanentLive, | 176 | permanentLive: this.permanentLive, |
127 | saveReplay: this.saveReplay, | 177 | saveReplay: this.saveReplay, |
178 | replaySettings, | ||
128 | latencyMode: this.latencyMode | 179 | latencyMode: this.latencyMode |
129 | } | 180 | } |
130 | } | 181 | } |
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<Partial<AttributesOnly<VideoModel>>> { | |||
706 | name: 'videoId', | 706 | name: 'videoId', |
707 | allowNull: false | 707 | allowNull: false |
708 | }, | 708 | }, |
709 | hooks: true, | ||
709 | onDelete: 'cascade' | 710 | onDelete: 'cascade' |
710 | }) | 711 | }) |
711 | VideoLive: VideoLiveModel | 712 | 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 () { | |||
83 | privacy: VideoPrivacy.PUBLIC, | 83 | privacy: VideoPrivacy.PUBLIC, |
84 | channelId, | 84 | channelId, |
85 | saveReplay: false, | 85 | saveReplay: false, |
86 | replaySettings: undefined, | ||
86 | permanentLive: false, | 87 | permanentLive: false, |
87 | latencyMode: LiveVideoLatencyMode.DEFAULT | 88 | latencyMode: LiveVideoLatencyMode.DEFAULT |
88 | } | 89 | } |
@@ -141,6 +142,12 @@ describe('Test video lives API validator', function () { | |||
141 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 142 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
142 | }) | 143 | }) |
143 | 144 | ||
145 | it('Should fail with a bad privacy for replay settings', async function () { | ||
146 | const fields = { ...baseCorrectParams, replaySettings: { privacy: 5 } } | ||
147 | |||
148 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
149 | }) | ||
150 | |||
144 | it('Should fail with another user channel', async function () { | 151 | it('Should fail with another user channel', async function () { |
145 | const user = { | 152 | const user = { |
146 | username: 'fake', | 153 | username: 'fake', |
@@ -256,7 +263,7 @@ describe('Test video lives API validator', function () { | |||
256 | }) | 263 | }) |
257 | 264 | ||
258 | it('Should forbid to save replay if not enabled by the admin', async function () { | 265 | it('Should forbid to save replay if not enabled by the admin', async function () { |
259 | const fields = { ...baseCorrectParams, saveReplay: true } | 266 | const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } |
260 | 267 | ||
261 | await server.config.updateCustomSubConfig({ | 268 | await server.config.updateCustomSubConfig({ |
262 | newConfig: { | 269 | newConfig: { |
@@ -277,7 +284,7 @@ describe('Test video lives API validator', function () { | |||
277 | }) | 284 | }) |
278 | 285 | ||
279 | it('Should allow to save replay if enabled by the admin', async function () { | 286 | it('Should allow to save replay if enabled by the admin', async function () { |
280 | const fields = { ...baseCorrectParams, saveReplay: true } | 287 | const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } |
281 | 288 | ||
282 | await server.config.updateCustomSubConfig({ | 289 | await server.config.updateCustomSubConfig({ |
283 | newConfig: { | 290 | newConfig: { |
@@ -464,6 +471,39 @@ describe('Test video lives API validator', function () { | |||
464 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 471 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
465 | }) | 472 | }) |
466 | 473 | ||
474 | it('Should fail with a bad privacy for replay settings', async function () { | ||
475 | const fields = { saveReplay: true, replaySettings: { privacy: 5 } } | ||
476 | |||
477 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
478 | }) | ||
479 | |||
480 | it('Should fail with save replay enabled but without replay settings', async function () { | ||
481 | await server.config.updateCustomSubConfig({ | ||
482 | newConfig: { | ||
483 | live: { | ||
484 | enabled: true, | ||
485 | allowReplay: true | ||
486 | } | ||
487 | } | ||
488 | }) | ||
489 | |||
490 | const fields = { saveReplay: true } | ||
491 | |||
492 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
493 | }) | ||
494 | |||
495 | it('Should fail with save replay disabled and replay settings', async function () { | ||
496 | const fields = { saveReplay: false, replaySettings: { privacy: VideoPrivacy.INTERNAL } } | ||
497 | |||
498 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
499 | }) | ||
500 | |||
501 | it('Should fail with only replay settings when save replay is disabled', async function () { | ||
502 | const fields = { replaySettings: { privacy: VideoPrivacy.INTERNAL } } | ||
503 | |||
504 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
505 | }) | ||
506 | |||
467 | it('Should fail to set latency if the server does not allow it', async function () { | 507 | it('Should fail to set latency if the server does not allow it', async function () { |
468 | const fields = { latencyMode: LiveVideoLatencyMode.HIGH_LATENCY } | 508 | const fields = { latencyMode: LiveVideoLatencyMode.HIGH_LATENCY } |
469 | 509 | ||
@@ -474,6 +514,9 @@ describe('Test video lives API validator', function () { | |||
474 | await command.update({ videoId: video.id, fields: { saveReplay: false } }) | 514 | await command.update({ videoId: video.id, fields: { saveReplay: false } }) |
475 | await command.update({ videoId: video.uuid, fields: { saveReplay: false } }) | 515 | await command.update({ videoId: video.uuid, fields: { saveReplay: false } }) |
476 | await command.update({ videoId: video.shortUUID, fields: { saveReplay: false } }) | 516 | await command.update({ videoId: video.shortUUID, fields: { saveReplay: false } }) |
517 | |||
518 | await command.update({ videoId: video.id, fields: { saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } }) | ||
519 | |||
477 | }) | 520 | }) |
478 | 521 | ||
479 | it('Should fail to update replay status if replay is not allowed on the instance', async function () { | 522 | 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 () { | |||
24 | let userAccessToken: string | 24 | let userAccessToken: string |
25 | let userChannelId: number | 25 | let userChannelId: number |
26 | 26 | ||
27 | async function createLiveWrapper (options: { | 27 | async function createLiveWrapper (options: { replay: boolean, permanent: boolean }) { |
28 | replay: boolean | ||
29 | permanent: boolean | ||
30 | }) { | ||
31 | const { replay, permanent } = options | 28 | const { replay, permanent } = options |
32 | 29 | ||
33 | const liveAttributes = { | 30 | const liveAttributes = { |
@@ -35,6 +32,7 @@ describe('Test live constraints', function () { | |||
35 | channelId: userChannelId, | 32 | channelId: userChannelId, |
36 | privacy: VideoPrivacy.PUBLIC, | 33 | privacy: VideoPrivacy.PUBLIC, |
37 | saveReplay: replay, | 34 | saveReplay: replay, |
35 | replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined, | ||
38 | permanentLive: permanent | 36 | permanentLive: permanent |
39 | } | 37 | } |
40 | 38 | ||
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 () { | |||
23 | privacy: VideoPrivacy.PUBLIC, | 23 | privacy: VideoPrivacy.PUBLIC, |
24 | name: 'my super live', | 24 | name: 'my super live', |
25 | saveReplay: options.replay, | 25 | saveReplay: options.replay, |
26 | replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined, | ||
26 | permanentLive: options.permanent | 27 | permanentLive: options.permanent |
27 | } | 28 | } |
28 | 29 | ||
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 () { | |||
27 | let liveVideoUUID: string | 27 | let liveVideoUUID: string |
28 | let ffmpegCommand: FfmpegCommand | 28 | let ffmpegCommand: FfmpegCommand |
29 | 29 | ||
30 | async function createLiveWrapper (options: { permanent: boolean, replay: boolean }) { | 30 | async function createLiveWrapper (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacy } }) { |
31 | if (liveVideoUUID) { | 31 | if (liveVideoUUID) { |
32 | try { | 32 | try { |
33 | await servers[0].videos.remove({ id: liveVideoUUID }) | 33 | await servers[0].videos.remove({ id: liveVideoUUID }) |
@@ -40,6 +40,7 @@ describe('Save replay setting', function () { | |||
40 | privacy: VideoPrivacy.PUBLIC, | 40 | privacy: VideoPrivacy.PUBLIC, |
41 | name: 'my super live', | 41 | name: 'my super live', |
42 | saveReplay: options.replay, | 42 | saveReplay: options.replay, |
43 | replaySettings: options.replaySettings, | ||
43 | permanentLive: options.permanent | 44 | permanentLive: options.permanent |
44 | } | 45 | } |
45 | 46 | ||
@@ -47,7 +48,7 @@ describe('Save replay setting', function () { | |||
47 | return uuid | 48 | return uuid |
48 | } | 49 | } |
49 | 50 | ||
50 | async function publishLive (options: { permanent: boolean, replay: boolean }) { | 51 | async function publishLive (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacy } }) { |
51 | liveVideoUUID = await createLiveWrapper(options) | 52 | liveVideoUUID = await createLiveWrapper(options) |
52 | 53 | ||
53 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | 54 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) |
@@ -61,7 +62,7 @@ describe('Save replay setting', function () { | |||
61 | return { ffmpegCommand, liveDetails } | 62 | return { ffmpegCommand, liveDetails } |
62 | } | 63 | } |
63 | 64 | ||
64 | async function publishLiveAndDelete (options: { permanent: boolean, replay: boolean }) { | 65 | async function publishLiveAndDelete (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacy } }) { |
65 | const { ffmpegCommand, liveDetails } = await publishLive(options) | 66 | const { ffmpegCommand, liveDetails } = await publishLive(options) |
66 | 67 | ||
67 | await Promise.all([ | 68 | await Promise.all([ |
@@ -76,7 +77,7 @@ describe('Save replay setting', function () { | |||
76 | return { liveDetails } | 77 | return { liveDetails } |
77 | } | 78 | } |
78 | 79 | ||
79 | async function publishLiveAndBlacklist (options: { permanent: boolean, replay: boolean }) { | 80 | async function publishLiveAndBlacklist (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacy } }) { |
80 | const { ffmpegCommand, liveDetails } = await publishLive(options) | 81 | const { ffmpegCommand, liveDetails } = await publishLive(options) |
81 | 82 | ||
82 | await Promise.all([ | 83 | await Promise.all([ |
@@ -112,6 +113,13 @@ describe('Save replay setting', function () { | |||
112 | } | 113 | } |
113 | } | 114 | } |
114 | 115 | ||
116 | async function checkVideoPrivacy (videoId: string, privacy: VideoPrivacy) { | ||
117 | for (const server of servers) { | ||
118 | const video = await server.videos.get({ id: videoId }) | ||
119 | expect(video.privacy.id).to.equal(privacy) | ||
120 | } | ||
121 | } | ||
122 | |||
115 | before(async function () { | 123 | before(async function () { |
116 | this.timeout(120000) | 124 | this.timeout(120000) |
117 | 125 | ||
@@ -247,12 +255,13 @@ describe('Save replay setting', function () { | |||
247 | it('Should correctly create and federate the "waiting for stream" live', async function () { | 255 | it('Should correctly create and federate the "waiting for stream" live', async function () { |
248 | this.timeout(20000) | 256 | this.timeout(20000) |
249 | 257 | ||
250 | liveVideoUUID = await createLiveWrapper({ permanent: false, replay: true }) | 258 | liveVideoUUID = await createLiveWrapper({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } }) |
251 | 259 | ||
252 | await waitJobs(servers) | 260 | await waitJobs(servers) |
253 | 261 | ||
254 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) | 262 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) |
255 | await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) | 263 | await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) |
264 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) | ||
256 | }) | 265 | }) |
257 | 266 | ||
258 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { | 267 | 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 () { | |||
265 | 274 | ||
266 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) | 275 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) |
267 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | 276 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) |
277 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) | ||
268 | }) | 278 | }) |
269 | 279 | ||
270 | it('Should correctly have saved the live and federated it after the streaming', async function () { | 280 | it('Should correctly have saved the live and federated it after the streaming', async function () { |
@@ -274,6 +284,8 @@ describe('Save replay setting', function () { | |||
274 | expect(session.endDate).to.not.exist | 284 | expect(session.endDate).to.not.exist |
275 | expect(session.endingProcessed).to.be.false | 285 | expect(session.endingProcessed).to.be.false |
276 | expect(session.saveReplay).to.be.true | 286 | expect(session.saveReplay).to.be.true |
287 | expect(session.replaySettings).to.exist | ||
288 | expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED) | ||
277 | 289 | ||
278 | await stopFfmpeg(ffmpegCommand) | 290 | await stopFfmpeg(ffmpegCommand) |
279 | 291 | ||
@@ -281,8 +293,9 @@ describe('Save replay setting', function () { | |||
281 | await waitJobs(servers) | 293 | await waitJobs(servers) |
282 | 294 | ||
283 | // Live has been transcoded | 295 | // Live has been transcoded |
284 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) | 296 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) |
285 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | 297 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) |
298 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.UNLISTED) | ||
286 | }) | 299 | }) |
287 | 300 | ||
288 | it('Should find the replay live session', async function () { | 301 | it('Should find the replay live session', async function () { |
@@ -296,6 +309,8 @@ describe('Save replay setting', function () { | |||
296 | expect(session.error).to.not.exist | 309 | expect(session.error).to.not.exist |
297 | expect(session.saveReplay).to.be.true | 310 | expect(session.saveReplay).to.be.true |
298 | expect(session.endingProcessed).to.be.true | 311 | expect(session.endingProcessed).to.be.true |
312 | expect(session.replaySettings).to.exist | ||
313 | expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED) | ||
299 | 314 | ||
300 | expect(session.replayVideo).to.exist | 315 | expect(session.replayVideo).to.exist |
301 | expect(session.replayVideo.id).to.exist | 316 | expect(session.replayVideo.id).to.exist |
@@ -306,13 +321,14 @@ describe('Save replay setting', function () { | |||
306 | it('Should update the saved live and correctly federate the updated attributes', async function () { | 321 | it('Should update the saved live and correctly federate the updated attributes', async function () { |
307 | this.timeout(30000) | 322 | this.timeout(30000) |
308 | 323 | ||
309 | await servers[0].videos.update({ id: liveVideoUUID, attributes: { name: 'video updated' } }) | 324 | await servers[0].videos.update({ id: liveVideoUUID, attributes: { name: 'video updated', privacy: VideoPrivacy.PUBLIC } }) |
310 | await waitJobs(servers) | 325 | await waitJobs(servers) |
311 | 326 | ||
312 | for (const server of servers) { | 327 | for (const server of servers) { |
313 | const video = await server.videos.get({ id: liveVideoUUID }) | 328 | const video = await server.videos.get({ id: liveVideoUUID }) |
314 | expect(video.name).to.equal('video updated') | 329 | expect(video.name).to.equal('video updated') |
315 | expect(video.isLive).to.be.false | 330 | expect(video.isLive).to.be.false |
331 | expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC) | ||
316 | } | 332 | } |
317 | }) | 333 | }) |
318 | 334 | ||
@@ -323,7 +339,7 @@ describe('Save replay setting', function () { | |||
323 | it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { | 339 | it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { |
324 | this.timeout(120000) | 340 | this.timeout(120000) |
325 | 341 | ||
326 | await publishLiveAndBlacklist({ permanent: false, replay: true }) | 342 | await publishLiveAndBlacklist({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }) |
327 | 343 | ||
328 | await checkVideosExist(liveVideoUUID, false) | 344 | await checkVideosExist(liveVideoUUID, false) |
329 | 345 | ||
@@ -338,7 +354,7 @@ describe('Save replay setting', function () { | |||
338 | it('Should correctly terminate the stream on delete and delete the video', async function () { | 354 | it('Should correctly terminate the stream on delete and delete the video', async function () { |
339 | this.timeout(40000) | 355 | this.timeout(40000) |
340 | 356 | ||
341 | await publishLiveAndDelete({ permanent: false, replay: true }) | 357 | await publishLiveAndDelete({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }) |
342 | 358 | ||
343 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) | 359 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) |
344 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | 360 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) |
@@ -348,103 +364,201 @@ describe('Save replay setting', function () { | |||
348 | describe('With save replay enabled on permanent live', function () { | 364 | describe('With save replay enabled on permanent live', function () { |
349 | let lastReplayUUID: string | 365 | let lastReplayUUID: string |
350 | 366 | ||
351 | it('Should correctly create and federate the "waiting for stream" live', async function () { | 367 | describe('With a first live and its replay', function () { |
352 | this.timeout(20000) | ||
353 | 368 | ||
354 | liveVideoUUID = await createLiveWrapper({ permanent: true, replay: true }) | 369 | it('Should correctly create and federate the "waiting for stream" live', async function () { |
370 | this.timeout(20000) | ||
355 | 371 | ||
356 | await waitJobs(servers) | 372 | liveVideoUUID = await createLiveWrapper({ permanent: true, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } }) |
357 | 373 | ||
358 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) | 374 | await waitJobs(servers) |
359 | await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) | ||
360 | }) | ||
361 | 375 | ||
362 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { | 376 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) |
363 | this.timeout(20000) | 377 | await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) |
378 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) | ||
379 | }) | ||
364 | 380 | ||
365 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | 381 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { |
366 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | 382 | this.timeout(20000) |
367 | 383 | ||
368 | await waitJobs(servers) | 384 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) |
385 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
369 | 386 | ||
370 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) | 387 | await waitJobs(servers) |
371 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | ||
372 | }) | ||
373 | 388 | ||
374 | it('Should correctly have saved the live and federated it after the streaming', async function () { | 389 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) |
375 | this.timeout(30000) | 390 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) |
391 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) | ||
392 | }) | ||
376 | 393 | ||
377 | const liveDetails = await servers[0].videos.get({ id: liveVideoUUID }) | 394 | it('Should correctly have saved the live and federated it after the streaming', async function () { |
395 | this.timeout(30000) | ||
378 | 396 | ||
379 | await stopFfmpeg(ffmpegCommand) | 397 | const liveDetails = await servers[0].videos.get({ id: liveVideoUUID }) |
380 | 398 | ||
381 | await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID) | 399 | await stopFfmpeg(ffmpegCommand) |
382 | await waitJobs(servers) | ||
383 | 400 | ||
384 | const video = await findExternalSavedVideo(servers[0], liveDetails) | 401 | await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID) |
385 | expect(video).to.exist | 402 | await waitJobs(servers) |
386 | 403 | ||
387 | for (const server of servers) { | 404 | const video = await findExternalSavedVideo(servers[0], liveDetails) |
388 | await server.videos.get({ id: video.uuid }) | 405 | expect(video).to.exist |
389 | } | ||
390 | 406 | ||
391 | lastReplayUUID = video.uuid | 407 | for (const server of servers) { |
392 | }) | 408 | await server.videos.get({ id: video.uuid }) |
409 | } | ||
393 | 410 | ||
394 | it('Should have appropriate ended session and replay live session', async function () { | 411 | lastReplayUUID = video.uuid |
395 | const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID }) | 412 | }) |
396 | expect(total).to.equal(1) | ||
397 | expect(data).to.have.lengthOf(1) | ||
398 | 413 | ||
399 | const sessionFromLive = data[0] | 414 | it('Should have appropriate ended session and replay live session', async function () { |
400 | const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID }) | 415 | const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID }) |
416 | expect(total).to.equal(1) | ||
417 | expect(data).to.have.lengthOf(1) | ||
401 | 418 | ||
402 | for (const session of [ sessionFromLive, sessionFromReplay ]) { | 419 | const sessionFromLive = data[0] |
403 | expect(session.startDate).to.exist | 420 | const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID }) |
404 | expect(session.endDate).to.exist | ||
405 | 421 | ||
406 | expect(session.error).to.not.exist | 422 | for (const session of [ sessionFromLive, sessionFromReplay ]) { |
423 | expect(session.startDate).to.exist | ||
424 | expect(session.endDate).to.exist | ||
407 | 425 | ||
408 | expect(session.replayVideo).to.exist | 426 | expect(session.replaySettings).to.exist |
409 | expect(session.replayVideo.id).to.exist | 427 | expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED) |
410 | expect(session.replayVideo.shortUUID).to.exist | ||
411 | expect(session.replayVideo.uuid).to.equal(lastReplayUUID) | ||
412 | } | ||
413 | }) | ||
414 | 428 | ||
415 | it('Should have cleaned up the live files', async function () { | 429 | expect(session.error).to.not.exist |
416 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | 430 | |
431 | expect(session.replayVideo).to.exist | ||
432 | expect(session.replayVideo.id).to.exist | ||
433 | expect(session.replayVideo.shortUUID).to.exist | ||
434 | expect(session.replayVideo.uuid).to.equal(lastReplayUUID) | ||
435 | } | ||
436 | }) | ||
437 | |||
438 | it('Should have the first live replay with correct settings', async function () { | ||
439 | await checkVideosExist(lastReplayUUID, false, HttpStatusCode.OK_200) | ||
440 | await checkVideoState(lastReplayUUID, VideoState.PUBLISHED) | ||
441 | await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.UNLISTED) | ||
442 | }) | ||
417 | }) | 443 | }) |
418 | 444 | ||
419 | it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { | 445 | describe('With a second live and its replay', function () { |
420 | this.timeout(120000) | 446 | it('Should update the replay settings', async function () { |
447 | await servers[0].live.update( | ||
448 | { videoId: liveVideoUUID, fields: { replaySettings: { privacy: VideoPrivacy.PUBLIC } } }) | ||
449 | await waitJobs(servers) | ||
450 | const live = await servers[0].live.get({ videoId: liveVideoUUID }) | ||
421 | 451 | ||
422 | await servers[0].videos.remove({ id: lastReplayUUID }) | 452 | expect(live.saveReplay).to.be.true |
423 | const { liveDetails } = await publishLiveAndBlacklist({ permanent: true, replay: true }) | 453 | expect(live.replaySettings).to.exist |
454 | expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC) | ||
424 | 455 | ||
425 | const replay = await findExternalSavedVideo(servers[0], liveDetails) | 456 | }) |
426 | expect(replay).to.exist | ||
427 | 457 | ||
428 | for (const videoId of [ liveVideoUUID, replay.uuid ]) { | 458 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { |
429 | await checkVideosExist(videoId, false) | 459 | this.timeout(20000) |
430 | 460 | ||
431 | await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | 461 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) |
432 | await servers[1].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 462 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) |
433 | } | ||
434 | 463 | ||
435 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | 464 | await waitJobs(servers) |
436 | }) | ||
437 | 465 | ||
438 | it('Should correctly terminate the stream on delete and not save the video', async function () { | 466 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) |
439 | this.timeout(40000) | 467 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) |
468 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) | ||
469 | }) | ||
440 | 470 | ||
441 | const { liveDetails } = await publishLiveAndDelete({ permanent: true, replay: true }) | 471 | it('Should correctly have saved the live and federated it after the streaming', async function () { |
472 | this.timeout(30000) | ||
473 | const liveDetails = await servers[0].videos.get({ id: liveVideoUUID }) | ||
442 | 474 | ||
443 | const replay = await findExternalSavedVideo(servers[0], liveDetails) | 475 | await stopFfmpeg(ffmpegCommand) |
444 | expect(replay).to.not.exist | ||
445 | 476 | ||
446 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) | 477 | await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID) |
447 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | 478 | await waitJobs(servers) |
479 | |||
480 | const video = await findExternalSavedVideo(servers[0], liveDetails) | ||
481 | expect(video).to.exist | ||
482 | |||
483 | for (const server of servers) { | ||
484 | await server.videos.get({ id: video.uuid }) | ||
485 | } | ||
486 | |||
487 | lastReplayUUID = video.uuid | ||
488 | }) | ||
489 | |||
490 | it('Should have appropriate ended session and replay live session', async function () { | ||
491 | const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID }) | ||
492 | expect(total).to.equal(2) | ||
493 | expect(data).to.have.lengthOf(2) | ||
494 | |||
495 | const sessionFromLive = data[1] | ||
496 | const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID }) | ||
497 | |||
498 | for (const session of [ sessionFromLive, sessionFromReplay ]) { | ||
499 | expect(session.startDate).to.exist | ||
500 | expect(session.endDate).to.exist | ||
501 | |||
502 | expect(session.replaySettings).to.exist | ||
503 | expect(session.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC) | ||
504 | |||
505 | expect(session.error).to.not.exist | ||
506 | |||
507 | expect(session.replayVideo).to.exist | ||
508 | expect(session.replayVideo.id).to.exist | ||
509 | expect(session.replayVideo.shortUUID).to.exist | ||
510 | expect(session.replayVideo.uuid).to.equal(lastReplayUUID) | ||
511 | } | ||
512 | }) | ||
513 | |||
514 | it('Should have the first live replay with correct settings', async function () { | ||
515 | await checkVideosExist(lastReplayUUID, true, HttpStatusCode.OK_200) | ||
516 | await checkVideoState(lastReplayUUID, VideoState.PUBLISHED) | ||
517 | await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.PUBLIC) | ||
518 | }) | ||
519 | |||
520 | it('Should have cleaned up the live files', async function () { | ||
521 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
522 | }) | ||
523 | |||
524 | it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { | ||
525 | this.timeout(120000) | ||
526 | |||
527 | await servers[0].videos.remove({ id: lastReplayUUID }) | ||
528 | const { liveDetails } = await publishLiveAndBlacklist({ | ||
529 | permanent: true, | ||
530 | replay: true, | ||
531 | replaySettings: { privacy: VideoPrivacy.PUBLIC } | ||
532 | }) | ||
533 | |||
534 | const replay = await findExternalSavedVideo(servers[0], liveDetails) | ||
535 | expect(replay).to.exist | ||
536 | |||
537 | for (const videoId of [ liveVideoUUID, replay.uuid ]) { | ||
538 | await checkVideosExist(videoId, false) | ||
539 | |||
540 | await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
541 | await servers[1].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
542 | } | ||
543 | |||
544 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
545 | }) | ||
546 | |||
547 | it('Should correctly terminate the stream on delete and not save the video', async function () { | ||
548 | this.timeout(40000) | ||
549 | |||
550 | const { liveDetails } = await publishLiveAndDelete({ | ||
551 | permanent: true, | ||
552 | replay: true, | ||
553 | replaySettings: { privacy: VideoPrivacy.PUBLIC } | ||
554 | }) | ||
555 | |||
556 | const replay = await findExternalSavedVideo(servers[0], liveDetails) | ||
557 | expect(replay).to.not.exist | ||
558 | |||
559 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) | ||
560 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
561 | }) | ||
448 | }) | 562 | }) |
449 | }) | 563 | }) |
450 | 564 | ||
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 () { | |||
87 | commentsEnabled: false, | 87 | commentsEnabled: false, |
88 | downloadEnabled: false, | 88 | downloadEnabled: false, |
89 | saveReplay: true, | 89 | saveReplay: true, |
90 | replaySettings: { privacy: VideoPrivacy.PUBLIC }, | ||
90 | latencyMode: LiveVideoLatencyMode.SMALL_LATENCY, | 91 | latencyMode: LiveVideoLatencyMode.SMALL_LATENCY, |
91 | privacy: VideoPrivacy.PUBLIC, | 92 | privacy: VideoPrivacy.PUBLIC, |
92 | previewfile: 'video_short1-preview.webm.jpg', | 93 | previewfile: 'video_short1-preview.webm.jpg', |
@@ -128,6 +129,9 @@ describe('Test live', function () { | |||
128 | if (server.url === servers[0].url) { | 129 | if (server.url === servers[0].url) { |
129 | expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live') | 130 | expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live') |
130 | expect(live.streamKey).to.not.be.empty | 131 | expect(live.streamKey).to.not.be.empty |
132 | |||
133 | expect(live.replaySettings).to.exist | ||
134 | expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC) | ||
131 | } else { | 135 | } else { |
132 | expect(live.rtmpUrl).to.not.exist | 136 | expect(live.rtmpUrl).to.not.exist |
133 | expect(live.streamKey).to.not.exist | 137 | expect(live.streamKey).to.not.exist |
@@ -196,6 +200,7 @@ describe('Test live', function () { | |||
196 | } | 200 | } |
197 | 201 | ||
198 | expect(live.saveReplay).to.be.false | 202 | expect(live.saveReplay).to.be.false |
203 | expect(live.replaySettings).to.not.exist | ||
199 | expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT) | 204 | expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT) |
200 | } | 205 | } |
201 | }) | 206 | }) |
@@ -366,7 +371,10 @@ describe('Test live', function () { | |||
366 | name: 'live video', | 371 | name: 'live video', |
367 | channelId: servers[0].store.channel.id, | 372 | channelId: servers[0].store.channel.id, |
368 | privacy: VideoPrivacy.PUBLIC, | 373 | privacy: VideoPrivacy.PUBLIC, |
369 | saveReplay | 374 | saveReplay, |
375 | replaySettings: saveReplay | ||
376 | ? { privacy: VideoPrivacy.PUBLIC } | ||
377 | : undefined | ||
370 | } | 378 | } |
371 | 379 | ||
372 | const { uuid } = await commands[0].create({ fields: liveAttributes }) | 380 | const { uuid } = await commands[0].create({ fields: liveAttributes }) |
@@ -670,6 +678,9 @@ describe('Test live', function () { | |||
670 | channelId: servers[0].store.channel.id, | 678 | channelId: servers[0].store.channel.id, |
671 | privacy: VideoPrivacy.PUBLIC, | 679 | privacy: VideoPrivacy.PUBLIC, |
672 | saveReplay: options.saveReplay, | 680 | saveReplay: options.saveReplay, |
681 | replaySettings: options.saveReplay | ||
682 | ? { privacy: VideoPrivacy.PUBLIC } | ||
683 | : undefined, | ||
673 | permanentLive: options.permanent | 684 | permanentLive: options.permanent |
674 | } | 685 | } |
675 | 686 | ||
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 () { | |||
342 | privacy: VideoPrivacy.PUBLIC, | 342 | privacy: VideoPrivacy.PUBLIC, |
343 | channelId: servers[1].store.channel.id, | 343 | channelId: servers[1].store.channel.id, |
344 | saveReplay: true, | 344 | saveReplay: true, |
345 | replaySettings: { privacy: VideoPrivacy.PUBLIC }, | ||
345 | permanentLive: false | 346 | permanentLive: false |
346 | } | 347 | } |
347 | }) | 348 | }) |
@@ -367,6 +368,7 @@ describe('Test user notifications', function () { | |||
367 | privacy: VideoPrivacy.PUBLIC, | 368 | privacy: VideoPrivacy.PUBLIC, |
368 | channelId: servers[1].store.channel.id, | 369 | channelId: servers[1].store.channel.id, |
369 | saveReplay: true, | 370 | saveReplay: true, |
371 | replaySettings: { privacy: VideoPrivacy.PUBLIC }, | ||
370 | permanentLive: true | 372 | permanentLive: true |
371 | } | 373 | } |
372 | }) | 374 | }) |
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) { | |||
27 | privacy: VideoPrivacy.PUBLIC, | 27 | privacy: VideoPrivacy.PUBLIC, |
28 | name: 'my super live', | 28 | name: 'my super live', |
29 | saveReplay: true, | 29 | saveReplay: true, |
30 | replaySettings: { privacy: VideoPrivacy.PUBLIC }, | ||
30 | permanentLive: permanent | 31 | permanentLive: permanent |
31 | } | 32 | } |
32 | 33 | ||
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 () { | |||
305 | }) | 305 | }) |
306 | 306 | ||
307 | { | 307 | { |
308 | const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: false, privacy: VideoPrivacy.PRIVATE }) | 308 | const { video, live } = await server.live.quickCreate({ |
309 | saveReplay: true, | ||
310 | permanentLive: false, | ||
311 | privacy: VideoPrivacy.PRIVATE | ||
312 | }) | ||
309 | normalLiveId = video.uuid | 313 | normalLiveId = video.uuid |
310 | normalLive = live | 314 | normalLive = live |
311 | } | 315 | } |
312 | 316 | ||
313 | { | 317 | { |
314 | const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: true, privacy: VideoPrivacy.PRIVATE }) | 318 | const { video, live } = await server.live.quickCreate({ |
319 | saveReplay: true, | ||
320 | permanentLive: true, | ||
321 | privacy: VideoPrivacy.PRIVATE | ||
322 | }) | ||
315 | permanentLiveId = video.uuid | 323 | permanentLiveId = video.uuid |
316 | permanentLive = live | 324 | permanentLive = live |
317 | } | 325 | } |
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 () { | |||
364 | }) | 364 | }) |
365 | 365 | ||
366 | { | 366 | { |
367 | const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: false, privacy: VideoPrivacy.PRIVATE }) | 367 | const { video, live } = await server.live.quickCreate({ |
368 | saveReplay: true, | ||
369 | permanentLive: false, | ||
370 | privacy: VideoPrivacy.PRIVATE | ||
371 | }) | ||
368 | normalLiveId = video.uuid | 372 | normalLiveId = video.uuid |
369 | normalLive = live | 373 | normalLive = live |
370 | } | 374 | } |
371 | 375 | ||
372 | { | 376 | { |
373 | const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: true, privacy: VideoPrivacy.PRIVATE }) | 377 | const { video, live } = await server.live.quickCreate({ |
378 | saveReplay: true, | ||
379 | permanentLive: true, | ||
380 | privacy: VideoPrivacy.PRIVATE | ||
381 | }) | ||
374 | permanentLiveId = video.uuid | 382 | permanentLiveId = video.uuid |
375 | permanentLive = live | 383 | permanentLive = live |
376 | } | 384 | } |
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 @@ | |||
1 | import { OutgoingHttpHeaders } from 'http' | 1 | import { OutgoingHttpHeaders } from 'http' |
2 | import { Writable } from 'stream' | ||
2 | import { RegisterServerAuthExternalOptions } from '@server/types' | 3 | import { RegisterServerAuthExternalOptions } from '@server/types' |
3 | import { | 4 | import { |
4 | MAbuseMessage, | 5 | MAbuseMessage, |
@@ -16,7 +17,7 @@ import { | |||
16 | MVideoFormattableDetails, | 17 | MVideoFormattableDetails, |
17 | MVideoId, | 18 | MVideoId, |
18 | MVideoImmutable, | 19 | MVideoImmutable, |
19 | MVideoLive, | 20 | MVideoLiveFormattable, |
20 | MVideoPlaylistFull, | 21 | MVideoPlaylistFull, |
21 | MVideoPlaylistFullSummary | 22 | MVideoPlaylistFullSummary |
22 | } from '@server/types/models' | 23 | } from '@server/types/models' |
@@ -43,7 +44,6 @@ import { | |||
43 | MVideoShareActor, | 44 | MVideoShareActor, |
44 | MVideoThumbnail | 45 | MVideoThumbnail |
45 | } from './models' | 46 | } from './models' |
46 | import { Writable } from 'stream' | ||
47 | import { MVideoSource } from './models/video/video-source' | 47 | import { MVideoSource } from './models/video/video-source' |
48 | 48 | ||
49 | declare module 'express' { | 49 | declare module 'express' { |
@@ -124,7 +124,7 @@ declare module 'express' { | |||
124 | onlyVideo?: MVideoThumbnail | 124 | onlyVideo?: MVideoThumbnail |
125 | videoId?: MVideoId | 125 | videoId?: MVideoId |
126 | 126 | ||
127 | videoLive?: MVideoLive | 127 | videoLive?: MVideoLiveFormattable |
128 | videoLiveSession?: MVideoLiveSession | 128 | videoLiveSession?: MVideoLiveSession |
129 | 129 | ||
130 | videoShare?: MVideoShareActor | 130 | 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' | |||
13 | export * from './video-comment' | 13 | export * from './video-comment' |
14 | export * from './video-file' | 14 | export * from './video-file' |
15 | export * from './video-import' | 15 | export * from './video-import' |
16 | export * from './video-live-replay-setting' | ||
16 | export * from './video-live-session' | 17 | export * from './video-live-session' |
17 | export * from './video-live' | 18 | export * from './video-live' |
18 | export * from './video-playlist' | 19 | 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 @@ | |||
1 | import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' | ||
2 | |||
3 | export type MLiveReplaySetting = Omit<VideoLiveReplaySettingModel, 'VideoLive' | 'VideoLiveSession'> | ||
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 @@ | |||
1 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' | 1 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' |
2 | import { PickWith } from '@shared/typescript-utils' | 2 | import { PickWith } from '@shared/typescript-utils' |
3 | import { MVideo } from './video' | 3 | import { MVideo } from './video' |
4 | import { MLiveReplaySetting } from './video-live-replay-setting' | ||
4 | 5 | ||
5 | type Use<K extends keyof VideoLiveSessionModel, M> = PickWith<VideoLiveSessionModel, K, M> | 6 | type Use<K extends keyof VideoLiveSessionModel, M> = PickWith<VideoLiveSessionModel, K, M> |
6 | 7 | ||
7 | // ############################################################################ | 8 | // ############################################################################ |
8 | 9 | ||
9 | export type MVideoLiveSession = Omit<VideoLiveSessionModel, 'Video' | 'VideoLive'> | 10 | export type MVideoLiveSession = Omit<VideoLiveSessionModel, 'Video' | 'VideoLive' | 'ReplaySetting'> |
10 | 11 | ||
11 | // ############################################################################ | 12 | // ############################################################################ |
12 | 13 | ||
13 | export type MVideoLiveSessionReplay = | 14 | export type MVideoLiveSessionReplay = |
14 | MVideoLiveSession & | 15 | MVideoLiveSession & |
15 | Use<'ReplayVideo', MVideo> | 16 | Use<'ReplayVideo', MVideo> & |
17 | 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 @@ | |||
1 | import { VideoLiveModel } from '@server/models/video/video-live' | 1 | import { VideoLiveModel } from '@server/models/video/video-live' |
2 | import { PickWith } from '@shared/typescript-utils' | 2 | import { PickWith } from '@shared/typescript-utils' |
3 | import { MVideo } from './video' | 3 | import { MVideo } from './video' |
4 | import { MLiveReplaySetting } from './video-live-replay-setting' | ||
4 | 5 | ||
5 | type Use<K extends keyof VideoLiveModel, M> = PickWith<VideoLiveModel, K, M> | 6 | type Use<K extends keyof VideoLiveModel, M> = PickWith<VideoLiveModel, K, M> |
6 | 7 | ||
7 | // ############################################################################ | 8 | // ############################################################################ |
8 | 9 | ||
9 | export type MVideoLive = Omit<VideoLiveModel, 'Video'> | 10 | export type MVideoLive = Omit<VideoLiveModel, 'Video' | 'ReplaySetting'> |
10 | 11 | ||
11 | // ############################################################################ | 12 | // ############################################################################ |
12 | 13 | ||
13 | export type MVideoLiveVideo = | 14 | export type MVideoLiveVideo = |
14 | MVideoLive & | 15 | MVideoLive & |
15 | Use<'Video', MVideo> | 16 | Use<'Video', MVideo> |
17 | |||
18 | // ############################################################################ | ||
19 | |||
20 | export type MVideoLiveVideoWithSetting = | ||
21 | MVideoLiveVideo & | ||
22 | Use<'ReplaySetting', MLiveReplaySetting> | ||