aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-12-03 14:10:54 +0100
committerChocobozzz <me@florianbigard.com>2020-12-03 15:21:16 +0100
commitbb4ba6d94c5051fdd665ebe63fffcc105778b8be (patch)
treed39302608c53e31395683bb5dd551eac6ced89f8 /server
parent19b7ebfaa822b12f6da25ad2ba10398b3ef25ec6 (diff)
downloadPeerTube-bb4ba6d94c5051fdd665ebe63fffcc105778b8be.tar.gz
PeerTube-bb4ba6d94c5051fdd665ebe63fffcc105778b8be.tar.zst
PeerTube-bb4ba6d94c5051fdd665ebe63fffcc105778b8be.zip
Add permanent live support
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/videos/index.ts2
-rw-r--r--server/controllers/api/videos/live.ts3
-rw-r--r--server/helpers/activitypub.ts10
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts3
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/migrations/0570-permanent-live.ts27
-rw-r--r--server/lib/activitypub/videos.ts2
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts27
-rw-r--r--server/lib/live-manager.ts40
-rw-r--r--server/middlewares/validators/videos/video-live.ts19
-rw-r--r--server/models/video/video-format-utils.ts4
-rw-r--r--server/models/video/video-live.ts5
-rw-r--r--server/tests/api/check-params/live.ts15
-rw-r--r--server/tests/api/live/index.ts1
-rw-r--r--server/tests/api/live/live-permanent.ts190
15 files changed, 326 insertions, 24 deletions
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 0dcd38ad2..71a0f97e2 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -174,7 +174,7 @@ function listVideoPrivacies (req: express.Request, res: express.Response) {
174} 174}
175 175
176async function addVideo (req: express.Request, res: express.Response) { 176async function addVideo (req: express.Request, res: express.Response) {
177 // Transferring the video could be long 177 // Uploading the video could be long
178 // Set timeout to 10 minutes, as Express's default is 2 minutes 178 // Set timeout to 10 minutes, as Express's default is 2 minutes
179 req.setTimeout(1000 * 60 * 10, () => { 179 req.setTimeout(1000 * 60 * 10, () => {
180 logger.error('Upload video has timed out.') 180 logger.error('Upload video has timed out.')
diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts
index a6f00c1bd..e67d89612 100644
--- a/server/controllers/api/videos/live.ts
+++ b/server/controllers/api/videos/live.ts
@@ -67,7 +67,9 @@ async function updateLiveVideo (req: express.Request, res: express.Response) {
67 67
68 const video = res.locals.videoAll 68 const video = res.locals.videoAll
69 const videoLive = res.locals.videoLive 69 const videoLive = res.locals.videoLive
70
70 videoLive.saveReplay = body.saveReplay || false 71 videoLive.saveReplay = body.saveReplay || false
72 videoLive.permanentLive = body.permanentLive || false
71 73
72 video.VideoLive = await videoLive.save() 74 video.VideoLive = await videoLive.save()
73 75
@@ -90,6 +92,7 @@ async function addLiveVideo (req: express.Request, res: express.Response) {
90 92
91 const videoLive = new VideoLiveModel() 93 const videoLive = new VideoLiveModel()
92 videoLive.saveReplay = videoInfo.saveReplay || false 94 videoLive.saveReplay = videoInfo.saveReplay || false
95 videoLive.permanentLive = videoInfo.permanentLive || false
93 videoLive.streamKey = uuidv4() 96 videoLive.streamKey = uuidv4()
94 97
95 const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ 98 const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index d28453d79..1188d6cf9 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -39,6 +39,16 @@ function getContextData (type: ContextType) {
39 sensitive: 'as:sensitive', 39 sensitive: 'as:sensitive',
40 language: 'sc:inLanguage', 40 language: 'sc:inLanguage',
41 41
42 isLiveBroadcast: 'sc:isLiveBroadcast',
43 liveSaveReplay: {
44 '@type': 'sc:Boolean',
45 '@id': 'pt:liveSaveReplay'
46 },
47 permanentLive: {
48 '@type': 'sc:Boolean',
49 '@id': 'pt:permanentLive'
50 },
51
42 Infohash: 'pt:Infohash', 52 Infohash: 'pt:Infohash',
43 Playlist: 'pt:Playlist', 53 Playlist: 'pt:Playlist',
44 PlaylistElement: 'pt:PlaylistElement', 54 PlaylistElement: 'pt:PlaylistElement',
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index cb385b07d..a01429c83 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -64,6 +64,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
64 if (!isBooleanValid(video.commentsEnabled)) video.commentsEnabled = false 64 if (!isBooleanValid(video.commentsEnabled)) video.commentsEnabled = false
65 if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false 65 if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false
66 if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false 66 if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false
67 if (!isBooleanValid(video.permanentLive)) video.permanentLive = false
67 68
68 return isActivityPubUrlValid(video.id) && 69 return isActivityPubUrlValid(video.id) &&
69 isVideoNameValid(video.name) && 70 isVideoNameValid(video.name) &&
@@ -74,8 +75,6 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
74 (!video.language || isRemoteStringIdentifierValid(video.language)) && 75 (!video.language || isRemoteStringIdentifierValid(video.language)) &&
75 isVideoViewsValid(video.views) && 76 isVideoViewsValid(video.views) &&
76 isBooleanValid(video.sensitive) && 77 isBooleanValid(video.sensitive) &&
77 isBooleanValid(video.commentsEnabled) &&
78 isBooleanValid(video.downloadEnabled) &&
79 isDateValid(video.published) && 78 isDateValid(video.published) &&
80 isDateValid(video.updated) && 79 isDateValid(video.updated) &&
81 (!video.originallyPublishedAt || isDateValid(video.originallyPublishedAt)) && 80 (!video.originallyPublishedAt || isDateValid(video.originallyPublishedAt)) &&
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 2c7acd757..9e642af95 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26 26
27const LAST_MIGRATION_VERSION = 565 27const LAST_MIGRATION_VERSION = 570
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
diff --git a/server/initializers/migrations/0570-permanent-live.ts b/server/initializers/migrations/0570-permanent-live.ts
new file mode 100644
index 000000000..9572a9b2d
--- /dev/null
+++ b/server/initializers/migrations/0570-permanent-live.ts
@@ -0,0 +1,27 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.BOOLEAN,
12 defaultValue: false,
13 allowNull: false
14 }
15
16 await utils.queryInterface.addColumn('videoLive', 'permanentLive', data)
17 }
18}
19
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
25 up,
26 down
27}
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 4053f487c..04f0bfc23 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -429,6 +429,7 @@ async function updateVideoFromAP (options: {
429 if (video.isLive) { 429 if (video.isLive) {
430 const [ videoLive ] = await VideoLiveModel.upsert({ 430 const [ videoLive ] = await VideoLiveModel.upsert({
431 saveReplay: videoObject.liveSaveReplay, 431 saveReplay: videoObject.liveSaveReplay,
432 permanentLive: videoObject.permanentLive,
432 videoId: video.id 433 videoId: video.id
433 }, { transaction: t, returning: true }) 434 }, { transaction: t, returning: true })
434 435
@@ -631,6 +632,7 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
631 const videoLive = new VideoLiveModel({ 632 const videoLive = new VideoLiveModel({
632 streamKey: null, 633 streamKey: null,
633 saveReplay: videoObject.liveSaveReplay, 634 saveReplay: videoObject.liveSaveReplay,
635 permanentLive: videoObject.permanentLive,
634 videoId: videoCreated.id 636 videoId: videoCreated.id
635 }) 637 })
636 638
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts
index d3c84ce75..e3c11caa2 100644
--- a/server/lib/job-queue/handlers/video-live-ending.ts
+++ b/server/lib/job-queue/handlers/video-live-ending.ts
@@ -1,5 +1,5 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { copy, readdir, remove } from 'fs-extra' 2import { copy, pathExists, readdir, remove } from 'fs-extra'
3import { join } from 'path' 3import { join } from 'path'
4import { getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils' 4import { getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils'
5import { VIDEO_LIVE } from '@server/initializers/constants' 5import { VIDEO_LIVE } from '@server/initializers/constants'
@@ -14,6 +14,7 @@ import { VideoStreamingPlaylistModel } from '@server/models/video/video-streamin
14import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' 14import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models'
15import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' 15import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models'
16import { logger } from '../../../helpers/logger' 16import { logger } from '../../../helpers/logger'
17import { LiveManager } from '@server/lib/live-manager'
17 18
18async function processVideoLiveEnding (job: Bull.Job) { 19async function processVideoLiveEnding (job: Bull.Job) {
19 const payload = job.data as VideoLiveEndingPayload 20 const payload = job.data as VideoLiveEndingPayload
@@ -36,6 +37,8 @@ async function processVideoLiveEnding (job: Bull.Job) {
36 return 37 return
37 } 38 }
38 39
40 LiveManager.Instance.cleanupShaSegments(video.uuid)
41
39 if (live.saveReplay !== true) { 42 if (live.saveReplay !== true) {
40 return cleanupLive(video, streamingPlaylist) 43 return cleanupLive(video, streamingPlaylist)
41 } 44 }
@@ -43,10 +46,19 @@ async function processVideoLiveEnding (job: Bull.Job) {
43 return saveLive(video, live) 46 return saveLive(video, live)
44} 47}
45 48
49async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) {
50 const hlsDirectory = getHLSDirectory(video)
51
52 await remove(hlsDirectory)
53
54 await streamingPlaylist.destroy()
55}
56
46// --------------------------------------------------------------------------- 57// ---------------------------------------------------------------------------
47 58
48export { 59export {
49 processVideoLiveEnding 60 processVideoLiveEnding,
61 cleanupLive
50} 62}
51 63
52// --------------------------------------------------------------------------- 64// ---------------------------------------------------------------------------
@@ -131,16 +143,9 @@ async function saveLive (video: MVideo, live: MVideoLive) {
131 await publishAndFederateIfNeeded(videoWithFiles, true) 143 await publishAndFederateIfNeeded(videoWithFiles, true)
132} 144}
133 145
134async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) {
135 const hlsDirectory = getHLSDirectory(video)
136
137 await remove(hlsDirectory)
138
139 streamingPlaylist.destroy()
140 .catch(err => logger.error('Cannot remove live streaming playlist.', { err }))
141}
142
143async function cleanupLiveFiles (hlsDirectory: string) { 146async function cleanupLiveFiles (hlsDirectory: string) {
147 if (!await pathExists(hlsDirectory)) return
148
144 const files = await readdir(hlsDirectory) 149 const files = await readdir(hlsDirectory)
145 150
146 for (const filename of files) { 151 for (const filename of files) {
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts
index 4f45ce530..dcf016169 100644
--- a/server/lib/live-manager.ts
+++ b/server/lib/live-manager.ts
@@ -19,6 +19,7 @@ import { VideoState, VideoStreamingPlaylistType } from '@shared/models'
19import { federateVideoIfNeeded } from './activitypub/videos' 19import { federateVideoIfNeeded } from './activitypub/videos'
20import { buildSha256Segment } from './hls' 20import { buildSha256Segment } from './hls'
21import { JobQueue } from './job-queue' 21import { JobQueue } from './job-queue'
22import { cleanupLive } from './job-queue/handlers/video-live-ending'
22import { PeerTubeSocket } from './peertube-socket' 23import { PeerTubeSocket } from './peertube-socket'
23import { isAbleToUploadVideo } from './user' 24import { isAbleToUploadVideo } from './user'
24import { getHLSDirectory } from './video-paths' 25import { getHLSDirectory } from './video-paths'
@@ -153,6 +154,10 @@ class LiveManager {
153 watchers.push(new Date().getTime()) 154 watchers.push(new Date().getTime())
154 } 155 }
155 156
157 cleanupShaSegments (videoUUID: string) {
158 this.segmentsSha256.delete(videoUUID)
159 }
160
156 private getContext () { 161 private getContext () {
157 return context 162 return context
158 } 163 }
@@ -184,6 +189,14 @@ class LiveManager {
184 return this.abortSession(sessionId) 189 return this.abortSession(sessionId)
185 } 190 }
186 191
192 // Cleanup old potential live files (could happen with a permanent live)
193 this.cleanupShaSegments(video.uuid)
194
195 const oldStreamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id)
196 if (oldStreamingPlaylist) {
197 await cleanupLive(video, oldStreamingPlaylist)
198 }
199
187 this.videoSessions.set(video.id, sessionId) 200 this.videoSessions.set(video.id, sessionId)
188 201
189 const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) 202 const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
@@ -372,7 +385,13 @@ class LiveManager {
372 logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', rtmpUrl) 385 logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', rtmpUrl)
373 386
374 this.transSessions.delete(sessionId) 387 this.transSessions.delete(sessionId)
388
375 this.watchersPerVideo.delete(videoLive.videoId) 389 this.watchersPerVideo.delete(videoLive.videoId)
390 this.videoSessions.delete(videoLive.videoId)
391
392 const newLivesPerUser = this.livesPerUser.get(user.id)
393 .filter(o => o.liveId !== videoLive.id)
394 this.livesPerUser.set(user.id, newLivesPerUser)
376 395
377 setTimeout(() => { 396 setTimeout(() => {
378 // Wait latest segments generation, and close watchers 397 // Wait latest segments generation, and close watchers
@@ -412,14 +431,21 @@ class LiveManager {
412 const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) 431 const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
413 if (!fullVideo) return 432 if (!fullVideo) return
414 433
415 JobQueue.Instance.createJob({ 434 const live = await VideoLiveModel.loadByVideoId(videoId)
416 type: 'video-live-ending', 435
417 payload: { 436 if (!live.permanentLive) {
418 videoId: fullVideo.id 437 JobQueue.Instance.createJob({
419 } 438 type: 'video-live-ending',
420 }, { delay: cleanupNow ? 0 : VIDEO_LIVE.CLEANUP_DELAY }) 439 payload: {
440 videoId: fullVideo.id
441 }
442 }, { delay: cleanupNow ? 0 : VIDEO_LIVE.CLEANUP_DELAY })
443
444 fullVideo.state = VideoState.LIVE_ENDED
445 } else {
446 fullVideo.state = VideoState.WAITING_FOR_LIVE
447 }
421 448
422 fullVideo.state = VideoState.LIVE_ENDED
423 await fullVideo.save() 449 await fullVideo.save()
424 450
425 PeerTubeSocket.Instance.sendVideoLiveNewState(fullVideo) 451 PeerTubeSocket.Instance.sendVideoLiveNewState(fullVideo)
diff --git a/server/middlewares/validators/videos/video-live.ts b/server/middlewares/validators/videos/video-live.ts
index ff92db910..69a14ccb1 100644
--- a/server/middlewares/validators/videos/video-live.ts
+++ b/server/middlewares/validators/videos/video-live.ts
@@ -49,9 +49,16 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
49 .customSanitizer(toBooleanOrNull) 49 .customSanitizer(toBooleanOrNull)
50 .custom(isBooleanValid).withMessage('Should have a valid saveReplay attribute'), 50 .custom(isBooleanValid).withMessage('Should have a valid saveReplay attribute'),
51 51
52 body('permanentLive')
53 .optional()
54 .customSanitizer(toBooleanOrNull)
55 .custom(isBooleanValid).withMessage('Should have a valid permanentLive attribute'),
56
52 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 57 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
53 logger.debug('Checking videoLiveAddValidator parameters', { parameters: req.body }) 58 logger.debug('Checking videoLiveAddValidator parameters', { parameters: req.body })
54 59
60 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
61
55 if (CONFIG.LIVE.ENABLED !== true) { 62 if (CONFIG.LIVE.ENABLED !== true) {
56 cleanUpReqFiles(req) 63 cleanUpReqFiles(req)
57 64
@@ -66,7 +73,12 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
66 .json({ error: 'Saving live replay is not allowed instance' }) 73 .json({ error: 'Saving live replay is not allowed instance' })
67 } 74 }
68 75
69 if (areValidationErrors(req, res)) return cleanUpReqFiles(req) 76 if (req.body.permanentLive && req.body.saveReplay) {
77 cleanUpReqFiles(req)
78
79 return res.status(400)
80 .json({ error: 'Cannot set this live as permanent while saving its replay' })
81 }
70 82
71 const user = res.locals.oauth.token.User 83 const user = res.locals.oauth.token.User
72 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) 84 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
@@ -116,6 +128,11 @@ const videoLiveUpdateValidator = [
116 128
117 if (areValidationErrors(req, res)) return 129 if (areValidationErrors(req, res)) return
118 130
131 if (req.body.permanentLive && req.body.saveReplay) {
132 return res.status(400)
133 .json({ error: 'Cannot set this live as permanent while saving its replay' })
134 }
135
119 if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) { 136 if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) {
120 return res.status(403) 137 return res.status(403)
121 .json({ error: 'Saving live replay is not allowed instance' }) 138 .json({ error: 'Saving live replay is not allowed instance' })
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index b1adbcb86..a1f022fb4 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -360,6 +360,10 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
360 ? video.VideoLive.saveReplay 360 ? video.VideoLive.saveReplay
361 : null, 361 : null,
362 362
363 permanentLive: video.isLive
364 ? video.VideoLive.permanentLive
365 : null,
366
363 state: video.state, 367 state: video.state,
364 commentsEnabled: video.commentsEnabled, 368 commentsEnabled: video.commentsEnabled,
365 downloadEnabled: video.downloadEnabled, 369 downloadEnabled: video.downloadEnabled,
diff --git a/server/models/video/video-live.ts b/server/models/video/video-live.ts
index f3bff74ea..875ba9b31 100644
--- a/server/models/video/video-live.ts
+++ b/server/models/video/video-live.ts
@@ -38,6 +38,10 @@ export class VideoLiveModel extends Model<VideoLiveModel> {
38 @Column 38 @Column
39 saveReplay: boolean 39 saveReplay: boolean
40 40
41 @AllowNull(false)
42 @Column
43 permanentLive: boolean
44
41 @CreatedAt 45 @CreatedAt
42 createdAt: Date 46 createdAt: Date
43 47
@@ -99,6 +103,7 @@ export class VideoLiveModel extends Model<VideoLiveModel> {
99 : null, 103 : null,
100 104
101 streamKey: this.streamKey, 105 streamKey: this.streamKey,
106 permanentLive: this.permanentLive,
102 saveReplay: this.saveReplay 107 saveReplay: this.saveReplay
103 } 108 }
104 } 109 }
diff --git a/server/tests/api/check-params/live.ts b/server/tests/api/check-params/live.ts
index 2b2d1beec..055f2f295 100644
--- a/server/tests/api/check-params/live.ts
+++ b/server/tests/api/check-params/live.ts
@@ -84,7 +84,8 @@ describe('Test video lives API validator', function () {
84 tags: [ 'tag1', 'tag2' ], 84 tags: [ 'tag1', 'tag2' ],
85 privacy: VideoPrivacy.PUBLIC, 85 privacy: VideoPrivacy.PUBLIC,
86 channelId, 86 channelId,
87 saveReplay: false 87 saveReplay: false,
88 permanentLive: false
88 } 89 }
89 }) 90 })
90 91
@@ -211,6 +212,12 @@ describe('Test video lives API validator', function () {
211 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) 212 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
212 }) 213 })
213 214
215 it('Should fail with save replay and permanent live set to true', async function () {
216 const fields = immutableAssign(baseCorrectParams, { saveReplay: true, permanentLive: true })
217
218 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
219 })
220
214 it('Should succeed with the correct parameters', async function () { 221 it('Should succeed with the correct parameters', async function () {
215 this.timeout(30000) 222 this.timeout(30000)
216 223
@@ -372,6 +379,12 @@ describe('Test video lives API validator', function () {
372 await updateLive(server.url, server.accessToken, videoIdNotLive, {}, 404) 379 await updateLive(server.url, server.accessToken, videoIdNotLive, {}, 404)
373 }) 380 })
374 381
382 it('Should fail with save replay and permanent live set to true', async function () {
383 const fields = { saveReplay: true, permanentLive: true }
384
385 await updateLive(server.url, server.accessToken, videoId, fields, 400)
386 })
387
375 it('Should succeed with the correct params', async function () { 388 it('Should succeed with the correct params', async function () {
376 await updateLive(server.url, server.accessToken, videoId, { saveReplay: false }) 389 await updateLive(server.url, server.accessToken, videoId, { saveReplay: false })
377 }) 390 })
diff --git a/server/tests/api/live/index.ts b/server/tests/api/live/index.ts
index 32219969a..c733f564e 100644
--- a/server/tests/api/live/index.ts
+++ b/server/tests/api/live/index.ts
@@ -1,3 +1,4 @@
1import './live-constraints' 1import './live-constraints'
2import './live-permanent'
2import './live-save-replay' 3import './live-save-replay'
3import './live' 4import './live'
diff --git a/server/tests/api/live/live-permanent.ts b/server/tests/api/live/live-permanent.ts
new file mode 100644
index 000000000..a64588ed7
--- /dev/null
+++ b/server/tests/api/live/live-permanent.ts
@@ -0,0 +1,190 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import * as chai from 'chai'
5import { LiveVideoCreate, VideoDetails, VideoPrivacy, VideoState } from '@shared/models'
6import {
7 checkLiveCleanup,
8 cleanupTests,
9 createLive,
10 doubleFollow,
11 flushAndRunMultipleServers,
12 getLive,
13 getPlaylistsCount,
14 getVideo,
15 sendRTMPStreamInVideo,
16 ServerInfo,
17 setAccessTokensToServers,
18 setDefaultVideoChannel,
19 stopFfmpeg,
20 updateCustomSubConfig,
21 updateLive,
22 wait,
23 waitJobs,
24 waitUntilLiveStarts
25} from '../../../../shared/extra-utils'
26
27const expect = chai.expect
28
29describe('Permenant live', function () {
30 let servers: ServerInfo[] = []
31 let videoUUID: string
32
33 async function createLiveWrapper (permanentLive: boolean) {
34 const attributes: LiveVideoCreate = {
35 channelId: servers[0].videoChannel.id,
36 privacy: VideoPrivacy.PUBLIC,
37 name: 'my super live',
38 saveReplay: false,
39 permanentLive
40 }
41
42 const res = await createLive(servers[0].url, servers[0].accessToken, attributes)
43 return res.body.video.uuid
44 }
45
46 async function checkVideoState (videoId: string, state: VideoState) {
47 for (const server of servers) {
48 const res = await getVideo(server.url, videoId)
49 expect((res.body as VideoDetails).state.id).to.equal(state)
50 }
51 }
52
53 before(async function () {
54 this.timeout(120000)
55
56 servers = await flushAndRunMultipleServers(2)
57
58 // Get the access tokens
59 await setAccessTokensToServers(servers)
60 await setDefaultVideoChannel(servers)
61
62 // Server 1 and server 2 follow each other
63 await doubleFollow(servers[0], servers[1])
64
65 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
66 live: {
67 enabled: true,
68 allowReplay: true,
69 maxDuration: null,
70 transcoding: {
71 enabled: true,
72 resolutions: {
73 '240p': true,
74 '360p': true,
75 '480p': true,
76 '720p': true,
77 '1080p': true,
78 '2160p': true
79 }
80 }
81 }
82 })
83 })
84
85 it('Should create a non permanent live and update it to be a permanent live', async function () {
86 this.timeout(20000)
87
88 const videoUUID = await createLiveWrapper(false)
89
90 {
91 const res = await getLive(servers[0].url, servers[0].accessToken, videoUUID)
92 expect(res.body.permanentLive).to.be.false
93 }
94
95 await updateLive(servers[0].url, servers[0].accessToken, videoUUID, { permanentLive: true })
96
97 {
98 const res = await getLive(servers[0].url, servers[0].accessToken, videoUUID)
99 expect(res.body.permanentLive).to.be.true
100 }
101 })
102
103 it('Should create a permanent live', async function () {
104 this.timeout(20000)
105
106 videoUUID = await createLiveWrapper(true)
107
108 const res = await getLive(servers[0].url, servers[0].accessToken, videoUUID)
109 expect(res.body.permanentLive).to.be.true
110
111 await waitJobs(servers)
112 })
113
114 it('Should stream into this permanent live', async function () {
115 this.timeout(40000)
116
117 const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, videoUUID)
118
119 for (const server of servers) {
120 await waitUntilLiveStarts(server.url, server.accessToken, videoUUID)
121 }
122
123 await checkVideoState(videoUUID, VideoState.PUBLISHED)
124
125 await stopFfmpeg(command)
126
127 await waitJobs(servers)
128 })
129
130 it('Should not have cleaned up this live', async function () {
131 this.timeout(40000)
132
133 await wait(5000)
134 await waitJobs(servers)
135
136 for (const server of servers) {
137 const res = await getVideo(server.url, videoUUID)
138
139 const videoDetails = res.body as VideoDetails
140 expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
141 }
142 })
143
144 it('Should have set this live to waiting for live state', async function () {
145 this.timeout(20000)
146
147 await checkVideoState(videoUUID, VideoState.WAITING_FOR_LIVE)
148 })
149
150 it('Should be able to stream again in the permanent live', async function () {
151 this.timeout(20000)
152
153 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
154 live: {
155 enabled: true,
156 allowReplay: true,
157 maxDuration: null,
158 transcoding: {
159 enabled: true,
160 resolutions: {
161 '240p': false,
162 '360p': false,
163 '480p': false,
164 '720p': false,
165 '1080p': false,
166 '2160p': false
167 }
168 }
169 }
170 })
171
172 const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, videoUUID)
173
174 for (const server of servers) {
175 await waitUntilLiveStarts(server.url, server.accessToken, videoUUID)
176 }
177
178 await checkVideoState(videoUUID, VideoState.PUBLISHED)
179
180 const count = await getPlaylistsCount(servers[0], videoUUID)
181 // master playlist and 720p playlist
182 expect(count).to.equal(2)
183
184 await stopFfmpeg(command)
185 })
186
187 after(async function () {
188 await cleanupTests(servers)
189 })
190})