aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-11-04 15:31:32 +0100
committerChocobozzz <chocobozzz@cpy.re>2020-11-09 15:33:04 +0100
commitbd54ad1953ee0484ba90cf5f588f4c282048f368 (patch)
tree36e84ed92589a8775bc18e1b056f6b8de14bf2bb /server
parent68e70a745b2010cd0199864a2addd60d8f99c732 (diff)
downloadPeerTube-bd54ad1953ee0484ba90cf5f588f4c282048f368.tar.gz
PeerTube-bd54ad1953ee0484ba90cf5f588f4c282048f368.tar.zst
PeerTube-bd54ad1953ee0484ba90cf5f588f4c282048f368.zip
Add live notification tests
Diffstat (limited to 'server')
-rw-r--r--server/lib/live-manager.ts2
-rw-r--r--server/lib/peertube-socket.ts14
-rw-r--r--server/models/video/video-file.ts4
-rw-r--r--server/models/video/video-format-utils.ts5
-rw-r--r--server/tests/api/live/index.ts6
-rw-r--r--server/tests/api/live/live.ts233
-rw-r--r--server/tests/api/videos/video-hls.ts13
7 files changed, 241 insertions, 36 deletions
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts
index 6eb05c9d6..d253d06fc 100644
--- a/server/lib/live-manager.ts
+++ b/server/lib/live-manager.ts
@@ -244,7 +244,7 @@ class LiveManager {
244 size: -1, 244 size: -1,
245 extname: '.ts', 245 extname: '.ts',
246 infoHash: null, 246 infoHash: null,
247 fps: -1, 247 fps,
248 videoStreamingPlaylistId: playlist.id 248 videoStreamingPlaylistId: playlist.id
249 }).catch(err => { 249 }).catch(err => {
250 logger.error('Cannot create file for live streaming.', { err }) 250 logger.error('Cannot create file for live streaming.', { err })
diff --git a/server/lib/peertube-socket.ts b/server/lib/peertube-socket.ts
index c918a8685..c4df399ca 100644
--- a/server/lib/peertube-socket.ts
+++ b/server/lib/peertube-socket.ts
@@ -6,6 +6,7 @@ import { UserNotificationModelForApi } from '@server/types/models/user'
6import { LiveVideoEventPayload, LiveVideoEventType } from '@shared/models' 6import { LiveVideoEventPayload, LiveVideoEventType } from '@shared/models'
7import { logger } from '../helpers/logger' 7import { logger } from '../helpers/logger'
8import { authenticateSocket } from '../middlewares' 8import { authenticateSocket } from '../middlewares'
9import { isIdValid } from '@server/helpers/custom-validators/misc'
9 10
10class PeerTubeSocket { 11class PeerTubeSocket {
11 12
@@ -39,8 +40,17 @@ class PeerTubeSocket {
39 40
40 this.liveVideosNamespace = io.of('/live-videos') 41 this.liveVideosNamespace = io.of('/live-videos')
41 .on('connection', socket => { 42 .on('connection', socket => {
42 socket.on('subscribe', ({ videoId }) => socket.join(videoId)) 43 socket.on('subscribe', ({ videoId }) => {
43 socket.on('unsubscribe', ({ videoId }) => socket.leave(videoId)) 44 if (!isIdValid(videoId)) return
45
46 socket.join(videoId)
47 })
48
49 socket.on('unsubscribe', ({ videoId }) => {
50 if (!isIdValid(videoId)) return
51
52 socket.leave(videoId)
53 })
44 }) 54 })
45 } 55 }
46 56
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 8c8fc0b51..5048cf9b7 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -329,6 +329,10 @@ export class VideoFileModel extends Model<VideoFileModel> {
329 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname] 329 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
330 } 330 }
331 331
332 isLive () {
333 return this.size === -1
334 }
335
332 hasSameUniqueKeysThan (other: MVideoFile) { 336 hasSameUniqueKeysThan (other: MVideoFile) {
333 return this.fps === other.fps && 337 return this.fps === other.fps &&
334 this.resolution === other.resolution && 338 this.resolution === other.resolution &&
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index 04e636a15..d4b213686 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -199,6 +199,7 @@ function videoFilesModelToFormattedJSON (
199 const video = extractVideo(model) 199 const video = extractVideo(model)
200 200
201 return [ ...videoFiles ] 201 return [ ...videoFiles ]
202 .filter(f => !f.isLive())
202 .sort(sortByResolutionDesc) 203 .sort(sortByResolutionDesc)
203 .map(videoFile => { 204 .map(videoFile => {
204 return { 205 return {
@@ -225,7 +226,9 @@ function addVideoFilesInAPAcc (
225 baseUrlWs: string, 226 baseUrlWs: string,
226 files: MVideoFile[] 227 files: MVideoFile[]
227) { 228) {
228 const sortedFiles = [ ...files ].sort(sortByResolutionDesc) 229 const sortedFiles = [ ...files ]
230 .filter(f => !f.isLive())
231 .sort(sortByResolutionDesc)
229 232
230 for (const file of sortedFiles) { 233 for (const file of sortedFiles) {
231 acc.push({ 234 acc.push({
diff --git a/server/tests/api/live/index.ts b/server/tests/api/live/index.ts
index ee77af286..32219969a 100644
--- a/server/tests/api/live/index.ts
+++ b/server/tests/api/live/index.ts
@@ -1,3 +1,3 @@
1export * from './live-constraints' 1import './live-constraints'
2export * from './live-save-replay' 2import './live-save-replay'
3export * from './live' 3import './live'
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts
index f7ccb453d..c795f201a 100644
--- a/server/tests/api/live/live.ts
+++ b/server/tests/api/live/live.ts
@@ -2,9 +2,12 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { LiveVideo, LiveVideoCreate, User, VideoDetails, VideoPrivacy } from '@shared/models' 5import { getLiveNotificationSocket } from '@shared/extra-utils/socket/socket-io'
6import { LiveVideo, LiveVideoCreate, User, Video, VideoDetails, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models'
6import { 7import {
7 addVideoToBlacklist, 8 addVideoToBlacklist,
9 checkLiveCleanup,
10 checkResolutionsInMasterPlaylist,
8 cleanupTests, 11 cleanupTests,
9 createLive, 12 createLive,
10 createUser, 13 createUser,
@@ -13,19 +16,23 @@ import {
13 getLive, 16 getLive,
14 getMyUserInformation, 17 getMyUserInformation,
15 getVideo, 18 getVideo,
19 getVideoIdFromUUID,
16 getVideosList, 20 getVideosList,
17 makeRawRequest, 21 makeRawRequest,
18 removeVideo, 22 removeVideo,
19 sendRTMPStream, 23 sendRTMPStream,
24 sendRTMPStreamInVideo,
20 ServerInfo, 25 ServerInfo,
21 setAccessTokensToServers, 26 setAccessTokensToServers,
22 setDefaultVideoChannel, 27 setDefaultVideoChannel,
28 stopFfmpeg,
23 testFfmpegStreamError, 29 testFfmpegStreamError,
24 testImage, 30 testImage,
25 updateCustomSubConfig, 31 updateCustomSubConfig,
26 updateLive, 32 updateLive,
27 userLogin, 33 userLogin,
28 waitJobs 34 waitJobs,
35 waitUntilLiveStarts
29} from '../../../../shared/extra-utils' 36} from '../../../../shared/extra-utils'
30 37
31const expect = chai.expect 38const expect = chai.expect
@@ -234,12 +241,12 @@ describe('Test live', function () {
234 async function createLiveWrapper () { 241 async function createLiveWrapper () {
235 const liveAttributes = { 242 const liveAttributes = {
236 name: 'user live', 243 name: 'user live',
237 channelId: userChannelId, 244 channelId: servers[0].videoChannel.id,
238 privacy: VideoPrivacy.PUBLIC, 245 privacy: VideoPrivacy.PUBLIC,
239 saveReplay: false 246 saveReplay: false
240 } 247 }
241 248
242 const res = await createLive(servers[0].url, userAccessToken, liveAttributes) 249 const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
243 const uuid = res.body.video.uuid 250 const uuid = res.body.video.uuid
244 251
245 const resLive = await getLive(servers[0].url, servers[0].accessToken, uuid) 252 const resLive = await getLive(servers[0].url, servers[0].accessToken, uuid)
@@ -295,42 +302,226 @@ describe('Test live', function () {
295 }) 302 })
296 303
297 describe('Live transcoding', function () { 304 describe('Live transcoding', function () {
305 let liveVideoId: string
306
307 async function createLiveWrapper (saveReplay: boolean) {
308 const liveAttributes = {
309 name: 'live video',
310 channelId: servers[0].videoChannel.id,
311 privacy: VideoPrivacy.PUBLIC,
312 saveReplay
313 }
314
315 const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
316 return res.body.video.uuid
317 }
318
319 async function testVideoResolutions (liveVideoId: string, resolutions: number[]) {
320 for (const server of servers) {
321 const resList = await getVideosList(server.url)
322 const videos: Video[] = resList.body.data
323
324 expect(videos.find(v => v.uuid === liveVideoId)).to.exist
325
326 const resVideo = await getVideo(server.url, liveVideoId)
327 const video: VideoDetails = resVideo.body
328
329 expect(video.streamingPlaylists).to.have.lengthOf(1)
330
331 const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)
332 expect(hlsPlaylist).to.exist
333
334 // Only finite files are displayed
335 expect(hlsPlaylist.files).to.have.lengthOf(0)
336
337 await checkResolutionsInMasterPlaylist(hlsPlaylist.playlistUrl, resolutions)
338 }
339 }
340
341 function updateConf (resolutions: number[]) {
342 return updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
343 live: {
344 enabled: true,
345 allowReplay: true,
346 maxDuration: null,
347 transcoding: {
348 enabled: true,
349 resolutions: {
350 '240p': resolutions.includes(240),
351 '360p': resolutions.includes(360),
352 '480p': resolutions.includes(480),
353 '720p': resolutions.includes(720),
354 '1080p': resolutions.includes(1080),
355 '2160p': resolutions.includes(2160)
356 }
357 }
358 }
359 })
360 }
361
362 before(async function () {
363 await updateConf([])
364 })
298 365
299 it('Should enable transcoding without additional resolutions', async function () { 366 it('Should enable transcoding without additional resolutions', async function () {
300 // enable 367 this.timeout(30000)
301 // stream 368
302 // wait federation + test 369 liveVideoId = await createLiveWrapper(false)
370
371 const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
372 await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoId)
373 await waitJobs(servers)
303 374
375 await testVideoResolutions(liveVideoId, [ 720 ])
376
377 await stopFfmpeg(command)
304 }) 378 })
305 379
306 it('Should enable transcoding with some resolutions', async function () { 380 it('Should enable transcoding with some resolutions', async function () {
307 // enable 381 this.timeout(30000)
308 // stream 382
309 // wait federation + test 383 const resolutions = [ 240, 480 ]
384 await updateConf(resolutions)
385 liveVideoId = await createLiveWrapper(false)
386
387 const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
388 await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoId)
389 await waitJobs(servers)
390
391 await testVideoResolutions(liveVideoId, resolutions)
392
393 await stopFfmpeg(command)
310 }) 394 })
311 395
312 it('Should enable transcoding with some resolutions and correctly save them', async function () { 396 it('Should enable transcoding with some resolutions and correctly save them', async function () {
313 // enable 397 this.timeout(60000)
314 // stream 398
315 // end stream 399 const resolutions = [ 240, 360, 720 ]
316 // wait federation + test 400 await updateConf(resolutions)
401 liveVideoId = await createLiveWrapper(true)
402
403 const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
404 await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoId)
405 await waitJobs(servers)
406
407 await testVideoResolutions(liveVideoId, resolutions)
408
409 await stopFfmpeg(command)
410
411 await waitJobs(servers)
412
413 for (const server of servers) {
414 const resVideo = await getVideo(server.url, liveVideoId)
415 const video: VideoDetails = resVideo.body
416
417 expect(video.duration).to.be.greaterThan(1)
418 expect(video.files).to.have.lengthOf(0)
419
420 const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)
421
422 expect(hlsPlaylist.files).to.have.lengthOf(resolutions.length)
423
424 for (const resolution of resolutions) {
425 const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
426
427 expect(file).to.exist
428 expect(file.fps).to.equal(25)
429 expect(file.size).to.be.greaterThan(1)
430
431 await makeRawRequest(file.torrentUrl, 200)
432 await makeRawRequest(file.fileUrl, 200)
433 }
434 }
317 }) 435 })
318 436
319 it('Should correctly have cleaned up the live files', async function () { 437 it('Should correctly have cleaned up the live files', async function () {
320 // check files 438 this.timeout(30000)
439
440 await checkLiveCleanup(servers[0], liveVideoId, [ 240, 360, 720 ])
321 }) 441 })
322 }) 442 })
323 443
324 describe('Live socket messages', function () { 444 describe('Live socket messages', function () {
325 445
326 it('Should correctly send a message when the live starts', async function () { 446 async function createLiveWrapper () {
327 // local 447 const liveAttributes = {
328 // federation 448 name: 'live video',
449 channelId: servers[0].videoChannel.id,
450 privacy: VideoPrivacy.PUBLIC
451 }
452
453 const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
454 return res.body.video.uuid
455 }
456
457 it('Should correctly send a message when the live starts and ends', async function () {
458 this.timeout(60000)
459
460 const localStateChanges: VideoState[] = []
461 const remoteStateChanges: VideoState[] = []
462
463 const liveVideoUUID = await createLiveWrapper()
464 await waitJobs(servers)
465
466 {
467 const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID)
468
469 const localSocket = getLiveNotificationSocket(servers[0].url)
470 localSocket.on('state-change', data => localStateChanges.push(data.state))
471 localSocket.emit('subscribe', { videoId })
472 }
473
474 {
475 const videoId = await getVideoIdFromUUID(servers[1].url, liveVideoUUID)
476
477 const remoteSocket = getLiveNotificationSocket(servers[1].url)
478 remoteSocket.on('state-change', data => remoteStateChanges.push(data.state))
479 remoteSocket.emit('subscribe', { videoId })
480 }
481
482 const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
483 await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoUUID)
484 await waitJobs(servers)
485
486 for (const stateChanges of [ localStateChanges, remoteStateChanges ]) {
487 expect(stateChanges).to.have.lengthOf(1)
488 expect(stateChanges[0]).to.equal(VideoState.PUBLISHED)
489 }
490
491 await stopFfmpeg(command)
492 await waitJobs(servers)
493
494 for (const stateChanges of [ localStateChanges, remoteStateChanges ]) {
495 expect(stateChanges).to.have.lengthOf(2)
496 expect(stateChanges[1]).to.equal(VideoState.LIVE_ENDED)
497 }
329 }) 498 })
330 499
331 it('Should correctly send a message when the live ends', async function () { 500 it('Should not receive a notification after unsubscribe', async function () {
332 // local 501 this.timeout(60000)
333 // federation 502
503 const stateChanges: VideoState[] = []
504
505 const liveVideoUUID = await createLiveWrapper()
506 await waitJobs(servers)
507
508 const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID)
509
510 const socket = getLiveNotificationSocket(servers[0].url)
511 socket.on('state-change', data => stateChanges.push(data.state))
512 socket.emit('subscribe', { videoId })
513
514 const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
515 await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoUUID)
516 await waitJobs(servers)
517
518 expect(stateChanges).to.have.lengthOf(1)
519 socket.emit('unsubscribe', { videoId })
520
521 await stopFfmpeg(command)
522 await waitJobs(servers)
523
524 expect(stateChanges).to.have.lengthOf(1)
334 }) 525 })
335 }) 526 })
336 527
diff --git a/server/tests/api/videos/video-hls.ts b/server/tests/api/videos/video-hls.ts
index 6555bc8b6..3a65cc1d2 100644
--- a/server/tests/api/videos/video-hls.ts
+++ b/server/tests/api/videos/video-hls.ts
@@ -1,9 +1,11 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
4import * as chai from 'chai'
5import { join } from 'path'
5import { 6import {
6 checkDirectoryIsEmpty, 7 checkDirectoryIsEmpty,
8 checkResolutionsInMasterPlaylist,
7 checkSegmentHash, 9 checkSegmentHash,
8 checkTmpIsEmpty, 10 checkTmpIsEmpty,
9 cleanupTests, 11 cleanupTests,
@@ -23,7 +25,6 @@ import {
23} from '../../../../shared/extra-utils' 25} from '../../../../shared/extra-utils'
24import { VideoDetails } from '../../../../shared/models/videos' 26import { VideoDetails } from '../../../../shared/models/videos'
25import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' 27import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
26import { join } from 'path'
27import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants' 28import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants'
28 29
29const expect = chai.expect 30const expect = chai.expect
@@ -66,16 +67,12 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
66 } 67 }
67 68
68 { 69 {
69 const res = await getPlaylist(hlsPlaylist.playlistUrl) 70 await checkResolutionsInMasterPlaylist(hlsPlaylist.playlistUrl, resolutions)
70 71
72 const res = await getPlaylist(hlsPlaylist.playlistUrl)
71 const masterPlaylist = res.text 73 const masterPlaylist = res.text
72 74
73 for (const resolution of resolutions) { 75 for (const resolution of resolutions) {
74 const reg = new RegExp(
75 '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',FRAME-RATE=\\d+,CODECS="avc1.64001f,mp4a.40.2"'
76 )
77
78 expect(masterPlaylist).to.match(reg)
79 expect(masterPlaylist).to.contain(`${resolution}.m3u8`) 76 expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
80 expect(masterPlaylist).to.contain(`${resolution}.m3u8`) 77 expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
81 } 78 }