]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/tests/api/live/live.ts
29081c6cca644357e9e8926da27af9b1138ee0a5
[github/Chocobozzz/PeerTube.git] / server / tests / api / live / live.ts
1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3 import 'mocha'
4 import * as chai from 'chai'
5 import { getLiveNotificationSocket } from '@shared/extra-utils/socket/socket-io'
6 import { LiveVideo, LiveVideoCreate, Video, VideoDetails, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models'
7 import {
8 addVideoToBlacklist,
9 checkLiveCleanup,
10 checkResolutionsInMasterPlaylist,
11 cleanupTests,
12 createLive,
13 doubleFollow,
14 flushAndRunMultipleServers,
15 getLive,
16 getVideo,
17 getVideoIdFromUUID,
18 getVideosList,
19 makeRawRequest,
20 removeVideo,
21 sendRTMPStream,
22 sendRTMPStreamInVideo,
23 ServerInfo,
24 setAccessTokensToServers,
25 setDefaultVideoChannel,
26 stopFfmpeg,
27 testFfmpegStreamError,
28 testImage,
29 updateCustomSubConfig,
30 updateLive,
31 viewVideo,
32 wait,
33 waitJobs,
34 waitUntilLiveStarts
35 } from '../../../../shared/extra-utils'
36 import { FfmpegCommand } from 'fluent-ffmpeg'
37
38 const expect = chai.expect
39
40 describe('Test live', function () {
41 let servers: ServerInfo[] = []
42
43 before(async function () {
44 this.timeout(120000)
45
46 servers = await flushAndRunMultipleServers(2)
47
48 // Get the access tokens
49 await setAccessTokensToServers(servers)
50 await setDefaultVideoChannel(servers)
51
52 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
53 live: {
54 enabled: true,
55 allowReplay: true,
56 transcoding: {
57 enabled: false
58 }
59 }
60 })
61
62 // Server 1 and server 2 follow each other
63 await doubleFollow(servers[0], servers[1])
64 })
65
66 describe('Live creation, update and delete', function () {
67 let liveVideoUUID: string
68
69 it('Should create a live with the appropriate parameters', async function () {
70 this.timeout(20000)
71
72 const attributes: LiveVideoCreate = {
73 category: 1,
74 licence: 2,
75 language: 'fr',
76 description: 'super live description',
77 support: 'support field',
78 channelId: servers[0].videoChannel.id,
79 nsfw: false,
80 waitTranscoding: false,
81 name: 'my super live',
82 tags: [ 'tag1', 'tag2' ],
83 commentsEnabled: false,
84 downloadEnabled: false,
85 saveReplay: true,
86 privacy: VideoPrivacy.PUBLIC,
87 previewfile: 'video_short1-preview.webm.jpg',
88 thumbnailfile: 'video_short1.webm.jpg'
89 }
90
91 const res = await createLive(servers[0].url, servers[0].accessToken, attributes)
92 liveVideoUUID = res.body.video.uuid
93
94 await waitJobs(servers)
95
96 for (const server of servers) {
97 const resVideo = await getVideo(server.url, liveVideoUUID)
98 const video: VideoDetails = resVideo.body
99
100 expect(video.category.id).to.equal(1)
101 expect(video.licence.id).to.equal(2)
102 expect(video.language.id).to.equal('fr')
103 expect(video.description).to.equal('super live description')
104 expect(video.support).to.equal('support field')
105
106 expect(video.channel.name).to.equal(servers[0].videoChannel.name)
107 expect(video.channel.host).to.equal(servers[0].videoChannel.host)
108
109 expect(video.nsfw).to.be.false
110 expect(video.waitTranscoding).to.be.false
111 expect(video.name).to.equal('my super live')
112 expect(video.tags).to.deep.equal([ 'tag1', 'tag2' ])
113 expect(video.commentsEnabled).to.be.false
114 expect(video.downloadEnabled).to.be.false
115 expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC)
116
117 await testImage(server.url, 'video_short1-preview.webm', video.previewPath)
118 await testImage(server.url, 'video_short1.webm', video.thumbnailPath)
119
120 const resLive = await getLive(server.url, server.accessToken, liveVideoUUID)
121 const live: LiveVideo = resLive.body
122
123 if (server.url === servers[0].url) {
124 expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live')
125 expect(live.streamKey).to.not.be.empty
126 } else {
127 expect(live.rtmpUrl).to.be.null
128 expect(live.streamKey).to.be.null
129 }
130
131 expect(live.saveReplay).to.be.true
132 }
133 })
134
135 it('Should have a default preview and thumbnail', async function () {
136 this.timeout(20000)
137
138 const attributes: LiveVideoCreate = {
139 name: 'default live thumbnail',
140 channelId: servers[0].videoChannel.id,
141 privacy: VideoPrivacy.UNLISTED,
142 nsfw: true
143 }
144
145 const res = await createLive(servers[0].url, servers[0].accessToken, attributes)
146 const videoId = res.body.video.uuid
147
148 await waitJobs(servers)
149
150 for (const server of servers) {
151 const resVideo = await getVideo(server.url, videoId)
152 const video: VideoDetails = resVideo.body
153
154 expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED)
155 expect(video.nsfw).to.be.true
156
157 await makeRawRequest(server.url + video.thumbnailPath, 200)
158 await makeRawRequest(server.url + video.previewPath, 200)
159 }
160 })
161
162 it('Should not have the live listed since nobody streams into', async function () {
163 for (const server of servers) {
164 const res = await getVideosList(server.url)
165
166 expect(res.body.total).to.equal(0)
167 expect(res.body.data).to.have.lengthOf(0)
168 }
169 })
170
171 it('Should not be able to update a live of another server', async function () {
172 await updateLive(servers[1].url, servers[1].accessToken, liveVideoUUID, { saveReplay: false }, 403)
173 })
174
175 it('Should update the live', async function () {
176 this.timeout(10000)
177
178 await updateLive(servers[0].url, servers[0].accessToken, liveVideoUUID, { saveReplay: false })
179 await waitJobs(servers)
180 })
181
182 it('Have the live updated', async function () {
183 for (const server of servers) {
184 const res = await getLive(server.url, server.accessToken, liveVideoUUID)
185 const live: LiveVideo = res.body
186
187 if (server.url === servers[0].url) {
188 expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live')
189 expect(live.streamKey).to.not.be.empty
190 } else {
191 expect(live.rtmpUrl).to.be.null
192 expect(live.streamKey).to.be.null
193 }
194
195 expect(live.saveReplay).to.be.false
196 }
197 })
198
199 it('Delete the live', async function () {
200 this.timeout(10000)
201
202 await removeVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
203 await waitJobs(servers)
204 })
205
206 it('Should have the live deleted', async function () {
207 for (const server of servers) {
208 await getVideo(server.url, liveVideoUUID, 404)
209 await getLive(server.url, server.accessToken, liveVideoUUID, 404)
210 }
211 })
212 })
213
214 describe('Stream checks', function () {
215 let liveVideo: LiveVideo & VideoDetails
216 let rtmpUrl: string
217
218 before(function () {
219 rtmpUrl = 'rtmp://' + servers[0].hostname + ':' + servers[0].rtmpPort + ''
220 })
221
222 async function createLiveWrapper () {
223 const liveAttributes = {
224 name: 'user live',
225 channelId: servers[0].videoChannel.id,
226 privacy: VideoPrivacy.PUBLIC,
227 saveReplay: false
228 }
229
230 const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
231 const uuid = res.body.video.uuid
232
233 const resLive = await getLive(servers[0].url, servers[0].accessToken, uuid)
234 const resVideo = await getVideo(servers[0].url, uuid)
235
236 return Object.assign(resVideo.body, resLive.body) as LiveVideo & VideoDetails
237 }
238
239 it('Should not allow a stream without the appropriate path', async function () {
240 this.timeout(30000)
241
242 liveVideo = await createLiveWrapper()
243
244 const command = sendRTMPStream(rtmpUrl + '/bad-live', liveVideo.streamKey)
245 await testFfmpegStreamError(command, true)
246 })
247
248 it('Should not allow a stream without the appropriate stream key', async function () {
249 this.timeout(30000)
250
251 const command = sendRTMPStream(rtmpUrl + '/live', 'bad-stream-key')
252 await testFfmpegStreamError(command, true)
253 })
254
255 it('Should succeed with the correct params', async function () {
256 this.timeout(30000)
257
258 const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey)
259 await testFfmpegStreamError(command, false)
260 })
261
262 it('Should not allow a stream on a live that was blacklisted', async function () {
263 this.timeout(30000)
264
265 liveVideo = await createLiveWrapper()
266
267 await addVideoToBlacklist(servers[0].url, servers[0].accessToken, liveVideo.uuid)
268
269 const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey)
270 await testFfmpegStreamError(command, true)
271 })
272
273 it('Should not allow a stream on a live that was deleted', async function () {
274 this.timeout(30000)
275
276 liveVideo = await createLiveWrapper()
277
278 await removeVideo(servers[0].url, servers[0].accessToken, liveVideo.uuid)
279
280 const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey)
281 await testFfmpegStreamError(command, true)
282 })
283 })
284
285 describe('Live transcoding', function () {
286 let liveVideoId: string
287
288 async function createLiveWrapper (saveReplay: boolean) {
289 const liveAttributes = {
290 name: 'live video',
291 channelId: servers[0].videoChannel.id,
292 privacy: VideoPrivacy.PUBLIC,
293 saveReplay
294 }
295
296 const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
297 return res.body.video.uuid
298 }
299
300 async function testVideoResolutions (liveVideoId: string, resolutions: number[]) {
301 for (const server of servers) {
302 const resList = await getVideosList(server.url)
303 const videos: Video[] = resList.body.data
304
305 expect(videos.find(v => v.uuid === liveVideoId)).to.exist
306
307 const resVideo = await getVideo(server.url, liveVideoId)
308 const video: VideoDetails = resVideo.body
309
310 expect(video.streamingPlaylists).to.have.lengthOf(1)
311
312 const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)
313 expect(hlsPlaylist).to.exist
314
315 // Only finite files are displayed
316 expect(hlsPlaylist.files).to.have.lengthOf(0)
317
318 await checkResolutionsInMasterPlaylist(hlsPlaylist.playlistUrl, resolutions)
319 }
320 }
321
322 function updateConf (resolutions: number[]) {
323 return updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
324 live: {
325 enabled: true,
326 allowReplay: true,
327 maxDuration: null,
328 transcoding: {
329 enabled: true,
330 resolutions: {
331 '240p': resolutions.includes(240),
332 '360p': resolutions.includes(360),
333 '480p': resolutions.includes(480),
334 '720p': resolutions.includes(720),
335 '1080p': resolutions.includes(1080),
336 '2160p': resolutions.includes(2160)
337 }
338 }
339 }
340 })
341 }
342
343 before(async function () {
344 await updateConf([])
345 })
346
347 it('Should enable transcoding without additional resolutions', async function () {
348 this.timeout(30000)
349
350 liveVideoId = await createLiveWrapper(false)
351
352 const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
353 await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoId)
354 await waitJobs(servers)
355
356 await testVideoResolutions(liveVideoId, [ 720 ])
357
358 await stopFfmpeg(command)
359 })
360
361 it('Should enable transcoding with some resolutions', async function () {
362 this.timeout(30000)
363
364 const resolutions = [ 240, 480 ]
365 await updateConf(resolutions)
366 liveVideoId = await createLiveWrapper(false)
367
368 const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
369 await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoId)
370 await waitJobs(servers)
371
372 await testVideoResolutions(liveVideoId, resolutions)
373
374 await stopFfmpeg(command)
375 })
376
377 it('Should enable transcoding with some resolutions and correctly save them', async function () {
378 this.timeout(60000)
379
380 const resolutions = [ 240, 360, 720 ]
381 await updateConf(resolutions)
382 liveVideoId = await createLiveWrapper(true)
383
384 const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
385 await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoId)
386 await waitJobs(servers)
387
388 await testVideoResolutions(liveVideoId, resolutions)
389
390 await stopFfmpeg(command)
391
392 await waitJobs(servers)
393
394 for (const server of servers) {
395 const resVideo = await getVideo(server.url, liveVideoId)
396 const video: VideoDetails = resVideo.body
397
398 expect(video.duration).to.be.greaterThan(1)
399 expect(video.files).to.have.lengthOf(0)
400
401 const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)
402
403 expect(hlsPlaylist.files).to.have.lengthOf(resolutions.length)
404
405 for (const resolution of resolutions) {
406 const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
407
408 expect(file).to.exist
409 expect(file.fps).to.equal(25)
410 expect(file.size).to.be.greaterThan(1)
411
412 await makeRawRequest(file.torrentUrl, 200)
413 await makeRawRequest(file.fileUrl, 200)
414 }
415 }
416 })
417
418 it('Should correctly have cleaned up the live files', async function () {
419 this.timeout(30000)
420
421 await checkLiveCleanup(servers[0], liveVideoId, [ 240, 360, 720 ])
422 })
423 })
424
425 describe('Live views', function () {
426 let liveVideoId: string
427 let command: FfmpegCommand
428
429 async function countViews (expected: number) {
430 for (const server of servers) {
431 const res = await getVideo(server.url, liveVideoId)
432 const video: VideoDetails = res.body
433
434 expect(video.views).to.equal(expected)
435 }
436 }
437
438 before(async function () {
439 this.timeout(30000)
440
441 const liveAttributes = {
442 name: 'live video',
443 channelId: servers[0].videoChannel.id,
444 privacy: VideoPrivacy.PUBLIC
445 }
446
447 const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
448 liveVideoId = res.body.video.uuid
449
450 command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
451 await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoId)
452 await waitJobs(servers)
453 })
454
455 it('Should display no views for a live', async function () {
456 await countViews(0)
457 })
458
459 it('Should view a live twice and display 1 view', async function () {
460 this.timeout(30000)
461
462 await viewVideo(servers[0].url, liveVideoId)
463 await viewVideo(servers[0].url, liveVideoId)
464
465 await wait(7000)
466
467 await waitJobs(servers)
468
469 await countViews(1)
470 })
471
472 it('Should wait and display 0 views', async function () {
473 this.timeout(30000)
474
475 await wait(7000)
476 await waitJobs(servers)
477
478 await countViews(0)
479 })
480
481 it('Should view a live on a remote and on local and display 2 views', async function () {
482 this.timeout(30000)
483
484 await viewVideo(servers[0].url, liveVideoId)
485 await viewVideo(servers[1].url, liveVideoId)
486 await viewVideo(servers[1].url, liveVideoId)
487
488 await wait(7000)
489 await waitJobs(servers)
490
491 await countViews(2)
492 })
493
494 after(async function () {
495 await stopFfmpeg(command)
496 })
497 })
498
499 describe('Live socket messages', function () {
500
501 async function createLiveWrapper () {
502 const liveAttributes = {
503 name: 'live video',
504 channelId: servers[0].videoChannel.id,
505 privacy: VideoPrivacy.PUBLIC
506 }
507
508 const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
509 return res.body.video.uuid
510 }
511
512 it('Should correctly send a message when the live starts and ends', async function () {
513 this.timeout(60000)
514
515 const localStateChanges: VideoState[] = []
516 const remoteStateChanges: VideoState[] = []
517
518 const liveVideoUUID = await createLiveWrapper()
519 await waitJobs(servers)
520
521 {
522 const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID)
523
524 const localSocket = getLiveNotificationSocket(servers[0].url)
525 localSocket.on('state-change', data => localStateChanges.push(data.state))
526 localSocket.emit('subscribe', { videoId })
527 }
528
529 {
530 const videoId = await getVideoIdFromUUID(servers[1].url, liveVideoUUID)
531
532 const remoteSocket = getLiveNotificationSocket(servers[1].url)
533 remoteSocket.on('state-change', data => remoteStateChanges.push(data.state))
534 remoteSocket.emit('subscribe', { videoId })
535 }
536
537 const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
538 await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoUUID)
539 await waitJobs(servers)
540
541 for (const stateChanges of [ localStateChanges, remoteStateChanges ]) {
542 expect(stateChanges).to.have.lengthOf(1)
543 expect(stateChanges[0]).to.equal(VideoState.PUBLISHED)
544 }
545
546 await stopFfmpeg(command)
547 await waitJobs(servers)
548
549 for (const stateChanges of [ localStateChanges, remoteStateChanges ]) {
550 expect(stateChanges).to.have.lengthOf(2)
551 expect(stateChanges[1]).to.equal(VideoState.LIVE_ENDED)
552 }
553 })
554
555 it('Should not receive a notification after unsubscribe', async function () {
556 this.timeout(60000)
557
558 const stateChanges: VideoState[] = []
559
560 const liveVideoUUID = await createLiveWrapper()
561 await waitJobs(servers)
562
563 const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID)
564
565 const socket = getLiveNotificationSocket(servers[0].url)
566 socket.on('state-change', data => stateChanges.push(data.state))
567 socket.emit('subscribe', { videoId })
568
569 const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
570 await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoUUID)
571 await waitJobs(servers)
572
573 expect(stateChanges).to.have.lengthOf(1)
574 socket.emit('unsubscribe', { videoId })
575
576 await stopFfmpeg(command)
577 await waitJobs(servers)
578
579 expect(stateChanges).to.have.lengthOf(1)
580 })
581 })
582
583 after(async function () {
584 await cleanupTests(servers)
585 })
586 })