diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-31 14:34:36 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-11 15:02:33 +0200 |
commit | 3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch) | |
tree | e4510b39bdac9c318fdb4b47018d08f15368b8f0 /packages/tests/src/api/live | |
parent | 04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff) | |
download | PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip |
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge
conflicts, but it's a major step forward:
* Server can be faster at startup because imports() are async and we can
easily lazy import big modules
* Angular doesn't seem to support ES import (with .js extension), so we
had to correctly organize peertube into a monorepo:
* Use yarn workspace feature
* Use typescript reference projects for dependencies
* Shared projects have been moved into "packages", each one is now a
node module (with a dedicated package.json/tsconfig.json)
* server/tools have been moved into apps/ and is now a dedicated app
bundled and published on NPM so users don't have to build peertube
cli tools manually
* server/tests have been moved into packages/ so we don't compile
them every time we want to run the server
* Use isolatedModule option:
* Had to move from const enum to const
(https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums)
* Had to explictely specify "type" imports when used in decorators
* Prefer tsx (that uses esbuild under the hood) instead of ts-node to
load typescript files (tests with mocha or scripts):
* To reduce test complexity as esbuild doesn't support decorator
metadata, we only test server files that do not import server
models
* We still build tests files into js files for a faster CI
* Remove unmaintained peertube CLI import script
* Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'packages/tests/src/api/live')
-rw-r--r-- | packages/tests/src/api/live/index.ts | 7 | ||||
-rw-r--r-- | packages/tests/src/api/live/live-constraints.ts | 237 | ||||
-rw-r--r-- | packages/tests/src/api/live/live-fast-restream.ts | 153 | ||||
-rw-r--r-- | packages/tests/src/api/live/live-permanent.ts | 204 | ||||
-rw-r--r-- | packages/tests/src/api/live/live-rtmps.ts | 143 | ||||
-rw-r--r-- | packages/tests/src/api/live/live-save-replay.ts | 583 | ||||
-rw-r--r-- | packages/tests/src/api/live/live-socket-messages.ts | 186 | ||||
-rw-r--r-- | packages/tests/src/api/live/live.ts | 766 |
8 files changed, 2279 insertions, 0 deletions
diff --git a/packages/tests/src/api/live/index.ts b/packages/tests/src/api/live/index.ts new file mode 100644 index 000000000..e61e6c611 --- /dev/null +++ b/packages/tests/src/api/live/index.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | import './live-constraints.js' | ||
2 | import './live-fast-restream.js' | ||
3 | import './live-socket-messages.js' | ||
4 | import './live-permanent.js' | ||
5 | import './live-rtmps.js' | ||
6 | import './live-save-replay.js' | ||
7 | import './live.js' | ||
diff --git a/packages/tests/src/api/live/live-constraints.ts b/packages/tests/src/api/live/live-constraints.ts new file mode 100644 index 000000000..f62994cbd --- /dev/null +++ b/packages/tests/src/api/live/live-constraints.ts | |||
@@ -0,0 +1,237 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { LiveVideoError, UserVideoQuota, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | ConfigCommand, | ||
9 | createMultipleServers, | ||
10 | doubleFollow, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultVideoChannel, | ||
14 | stopFfmpeg, | ||
15 | waitJobs, | ||
16 | waitUntilLiveReplacedByReplayOnAllServers, | ||
17 | waitUntilLiveWaitingOnAllServers | ||
18 | } from '@peertube/peertube-server-commands' | ||
19 | import { checkLiveCleanup } from '../../shared/live.js' | ||
20 | |||
21 | describe('Test live constraints', function () { | ||
22 | let servers: PeerTubeServer[] = [] | ||
23 | let userId: number | ||
24 | let userAccessToken: string | ||
25 | let userChannelId: number | ||
26 | |||
27 | async function createLiveWrapper (options: { replay: boolean, permanent: boolean }) { | ||
28 | const { replay, permanent } = options | ||
29 | |||
30 | const liveAttributes = { | ||
31 | name: 'user live', | ||
32 | channelId: userChannelId, | ||
33 | privacy: VideoPrivacy.PUBLIC, | ||
34 | saveReplay: replay, | ||
35 | replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined, | ||
36 | permanentLive: permanent | ||
37 | } | ||
38 | |||
39 | const { uuid } = await servers[0].live.create({ token: userAccessToken, fields: liveAttributes }) | ||
40 | return uuid | ||
41 | } | ||
42 | |||
43 | async function checkSaveReplay (videoId: string, resolutions = [ 720 ]) { | ||
44 | for (const server of servers) { | ||
45 | const video = await server.videos.get({ id: videoId }) | ||
46 | expect(video.isLive).to.be.false | ||
47 | expect(video.duration).to.be.greaterThan(0) | ||
48 | } | ||
49 | |||
50 | await checkLiveCleanup({ server: servers[0], permanent: false, videoUUID: videoId, savedResolutions: resolutions }) | ||
51 | } | ||
52 | |||
53 | function updateQuota (options: { total: number, daily: number }) { | ||
54 | return servers[0].users.update({ | ||
55 | userId, | ||
56 | videoQuota: options.total, | ||
57 | videoQuotaDaily: options.daily | ||
58 | }) | ||
59 | } | ||
60 | |||
61 | before(async function () { | ||
62 | this.timeout(120000) | ||
63 | |||
64 | servers = await createMultipleServers(2) | ||
65 | |||
66 | // Get the access tokens | ||
67 | await setAccessTokensToServers(servers) | ||
68 | await setDefaultVideoChannel(servers) | ||
69 | |||
70 | await servers[0].config.updateCustomSubConfig({ | ||
71 | newConfig: { | ||
72 | live: { | ||
73 | enabled: true, | ||
74 | allowReplay: true, | ||
75 | transcoding: { | ||
76 | enabled: false | ||
77 | } | ||
78 | } | ||
79 | } | ||
80 | }) | ||
81 | |||
82 | { | ||
83 | const res = await servers[0].users.generate('user1') | ||
84 | userId = res.userId | ||
85 | userChannelId = res.userChannelId | ||
86 | userAccessToken = res.token | ||
87 | |||
88 | await updateQuota({ total: 1, daily: -1 }) | ||
89 | } | ||
90 | |||
91 | // Server 1 and server 2 follow each other | ||
92 | await doubleFollow(servers[0], servers[1]) | ||
93 | }) | ||
94 | |||
95 | it('Should not have size limit if save replay is disabled', async function () { | ||
96 | this.timeout(60000) | ||
97 | |||
98 | const userVideoLiveoId = await createLiveWrapper({ replay: false, permanent: false }) | ||
99 | await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: false }) | ||
100 | }) | ||
101 | |||
102 | it('Should have size limit depending on user global quota if save replay is enabled on non permanent live', async function () { | ||
103 | this.timeout(60000) | ||
104 | |||
105 | // Wait for user quota memoize cache invalidation | ||
106 | await wait(5000) | ||
107 | |||
108 | const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false }) | ||
109 | await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true }) | ||
110 | |||
111 | await waitUntilLiveReplacedByReplayOnAllServers(servers, userVideoLiveoId) | ||
112 | await waitJobs(servers) | ||
113 | |||
114 | await checkSaveReplay(userVideoLiveoId) | ||
115 | |||
116 | const session = await servers[0].live.getReplaySession({ videoId: userVideoLiveoId }) | ||
117 | expect(session.error).to.equal(LiveVideoError.QUOTA_EXCEEDED) | ||
118 | }) | ||
119 | |||
120 | it('Should have size limit depending on user global quota if save replay is enabled on a permanent live', async function () { | ||
121 | this.timeout(60000) | ||
122 | |||
123 | // Wait for user quota memoize cache invalidation | ||
124 | await wait(5000) | ||
125 | |||
126 | const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: true }) | ||
127 | await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true }) | ||
128 | |||
129 | await waitJobs(servers) | ||
130 | await waitUntilLiveWaitingOnAllServers(servers, userVideoLiveoId) | ||
131 | |||
132 | const session = await servers[0].live.findLatestSession({ videoId: userVideoLiveoId }) | ||
133 | expect(session.error).to.equal(LiveVideoError.QUOTA_EXCEEDED) | ||
134 | }) | ||
135 | |||
136 | it('Should have size limit depending on user daily quota if save replay is enabled', async function () { | ||
137 | this.timeout(60000) | ||
138 | |||
139 | // Wait for user quota memoize cache invalidation | ||
140 | await wait(5000) | ||
141 | |||
142 | await updateQuota({ total: -1, daily: 1 }) | ||
143 | |||
144 | const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false }) | ||
145 | await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true }) | ||
146 | |||
147 | await waitUntilLiveReplacedByReplayOnAllServers(servers, userVideoLiveoId) | ||
148 | await waitJobs(servers) | ||
149 | |||
150 | await checkSaveReplay(userVideoLiveoId) | ||
151 | |||
152 | const session = await servers[0].live.getReplaySession({ videoId: userVideoLiveoId }) | ||
153 | expect(session.error).to.equal(LiveVideoError.QUOTA_EXCEEDED) | ||
154 | }) | ||
155 | |||
156 | it('Should succeed without quota limit', async function () { | ||
157 | this.timeout(60000) | ||
158 | |||
159 | // Wait for user quota memoize cache invalidation | ||
160 | await wait(5000) | ||
161 | |||
162 | await updateQuota({ total: 10 * 1000 * 1000, daily: -1 }) | ||
163 | |||
164 | const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false }) | ||
165 | await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: false }) | ||
166 | }) | ||
167 | |||
168 | it('Should have the same quota in admin and as a user', async function () { | ||
169 | this.timeout(120000) | ||
170 | |||
171 | const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false }) | ||
172 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ token: userAccessToken, videoId: userVideoLiveoId }) | ||
173 | |||
174 | await servers[0].live.waitUntilPublished({ videoId: userVideoLiveoId }) | ||
175 | // Wait previous live cleanups | ||
176 | await wait(3000) | ||
177 | |||
178 | const baseQuota = await servers[0].users.getMyQuotaUsed({ token: userAccessToken }) | ||
179 | |||
180 | let quotaUser: UserVideoQuota | ||
181 | |||
182 | do { | ||
183 | await wait(500) | ||
184 | |||
185 | quotaUser = await servers[0].users.getMyQuotaUsed({ token: userAccessToken }) | ||
186 | } while (quotaUser.videoQuotaUsed <= baseQuota.videoQuotaUsed) | ||
187 | |||
188 | const { data } = await servers[0].users.list() | ||
189 | const quotaAdmin = data.find(u => u.username === 'user1') | ||
190 | |||
191 | expect(quotaUser.videoQuotaUsed).to.be.above(baseQuota.videoQuotaUsed) | ||
192 | expect(quotaUser.videoQuotaUsedDaily).to.be.above(baseQuota.videoQuotaUsedDaily) | ||
193 | |||
194 | expect(quotaAdmin.videoQuotaUsed).to.be.above(baseQuota.videoQuotaUsed) | ||
195 | expect(quotaAdmin.videoQuotaUsedDaily).to.be.above(baseQuota.videoQuotaUsedDaily) | ||
196 | |||
197 | expect(quotaUser.videoQuotaUsed).to.be.above(10) | ||
198 | expect(quotaUser.videoQuotaUsedDaily).to.be.above(10) | ||
199 | expect(quotaAdmin.videoQuotaUsed).to.be.above(10) | ||
200 | expect(quotaAdmin.videoQuotaUsedDaily).to.be.above(10) | ||
201 | |||
202 | await stopFfmpeg(ffmpegCommand) | ||
203 | }) | ||
204 | |||
205 | it('Should have max duration limit', async function () { | ||
206 | this.timeout(240000) | ||
207 | |||
208 | await servers[0].config.updateCustomSubConfig({ | ||
209 | newConfig: { | ||
210 | live: { | ||
211 | enabled: true, | ||
212 | allowReplay: true, | ||
213 | maxDuration: 15, | ||
214 | transcoding: { | ||
215 | enabled: true, | ||
216 | resolutions: ConfigCommand.getCustomConfigResolutions(true) | ||
217 | } | ||
218 | } | ||
219 | } | ||
220 | }) | ||
221 | |||
222 | const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false }) | ||
223 | await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true }) | ||
224 | |||
225 | await waitUntilLiveReplacedByReplayOnAllServers(servers, userVideoLiveoId) | ||
226 | await waitJobs(servers) | ||
227 | |||
228 | await checkSaveReplay(userVideoLiveoId, [ 720, 480, 360, 240, 144 ]) | ||
229 | |||
230 | const session = await servers[0].live.getReplaySession({ videoId: userVideoLiveoId }) | ||
231 | expect(session.error).to.equal(LiveVideoError.DURATION_EXCEEDED) | ||
232 | }) | ||
233 | |||
234 | after(async function () { | ||
235 | await cleanupTests(servers) | ||
236 | }) | ||
237 | }) | ||
diff --git a/packages/tests/src/api/live/live-fast-restream.ts b/packages/tests/src/api/live/live-fast-restream.ts new file mode 100644 index 000000000..d34b00cbe --- /dev/null +++ b/packages/tests/src/api/live/live-fast-restream.ts | |||
@@ -0,0 +1,153 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | setDefaultVideoChannel, | ||
12 | stopFfmpeg, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('Fast restream in live', function () { | ||
17 | let server: PeerTubeServer | ||
18 | |||
19 | async function createLiveWrapper (options: { permanent: boolean, replay: boolean }) { | ||
20 | const attributes: LiveVideoCreate = { | ||
21 | channelId: server.store.channel.id, | ||
22 | privacy: VideoPrivacy.PUBLIC, | ||
23 | name: 'my super live', | ||
24 | saveReplay: options.replay, | ||
25 | replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined, | ||
26 | permanentLive: options.permanent | ||
27 | } | ||
28 | |||
29 | const { uuid } = await server.live.create({ fields: attributes }) | ||
30 | return uuid | ||
31 | } | ||
32 | |||
33 | async function fastRestreamWrapper ({ replay }: { replay: boolean }) { | ||
34 | const liveVideoUUID = await createLiveWrapper({ permanent: true, replay }) | ||
35 | await waitJobs([ server ]) | ||
36 | |||
37 | const rtmpOptions = { | ||
38 | videoId: liveVideoUUID, | ||
39 | copyCodecs: true, | ||
40 | fixtureName: 'video_short.mp4' | ||
41 | } | ||
42 | |||
43 | // Streaming session #1 | ||
44 | let ffmpegCommand = await server.live.sendRTMPStreamInVideo(rtmpOptions) | ||
45 | await server.live.waitUntilPublished({ videoId: liveVideoUUID }) | ||
46 | |||
47 | const video = await server.videos.get({ id: liveVideoUUID }) | ||
48 | const session1PlaylistId = video.streamingPlaylists[0].id | ||
49 | |||
50 | await stopFfmpeg(ffmpegCommand) | ||
51 | await server.live.waitUntilWaiting({ videoId: liveVideoUUID }) | ||
52 | |||
53 | // Streaming session #2 | ||
54 | ffmpegCommand = await server.live.sendRTMPStreamInVideo(rtmpOptions) | ||
55 | |||
56 | let hasNewPlaylist = false | ||
57 | do { | ||
58 | const video = await server.videos.get({ id: liveVideoUUID }) | ||
59 | hasNewPlaylist = video.streamingPlaylists.length === 1 && video.streamingPlaylists[0].id !== session1PlaylistId | ||
60 | |||
61 | await wait(100) | ||
62 | } while (!hasNewPlaylist) | ||
63 | |||
64 | await server.live.waitUntilSegmentGeneration({ | ||
65 | server, | ||
66 | videoUUID: liveVideoUUID, | ||
67 | segment: 1, | ||
68 | playlistNumber: 0 | ||
69 | }) | ||
70 | |||
71 | return { ffmpegCommand, liveVideoUUID } | ||
72 | } | ||
73 | |||
74 | async function ensureLastLiveWorks (liveId: string) { | ||
75 | // Equivalent to PEERTUBE_TEST_CONSTANTS_VIDEO_LIVE_CLEANUP_DELAY | ||
76 | for (let i = 0; i < 100; i++) { | ||
77 | const video = await server.videos.get({ id: liveId }) | ||
78 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
79 | |||
80 | try { | ||
81 | await server.live.getSegmentFile({ videoUUID: liveId, segment: 0, playlistNumber: 0 }) | ||
82 | await server.streamingPlaylists.get({ url: video.streamingPlaylists[0].playlistUrl }) | ||
83 | await server.streamingPlaylists.getSegmentSha256({ url: video.streamingPlaylists[0].segmentsSha256Url }) | ||
84 | } catch (err) { | ||
85 | // FIXME: try to debug error in CI "Unexpected end of JSON input" | ||
86 | console.error(err) | ||
87 | throw err | ||
88 | } | ||
89 | |||
90 | await wait(100) | ||
91 | } | ||
92 | } | ||
93 | |||
94 | async function runTest (replay: boolean) { | ||
95 | const { ffmpegCommand, liveVideoUUID } = await fastRestreamWrapper({ replay }) | ||
96 | |||
97 | // TODO: remove, we try to debug a test timeout failure here | ||
98 | console.log('Ensuring last live works') | ||
99 | |||
100 | await ensureLastLiveWorks(liveVideoUUID) | ||
101 | |||
102 | await stopFfmpeg(ffmpegCommand) | ||
103 | await server.live.waitUntilWaiting({ videoId: liveVideoUUID }) | ||
104 | |||
105 | // Wait for replays | ||
106 | await waitJobs([ server ]) | ||
107 | |||
108 | const { total, data: sessions } = await server.live.listSessions({ videoId: liveVideoUUID }) | ||
109 | |||
110 | expect(total).to.equal(2) | ||
111 | expect(sessions).to.have.lengthOf(2) | ||
112 | |||
113 | for (const session of sessions) { | ||
114 | expect(session.error).to.be.null | ||
115 | |||
116 | if (replay) { | ||
117 | expect(session.replayVideo).to.exist | ||
118 | |||
119 | await server.videos.get({ id: session.replayVideo.uuid }) | ||
120 | } else { | ||
121 | expect(session.replayVideo).to.not.exist | ||
122 | } | ||
123 | } | ||
124 | } | ||
125 | |||
126 | before(async function () { | ||
127 | this.timeout(120000) | ||
128 | |||
129 | const env = { PEERTUBE_TEST_CONSTANTS_VIDEO_LIVE_CLEANUP_DELAY: '10000' } | ||
130 | server = await createSingleServer(1, {}, { env }) | ||
131 | |||
132 | // Get the access tokens | ||
133 | await setAccessTokensToServers([ server ]) | ||
134 | await setDefaultVideoChannel([ server ]) | ||
135 | |||
136 | await server.config.enableMinimumTranscoding({ webVideo: false, hls: true }) | ||
137 | await server.config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' }) | ||
138 | }) | ||
139 | |||
140 | it('Should correctly fast restream in a permanent live with and without save replay', async function () { | ||
141 | this.timeout(480000) | ||
142 | |||
143 | // A test can take a long time, so prefer to run them in parallel | ||
144 | await Promise.all([ | ||
145 | runTest(true), | ||
146 | runTest(false) | ||
147 | ]) | ||
148 | }) | ||
149 | |||
150 | after(async function () { | ||
151 | await cleanupTests([ server ]) | ||
152 | }) | ||
153 | }) | ||
diff --git a/packages/tests/src/api/live/live-permanent.ts b/packages/tests/src/api/live/live-permanent.ts new file mode 100644 index 000000000..4ffcc7ed4 --- /dev/null +++ b/packages/tests/src/api/live/live-permanent.ts | |||
@@ -0,0 +1,204 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { LiveVideoCreate, VideoPrivacy, VideoState, VideoStateType } from '@peertube/peertube-models' | ||
6 | import { checkLiveCleanup } from '@tests/shared/live.js' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | ConfigCommand, | ||
10 | createMultipleServers, | ||
11 | doubleFollow, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers, | ||
14 | setDefaultVideoChannel, | ||
15 | stopFfmpeg, | ||
16 | waitJobs | ||
17 | } from '@peertube/peertube-server-commands' | ||
18 | |||
19 | describe('Permanent live', function () { | ||
20 | let servers: PeerTubeServer[] = [] | ||
21 | let videoUUID: string | ||
22 | |||
23 | async function createLiveWrapper (permanentLive: boolean) { | ||
24 | const attributes: LiveVideoCreate = { | ||
25 | channelId: servers[0].store.channel.id, | ||
26 | privacy: VideoPrivacy.PUBLIC, | ||
27 | name: 'my super live', | ||
28 | saveReplay: false, | ||
29 | permanentLive | ||
30 | } | ||
31 | |||
32 | const { uuid } = await servers[0].live.create({ fields: attributes }) | ||
33 | return uuid | ||
34 | } | ||
35 | |||
36 | async function checkVideoState (videoId: string, state: VideoStateType) { | ||
37 | for (const server of servers) { | ||
38 | const video = await server.videos.get({ id: videoId }) | ||
39 | expect(video.state.id).to.equal(state) | ||
40 | } | ||
41 | } | ||
42 | |||
43 | before(async function () { | ||
44 | this.timeout(120000) | ||
45 | |||
46 | servers = await createMultipleServers(2) | ||
47 | |||
48 | // Get the access tokens | ||
49 | await setAccessTokensToServers(servers) | ||
50 | await setDefaultVideoChannel(servers) | ||
51 | |||
52 | // Server 1 and server 2 follow each other | ||
53 | await doubleFollow(servers[0], servers[1]) | ||
54 | |||
55 | await servers[0].config.updateCustomSubConfig({ | ||
56 | newConfig: { | ||
57 | live: { | ||
58 | enabled: true, | ||
59 | allowReplay: true, | ||
60 | maxDuration: -1, | ||
61 | transcoding: { | ||
62 | enabled: true, | ||
63 | resolutions: ConfigCommand.getCustomConfigResolutions(true) | ||
64 | } | ||
65 | } | ||
66 | } | ||
67 | }) | ||
68 | }) | ||
69 | |||
70 | it('Should create a non permanent live and update it to be a permanent live', async function () { | ||
71 | this.timeout(20000) | ||
72 | |||
73 | const videoUUID = await createLiveWrapper(false) | ||
74 | |||
75 | { | ||
76 | const live = await servers[0].live.get({ videoId: videoUUID }) | ||
77 | expect(live.permanentLive).to.be.false | ||
78 | } | ||
79 | |||
80 | await servers[0].live.update({ videoId: videoUUID, fields: { permanentLive: true } }) | ||
81 | |||
82 | { | ||
83 | const live = await servers[0].live.get({ videoId: videoUUID }) | ||
84 | expect(live.permanentLive).to.be.true | ||
85 | } | ||
86 | }) | ||
87 | |||
88 | it('Should create a permanent live', async function () { | ||
89 | this.timeout(20000) | ||
90 | |||
91 | videoUUID = await createLiveWrapper(true) | ||
92 | |||
93 | const live = await servers[0].live.get({ videoId: videoUUID }) | ||
94 | expect(live.permanentLive).to.be.true | ||
95 | |||
96 | await waitJobs(servers) | ||
97 | }) | ||
98 | |||
99 | it('Should stream into this permanent live', async function () { | ||
100 | this.timeout(240_000) | ||
101 | |||
102 | const beforePublication = new Date() | ||
103 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUID }) | ||
104 | |||
105 | for (const server of servers) { | ||
106 | await server.live.waitUntilPublished({ videoId: videoUUID }) | ||
107 | } | ||
108 | |||
109 | await checkVideoState(videoUUID, VideoState.PUBLISHED) | ||
110 | |||
111 | for (const server of servers) { | ||
112 | const video = await server.videos.get({ id: videoUUID }) | ||
113 | expect(new Date(video.publishedAt)).greaterThan(beforePublication) | ||
114 | } | ||
115 | |||
116 | await stopFfmpeg(ffmpegCommand) | ||
117 | await servers[0].live.waitUntilWaiting({ videoId: videoUUID }) | ||
118 | |||
119 | await waitJobs(servers) | ||
120 | }) | ||
121 | |||
122 | it('Should have cleaned up this live', async function () { | ||
123 | this.timeout(40000) | ||
124 | |||
125 | await wait(5000) | ||
126 | await waitJobs(servers) | ||
127 | |||
128 | for (const server of servers) { | ||
129 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
130 | |||
131 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(0) | ||
132 | } | ||
133 | |||
134 | await checkLiveCleanup({ server: servers[0], permanent: true, videoUUID }) | ||
135 | }) | ||
136 | |||
137 | it('Should have set this live to waiting for live state', async function () { | ||
138 | this.timeout(20000) | ||
139 | |||
140 | await checkVideoState(videoUUID, VideoState.WAITING_FOR_LIVE) | ||
141 | }) | ||
142 | |||
143 | it('Should be able to stream again in the permanent live', async function () { | ||
144 | this.timeout(60000) | ||
145 | |||
146 | await servers[0].config.updateCustomSubConfig({ | ||
147 | newConfig: { | ||
148 | live: { | ||
149 | enabled: true, | ||
150 | allowReplay: true, | ||
151 | maxDuration: -1, | ||
152 | transcoding: { | ||
153 | enabled: true, | ||
154 | resolutions: ConfigCommand.getCustomConfigResolutions(false) | ||
155 | } | ||
156 | } | ||
157 | } | ||
158 | }) | ||
159 | |||
160 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUID }) | ||
161 | |||
162 | for (const server of servers) { | ||
163 | await server.live.waitUntilPublished({ videoId: videoUUID }) | ||
164 | } | ||
165 | |||
166 | await checkVideoState(videoUUID, VideoState.PUBLISHED) | ||
167 | |||
168 | const count = await servers[0].live.countPlaylists({ videoUUID }) | ||
169 | // master playlist and 720p playlist | ||
170 | expect(count).to.equal(2) | ||
171 | |||
172 | await stopFfmpeg(ffmpegCommand) | ||
173 | }) | ||
174 | |||
175 | it('Should have appropriate sessions', async function () { | ||
176 | this.timeout(60000) | ||
177 | |||
178 | await servers[0].live.waitUntilWaiting({ videoId: videoUUID }) | ||
179 | |||
180 | const { data, total } = await servers[0].live.listSessions({ videoId: videoUUID }) | ||
181 | expect(total).to.equal(2) | ||
182 | expect(data).to.have.lengthOf(2) | ||
183 | |||
184 | for (const session of data) { | ||
185 | expect(session.startDate).to.exist | ||
186 | expect(session.endDate).to.exist | ||
187 | |||
188 | expect(session.error).to.not.exist | ||
189 | } | ||
190 | }) | ||
191 | |||
192 | it('Should remove the live and have cleaned up the directory', async function () { | ||
193 | this.timeout(60000) | ||
194 | |||
195 | await servers[0].videos.remove({ id: videoUUID }) | ||
196 | await waitJobs(servers) | ||
197 | |||
198 | await checkLiveCleanup({ server: servers[0], permanent: true, videoUUID }) | ||
199 | }) | ||
200 | |||
201 | after(async function () { | ||
202 | await cleanupTests(servers) | ||
203 | }) | ||
204 | }) | ||
diff --git a/packages/tests/src/api/live/live-rtmps.ts b/packages/tests/src/api/live/live-rtmps.ts new file mode 100644 index 000000000..4ab59ed4c --- /dev/null +++ b/packages/tests/src/api/live/live-rtmps.ts | |||
@@ -0,0 +1,143 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
5 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | PeerTubeServer, | ||
10 | sendRTMPStream, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | stopFfmpeg, | ||
14 | testFfmpegStreamError, | ||
15 | waitUntilLivePublishedOnAllServers | ||
16 | } from '@peertube/peertube-server-commands' | ||
17 | |||
18 | describe('Test live RTMPS', function () { | ||
19 | let server: PeerTubeServer | ||
20 | let rtmpUrl: string | ||
21 | let rtmpsUrl: string | ||
22 | |||
23 | async function createLiveWrapper () { | ||
24 | const liveAttributes = { | ||
25 | name: 'live', | ||
26 | channelId: server.store.channel.id, | ||
27 | privacy: VideoPrivacy.PUBLIC, | ||
28 | saveReplay: false | ||
29 | } | ||
30 | |||
31 | const { uuid } = await server.live.create({ fields: liveAttributes }) | ||
32 | |||
33 | const live = await server.live.get({ videoId: uuid }) | ||
34 | const video = await server.videos.get({ id: uuid }) | ||
35 | |||
36 | return Object.assign(video, live) | ||
37 | } | ||
38 | |||
39 | before(async function () { | ||
40 | this.timeout(120000) | ||
41 | |||
42 | server = await createSingleServer(1) | ||
43 | |||
44 | // Get the access tokens | ||
45 | await setAccessTokensToServers([ server ]) | ||
46 | await setDefaultVideoChannel([ server ]) | ||
47 | |||
48 | await server.config.updateCustomSubConfig({ | ||
49 | newConfig: { | ||
50 | live: { | ||
51 | enabled: true, | ||
52 | allowReplay: true, | ||
53 | transcoding: { | ||
54 | enabled: false | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | }) | ||
59 | |||
60 | rtmpUrl = 'rtmp://' + server.hostname + ':' + server.rtmpPort + '/live' | ||
61 | rtmpsUrl = 'rtmps://' + server.hostname + ':' + server.rtmpsPort + '/live' | ||
62 | }) | ||
63 | |||
64 | it('Should enable RTMPS endpoint only', async function () { | ||
65 | this.timeout(240000) | ||
66 | |||
67 | await server.kill() | ||
68 | await server.run({ | ||
69 | live: { | ||
70 | rtmp: { | ||
71 | enabled: false | ||
72 | }, | ||
73 | rtmps: { | ||
74 | enabled: true, | ||
75 | port: server.rtmpsPort, | ||
76 | key_file: buildAbsoluteFixturePath('rtmps.key'), | ||
77 | cert_file: buildAbsoluteFixturePath('rtmps.cert') | ||
78 | } | ||
79 | } | ||
80 | }) | ||
81 | |||
82 | { | ||
83 | const liveVideo = await createLiveWrapper() | ||
84 | |||
85 | expect(liveVideo.rtmpUrl).to.not.exist | ||
86 | expect(liveVideo.rtmpsUrl).to.equal(rtmpsUrl) | ||
87 | |||
88 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl, streamKey: liveVideo.streamKey }) | ||
89 | await testFfmpegStreamError(command, true) | ||
90 | } | ||
91 | |||
92 | { | ||
93 | const liveVideo = await createLiveWrapper() | ||
94 | |||
95 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpsUrl, streamKey: liveVideo.streamKey }) | ||
96 | await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid) | ||
97 | await stopFfmpeg(command) | ||
98 | } | ||
99 | }) | ||
100 | |||
101 | it('Should enable both RTMP and RTMPS', async function () { | ||
102 | this.timeout(240000) | ||
103 | |||
104 | await server.kill() | ||
105 | await server.run({ | ||
106 | live: { | ||
107 | rtmp: { | ||
108 | enabled: true, | ||
109 | port: server.rtmpPort | ||
110 | }, | ||
111 | rtmps: { | ||
112 | enabled: true, | ||
113 | port: server.rtmpsPort, | ||
114 | key_file: buildAbsoluteFixturePath('rtmps.key'), | ||
115 | cert_file: buildAbsoluteFixturePath('rtmps.cert') | ||
116 | } | ||
117 | } | ||
118 | }) | ||
119 | |||
120 | { | ||
121 | const liveVideo = await createLiveWrapper() | ||
122 | |||
123 | expect(liveVideo.rtmpUrl).to.equal(rtmpUrl) | ||
124 | expect(liveVideo.rtmpsUrl).to.equal(rtmpsUrl) | ||
125 | |||
126 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl, streamKey: liveVideo.streamKey }) | ||
127 | await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid) | ||
128 | await stopFfmpeg(command) | ||
129 | } | ||
130 | |||
131 | { | ||
132 | const liveVideo = await createLiveWrapper() | ||
133 | |||
134 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpsUrl, streamKey: liveVideo.streamKey }) | ||
135 | await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid) | ||
136 | await stopFfmpeg(command) | ||
137 | } | ||
138 | }) | ||
139 | |||
140 | after(async function () { | ||
141 | await cleanupTests([ server ]) | ||
142 | }) | ||
143 | }) | ||
diff --git a/packages/tests/src/api/live/live-save-replay.ts b/packages/tests/src/api/live/live-save-replay.ts new file mode 100644 index 000000000..84135365b --- /dev/null +++ b/packages/tests/src/api/live/live-save-replay.ts | |||
@@ -0,0 +1,583 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { | ||
7 | HttpStatusCode, | ||
8 | HttpStatusCodeType, | ||
9 | LiveVideoCreate, | ||
10 | LiveVideoError, | ||
11 | VideoPrivacy, | ||
12 | VideoPrivacyType, | ||
13 | VideoState, | ||
14 | VideoStateType | ||
15 | } from '@peertube/peertube-models' | ||
16 | import { checkLiveCleanup } from '@tests/shared/live.js' | ||
17 | import { | ||
18 | cleanupTests, | ||
19 | ConfigCommand, | ||
20 | createMultipleServers, | ||
21 | doubleFollow, | ||
22 | findExternalSavedVideo, | ||
23 | PeerTubeServer, | ||
24 | setAccessTokensToServers, | ||
25 | setDefaultVideoChannel, | ||
26 | stopFfmpeg, | ||
27 | testFfmpegStreamError, | ||
28 | waitJobs, | ||
29 | waitUntilLivePublishedOnAllServers, | ||
30 | waitUntilLiveReplacedByReplayOnAllServers, | ||
31 | waitUntilLiveWaitingOnAllServers | ||
32 | } from '@peertube/peertube-server-commands' | ||
33 | |||
34 | describe('Save replay setting', function () { | ||
35 | let servers: PeerTubeServer[] = [] | ||
36 | let liveVideoUUID: string | ||
37 | let ffmpegCommand: FfmpegCommand | ||
38 | |||
39 | async function createLiveWrapper (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacyType } }) { | ||
40 | if (liveVideoUUID) { | ||
41 | try { | ||
42 | await servers[0].videos.remove({ id: liveVideoUUID }) | ||
43 | await waitJobs(servers) | ||
44 | } catch {} | ||
45 | } | ||
46 | |||
47 | const attributes: LiveVideoCreate = { | ||
48 | channelId: servers[0].store.channel.id, | ||
49 | privacy: VideoPrivacy.PUBLIC, | ||
50 | name: 'live'.repeat(30), | ||
51 | saveReplay: options.replay, | ||
52 | replaySettings: options.replaySettings, | ||
53 | permanentLive: options.permanent | ||
54 | } | ||
55 | |||
56 | const { uuid } = await servers[0].live.create({ fields: attributes }) | ||
57 | return uuid | ||
58 | } | ||
59 | |||
60 | async function publishLive (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacyType } }) { | ||
61 | liveVideoUUID = await createLiveWrapper(options) | ||
62 | |||
63 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
64 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
65 | |||
66 | const liveDetails = await servers[0].videos.get({ id: liveVideoUUID }) | ||
67 | |||
68 | await waitJobs(servers) | ||
69 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) | ||
70 | |||
71 | return { ffmpegCommand, liveDetails } | ||
72 | } | ||
73 | |||
74 | async function publishLiveAndDelete (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacyType } }) { | ||
75 | const { ffmpegCommand, liveDetails } = await publishLive(options) | ||
76 | |||
77 | await Promise.all([ | ||
78 | servers[0].videos.remove({ id: liveVideoUUID }), | ||
79 | testFfmpegStreamError(ffmpegCommand, true) | ||
80 | ]) | ||
81 | |||
82 | await waitJobs(servers) | ||
83 | await wait(5000) | ||
84 | await waitJobs(servers) | ||
85 | |||
86 | return { liveDetails } | ||
87 | } | ||
88 | |||
89 | async function publishLiveAndBlacklist (options: { | ||
90 | permanent: boolean | ||
91 | replay: boolean | ||
92 | replaySettings?: { privacy: VideoPrivacyType } | ||
93 | }) { | ||
94 | const { ffmpegCommand, liveDetails } = await publishLive(options) | ||
95 | |||
96 | await Promise.all([ | ||
97 | servers[0].blacklist.add({ videoId: liveVideoUUID, reason: 'bad live', unfederate: true }), | ||
98 | testFfmpegStreamError(ffmpegCommand, true) | ||
99 | ]) | ||
100 | |||
101 | await waitJobs(servers) | ||
102 | await wait(5000) | ||
103 | await waitJobs(servers) | ||
104 | |||
105 | return { liveDetails } | ||
106 | } | ||
107 | |||
108 | async function checkVideosExist (videoId: string, existsInList: boolean, expectedStatus?: HttpStatusCodeType) { | ||
109 | for (const server of servers) { | ||
110 | const length = existsInList ? 1 : 0 | ||
111 | |||
112 | const { data, total } = await server.videos.list() | ||
113 | expect(data).to.have.lengthOf(length) | ||
114 | expect(total).to.equal(length) | ||
115 | |||
116 | if (expectedStatus) { | ||
117 | await server.videos.get({ id: videoId, expectedStatus }) | ||
118 | } | ||
119 | } | ||
120 | } | ||
121 | |||
122 | async function checkVideoState (videoId: string, state: VideoStateType) { | ||
123 | for (const server of servers) { | ||
124 | const video = await server.videos.get({ id: videoId }) | ||
125 | expect(video.state.id).to.equal(state) | ||
126 | } | ||
127 | } | ||
128 | |||
129 | async function checkVideoPrivacy (videoId: string, privacy: VideoPrivacyType) { | ||
130 | for (const server of servers) { | ||
131 | const video = await server.videos.get({ id: videoId }) | ||
132 | expect(video.privacy.id).to.equal(privacy) | ||
133 | } | ||
134 | } | ||
135 | |||
136 | before(async function () { | ||
137 | this.timeout(120000) | ||
138 | |||
139 | servers = await createMultipleServers(2) | ||
140 | |||
141 | // Get the access tokens | ||
142 | await setAccessTokensToServers(servers) | ||
143 | await setDefaultVideoChannel(servers) | ||
144 | |||
145 | // Server 1 and server 2 follow each other | ||
146 | await doubleFollow(servers[0], servers[1]) | ||
147 | |||
148 | await servers[0].config.updateCustomSubConfig({ | ||
149 | newConfig: { | ||
150 | live: { | ||
151 | enabled: true, | ||
152 | allowReplay: true, | ||
153 | maxDuration: -1, | ||
154 | transcoding: { | ||
155 | enabled: false, | ||
156 | resolutions: ConfigCommand.getCustomConfigResolutions(true) | ||
157 | } | ||
158 | } | ||
159 | } | ||
160 | }) | ||
161 | }) | ||
162 | |||
163 | describe('With save replay disabled', function () { | ||
164 | let sessionStartDateMin: Date | ||
165 | let sessionStartDateMax: Date | ||
166 | let sessionEndDateMin: Date | ||
167 | |||
168 | it('Should correctly create and federate the "waiting for stream" live', async function () { | ||
169 | this.timeout(40000) | ||
170 | |||
171 | liveVideoUUID = await createLiveWrapper({ permanent: false, replay: false }) | ||
172 | |||
173 | await waitJobs(servers) | ||
174 | |||
175 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) | ||
176 | await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) | ||
177 | }) | ||
178 | |||
179 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { | ||
180 | this.timeout(120000) | ||
181 | |||
182 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
183 | |||
184 | sessionStartDateMin = new Date() | ||
185 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
186 | sessionStartDateMax = new Date() | ||
187 | |||
188 | await waitJobs(servers) | ||
189 | |||
190 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) | ||
191 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | ||
192 | }) | ||
193 | |||
194 | it('Should correctly delete the video files after the stream ended', async function () { | ||
195 | this.timeout(120000) | ||
196 | |||
197 | sessionEndDateMin = new Date() | ||
198 | await stopFfmpeg(ffmpegCommand) | ||
199 | |||
200 | for (const server of servers) { | ||
201 | await server.live.waitUntilEnded({ videoId: liveVideoUUID }) | ||
202 | } | ||
203 | await waitJobs(servers) | ||
204 | |||
205 | // Live still exist, but cannot be played anymore | ||
206 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) | ||
207 | await checkVideoState(liveVideoUUID, VideoState.LIVE_ENDED) | ||
208 | |||
209 | // No resolutions saved since we did not save replay | ||
210 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
211 | }) | ||
212 | |||
213 | it('Should have appropriate ended session', async function () { | ||
214 | const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID }) | ||
215 | expect(total).to.equal(1) | ||
216 | expect(data).to.have.lengthOf(1) | ||
217 | |||
218 | const session = data[0] | ||
219 | |||
220 | const startDate = new Date(session.startDate) | ||
221 | expect(startDate).to.be.above(sessionStartDateMin) | ||
222 | expect(startDate).to.be.below(sessionStartDateMax) | ||
223 | |||
224 | expect(session.endDate).to.exist | ||
225 | expect(new Date(session.endDate)).to.be.above(sessionEndDateMin) | ||
226 | |||
227 | expect(session.saveReplay).to.be.false | ||
228 | expect(session.error).to.not.exist | ||
229 | expect(session.replayVideo).to.not.exist | ||
230 | }) | ||
231 | |||
232 | it('Should correctly terminate the stream on blacklist and delete the live', async function () { | ||
233 | this.timeout(120000) | ||
234 | |||
235 | await publishLiveAndBlacklist({ permanent: false, replay: false }) | ||
236 | |||
237 | await checkVideosExist(liveVideoUUID, false) | ||
238 | |||
239 | await servers[0].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
240 | await servers[1].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
241 | |||
242 | await wait(5000) | ||
243 | await waitJobs(servers) | ||
244 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
245 | }) | ||
246 | |||
247 | it('Should have blacklisted session error', async function () { | ||
248 | const session = await servers[0].live.findLatestSession({ videoId: liveVideoUUID }) | ||
249 | expect(session.startDate).to.exist | ||
250 | expect(session.endDate).to.exist | ||
251 | |||
252 | expect(session.error).to.equal(LiveVideoError.BLACKLISTED) | ||
253 | expect(session.replayVideo).to.not.exist | ||
254 | }) | ||
255 | |||
256 | it('Should correctly terminate the stream on delete and delete the video', async function () { | ||
257 | this.timeout(120000) | ||
258 | |||
259 | await publishLiveAndDelete({ permanent: false, replay: false }) | ||
260 | |||
261 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) | ||
262 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
263 | }) | ||
264 | }) | ||
265 | |||
266 | describe('With save replay enabled on non permanent live', function () { | ||
267 | |||
268 | it('Should correctly create and federate the "waiting for stream" live', async function () { | ||
269 | this.timeout(120000) | ||
270 | |||
271 | liveVideoUUID = await createLiveWrapper({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } }) | ||
272 | |||
273 | await waitJobs(servers) | ||
274 | |||
275 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) | ||
276 | await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) | ||
277 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) | ||
278 | }) | ||
279 | |||
280 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { | ||
281 | this.timeout(120000) | ||
282 | |||
283 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
284 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
285 | |||
286 | await waitJobs(servers) | ||
287 | |||
288 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) | ||
289 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | ||
290 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) | ||
291 | }) | ||
292 | |||
293 | it('Should correctly have saved the live and federated it after the streaming', async function () { | ||
294 | this.timeout(120000) | ||
295 | |||
296 | const session = await servers[0].live.findLatestSession({ videoId: liveVideoUUID }) | ||
297 | expect(session.endDate).to.not.exist | ||
298 | expect(session.endingProcessed).to.be.false | ||
299 | expect(session.saveReplay).to.be.true | ||
300 | expect(session.replaySettings).to.exist | ||
301 | expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED) | ||
302 | |||
303 | await stopFfmpeg(ffmpegCommand) | ||
304 | |||
305 | await waitUntilLiveReplacedByReplayOnAllServers(servers, liveVideoUUID) | ||
306 | await waitJobs(servers) | ||
307 | |||
308 | // Live has been transcoded | ||
309 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) | ||
310 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | ||
311 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.UNLISTED) | ||
312 | }) | ||
313 | |||
314 | it('Should find the replay live session', async function () { | ||
315 | const session = await servers[0].live.getReplaySession({ videoId: liveVideoUUID }) | ||
316 | |||
317 | expect(session).to.exist | ||
318 | |||
319 | expect(session.startDate).to.exist | ||
320 | expect(session.endDate).to.exist | ||
321 | |||
322 | expect(session.error).to.not.exist | ||
323 | expect(session.saveReplay).to.be.true | ||
324 | expect(session.endingProcessed).to.be.true | ||
325 | expect(session.replaySettings).to.exist | ||
326 | expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED) | ||
327 | |||
328 | expect(session.replayVideo).to.exist | ||
329 | expect(session.replayVideo.id).to.exist | ||
330 | expect(session.replayVideo.shortUUID).to.exist | ||
331 | expect(session.replayVideo.uuid).to.equal(liveVideoUUID) | ||
332 | }) | ||
333 | |||
334 | it('Should update the saved live and correctly federate the updated attributes', async function () { | ||
335 | this.timeout(120000) | ||
336 | |||
337 | await servers[0].videos.update({ id: liveVideoUUID, attributes: { name: 'video updated', privacy: VideoPrivacy.PUBLIC } }) | ||
338 | await waitJobs(servers) | ||
339 | |||
340 | for (const server of servers) { | ||
341 | const video = await server.videos.get({ id: liveVideoUUID }) | ||
342 | expect(video.name).to.equal('video updated') | ||
343 | expect(video.isLive).to.be.false | ||
344 | expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC) | ||
345 | } | ||
346 | }) | ||
347 | |||
348 | it('Should have cleaned up the live files', async function () { | ||
349 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false, savedResolutions: [ 720 ] }) | ||
350 | }) | ||
351 | |||
352 | it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { | ||
353 | this.timeout(120000) | ||
354 | |||
355 | await publishLiveAndBlacklist({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }) | ||
356 | |||
357 | await checkVideosExist(liveVideoUUID, false) | ||
358 | |||
359 | await servers[0].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
360 | await servers[1].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
361 | |||
362 | await wait(5000) | ||
363 | await waitJobs(servers) | ||
364 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false, savedResolutions: [ 720 ] }) | ||
365 | }) | ||
366 | |||
367 | it('Should correctly terminate the stream on delete and delete the video', async function () { | ||
368 | this.timeout(120000) | ||
369 | |||
370 | await publishLiveAndDelete({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }) | ||
371 | |||
372 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) | ||
373 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
374 | }) | ||
375 | }) | ||
376 | |||
377 | describe('With save replay enabled on permanent live', function () { | ||
378 | let lastReplayUUID: string | ||
379 | |||
380 | describe('With a first live and its replay', function () { | ||
381 | |||
382 | it('Should correctly create and federate the "waiting for stream" live', async function () { | ||
383 | this.timeout(120000) | ||
384 | |||
385 | liveVideoUUID = await createLiveWrapper({ permanent: true, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } }) | ||
386 | |||
387 | await waitJobs(servers) | ||
388 | |||
389 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) | ||
390 | await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) | ||
391 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) | ||
392 | }) | ||
393 | |||
394 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { | ||
395 | this.timeout(120000) | ||
396 | |||
397 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
398 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
399 | |||
400 | await waitJobs(servers) | ||
401 | |||
402 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) | ||
403 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | ||
404 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) | ||
405 | }) | ||
406 | |||
407 | it('Should correctly have saved the live and federated it after the streaming', async function () { | ||
408 | this.timeout(120000) | ||
409 | |||
410 | const liveDetails = await servers[0].videos.get({ id: liveVideoUUID }) | ||
411 | |||
412 | await stopFfmpeg(ffmpegCommand) | ||
413 | |||
414 | await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID) | ||
415 | await waitJobs(servers) | ||
416 | |||
417 | const video = await findExternalSavedVideo(servers[0], liveDetails) | ||
418 | expect(video).to.exist | ||
419 | |||
420 | for (const server of servers) { | ||
421 | await server.videos.get({ id: video.uuid }) | ||
422 | } | ||
423 | |||
424 | lastReplayUUID = video.uuid | ||
425 | }) | ||
426 | |||
427 | it('Should have appropriate ended session and replay live session', async function () { | ||
428 | const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID }) | ||
429 | expect(total).to.equal(1) | ||
430 | expect(data).to.have.lengthOf(1) | ||
431 | |||
432 | const sessionFromLive = data[0] | ||
433 | const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID }) | ||
434 | |||
435 | for (const session of [ sessionFromLive, sessionFromReplay ]) { | ||
436 | expect(session.startDate).to.exist | ||
437 | expect(session.endDate).to.exist | ||
438 | |||
439 | expect(session.replaySettings).to.exist | ||
440 | expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED) | ||
441 | |||
442 | expect(session.error).to.not.exist | ||
443 | |||
444 | expect(session.replayVideo).to.exist | ||
445 | expect(session.replayVideo.id).to.exist | ||
446 | expect(session.replayVideo.shortUUID).to.exist | ||
447 | expect(session.replayVideo.uuid).to.equal(lastReplayUUID) | ||
448 | } | ||
449 | }) | ||
450 | |||
451 | it('Should have the first live replay with correct settings', async function () { | ||
452 | await checkVideosExist(lastReplayUUID, false, HttpStatusCode.OK_200) | ||
453 | await checkVideoState(lastReplayUUID, VideoState.PUBLISHED) | ||
454 | await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.UNLISTED) | ||
455 | }) | ||
456 | }) | ||
457 | |||
458 | describe('With a second live and its replay', function () { | ||
459 | |||
460 | it('Should update the replay settings', async function () { | ||
461 | await servers[0].live.update({ videoId: liveVideoUUID, fields: { replaySettings: { privacy: VideoPrivacy.PUBLIC } } }) | ||
462 | await waitJobs(servers) | ||
463 | |||
464 | const live = await servers[0].live.get({ videoId: liveVideoUUID }) | ||
465 | |||
466 | expect(live.saveReplay).to.be.true | ||
467 | expect(live.replaySettings).to.exist | ||
468 | expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC) | ||
469 | |||
470 | }) | ||
471 | |||
472 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { | ||
473 | this.timeout(120000) | ||
474 | |||
475 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
476 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
477 | |||
478 | await waitJobs(servers) | ||
479 | |||
480 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) | ||
481 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | ||
482 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) | ||
483 | }) | ||
484 | |||
485 | it('Should correctly have saved the live and federated it after the streaming', async function () { | ||
486 | this.timeout(120000) | ||
487 | |||
488 | const liveDetails = await servers[0].videos.get({ id: liveVideoUUID }) | ||
489 | |||
490 | await stopFfmpeg(ffmpegCommand) | ||
491 | |||
492 | await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID) | ||
493 | await waitJobs(servers) | ||
494 | |||
495 | const video = await findExternalSavedVideo(servers[0], liveDetails) | ||
496 | expect(video).to.exist | ||
497 | |||
498 | for (const server of servers) { | ||
499 | await server.videos.get({ id: video.uuid }) | ||
500 | } | ||
501 | |||
502 | lastReplayUUID = video.uuid | ||
503 | }) | ||
504 | |||
505 | it('Should have appropriate ended session and replay live session', async function () { | ||
506 | const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID }) | ||
507 | expect(total).to.equal(2) | ||
508 | expect(data).to.have.lengthOf(2) | ||
509 | |||
510 | const sessionFromLive = data[1] | ||
511 | const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID }) | ||
512 | |||
513 | for (const session of [ sessionFromLive, sessionFromReplay ]) { | ||
514 | expect(session.startDate).to.exist | ||
515 | expect(session.endDate).to.exist | ||
516 | |||
517 | expect(session.replaySettings).to.exist | ||
518 | expect(session.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC) | ||
519 | |||
520 | expect(session.error).to.not.exist | ||
521 | |||
522 | expect(session.replayVideo).to.exist | ||
523 | expect(session.replayVideo.id).to.exist | ||
524 | expect(session.replayVideo.shortUUID).to.exist | ||
525 | expect(session.replayVideo.uuid).to.equal(lastReplayUUID) | ||
526 | } | ||
527 | }) | ||
528 | |||
529 | it('Should have the first live replay with correct settings', async function () { | ||
530 | await checkVideosExist(lastReplayUUID, true, HttpStatusCode.OK_200) | ||
531 | await checkVideoState(lastReplayUUID, VideoState.PUBLISHED) | ||
532 | await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.PUBLIC) | ||
533 | }) | ||
534 | |||
535 | it('Should have cleaned up the live files', async function () { | ||
536 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
537 | }) | ||
538 | |||
539 | it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { | ||
540 | this.timeout(120000) | ||
541 | |||
542 | await servers[0].videos.remove({ id: lastReplayUUID }) | ||
543 | const { liveDetails } = await publishLiveAndBlacklist({ | ||
544 | permanent: true, | ||
545 | replay: true, | ||
546 | replaySettings: { privacy: VideoPrivacy.PUBLIC } | ||
547 | }) | ||
548 | |||
549 | const replay = await findExternalSavedVideo(servers[0], liveDetails) | ||
550 | expect(replay).to.exist | ||
551 | |||
552 | for (const videoId of [ liveVideoUUID, replay.uuid ]) { | ||
553 | await checkVideosExist(videoId, false) | ||
554 | |||
555 | await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
556 | await servers[1].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
557 | } | ||
558 | |||
559 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
560 | }) | ||
561 | |||
562 | it('Should correctly terminate the stream on delete and not save the video', async function () { | ||
563 | this.timeout(120000) | ||
564 | |||
565 | const { liveDetails } = await publishLiveAndDelete({ | ||
566 | permanent: true, | ||
567 | replay: true, | ||
568 | replaySettings: { privacy: VideoPrivacy.PUBLIC } | ||
569 | }) | ||
570 | |||
571 | const replay = await findExternalSavedVideo(servers[0], liveDetails) | ||
572 | expect(replay).to.not.exist | ||
573 | |||
574 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) | ||
575 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
576 | }) | ||
577 | }) | ||
578 | }) | ||
579 | |||
580 | after(async function () { | ||
581 | await cleanupTests(servers) | ||
582 | }) | ||
583 | }) | ||
diff --git a/packages/tests/src/api/live/live-socket-messages.ts b/packages/tests/src/api/live/live-socket-messages.ts new file mode 100644 index 000000000..80bae154c --- /dev/null +++ b/packages/tests/src/api/live/live-socket-messages.ts | |||
@@ -0,0 +1,186 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { LiveVideoEventPayload, VideoPrivacy, VideoState, VideoStateType } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | stopFfmpeg, | ||
14 | waitJobs, | ||
15 | waitUntilLivePublishedOnAllServers | ||
16 | } from '@peertube/peertube-server-commands' | ||
17 | |||
18 | describe('Test live socket messages', function () { | ||
19 | let servers: PeerTubeServer[] = [] | ||
20 | |||
21 | before(async function () { | ||
22 | this.timeout(120000) | ||
23 | |||
24 | servers = await createMultipleServers(2) | ||
25 | |||
26 | // Get the access tokens | ||
27 | await setAccessTokensToServers(servers) | ||
28 | await setDefaultVideoChannel(servers) | ||
29 | |||
30 | await servers[0].config.updateCustomSubConfig({ | ||
31 | newConfig: { | ||
32 | live: { | ||
33 | enabled: true, | ||
34 | allowReplay: true, | ||
35 | transcoding: { | ||
36 | enabled: false | ||
37 | } | ||
38 | } | ||
39 | } | ||
40 | }) | ||
41 | |||
42 | // Server 1 and server 2 follow each other | ||
43 | await doubleFollow(servers[0], servers[1]) | ||
44 | }) | ||
45 | |||
46 | describe('Live socket messages', function () { | ||
47 | |||
48 | async function createLiveWrapper () { | ||
49 | const liveAttributes = { | ||
50 | name: 'live video', | ||
51 | channelId: servers[0].store.channel.id, | ||
52 | privacy: VideoPrivacy.PUBLIC | ||
53 | } | ||
54 | |||
55 | const { uuid } = await servers[0].live.create({ fields: liveAttributes }) | ||
56 | return uuid | ||
57 | } | ||
58 | |||
59 | it('Should correctly send a message when the live starts and ends', async function () { | ||
60 | this.timeout(60000) | ||
61 | |||
62 | const localStateChanges: VideoStateType[] = [] | ||
63 | const remoteStateChanges: VideoStateType[] = [] | ||
64 | |||
65 | const liveVideoUUID = await createLiveWrapper() | ||
66 | await waitJobs(servers) | ||
67 | |||
68 | { | ||
69 | const videoId = await servers[0].videos.getId({ uuid: liveVideoUUID }) | ||
70 | |||
71 | const localSocket = servers[0].socketIO.getLiveNotificationSocket() | ||
72 | localSocket.on('state-change', data => localStateChanges.push(data.state)) | ||
73 | localSocket.emit('subscribe', { videoId }) | ||
74 | } | ||
75 | |||
76 | { | ||
77 | const videoId = await servers[1].videos.getId({ uuid: liveVideoUUID }) | ||
78 | |||
79 | const remoteSocket = servers[1].socketIO.getLiveNotificationSocket() | ||
80 | remoteSocket.on('state-change', data => remoteStateChanges.push(data.state)) | ||
81 | remoteSocket.emit('subscribe', { videoId }) | ||
82 | } | ||
83 | |||
84 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
85 | |||
86 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
87 | await waitJobs(servers) | ||
88 | |||
89 | for (const stateChanges of [ localStateChanges, remoteStateChanges ]) { | ||
90 | expect(stateChanges).to.have.length.at.least(1) | ||
91 | expect(stateChanges[stateChanges.length - 1]).to.equal(VideoState.PUBLISHED) | ||
92 | } | ||
93 | |||
94 | await stopFfmpeg(ffmpegCommand) | ||
95 | |||
96 | for (const server of servers) { | ||
97 | await server.live.waitUntilEnded({ videoId: liveVideoUUID }) | ||
98 | } | ||
99 | await waitJobs(servers) | ||
100 | |||
101 | for (const stateChanges of [ localStateChanges, remoteStateChanges ]) { | ||
102 | expect(stateChanges).to.have.length.at.least(2) | ||
103 | expect(stateChanges[stateChanges.length - 1]).to.equal(VideoState.LIVE_ENDED) | ||
104 | } | ||
105 | }) | ||
106 | |||
107 | it('Should correctly send views change notification', async function () { | ||
108 | this.timeout(60000) | ||
109 | |||
110 | let localLastVideoViews = 0 | ||
111 | let remoteLastVideoViews = 0 | ||
112 | |||
113 | const liveVideoUUID = await createLiveWrapper() | ||
114 | await waitJobs(servers) | ||
115 | |||
116 | { | ||
117 | const videoId = await servers[0].videos.getId({ uuid: liveVideoUUID }) | ||
118 | |||
119 | const localSocket = servers[0].socketIO.getLiveNotificationSocket() | ||
120 | localSocket.on('views-change', (data: LiveVideoEventPayload) => { localLastVideoViews = data.viewers }) | ||
121 | localSocket.emit('subscribe', { videoId }) | ||
122 | } | ||
123 | |||
124 | { | ||
125 | const videoId = await servers[1].videos.getId({ uuid: liveVideoUUID }) | ||
126 | |||
127 | const remoteSocket = servers[1].socketIO.getLiveNotificationSocket() | ||
128 | remoteSocket.on('views-change', (data: LiveVideoEventPayload) => { remoteLastVideoViews = data.viewers }) | ||
129 | remoteSocket.emit('subscribe', { videoId }) | ||
130 | } | ||
131 | |||
132 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
133 | |||
134 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
135 | await waitJobs(servers) | ||
136 | |||
137 | expect(localLastVideoViews).to.equal(0) | ||
138 | expect(remoteLastVideoViews).to.equal(0) | ||
139 | |||
140 | await servers[0].views.simulateView({ id: liveVideoUUID }) | ||
141 | await servers[1].views.simulateView({ id: liveVideoUUID }) | ||
142 | |||
143 | await waitJobs(servers) | ||
144 | |||
145 | expect(localLastVideoViews).to.equal(2) | ||
146 | expect(remoteLastVideoViews).to.equal(2) | ||
147 | |||
148 | await stopFfmpeg(ffmpegCommand) | ||
149 | }) | ||
150 | |||
151 | it('Should not receive a notification after unsubscribe', async function () { | ||
152 | this.timeout(120000) | ||
153 | |||
154 | const stateChanges: VideoStateType[] = [] | ||
155 | |||
156 | const liveVideoUUID = await createLiveWrapper() | ||
157 | await waitJobs(servers) | ||
158 | |||
159 | const videoId = await servers[0].videos.getId({ uuid: liveVideoUUID }) | ||
160 | |||
161 | const socket = servers[0].socketIO.getLiveNotificationSocket() | ||
162 | socket.on('state-change', data => stateChanges.push(data.state)) | ||
163 | socket.emit('subscribe', { videoId }) | ||
164 | |||
165 | const command = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
166 | |||
167 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
168 | await waitJobs(servers) | ||
169 | |||
170 | // Notifier waits before sending a notification | ||
171 | await wait(10000) | ||
172 | |||
173 | expect(stateChanges).to.have.lengthOf(1) | ||
174 | socket.emit('unsubscribe', { videoId }) | ||
175 | |||
176 | await stopFfmpeg(command) | ||
177 | await waitJobs(servers) | ||
178 | |||
179 | expect(stateChanges).to.have.lengthOf(1) | ||
180 | }) | ||
181 | }) | ||
182 | |||
183 | after(async function () { | ||
184 | await cleanupTests(servers) | ||
185 | }) | ||
186 | }) | ||
diff --git a/packages/tests/src/api/live/live.ts b/packages/tests/src/api/live/live.ts new file mode 100644 index 000000000..20804f889 --- /dev/null +++ b/packages/tests/src/api/live/live.ts | |||
@@ -0,0 +1,766 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { basename, join } from 'path' | ||
5 | import { getAllFiles, wait } from '@peertube/peertube-core-utils' | ||
6 | import { ffprobePromise, getVideoStream } from '@peertube/peertube-ffmpeg' | ||
7 | import { | ||
8 | HttpStatusCode, | ||
9 | LiveVideo, | ||
10 | LiveVideoCreate, | ||
11 | LiveVideoLatencyMode, | ||
12 | VideoDetails, | ||
13 | VideoPrivacy, | ||
14 | VideoState, | ||
15 | VideoStreamingPlaylistType | ||
16 | } from '@peertube/peertube-models' | ||
17 | import { | ||
18 | cleanupTests, | ||
19 | createMultipleServers, | ||
20 | doubleFollow, | ||
21 | killallServers, | ||
22 | LiveCommand, | ||
23 | makeGetRequest, | ||
24 | makeRawRequest, | ||
25 | PeerTubeServer, | ||
26 | sendRTMPStream, | ||
27 | setAccessTokensToServers, | ||
28 | setDefaultVideoChannel, | ||
29 | stopFfmpeg, | ||
30 | testFfmpegStreamError, | ||
31 | waitJobs, | ||
32 | waitUntilLivePublishedOnAllServers | ||
33 | } from '@peertube/peertube-server-commands' | ||
34 | import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js' | ||
35 | import { testLiveVideoResolutions } from '@tests/shared/live.js' | ||
36 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
37 | |||
38 | describe('Test live', function () { | ||
39 | let servers: PeerTubeServer[] = [] | ||
40 | let commands: LiveCommand[] | ||
41 | |||
42 | before(async function () { | ||
43 | this.timeout(120000) | ||
44 | |||
45 | servers = await createMultipleServers(2) | ||
46 | |||
47 | // Get the access tokens | ||
48 | await setAccessTokensToServers(servers) | ||
49 | await setDefaultVideoChannel(servers) | ||
50 | |||
51 | await servers[0].config.updateCustomSubConfig({ | ||
52 | newConfig: { | ||
53 | live: { | ||
54 | enabled: true, | ||
55 | allowReplay: true, | ||
56 | latencySetting: { | ||
57 | enabled: true | ||
58 | }, | ||
59 | transcoding: { | ||
60 | enabled: false | ||
61 | } | ||
62 | } | ||
63 | } | ||
64 | }) | ||
65 | |||
66 | // Server 1 and server 2 follow each other | ||
67 | await doubleFollow(servers[0], servers[1]) | ||
68 | |||
69 | commands = servers.map(s => s.live) | ||
70 | }) | ||
71 | |||
72 | describe('Live creation, update and delete', function () { | ||
73 | let liveVideoUUID: string | ||
74 | |||
75 | it('Should create a live with the appropriate parameters', async function () { | ||
76 | this.timeout(20000) | ||
77 | |||
78 | const attributes: LiveVideoCreate = { | ||
79 | category: 1, | ||
80 | licence: 2, | ||
81 | language: 'fr', | ||
82 | description: 'super live description', | ||
83 | support: 'support field', | ||
84 | channelId: servers[0].store.channel.id, | ||
85 | nsfw: false, | ||
86 | waitTranscoding: false, | ||
87 | name: 'my super live', | ||
88 | tags: [ 'tag1', 'tag2' ], | ||
89 | commentsEnabled: false, | ||
90 | downloadEnabled: false, | ||
91 | saveReplay: true, | ||
92 | replaySettings: { privacy: VideoPrivacy.PUBLIC }, | ||
93 | latencyMode: LiveVideoLatencyMode.SMALL_LATENCY, | ||
94 | privacy: VideoPrivacy.PUBLIC, | ||
95 | previewfile: 'video_short1-preview.webm.jpg', | ||
96 | thumbnailfile: 'video_short1.webm.jpg' | ||
97 | } | ||
98 | |||
99 | const live = await commands[0].create({ fields: attributes }) | ||
100 | liveVideoUUID = live.uuid | ||
101 | |||
102 | await waitJobs(servers) | ||
103 | |||
104 | for (const server of servers) { | ||
105 | const video = await server.videos.get({ id: liveVideoUUID }) | ||
106 | |||
107 | expect(video.category.id).to.equal(1) | ||
108 | expect(video.licence.id).to.equal(2) | ||
109 | expect(video.language.id).to.equal('fr') | ||
110 | expect(video.description).to.equal('super live description') | ||
111 | expect(video.support).to.equal('support field') | ||
112 | |||
113 | expect(video.channel.name).to.equal(servers[0].store.channel.name) | ||
114 | expect(video.channel.host).to.equal(servers[0].store.channel.host) | ||
115 | |||
116 | expect(video.isLive).to.be.true | ||
117 | |||
118 | expect(video.nsfw).to.be.false | ||
119 | expect(video.waitTranscoding).to.be.false | ||
120 | expect(video.name).to.equal('my super live') | ||
121 | expect(video.tags).to.deep.equal([ 'tag1', 'tag2' ]) | ||
122 | expect(video.commentsEnabled).to.be.false | ||
123 | expect(video.downloadEnabled).to.be.false | ||
124 | expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC) | ||
125 | |||
126 | await testImageGeneratedByFFmpeg(server.url, 'video_short1-preview.webm', video.previewPath) | ||
127 | await testImageGeneratedByFFmpeg(server.url, 'video_short1.webm', video.thumbnailPath) | ||
128 | |||
129 | const live = await server.live.get({ videoId: liveVideoUUID }) | ||
130 | |||
131 | if (server.url === servers[0].url) { | ||
132 | expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live') | ||
133 | expect(live.streamKey).to.not.be.empty | ||
134 | |||
135 | expect(live.replaySettings).to.exist | ||
136 | expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC) | ||
137 | } else { | ||
138 | expect(live.rtmpUrl).to.not.exist | ||
139 | expect(live.streamKey).to.not.exist | ||
140 | } | ||
141 | |||
142 | expect(live.saveReplay).to.be.true | ||
143 | expect(live.latencyMode).to.equal(LiveVideoLatencyMode.SMALL_LATENCY) | ||
144 | } | ||
145 | }) | ||
146 | |||
147 | it('Should have a default preview and thumbnail', async function () { | ||
148 | this.timeout(20000) | ||
149 | |||
150 | const attributes: LiveVideoCreate = { | ||
151 | name: 'default live thumbnail', | ||
152 | channelId: servers[0].store.channel.id, | ||
153 | privacy: VideoPrivacy.UNLISTED, | ||
154 | nsfw: true | ||
155 | } | ||
156 | |||
157 | const live = await commands[0].create({ fields: attributes }) | ||
158 | const videoId = live.uuid | ||
159 | |||
160 | await waitJobs(servers) | ||
161 | |||
162 | for (const server of servers) { | ||
163 | const video = await server.videos.get({ id: videoId }) | ||
164 | expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED) | ||
165 | expect(video.nsfw).to.be.true | ||
166 | |||
167 | await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
168 | await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
169 | } | ||
170 | }) | ||
171 | |||
172 | it('Should not have the live listed since nobody streams into', async function () { | ||
173 | for (const server of servers) { | ||
174 | const { total, data } = await server.videos.list() | ||
175 | |||
176 | expect(total).to.equal(0) | ||
177 | expect(data).to.have.lengthOf(0) | ||
178 | } | ||
179 | }) | ||
180 | |||
181 | it('Should not be able to update a live of another server', async function () { | ||
182 | await commands[1].update({ videoId: liveVideoUUID, fields: { saveReplay: false }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
183 | }) | ||
184 | |||
185 | it('Should update the live', async function () { | ||
186 | await commands[0].update({ videoId: liveVideoUUID, fields: { saveReplay: false, latencyMode: LiveVideoLatencyMode.DEFAULT } }) | ||
187 | await waitJobs(servers) | ||
188 | }) | ||
189 | |||
190 | it('Have the live updated', async function () { | ||
191 | for (const server of servers) { | ||
192 | const live = await server.live.get({ videoId: liveVideoUUID }) | ||
193 | |||
194 | if (server.url === servers[0].url) { | ||
195 | expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live') | ||
196 | expect(live.streamKey).to.not.be.empty | ||
197 | } else { | ||
198 | expect(live.rtmpUrl).to.not.exist | ||
199 | expect(live.streamKey).to.not.exist | ||
200 | } | ||
201 | |||
202 | expect(live.saveReplay).to.be.false | ||
203 | expect(live.replaySettings).to.not.exist | ||
204 | expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT) | ||
205 | } | ||
206 | }) | ||
207 | |||
208 | it('Delete the live', async function () { | ||
209 | await servers[0].videos.remove({ id: liveVideoUUID }) | ||
210 | await waitJobs(servers) | ||
211 | }) | ||
212 | |||
213 | it('Should have the live deleted', async function () { | ||
214 | for (const server of servers) { | ||
215 | await server.videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
216 | await server.live.get({ videoId: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
217 | } | ||
218 | }) | ||
219 | }) | ||
220 | |||
221 | describe('Live filters', function () { | ||
222 | let ffmpegCommand: any | ||
223 | let liveVideoId: string | ||
224 | let vodVideoId: string | ||
225 | |||
226 | before(async function () { | ||
227 | this.timeout(240000) | ||
228 | |||
229 | vodVideoId = (await servers[0].videos.quickUpload({ name: 'vod video' })).uuid | ||
230 | |||
231 | const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: servers[0].store.channel.id } | ||
232 | const live = await commands[0].create({ fields: liveOptions }) | ||
233 | liveVideoId = live.uuid | ||
234 | |||
235 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoId }) | ||
236 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
237 | await waitJobs(servers) | ||
238 | }) | ||
239 | |||
240 | it('Should only display lives', async function () { | ||
241 | const { data, total } = await servers[0].videos.list({ isLive: true }) | ||
242 | |||
243 | expect(total).to.equal(1) | ||
244 | expect(data).to.have.lengthOf(1) | ||
245 | expect(data[0].name).to.equal('live') | ||
246 | }) | ||
247 | |||
248 | it('Should not display lives', async function () { | ||
249 | const { data, total } = await servers[0].videos.list({ isLive: false }) | ||
250 | |||
251 | expect(total).to.equal(1) | ||
252 | expect(data).to.have.lengthOf(1) | ||
253 | expect(data[0].name).to.equal('vod video') | ||
254 | }) | ||
255 | |||
256 | it('Should display my lives', async function () { | ||
257 | this.timeout(60000) | ||
258 | |||
259 | await stopFfmpeg(ffmpegCommand) | ||
260 | await waitJobs(servers) | ||
261 | |||
262 | const { data } = await servers[0].videos.listMyVideos({ isLive: true }) | ||
263 | |||
264 | const result = data.every(v => v.isLive) | ||
265 | expect(result).to.be.true | ||
266 | }) | ||
267 | |||
268 | it('Should not display my lives', async function () { | ||
269 | const { data } = await servers[0].videos.listMyVideos({ isLive: false }) | ||
270 | |||
271 | const result = data.every(v => !v.isLive) | ||
272 | expect(result).to.be.true | ||
273 | }) | ||
274 | |||
275 | after(async function () { | ||
276 | await servers[0].videos.remove({ id: vodVideoId }) | ||
277 | await servers[0].videos.remove({ id: liveVideoId }) | ||
278 | }) | ||
279 | }) | ||
280 | |||
281 | describe('Stream checks', function () { | ||
282 | let liveVideo: LiveVideo & VideoDetails | ||
283 | let rtmpUrl: string | ||
284 | |||
285 | before(function () { | ||
286 | rtmpUrl = 'rtmp://' + servers[0].hostname + ':' + servers[0].rtmpPort + '' | ||
287 | }) | ||
288 | |||
289 | async function createLiveWrapper () { | ||
290 | const liveAttributes = { | ||
291 | name: 'user live', | ||
292 | channelId: servers[0].store.channel.id, | ||
293 | privacy: VideoPrivacy.PUBLIC, | ||
294 | saveReplay: false | ||
295 | } | ||
296 | |||
297 | const { uuid } = await commands[0].create({ fields: liveAttributes }) | ||
298 | |||
299 | const live = await commands[0].get({ videoId: uuid }) | ||
300 | const video = await servers[0].videos.get({ id: uuid }) | ||
301 | |||
302 | return Object.assign(video, live) | ||
303 | } | ||
304 | |||
305 | it('Should not allow a stream without the appropriate path', async function () { | ||
306 | this.timeout(60000) | ||
307 | |||
308 | liveVideo = await createLiveWrapper() | ||
309 | |||
310 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/bad-live', streamKey: liveVideo.streamKey }) | ||
311 | await testFfmpegStreamError(command, true) | ||
312 | }) | ||
313 | |||
314 | it('Should not allow a stream without the appropriate stream key', async function () { | ||
315 | this.timeout(60000) | ||
316 | |||
317 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: 'bad-stream-key' }) | ||
318 | await testFfmpegStreamError(command, true) | ||
319 | }) | ||
320 | |||
321 | it('Should succeed with the correct params', async function () { | ||
322 | this.timeout(60000) | ||
323 | |||
324 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey }) | ||
325 | await testFfmpegStreamError(command, false) | ||
326 | }) | ||
327 | |||
328 | it('Should list this live now someone stream into it', async function () { | ||
329 | for (const server of servers) { | ||
330 | const { total, data } = await server.videos.list() | ||
331 | |||
332 | expect(total).to.equal(1) | ||
333 | expect(data).to.have.lengthOf(1) | ||
334 | |||
335 | const video = data[0] | ||
336 | expect(video.name).to.equal('user live') | ||
337 | expect(video.isLive).to.be.true | ||
338 | } | ||
339 | }) | ||
340 | |||
341 | it('Should not allow a stream on a live that was blacklisted', async function () { | ||
342 | this.timeout(60000) | ||
343 | |||
344 | liveVideo = await createLiveWrapper() | ||
345 | |||
346 | await servers[0].blacklist.add({ videoId: liveVideo.uuid }) | ||
347 | |||
348 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey }) | ||
349 | await testFfmpegStreamError(command, true) | ||
350 | }) | ||
351 | |||
352 | it('Should not allow a stream on a live that was deleted', async function () { | ||
353 | this.timeout(60000) | ||
354 | |||
355 | liveVideo = await createLiveWrapper() | ||
356 | |||
357 | await servers[0].videos.remove({ id: liveVideo.uuid }) | ||
358 | |||
359 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey }) | ||
360 | await testFfmpegStreamError(command, true) | ||
361 | }) | ||
362 | }) | ||
363 | |||
364 | describe('Live transcoding', function () { | ||
365 | let liveVideoId: string | ||
366 | let sqlCommandServer1: SQLCommand | ||
367 | |||
368 | async function createLiveWrapper (saveReplay: boolean) { | ||
369 | const liveAttributes = { | ||
370 | name: 'live video', | ||
371 | channelId: servers[0].store.channel.id, | ||
372 | privacy: VideoPrivacy.PUBLIC, | ||
373 | saveReplay, | ||
374 | replaySettings: saveReplay | ||
375 | ? { privacy: VideoPrivacy.PUBLIC } | ||
376 | : undefined | ||
377 | } | ||
378 | |||
379 | const { uuid } = await commands[0].create({ fields: liveAttributes }) | ||
380 | return uuid | ||
381 | } | ||
382 | |||
383 | function updateConf (resolutions: number[]) { | ||
384 | return servers[0].config.updateCustomSubConfig({ | ||
385 | newConfig: { | ||
386 | live: { | ||
387 | enabled: true, | ||
388 | allowReplay: true, | ||
389 | maxDuration: -1, | ||
390 | transcoding: { | ||
391 | enabled: true, | ||
392 | resolutions: { | ||
393 | '144p': resolutions.includes(144), | ||
394 | '240p': resolutions.includes(240), | ||
395 | '360p': resolutions.includes(360), | ||
396 | '480p': resolutions.includes(480), | ||
397 | '720p': resolutions.includes(720), | ||
398 | '1080p': resolutions.includes(1080), | ||
399 | '2160p': resolutions.includes(2160) | ||
400 | } | ||
401 | } | ||
402 | } | ||
403 | } | ||
404 | }) | ||
405 | } | ||
406 | |||
407 | before(async function () { | ||
408 | await updateConf([]) | ||
409 | |||
410 | sqlCommandServer1 = new SQLCommand(servers[0]) | ||
411 | }) | ||
412 | |||
413 | it('Should enable transcoding without additional resolutions', async function () { | ||
414 | this.timeout(120000) | ||
415 | |||
416 | liveVideoId = await createLiveWrapper(false) | ||
417 | |||
418 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId }) | ||
419 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
420 | await waitJobs(servers) | ||
421 | |||
422 | await testLiveVideoResolutions({ | ||
423 | originServer: servers[0], | ||
424 | sqlCommand: sqlCommandServer1, | ||
425 | servers, | ||
426 | liveVideoId, | ||
427 | resolutions: [ 720 ], | ||
428 | transcoded: true | ||
429 | }) | ||
430 | |||
431 | await stopFfmpeg(ffmpegCommand) | ||
432 | }) | ||
433 | |||
434 | it('Should transcode audio only RTMP stream', async function () { | ||
435 | this.timeout(120000) | ||
436 | |||
437 | liveVideoId = await createLiveWrapper(false) | ||
438 | |||
439 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short_no_audio.mp4' }) | ||
440 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
441 | await waitJobs(servers) | ||
442 | |||
443 | await stopFfmpeg(ffmpegCommand) | ||
444 | }) | ||
445 | |||
446 | it('Should enable transcoding with some resolutions', async function () { | ||
447 | this.timeout(240000) | ||
448 | |||
449 | const resolutions = [ 240, 480 ] | ||
450 | await updateConf(resolutions) | ||
451 | liveVideoId = await createLiveWrapper(false) | ||
452 | |||
453 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId }) | ||
454 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
455 | await waitJobs(servers) | ||
456 | |||
457 | await testLiveVideoResolutions({ | ||
458 | originServer: servers[0], | ||
459 | sqlCommand: sqlCommandServer1, | ||
460 | servers, | ||
461 | liveVideoId, | ||
462 | resolutions: resolutions.concat([ 720 ]), | ||
463 | transcoded: true | ||
464 | }) | ||
465 | |||
466 | await stopFfmpeg(ffmpegCommand) | ||
467 | }) | ||
468 | |||
469 | it('Should correctly set the appropriate bitrate depending on the input', async function () { | ||
470 | this.timeout(120000) | ||
471 | |||
472 | liveVideoId = await createLiveWrapper(false) | ||
473 | |||
474 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ | ||
475 | videoId: liveVideoId, | ||
476 | fixtureName: 'video_short.mp4', | ||
477 | copyCodecs: true | ||
478 | }) | ||
479 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
480 | await waitJobs(servers) | ||
481 | |||
482 | const video = await servers[0].videos.get({ id: liveVideoId }) | ||
483 | |||
484 | const masterPlaylist = video.streamingPlaylists[0].playlistUrl | ||
485 | const probe = await ffprobePromise(masterPlaylist) | ||
486 | |||
487 | const bitrates = probe.streams.map(s => parseInt(s.tags.variant_bitrate)) | ||
488 | for (const bitrate of bitrates) { | ||
489 | expect(bitrate).to.exist | ||
490 | expect(isNaN(bitrate)).to.be.false | ||
491 | expect(bitrate).to.be.below(61_000_000) // video_short.mp4 bitrate | ||
492 | } | ||
493 | |||
494 | await stopFfmpeg(ffmpegCommand) | ||
495 | }) | ||
496 | |||
497 | it('Should enable transcoding with some resolutions and correctly save them', async function () { | ||
498 | this.timeout(500_000) | ||
499 | |||
500 | const resolutions = [ 240, 360, 720 ] | ||
501 | |||
502 | await updateConf(resolutions) | ||
503 | liveVideoId = await createLiveWrapper(true) | ||
504 | |||
505 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' }) | ||
506 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
507 | await waitJobs(servers) | ||
508 | |||
509 | await testLiveVideoResolutions({ | ||
510 | originServer: servers[0], | ||
511 | sqlCommand: sqlCommandServer1, | ||
512 | servers, | ||
513 | liveVideoId, | ||
514 | resolutions, | ||
515 | transcoded: true | ||
516 | }) | ||
517 | |||
518 | await stopFfmpeg(ffmpegCommand) | ||
519 | await commands[0].waitUntilEnded({ videoId: liveVideoId }) | ||
520 | |||
521 | await waitJobs(servers) | ||
522 | |||
523 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
524 | |||
525 | const maxBitrateLimits = { | ||
526 | 720: 6500 * 1000, // 60FPS | ||
527 | 360: 1250 * 1000, | ||
528 | 240: 700 * 1000 | ||
529 | } | ||
530 | |||
531 | const minBitrateLimits = { | ||
532 | 720: 4800 * 1000, | ||
533 | 360: 1000 * 1000, | ||
534 | 240: 550 * 1000 | ||
535 | } | ||
536 | |||
537 | for (const server of servers) { | ||
538 | const video = await server.videos.get({ id: liveVideoId }) | ||
539 | |||
540 | expect(video.state.id).to.equal(VideoState.PUBLISHED) | ||
541 | expect(video.duration).to.be.greaterThan(1) | ||
542 | expect(video.files).to.have.lengthOf(0) | ||
543 | |||
544 | const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS) | ||
545 | await makeRawRequest({ url: hlsPlaylist.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
546 | await makeRawRequest({ url: hlsPlaylist.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) | ||
547 | |||
548 | // We should have generated random filenames | ||
549 | expect(basename(hlsPlaylist.playlistUrl)).to.not.equal('master.m3u8') | ||
550 | expect(basename(hlsPlaylist.segmentsSha256Url)).to.not.equal('segments-sha256.json') | ||
551 | |||
552 | expect(hlsPlaylist.files).to.have.lengthOf(resolutions.length) | ||
553 | |||
554 | for (const resolution of resolutions) { | ||
555 | const file = hlsPlaylist.files.find(f => f.resolution.id === resolution) | ||
556 | |||
557 | expect(file).to.exist | ||
558 | expect(file.size).to.be.greaterThan(1) | ||
559 | |||
560 | if (resolution >= 720) { | ||
561 | expect(file.fps).to.be.approximately(60, 10) | ||
562 | } else { | ||
563 | expect(file.fps).to.be.approximately(30, 3) | ||
564 | } | ||
565 | |||
566 | const filename = basename(file.fileUrl) | ||
567 | expect(filename).to.not.contain(video.uuid) | ||
568 | |||
569 | const segmentPath = servers[0].servers.buildDirectory(join('streaming-playlists', 'hls', video.uuid, filename)) | ||
570 | |||
571 | const probe = await ffprobePromise(segmentPath) | ||
572 | const videoStream = await getVideoStream(segmentPath, probe) | ||
573 | |||
574 | expect(probe.format.bit_rate).to.be.below(maxBitrateLimits[videoStream.height]) | ||
575 | expect(probe.format.bit_rate).to.be.at.least(minBitrateLimits[videoStream.height]) | ||
576 | |||
577 | await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
578 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
579 | } | ||
580 | } | ||
581 | }) | ||
582 | |||
583 | it('Should not generate an upper resolution than original file', async function () { | ||
584 | this.timeout(500_000) | ||
585 | |||
586 | const resolutions = [ 240, 480 ] | ||
587 | await updateConf(resolutions) | ||
588 | |||
589 | await servers[0].config.updateExistingSubConfig({ | ||
590 | newConfig: { | ||
591 | live: { | ||
592 | transcoding: { | ||
593 | alwaysTranscodeOriginalResolution: false | ||
594 | } | ||
595 | } | ||
596 | } | ||
597 | }) | ||
598 | |||
599 | liveVideoId = await createLiveWrapper(true) | ||
600 | |||
601 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' }) | ||
602 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
603 | await waitJobs(servers) | ||
604 | |||
605 | await testLiveVideoResolutions({ | ||
606 | originServer: servers[0], | ||
607 | sqlCommand: sqlCommandServer1, | ||
608 | servers, | ||
609 | liveVideoId, | ||
610 | resolutions, | ||
611 | transcoded: true | ||
612 | }) | ||
613 | |||
614 | await stopFfmpeg(ffmpegCommand) | ||
615 | await commands[0].waitUntilEnded({ videoId: liveVideoId }) | ||
616 | |||
617 | await waitJobs(servers) | ||
618 | |||
619 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
620 | |||
621 | const video = await servers[0].videos.get({ id: liveVideoId }) | ||
622 | const hlsFiles = video.streamingPlaylists[0].files | ||
623 | |||
624 | expect(video.files).to.have.lengthOf(0) | ||
625 | expect(hlsFiles).to.have.lengthOf(resolutions.length) | ||
626 | |||
627 | // eslint-disable-next-line @typescript-eslint/require-array-sort-compare | ||
628 | expect(getAllFiles(video).map(f => f.resolution.id).sort()).to.deep.equal(resolutions) | ||
629 | }) | ||
630 | |||
631 | it('Should only keep the original resolution if all resolutions are disabled', async function () { | ||
632 | this.timeout(600_000) | ||
633 | |||
634 | await updateConf([]) | ||
635 | liveVideoId = await createLiveWrapper(true) | ||
636 | |||
637 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' }) | ||
638 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
639 | await waitJobs(servers) | ||
640 | |||
641 | await testLiveVideoResolutions({ | ||
642 | originServer: servers[0], | ||
643 | sqlCommand: sqlCommandServer1, | ||
644 | servers, | ||
645 | liveVideoId, | ||
646 | resolutions: [ 720 ], | ||
647 | transcoded: true | ||
648 | }) | ||
649 | |||
650 | await stopFfmpeg(ffmpegCommand) | ||
651 | await commands[0].waitUntilEnded({ videoId: liveVideoId }) | ||
652 | |||
653 | await waitJobs(servers) | ||
654 | |||
655 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
656 | |||
657 | const video = await servers[0].videos.get({ id: liveVideoId }) | ||
658 | const hlsFiles = video.streamingPlaylists[0].files | ||
659 | |||
660 | expect(video.files).to.have.lengthOf(0) | ||
661 | expect(hlsFiles).to.have.lengthOf(1) | ||
662 | |||
663 | expect(hlsFiles[0].resolution.id).to.equal(720) | ||
664 | }) | ||
665 | |||
666 | after(async function () { | ||
667 | await sqlCommandServer1.cleanup() | ||
668 | }) | ||
669 | }) | ||
670 | |||
671 | describe('After a server restart', function () { | ||
672 | let liveVideoId: string | ||
673 | let liveVideoReplayId: string | ||
674 | let permanentLiveVideoReplayId: string | ||
675 | |||
676 | let permanentLiveReplayName: string | ||
677 | |||
678 | let beforeServerRestart: Date | ||
679 | |||
680 | async function createLiveWrapper (options: { saveReplay: boolean, permanent: boolean }) { | ||
681 | const liveAttributes: LiveVideoCreate = { | ||
682 | name: 'live video', | ||
683 | channelId: servers[0].store.channel.id, | ||
684 | privacy: VideoPrivacy.PUBLIC, | ||
685 | saveReplay: options.saveReplay, | ||
686 | replaySettings: options.saveReplay | ||
687 | ? { privacy: VideoPrivacy.PUBLIC } | ||
688 | : undefined, | ||
689 | permanentLive: options.permanent | ||
690 | } | ||
691 | |||
692 | const { uuid } = await commands[0].create({ fields: liveAttributes }) | ||
693 | return uuid | ||
694 | } | ||
695 | |||
696 | before(async function () { | ||
697 | this.timeout(600_000) | ||
698 | |||
699 | liveVideoId = await createLiveWrapper({ saveReplay: false, permanent: false }) | ||
700 | liveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: false }) | ||
701 | permanentLiveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: true }) | ||
702 | |||
703 | await Promise.all([ | ||
704 | commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId }), | ||
705 | commands[0].sendRTMPStreamInVideo({ videoId: permanentLiveVideoReplayId }), | ||
706 | commands[0].sendRTMPStreamInVideo({ videoId: liveVideoReplayId }) | ||
707 | ]) | ||
708 | |||
709 | await Promise.all([ | ||
710 | commands[0].waitUntilPublished({ videoId: liveVideoId }), | ||
711 | commands[0].waitUntilPublished({ videoId: permanentLiveVideoReplayId }), | ||
712 | commands[0].waitUntilPublished({ videoId: liveVideoReplayId }) | ||
713 | ]) | ||
714 | |||
715 | for (const videoUUID of [ liveVideoId, liveVideoReplayId, permanentLiveVideoReplayId ]) { | ||
716 | await commands[0].waitUntilSegmentGeneration({ | ||
717 | server: servers[0], | ||
718 | videoUUID, | ||
719 | playlistNumber: 0, | ||
720 | segment: 2 | ||
721 | }) | ||
722 | } | ||
723 | |||
724 | { | ||
725 | const video = await servers[0].videos.get({ id: permanentLiveVideoReplayId }) | ||
726 | permanentLiveReplayName = video.name + ' - ' + new Date(video.publishedAt).toLocaleString() | ||
727 | } | ||
728 | |||
729 | await killallServers([ servers[0] ]) | ||
730 | |||
731 | beforeServerRestart = new Date() | ||
732 | await servers[0].run() | ||
733 | |||
734 | await wait(5000) | ||
735 | await waitJobs(servers) | ||
736 | }) | ||
737 | |||
738 | it('Should cleanup lives', async function () { | ||
739 | this.timeout(60000) | ||
740 | |||
741 | await commands[0].waitUntilEnded({ videoId: liveVideoId }) | ||
742 | await commands[0].waitUntilWaiting({ videoId: permanentLiveVideoReplayId }) | ||
743 | }) | ||
744 | |||
745 | it('Should save a non permanent live replay', async function () { | ||
746 | this.timeout(240000) | ||
747 | |||
748 | await commands[0].waitUntilPublished({ videoId: liveVideoReplayId }) | ||
749 | |||
750 | const session = await commands[0].getReplaySession({ videoId: liveVideoReplayId }) | ||
751 | expect(session.endDate).to.exist | ||
752 | expect(new Date(session.endDate)).to.be.above(beforeServerRestart) | ||
753 | }) | ||
754 | |||
755 | it('Should have saved a permanent live replay', async function () { | ||
756 | this.timeout(120000) | ||
757 | |||
758 | const { data } = await servers[0].videos.listMyVideos({ sort: '-publishedAt' }) | ||
759 | expect(data.find(v => v.name === permanentLiveReplayName)).to.exist | ||
760 | }) | ||
761 | }) | ||
762 | |||
763 | after(async function () { | ||
764 | await cleanupTests(servers) | ||
765 | }) | ||
766 | }) | ||