diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/lib/live-manager.ts | 2 | ||||
-rw-r--r-- | server/lib/peertube-socket.ts | 14 | ||||
-rw-r--r-- | server/models/video/video-file.ts | 4 | ||||
-rw-r--r-- | server/models/video/video-format-utils.ts | 5 | ||||
-rw-r--r-- | server/tests/api/live/index.ts | 6 | ||||
-rw-r--r-- | server/tests/api/live/live.ts | 233 | ||||
-rw-r--r-- | server/tests/api/videos/video-hls.ts | 13 |
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' | |||
6 | import { LiveVideoEventPayload, LiveVideoEventType } from '@shared/models' | 6 | import { LiveVideoEventPayload, LiveVideoEventType } from '@shared/models' |
7 | import { logger } from '../helpers/logger' | 7 | import { logger } from '../helpers/logger' |
8 | import { authenticateSocket } from '../middlewares' | 8 | import { authenticateSocket } from '../middlewares' |
9 | import { isIdValid } from '@server/helpers/custom-validators/misc' | ||
9 | 10 | ||
10 | class PeerTubeSocket { | 11 | class 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 @@ | |||
1 | export * from './live-constraints' | 1 | import './live-constraints' |
2 | export * from './live-save-replay' | 2 | import './live-save-replay' |
3 | export * from './live' | 3 | import './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 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { LiveVideo, LiveVideoCreate, User, VideoDetails, VideoPrivacy } from '@shared/models' | 5 | import { getLiveNotificationSocket } from '@shared/extra-utils/socket/socket-io' |
6 | import { LiveVideo, LiveVideoCreate, User, Video, VideoDetails, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models' | ||
6 | import { | 7 | import { |
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 | ||
31 | const expect = chai.expect | 38 | const 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 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | ||
5 | import { join } from 'path' | ||
5 | import { | 6 | import { |
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' |
24 | import { VideoDetails } from '../../../../shared/models/videos' | 26 | import { VideoDetails } from '../../../../shared/models/videos' |
25 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' | 27 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' |
26 | import { join } from 'path' | ||
27 | import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants' | 28 | import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants' |
28 | 29 | ||
29 | const expect = chai.expect | 30 | const 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 | } |