]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/tests/api/runners/runner-vod-transcoding.ts
43e005bbee98d15ce529964029183b8ac32a9bda
[github/Chocobozzz/PeerTube.git] / server / tests / api / runners / runner-vod-transcoding.ts
1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3 import { expect } from 'chai'
4 import { readFile } from 'fs-extra'
5 import { completeCheckHlsPlaylist } from '@server/tests/shared'
6 import { buildAbsoluteFixturePath } from '@shared/core-utils'
7 import {
8 HttpStatusCode,
9 RunnerJobSuccessPayload,
10 RunnerJobVODAudioMergeTranscodingPayload,
11 RunnerJobVODHLSTranscodingPayload,
12 RunnerJobVODPayload,
13 RunnerJobVODWebVideoTranscodingPayload,
14 VideoState,
15 VODAudioMergeTranscodingSuccess,
16 VODHLSTranscodingSuccess,
17 VODWebVideoTranscodingSuccess
18 } from '@shared/models'
19 import {
20 cleanupTests,
21 createMultipleServers,
22 doubleFollow,
23 makeGetRequest,
24 makeRawRequest,
25 PeerTubeServer,
26 setAccessTokensToServers,
27 setDefaultVideoChannel,
28 waitJobs
29 } from '@shared/server-commands'
30
31 async function processAllJobs (server: PeerTubeServer, runnerToken: string) {
32 do {
33 const { availableJobs } = await server.runnerJobs.requestVOD({ runnerToken })
34 if (availableJobs.length === 0) break
35
36 const { job } = await server.runnerJobs.accept<RunnerJobVODPayload>({ runnerToken, jobUUID: availableJobs[0].uuid })
37
38 const payload: RunnerJobSuccessPayload = {
39 videoFile: `video_short_${job.payload.output.resolution}p.mp4`,
40 resolutionPlaylistFile: `video_short_${job.payload.output.resolution}p.m3u8`
41 }
42 await server.runnerJobs.success({ runnerToken, jobUUID: job.uuid, jobToken: job.jobToken, payload })
43 } while (true)
44
45 await waitJobs([ server ])
46 }
47
48 describe('Test runner VOD transcoding', function () {
49 let servers: PeerTubeServer[] = []
50 let runnerToken: string
51
52 before(async function () {
53 this.timeout(120_000)
54
55 servers = await createMultipleServers(2)
56
57 await setAccessTokensToServers(servers)
58 await setDefaultVideoChannel(servers)
59
60 await doubleFollow(servers[0], servers[1])
61
62 await servers[0].config.enableRemoteTranscoding()
63 runnerToken = await servers[0].runners.autoRegisterRunner()
64 })
65
66 describe('Without transcoding', function () {
67
68 before(async function () {
69 this.timeout(60000)
70
71 await servers[0].config.disableTranscoding()
72 await servers[0].videos.quickUpload({ name: 'video' })
73
74 await waitJobs(servers)
75 })
76
77 it('Should not have available jobs', async function () {
78 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
79 expect(availableJobs).to.have.lengthOf(0)
80 })
81 })
82
83 describe('With classic transcoding enabled', function () {
84
85 before(async function () {
86 this.timeout(60000)
87
88 await servers[0].config.enableTranscoding(true, true)
89 })
90
91 it('Should error a transcoding job', async function () {
92 this.timeout(60000)
93
94 await servers[0].runnerJobs.cancelAllJobs()
95 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
96 await waitJobs(servers)
97
98 const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
99 const jobUUID = availableJobs[0].uuid
100
101 const { job } = await servers[0].runnerJobs.accept({ runnerToken, jobUUID })
102 const jobToken = job.jobToken
103
104 await servers[0].runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
105
106 const video = await servers[0].videos.get({ id: uuid })
107 expect(video.state.id).to.equal(VideoState.TRANSCODING_FAILED)
108 })
109
110 it('Should cancel a transcoding job', async function () {
111 await servers[0].runnerJobs.cancelAllJobs()
112 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
113 await waitJobs(servers)
114
115 const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
116 const jobUUID = availableJobs[0].uuid
117
118 await servers[0].runnerJobs.cancelByAdmin({ jobUUID })
119
120 const video = await servers[0].videos.get({ id: uuid })
121 expect(video.state.id).to.equal(VideoState.PUBLISHED)
122 })
123 })
124
125 describe('Web video transcoding only', function () {
126 let videoUUID: string
127 let jobToken: string
128 let jobUUID: string
129
130 before(async function () {
131 this.timeout(60000)
132
133 await servers[0].runnerJobs.cancelAllJobs()
134 await servers[0].config.enableTranscoding(true, false)
135
136 const { uuid } = await servers[0].videos.quickUpload({ name: 'web video', fixture: 'video_short.webm' })
137 videoUUID = uuid
138
139 await waitJobs(servers)
140 })
141
142 it('Should have jobs available for remote runners', async function () {
143 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
144 expect(availableJobs).to.have.lengthOf(1)
145
146 jobUUID = availableJobs[0].uuid
147 })
148
149 it('Should have a valid first transcoding job', async function () {
150 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
151 jobToken = job.jobToken
152
153 expect(job.type === 'vod-web-video-transcoding')
154 expect(job.payload.input.videoFileUrl).to.exist
155 expect(job.payload.output.resolution).to.equal(720)
156 expect(job.payload.output.fps).to.equal(25)
157
158 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
159 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.webm'))
160
161 expect(body).to.deep.equal(inputFile)
162 })
163
164 it('Should transcode the max video resolution and send it back to the server', async function () {
165 this.timeout(60000)
166
167 const payload: VODWebVideoTranscodingSuccess = {
168 videoFile: 'video_short.mp4'
169 }
170 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
171
172 await waitJobs(servers)
173 })
174
175 it('Should have the video updated', async function () {
176 for (const server of servers) {
177 const video = await server.videos.get({ id: videoUUID })
178 expect(video.files).to.have.lengthOf(1)
179 expect(video.streamingPlaylists).to.have.lengthOf(0)
180
181 const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
182 expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short.mp4')))
183 }
184 })
185
186 it('Should have 4 lower resolution to transcode', async function () {
187 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
188 expect(availableJobs).to.have.lengthOf(4)
189
190 for (const resolution of [ 480, 360, 240, 144 ]) {
191 const job = availableJobs.find(j => j.payload.output.resolution === resolution)
192 expect(job).to.exist
193 expect(job.type).to.equal('vod-web-video-transcoding')
194
195 if (resolution === 240) jobUUID = job.uuid
196 }
197 })
198
199 it('Should process one of these transcoding jobs', async function () {
200 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
201 jobToken = job.jobToken
202
203 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
204 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
205
206 expect(body).to.deep.equal(inputFile)
207
208 const payload: VODWebVideoTranscodingSuccess = { videoFile: `video_short_${job.payload.output.resolution}p.mp4` }
209 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
210 })
211
212 it('Should process all other jobs', async function () {
213 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
214 expect(availableJobs).to.have.lengthOf(3)
215
216 for (const resolution of [ 480, 360, 144 ]) {
217 const availableJob = availableJobs.find(j => j.payload.output.resolution === resolution)
218 expect(availableJob).to.exist
219 jobUUID = availableJob.uuid
220
221 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
222 jobToken = job.jobToken
223
224 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
225 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
226 expect(body).to.deep.equal(inputFile)
227
228 const payload: VODWebVideoTranscodingSuccess = { videoFile: `video_short_${resolution}p.mp4` }
229 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
230 }
231
232 await waitJobs(servers)
233 })
234
235 it('Should have the video updated', async function () {
236 for (const server of servers) {
237 const video = await server.videos.get({ id: videoUUID })
238 expect(video.files).to.have.lengthOf(5)
239 expect(video.streamingPlaylists).to.have.lengthOf(0)
240
241 const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
242 expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short.mp4')))
243
244 for (const file of video.files) {
245 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
246 await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 })
247 }
248 }
249 })
250
251 it('Should not have available jobs anymore', async function () {
252 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
253 expect(availableJobs).to.have.lengthOf(0)
254 })
255 })
256
257 describe('HLS transcoding only', function () {
258 let videoUUID: string
259 let jobToken: string
260 let jobUUID: string
261
262 before(async function () {
263 this.timeout(60000)
264
265 await servers[0].config.enableTranscoding(false, true)
266
267 const { uuid } = await servers[0].videos.quickUpload({ name: 'hls video', fixture: 'video_short.webm' })
268 videoUUID = uuid
269
270 await waitJobs(servers)
271 })
272
273 it('Should run the optimize job', async function () {
274 this.timeout(60000)
275
276 await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
277 })
278
279 it('Should have 5 HLS resolution to transcode', async function () {
280 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
281 expect(availableJobs).to.have.lengthOf(5)
282
283 for (const resolution of [ 720, 480, 360, 240, 144 ]) {
284 const job = availableJobs.find(j => j.payload.output.resolution === resolution)
285 expect(job).to.exist
286 expect(job.type).to.equal('vod-hls-transcoding')
287
288 if (resolution === 480) jobUUID = job.uuid
289 }
290 })
291
292 it('Should process one of these transcoding jobs', async function () {
293 this.timeout(60000)
294
295 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
296 jobToken = job.jobToken
297
298 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
299 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
300
301 expect(body).to.deep.equal(inputFile)
302
303 const payload: VODHLSTranscodingSuccess = {
304 videoFile: 'video_short_480p.mp4',
305 resolutionPlaylistFile: 'video_short_480p.m3u8'
306 }
307 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
308
309 await waitJobs(servers)
310 })
311
312 it('Should have the video updated', async function () {
313 for (const server of servers) {
314 const video = await server.videos.get({ id: videoUUID })
315
316 expect(video.files).to.have.lengthOf(1)
317 expect(video.streamingPlaylists).to.have.lengthOf(1)
318
319 const hls = video.streamingPlaylists[0]
320 expect(hls.files).to.have.lengthOf(1)
321
322 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
323 }
324 })
325
326 it('Should process all other jobs', async function () {
327 this.timeout(60000)
328
329 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
330 expect(availableJobs).to.have.lengthOf(4)
331
332 let maxQualityFile = 'video_short.mp4'
333
334 for (const resolution of [ 720, 360, 240, 144 ]) {
335 const availableJob = availableJobs.find(j => j.payload.output.resolution === resolution)
336 expect(availableJob).to.exist
337 jobUUID = availableJob.uuid
338
339 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
340 jobToken = job.jobToken
341
342 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
343 const inputFile = await readFile(buildAbsoluteFixturePath(maxQualityFile))
344 expect(body).to.deep.equal(inputFile)
345
346 const payload: VODHLSTranscodingSuccess = {
347 videoFile: `video_short_${resolution}p.mp4`,
348 resolutionPlaylistFile: `video_short_${resolution}p.m3u8`
349 }
350 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
351
352 if (resolution === 720) {
353 maxQualityFile = 'video_short_720p.mp4'
354 }
355 }
356
357 await waitJobs(servers)
358 })
359
360 it('Should have the video updated', async function () {
361 for (const server of servers) {
362 const video = await server.videos.get({ id: videoUUID })
363
364 expect(video.files).to.have.lengthOf(0)
365 expect(video.streamingPlaylists).to.have.lengthOf(1)
366
367 const hls = video.streamingPlaylists[0]
368 expect(hls.files).to.have.lengthOf(5)
369
370 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: true, servers, resolutions: [ 720, 480, 360, 240, 144 ] })
371 }
372 })
373
374 it('Should not have available jobs anymore', async function () {
375 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
376 expect(availableJobs).to.have.lengthOf(0)
377 })
378 })
379
380 describe('Web video and HLS transcoding', function () {
381
382 before(async function () {
383 this.timeout(60000)
384
385 await servers[0].config.enableTranscoding(true, true)
386
387 await servers[0].videos.quickUpload({ name: 'web video and hls video', fixture: 'video_short.webm' })
388
389 await waitJobs(servers)
390 })
391
392 it('Should process the first optimize job', async function () {
393 this.timeout(60000)
394
395 await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
396 })
397
398 it('Should have 9 jobs to process', async function () {
399 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
400
401 expect(availableJobs).to.have.lengthOf(9)
402
403 const webVideoJobs = availableJobs.filter(j => j.type === 'vod-web-video-transcoding')
404 const hlsJobs = availableJobs.filter(j => j.type === 'vod-hls-transcoding')
405
406 expect(webVideoJobs).to.have.lengthOf(4)
407 expect(hlsJobs).to.have.lengthOf(5)
408 })
409
410 it('Should process all available jobs', async function () {
411 await processAllJobs(servers[0], runnerToken)
412 })
413 })
414
415 describe('Audio merge transcoding', function () {
416 let videoUUID: string
417 let jobToken: string
418 let jobUUID: string
419
420 before(async function () {
421 this.timeout(60000)
422
423 await servers[0].config.enableTranscoding(true, true)
424
425 const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
426 const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' })
427 videoUUID = uuid
428
429 await waitJobs(servers)
430 })
431
432 it('Should have an audio merge transcoding job', async function () {
433 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
434 expect(availableJobs).to.have.lengthOf(1)
435
436 expect(availableJobs[0].type).to.equal('vod-audio-merge-transcoding')
437
438 jobUUID = availableJobs[0].uuid
439 })
440
441 it('Should have a valid remote audio merge transcoding job', async function () {
442 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODAudioMergeTranscodingPayload>({ runnerToken, jobUUID })
443 jobToken = job.jobToken
444
445 expect(job.type === 'vod-audio-merge-transcoding')
446 expect(job.payload.input.audioFileUrl).to.exist
447 expect(job.payload.input.previewFileUrl).to.exist
448 expect(job.payload.output.resolution).to.equal(480)
449
450 {
451 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.audioFileUrl, jobToken, runnerToken })
452 const inputFile = await readFile(buildAbsoluteFixturePath('sample.ogg'))
453 expect(body).to.deep.equal(inputFile)
454 }
455
456 {
457 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.previewFileUrl, jobToken, runnerToken })
458
459 const video = await servers[0].videos.get({ id: videoUUID })
460 const { body: inputFile } = await makeGetRequest({
461 url: servers[0].url,
462 path: video.previewPath,
463 expectedStatus: HttpStatusCode.OK_200
464 })
465
466 expect(body).to.deep.equal(inputFile)
467 }
468 })
469
470 it('Should merge the audio', async function () {
471 this.timeout(60000)
472
473 const payload: VODAudioMergeTranscodingSuccess = { videoFile: 'video_short_480p.mp4' }
474 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
475
476 await waitJobs(servers)
477 })
478
479 it('Should have the video updated', async function () {
480 for (const server of servers) {
481 const video = await server.videos.get({ id: videoUUID })
482 expect(video.files).to.have.lengthOf(1)
483 expect(video.streamingPlaylists).to.have.lengthOf(0)
484
485 const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
486 expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short_480p.mp4')))
487 }
488 })
489
490 it('Should have 7 lower resolutions to transcode', async function () {
491 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
492 expect(availableJobs).to.have.lengthOf(7)
493
494 for (const resolution of [ 360, 240, 144 ]) {
495 const jobs = availableJobs.filter(j => j.payload.output.resolution === resolution)
496 expect(jobs).to.have.lengthOf(2)
497 }
498
499 jobUUID = availableJobs.find(j => j.payload.output.resolution === 480).uuid
500 })
501
502 it('Should process one other job', async function () {
503 this.timeout(60000)
504
505 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
506 jobToken = job.jobToken
507
508 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
509 const inputFile = await readFile(buildAbsoluteFixturePath('video_short_480p.mp4'))
510 expect(body).to.deep.equal(inputFile)
511
512 const payload: VODHLSTranscodingSuccess = {
513 videoFile: `video_short_480p.mp4`,
514 resolutionPlaylistFile: `video_short_480p.m3u8`
515 }
516 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
517
518 await waitJobs(servers)
519 })
520
521 it('Should have the video updated', async function () {
522 for (const server of servers) {
523 const video = await server.videos.get({ id: videoUUID })
524
525 expect(video.files).to.have.lengthOf(1)
526 expect(video.streamingPlaylists).to.have.lengthOf(1)
527
528 const hls = video.streamingPlaylists[0]
529 expect(hls.files).to.have.lengthOf(1)
530
531 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
532 }
533 })
534
535 it('Should process all available jobs', async function () {
536 await processAllJobs(servers[0], runnerToken)
537 })
538 })
539
540 after(async function () {
541 await cleanupTests(servers)
542 })
543 })