diff options
Diffstat (limited to 'server/tests/api/live/live.ts')
-rw-r--r-- | server/tests/api/live/live.ts | 764 |
1 files changed, 0 insertions, 764 deletions
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts deleted file mode 100644 index 2b302a8a2..000000000 --- a/server/tests/api/live/live.ts +++ /dev/null | |||
@@ -1,764 +0,0 @@ | |||
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 { SQLCommand, testImageGeneratedByFFmpeg, testLiveVideoResolutions } from '@server/tests/shared' | ||
6 | import { getAllFiles, wait } from '@shared/core-utils' | ||
7 | import { ffprobePromise, getVideoStream } from '@shared/ffmpeg' | ||
8 | import { | ||
9 | HttpStatusCode, | ||
10 | LiveVideo, | ||
11 | LiveVideoCreate, | ||
12 | LiveVideoLatencyMode, | ||
13 | VideoDetails, | ||
14 | VideoPrivacy, | ||
15 | VideoState, | ||
16 | VideoStreamingPlaylistType | ||
17 | } from '@shared/models' | ||
18 | import { | ||
19 | cleanupTests, | ||
20 | createMultipleServers, | ||
21 | doubleFollow, | ||
22 | killallServers, | ||
23 | LiveCommand, | ||
24 | makeGetRequest, | ||
25 | makeRawRequest, | ||
26 | PeerTubeServer, | ||
27 | sendRTMPStream, | ||
28 | setAccessTokensToServers, | ||
29 | setDefaultVideoChannel, | ||
30 | stopFfmpeg, | ||
31 | testFfmpegStreamError, | ||
32 | waitJobs, | ||
33 | waitUntilLivePublishedOnAllServers | ||
34 | } from '@shared/server-commands' | ||
35 | |||
36 | describe('Test live', function () { | ||
37 | let servers: PeerTubeServer[] = [] | ||
38 | let commands: LiveCommand[] | ||
39 | |||
40 | before(async function () { | ||
41 | this.timeout(120000) | ||
42 | |||
43 | servers = await createMultipleServers(2) | ||
44 | |||
45 | // Get the access tokens | ||
46 | await setAccessTokensToServers(servers) | ||
47 | await setDefaultVideoChannel(servers) | ||
48 | |||
49 | await servers[0].config.updateCustomSubConfig({ | ||
50 | newConfig: { | ||
51 | live: { | ||
52 | enabled: true, | ||
53 | allowReplay: true, | ||
54 | latencySetting: { | ||
55 | enabled: true | ||
56 | }, | ||
57 | transcoding: { | ||
58 | enabled: false | ||
59 | } | ||
60 | } | ||
61 | } | ||
62 | }) | ||
63 | |||
64 | // Server 1 and server 2 follow each other | ||
65 | await doubleFollow(servers[0], servers[1]) | ||
66 | |||
67 | commands = servers.map(s => s.live) | ||
68 | }) | ||
69 | |||
70 | describe('Live creation, update and delete', function () { | ||
71 | let liveVideoUUID: string | ||
72 | |||
73 | it('Should create a live with the appropriate parameters', async function () { | ||
74 | this.timeout(20000) | ||
75 | |||
76 | const attributes: LiveVideoCreate = { | ||
77 | category: 1, | ||
78 | licence: 2, | ||
79 | language: 'fr', | ||
80 | description: 'super live description', | ||
81 | support: 'support field', | ||
82 | channelId: servers[0].store.channel.id, | ||
83 | nsfw: false, | ||
84 | waitTranscoding: false, | ||
85 | name: 'my super live', | ||
86 | tags: [ 'tag1', 'tag2' ], | ||
87 | commentsEnabled: false, | ||
88 | downloadEnabled: false, | ||
89 | saveReplay: true, | ||
90 | replaySettings: { privacy: VideoPrivacy.PUBLIC }, | ||
91 | latencyMode: LiveVideoLatencyMode.SMALL_LATENCY, | ||
92 | privacy: VideoPrivacy.PUBLIC, | ||
93 | previewfile: 'video_short1-preview.webm.jpg', | ||
94 | thumbnailfile: 'video_short1.webm.jpg' | ||
95 | } | ||
96 | |||
97 | const live = await commands[0].create({ fields: attributes }) | ||
98 | liveVideoUUID = live.uuid | ||
99 | |||
100 | await waitJobs(servers) | ||
101 | |||
102 | for (const server of servers) { | ||
103 | const video = await server.videos.get({ id: liveVideoUUID }) | ||
104 | |||
105 | expect(video.category.id).to.equal(1) | ||
106 | expect(video.licence.id).to.equal(2) | ||
107 | expect(video.language.id).to.equal('fr') | ||
108 | expect(video.description).to.equal('super live description') | ||
109 | expect(video.support).to.equal('support field') | ||
110 | |||
111 | expect(video.channel.name).to.equal(servers[0].store.channel.name) | ||
112 | expect(video.channel.host).to.equal(servers[0].store.channel.host) | ||
113 | |||
114 | expect(video.isLive).to.be.true | ||
115 | |||
116 | expect(video.nsfw).to.be.false | ||
117 | expect(video.waitTranscoding).to.be.false | ||
118 | expect(video.name).to.equal('my super live') | ||
119 | expect(video.tags).to.deep.equal([ 'tag1', 'tag2' ]) | ||
120 | expect(video.commentsEnabled).to.be.false | ||
121 | expect(video.downloadEnabled).to.be.false | ||
122 | expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC) | ||
123 | |||
124 | await testImageGeneratedByFFmpeg(server.url, 'video_short1-preview.webm', video.previewPath) | ||
125 | await testImageGeneratedByFFmpeg(server.url, 'video_short1.webm', video.thumbnailPath) | ||
126 | |||
127 | const live = await server.live.get({ videoId: liveVideoUUID }) | ||
128 | |||
129 | if (server.url === servers[0].url) { | ||
130 | expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live') | ||
131 | expect(live.streamKey).to.not.be.empty | ||
132 | |||
133 | expect(live.replaySettings).to.exist | ||
134 | expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC) | ||
135 | } else { | ||
136 | expect(live.rtmpUrl).to.not.exist | ||
137 | expect(live.streamKey).to.not.exist | ||
138 | } | ||
139 | |||
140 | expect(live.saveReplay).to.be.true | ||
141 | expect(live.latencyMode).to.equal(LiveVideoLatencyMode.SMALL_LATENCY) | ||
142 | } | ||
143 | }) | ||
144 | |||
145 | it('Should have a default preview and thumbnail', async function () { | ||
146 | this.timeout(20000) | ||
147 | |||
148 | const attributes: LiveVideoCreate = { | ||
149 | name: 'default live thumbnail', | ||
150 | channelId: servers[0].store.channel.id, | ||
151 | privacy: VideoPrivacy.UNLISTED, | ||
152 | nsfw: true | ||
153 | } | ||
154 | |||
155 | const live = await commands[0].create({ fields: attributes }) | ||
156 | const videoId = live.uuid | ||
157 | |||
158 | await waitJobs(servers) | ||
159 | |||
160 | for (const server of servers) { | ||
161 | const video = await server.videos.get({ id: videoId }) | ||
162 | expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED) | ||
163 | expect(video.nsfw).to.be.true | ||
164 | |||
165 | await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
166 | await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
167 | } | ||
168 | }) | ||
169 | |||
170 | it('Should not have the live listed since nobody streams into', async function () { | ||
171 | for (const server of servers) { | ||
172 | const { total, data } = await server.videos.list() | ||
173 | |||
174 | expect(total).to.equal(0) | ||
175 | expect(data).to.have.lengthOf(0) | ||
176 | } | ||
177 | }) | ||
178 | |||
179 | it('Should not be able to update a live of another server', async function () { | ||
180 | await commands[1].update({ videoId: liveVideoUUID, fields: { saveReplay: false }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
181 | }) | ||
182 | |||
183 | it('Should update the live', async function () { | ||
184 | await commands[0].update({ videoId: liveVideoUUID, fields: { saveReplay: false, latencyMode: LiveVideoLatencyMode.DEFAULT } }) | ||
185 | await waitJobs(servers) | ||
186 | }) | ||
187 | |||
188 | it('Have the live updated', async function () { | ||
189 | for (const server of servers) { | ||
190 | const live = await server.live.get({ videoId: liveVideoUUID }) | ||
191 | |||
192 | if (server.url === servers[0].url) { | ||
193 | expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live') | ||
194 | expect(live.streamKey).to.not.be.empty | ||
195 | } else { | ||
196 | expect(live.rtmpUrl).to.not.exist | ||
197 | expect(live.streamKey).to.not.exist | ||
198 | } | ||
199 | |||
200 | expect(live.saveReplay).to.be.false | ||
201 | expect(live.replaySettings).to.not.exist | ||
202 | expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT) | ||
203 | } | ||
204 | }) | ||
205 | |||
206 | it('Delete the live', async function () { | ||
207 | await servers[0].videos.remove({ id: liveVideoUUID }) | ||
208 | await waitJobs(servers) | ||
209 | }) | ||
210 | |||
211 | it('Should have the live deleted', async function () { | ||
212 | for (const server of servers) { | ||
213 | await server.videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
214 | await server.live.get({ videoId: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
215 | } | ||
216 | }) | ||
217 | }) | ||
218 | |||
219 | describe('Live filters', function () { | ||
220 | let ffmpegCommand: any | ||
221 | let liveVideoId: string | ||
222 | let vodVideoId: string | ||
223 | |||
224 | before(async function () { | ||
225 | this.timeout(240000) | ||
226 | |||
227 | vodVideoId = (await servers[0].videos.quickUpload({ name: 'vod video' })).uuid | ||
228 | |||
229 | const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: servers[0].store.channel.id } | ||
230 | const live = await commands[0].create({ fields: liveOptions }) | ||
231 | liveVideoId = live.uuid | ||
232 | |||
233 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoId }) | ||
234 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
235 | await waitJobs(servers) | ||
236 | }) | ||
237 | |||
238 | it('Should only display lives', async function () { | ||
239 | const { data, total } = await servers[0].videos.list({ isLive: true }) | ||
240 | |||
241 | expect(total).to.equal(1) | ||
242 | expect(data).to.have.lengthOf(1) | ||
243 | expect(data[0].name).to.equal('live') | ||
244 | }) | ||
245 | |||
246 | it('Should not display lives', async function () { | ||
247 | const { data, total } = await servers[0].videos.list({ isLive: false }) | ||
248 | |||
249 | expect(total).to.equal(1) | ||
250 | expect(data).to.have.lengthOf(1) | ||
251 | expect(data[0].name).to.equal('vod video') | ||
252 | }) | ||
253 | |||
254 | it('Should display my lives', async function () { | ||
255 | this.timeout(60000) | ||
256 | |||
257 | await stopFfmpeg(ffmpegCommand) | ||
258 | await waitJobs(servers) | ||
259 | |||
260 | const { data } = await servers[0].videos.listMyVideos({ isLive: true }) | ||
261 | |||
262 | const result = data.every(v => v.isLive) | ||
263 | expect(result).to.be.true | ||
264 | }) | ||
265 | |||
266 | it('Should not display my lives', async function () { | ||
267 | const { data } = await servers[0].videos.listMyVideos({ isLive: false }) | ||
268 | |||
269 | const result = data.every(v => !v.isLive) | ||
270 | expect(result).to.be.true | ||
271 | }) | ||
272 | |||
273 | after(async function () { | ||
274 | await servers[0].videos.remove({ id: vodVideoId }) | ||
275 | await servers[0].videos.remove({ id: liveVideoId }) | ||
276 | }) | ||
277 | }) | ||
278 | |||
279 | describe('Stream checks', function () { | ||
280 | let liveVideo: LiveVideo & VideoDetails | ||
281 | let rtmpUrl: string | ||
282 | |||
283 | before(function () { | ||
284 | rtmpUrl = 'rtmp://' + servers[0].hostname + ':' + servers[0].rtmpPort + '' | ||
285 | }) | ||
286 | |||
287 | async function createLiveWrapper () { | ||
288 | const liveAttributes = { | ||
289 | name: 'user live', | ||
290 | channelId: servers[0].store.channel.id, | ||
291 | privacy: VideoPrivacy.PUBLIC, | ||
292 | saveReplay: false | ||
293 | } | ||
294 | |||
295 | const { uuid } = await commands[0].create({ fields: liveAttributes }) | ||
296 | |||
297 | const live = await commands[0].get({ videoId: uuid }) | ||
298 | const video = await servers[0].videos.get({ id: uuid }) | ||
299 | |||
300 | return Object.assign(video, live) | ||
301 | } | ||
302 | |||
303 | it('Should not allow a stream without the appropriate path', async function () { | ||
304 | this.timeout(60000) | ||
305 | |||
306 | liveVideo = await createLiveWrapper() | ||
307 | |||
308 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/bad-live', streamKey: liveVideo.streamKey }) | ||
309 | await testFfmpegStreamError(command, true) | ||
310 | }) | ||
311 | |||
312 | it('Should not allow a stream without the appropriate stream key', async function () { | ||
313 | this.timeout(60000) | ||
314 | |||
315 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: 'bad-stream-key' }) | ||
316 | await testFfmpegStreamError(command, true) | ||
317 | }) | ||
318 | |||
319 | it('Should succeed with the correct params', async function () { | ||
320 | this.timeout(60000) | ||
321 | |||
322 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey }) | ||
323 | await testFfmpegStreamError(command, false) | ||
324 | }) | ||
325 | |||
326 | it('Should list this live now someone stream into it', async function () { | ||
327 | for (const server of servers) { | ||
328 | const { total, data } = await server.videos.list() | ||
329 | |||
330 | expect(total).to.equal(1) | ||
331 | expect(data).to.have.lengthOf(1) | ||
332 | |||
333 | const video = data[0] | ||
334 | expect(video.name).to.equal('user live') | ||
335 | expect(video.isLive).to.be.true | ||
336 | } | ||
337 | }) | ||
338 | |||
339 | it('Should not allow a stream on a live that was blacklisted', async function () { | ||
340 | this.timeout(60000) | ||
341 | |||
342 | liveVideo = await createLiveWrapper() | ||
343 | |||
344 | await servers[0].blacklist.add({ videoId: liveVideo.uuid }) | ||
345 | |||
346 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey }) | ||
347 | await testFfmpegStreamError(command, true) | ||
348 | }) | ||
349 | |||
350 | it('Should not allow a stream on a live that was deleted', async function () { | ||
351 | this.timeout(60000) | ||
352 | |||
353 | liveVideo = await createLiveWrapper() | ||
354 | |||
355 | await servers[0].videos.remove({ id: liveVideo.uuid }) | ||
356 | |||
357 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey }) | ||
358 | await testFfmpegStreamError(command, true) | ||
359 | }) | ||
360 | }) | ||
361 | |||
362 | describe('Live transcoding', function () { | ||
363 | let liveVideoId: string | ||
364 | let sqlCommandServer1: SQLCommand | ||
365 | |||
366 | async function createLiveWrapper (saveReplay: boolean) { | ||
367 | const liveAttributes = { | ||
368 | name: 'live video', | ||
369 | channelId: servers[0].store.channel.id, | ||
370 | privacy: VideoPrivacy.PUBLIC, | ||
371 | saveReplay, | ||
372 | replaySettings: saveReplay | ||
373 | ? { privacy: VideoPrivacy.PUBLIC } | ||
374 | : undefined | ||
375 | } | ||
376 | |||
377 | const { uuid } = await commands[0].create({ fields: liveAttributes }) | ||
378 | return uuid | ||
379 | } | ||
380 | |||
381 | function updateConf (resolutions: number[]) { | ||
382 | return servers[0].config.updateCustomSubConfig({ | ||
383 | newConfig: { | ||
384 | live: { | ||
385 | enabled: true, | ||
386 | allowReplay: true, | ||
387 | maxDuration: -1, | ||
388 | transcoding: { | ||
389 | enabled: true, | ||
390 | resolutions: { | ||
391 | '144p': resolutions.includes(144), | ||
392 | '240p': resolutions.includes(240), | ||
393 | '360p': resolutions.includes(360), | ||
394 | '480p': resolutions.includes(480), | ||
395 | '720p': resolutions.includes(720), | ||
396 | '1080p': resolutions.includes(1080), | ||
397 | '2160p': resolutions.includes(2160) | ||
398 | } | ||
399 | } | ||
400 | } | ||
401 | } | ||
402 | }) | ||
403 | } | ||
404 | |||
405 | before(async function () { | ||
406 | await updateConf([]) | ||
407 | |||
408 | sqlCommandServer1 = new SQLCommand(servers[0]) | ||
409 | }) | ||
410 | |||
411 | it('Should enable transcoding without additional resolutions', async function () { | ||
412 | this.timeout(120000) | ||
413 | |||
414 | liveVideoId = await createLiveWrapper(false) | ||
415 | |||
416 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId }) | ||
417 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
418 | await waitJobs(servers) | ||
419 | |||
420 | await testLiveVideoResolutions({ | ||
421 | originServer: servers[0], | ||
422 | sqlCommand: sqlCommandServer1, | ||
423 | servers, | ||
424 | liveVideoId, | ||
425 | resolutions: [ 720 ], | ||
426 | transcoded: true | ||
427 | }) | ||
428 | |||
429 | await stopFfmpeg(ffmpegCommand) | ||
430 | }) | ||
431 | |||
432 | it('Should transcode audio only RTMP stream', async function () { | ||
433 | this.timeout(120000) | ||
434 | |||
435 | liveVideoId = await createLiveWrapper(false) | ||
436 | |||
437 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short_no_audio.mp4' }) | ||
438 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
439 | await waitJobs(servers) | ||
440 | |||
441 | await stopFfmpeg(ffmpegCommand) | ||
442 | }) | ||
443 | |||
444 | it('Should enable transcoding with some resolutions', async function () { | ||
445 | this.timeout(240000) | ||
446 | |||
447 | const resolutions = [ 240, 480 ] | ||
448 | await updateConf(resolutions) | ||
449 | liveVideoId = await createLiveWrapper(false) | ||
450 | |||
451 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId }) | ||
452 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
453 | await waitJobs(servers) | ||
454 | |||
455 | await testLiveVideoResolutions({ | ||
456 | originServer: servers[0], | ||
457 | sqlCommand: sqlCommandServer1, | ||
458 | servers, | ||
459 | liveVideoId, | ||
460 | resolutions: resolutions.concat([ 720 ]), | ||
461 | transcoded: true | ||
462 | }) | ||
463 | |||
464 | await stopFfmpeg(ffmpegCommand) | ||
465 | }) | ||
466 | |||
467 | it('Should correctly set the appropriate bitrate depending on the input', async function () { | ||
468 | this.timeout(120000) | ||
469 | |||
470 | liveVideoId = await createLiveWrapper(false) | ||
471 | |||
472 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ | ||
473 | videoId: liveVideoId, | ||
474 | fixtureName: 'video_short.mp4', | ||
475 | copyCodecs: true | ||
476 | }) | ||
477 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
478 | await waitJobs(servers) | ||
479 | |||
480 | const video = await servers[0].videos.get({ id: liveVideoId }) | ||
481 | |||
482 | const masterPlaylist = video.streamingPlaylists[0].playlistUrl | ||
483 | const probe = await ffprobePromise(masterPlaylist) | ||
484 | |||
485 | const bitrates = probe.streams.map(s => parseInt(s.tags.variant_bitrate)) | ||
486 | for (const bitrate of bitrates) { | ||
487 | expect(bitrate).to.exist | ||
488 | expect(isNaN(bitrate)).to.be.false | ||
489 | expect(bitrate).to.be.below(61_000_000) // video_short.mp4 bitrate | ||
490 | } | ||
491 | |||
492 | await stopFfmpeg(ffmpegCommand) | ||
493 | }) | ||
494 | |||
495 | it('Should enable transcoding with some resolutions and correctly save them', async function () { | ||
496 | this.timeout(500_000) | ||
497 | |||
498 | const resolutions = [ 240, 360, 720 ] | ||
499 | |||
500 | await updateConf(resolutions) | ||
501 | liveVideoId = await createLiveWrapper(true) | ||
502 | |||
503 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' }) | ||
504 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
505 | await waitJobs(servers) | ||
506 | |||
507 | await testLiveVideoResolutions({ | ||
508 | originServer: servers[0], | ||
509 | sqlCommand: sqlCommandServer1, | ||
510 | servers, | ||
511 | liveVideoId, | ||
512 | resolutions, | ||
513 | transcoded: true | ||
514 | }) | ||
515 | |||
516 | await stopFfmpeg(ffmpegCommand) | ||
517 | await commands[0].waitUntilEnded({ videoId: liveVideoId }) | ||
518 | |||
519 | await waitJobs(servers) | ||
520 | |||
521 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
522 | |||
523 | const maxBitrateLimits = { | ||
524 | 720: 6500 * 1000, // 60FPS | ||
525 | 360: 1250 * 1000, | ||
526 | 240: 700 * 1000 | ||
527 | } | ||
528 | |||
529 | const minBitrateLimits = { | ||
530 | 720: 4800 * 1000, | ||
531 | 360: 1000 * 1000, | ||
532 | 240: 550 * 1000 | ||
533 | } | ||
534 | |||
535 | for (const server of servers) { | ||
536 | const video = await server.videos.get({ id: liveVideoId }) | ||
537 | |||
538 | expect(video.state.id).to.equal(VideoState.PUBLISHED) | ||
539 | expect(video.duration).to.be.greaterThan(1) | ||
540 | expect(video.files).to.have.lengthOf(0) | ||
541 | |||
542 | const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS) | ||
543 | await makeRawRequest({ url: hlsPlaylist.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
544 | await makeRawRequest({ url: hlsPlaylist.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) | ||
545 | |||
546 | // We should have generated random filenames | ||
547 | expect(basename(hlsPlaylist.playlistUrl)).to.not.equal('master.m3u8') | ||
548 | expect(basename(hlsPlaylist.segmentsSha256Url)).to.not.equal('segments-sha256.json') | ||
549 | |||
550 | expect(hlsPlaylist.files).to.have.lengthOf(resolutions.length) | ||
551 | |||
552 | for (const resolution of resolutions) { | ||
553 | const file = hlsPlaylist.files.find(f => f.resolution.id === resolution) | ||
554 | |||
555 | expect(file).to.exist | ||
556 | expect(file.size).to.be.greaterThan(1) | ||
557 | |||
558 | if (resolution >= 720) { | ||
559 | expect(file.fps).to.be.approximately(60, 10) | ||
560 | } else { | ||
561 | expect(file.fps).to.be.approximately(30, 3) | ||
562 | } | ||
563 | |||
564 | const filename = basename(file.fileUrl) | ||
565 | expect(filename).to.not.contain(video.uuid) | ||
566 | |||
567 | const segmentPath = servers[0].servers.buildDirectory(join('streaming-playlists', 'hls', video.uuid, filename)) | ||
568 | |||
569 | const probe = await ffprobePromise(segmentPath) | ||
570 | const videoStream = await getVideoStream(segmentPath, probe) | ||
571 | |||
572 | expect(probe.format.bit_rate).to.be.below(maxBitrateLimits[videoStream.height]) | ||
573 | expect(probe.format.bit_rate).to.be.at.least(minBitrateLimits[videoStream.height]) | ||
574 | |||
575 | await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
576 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
577 | } | ||
578 | } | ||
579 | }) | ||
580 | |||
581 | it('Should not generate an upper resolution than original file', async function () { | ||
582 | this.timeout(500_000) | ||
583 | |||
584 | const resolutions = [ 240, 480 ] | ||
585 | await updateConf(resolutions) | ||
586 | |||
587 | await servers[0].config.updateExistingSubConfig({ | ||
588 | newConfig: { | ||
589 | live: { | ||
590 | transcoding: { | ||
591 | alwaysTranscodeOriginalResolution: false | ||
592 | } | ||
593 | } | ||
594 | } | ||
595 | }) | ||
596 | |||
597 | liveVideoId = await createLiveWrapper(true) | ||
598 | |||
599 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' }) | ||
600 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
601 | await waitJobs(servers) | ||
602 | |||
603 | await testLiveVideoResolutions({ | ||
604 | originServer: servers[0], | ||
605 | sqlCommand: sqlCommandServer1, | ||
606 | servers, | ||
607 | liveVideoId, | ||
608 | resolutions, | ||
609 | transcoded: true | ||
610 | }) | ||
611 | |||
612 | await stopFfmpeg(ffmpegCommand) | ||
613 | await commands[0].waitUntilEnded({ videoId: liveVideoId }) | ||
614 | |||
615 | await waitJobs(servers) | ||
616 | |||
617 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
618 | |||
619 | const video = await servers[0].videos.get({ id: liveVideoId }) | ||
620 | const hlsFiles = video.streamingPlaylists[0].files | ||
621 | |||
622 | expect(video.files).to.have.lengthOf(0) | ||
623 | expect(hlsFiles).to.have.lengthOf(resolutions.length) | ||
624 | |||
625 | // eslint-disable-next-line @typescript-eslint/require-array-sort-compare | ||
626 | expect(getAllFiles(video).map(f => f.resolution.id).sort()).to.deep.equal(resolutions) | ||
627 | }) | ||
628 | |||
629 | it('Should only keep the original resolution if all resolutions are disabled', async function () { | ||
630 | this.timeout(600_000) | ||
631 | |||
632 | await updateConf([]) | ||
633 | liveVideoId = await createLiveWrapper(true) | ||
634 | |||
635 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' }) | ||
636 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
637 | await waitJobs(servers) | ||
638 | |||
639 | await testLiveVideoResolutions({ | ||
640 | originServer: servers[0], | ||
641 | sqlCommand: sqlCommandServer1, | ||
642 | servers, | ||
643 | liveVideoId, | ||
644 | resolutions: [ 720 ], | ||
645 | transcoded: true | ||
646 | }) | ||
647 | |||
648 | await stopFfmpeg(ffmpegCommand) | ||
649 | await commands[0].waitUntilEnded({ videoId: liveVideoId }) | ||
650 | |||
651 | await waitJobs(servers) | ||
652 | |||
653 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
654 | |||
655 | const video = await servers[0].videos.get({ id: liveVideoId }) | ||
656 | const hlsFiles = video.streamingPlaylists[0].files | ||
657 | |||
658 | expect(video.files).to.have.lengthOf(0) | ||
659 | expect(hlsFiles).to.have.lengthOf(1) | ||
660 | |||
661 | expect(hlsFiles[0].resolution.id).to.equal(720) | ||
662 | }) | ||
663 | |||
664 | after(async function () { | ||
665 | await sqlCommandServer1.cleanup() | ||
666 | }) | ||
667 | }) | ||
668 | |||
669 | describe('After a server restart', function () { | ||
670 | let liveVideoId: string | ||
671 | let liveVideoReplayId: string | ||
672 | let permanentLiveVideoReplayId: string | ||
673 | |||
674 | let permanentLiveReplayName: string | ||
675 | |||
676 | let beforeServerRestart: Date | ||
677 | |||
678 | async function createLiveWrapper (options: { saveReplay: boolean, permanent: boolean }) { | ||
679 | const liveAttributes: LiveVideoCreate = { | ||
680 | name: 'live video', | ||
681 | channelId: servers[0].store.channel.id, | ||
682 | privacy: VideoPrivacy.PUBLIC, | ||
683 | saveReplay: options.saveReplay, | ||
684 | replaySettings: options.saveReplay | ||
685 | ? { privacy: VideoPrivacy.PUBLIC } | ||
686 | : undefined, | ||
687 | permanentLive: options.permanent | ||
688 | } | ||
689 | |||
690 | const { uuid } = await commands[0].create({ fields: liveAttributes }) | ||
691 | return uuid | ||
692 | } | ||
693 | |||
694 | before(async function () { | ||
695 | this.timeout(600_000) | ||
696 | |||
697 | liveVideoId = await createLiveWrapper({ saveReplay: false, permanent: false }) | ||
698 | liveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: false }) | ||
699 | permanentLiveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: true }) | ||
700 | |||
701 | await Promise.all([ | ||
702 | commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId }), | ||
703 | commands[0].sendRTMPStreamInVideo({ videoId: permanentLiveVideoReplayId }), | ||
704 | commands[0].sendRTMPStreamInVideo({ videoId: liveVideoReplayId }) | ||
705 | ]) | ||
706 | |||
707 | await Promise.all([ | ||
708 | commands[0].waitUntilPublished({ videoId: liveVideoId }), | ||
709 | commands[0].waitUntilPublished({ videoId: permanentLiveVideoReplayId }), | ||
710 | commands[0].waitUntilPublished({ videoId: liveVideoReplayId }) | ||
711 | ]) | ||
712 | |||
713 | for (const videoUUID of [ liveVideoId, liveVideoReplayId, permanentLiveVideoReplayId ]) { | ||
714 | await commands[0].waitUntilSegmentGeneration({ | ||
715 | server: servers[0], | ||
716 | videoUUID, | ||
717 | playlistNumber: 0, | ||
718 | segment: 2 | ||
719 | }) | ||
720 | } | ||
721 | |||
722 | { | ||
723 | const video = await servers[0].videos.get({ id: permanentLiveVideoReplayId }) | ||
724 | permanentLiveReplayName = video.name + ' - ' + new Date(video.publishedAt).toLocaleString() | ||
725 | } | ||
726 | |||
727 | await killallServers([ servers[0] ]) | ||
728 | |||
729 | beforeServerRestart = new Date() | ||
730 | await servers[0].run() | ||
731 | |||
732 | await wait(5000) | ||
733 | await waitJobs(servers) | ||
734 | }) | ||
735 | |||
736 | it('Should cleanup lives', async function () { | ||
737 | this.timeout(60000) | ||
738 | |||
739 | await commands[0].waitUntilEnded({ videoId: liveVideoId }) | ||
740 | await commands[0].waitUntilWaiting({ videoId: permanentLiveVideoReplayId }) | ||
741 | }) | ||
742 | |||
743 | it('Should save a non permanent live replay', async function () { | ||
744 | this.timeout(240000) | ||
745 | |||
746 | await commands[0].waitUntilPublished({ videoId: liveVideoReplayId }) | ||
747 | |||
748 | const session = await commands[0].getReplaySession({ videoId: liveVideoReplayId }) | ||
749 | expect(session.endDate).to.exist | ||
750 | expect(new Date(session.endDate)).to.be.above(beforeServerRestart) | ||
751 | }) | ||
752 | |||
753 | it('Should have saved a permanent live replay', async function () { | ||
754 | this.timeout(120000) | ||
755 | |||
756 | const { data } = await servers[0].videos.listMyVideos({ sort: '-publishedAt' }) | ||
757 | expect(data.find(v => v.name === permanentLiveReplayName)).to.exist | ||
758 | }) | ||
759 | }) | ||
760 | |||
761 | after(async function () { | ||
762 | await cleanupTests(servers) | ||
763 | }) | ||
764 | }) | ||