1
0
Fork 0

Add live notification tests

This commit is contained in:
Chocobozzz 2020-11-04 15:31:32 +01:00 committed by Chocobozzz
parent 68e70a745b
commit bd54ad1953
11 changed files with 286 additions and 37 deletions

View file

@ -189,6 +189,7 @@
"@types/redis": "^2.8.5", "@types/redis": "^2.8.5",
"@types/request": "^2.0.3", "@types/request": "^2.0.3",
"@types/socket.io": "^2.1.2", "@types/socket.io": "^2.1.2",
"@types/socket.io-client": "^1.4.34",
"@types/supertest": "^2.0.3", "@types/supertest": "^2.0.3",
"@types/validator": "^13.0.0", "@types/validator": "^13.0.0",
"@types/webtorrent": "^0.107.0", "@types/webtorrent": "^0.107.0",
@ -211,6 +212,7 @@
"marked-man": "^0.7.0", "marked-man": "^0.7.0",
"mocha": "^8.0.1", "mocha": "^8.0.1",
"nodemon": "^2.0.1", "nodemon": "^2.0.1",
"socket.io-client": "^2.3.1",
"source-map-support": "^0.5.0", "source-map-support": "^0.5.0",
"supertest": "^4.0.2", "supertest": "^4.0.2",
"swagger-cli": "^4.0.2", "swagger-cli": "^4.0.2",

View file

@ -244,7 +244,7 @@ class LiveManager {
size: -1, size: -1,
extname: '.ts', extname: '.ts',
infoHash: null, infoHash: null,
fps: -1, fps,
videoStreamingPlaylistId: playlist.id videoStreamingPlaylistId: playlist.id
}).catch(err => { }).catch(err => {
logger.error('Cannot create file for live streaming.', { err }) logger.error('Cannot create file for live streaming.', { err })

View file

@ -6,6 +6,7 @@ import { UserNotificationModelForApi } from '@server/types/models/user'
import { LiveVideoEventPayload, LiveVideoEventType } from '@shared/models' import { LiveVideoEventPayload, LiveVideoEventType } from '@shared/models'
import { logger } from '../helpers/logger' import { logger } from '../helpers/logger'
import { authenticateSocket } from '../middlewares' import { authenticateSocket } from '../middlewares'
import { isIdValid } from '@server/helpers/custom-validators/misc'
class PeerTubeSocket { class PeerTubeSocket {
@ -39,8 +40,17 @@ class PeerTubeSocket {
this.liveVideosNamespace = io.of('/live-videos') this.liveVideosNamespace = io.of('/live-videos')
.on('connection', socket => { .on('connection', socket => {
socket.on('subscribe', ({ videoId }) => socket.join(videoId)) socket.on('subscribe', ({ videoId }) => {
socket.on('unsubscribe', ({ videoId }) => socket.leave(videoId)) if (!isIdValid(videoId)) return
socket.join(videoId)
})
socket.on('unsubscribe', ({ videoId }) => {
if (!isIdValid(videoId)) return
socket.leave(videoId)
})
}) })
} }

View file

@ -329,6 +329,10 @@ export class VideoFileModel extends Model<VideoFileModel> {
return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname] return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
} }
isLive () {
return this.size === -1
}
hasSameUniqueKeysThan (other: MVideoFile) { hasSameUniqueKeysThan (other: MVideoFile) {
return this.fps === other.fps && return this.fps === other.fps &&
this.resolution === other.resolution && this.resolution === other.resolution &&

View file

@ -199,6 +199,7 @@ function videoFilesModelToFormattedJSON (
const video = extractVideo(model) const video = extractVideo(model)
return [ ...videoFiles ] return [ ...videoFiles ]
.filter(f => !f.isLive())
.sort(sortByResolutionDesc) .sort(sortByResolutionDesc)
.map(videoFile => { .map(videoFile => {
return { return {
@ -225,7 +226,9 @@ function addVideoFilesInAPAcc (
baseUrlWs: string, baseUrlWs: string,
files: MVideoFile[] files: MVideoFile[]
) { ) {
const sortedFiles = [ ...files ].sort(sortByResolutionDesc) const sortedFiles = [ ...files ]
.filter(f => !f.isLive())
.sort(sortByResolutionDesc)
for (const file of sortedFiles) { for (const file of sortedFiles) {
acc.push({ acc.push({

View file

@ -1,3 +1,3 @@
export * from './live-constraints' import './live-constraints'
export * from './live-save-replay' import './live-save-replay'
export * from './live' import './live'

View file

@ -2,9 +2,12 @@
import 'mocha' import 'mocha'
import * as chai from 'chai' import * as chai from 'chai'
import { LiveVideo, LiveVideoCreate, User, VideoDetails, VideoPrivacy } from '@shared/models' import { getLiveNotificationSocket } from '@shared/extra-utils/socket/socket-io'
import { LiveVideo, LiveVideoCreate, User, Video, VideoDetails, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models'
import { import {
addVideoToBlacklist, addVideoToBlacklist,
checkLiveCleanup,
checkResolutionsInMasterPlaylist,
cleanupTests, cleanupTests,
createLive, createLive,
createUser, createUser,
@ -13,19 +16,23 @@ import {
getLive, getLive,
getMyUserInformation, getMyUserInformation,
getVideo, getVideo,
getVideoIdFromUUID,
getVideosList, getVideosList,
makeRawRequest, makeRawRequest,
removeVideo, removeVideo,
sendRTMPStream, sendRTMPStream,
sendRTMPStreamInVideo,
ServerInfo, ServerInfo,
setAccessTokensToServers, setAccessTokensToServers,
setDefaultVideoChannel, setDefaultVideoChannel,
stopFfmpeg,
testFfmpegStreamError, testFfmpegStreamError,
testImage, testImage,
updateCustomSubConfig, updateCustomSubConfig,
updateLive, updateLive,
userLogin, userLogin,
waitJobs waitJobs,
waitUntilLiveStarts
} from '../../../../shared/extra-utils' } from '../../../../shared/extra-utils'
const expect = chai.expect const expect = chai.expect
@ -234,12 +241,12 @@ describe('Test live', function () {
async function createLiveWrapper () { async function createLiveWrapper () {
const liveAttributes = { const liveAttributes = {
name: 'user live', name: 'user live',
channelId: userChannelId, channelId: servers[0].videoChannel.id,
privacy: VideoPrivacy.PUBLIC, privacy: VideoPrivacy.PUBLIC,
saveReplay: false saveReplay: false
} }
const res = await createLive(servers[0].url, userAccessToken, liveAttributes) const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
const uuid = res.body.video.uuid const uuid = res.body.video.uuid
const resLive = await getLive(servers[0].url, servers[0].accessToken, uuid) const resLive = await getLive(servers[0].url, servers[0].accessToken, uuid)
@ -295,42 +302,226 @@ describe('Test live', function () {
}) })
describe('Live transcoding', function () { describe('Live transcoding', function () {
let liveVideoId: string
async function createLiveWrapper (saveReplay: boolean) {
const liveAttributes = {
name: 'live video',
channelId: servers[0].videoChannel.id,
privacy: VideoPrivacy.PUBLIC,
saveReplay
}
const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
return res.body.video.uuid
}
async function testVideoResolutions (liveVideoId: string, resolutions: number[]) {
for (const server of servers) {
const resList = await getVideosList(server.url)
const videos: Video[] = resList.body.data
expect(videos.find(v => v.uuid === liveVideoId)).to.exist
const resVideo = await getVideo(server.url, liveVideoId)
const video: VideoDetails = resVideo.body
expect(video.streamingPlaylists).to.have.lengthOf(1)
const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)
expect(hlsPlaylist).to.exist
// Only finite files are displayed
expect(hlsPlaylist.files).to.have.lengthOf(0)
await checkResolutionsInMasterPlaylist(hlsPlaylist.playlistUrl, resolutions)
}
}
function updateConf (resolutions: number[]) {
return updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
live: {
enabled: true,
allowReplay: true,
maxDuration: null,
transcoding: {
enabled: true,
resolutions: {
'240p': resolutions.includes(240),
'360p': resolutions.includes(360),
'480p': resolutions.includes(480),
'720p': resolutions.includes(720),
'1080p': resolutions.includes(1080),
'2160p': resolutions.includes(2160)
}
}
}
})
}
before(async function () {
await updateConf([])
})
it('Should enable transcoding without additional resolutions', async function () { it('Should enable transcoding without additional resolutions', async function () {
// enable this.timeout(30000)
// stream
// wait federation + test
liveVideoId = await createLiveWrapper(false)
const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoId)
await waitJobs(servers)
await testVideoResolutions(liveVideoId, [ 720 ])
await stopFfmpeg(command)
}) })
it('Should enable transcoding with some resolutions', async function () { it('Should enable transcoding with some resolutions', async function () {
// enable this.timeout(30000)
// stream
// wait federation + test const resolutions = [ 240, 480 ]
await updateConf(resolutions)
liveVideoId = await createLiveWrapper(false)
const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoId)
await waitJobs(servers)
await testVideoResolutions(liveVideoId, resolutions)
await stopFfmpeg(command)
}) })
it('Should enable transcoding with some resolutions and correctly save them', async function () { it('Should enable transcoding with some resolutions and correctly save them', async function () {
// enable this.timeout(60000)
// stream
// end stream const resolutions = [ 240, 360, 720 ]
// wait federation + test await updateConf(resolutions)
liveVideoId = await createLiveWrapper(true)
const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoId)
await waitJobs(servers)
await testVideoResolutions(liveVideoId, resolutions)
await stopFfmpeg(command)
await waitJobs(servers)
for (const server of servers) {
const resVideo = await getVideo(server.url, liveVideoId)
const video: VideoDetails = resVideo.body
expect(video.duration).to.be.greaterThan(1)
expect(video.files).to.have.lengthOf(0)
const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)
expect(hlsPlaylist.files).to.have.lengthOf(resolutions.length)
for (const resolution of resolutions) {
const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
expect(file).to.exist
expect(file.fps).to.equal(25)
expect(file.size).to.be.greaterThan(1)
await makeRawRequest(file.torrentUrl, 200)
await makeRawRequest(file.fileUrl, 200)
}
}
}) })
it('Should correctly have cleaned up the live files', async function () { it('Should correctly have cleaned up the live files', async function () {
// check files this.timeout(30000)
await checkLiveCleanup(servers[0], liveVideoId, [ 240, 360, 720 ])
}) })
}) })
describe('Live socket messages', function () { describe('Live socket messages', function () {
it('Should correctly send a message when the live starts', async function () { async function createLiveWrapper () {
// local const liveAttributes = {
// federation name: 'live video',
channelId: servers[0].videoChannel.id,
privacy: VideoPrivacy.PUBLIC
}
const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
return res.body.video.uuid
}
it('Should correctly send a message when the live starts and ends', async function () {
this.timeout(60000)
const localStateChanges: VideoState[] = []
const remoteStateChanges: VideoState[] = []
const liveVideoUUID = await createLiveWrapper()
await waitJobs(servers)
{
const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID)
const localSocket = getLiveNotificationSocket(servers[0].url)
localSocket.on('state-change', data => localStateChanges.push(data.state))
localSocket.emit('subscribe', { videoId })
}
{
const videoId = await getVideoIdFromUUID(servers[1].url, liveVideoUUID)
const remoteSocket = getLiveNotificationSocket(servers[1].url)
remoteSocket.on('state-change', data => remoteStateChanges.push(data.state))
remoteSocket.emit('subscribe', { videoId })
}
const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoUUID)
await waitJobs(servers)
for (const stateChanges of [ localStateChanges, remoteStateChanges ]) {
expect(stateChanges).to.have.lengthOf(1)
expect(stateChanges[0]).to.equal(VideoState.PUBLISHED)
}
await stopFfmpeg(command)
await waitJobs(servers)
for (const stateChanges of [ localStateChanges, remoteStateChanges ]) {
expect(stateChanges).to.have.lengthOf(2)
expect(stateChanges[1]).to.equal(VideoState.LIVE_ENDED)
}
}) })
it('Should correctly send a message when the live ends', async function () { it('Should not receive a notification after unsubscribe', async function () {
// local this.timeout(60000)
// federation
const stateChanges: VideoState[] = []
const liveVideoUUID = await createLiveWrapper()
await waitJobs(servers)
const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID)
const socket = getLiveNotificationSocket(servers[0].url)
socket.on('state-change', data => stateChanges.push(data.state))
socket.emit('subscribe', { videoId })
const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoUUID)
await waitJobs(servers)
expect(stateChanges).to.have.lengthOf(1)
socket.emit('unsubscribe', { videoId })
await stopFfmpeg(command)
await waitJobs(servers)
expect(stateChanges).to.have.lengthOf(1)
}) })
}) })

View file

@ -1,9 +1,11 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import * as chai from 'chai'
import 'mocha' import 'mocha'
import * as chai from 'chai'
import { join } from 'path'
import { import {
checkDirectoryIsEmpty, checkDirectoryIsEmpty,
checkResolutionsInMasterPlaylist,
checkSegmentHash, checkSegmentHash,
checkTmpIsEmpty, checkTmpIsEmpty,
cleanupTests, cleanupTests,
@ -23,7 +25,6 @@ import {
} from '../../../../shared/extra-utils' } from '../../../../shared/extra-utils'
import { VideoDetails } from '../../../../shared/models/videos' import { VideoDetails } from '../../../../shared/models/videos'
import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
import { join } from 'path'
import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants' import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants'
const expect = chai.expect const expect = chai.expect
@ -66,16 +67,12 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
} }
{ {
const res = await getPlaylist(hlsPlaylist.playlistUrl) await checkResolutionsInMasterPlaylist(hlsPlaylist.playlistUrl, resolutions)
const res = await getPlaylist(hlsPlaylist.playlistUrl)
const masterPlaylist = res.text const masterPlaylist = res.text
for (const resolution of resolutions) { for (const resolution of resolutions) {
const reg = new RegExp(
'#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',FRAME-RATE=\\d+,CODECS="avc1.64001f,mp4a.40.2"'
)
expect(masterPlaylist).to.match(reg)
expect(masterPlaylist).to.contain(`${resolution}.m3u8`) expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
expect(masterPlaylist).to.contain(`${resolution}.m3u8`) expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
} }

View file

@ -6,8 +6,13 @@ function getUserNotificationSocket (serverUrl: string, accessToken: string) {
}) })
} }
function getLiveNotificationSocket (serverUrl: string) {
return io(serverUrl + '/live-videos')
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
getUserNotificationSocket getUserNotificationSocket,
getLiveNotificationSocket
} }

View file

@ -41,11 +41,26 @@ async function checkSegmentHash (
expect(sha256(res2.body)).to.equal(sha256Server) expect(sha256(res2.body)).to.equal(sha256Server)
} }
async function checkResolutionsInMasterPlaylist (playlistUrl: string, resolutions: number[]) {
const res = await getPlaylist(playlistUrl)
const masterPlaylist = res.text
for (const resolution of resolutions) {
const reg = new RegExp(
'#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
)
expect(masterPlaylist).to.match(reg)
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
getPlaylist, getPlaylist,
getSegment, getSegment,
checkResolutionsInMasterPlaylist,
getSegmentSha256, getSegmentSha256,
checkSegmentHash checkSegmentHash
} }

View file

@ -796,6 +796,11 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/socket.io-client@^1.4.34":
version "1.4.34"
resolved "https://registry.yarnpkg.com/@types/socket.io-client/-/socket.io-client-1.4.34.tgz#8ca5f5732a9ad92b79aba71083cda5e5821e3ed9"
integrity sha512-Lzia5OTQFJZJ5R4HsEEldywiiqT9+W2rDbyHJiiTGqOcju89sCsQ8aUXDljY6Ls33wKZZGC0bfMhr/VpOyjtXg==
"@types/socket.io@^2.1.2": "@types/socket.io@^2.1.2":
version "2.1.11" version "2.1.11"
resolved "https://registry.yarnpkg.com/@types/socket.io/-/socket.io-2.1.11.tgz#e0d6759880e5f9818d5297a3328b36641bae996b" resolved "https://registry.yarnpkg.com/@types/socket.io/-/socket.io-2.1.11.tgz#e0d6759880e5f9818d5297a3328b36641bae996b"
@ -6910,6 +6915,23 @@ socket.io-client@2.3.0:
socket.io-parser "~3.3.0" socket.io-parser "~3.3.0"
to-array "0.1.4" to-array "0.1.4"
socket.io-client@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.1.tgz#91a4038ef4d03c19967bb3c646fec6e0eaa78cff"
integrity sha512-YXmXn3pA8abPOY//JtYxou95Ihvzmg8U6kQyolArkIyLd0pgVhrfor/iMsox8cn07WCOOvvuJ6XKegzIucPutQ==
dependencies:
backo2 "1.0.2"
component-bind "1.0.0"
component-emitter "~1.3.0"
debug "~3.1.0"
engine.io-client "~3.4.0"
has-binary2 "~1.0.2"
indexof "0.0.1"
parseqs "0.0.6"
parseuri "0.0.6"
socket.io-parser "~3.3.0"
to-array "0.1.4"
socket.io-parser@~3.3.0: socket.io-parser@~3.3.0:
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.1.tgz#f07d9c8cb3fb92633aa93e76d98fd3a334623199" resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.1.tgz#f07d9c8cb3fb92633aa93e76d98fd3a334623199"