aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/tests/api/runners
diff options
context:
space:
mode:
Diffstat (limited to 'server/tests/api/runners')
-rw-r--r--server/tests/api/runners/index.ts4
-rw-r--r--server/tests/api/runners/runner-common.ts662
-rw-r--r--server/tests/api/runners/runner-live-transcoding.ts330
-rw-r--r--server/tests/api/runners/runner-socket.ts116
-rw-r--r--server/tests/api/runners/runner-vod-transcoding.ts541
5 files changed, 1653 insertions, 0 deletions
diff --git a/server/tests/api/runners/index.ts b/server/tests/api/runners/index.ts
new file mode 100644
index 000000000..7f33ec8dd
--- /dev/null
+++ b/server/tests/api/runners/index.ts
@@ -0,0 +1,4 @@
1export * from './runner-common'
2export * from './runner-live-transcoding'
3export * from './runner-socket'
4export * from './runner-vod-transcoding'
diff --git a/server/tests/api/runners/runner-common.ts b/server/tests/api/runners/runner-common.ts
new file mode 100644
index 000000000..a2204753b
--- /dev/null
+++ b/server/tests/api/runners/runner-common.ts
@@ -0,0 +1,662 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@shared/core-utils'
5import { HttpStatusCode, Runner, RunnerJob, RunnerJobAdmin, RunnerJobState, RunnerRegistrationToken } from '@shared/models'
6import {
7 cleanupTests,
8 createSingleServer,
9 makePostBodyRequest,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultVideoChannel,
13 waitJobs
14} from '@shared/server-commands'
15
16describe('Test runner common actions', function () {
17 let server: PeerTubeServer
18 let registrationToken: string
19 let runnerToken: string
20 let jobMaxPriority: string
21
22 before(async function () {
23 this.timeout(120_000)
24
25 server = await createSingleServer(1, {
26 remote_runners: {
27 stalled_jobs: {
28 vod: '5 seconds'
29 }
30 }
31 })
32
33 await setAccessTokensToServers([ server ])
34 await setDefaultVideoChannel([ server ])
35
36 await server.config.enableTranscoding(true, true)
37 await server.config.enableRemoteTranscoding()
38 })
39
40 describe('Managing runner registration tokens', function () {
41 let base: RunnerRegistrationToken[]
42 let registrationTokenToDelete: RunnerRegistrationToken
43
44 it('Should have a default registration token', async function () {
45 const { total, data } = await server.runnerRegistrationTokens.list()
46
47 expect(total).to.equal(1)
48 expect(data).to.have.lengthOf(1)
49
50 const token = data[0]
51 expect(token.id).to.exist
52 expect(token.createdAt).to.exist
53 expect(token.updatedAt).to.exist
54 expect(token.registeredRunnersCount).to.equal(0)
55 expect(token.registrationToken).to.exist
56 })
57
58 it('Should create other registration tokens', async function () {
59 await server.runnerRegistrationTokens.generate()
60 await server.runnerRegistrationTokens.generate()
61
62 const { total, data } = await server.runnerRegistrationTokens.list()
63 expect(total).to.equal(3)
64 expect(data).to.have.lengthOf(3)
65 })
66
67 it('Should list registration tokens', async function () {
68 {
69 const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' })
70 expect(total).to.equal(3)
71 expect(data).to.have.lengthOf(3)
72 expect(new Date(data[0].createdAt)).to.be.below(new Date(data[1].createdAt))
73 expect(new Date(data[1].createdAt)).to.be.below(new Date(data[2].createdAt))
74
75 base = data
76
77 registrationTokenToDelete = data[0]
78 registrationToken = data[1].registrationToken
79 }
80
81 {
82 const { total, data } = await server.runnerRegistrationTokens.list({ sort: '-createdAt', start: 2, count: 1 })
83 expect(total).to.equal(3)
84 expect(data).to.have.lengthOf(1)
85 expect(data[0].registrationToken).to.equal(base[0].registrationToken)
86 }
87 })
88
89 it('Should have appropriate registeredRunnersCount for registration tokens', async function () {
90 await server.runners.register({ name: 'to delete 1', registrationToken: registrationTokenToDelete.registrationToken })
91 await server.runners.register({ name: 'to delete 2', registrationToken: registrationTokenToDelete.registrationToken })
92
93 const { data } = await server.runnerRegistrationTokens.list()
94
95 for (const d of data) {
96 if (d.registrationToken === registrationTokenToDelete.registrationToken) {
97 expect(d.registeredRunnersCount).to.equal(2)
98 } else {
99 expect(d.registeredRunnersCount).to.equal(0)
100 }
101 }
102
103 const { data: runners } = await server.runners.list()
104 expect(runners).to.have.lengthOf(2)
105 })
106
107 it('Should delete a registration token', async function () {
108 await server.runnerRegistrationTokens.delete({ id: registrationTokenToDelete.id })
109
110 const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' })
111 expect(total).to.equal(2)
112 expect(data).to.have.lengthOf(2)
113
114 for (const d of data) {
115 expect(d.registeredRunnersCount).to.equal(0)
116 expect(d.registrationToken).to.not.equal(registrationTokenToDelete.registrationToken)
117 }
118 })
119
120 it('Should have removed runners of this registration token', async function () {
121 const { data: runners } = await server.runners.list()
122 expect(runners).to.have.lengthOf(0)
123 })
124 })
125
126 describe('Managing runners', function () {
127 let toDelete: Runner
128
129 it('Should not have runners available', async function () {
130 const { total, data } = await server.runners.list()
131
132 expect(data).to.have.lengthOf(0)
133 expect(total).to.equal(0)
134 })
135
136 it('Should register runners', async function () {
137 const now = new Date()
138
139 const result = await server.runners.register({
140 name: 'runner 1',
141 description: 'my super runner 1',
142 registrationToken
143 })
144 expect(result.runnerToken).to.exist
145 runnerToken = result.runnerToken
146
147 await server.runners.register({
148 name: 'runner 2',
149 registrationToken
150 })
151
152 const { total, data } = await server.runners.list({ sort: 'createdAt' })
153 expect(total).to.equal(2)
154 expect(data).to.have.lengthOf(2)
155
156 for (const d of data) {
157 expect(d.id).to.exist
158 expect(d.createdAt).to.exist
159 expect(d.updatedAt).to.exist
160 expect(new Date(d.createdAt)).to.be.above(now)
161 expect(new Date(d.updatedAt)).to.be.above(now)
162 expect(new Date(d.lastContact)).to.be.above(now)
163 expect(d.ip).to.exist
164 }
165
166 expect(data[0].name).to.equal('runner 1')
167 expect(data[0].description).to.equal('my super runner 1')
168
169 expect(data[1].name).to.equal('runner 2')
170 expect(data[1].description).to.be.null
171
172 toDelete = data[1]
173 })
174
175 it('Should list runners', async function () {
176 const { total, data } = await server.runners.list({ sort: '-createdAt', start: 1, count: 1 })
177
178 expect(total).to.equal(2)
179 expect(data).to.have.lengthOf(1)
180 expect(data[0].name).to.equal('runner 1')
181 })
182
183 it('Should delete a runner', async function () {
184 await server.runners.delete({ id: toDelete.id })
185
186 const { total, data } = await server.runners.list()
187
188 expect(total).to.equal(1)
189 expect(data).to.have.lengthOf(1)
190 expect(data[0].name).to.equal('runner 1')
191 })
192
193 it('Should unregister a runner', async function () {
194 const registered = await server.runners.autoRegisterRunner()
195
196 {
197 const { total, data } = await server.runners.list()
198 expect(total).to.equal(2)
199 expect(data).to.have.lengthOf(2)
200 }
201
202 await server.runners.unregister({ runnerToken: registered })
203
204 {
205 const { total, data } = await server.runners.list()
206 expect(total).to.equal(1)
207 expect(data).to.have.lengthOf(1)
208 expect(data[0].name).to.equal('runner 1')
209 }
210 })
211 })
212
213 describe('Managing runner jobs', function () {
214 let jobUUID: string
215 let jobToken: string
216 let lastRunnerContact: Date
217 let failedJob: RunnerJob
218
219 async function checkMainJobState (
220 mainJobState: RunnerJobState,
221 otherJobStates: RunnerJobState[] = [ RunnerJobState.PENDING, RunnerJobState.WAITING_FOR_PARENT_JOB ]
222 ) {
223 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
224
225 for (const job of data) {
226 if (job.uuid === jobUUID) {
227 expect(job.state.id).to.equal(mainJobState)
228 } else {
229 expect(otherJobStates).to.include(job.state.id)
230 }
231 }
232 }
233
234 function getMainJob () {
235 return server.runnerJobs.getJob({ uuid: jobUUID })
236 }
237
238 describe('List jobs', function () {
239
240 it('Should not have jobs', async function () {
241 const { total, data } = await server.runnerJobs.list()
242
243 expect(data).to.have.lengthOf(0)
244 expect(total).to.equal(0)
245 })
246
247 it('Should upload a video and have available jobs', async function () {
248 await server.videos.quickUpload({ name: 'to transcode' })
249 await waitJobs([ server ])
250
251 const { total, data } = await server.runnerJobs.list()
252
253 expect(data).to.have.lengthOf(10)
254 expect(total).to.equal(10)
255
256 for (const job of data) {
257 expect(job.startedAt).to.not.exist
258 expect(job.finishedAt).to.not.exist
259 expect(job.payload).to.exist
260 expect(job.privatePayload).to.exist
261 }
262
263 const hlsJobs = data.filter(d => d.type === 'vod-hls-transcoding')
264 const webVideoJobs = data.filter(d => d.type === 'vod-web-video-transcoding')
265
266 expect(hlsJobs).to.have.lengthOf(5)
267 expect(webVideoJobs).to.have.lengthOf(5)
268
269 const pendingJobs = data.filter(d => d.state.id === RunnerJobState.PENDING)
270 const waitingJobs = data.filter(d => d.state.id === RunnerJobState.WAITING_FOR_PARENT_JOB)
271
272 expect(pendingJobs).to.have.lengthOf(1)
273 expect(waitingJobs).to.have.lengthOf(9)
274 })
275
276 it('Should upload another video and list/sort jobs', async function () {
277 await server.videos.quickUpload({ name: 'to transcode 2' })
278 await waitJobs([ server ])
279
280 {
281 const { total, data } = await server.runnerJobs.list({ start: 0, count: 30 })
282
283 expect(data).to.have.lengthOf(20)
284 expect(total).to.equal(20)
285
286 jobUUID = data[16].uuid
287 }
288
289 {
290 const { total, data } = await server.runnerJobs.list({ start: 3, count: 1, sort: 'createdAt' })
291 expect(total).to.equal(20)
292
293 expect(data).to.have.lengthOf(1)
294 expect(data[0].uuid).to.equal(jobUUID)
295 }
296
297 {
298 let previousPriority = Infinity
299 const { total, data } = await server.runnerJobs.list({ start: 0, count: 100, sort: '-priority' })
300 expect(total).to.equal(20)
301
302 for (const job of data) {
303 expect(job.priority).to.be.at.most(previousPriority)
304 previousPriority = job.priority
305
306 if (job.state.id === RunnerJobState.PENDING) {
307 jobMaxPriority = job.uuid
308 }
309 }
310 }
311 })
312
313 it('Should search jobs', async function () {
314 {
315 const { total, data } = await server.runnerJobs.list({ search: jobUUID })
316
317 expect(data).to.have.lengthOf(1)
318 expect(total).to.equal(1)
319
320 expect(data[0].uuid).to.equal(jobUUID)
321 }
322
323 {
324 const { total, data } = await server.runnerJobs.list({ search: 'toto' })
325
326 expect(data).to.have.lengthOf(0)
327 expect(total).to.equal(0)
328 }
329
330 {
331 const { total, data } = await server.runnerJobs.list({ search: 'hls' })
332
333 expect(data).to.not.have.lengthOf(0)
334 expect(total).to.not.equal(0)
335 }
336 })
337 })
338
339 describe('Accept/update/abort/process a job', function () {
340
341 it('Should request available jobs', async function () {
342 lastRunnerContact = new Date()
343
344 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
345
346 // Only optimize jobs are available
347 expect(availableJobs).to.have.lengthOf(2)
348
349 for (const job of availableJobs) {
350 expect(job.uuid).to.exist
351 expect(job.payload.input).to.exist
352 expect(job.payload.output).to.exist
353
354 expect((job as RunnerJobAdmin).privatePayload).to.not.exist
355 }
356
357 const hlsJobs = availableJobs.filter(d => d.type === 'vod-hls-transcoding')
358 const webVideoJobs = availableJobs.filter(d => d.type === 'vod-web-video-transcoding')
359
360 expect(hlsJobs).to.have.lengthOf(0)
361 expect(webVideoJobs).to.have.lengthOf(2)
362
363 jobUUID = webVideoJobs[0].uuid
364 })
365
366 it('Should have sorted available jobs by priority', async function () {
367 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
368
369 expect(availableJobs[0].uuid).to.equal(jobMaxPriority)
370 })
371
372 it('Should have last runner contact updated', async function () {
373 await wait(1000)
374
375 const { data } = await server.runners.list({ sort: 'createdAt' })
376 expect(new Date(data[0].lastContact)).to.be.above(lastRunnerContact)
377 })
378
379 it('Should accept a job', async function () {
380 const startedAt = new Date()
381
382 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
383 jobToken = job.jobToken
384
385 const checkProcessingJob = (job: RunnerJob & { jobToken?: string }, fromAccept: boolean) => {
386 expect(job.uuid).to.equal(jobUUID)
387
388 expect(job.type).to.equal('vod-web-video-transcoding')
389 expect(job.state.label).to.equal('Processing')
390 expect(job.state.id).to.equal(RunnerJobState.PROCESSING)
391
392 expect(job.runner).to.exist
393 expect(job.runner.name).to.equal('runner 1')
394 expect(job.runner.description).to.equal('my super runner 1')
395
396 expect(job.progress).to.be.null
397
398 expect(job.startedAt).to.exist
399 expect(new Date(job.startedAt)).to.be.above(startedAt)
400
401 expect(job.finishedAt).to.not.exist
402
403 expect(job.failures).to.equal(0)
404
405 expect(job.payload).to.exist
406
407 if (fromAccept) {
408 expect(job.jobToken).to.exist
409 expect((job as RunnerJobAdmin).privatePayload).to.not.exist
410 } else {
411 expect(job.jobToken).to.not.exist
412 expect((job as RunnerJobAdmin).privatePayload).to.exist
413 }
414 }
415
416 checkProcessingJob(job, true)
417
418 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
419
420 const processingJob = data.find(j => j.uuid === jobUUID)
421 checkProcessingJob(processingJob, false)
422
423 await checkMainJobState(RunnerJobState.PROCESSING)
424 })
425
426 it('Should update a job', async function () {
427 await server.runnerJobs.update({ runnerToken, jobUUID, jobToken, progress: 53 })
428
429 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
430
431 for (const job of data) {
432 if (job.state.id === RunnerJobState.PROCESSING) {
433 expect(job.progress).to.equal(53)
434 } else {
435 expect(job.progress).to.be.null
436 }
437 }
438 })
439
440 it('Should abort a job', async function () {
441 await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'for tests' })
442
443 await checkMainJobState(RunnerJobState.PENDING)
444
445 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
446 for (const job of data) {
447 expect(job.progress).to.be.null
448 }
449 })
450
451 it('Should accept the same job again and post a success', async function () {
452 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
453 expect(availableJobs.find(j => j.uuid === jobUUID)).to.exist
454
455 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
456 jobToken = job.jobToken
457
458 await checkMainJobState(RunnerJobState.PROCESSING)
459
460 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
461
462 for (const job of data) {
463 expect(job.progress).to.be.null
464 }
465
466 const payload = {
467 videoFile: 'video_short.mp4'
468 }
469
470 await server.runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
471 })
472
473 it('Should not have available jobs anymore', async function () {
474 await checkMainJobState(RunnerJobState.COMPLETED)
475
476 const job = await getMainJob()
477 expect(job.finishedAt).to.exist
478
479 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
480 expect(availableJobs.find(j => j.uuid === jobUUID)).to.not.exist
481 })
482 })
483
484 describe('Error job', function () {
485
486 it('Should accept another job and post an error', async function () {
487 await server.runnerJobs.cancelAllJobs()
488 await server.videos.quickUpload({ name: 'video' })
489 await waitJobs([ server ])
490
491 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
492 jobUUID = availableJobs[0].uuid
493
494 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
495 jobToken = job.jobToken
496
497 await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
498 })
499
500 it('Should have job failures increased', async function () {
501 const job = await getMainJob()
502 expect(job.state.id).to.equal(RunnerJobState.PENDING)
503 expect(job.failures).to.equal(1)
504 expect(job.error).to.be.null
505 expect(job.progress).to.be.null
506 expect(job.finishedAt).to.not.exist
507 })
508
509 it('Should error a job when job attempts is too big', async function () {
510 for (let i = 0; i < 4; i++) {
511 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
512 jobToken = job.jobToken
513
514 await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error ' + i })
515 }
516
517 const job = await getMainJob()
518 expect(job.failures).to.equal(5)
519 expect(job.state.id).to.equal(RunnerJobState.ERRORED)
520 expect(job.state.label).to.equal('Errored')
521 expect(job.error).to.equal('Error 3')
522 expect(job.progress).to.be.null
523 expect(job.finishedAt).to.exist
524
525 failedJob = job
526 })
527
528 it('Should have failed children jobs too', async function () {
529 const { data } = await server.runnerJobs.list({ count: 50, sort: '-updatedAt' })
530
531 const children = data.filter(j => j.parent?.uuid === failedJob.uuid)
532 expect(children).to.have.lengthOf(9)
533
534 for (const child of children) {
535 expect(child.parent.uuid).to.equal(failedJob.uuid)
536 expect(child.parent.type).to.equal(failedJob.type)
537 expect(child.parent.state.id).to.equal(failedJob.state.id)
538 expect(child.parent.state.label).to.equal(failedJob.state.label)
539
540 expect(child.state.id).to.equal(RunnerJobState.PARENT_ERRORED)
541 expect(child.state.label).to.equal('Parent job failed')
542 }
543 })
544 })
545
546 describe('Cancel', function () {
547
548 it('Should cancel a pending job', async function () {
549 await server.videos.quickUpload({ name: 'video' })
550 await waitJobs([ server ])
551
552 {
553 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
554
555 const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING)
556 jobUUID = pendingJob.uuid
557
558 await server.runnerJobs.cancelByAdmin({ jobUUID })
559 }
560
561 {
562 const job = await getMainJob()
563 expect(job.state.id).to.equal(RunnerJobState.CANCELLED)
564 expect(job.state.label).to.equal('Cancelled')
565 }
566
567 {
568 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
569 const children = data.filter(j => j.parent?.uuid === jobUUID)
570 expect(children).to.have.lengthOf(9)
571
572 for (const child of children) {
573 expect(child.state.id).to.equal(RunnerJobState.PARENT_CANCELLED)
574 }
575 }
576 })
577
578 it('Should cancel an already accepted job and skip success/error', async function () {
579 await server.videos.quickUpload({ name: 'video' })
580 await waitJobs([ server ])
581
582 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
583 jobUUID = availableJobs[0].uuid
584
585 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
586 jobToken = job.jobToken
587
588 await server.runnerJobs.cancelByAdmin({ jobUUID })
589
590 await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'aborted', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
591 })
592 })
593
594 describe('Stalled jobs', function () {
595
596 it('Should abort stalled jobs', async function () {
597 this.timeout(60000)
598
599 await server.videos.quickUpload({ name: 'video' })
600 await server.videos.quickUpload({ name: 'video' })
601 await waitJobs([ server ])
602
603 const { job: job1 } = await server.runnerJobs.autoAccept({ runnerToken })
604 const { job: stalledJob } = await server.runnerJobs.autoAccept({ runnerToken })
605
606 for (let i = 0; i < 6; i++) {
607 await wait(2000)
608
609 await server.runnerJobs.update({ runnerToken, jobToken: job1.jobToken, jobUUID: job1.uuid })
610 }
611
612 const refreshedJob1 = await server.runnerJobs.getJob({ uuid: job1.uuid })
613 const refreshedStalledJob = await server.runnerJobs.getJob({ uuid: stalledJob.uuid })
614
615 expect(refreshedJob1.state.id).to.equal(RunnerJobState.PROCESSING)
616 expect(refreshedStalledJob.state.id).to.equal(RunnerJobState.PENDING)
617 })
618 })
619
620 describe('Rate limit', function () {
621
622 before(async function () {
623 this.timeout(60000)
624
625 await server.kill()
626
627 await server.run({
628 rates_limit: {
629 api: {
630 max: 10
631 }
632 }
633 })
634 })
635
636 it('Should rate limit an unknown runner', async function () {
637 const path = '/api/v1/ping'
638 const fields = { runnerToken: 'toto' }
639
640 for (let i = 0; i < 20; i++) {
641 try {
642 await makePostBodyRequest({ url: server.url, path, fields, expectedStatus: HttpStatusCode.OK_200 })
643 } catch {}
644 }
645
646 await makePostBodyRequest({ url: server.url, path, fields, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
647 })
648
649 it('Should not rate limit a registered runner', async function () {
650 const path = '/api/v1/ping'
651
652 for (let i = 0; i < 20; i++) {
653 await makePostBodyRequest({ url: server.url, path, fields: { runnerToken }, expectedStatus: HttpStatusCode.OK_200 })
654 }
655 })
656 })
657 })
658
659 after(async function () {
660 await cleanupTests([ server ])
661 })
662})
diff --git a/server/tests/api/runners/runner-live-transcoding.ts b/server/tests/api/runners/runner-live-transcoding.ts
new file mode 100644
index 000000000..b11d54039
--- /dev/null
+++ b/server/tests/api/runners/runner-live-transcoding.ts
@@ -0,0 +1,330 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { FfmpegCommand } from 'fluent-ffmpeg'
5import { readFile } from 'fs-extra'
6import { buildAbsoluteFixturePath, wait } from '@shared/core-utils'
7import {
8 HttpStatusCode,
9 LiveRTMPHLSTranscodingUpdatePayload,
10 LiveVideo,
11 LiveVideoError,
12 RunnerJob,
13 RunnerJobLiveRTMPHLSTranscodingPayload,
14 Video,
15 VideoPrivacy,
16 VideoState
17} from '@shared/models'
18import {
19 cleanupTests,
20 createSingleServer,
21 makeRawRequest,
22 PeerTubeServer,
23 sendRTMPStream,
24 setAccessTokensToServers,
25 setDefaultVideoChannel,
26 stopFfmpeg,
27 testFfmpegStreamError,
28 waitJobs
29} from '@shared/server-commands'
30
31describe('Test runner live transcoding', function () {
32 let server: PeerTubeServer
33 let runnerToken: string
34 let baseUrl: string
35
36 before(async function () {
37 this.timeout(120_000)
38
39 server = await createSingleServer(1)
40
41 await setAccessTokensToServers([ server ])
42 await setDefaultVideoChannel([ server ])
43
44 await server.config.enableRemoteTranscoding()
45 await server.config.enableTranscoding()
46 runnerToken = await server.runners.autoRegisterRunner()
47
48 baseUrl = server.url + '/static/streaming-playlists/hls'
49 })
50
51 describe('Without transcoding enabled', function () {
52
53 before(async function () {
54 await server.config.enableLive({
55 allowReplay: false,
56 resolutions: 'min',
57 transcoding: false
58 })
59 })
60
61 it('Should not have available jobs', async function () {
62 this.timeout(120000)
63
64 const { live, video } = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
65
66 const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
67 await server.live.waitUntilPublished({ videoId: video.id })
68
69 await waitJobs([ server ])
70
71 const { availableJobs } = await server.runnerJobs.requestLive({ runnerToken })
72 expect(availableJobs).to.have.lengthOf(0)
73
74 await stopFfmpeg(ffmpegCommand)
75 })
76 })
77
78 describe('With transcoding enabled on classic live', function () {
79 let live: LiveVideo
80 let video: Video
81 let ffmpegCommand: FfmpegCommand
82 let jobUUID: string
83 let acceptedJob: RunnerJob & { jobToken: string }
84
85 async function testPlaylistFile (fixture: string, expected: string) {
86 const text = await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/${fixture}` })
87 expect(await readFile(buildAbsoluteFixturePath(expected), 'utf-8')).to.equal(text)
88
89 }
90
91 async function testTSFile (fixture: string, expected: string) {
92 const { body } = await makeRawRequest({ url: `${baseUrl}/${video.uuid}/${fixture}`, expectedStatus: HttpStatusCode.OK_200 })
93 expect(await readFile(buildAbsoluteFixturePath(expected))).to.deep.equal(body)
94 }
95
96 before(async function () {
97 await server.config.enableLive({
98 allowReplay: true,
99 resolutions: 'max',
100 transcoding: true
101 })
102 })
103
104 it('Should publish a a live and have available jobs', async function () {
105 this.timeout(120000)
106
107 const data = await server.live.quickCreate({ permanentLive: false, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
108 live = data.live
109 video = data.video
110
111 ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
112 await waitJobs([ server ])
113
114 const job = await server.runnerJobs.requestLiveJob(runnerToken)
115 jobUUID = job.uuid
116
117 expect(job.type).to.equal('live-rtmp-hls-transcoding')
118 expect(job.payload.input.rtmpUrl).to.exist
119
120 expect(job.payload.output.toTranscode).to.have.lengthOf(5)
121
122 for (const { resolution, fps } of job.payload.output.toTranscode) {
123 expect([ 720, 480, 360, 240, 144 ]).to.contain(resolution)
124
125 expect(fps).to.be.above(25)
126 expect(fps).to.be.below(70)
127 }
128 })
129
130 it('Should update the live with a new chunk', async function () {
131 this.timeout(120000)
132
133 const { job } = await server.runnerJobs.accept<RunnerJobLiveRTMPHLSTranscodingPayload>({ jobUUID, runnerToken })
134 acceptedJob = job
135
136 {
137 const payload: LiveRTMPHLSTranscodingUpdatePayload = {
138 masterPlaylistFile: 'live/master.m3u8',
139 resolutionPlaylistFile: 'live/0.m3u8',
140 resolutionPlaylistFilename: '0.m3u8',
141 type: 'add-chunk',
142 videoChunkFile: 'live/0-000067.ts',
143 videoChunkFilename: '0-000067.ts'
144 }
145 await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: job.jobToken, payload, progress: 50 })
146
147 const updatedJob = await server.runnerJobs.getJob({ uuid: job.uuid })
148 expect(updatedJob.progress).to.equal(50)
149 }
150
151 {
152 const payload: LiveRTMPHLSTranscodingUpdatePayload = {
153 resolutionPlaylistFile: 'live/1.m3u8',
154 resolutionPlaylistFilename: '1.m3u8',
155 type: 'add-chunk',
156 videoChunkFile: 'live/1-000068.ts',
157 videoChunkFilename: '1-000068.ts'
158 }
159 await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: job.jobToken, payload })
160 }
161
162 await wait(1000)
163
164 await testPlaylistFile('master.m3u8', 'live/master.m3u8')
165 await testPlaylistFile('0.m3u8', 'live/0.m3u8')
166 await testPlaylistFile('1.m3u8', 'live/1.m3u8')
167
168 await testTSFile('0-000067.ts', 'live/0-000067.ts')
169 await testTSFile('1-000068.ts', 'live/1-000068.ts')
170 })
171
172 it('Should replace existing m3u8 on update', async function () {
173 this.timeout(120000)
174
175 const payload: LiveRTMPHLSTranscodingUpdatePayload = {
176 masterPlaylistFile: 'live/1.m3u8',
177 resolutionPlaylistFilename: '0.m3u8',
178 resolutionPlaylistFile: 'live/1.m3u8',
179 type: 'add-chunk',
180 videoChunkFile: 'live/1-000069.ts',
181 videoChunkFilename: '1-000068.ts'
182 }
183 await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload })
184 await wait(1000)
185
186 await testPlaylistFile('master.m3u8', 'live/1.m3u8')
187 await testPlaylistFile('0.m3u8', 'live/1.m3u8')
188 await testTSFile('1-000068.ts', 'live/1-000069.ts')
189 })
190
191 it('Should update the live with removed chunks', async function () {
192 this.timeout(120000)
193
194 const payload: LiveRTMPHLSTranscodingUpdatePayload = {
195 resolutionPlaylistFile: 'live/0.m3u8',
196 resolutionPlaylistFilename: '0.m3u8',
197 type: 'remove-chunk',
198 videoChunkFilename: '1-000068.ts'
199 }
200 await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload })
201
202 await wait(1000)
203
204 await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/master.m3u8` })
205 await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/0.m3u8` })
206 await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/1.m3u8` })
207 await makeRawRequest({ url: `${baseUrl}/${video.uuid}/0-000067.ts`, expectedStatus: HttpStatusCode.OK_200 })
208 await makeRawRequest({ url: `${baseUrl}/${video.uuid}/1-000068.ts`, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
209 })
210
211 it('Should complete the live and save the replay', async function () {
212 this.timeout(120000)
213
214 for (const segment of [ '0-000069.ts', '0-000070.ts' ]) {
215 const payload: LiveRTMPHLSTranscodingUpdatePayload = {
216 masterPlaylistFile: 'live/master.m3u8',
217 resolutionPlaylistFilename: '0.m3u8',
218 resolutionPlaylistFile: 'live/0.m3u8',
219 type: 'add-chunk',
220 videoChunkFile: 'live/' + segment,
221 videoChunkFilename: segment
222 }
223 await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload })
224
225 await wait(1000)
226 }
227
228 await waitJobs([ server ])
229
230 {
231 const { state } = await server.videos.get({ id: video.uuid })
232 expect(state.id).to.equal(VideoState.PUBLISHED)
233 }
234
235 await stopFfmpeg(ffmpegCommand)
236
237 await server.runnerJobs.success({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload: {} })
238
239 await wait(1500)
240 await waitJobs([ server ])
241
242 {
243 const { state } = await server.videos.get({ id: video.uuid })
244 expect(state.id).to.equal(VideoState.LIVE_ENDED)
245
246 const session = await server.live.findLatestSession({ videoId: video.uuid })
247 expect(session.error).to.be.null
248 }
249 })
250 })
251
252 describe('With transcoding enabled on cancelled/aborted/errored live', function () {
253 let live: LiveVideo
254 let video: Video
255 let ffmpegCommand: FfmpegCommand
256
257 async function prepare () {
258 ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
259 await server.runnerJobs.requestLiveJob(runnerToken)
260
261 const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'live-rtmp-hls-transcoding' })
262
263 return job
264 }
265
266 async function checkSessionError (error: LiveVideoError) {
267 await wait(1500)
268 await waitJobs([ server ])
269
270 const session = await server.live.findLatestSession({ videoId: video.uuid })
271 expect(session.error).to.equal(error)
272 }
273
274 before(async function () {
275 await server.config.enableLive({
276 allowReplay: true,
277 resolutions: 'max',
278 transcoding: true
279 })
280
281 const data = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
282 live = data.live
283 video = data.video
284 })
285
286 it('Should abort a running live', async function () {
287 this.timeout(120000)
288
289 const job = await prepare()
290
291 await Promise.all([
292 server.runnerJobs.abort({ jobUUID: job.uuid, runnerToken, jobToken: job.jobToken, reason: 'abort' }),
293 testFfmpegStreamError(ffmpegCommand, true)
294 ])
295
296 // Abort is not supported
297 await checkSessionError(LiveVideoError.RUNNER_JOB_ERROR)
298 })
299
300 it('Should cancel a running live', async function () {
301 this.timeout(120000)
302
303 const job = await prepare()
304
305 await Promise.all([
306 server.runnerJobs.cancelByAdmin({ jobUUID: job.uuid }),
307 testFfmpegStreamError(ffmpegCommand, true)
308 ])
309
310 await checkSessionError(LiveVideoError.RUNNER_JOB_CANCEL)
311 })
312
313 it('Should error a running live', async function () {
314 this.timeout(120000)
315
316 const job = await prepare()
317
318 await Promise.all([
319 server.runnerJobs.error({ jobUUID: job.uuid, runnerToken, jobToken: job.jobToken, message: 'error' }),
320 testFfmpegStreamError(ffmpegCommand, true)
321 ])
322
323 await checkSessionError(LiveVideoError.RUNNER_JOB_ERROR)
324 })
325 })
326
327 after(async function () {
328 await cleanupTests([ server ])
329 })
330})
diff --git a/server/tests/api/runners/runner-socket.ts b/server/tests/api/runners/runner-socket.ts
new file mode 100644
index 000000000..df640f99c
--- /dev/null
+++ b/server/tests/api/runners/runner-socket.ts
@@ -0,0 +1,116 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@shared/core-utils'
5import {
6 cleanupTests,
7 createSingleServer,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 setDefaultVideoChannel,
11 waitJobs
12} from '@shared/server-commands'
13
14describe('Test runner socket', function () {
15 let server: PeerTubeServer
16 let runnerToken: string
17
18 before(async function () {
19 this.timeout(120_000)
20
21 server = await createSingleServer(1)
22
23 await setAccessTokensToServers([ server ])
24 await setDefaultVideoChannel([ server ])
25
26 await server.config.enableTranscoding(true, true)
27 await server.config.enableRemoteTranscoding()
28 runnerToken = await server.runners.autoRegisterRunner()
29 })
30
31 it('Should throw an error without runner token', function (done) {
32 const localSocket = server.socketIO.getRunnersSocket({ runnerToken: null })
33 localSocket.on('connect_error', err => {
34 expect(err.message).to.contain('No runner token provided')
35 done()
36 })
37 })
38
39 it('Should throw an error with a bad runner token', function (done) {
40 const localSocket = server.socketIO.getRunnersSocket({ runnerToken: 'ergag' })
41 localSocket.on('connect_error', err => {
42 expect(err.message).to.contain('Invalid runner token')
43 done()
44 })
45 })
46
47 it('Should not send ping if there is no available jobs', async function () {
48 let pings = 0
49 const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
50 localSocket.on('available-jobs', () => pings++)
51
52 expect(pings).to.equal(0)
53 })
54
55 it('Should send a ping on available job', async function () {
56 let pings = 0
57 const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
58 localSocket.on('available-jobs', () => pings++)
59
60 await server.videos.quickUpload({ name: 'video1' })
61
62 // Wait for debounce
63 await wait(1000)
64 await waitJobs([ server ])
65 expect(pings).to.equal(1)
66
67 await server.videos.quickUpload({ name: 'video2' })
68
69 // Wait for debounce
70 await wait(1000)
71 await waitJobs([ server ])
72 expect(pings).to.equal(2)
73
74 await server.runnerJobs.cancelAllJobs()
75 })
76
77 it('Should send a ping when a child is ready', async function () {
78 let pings = 0
79 const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
80 localSocket.on('available-jobs', () => pings++)
81
82 await server.videos.quickUpload({ name: 'video3' })
83 // Wait for debounce
84 await wait(1000)
85 await waitJobs([ server ])
86
87 expect(pings).to.equal(1)
88
89 await server.runnerJobs.autoProcessWebVideoJob(runnerToken)
90 // Wait for debounce
91 await wait(1000)
92 await waitJobs([ server ])
93
94 expect(pings).to.equal(2)
95 })
96
97 it('Should not send a ping if the ended job does not have a child', async function () {
98 let pings = 0
99 const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
100 localSocket.on('available-jobs', () => pings++)
101
102 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
103 const job = availableJobs.find(j => j.type === 'vod-web-video-transcoding')
104 await server.runnerJobs.autoProcessWebVideoJob(runnerToken, job.uuid)
105
106 // Wait for debounce
107 await wait(1000)
108 await waitJobs([ server ])
109
110 expect(pings).to.equal(0)
111 })
112
113 after(async function () {
114 await cleanupTests([ server ])
115 })
116})
diff --git a/server/tests/api/runners/runner-vod-transcoding.ts b/server/tests/api/runners/runner-vod-transcoding.ts
new file mode 100644
index 000000000..92a47ac3b
--- /dev/null
+++ b/server/tests/api/runners/runner-vod-transcoding.ts
@@ -0,0 +1,541 @@
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
158 const { body } = await servers[0].runnerJobs.getInputFile({ 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.getInputFile({ 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_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
224 const { body } = await servers[0].runnerJobs.getInputFile({ 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
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
296 const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
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
340 const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
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 {
449 const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.audioFileUrl, jobToken, runnerToken })
450 const inputFile = await readFile(buildAbsoluteFixturePath('sample.ogg'))
451 expect(body).to.deep.equal(inputFile)
452 }
453
454 {
455 const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.previewFileUrl, jobToken, runnerToken })
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
506 const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
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})