aboutsummaryrefslogtreecommitdiffhomepage
path: root/packages/tests/src/api/live
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-07-31 14:34:36 +0200
committerChocobozzz <me@florianbigard.com>2023-08-11 15:02:33 +0200
commit3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch)
treee4510b39bdac9c318fdb4b47018d08f15368b8f0 /packages/tests/src/api/live
parent04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff)
downloadPeerTube-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.ts7
-rw-r--r--packages/tests/src/api/live/live-constraints.ts237
-rw-r--r--packages/tests/src/api/live/live-fast-restream.ts153
-rw-r--r--packages/tests/src/api/live/live-permanent.ts204
-rw-r--r--packages/tests/src/api/live/live-rtmps.ts143
-rw-r--r--packages/tests/src/api/live/live-save-replay.ts583
-rw-r--r--packages/tests/src/api/live/live-socket-messages.ts186
-rw-r--r--packages/tests/src/api/live/live.ts766
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 @@
1import './live-constraints.js'
2import './live-fast-restream.js'
3import './live-socket-messages.js'
4import './live-permanent.js'
5import './live-rtmps.js'
6import './live-save-replay.js'
7import './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
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { LiveVideoError, UserVideoQuota, VideoPrivacy } from '@peertube/peertube-models'
6import {
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'
19import { checkLiveCleanup } from '../../shared/live.js'
20
21describe('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
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createSingleServer,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 setDefaultVideoChannel,
12 stopFfmpeg,
13 waitJobs
14} from '@peertube/peertube-server-commands'
15
16describe('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
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { LiveVideoCreate, VideoPrivacy, VideoState, VideoStateType } from '@peertube/peertube-models'
6import { checkLiveCleanup } from '@tests/shared/live.js'
7import {
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
19describe('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
3import { expect } from 'chai'
4import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
5import { VideoPrivacy } from '@peertube/peertube-models'
6import {
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
18describe('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
3import { expect } from 'chai'
4import { FfmpegCommand } from 'fluent-ffmpeg'
5import { wait } from '@peertube/peertube-core-utils'
6import {
7 HttpStatusCode,
8 HttpStatusCodeType,
9 LiveVideoCreate,
10 LiveVideoError,
11 VideoPrivacy,
12 VideoPrivacyType,
13 VideoState,
14 VideoStateType
15} from '@peertube/peertube-models'
16import { checkLiveCleanup } from '@tests/shared/live.js'
17import {
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
34describe('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
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { LiveVideoEventPayload, VideoPrivacy, VideoState, VideoStateType } from '@peertube/peertube-models'
6import {
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
18describe('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
3import { expect } from 'chai'
4import { basename, join } from 'path'
5import { getAllFiles, wait } from '@peertube/peertube-core-utils'
6import { ffprobePromise, getVideoStream } from '@peertube/peertube-ffmpeg'
7import {
8 HttpStatusCode,
9 LiveVideo,
10 LiveVideoCreate,
11 LiveVideoLatencyMode,
12 VideoDetails,
13 VideoPrivacy,
14 VideoState,
15 VideoStreamingPlaylistType
16} from '@peertube/peertube-models'
17import {
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'
34import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js'
35import { testLiveVideoResolutions } from '@tests/shared/live.js'
36import { SQLCommand } from '@tests/shared/sql-command.js'
37
38describe('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})