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