]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/tests/api/runners/runner-vod-transcoding.ts
Support studio transcoding in peertube runner
[github/Chocobozzz/PeerTube.git] / server / tests / api / runners / runner-vod-transcoding.ts
CommitLineData
d102de1b
C
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { readFile } from 'fs-extra'
5import { completeCheckHlsPlaylist } from '@server/tests/shared'
6import { buildAbsoluteFixturePath } from '@shared/core-utils'
7import {
8 HttpStatusCode,
9 RunnerJobSuccessPayload,
10 RunnerJobVODAudioMergeTranscodingPayload,
11 RunnerJobVODHLSTranscodingPayload,
12 RunnerJobVODPayload,
13 RunnerJobVODWebVideoTranscodingPayload,
14 VideoState,
15 VODAudioMergeTranscodingSuccess,
16 VODHLSTranscodingSuccess,
17 VODWebVideoTranscodingSuccess
18} from '@shared/models'
19import {
20 cleanupTests,
21 createMultipleServers,
22 doubleFollow,
23 makeGetRequest,
24 makeRawRequest,
25 PeerTubeServer,
26 setAccessTokensToServers,
27 setDefaultVideoChannel,
28 waitJobs
29} from '@shared/server-commands'
30
31async 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
48describe('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
5e47f6ab 158 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
d102de1b
C
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
5e47f6ab 203 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
d102de1b
C
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_240p.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
5e47f6ab 224 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
d102de1b
C
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
233 it('Should have the video updated', async function () {
234 for (const server of servers) {
235 const video = await server.videos.get({ id: videoUUID })
236 expect(video.files).to.have.lengthOf(5)
237 expect(video.streamingPlaylists).to.have.lengthOf(0)
238
239 const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
240 expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short.mp4')))
241
242 for (const file of video.files) {
243 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
244 await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 })
245 }
246 }
247 })
248
249 it('Should not have available jobs anymore', async function () {
250 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
251 expect(availableJobs).to.have.lengthOf(0)
252 })
253 })
254
255 describe('HLS transcoding only', function () {
256 let videoUUID: string
257 let jobToken: string
258 let jobUUID: string
259
260 before(async function () {
261 this.timeout(60000)
262
263 await servers[0].config.enableTranscoding(false, true)
264
265 const { uuid } = await servers[0].videos.quickUpload({ name: 'hls video', fixture: 'video_short.webm' })
266 videoUUID = uuid
267
268 await waitJobs(servers)
269 })
270
271 it('Should run the optimize job', async function () {
272 this.timeout(60000)
273
274 await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
275 })
276
277 it('Should have 5 HLS resolution to transcode', async function () {
278 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
279 expect(availableJobs).to.have.lengthOf(5)
280
281 for (const resolution of [ 720, 480, 360, 240, 144 ]) {
282 const job = availableJobs.find(j => j.payload.output.resolution === resolution)
283 expect(job).to.exist
284 expect(job.type).to.equal('vod-hls-transcoding')
285
286 if (resolution === 480) jobUUID = job.uuid
287 }
288 })
289
290 it('Should process one of these transcoding jobs', async function () {
291 this.timeout(60000)
292
293 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
294 jobToken = job.jobToken
295
5e47f6ab 296 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
d102de1b
C
297 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
298
299 expect(body).to.deep.equal(inputFile)
300
301 const payload: VODHLSTranscodingSuccess = {
302 videoFile: 'video_short_480p.mp4',
303 resolutionPlaylistFile: 'video_short_480p.m3u8'
304 }
305 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
306
307 await waitJobs(servers)
308 })
309
310 it('Should have the video updated', async function () {
311 for (const server of servers) {
312 const video = await server.videos.get({ id: videoUUID })
313
314 expect(video.files).to.have.lengthOf(1)
315 expect(video.streamingPlaylists).to.have.lengthOf(1)
316
317 const hls = video.streamingPlaylists[0]
318 expect(hls.files).to.have.lengthOf(1)
319
320 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
321 }
322 })
323
324 it('Should process all other jobs', async function () {
325 this.timeout(60000)
326
327 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
328 expect(availableJobs).to.have.lengthOf(4)
329
330 let maxQualityFile = 'video_short.mp4'
331
332 for (const resolution of [ 720, 360, 240, 144 ]) {
333 const availableJob = availableJobs.find(j => j.payload.output.resolution === resolution)
334 expect(availableJob).to.exist
335 jobUUID = availableJob.uuid
336
337 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
338 jobToken = job.jobToken
339
5e47f6ab 340 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
d102de1b
C
341 const inputFile = await readFile(buildAbsoluteFixturePath(maxQualityFile))
342 expect(body).to.deep.equal(inputFile)
343
344 const payload: VODHLSTranscodingSuccess = {
345 videoFile: `video_short_${resolution}p.mp4`,
346 resolutionPlaylistFile: `video_short_${resolution}p.m3u8`
347 }
348 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
349
350 if (resolution === 720) {
351 maxQualityFile = 'video_short_720p.mp4'
352 }
353 }
354
355 await waitJobs(servers)
356 })
357
358 it('Should have the video updated', async function () {
359 for (const server of servers) {
360 const video = await server.videos.get({ id: videoUUID })
361
362 expect(video.files).to.have.lengthOf(0)
363 expect(video.streamingPlaylists).to.have.lengthOf(1)
364
365 const hls = video.streamingPlaylists[0]
366 expect(hls.files).to.have.lengthOf(5)
367
368 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: true, servers, resolutions: [ 720, 480, 360, 240, 144 ] })
369 }
370 })
371
372 it('Should not have available jobs anymore', async function () {
373 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
374 expect(availableJobs).to.have.lengthOf(0)
375 })
376 })
377
378 describe('Web video and HLS transcoding', function () {
379
380 before(async function () {
381 this.timeout(60000)
382
383 await servers[0].config.enableTranscoding(true, true)
384
385 await servers[0].videos.quickUpload({ name: 'web video and hls video', fixture: 'video_short.webm' })
386
387 await waitJobs(servers)
388 })
389
390 it('Should process the first optimize job', async function () {
391 this.timeout(60000)
392
393 await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
394 })
395
396 it('Should have 9 jobs to process', async function () {
397 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
398
399 expect(availableJobs).to.have.lengthOf(9)
400
401 const webVideoJobs = availableJobs.filter(j => j.type === 'vod-web-video-transcoding')
402 const hlsJobs = availableJobs.filter(j => j.type === 'vod-hls-transcoding')
403
404 expect(webVideoJobs).to.have.lengthOf(4)
405 expect(hlsJobs).to.have.lengthOf(5)
406 })
407
408 it('Should process all available jobs', async function () {
409 await processAllJobs(servers[0], runnerToken)
410 })
411 })
412
413 describe('Audio merge transcoding', function () {
414 let videoUUID: string
415 let jobToken: string
416 let jobUUID: string
417
418 before(async function () {
419 this.timeout(60000)
420
421 await servers[0].config.enableTranscoding(true, true)
422
423 const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
424 const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' })
425 videoUUID = uuid
426
427 await waitJobs(servers)
428 })
429
430 it('Should have an audio merge transcoding job', async function () {
431 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
432 expect(availableJobs).to.have.lengthOf(1)
433
434 expect(availableJobs[0].type).to.equal('vod-audio-merge-transcoding')
435
436 jobUUID = availableJobs[0].uuid
437 })
438
439 it('Should have a valid remote audio merge transcoding job', async function () {
440 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODAudioMergeTranscodingPayload>({ runnerToken, jobUUID })
441 jobToken = job.jobToken
442
443 expect(job.type === 'vod-audio-merge-transcoding')
444 expect(job.payload.input.audioFileUrl).to.exist
445 expect(job.payload.input.previewFileUrl).to.exist
446 expect(job.payload.output.resolution).to.equal(480)
447
448 {
5e47f6ab 449 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.audioFileUrl, jobToken, runnerToken })
d102de1b
C
450 const inputFile = await readFile(buildAbsoluteFixturePath('sample.ogg'))
451 expect(body).to.deep.equal(inputFile)
452 }
453
454 {
5e47f6ab 455 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.previewFileUrl, jobToken, runnerToken })
d102de1b
C
456
457 const video = await servers[0].videos.get({ id: videoUUID })
458 const { body: inputFile } = await makeGetRequest({
459 url: servers[0].url,
460 path: video.previewPath,
461 expectedStatus: HttpStatusCode.OK_200
462 })
463
464 expect(body).to.deep.equal(inputFile)
465 }
466 })
467
468 it('Should merge the audio', async function () {
469 this.timeout(60000)
470
471 const payload: VODAudioMergeTranscodingSuccess = { videoFile: 'video_short_480p.mp4' }
472 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
473
474 await waitJobs(servers)
475 })
476
477 it('Should have the video updated', async function () {
478 for (const server of servers) {
479 const video = await server.videos.get({ id: videoUUID })
480 expect(video.files).to.have.lengthOf(1)
481 expect(video.streamingPlaylists).to.have.lengthOf(0)
482
483 const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
484 expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short_480p.mp4')))
485 }
486 })
487
488 it('Should have 7 lower resolutions to transcode', async function () {
489 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
490 expect(availableJobs).to.have.lengthOf(7)
491
492 for (const resolution of [ 360, 240, 144 ]) {
493 const jobs = availableJobs.filter(j => j.payload.output.resolution === resolution)
494 expect(jobs).to.have.lengthOf(2)
495 }
496
497 jobUUID = availableJobs.find(j => j.payload.output.resolution === 480).uuid
498 })
499
500 it('Should process one other job', async function () {
501 this.timeout(60000)
502
503 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
504 jobToken = job.jobToken
505
5e47f6ab 506 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
d102de1b
C
507 const inputFile = await readFile(buildAbsoluteFixturePath('video_short_480p.mp4'))
508 expect(body).to.deep.equal(inputFile)
509
510 const payload: VODHLSTranscodingSuccess = {
511 videoFile: `video_short_480p.mp4`,
512 resolutionPlaylistFile: `video_short_480p.m3u8`
513 }
514 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
515
516 await waitJobs(servers)
517 })
518
519 it('Should have the video updated', async function () {
520 for (const server of servers) {
521 const video = await server.videos.get({ id: videoUUID })
522
523 expect(video.files).to.have.lengthOf(1)
524 expect(video.streamingPlaylists).to.have.lengthOf(1)
525
526 const hls = video.streamingPlaylists[0]
527 expect(hls.files).to.have.lengthOf(1)
528
529 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
530 }
531 })
532
533 it('Should process all available jobs', async function () {
534 await processAllJobs(servers[0], runnerToken)
535 })
536 })
537
538 after(async function () {
539 await cleanupTests(servers)
540 })
541})