X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Ftests%2Fapi%2Flive%2Flive.ts;h=f9b0d257bc846c9863663e6b8dbef143358afb13;hb=a687879e94fa5d3ecdd76bec3d94d0e1698ee913;hp=0b405dd94dcbfe44b48c8d2a6cd1f1f93f09a9ea;hpb=9f430a53be016f8db2736d5d8111282660b50f4c;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index 0b405dd94..f9b0d257b 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts @@ -1,18 +1,27 @@ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ -import 'mocha' -import * as chai from 'chai' +import { expect } from 'chai' import { basename, join } from 'path' -import { ffprobePromise, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils' +import { SQLCommand, testImage, testLiveVideoResolutions } from '@server/tests/shared' +import { getAllFiles, wait } from '@shared/core-utils' +import { ffprobePromise, getVideoStream } from '@shared/ffmpeg' +import { + HttpStatusCode, + LiveVideo, + LiveVideoCreate, + LiveVideoLatencyMode, + VideoDetails, + VideoPrivacy, + VideoState, + VideoStreamingPlaylistType +} from '@shared/models' import { - checkLiveCleanupAfterSave, - checkLiveSegmentHash, - checkResolutionsInMasterPlaylist, cleanupTests, createMultipleServers, doubleFollow, killallServers, LiveCommand, + makeGetRequest, makeRawRequest, PeerTubeServer, sendRTMPStream, @@ -20,22 +29,9 @@ import { setDefaultVideoChannel, stopFfmpeg, testFfmpegStreamError, - testImage, - wait, waitJobs, waitUntilLivePublishedOnAllServers -} from '@shared/extra-utils' -import { - HttpStatusCode, - LiveVideo, - LiveVideoCreate, - VideoDetails, - VideoPrivacy, - VideoState, - VideoStreamingPlaylistType -} from '@shared/models' - -const expect = chai.expect +} from '@shared/server-commands' describe('Test live', function () { let servers: PeerTubeServer[] = [] @@ -55,6 +51,9 @@ describe('Test live', function () { live: { enabled: true, allowReplay: true, + latencySetting: { + enabled: true + }, transcoding: { enabled: false } @@ -88,6 +87,8 @@ describe('Test live', function () { commentsEnabled: false, downloadEnabled: false, saveReplay: true, + replaySettings: { privacy: VideoPrivacy.PUBLIC }, + latencyMode: LiveVideoLatencyMode.SMALL_LATENCY, privacy: VideoPrivacy.PUBLIC, previewfile: 'video_short1-preview.webm.jpg', thumbnailfile: 'video_short1.webm.jpg' @@ -128,12 +129,16 @@ describe('Test live', function () { if (server.url === servers[0].url) { expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live') expect(live.streamKey).to.not.be.empty + + expect(live.replaySettings).to.exist + expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC) } else { - expect(live.rtmpUrl).to.be.null - expect(live.streamKey).to.be.null + expect(live.rtmpUrl).to.not.exist + expect(live.streamKey).to.not.exist } expect(live.saveReplay).to.be.true + expect(live.latencyMode).to.equal(LiveVideoLatencyMode.SMALL_LATENCY) } }) @@ -157,8 +162,8 @@ describe('Test live', function () { expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED) expect(video.nsfw).to.be.true - await makeRawRequest(server.url + video.thumbnailPath, HttpStatusCode.OK_200) - await makeRawRequest(server.url + video.previewPath, HttpStatusCode.OK_200) + await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) + await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 }) } }) @@ -178,7 +183,7 @@ describe('Test live', function () { it('Should update the live', async function () { this.timeout(10000) - await commands[0].update({ videoId: liveVideoUUID, fields: { saveReplay: false } }) + await commands[0].update({ videoId: liveVideoUUID, fields: { saveReplay: false, latencyMode: LiveVideoLatencyMode.DEFAULT } }) await waitJobs(servers) }) @@ -190,11 +195,13 @@ describe('Test live', function () { expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live') expect(live.streamKey).to.not.be.empty } else { - expect(live.rtmpUrl).to.be.null - expect(live.streamKey).to.be.null + expect(live.rtmpUrl).to.not.exist + expect(live.streamKey).to.not.exist } expect(live.saveReplay).to.be.false + expect(live.replaySettings).to.not.exist + expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT) } }) @@ -219,7 +226,7 @@ describe('Test live', function () { let vodVideoId: string before(async function () { - this.timeout(120000) + this.timeout(240000) vodVideoId = (await servers[0].videos.quickUpload({ name: 'vod video' })).uuid @@ -358,59 +365,23 @@ describe('Test live', function () { describe('Live transcoding', function () { let liveVideoId: string + let sqlCommandServer1: SQLCommand async function createLiveWrapper (saveReplay: boolean) { const liveAttributes = { name: 'live video', channelId: servers[0].store.channel.id, privacy: VideoPrivacy.PUBLIC, - saveReplay + saveReplay, + replaySettings: saveReplay + ? { privacy: VideoPrivacy.PUBLIC } + : undefined } const { uuid } = await commands[0].create({ fields: liveAttributes }) return uuid } - async function testVideoResolutions (liveVideoId: string, resolutions: number[]) { - for (const server of servers) { - const { data } = await server.videos.list() - expect(data.find(v => v.uuid === liveVideoId)).to.exist - - const video = await server.videos.get({ id: liveVideoId }) - - 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({ server, playlistUrl: hlsPlaylist.playlistUrl, resolutions }) - - for (let i = 0; i < resolutions.length; i++) { - const segmentNum = 3 - const segmentName = `${i}-00000${segmentNum}.ts` - await commands[0].waitUntilSegmentGeneration({ videoUUID: video.uuid, resolution: i, segment: segmentNum }) - - const subPlaylist = await servers[0].streamingPlaylists.get({ - url: `${servers[0].url}/static/streaming-playlists/hls/${video.uuid}/${i}.m3u8` - }) - - expect(subPlaylist).to.contain(segmentName) - - const baseUrlAndPath = servers[0].url + '/static/streaming-playlists/hls' - await checkLiveSegmentHash({ - server, - baseUrlSegment: baseUrlAndPath, - videoUUID: video.uuid, - segmentName, - hlsPlaylist - }) - } - } - } - function updateConf (resolutions: number[]) { return servers[0].config.updateCustomSubConfig({ newConfig: { @@ -421,6 +392,7 @@ describe('Test live', function () { transcoding: { enabled: true, resolutions: { + '144p': resolutions.includes(144), '240p': resolutions.includes(240), '360p': resolutions.includes(360), '480p': resolutions.includes(480), @@ -436,6 +408,8 @@ describe('Test live', function () { before(async function () { await updateConf([]) + + sqlCommandServer1 = new SQLCommand(servers[0]) }) it('Should enable transcoding without additional resolutions', async function () { @@ -447,7 +421,27 @@ describe('Test live', function () { await waitUntilLivePublishedOnAllServers(servers, liveVideoId) await waitJobs(servers) - await testVideoResolutions(liveVideoId, [ 720 ]) + await testLiveVideoResolutions({ + originServer: servers[0], + sqlCommand: sqlCommandServer1, + servers, + liveVideoId, + resolutions: [ 720 ], + objectStorage: false, + transcoded: true + }) + + await stopFfmpeg(ffmpegCommand) + }) + + it('Should transcode audio only RTMP stream', async function () { + this.timeout(120000) + + liveVideoId = await createLiveWrapper(false) + + const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short_no_audio.mp4' }) + await waitUntilLivePublishedOnAllServers(servers, liveVideoId) + await waitJobs(servers) await stopFfmpeg(ffmpegCommand) }) @@ -463,7 +457,15 @@ describe('Test live', function () { await waitUntilLivePublishedOnAllServers(servers, liveVideoId) await waitJobs(servers) - await testVideoResolutions(liveVideoId, resolutions) + await testLiveVideoResolutions({ + originServer: servers[0], + sqlCommand: sqlCommandServer1, + servers, + liveVideoId, + resolutions: resolutions.concat([ 720 ]), + objectStorage: false, + transcoded: true + }) await stopFfmpeg(ffmpegCommand) }) @@ -497,7 +499,7 @@ describe('Test live', function () { }) it('Should enable transcoding with some resolutions and correctly save them', async function () { - this.timeout(200000) + this.timeout(400_000) const resolutions = [ 240, 360, 720 ] @@ -508,7 +510,15 @@ describe('Test live', function () { await waitUntilLivePublishedOnAllServers(servers, liveVideoId) await waitJobs(servers) - await testVideoResolutions(liveVideoId, resolutions) + await testLiveVideoResolutions({ + originServer: servers[0], + sqlCommand: sqlCommandServer1, + servers, + liveVideoId, + resolutions, + objectStorage: false, + transcoded: true + }) await stopFfmpeg(ffmpegCommand) await commands[0].waitUntilEnded({ videoId: liveVideoId }) @@ -524,7 +534,7 @@ describe('Test live', function () { } const minBitrateLimits = { - 720: 5500 * 1000, + 720: 4800 * 1000, 360: 1000 * 1000, 240: 550 * 1000 } @@ -537,8 +547,8 @@ describe('Test live', function () { expect(video.files).to.have.lengthOf(0) const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS) - await makeRawRequest(hlsPlaylist.playlistUrl, HttpStatusCode.OK_200) - await makeRawRequest(hlsPlaylist.segmentsSha256Url, HttpStatusCode.OK_200) + await makeRawRequest({ url: hlsPlaylist.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) + await makeRawRequest({ url: hlsPlaylist.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) // We should have generated random filenames expect(basename(hlsPlaylist.playlistUrl)).to.not.equal('master.m3u8') @@ -553,9 +563,9 @@ describe('Test live', function () { expect(file.size).to.be.greaterThan(1) if (resolution >= 720) { - expect(file.fps).to.be.approximately(60, 2) + expect(file.fps).to.be.approximately(60, 10) } else { - expect(file.fps).to.be.approximately(30, 2) + expect(file.fps).to.be.approximately(30, 3) } const filename = basename(file.fileUrl) @@ -564,34 +574,126 @@ describe('Test live', function () { const segmentPath = servers[0].servers.buildDirectory(join('streaming-playlists', 'hls', video.uuid, filename)) const probe = await ffprobePromise(segmentPath) - const videoStream = await getVideoStreamFromFile(segmentPath, probe) + const videoStream = await getVideoStream(segmentPath, probe) expect(probe.format.bit_rate).to.be.below(maxBitrateLimits[videoStream.height]) expect(probe.format.bit_rate).to.be.at.least(minBitrateLimits[videoStream.height]) - await makeRawRequest(file.torrentUrl, HttpStatusCode.OK_200) - await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) + await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 }) + await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) } } }) - it('Should correctly have cleaned up the live files', async function () { - this.timeout(30000) + it('Should not generate an upper resolution than original file', async function () { + this.timeout(400_000) + + const resolutions = [ 240, 480 ] + await updateConf(resolutions) + + await servers[0].config.updateExistingSubConfig({ + newConfig: { + live: { + transcoding: { + alwaysTranscodeOriginalResolution: false + } + } + } + }) + + liveVideoId = await createLiveWrapper(true) + + const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' }) + await waitUntilLivePublishedOnAllServers(servers, liveVideoId) + await waitJobs(servers) + + await testLiveVideoResolutions({ + originServer: servers[0], + sqlCommand: sqlCommandServer1, + servers, + liveVideoId, + resolutions, + objectStorage: false, + transcoded: true + }) + + await stopFfmpeg(ffmpegCommand) + await commands[0].waitUntilEnded({ videoId: liveVideoId }) + + await waitJobs(servers) + + await waitUntilLivePublishedOnAllServers(servers, liveVideoId) + + const video = await servers[0].videos.get({ id: liveVideoId }) + const hlsFiles = video.streamingPlaylists[0].files + + expect(video.files).to.have.lengthOf(0) + expect(hlsFiles).to.have.lengthOf(resolutions.length) + + // eslint-disable-next-line @typescript-eslint/require-array-sort-compare + expect(getAllFiles(video).map(f => f.resolution.id).sort()).to.deep.equal(resolutions) + }) + + it('Should only keep the original resolution if all resolutions are disabled', async function () { + this.timeout(600_000) + + await updateConf([]) + liveVideoId = await createLiveWrapper(true) + + const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' }) + await waitUntilLivePublishedOnAllServers(servers, liveVideoId) + await waitJobs(servers) + + await testLiveVideoResolutions({ + originServer: servers[0], + sqlCommand: sqlCommandServer1, + servers, + liveVideoId, + resolutions: [ 720 ], + objectStorage: false, + transcoded: true + }) - await checkLiveCleanupAfterSave(servers[0], liveVideoId, [ 240, 360, 720 ]) + await stopFfmpeg(ffmpegCommand) + await commands[0].waitUntilEnded({ videoId: liveVideoId }) + + await waitJobs(servers) + + await waitUntilLivePublishedOnAllServers(servers, liveVideoId) + + const video = await servers[0].videos.get({ id: liveVideoId }) + const hlsFiles = video.streamingPlaylists[0].files + + expect(video.files).to.have.lengthOf(0) + expect(hlsFiles).to.have.lengthOf(1) + + expect(hlsFiles[0].resolution.id).to.equal(720) + }) + + after(async function () { + await sqlCommandServer1.cleanup() }) }) describe('After a server restart', function () { let liveVideoId: string let liveVideoReplayId: string + let permanentLiveVideoReplayId: string - async function createLiveWrapper (saveReplay: boolean) { - const liveAttributes = { + let permanentLiveReplayName: string + + let beforeServerRestart: Date + + async function createLiveWrapper (options: { saveReplay: boolean, permanent: boolean }) { + const liveAttributes: LiveVideoCreate = { name: 'live video', channelId: servers[0].store.channel.id, privacy: VideoPrivacy.PUBLIC, - saveReplay + saveReplay: options.saveReplay, + replaySettings: options.saveReplay + ? { privacy: VideoPrivacy.PUBLIC } + : undefined, + permanentLive: options.permanent } const { uuid } = await commands[0].create({ fields: liveAttributes }) @@ -599,40 +701,70 @@ describe('Test live', function () { } before(async function () { - this.timeout(120000) + this.timeout(600_000) - liveVideoId = await createLiveWrapper(false) - liveVideoReplayId = await createLiveWrapper(true) + liveVideoId = await createLiveWrapper({ saveReplay: false, permanent: false }) + liveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: false }) + permanentLiveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: true }) await Promise.all([ commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId }), + commands[0].sendRTMPStreamInVideo({ videoId: permanentLiveVideoReplayId }), commands[0].sendRTMPStreamInVideo({ videoId: liveVideoReplayId }) ]) await Promise.all([ commands[0].waitUntilPublished({ videoId: liveVideoId }), + commands[0].waitUntilPublished({ videoId: permanentLiveVideoReplayId }), commands[0].waitUntilPublished({ videoId: liveVideoReplayId }) ]) - await commands[0].waitUntilSegmentGeneration({ videoUUID: liveVideoId, resolution: 0, segment: 2 }) - await commands[0].waitUntilSegmentGeneration({ videoUUID: liveVideoReplayId, resolution: 0, segment: 2 }) + for (const videoUUID of [ liveVideoId, liveVideoReplayId, permanentLiveVideoReplayId ]) { + await commands[0].waitUntilSegmentGeneration({ + server: servers[0], + videoUUID, + playlistNumber: 0, + segment: 2, + objectStorage: false + }) + } + + { + const video = await servers[0].videos.get({ id: permanentLiveVideoReplayId }) + permanentLiveReplayName = video.name + ' - ' + new Date(video.publishedAt).toLocaleString() + } await killallServers([ servers[0] ]) + + beforeServerRestart = new Date() await servers[0].run() await wait(5000) + await waitJobs(servers) }) it('Should cleanup lives', async function () { this.timeout(60000) await commands[0].waitUntilEnded({ videoId: liveVideoId }) + await commands[0].waitUntilWaiting({ videoId: permanentLiveVideoReplayId }) }) - it('Should save a live replay', async function () { - this.timeout(120000) + it('Should save a non permanent live replay', async function () { + this.timeout(240000) await commands[0].waitUntilPublished({ videoId: liveVideoReplayId }) + + const session = await commands[0].getReplaySession({ videoId: liveVideoReplayId }) + expect(session.endDate).to.exist + expect(new Date(session.endDate)).to.be.above(beforeServerRestart) + }) + + it('Should have saved a permanent live replay', async function () { + this.timeout(120000) + + const { data } = await servers[0].videos.listMyVideos({ sort: '-publishedAt' }) + expect(data.find(v => v.name === permanentLiveReplayName)).to.exist }) })