aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html9
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts9
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts14
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.ts2
-rw-r--r--server/controllers/api/videos/live.ts38
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/database.ts2
-rw-r--r--server/initializers/migrations/0760-video-live-replay-setting.ts125
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts15
-rw-r--r--server/lib/live/live-manager.ts33
-rw-r--r--server/middlewares/validators/videos/video-live.ts54
-rw-r--r--server/models/video/sql/video/shared/video-table-attributes.ts1
-rw-r--r--server/models/video/video-live-replay-setting.ts42
-rw-r--r--server/models/video/video-live-session.ts49
-rw-r--r--server/models/video/video-live.ts57
-rw-r--r--server/models/video/video.ts1
-rw-r--r--server/tests/api/check-params/live.ts47
-rw-r--r--server/tests/api/live/live-constraints.ts6
-rw-r--r--server/tests/api/live/live-fast-restream.ts1
-rw-r--r--server/tests/api/live/live-save-replay.ts264
-rw-r--r--server/tests/api/live/live.ts13
-rw-r--r--server/tests/api/notifications/user-notifications.ts2
-rw-r--r--server/tests/api/object-storage/live.ts1
-rw-r--r--server/tests/api/object-storage/video-static-file-privacy.ts12
-rw-r--r--server/tests/api/videos/video-static-file-privacy.ts12
-rw-r--r--server/types/express.d.ts6
-rw-r--r--server/types/models/video/index.ts1
-rw-r--r--server/types/models/video/video-live-replay-setting.ts3
-rw-r--r--server/types/models/video/video-live-session.ts6
-rw-r--r--server/types/models/video/video-live.ts9
-rw-r--r--shared/models/videos/live/live-video-create.model.ts2
-rw-r--r--shared/models/videos/live/live-video-session.model.ts3
-rw-r--r--shared/models/videos/live/live-video-update.model.ts2
-rw-r--r--shared/models/videos/live/live-video.model.ts2
-rw-r--r--shared/server-commands/videos/live-command.ts1
-rw-r--r--support/doc/api/openapi.yaml20
36 files changed, 746 insertions, 120 deletions
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
index fdd6b2311..b0da84979 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
@@ -272,7 +272,7 @@
272 </div> 272 </div>
273 </div> 273 </div>
274 274
275 <div class="form-group" *ngIf="isSaveReplayEnabled()"> 275 <div class="form-group" *ngIf="isSaveReplayAllowed()">
276 <my-peertube-checkbox inputName="liveVideoSaveReplay" formControlName="saveReplay"> 276 <my-peertube-checkbox inputName="liveVideoSaveReplay" formControlName="saveReplay">
277 <ng-template ptTemplate="label"> 277 <ng-template ptTemplate="label">
278 <ng-container i18n>Automatically publish a replay when your live ends</ng-container> 278 <ng-container i18n>Automatically publish a replay when your live ends</ng-container>
@@ -284,6 +284,13 @@
284 </my-peertube-checkbox> 284 </my-peertube-checkbox>
285 </div> 285 </div>
286 286
287 <div class="form-group mx-4" *ngIf="isSaveReplayEnabled()">
288 <label i18n for="replayPrivacy">Privacy of the new replay</label>
289 <my-select-options
290 labelForId="replayPrivacy" [items]="videoPrivacies" [clearable]="false" formControlName="replayPrivacy"
291 ></my-select-options>
292 </div>
293
287 <div class="form-group" *ngIf="isLatencyModeEnabled()"> 294 <div class="form-group" *ngIf="isLatencyModeEnabled()">
288 <label i18n for="latencyMode">Latency mode</label> 295 <label i18n for="latencyMode">Latency mode</label>
289 <my-select-options 296 <my-select-options
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
index 89687f35e..8ed54ce6b 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
@@ -165,7 +165,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
165 liveStreamKey: null, 165 liveStreamKey: null,
166 permanentLive: null, 166 permanentLive: null,
167 latencyMode: null, 167 latencyMode: null,
168 saveReplay: null 168 saveReplay: null,
169 replayPrivacy: null
169 } 170 }
170 171
171 this.formValidatorService.updateFormGroup( 172 this.formValidatorService.updateFormGroup(
@@ -303,10 +304,14 @@ export class VideoEditComponent implements OnInit, OnDestroy {
303 modalRef.componentInstance.captionEdited.subscribe(this.onCaptionEdited.bind(this)) 304 modalRef.componentInstance.captionEdited.subscribe(this.onCaptionEdited.bind(this))
304 } 305 }
305 306
306 isSaveReplayEnabled () { 307 isSaveReplayAllowed () {
307 return this.serverConfig.live.allowReplay 308 return this.serverConfig.live.allowReplay
308 } 309 }
309 310
311 isSaveReplayEnabled () {
312 return this.form.value['saveReplay'] === true
313 }
314
310 isPermanentLiveEnabled () { 315 isPermanentLiveEnabled () {
311 return this.form.value['permanentLive'] === true 316 return this.form.value['permanentLive'] === true
312 } 317 }
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
index 83a6b2229..904492994 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
@@ -8,7 +8,15 @@ import { Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared
8import { LiveVideoService } from '@app/shared/shared-video-live' 8import { LiveVideoService } from '@app/shared/shared-video-live'
9import { LoadingBarService } from '@ngx-loading-bar/core' 9import { LoadingBarService } from '@ngx-loading-bar/core'
10import { logger } from '@root-helpers/logger' 10import { logger } from '@root-helpers/logger'
11import { LiveVideo, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, PeerTubeProblemDocument, ServerErrorCode } from '@shared/models' 11import {
12 LiveVideo,
13 LiveVideoCreate,
14 LiveVideoLatencyMode,
15 LiveVideoUpdate,
16 PeerTubeProblemDocument,
17 ServerErrorCode,
18 VideoPrivacy
19} from '@shared/models'
12import { VideoSend } from './video-send' 20import { VideoSend } from './video-send'
13 21
14@Component({ 22@Component({
@@ -79,11 +87,12 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
79 permanentLive: this.firstStepPermanentLive, 87 permanentLive: this.firstStepPermanentLive,
80 latencyMode: LiveVideoLatencyMode.DEFAULT, 88 latencyMode: LiveVideoLatencyMode.DEFAULT,
81 saveReplay: this.isReplayAllowed(), 89 saveReplay: this.isReplayAllowed(),
90 replaySettings: { privacy: VideoPrivacy.PRIVATE },
82 channelId: this.firstStepChannelId 91 channelId: this.firstStepChannelId
83 } 92 }
84 93
85 // Go live in private mode, but correctly fill the update form with the first user choice 94 // Go live in private mode, but correctly fill the update form with the first user choice
86 const toPatch = { ...video, privacy: this.firstStepPrivacyId } 95 const toPatch = { ...video, privacy: this.firstStepPrivacyId, replayPrivacy: video.replaySettings.privacy }
87 this.form.patchValue(toPatch) 96 this.form.patchValue(toPatch)
88 97
89 this.liveVideoService.goLive(video) 98 this.liveVideoService.goLive(video)
@@ -130,6 +139,7 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
130 139
131 const liveVideoUpdate: LiveVideoUpdate = { 140 const liveVideoUpdate: LiveVideoUpdate = {
132 saveReplay: this.form.value.saveReplay, 141 saveReplay: this.form.value.saveReplay,
142 replaySettings: { privacy: this.form.value.replayPrivacy },
133 latencyMode: this.form.value.latencyMode, 143 latencyMode: this.form.value.latencyMode,
134 permanentLive: this.form.value.permanentLive 144 permanentLive: this.form.value.permanentLive
135 } 145 }
diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts
index 02398a036..412b43967 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.ts
+++ b/client/src/app/+videos/+video-edit/video-update.component.ts
@@ -67,6 +67,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
67 if (this.liveVideo) { 67 if (this.liveVideo) {
68 this.form.patchValue({ 68 this.form.patchValue({
69 saveReplay: this.liveVideo.saveReplay, 69 saveReplay: this.liveVideo.saveReplay,
70 replayPrivacy: this.liveVideo.replaySettings ? this.liveVideo.replaySettings.privacy : VideoPrivacy.PRIVATE,
70 latencyMode: this.liveVideo.latencyMode, 71 latencyMode: this.liveVideo.latencyMode,
71 permanentLive: this.liveVideo.permanentLive 72 permanentLive: this.liveVideo.permanentLive
72 }) 73 })
@@ -127,6 +128,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
127 128
128 const liveVideoUpdate: LiveVideoUpdate = { 129 const liveVideoUpdate: LiveVideoUpdate = {
129 saveReplay: !!this.form.value.saveReplay, 130 saveReplay: !!this.form.value.saveReplay,
131 replaySettings: { privacy: this.form.value.replayPrivacy },
130 permanentLive: !!this.form.value.permanentLive, 132 permanentLive: !!this.form.value.permanentLive,
131 latencyMode: this.form.value.latencyMode 133 latencyMode: this.form.value.latencyMode
132 } 134 }
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'
17import { VideoLiveModel } from '@server/models/video/video-live' 17import { VideoLiveModel } from '@server/models/video/video-live'
18import { VideoLiveSessionModel } from '@server/models/video/video-live-session' 18import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
19import { MVideoDetails, MVideoFullLight } from '@server/types/models' 19import { MVideoDetails, MVideoFullLight, MVideoLive } from '@server/types/models'
20import { buildUUID, uuidToShort } from '@shared/extra-utils' 20import { buildUUID, uuidToShort } from '@shared/extra-utils'
21import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoState } from '@shared/models' 21import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoState } from '@shared/models'
22import { logger } from '../../../helpers/logger' 22import { logger } from '../../../helpers/logger'
@@ -24,6 +24,7 @@ import { sequelizeTypescript } from '../../../initializers/database'
24import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' 24import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail'
25import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares' 25import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares'
26import { VideoModel } from '../../../models/video/video' 26import { VideoModel } from '../../../models/video/video'
27import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
27 28
28const liveRouter = express.Router() 29const 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
123async 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
119async function addLiveVideo (req: express.Request, res: express.Response) { 144async 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
29const LAST_MIGRATION_VERSION = 755 29const 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
52import { VideoTagModel } from '../models/video/video-tag' 52import { VideoTagModel } from '../models/video/video-tag'
53import { VideoViewModel } from '../models/view/video-view' 53import { VideoViewModel } from '../models/view/video-view'
54import { CONFIG } from './config' 54import { CONFIG } from './config'
55import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
55 56
56require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string 57require('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 @@
1import * as Sequelize from 'sequelize'
2
3async 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
118function down (options) {
119 throw new Error('Not implemented.')
120}
121
122export {
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
19import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' 19import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models'
20import { logger, loggerTagsFactory } from '../../../helpers/logger' 20import { logger, loggerTagsFactory } from '../../../helpers/logger'
21import { VideoPathManager } from '@server/lib/video-path-manager' 21import { VideoPathManager } from '@server/lib/video-path-manager'
22import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
22 23
23const lTags = loggerTagsFactory('live', 'job') 24const 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'
19import { VideoLiveModel } from '@server/models/video/video-live' 19import { VideoLiveModel } from '@server/models/video/video-live'
20import { VideoLiveSessionModel } from '@server/models/video/video-live-session' 20import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
21import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 21import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
22import { MVideo, MVideoLiveSession, MVideoLiveVideo } from '@server/types/models' 22import { MVideo, MVideoLiveSession, MVideoLiveVideo, MVideoLiveVideoWithSetting } from '@server/types/models'
23import { pick, wait } from '@shared/core-utils' 23import { pick, wait } from '@shared/core-utils'
24import { LiveVideoError, VideoState } from '@shared/models' 24import { LiveVideoError, VideoState } from '@shared/models'
25import { federateVideoIfNeeded } from '../activitypub/videos' 25import { federateVideoIfNeeded } from '../activitypub/videos'
@@ -30,6 +30,8 @@ import { Hooks } from '../plugins/hooks'
30import { LiveQuotaStore } from './live-quota-store' 30import { LiveQuotaStore } from './live-quota-store'
31import { cleanupAndDestroyPermanentLive } from './live-utils' 31import { cleanupAndDestroyPermanentLive } from './live-utils'
32import { MuxingSession } from './shared' 32import { MuxingSession } from './shared'
33import { sequelizeTypescript } from '@server/initializers/database'
34import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
33 35
34const NodeRtmpSession = require('node-media-server/src/node_rtmp_session') 36const NodeRtmpSession = require('node-media-server/src/node_rtmp_session')
35const context = require('node-media-server/src/node_core_ctx') 37const 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'
19import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' 19import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
20import { isVideoNameValid } from '../../../helpers/custom-validators/videos' 20import { isVideoNameValid, isVideoPrivacyValid } from '../../../helpers/custom-validators/videos'
21import { cleanUpReqFiles } from '../../../helpers/express-utils' 21import { cleanUpReqFiles } from '../../../helpers/express-utils'
22import { logger } from '../../../helpers/logger' 22import { logger } from '../../../helpers/logger'
23import { CONFIG } from '../../../initializers/config' 23import { 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
288function 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 @@
1import { isVideoPrivacyValid } from '@server/helpers/custom-validators/videos'
2import { MLiveReplaySetting } from '@server/types/models/video/video-live-replay-setting'
3import { VideoPrivacy } from '@shared/models/videos/video-privacy.enum'
4import { Transaction } from 'sequelize'
5import { AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
6import { throwIfNotValid } from '../shared/sequelize-helpers'
7
8@Table({
9 tableName: 'videoLiveReplaySetting'
10})
11export 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 @@
1import { FindOptions } from 'sequelize' 1import { FindOptions } from 'sequelize'
2import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 2import {
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'
3import { MVideoLiveSession, MVideoLiveSessionReplay } from '@server/types/models' 15import { MVideoLiveSession, MVideoLiveSessionReplay } from '@server/types/models'
4import { uuidToShort } from '@shared/extra-utils' 16import { uuidToShort } from '@shared/extra-utils'
5import { LiveVideoError, LiveVideoSession } from '@shared/models' 17import { LiveVideoError, LiveVideoSession } from '@shared/models'
6import { AttributesOnly } from '@shared/typescript-utils' 18import { AttributesOnly } from '@shared/typescript-utils'
7import { VideoModel } from './video' 19import { VideoModel } from './video'
20import { VideoLiveReplaySettingModel } from './video-live-replay-setting'
8 21
9export enum ScopeNames { 22export 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 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import {
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'
2import { CONFIG } from '@server/initializers/config' 14import { CONFIG } from '@server/initializers/config'
3import { WEBSERVER } from '@server/initializers/constants' 15import { WEBSERVER } from '@server/initializers/constants'
4import { MVideoLive, MVideoLiveVideo } from '@server/types/models' 16import { MVideoLive, MVideoLiveVideoWithSetting } from '@server/types/models'
5import { LiveVideo, LiveVideoLatencyMode, VideoState } from '@shared/models' 17import { LiveVideo, LiveVideoLatencyMode, VideoState } from '@shared/models'
6import { AttributesOnly } from '@shared/typescript-utils' 18import { AttributesOnly } from '@shared/typescript-utils'
7import { VideoModel } from './video' 19import { VideoModel } from './video'
8import { VideoBlacklistModel } from './video-blacklist' 20import { VideoBlacklistModel } from './video-blacklist'
21import { 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 @@
1import { OutgoingHttpHeaders } from 'http' 1import { OutgoingHttpHeaders } from 'http'
2import { Writable } from 'stream'
2import { RegisterServerAuthExternalOptions } from '@server/types' 3import { RegisterServerAuthExternalOptions } from '@server/types'
3import { 4import {
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'
46import { Writable } from 'stream'
47import { MVideoSource } from './models/video/video-source' 47import { MVideoSource } from './models/video/video-source'
48 48
49declare module 'express' { 49declare 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'
13export * from './video-comment' 13export * from './video-comment'
14export * from './video-file' 14export * from './video-file'
15export * from './video-import' 15export * from './video-import'
16export * from './video-live-replay-setting'
16export * from './video-live-session' 17export * from './video-live-session'
17export * from './video-live' 18export * from './video-live'
18export * from './video-playlist' 19export * 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 @@
1import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
2
3export 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 @@
1import { VideoLiveSessionModel } from '@server/models/video/video-live-session' 1import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
2import { PickWith } from '@shared/typescript-utils' 2import { PickWith } from '@shared/typescript-utils'
3import { MVideo } from './video' 3import { MVideo } from './video'
4import { MLiveReplaySetting } from './video-live-replay-setting'
4 5
5type Use<K extends keyof VideoLiveSessionModel, M> = PickWith<VideoLiveSessionModel, K, M> 6type Use<K extends keyof VideoLiveSessionModel, M> = PickWith<VideoLiveSessionModel, K, M>
6 7
7// ############################################################################ 8// ############################################################################
8 9
9export type MVideoLiveSession = Omit<VideoLiveSessionModel, 'Video' | 'VideoLive'> 10export type MVideoLiveSession = Omit<VideoLiveSessionModel, 'Video' | 'VideoLive' | 'ReplaySetting'>
10 11
11// ############################################################################ 12// ############################################################################
12 13
13export type MVideoLiveSessionReplay = 14export 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 @@
1import { VideoLiveModel } from '@server/models/video/video-live' 1import { VideoLiveModel } from '@server/models/video/video-live'
2import { PickWith } from '@shared/typescript-utils' 2import { PickWith } from '@shared/typescript-utils'
3import { MVideo } from './video' 3import { MVideo } from './video'
4import { MLiveReplaySetting } from './video-live-replay-setting'
4 5
5type Use<K extends keyof VideoLiveModel, M> = PickWith<VideoLiveModel, K, M> 6type Use<K extends keyof VideoLiveModel, M> = PickWith<VideoLiveModel, K, M>
6 7
7// ############################################################################ 8// ############################################################################
8 9
9export type MVideoLive = Omit<VideoLiveModel, 'Video'> 10export type MVideoLive = Omit<VideoLiveModel, 'Video' | 'ReplaySetting'>
10 11
11// ############################################################################ 12// ############################################################################
12 13
13export type MVideoLiveVideo = 14export type MVideoLiveVideo =
14 MVideoLive & 15 MVideoLive &
15 Use<'Video', MVideo> 16 Use<'Video', MVideo>
17
18// ############################################################################
19
20export type MVideoLiveVideoWithSetting =
21 MVideoLiveVideo &
22 Use<'ReplaySetting', MLiveReplaySetting>
diff --git a/shared/models/videos/live/live-video-create.model.ts b/shared/models/videos/live/live-video-create.model.ts
index bd245dec5..f8ae9e5a9 100644
--- a/shared/models/videos/live/live-video-create.model.ts
+++ b/shared/models/videos/live/live-video-create.model.ts
@@ -1,4 +1,5 @@
1import { VideoCreate } from '../video-create.model' 1import { VideoCreate } from '../video-create.model'
2import { VideoPrivacy } from '../video-privacy.enum'
2import { LiveVideoLatencyMode } from './live-video-latency-mode.enum' 3import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
3 4
4export interface LiveVideoCreate extends VideoCreate { 5export interface LiveVideoCreate extends VideoCreate {
@@ -6,4 +7,5 @@ export interface LiveVideoCreate extends VideoCreate {
6 latencyMode?: LiveVideoLatencyMode 7 latencyMode?: LiveVideoLatencyMode
7 8
8 saveReplay?: boolean 9 saveReplay?: boolean
10 replaySettings?: { privacy: VideoPrivacy }
9} 11}
diff --git a/shared/models/videos/live/live-video-session.model.ts b/shared/models/videos/live/live-video-session.model.ts
index 2464e2570..888c20a8a 100644
--- a/shared/models/videos/live/live-video-session.model.ts
+++ b/shared/models/videos/live/live-video-session.model.ts
@@ -1,3 +1,4 @@
1import { VideoPrivacy } from '../video-privacy.enum'
1import { LiveVideoError } from './live-video-error.enum' 2import { LiveVideoError } from './live-video-error.enum'
2 3
3export interface LiveVideoSession { 4export interface LiveVideoSession {
@@ -11,6 +12,8 @@ export interface LiveVideoSession {
11 saveReplay: boolean 12 saveReplay: boolean
12 endingProcessed: boolean 13 endingProcessed: boolean
13 14
15 replaySettings?: { privacy: VideoPrivacy }
16
14 replayVideo: { 17 replayVideo: {
15 id: number 18 id: number
16 uuid: string 19 uuid: string
diff --git a/shared/models/videos/live/live-video-update.model.ts b/shared/models/videos/live/live-video-update.model.ts
index 93bb4d30d..d6aa6fb37 100644
--- a/shared/models/videos/live/live-video-update.model.ts
+++ b/shared/models/videos/live/live-video-update.model.ts
@@ -1,7 +1,9 @@
1import { VideoPrivacy } from '../video-privacy.enum'
1import { LiveVideoLatencyMode } from './live-video-latency-mode.enum' 2import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
2 3
3export interface LiveVideoUpdate { 4export interface LiveVideoUpdate {
4 permanentLive?: boolean 5 permanentLive?: boolean
5 saveReplay?: boolean 6 saveReplay?: boolean
7 replaySettings?: { privacy: VideoPrivacy }
6 latencyMode?: LiveVideoLatencyMode 8 latencyMode?: LiveVideoLatencyMode
7} 9}
diff --git a/shared/models/videos/live/live-video.model.ts b/shared/models/videos/live/live-video.model.ts
index d0f57f883..fd8454123 100644
--- a/shared/models/videos/live/live-video.model.ts
+++ b/shared/models/videos/live/live-video.model.ts
@@ -1,3 +1,4 @@
1import { VideoPrivacy } from '../video-privacy.enum'
1import { LiveVideoLatencyMode } from './live-video-latency-mode.enum' 2import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
2 3
3export interface LiveVideo { 4export interface LiveVideo {
@@ -7,6 +8,7 @@ export interface LiveVideo {
7 streamKey?: string 8 streamKey?: string
8 9
9 saveReplay: boolean 10 saveReplay: boolean
11 replaySettings?: { privacy: VideoPrivacy }
10 permanentLive: boolean 12 permanentLive: boolean
11 latencyMode: LiveVideoLatencyMode 13 latencyMode: LiveVideoLatencyMode
12} 14}
diff --git a/shared/server-commands/videos/live-command.ts b/shared/server-commands/videos/live-command.ts
index 4541465ba..3273e3a8f 100644
--- a/shared/server-commands/videos/live-command.ts
+++ b/shared/server-commands/videos/live-command.ts
@@ -130,6 +130,7 @@ export class LiveCommand extends AbstractCommand {
130 name: 'live', 130 name: 'live',
131 permanentLive, 131 permanentLive,
132 saveReplay, 132 saveReplay,
133 replaySettings: { privacy },
133 channelId: this.server.store.channel.id, 134 channelId: this.server.store.channel.id,
134 privacy 135 privacy
135 } 136 }
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index a63694211..959a70438 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -2446,7 +2446,7 @@ paths:
2446 /api/v1/videos/privacies: 2446 /api/v1/videos/privacies:
2447 get: 2447 get:
2448 summary: List available video privacy policies 2448 summary: List available video privacy policies
2449 operationId: getPrivacyPolicies 2449 operationId: getVideoPrivacyPolicies
2450 tags: 2450 tags:
2451 - Video 2451 - Video
2452 responses: 2452 responses:
@@ -3087,6 +3087,8 @@ paths:
3087 type: integer 3087 type: integer
3088 saveReplay: 3088 saveReplay:
3089 type: boolean 3089 type: boolean
3090 replaySettings:
3091 $ref: '#/components/schemas/LiveVideoReplaySettings'
3090 permanentLive: 3092 permanentLive:
3091 description: User can stream multiple times in a permanent live 3093 description: User can stream multiple times in a permanent live
3092 type: boolean 3094 type: boolean
@@ -6088,7 +6090,7 @@ components:
6088 - 1 6090 - 1
6089 - 2 6091 - 2
6090 - 3 6092 - 3
6091 description: Video playlist privacy policy (see [/video-playlists/privacies]) 6093 description: Video playlist privacy policy (see [/video-playlists/privacies](#operation/getPlaylistPrivacyPolicies))
6092 VideoPlaylistPrivacyConstant: 6094 VideoPlaylistPrivacyConstant:
6093 properties: 6095 properties:
6094 id: 6096 id:
@@ -6116,7 +6118,7 @@ components:
6116 - 2 6118 - 2
6117 - 3 6119 - 3
6118 - 4 6120 - 4
6119 description: privacy id of the video (see [/videos/privacies](#operation/getPrivacyPolicies)) 6121 description: privacy id of the video (see [/videos/privacies](#operation/getVideoPrivacyPolicies))
6120 VideoPrivacyConstant: 6122 VideoPrivacyConstant:
6121 properties: 6123 properties:
6122 id: 6124 id:
@@ -6177,6 +6179,14 @@ components:
6177 - 2 6179 - 2
6178 - 3 6180 - 3
6179 description: 'The live latency mode (Default = `1`, High latency = `2`, Small Latency = `3`)' 6181 description: 'The live latency mode (Default = `1`, High latency = `2`, Small Latency = `3`)'
6182
6183 LiveVideoReplaySettings:
6184 type: object
6185 properties:
6186 privacy:
6187 # description: Video playlist privacy policy (see [../video-playlists/privacies])
6188 $ref: '#/components/schemas/VideoPrivacySet'
6189
6180 6190
6181 VideoStateConstant: 6191 VideoStateConstant:
6182 properties: 6192 properties:
@@ -8693,6 +8703,8 @@ components:
8693 properties: 8703 properties:
8694 saveReplay: 8704 saveReplay:
8695 type: boolean 8705 type: boolean
8706 replaySettings:
8707 $ref: '#/components/schemas/LiveVideoReplaySettings'
8696 permanentLive: 8708 permanentLive:
8697 description: User can stream multiple times in a permanent live 8709 description: User can stream multiple times in a permanent live
8698 type: boolean 8710 type: boolean
@@ -8713,6 +8725,8 @@ components:
8713 description: RTMP stream key to use to stream into this live video. Included in the response if an appropriate token is provided 8725 description: RTMP stream key to use to stream into this live video. Included in the response if an appropriate token is provided
8714 saveReplay: 8726 saveReplay:
8715 type: boolean 8727 type: boolean
8728 replaySettings:
8729 $ref: '#/components/schemas/LiveVideoReplaySettings'
8716 permanentLive: 8730 permanentLive:
8717 description: User can stream multiple times in a permanent live 8731 description: User can stream multiple times in a permanent live
8718 type: boolean 8732 type: boolean