aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/tests/api/runners
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-07-31 14:34:36 +0200
committerChocobozzz <me@florianbigard.com>2023-08-11 15:02:33 +0200
commit3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch)
treee4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/tests/api/runners
parent04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff)
downloadPeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'server/tests/api/runners')
-rw-r--r--server/tests/api/runners/index.ts5
-rw-r--r--server/tests/api/runners/runner-common.ts743
-rw-r--r--server/tests/api/runners/runner-live-transcoding.ts330
-rw-r--r--server/tests/api/runners/runner-socket.ts120
-rw-r--r--server/tests/api/runners/runner-studio-transcoding.ts168
-rw-r--r--server/tests/api/runners/runner-vod-transcoding.ts545
6 files changed, 0 insertions, 1911 deletions
diff --git a/server/tests/api/runners/index.ts b/server/tests/api/runners/index.ts
deleted file mode 100644
index 642a3a96d..000000000
--- a/server/tests/api/runners/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
1export * from './runner-common'
2export * from './runner-live-transcoding'
3export * from './runner-socket'
4export * from './runner-studio-transcoding'
5export * from './runner-vod-transcoding'
diff --git a/server/tests/api/runners/runner-common.ts b/server/tests/api/runners/runner-common.ts
deleted file mode 100644
index 9b2eb8b27..000000000
--- a/server/tests/api/runners/runner-common.ts
+++ /dev/null
@@ -1,743 +0,0 @@
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 HttpStatusCode,
7 Runner,
8 RunnerJob,
9 RunnerJobAdmin,
10 RunnerJobState,
11 RunnerJobVODWebVideoTranscodingPayload,
12 RunnerRegistrationToken
13} from '@shared/models'
14import {
15 cleanupTests,
16 createSingleServer,
17 PeerTubeServer,
18 setAccessTokensToServers,
19 setDefaultVideoChannel,
20 waitJobs
21} from '@shared/server-commands'
22
23describe('Test runner common actions', function () {
24 let server: PeerTubeServer
25 let registrationToken: string
26 let runnerToken: string
27 let jobMaxPriority: string
28
29 before(async function () {
30 this.timeout(120_000)
31
32 server = await createSingleServer(1, {
33 remote_runners: {
34 stalled_jobs: {
35 vod: '5 seconds'
36 }
37 }
38 })
39
40 await setAccessTokensToServers([ server ])
41 await setDefaultVideoChannel([ server ])
42
43 await server.config.enableTranscoding({ hls: true, webVideo: true })
44 await server.config.enableRemoteTranscoding()
45 })
46
47 describe('Managing runner registration tokens', function () {
48 let base: RunnerRegistrationToken[]
49 let registrationTokenToDelete: RunnerRegistrationToken
50
51 it('Should have a default registration token', async function () {
52 const { total, data } = await server.runnerRegistrationTokens.list()
53
54 expect(total).to.equal(1)
55 expect(data).to.have.lengthOf(1)
56
57 const token = data[0]
58 expect(token.id).to.exist
59 expect(token.createdAt).to.exist
60 expect(token.updatedAt).to.exist
61 expect(token.registeredRunnersCount).to.equal(0)
62 expect(token.registrationToken).to.exist
63 })
64
65 it('Should create other registration tokens', async function () {
66 await server.runnerRegistrationTokens.generate()
67 await server.runnerRegistrationTokens.generate()
68
69 const { total, data } = await server.runnerRegistrationTokens.list()
70 expect(total).to.equal(3)
71 expect(data).to.have.lengthOf(3)
72 })
73
74 it('Should list registration tokens', async function () {
75 {
76 const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' })
77 expect(total).to.equal(3)
78 expect(data).to.have.lengthOf(3)
79 expect(new Date(data[0].createdAt)).to.be.below(new Date(data[1].createdAt))
80 expect(new Date(data[1].createdAt)).to.be.below(new Date(data[2].createdAt))
81
82 base = data
83
84 registrationTokenToDelete = data[0]
85 registrationToken = data[1].registrationToken
86 }
87
88 {
89 const { total, data } = await server.runnerRegistrationTokens.list({ sort: '-createdAt', start: 2, count: 1 })
90 expect(total).to.equal(3)
91 expect(data).to.have.lengthOf(1)
92 expect(data[0].registrationToken).to.equal(base[0].registrationToken)
93 }
94 })
95
96 it('Should have appropriate registeredRunnersCount for registration tokens', async function () {
97 await server.runners.register({ name: 'to delete 1', registrationToken: registrationTokenToDelete.registrationToken })
98 await server.runners.register({ name: 'to delete 2', registrationToken: registrationTokenToDelete.registrationToken })
99
100 const { data } = await server.runnerRegistrationTokens.list()
101
102 for (const d of data) {
103 if (d.registrationToken === registrationTokenToDelete.registrationToken) {
104 expect(d.registeredRunnersCount).to.equal(2)
105 } else {
106 expect(d.registeredRunnersCount).to.equal(0)
107 }
108 }
109
110 const { data: runners } = await server.runners.list()
111 expect(runners).to.have.lengthOf(2)
112 })
113
114 it('Should delete a registration token', async function () {
115 await server.runnerRegistrationTokens.delete({ id: registrationTokenToDelete.id })
116
117 const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' })
118 expect(total).to.equal(2)
119 expect(data).to.have.lengthOf(2)
120
121 for (const d of data) {
122 expect(d.registeredRunnersCount).to.equal(0)
123 expect(d.registrationToken).to.not.equal(registrationTokenToDelete.registrationToken)
124 }
125 })
126
127 it('Should have removed runners of this registration token', async function () {
128 const { data: runners } = await server.runners.list()
129 expect(runners).to.have.lengthOf(0)
130 })
131 })
132
133 describe('Managing runners', function () {
134 let toDelete: Runner
135
136 it('Should not have runners available', async function () {
137 const { total, data } = await server.runners.list()
138
139 expect(data).to.have.lengthOf(0)
140 expect(total).to.equal(0)
141 })
142
143 it('Should register runners', async function () {
144 const now = new Date()
145
146 const result = await server.runners.register({
147 name: 'runner 1',
148 description: 'my super runner 1',
149 registrationToken
150 })
151 expect(result.runnerToken).to.exist
152 runnerToken = result.runnerToken
153
154 await server.runners.register({
155 name: 'runner 2',
156 registrationToken
157 })
158
159 const { total, data } = await server.runners.list({ sort: 'createdAt' })
160 expect(total).to.equal(2)
161 expect(data).to.have.lengthOf(2)
162
163 for (const d of data) {
164 expect(d.id).to.exist
165 expect(d.createdAt).to.exist
166 expect(d.updatedAt).to.exist
167 expect(new Date(d.createdAt)).to.be.above(now)
168 expect(new Date(d.updatedAt)).to.be.above(now)
169 expect(new Date(d.lastContact)).to.be.above(now)
170 expect(d.ip).to.exist
171 }
172
173 expect(data[0].name).to.equal('runner 1')
174 expect(data[0].description).to.equal('my super runner 1')
175
176 expect(data[1].name).to.equal('runner 2')
177 expect(data[1].description).to.be.null
178
179 toDelete = data[1]
180 })
181
182 it('Should list runners', async function () {
183 const { total, data } = await server.runners.list({ sort: '-createdAt', start: 1, count: 1 })
184
185 expect(total).to.equal(2)
186 expect(data).to.have.lengthOf(1)
187 expect(data[0].name).to.equal('runner 1')
188 })
189
190 it('Should delete a runner', async function () {
191 await server.runners.delete({ id: toDelete.id })
192
193 const { total, data } = await server.runners.list()
194
195 expect(total).to.equal(1)
196 expect(data).to.have.lengthOf(1)
197 expect(data[0].name).to.equal('runner 1')
198 })
199
200 it('Should unregister a runner', async function () {
201 const registered = await server.runners.autoRegisterRunner()
202
203 {
204 const { total, data } = await server.runners.list()
205 expect(total).to.equal(2)
206 expect(data).to.have.lengthOf(2)
207 }
208
209 await server.runners.unregister({ runnerToken: registered })
210
211 {
212 const { total, data } = await server.runners.list()
213 expect(total).to.equal(1)
214 expect(data).to.have.lengthOf(1)
215 expect(data[0].name).to.equal('runner 1')
216 }
217 })
218 })
219
220 describe('Managing runner jobs', function () {
221 let jobUUID: string
222 let jobToken: string
223 let lastRunnerContact: Date
224 let failedJob: RunnerJob
225
226 async function checkMainJobState (
227 mainJobState: RunnerJobState,
228 otherJobStates: RunnerJobState[] = [ RunnerJobState.PENDING, RunnerJobState.WAITING_FOR_PARENT_JOB ]
229 ) {
230 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
231
232 for (const job of data) {
233 if (job.uuid === jobUUID) {
234 expect(job.state.id).to.equal(mainJobState)
235 } else {
236 expect(otherJobStates).to.include(job.state.id)
237 }
238 }
239 }
240
241 function getMainJob () {
242 return server.runnerJobs.getJob({ uuid: jobUUID })
243 }
244
245 describe('List jobs', function () {
246
247 it('Should not have jobs', async function () {
248 const { total, data } = await server.runnerJobs.list()
249
250 expect(data).to.have.lengthOf(0)
251 expect(total).to.equal(0)
252 })
253
254 it('Should upload a video and have available jobs', async function () {
255 await server.videos.quickUpload({ name: 'to transcode' })
256 await waitJobs([ server ])
257
258 const { total, data } = await server.runnerJobs.list()
259
260 expect(data).to.have.lengthOf(10)
261 expect(total).to.equal(10)
262
263 for (const job of data) {
264 expect(job.startedAt).to.not.exist
265 expect(job.finishedAt).to.not.exist
266 expect(job.payload).to.exist
267 expect(job.privatePayload).to.exist
268 }
269
270 const hlsJobs = data.filter(d => d.type === 'vod-hls-transcoding')
271 const webVideoJobs = data.filter(d => d.type === 'vod-web-video-transcoding')
272
273 expect(hlsJobs).to.have.lengthOf(5)
274 expect(webVideoJobs).to.have.lengthOf(5)
275
276 const pendingJobs = data.filter(d => d.state.id === RunnerJobState.PENDING)
277 const waitingJobs = data.filter(d => d.state.id === RunnerJobState.WAITING_FOR_PARENT_JOB)
278
279 expect(pendingJobs).to.have.lengthOf(1)
280 expect(waitingJobs).to.have.lengthOf(9)
281 })
282
283 it('Should upload another video and list/sort jobs', async function () {
284 await server.videos.quickUpload({ name: 'to transcode 2' })
285 await waitJobs([ server ])
286
287 {
288 const { total, data } = await server.runnerJobs.list({ start: 0, count: 30 })
289
290 expect(data).to.have.lengthOf(20)
291 expect(total).to.equal(20)
292
293 jobUUID = data[16].uuid
294 }
295
296 {
297 const { total, data } = await server.runnerJobs.list({ start: 3, count: 1, sort: 'createdAt' })
298 expect(total).to.equal(20)
299
300 expect(data).to.have.lengthOf(1)
301 expect(data[0].uuid).to.equal(jobUUID)
302 }
303
304 {
305 let previousPriority = Infinity
306 const { total, data } = await server.runnerJobs.list({ start: 0, count: 100, sort: '-priority' })
307 expect(total).to.equal(20)
308
309 for (const job of data) {
310 expect(job.priority).to.be.at.most(previousPriority)
311 previousPriority = job.priority
312
313 if (job.state.id === RunnerJobState.PENDING) {
314 jobMaxPriority = job.uuid
315 }
316 }
317 }
318 })
319
320 it('Should search jobs', async function () {
321 {
322 const { total, data } = await server.runnerJobs.list({ search: jobUUID })
323
324 expect(data).to.have.lengthOf(1)
325 expect(total).to.equal(1)
326
327 expect(data[0].uuid).to.equal(jobUUID)
328 }
329
330 {
331 const { total, data } = await server.runnerJobs.list({ search: 'toto' })
332
333 expect(data).to.have.lengthOf(0)
334 expect(total).to.equal(0)
335 }
336
337 {
338 const { total, data } = await server.runnerJobs.list({ search: 'hls' })
339
340 expect(data).to.not.have.lengthOf(0)
341 expect(total).to.not.equal(0)
342
343 for (const job of data) {
344 expect(job.type).to.include('hls')
345 }
346 }
347 })
348
349 it('Should filter jobs', async function () {
350 {
351 const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.WAITING_FOR_PARENT_JOB ] })
352
353 expect(data).to.not.have.lengthOf(0)
354 expect(total).to.not.equal(0)
355
356 for (const job of data) {
357 expect(job.state.label).to.equal('Waiting for parent job to finish')
358 }
359 }
360
361 {
362 const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.COMPLETED ] })
363
364 expect(data).to.have.lengthOf(0)
365 expect(total).to.equal(0)
366 }
367 })
368 })
369
370 describe('Accept/update/abort/process a job', function () {
371
372 it('Should request available jobs', async function () {
373 lastRunnerContact = new Date()
374
375 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
376
377 // Only optimize jobs are available
378 expect(availableJobs).to.have.lengthOf(2)
379
380 for (const job of availableJobs) {
381 expect(job.uuid).to.exist
382 expect(job.payload.input).to.exist
383 expect((job.payload as RunnerJobVODWebVideoTranscodingPayload).output).to.exist
384
385 expect((job as RunnerJobAdmin).privatePayload).to.not.exist
386 }
387
388 const hlsJobs = availableJobs.filter(d => d.type === 'vod-hls-transcoding')
389 const webVideoJobs = availableJobs.filter(d => d.type === 'vod-web-video-transcoding')
390
391 expect(hlsJobs).to.have.lengthOf(0)
392 expect(webVideoJobs).to.have.lengthOf(2)
393
394 jobUUID = webVideoJobs[0].uuid
395 })
396
397 it('Should have sorted available jobs by priority', async function () {
398 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
399
400 expect(availableJobs[0].uuid).to.equal(jobMaxPriority)
401 })
402
403 it('Should have last runner contact updated', async function () {
404 await wait(1000)
405
406 const { data } = await server.runners.list({ sort: 'createdAt' })
407 expect(new Date(data[0].lastContact)).to.be.above(lastRunnerContact)
408 })
409
410 it('Should accept a job', async function () {
411 const startedAt = new Date()
412
413 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
414 jobToken = job.jobToken
415
416 const checkProcessingJob = (job: RunnerJob & { jobToken?: string }, fromAccept: boolean) => {
417 expect(job.uuid).to.equal(jobUUID)
418
419 expect(job.type).to.equal('vod-web-video-transcoding')
420 expect(job.state.label).to.equal('Processing')
421 expect(job.state.id).to.equal(RunnerJobState.PROCESSING)
422
423 expect(job.runner).to.exist
424 expect(job.runner.name).to.equal('runner 1')
425 expect(job.runner.description).to.equal('my super runner 1')
426
427 expect(job.progress).to.be.null
428
429 expect(job.startedAt).to.exist
430 expect(new Date(job.startedAt)).to.be.above(startedAt)
431
432 expect(job.finishedAt).to.not.exist
433
434 expect(job.failures).to.equal(0)
435
436 expect(job.payload).to.exist
437
438 if (fromAccept) {
439 expect(job.jobToken).to.exist
440 expect((job as RunnerJobAdmin).privatePayload).to.not.exist
441 } else {
442 expect(job.jobToken).to.not.exist
443 expect((job as RunnerJobAdmin).privatePayload).to.exist
444 }
445 }
446
447 checkProcessingJob(job, true)
448
449 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
450
451 const processingJob = data.find(j => j.uuid === jobUUID)
452 checkProcessingJob(processingJob, false)
453
454 await checkMainJobState(RunnerJobState.PROCESSING)
455 })
456
457 it('Should update a job', async function () {
458 await server.runnerJobs.update({ runnerToken, jobUUID, jobToken, progress: 53 })
459
460 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
461
462 for (const job of data) {
463 if (job.state.id === RunnerJobState.PROCESSING) {
464 expect(job.progress).to.equal(53)
465 } else {
466 expect(job.progress).to.be.null
467 }
468 }
469 })
470
471 it('Should abort a job', async function () {
472 await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'for tests' })
473
474 await checkMainJobState(RunnerJobState.PENDING)
475
476 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
477 for (const job of data) {
478 expect(job.progress).to.be.null
479 }
480 })
481
482 it('Should accept the same job again and post a success', async function () {
483 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
484 expect(availableJobs.find(j => j.uuid === jobUUID)).to.exist
485
486 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
487 jobToken = job.jobToken
488
489 await checkMainJobState(RunnerJobState.PROCESSING)
490
491 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
492
493 for (const job of data) {
494 expect(job.progress).to.be.null
495 }
496
497 const payload = {
498 videoFile: 'video_short.mp4'
499 }
500
501 await server.runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
502 })
503
504 it('Should not have available jobs anymore', async function () {
505 await checkMainJobState(RunnerJobState.COMPLETED)
506
507 const job = await getMainJob()
508 expect(job.finishedAt).to.exist
509
510 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
511 expect(availableJobs.find(j => j.uuid === jobUUID)).to.not.exist
512 })
513 })
514
515 describe('Error job', function () {
516
517 it('Should accept another job and post an error', async function () {
518 await server.runnerJobs.cancelAllJobs()
519 await server.videos.quickUpload({ name: 'video' })
520 await waitJobs([ server ])
521
522 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
523 jobUUID = availableJobs[0].uuid
524
525 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
526 jobToken = job.jobToken
527
528 await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
529 })
530
531 it('Should have job failures increased', async function () {
532 const job = await getMainJob()
533 expect(job.state.id).to.equal(RunnerJobState.PENDING)
534 expect(job.failures).to.equal(1)
535 expect(job.error).to.be.null
536 expect(job.progress).to.be.null
537 expect(job.finishedAt).to.not.exist
538 })
539
540 it('Should error a job when job attempts is too big', async function () {
541 for (let i = 0; i < 4; i++) {
542 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
543 jobToken = job.jobToken
544
545 await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error ' + i })
546 }
547
548 const job = await getMainJob()
549 expect(job.failures).to.equal(5)
550 expect(job.state.id).to.equal(RunnerJobState.ERRORED)
551 expect(job.state.label).to.equal('Errored')
552 expect(job.error).to.equal('Error 3')
553 expect(job.progress).to.be.null
554 expect(job.finishedAt).to.exist
555
556 failedJob = job
557 })
558
559 it('Should have failed children jobs too', async function () {
560 const { data } = await server.runnerJobs.list({ count: 50, sort: '-updatedAt' })
561
562 const children = data.filter(j => j.parent?.uuid === failedJob.uuid)
563 expect(children).to.have.lengthOf(9)
564
565 for (const child of children) {
566 expect(child.parent.uuid).to.equal(failedJob.uuid)
567 expect(child.parent.type).to.equal(failedJob.type)
568 expect(child.parent.state.id).to.equal(failedJob.state.id)
569 expect(child.parent.state.label).to.equal(failedJob.state.label)
570
571 expect(child.state.id).to.equal(RunnerJobState.PARENT_ERRORED)
572 expect(child.state.label).to.equal('Parent job failed')
573 }
574 })
575 })
576
577 describe('Cancel', function () {
578
579 it('Should cancel a pending job', async function () {
580 await server.videos.quickUpload({ name: 'video' })
581 await waitJobs([ server ])
582
583 {
584 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
585
586 const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING)
587 jobUUID = pendingJob.uuid
588
589 await server.runnerJobs.cancelByAdmin({ jobUUID })
590 }
591
592 {
593 const job = await getMainJob()
594 expect(job.state.id).to.equal(RunnerJobState.CANCELLED)
595 expect(job.state.label).to.equal('Cancelled')
596 }
597
598 {
599 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
600 const children = data.filter(j => j.parent?.uuid === jobUUID)
601 expect(children).to.have.lengthOf(9)
602
603 for (const child of children) {
604 expect(child.state.id).to.equal(RunnerJobState.PARENT_CANCELLED)
605 }
606 }
607 })
608
609 it('Should cancel an already accepted job and skip success/error', async function () {
610 await server.videos.quickUpload({ name: 'video' })
611 await waitJobs([ server ])
612
613 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
614 jobUUID = availableJobs[0].uuid
615
616 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
617 jobToken = job.jobToken
618
619 await server.runnerJobs.cancelByAdmin({ jobUUID })
620
621 await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'aborted', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
622 })
623 })
624
625 describe('Remove', function () {
626
627 it('Should remove a pending job', async function () {
628 await server.videos.quickUpload({ name: 'video' })
629 await waitJobs([ server ])
630
631 {
632 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
633
634 const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING)
635 jobUUID = pendingJob.uuid
636
637 await server.runnerJobs.deleteByAdmin({ jobUUID })
638 }
639
640 {
641 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
642
643 const parent = data.find(j => j.uuid === jobUUID)
644 expect(parent).to.not.exist
645
646 const children = data.filter(j => j.parent?.uuid === jobUUID)
647 expect(children).to.have.lengthOf(0)
648 }
649 })
650 })
651
652 describe('Stalled jobs', function () {
653
654 it('Should abort stalled jobs', async function () {
655 this.timeout(60000)
656
657 await server.videos.quickUpload({ name: 'video' })
658 await server.videos.quickUpload({ name: 'video' })
659 await waitJobs([ server ])
660
661 const { job: job1 } = await server.runnerJobs.autoAccept({ runnerToken })
662 const { job: stalledJob } = await server.runnerJobs.autoAccept({ runnerToken })
663
664 for (let i = 0; i < 6; i++) {
665 await wait(2000)
666
667 await server.runnerJobs.update({ runnerToken, jobToken: job1.jobToken, jobUUID: job1.uuid })
668 }
669
670 const refreshedJob1 = await server.runnerJobs.getJob({ uuid: job1.uuid })
671 const refreshedStalledJob = await server.runnerJobs.getJob({ uuid: stalledJob.uuid })
672
673 expect(refreshedJob1.state.id).to.equal(RunnerJobState.PROCESSING)
674 expect(refreshedStalledJob.state.id).to.equal(RunnerJobState.PENDING)
675 })
676 })
677
678 describe('Rate limit', function () {
679
680 before(async function () {
681 this.timeout(60000)
682
683 await server.kill()
684
685 await server.run({
686 rates_limit: {
687 api: {
688 max: 10
689 }
690 }
691 })
692 })
693
694 it('Should rate limit an unknown runner, but not a registered one', async function () {
695 this.timeout(60000)
696
697 await server.videos.quickUpload({ name: 'video' })
698 await waitJobs([ server ])
699
700 const { job } = await server.runnerJobs.autoAccept({ runnerToken })
701
702 for (let i = 0; i < 20; i++) {
703 try {
704 await server.runnerJobs.request({ runnerToken })
705 await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid })
706 } catch {}
707 }
708
709 // Invalid
710 {
711 await server.runnerJobs.request({ runnerToken: 'toto', expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
712 await server.runnerJobs.update({
713 runnerToken: 'toto',
714 jobToken: job.jobToken,
715 jobUUID: job.uuid,
716 expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429
717 })
718 }
719
720 // Not provided
721 {
722 await server.runnerJobs.request({ runnerToken: undefined, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
723 await server.runnerJobs.update({
724 runnerToken: undefined,
725 jobToken: job.jobToken,
726 jobUUID: job.uuid,
727 expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429
728 })
729 }
730
731 // Registered
732 {
733 await server.runnerJobs.request({ runnerToken })
734 await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid })
735 }
736 })
737 })
738 })
739
740 after(async function () {
741 await cleanupTests([ server ])
742 })
743})
diff --git a/server/tests/api/runners/runner-live-transcoding.ts b/server/tests/api/runners/runner-live-transcoding.ts
deleted file mode 100644
index b11d54039..000000000
--- a/server/tests/api/runners/runner-live-transcoding.ts
+++ /dev/null
@@ -1,330 +0,0 @@
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
deleted file mode 100644
index 215164e48..000000000
--- a/server/tests/api/runners/runner-socket.ts
+++ /dev/null
@@ -1,120 +0,0 @@
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({ hls: true, webVideo: 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 await waitJobs([ server ])
62
63 // eslint-disable-next-line no-unmodified-loop-condition
64 while (pings !== 1) {
65 await wait(500)
66 }
67
68 await server.videos.quickUpload({ name: 'video2' })
69 await waitJobs([ server ])
70
71 // eslint-disable-next-line no-unmodified-loop-condition
72 while ((pings as number) !== 2) {
73 await wait(500)
74 }
75
76 await server.runnerJobs.cancelAllJobs()
77 })
78
79 it('Should send a ping when a child is ready', async function () {
80 let pings = 0
81 const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
82 localSocket.on('available-jobs', () => pings++)
83
84 await server.videos.quickUpload({ name: 'video3' })
85 await waitJobs([ server ])
86
87 // eslint-disable-next-line no-unmodified-loop-condition
88 while (pings !== 1) {
89 await wait(500)
90 }
91
92 await server.runnerJobs.autoProcessWebVideoJob(runnerToken)
93 await waitJobs([ server ])
94
95 // eslint-disable-next-line no-unmodified-loop-condition
96 while ((pings as number) !== 2) {
97 await wait(500)
98 }
99 })
100
101 it('Should not send a ping if the ended job does not have a child', async function () {
102 let pings = 0
103 const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
104 localSocket.on('available-jobs', () => pings++)
105
106 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
107 const job = availableJobs.find(j => j.type === 'vod-web-video-transcoding')
108 await server.runnerJobs.autoProcessWebVideoJob(runnerToken, job.uuid)
109
110 // Wait for debounce
111 await wait(1000)
112 await waitJobs([ server ])
113
114 expect(pings).to.equal(0)
115 })
116
117 after(async function () {
118 await cleanupTests([ server ])
119 })
120})
diff --git a/server/tests/api/runners/runner-studio-transcoding.ts b/server/tests/api/runners/runner-studio-transcoding.ts
deleted file mode 100644
index f5cea6cea..000000000
--- a/server/tests/api/runners/runner-studio-transcoding.ts
+++ /dev/null
@@ -1,168 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { readFile } from 'fs-extra'
5import { checkPersistentTmpIsEmpty, checkVideoDuration } from '@server/tests/shared'
6import { buildAbsoluteFixturePath } from '@shared/core-utils'
7import {
8 RunnerJobStudioTranscodingPayload,
9 VideoStudioTranscodingSuccess,
10 VideoState,
11 VideoStudioTask,
12 VideoStudioTaskIntro
13} from '@shared/models'
14import {
15 cleanupTests,
16 createMultipleServers,
17 doubleFollow,
18 PeerTubeServer,
19 setAccessTokensToServers,
20 setDefaultVideoChannel,
21 VideoStudioCommand,
22 waitJobs
23} from '@shared/server-commands'
24
25describe('Test runner video studio transcoding', function () {
26 let servers: PeerTubeServer[] = []
27 let runnerToken: string
28 let videoUUID: string
29 let jobUUID: string
30
31 async function renewStudio (tasks: VideoStudioTask[] = VideoStudioCommand.getComplexTask()) {
32 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
33 videoUUID = uuid
34
35 await waitJobs(servers)
36
37 await servers[0].videoStudio.createEditionTasks({ videoId: uuid, tasks })
38 await waitJobs(servers)
39
40 const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
41 expect(availableJobs).to.have.lengthOf(1)
42
43 jobUUID = availableJobs[0].uuid
44 }
45
46 before(async function () {
47 this.timeout(120_000)
48
49 servers = await createMultipleServers(2)
50
51 await setAccessTokensToServers(servers)
52 await setDefaultVideoChannel(servers)
53
54 await doubleFollow(servers[0], servers[1])
55
56 await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
57 await servers[0].config.enableStudio()
58 await servers[0].config.enableRemoteStudio()
59
60 runnerToken = await servers[0].runners.autoRegisterRunner()
61 })
62
63 it('Should error a studio transcoding job', async function () {
64 this.timeout(60000)
65
66 await renewStudio()
67
68 for (let i = 0; i < 5; i++) {
69 const { job } = await servers[0].runnerJobs.accept({ runnerToken, jobUUID })
70 const jobToken = job.jobToken
71
72 await servers[0].runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
73 }
74
75 const video = await servers[0].videos.get({ id: videoUUID })
76 expect(video.state.id).to.equal(VideoState.PUBLISHED)
77
78 await checkPersistentTmpIsEmpty(servers[0])
79 })
80
81 it('Should cancel a transcoding job', async function () {
82 this.timeout(60000)
83
84 await renewStudio()
85
86 await servers[0].runnerJobs.cancelByAdmin({ jobUUID })
87
88 const video = await servers[0].videos.get({ id: videoUUID })
89 expect(video.state.id).to.equal(VideoState.PUBLISHED)
90
91 await checkPersistentTmpIsEmpty(servers[0])
92 })
93
94 it('Should execute a remote studio job', async function () {
95 this.timeout(240_000)
96
97 const tasks = [
98 {
99 name: 'add-outro' as 'add-outro',
100 options: {
101 file: 'video_short.webm'
102 }
103 },
104 {
105 name: 'add-watermark' as 'add-watermark',
106 options: {
107 file: 'custom-thumbnail.png'
108 }
109 },
110 {
111 name: 'add-intro' as 'add-intro',
112 options: {
113 file: 'video_very_short_240p.mp4'
114 }
115 }
116 ]
117
118 await renewStudio(tasks)
119
120 for (const server of servers) {
121 await checkVideoDuration(server, videoUUID, 5)
122 }
123
124 const { job } = await servers[0].runnerJobs.accept<RunnerJobStudioTranscodingPayload>({ runnerToken, jobUUID })
125 const jobToken = job.jobToken
126
127 expect(job.type === 'video-studio-transcoding')
128 expect(job.payload.input.videoFileUrl).to.exist
129
130 // Check video input file
131 {
132 await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
133 }
134
135 // Check task files
136 for (let i = 0; i < tasks.length; i++) {
137 const task = tasks[i]
138 const payloadTask = job.payload.tasks[i]
139
140 expect(payloadTask.name).to.equal(task.name)
141
142 const inputFile = await readFile(buildAbsoluteFixturePath(task.options.file))
143
144 const { body } = await servers[0].runnerJobs.getJobFile({
145 url: (payloadTask as VideoStudioTaskIntro).options.file as string,
146 jobToken,
147 runnerToken
148 })
149
150 expect(body).to.deep.equal(inputFile)
151 }
152
153 const payload: VideoStudioTranscodingSuccess = { videoFile: 'video_very_short_240p.mp4' }
154 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
155
156 await waitJobs(servers)
157
158 for (const server of servers) {
159 await checkVideoDuration(server, videoUUID, 2)
160 }
161
162 await checkPersistentTmpIsEmpty(servers[0])
163 })
164
165 after(async function () {
166 await cleanupTests(servers)
167 })
168})
diff --git a/server/tests/api/runners/runner-vod-transcoding.ts b/server/tests/api/runners/runner-vod-transcoding.ts
deleted file mode 100644
index ee6be4ee9..000000000
--- a/server/tests/api/runners/runner-vod-transcoding.ts
+++ /dev/null
@@ -1,545 +0,0 @@
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({ hls: true, webVideo: 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 for (let i = 0; i < 5; i++) {
102 const { job } = await servers[0].runnerJobs.accept({ runnerToken, jobUUID })
103 const jobToken = job.jobToken
104
105 await servers[0].runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
106 }
107
108 const video = await servers[0].videos.get({ id: uuid })
109 expect(video.state.id).to.equal(VideoState.TRANSCODING_FAILED)
110 })
111
112 it('Should cancel a transcoding job', async function () {
113 await servers[0].runnerJobs.cancelAllJobs()
114 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
115 await waitJobs(servers)
116
117 const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
118 const jobUUID = availableJobs[0].uuid
119
120 await servers[0].runnerJobs.cancelByAdmin({ jobUUID })
121
122 const video = await servers[0].videos.get({ id: uuid })
123 expect(video.state.id).to.equal(VideoState.PUBLISHED)
124 })
125 })
126
127 describe('Web video transcoding only', function () {
128 let videoUUID: string
129 let jobToken: string
130 let jobUUID: string
131
132 before(async function () {
133 this.timeout(60000)
134
135 await servers[0].runnerJobs.cancelAllJobs()
136 await servers[0].config.enableTranscoding({ hls: false, webVideo: true })
137
138 const { uuid } = await servers[0].videos.quickUpload({ name: 'web video', fixture: 'video_short.webm' })
139 videoUUID = uuid
140
141 await waitJobs(servers)
142 })
143
144 it('Should have jobs available for remote runners', async function () {
145 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
146 expect(availableJobs).to.have.lengthOf(1)
147
148 jobUUID = availableJobs[0].uuid
149 })
150
151 it('Should have a valid first transcoding job', async function () {
152 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
153 jobToken = job.jobToken
154
155 expect(job.type === 'vod-web-video-transcoding')
156 expect(job.payload.input.videoFileUrl).to.exist
157 expect(job.payload.output.resolution).to.equal(720)
158 expect(job.payload.output.fps).to.equal(25)
159
160 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
161 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.webm'))
162
163 expect(body).to.deep.equal(inputFile)
164 })
165
166 it('Should transcode the max video resolution and send it back to the server', async function () {
167 this.timeout(60000)
168
169 const payload: VODWebVideoTranscodingSuccess = {
170 videoFile: 'video_short.mp4'
171 }
172 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
173
174 await waitJobs(servers)
175 })
176
177 it('Should have the video updated', async function () {
178 for (const server of servers) {
179 const video = await server.videos.get({ id: videoUUID })
180 expect(video.files).to.have.lengthOf(1)
181 expect(video.streamingPlaylists).to.have.lengthOf(0)
182
183 const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
184 expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short.mp4')))
185 }
186 })
187
188 it('Should have 4 lower resolution to transcode', async function () {
189 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
190 expect(availableJobs).to.have.lengthOf(4)
191
192 for (const resolution of [ 480, 360, 240, 144 ]) {
193 const job = availableJobs.find(j => j.payload.output.resolution === resolution)
194 expect(job).to.exist
195 expect(job.type).to.equal('vod-web-video-transcoding')
196
197 if (resolution === 240) jobUUID = job.uuid
198 }
199 })
200
201 it('Should process one of these transcoding jobs', async function () {
202 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
203 jobToken = job.jobToken
204
205 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
206 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
207
208 expect(body).to.deep.equal(inputFile)
209
210 const payload: VODWebVideoTranscodingSuccess = { videoFile: `video_short_${job.payload.output.resolution}p.mp4` }
211 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
212 })
213
214 it('Should process all other jobs', async function () {
215 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
216 expect(availableJobs).to.have.lengthOf(3)
217
218 for (const resolution of [ 480, 360, 144 ]) {
219 const availableJob = availableJobs.find(j => j.payload.output.resolution === resolution)
220 expect(availableJob).to.exist
221 jobUUID = availableJob.uuid
222
223 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
224 jobToken = job.jobToken
225
226 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
227 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
228 expect(body).to.deep.equal(inputFile)
229
230 const payload: VODWebVideoTranscodingSuccess = { videoFile: `video_short_${resolution}p.mp4` }
231 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
232 }
233
234 await waitJobs(servers)
235 })
236
237 it('Should have the video updated', async function () {
238 for (const server of servers) {
239 const video = await server.videos.get({ id: videoUUID })
240 expect(video.files).to.have.lengthOf(5)
241 expect(video.streamingPlaylists).to.have.lengthOf(0)
242
243 const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
244 expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short.mp4')))
245
246 for (const file of video.files) {
247 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
248 await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 })
249 }
250 }
251 })
252
253 it('Should not have available jobs anymore', async function () {
254 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
255 expect(availableJobs).to.have.lengthOf(0)
256 })
257 })
258
259 describe('HLS transcoding only', function () {
260 let videoUUID: string
261 let jobToken: string
262 let jobUUID: string
263
264 before(async function () {
265 this.timeout(60000)
266
267 await servers[0].config.enableTranscoding({ hls: true, webVideo: false })
268
269 const { uuid } = await servers[0].videos.quickUpload({ name: 'hls video', fixture: 'video_short.webm' })
270 videoUUID = uuid
271
272 await waitJobs(servers)
273 })
274
275 it('Should run the optimize job', async function () {
276 this.timeout(60000)
277
278 await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
279 })
280
281 it('Should have 5 HLS resolution to transcode', async function () {
282 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
283 expect(availableJobs).to.have.lengthOf(5)
284
285 for (const resolution of [ 720, 480, 360, 240, 144 ]) {
286 const job = availableJobs.find(j => j.payload.output.resolution === resolution)
287 expect(job).to.exist
288 expect(job.type).to.equal('vod-hls-transcoding')
289
290 if (resolution === 480) jobUUID = job.uuid
291 }
292 })
293
294 it('Should process one of these transcoding jobs', async function () {
295 this.timeout(60000)
296
297 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
298 jobToken = job.jobToken
299
300 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
301 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
302
303 expect(body).to.deep.equal(inputFile)
304
305 const payload: VODHLSTranscodingSuccess = {
306 videoFile: 'video_short_480p.mp4',
307 resolutionPlaylistFile: 'video_short_480p.m3u8'
308 }
309 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
310
311 await waitJobs(servers)
312 })
313
314 it('Should have the video updated', async function () {
315 for (const server of servers) {
316 const video = await server.videos.get({ id: videoUUID })
317
318 expect(video.files).to.have.lengthOf(1)
319 expect(video.streamingPlaylists).to.have.lengthOf(1)
320
321 const hls = video.streamingPlaylists[0]
322 expect(hls.files).to.have.lengthOf(1)
323
324 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
325 }
326 })
327
328 it('Should process all other jobs', async function () {
329 this.timeout(60000)
330
331 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
332 expect(availableJobs).to.have.lengthOf(4)
333
334 let maxQualityFile = 'video_short.mp4'
335
336 for (const resolution of [ 720, 360, 240, 144 ]) {
337 const availableJob = availableJobs.find(j => j.payload.output.resolution === resolution)
338 expect(availableJob).to.exist
339 jobUUID = availableJob.uuid
340
341 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
342 jobToken = job.jobToken
343
344 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
345 const inputFile = await readFile(buildAbsoluteFixturePath(maxQualityFile))
346 expect(body).to.deep.equal(inputFile)
347
348 const payload: VODHLSTranscodingSuccess = {
349 videoFile: `video_short_${resolution}p.mp4`,
350 resolutionPlaylistFile: `video_short_${resolution}p.m3u8`
351 }
352 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
353
354 if (resolution === 720) {
355 maxQualityFile = 'video_short_720p.mp4'
356 }
357 }
358
359 await waitJobs(servers)
360 })
361
362 it('Should have the video updated', async function () {
363 for (const server of servers) {
364 const video = await server.videos.get({ id: videoUUID })
365
366 expect(video.files).to.have.lengthOf(0)
367 expect(video.streamingPlaylists).to.have.lengthOf(1)
368
369 const hls = video.streamingPlaylists[0]
370 expect(hls.files).to.have.lengthOf(5)
371
372 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: true, servers, resolutions: [ 720, 480, 360, 240, 144 ] })
373 }
374 })
375
376 it('Should not have available jobs anymore', async function () {
377 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
378 expect(availableJobs).to.have.lengthOf(0)
379 })
380 })
381
382 describe('Web video and HLS transcoding', function () {
383
384 before(async function () {
385 this.timeout(60000)
386
387 await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
388
389 await servers[0].videos.quickUpload({ name: 'web video and hls video', fixture: 'video_short.webm' })
390
391 await waitJobs(servers)
392 })
393
394 it('Should process the first optimize job', async function () {
395 this.timeout(60000)
396
397 await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
398 })
399
400 it('Should have 9 jobs to process', async function () {
401 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
402
403 expect(availableJobs).to.have.lengthOf(9)
404
405 const webVideoJobs = availableJobs.filter(j => j.type === 'vod-web-video-transcoding')
406 const hlsJobs = availableJobs.filter(j => j.type === 'vod-hls-transcoding')
407
408 expect(webVideoJobs).to.have.lengthOf(4)
409 expect(hlsJobs).to.have.lengthOf(5)
410 })
411
412 it('Should process all available jobs', async function () {
413 await processAllJobs(servers[0], runnerToken)
414 })
415 })
416
417 describe('Audio merge transcoding', function () {
418 let videoUUID: string
419 let jobToken: string
420 let jobUUID: string
421
422 before(async function () {
423 this.timeout(60000)
424
425 await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
426
427 const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' }
428 const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' })
429 videoUUID = uuid
430
431 await waitJobs(servers)
432 })
433
434 it('Should have an audio merge transcoding job', async function () {
435 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
436 expect(availableJobs).to.have.lengthOf(1)
437
438 expect(availableJobs[0].type).to.equal('vod-audio-merge-transcoding')
439
440 jobUUID = availableJobs[0].uuid
441 })
442
443 it('Should have a valid remote audio merge transcoding job', async function () {
444 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODAudioMergeTranscodingPayload>({ runnerToken, jobUUID })
445 jobToken = job.jobToken
446
447 expect(job.type === 'vod-audio-merge-transcoding')
448 expect(job.payload.input.audioFileUrl).to.exist
449 expect(job.payload.input.previewFileUrl).to.exist
450 expect(job.payload.output.resolution).to.equal(480)
451
452 {
453 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.audioFileUrl, jobToken, runnerToken })
454 const inputFile = await readFile(buildAbsoluteFixturePath('sample.ogg'))
455 expect(body).to.deep.equal(inputFile)
456 }
457
458 {
459 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.previewFileUrl, jobToken, runnerToken })
460
461 const video = await servers[0].videos.get({ id: videoUUID })
462 const { body: inputFile } = await makeGetRequest({
463 url: servers[0].url,
464 path: video.previewPath,
465 expectedStatus: HttpStatusCode.OK_200
466 })
467
468 expect(body).to.deep.equal(inputFile)
469 }
470 })
471
472 it('Should merge the audio', async function () {
473 this.timeout(60000)
474
475 const payload: VODAudioMergeTranscodingSuccess = { videoFile: 'video_short_480p.mp4' }
476 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
477
478 await waitJobs(servers)
479 })
480
481 it('Should have the video updated', async function () {
482 for (const server of servers) {
483 const video = await server.videos.get({ id: videoUUID })
484 expect(video.files).to.have.lengthOf(1)
485 expect(video.streamingPlaylists).to.have.lengthOf(0)
486
487 const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
488 expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short_480p.mp4')))
489 }
490 })
491
492 it('Should have 7 lower resolutions to transcode', async function () {
493 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
494 expect(availableJobs).to.have.lengthOf(7)
495
496 for (const resolution of [ 360, 240, 144 ]) {
497 const jobs = availableJobs.filter(j => j.payload.output.resolution === resolution)
498 expect(jobs).to.have.lengthOf(2)
499 }
500
501 jobUUID = availableJobs.find(j => j.payload.output.resolution === 480).uuid
502 })
503
504 it('Should process one other job', async function () {
505 this.timeout(60000)
506
507 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
508 jobToken = job.jobToken
509
510 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
511 const inputFile = await readFile(buildAbsoluteFixturePath('video_short_480p.mp4'))
512 expect(body).to.deep.equal(inputFile)
513
514 const payload: VODHLSTranscodingSuccess = {
515 videoFile: `video_short_480p.mp4`,
516 resolutionPlaylistFile: `video_short_480p.m3u8`
517 }
518 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
519
520 await waitJobs(servers)
521 })
522
523 it('Should have the video updated', async function () {
524 for (const server of servers) {
525 const video = await server.videos.get({ id: videoUUID })
526
527 expect(video.files).to.have.lengthOf(1)
528 expect(video.streamingPlaylists).to.have.lengthOf(1)
529
530 const hls = video.streamingPlaylists[0]
531 expect(hls.files).to.have.lengthOf(1)
532
533 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
534 }
535 })
536
537 it('Should process all available jobs', async function () {
538 await processAllJobs(servers[0], runnerToken)
539 })
540 })
541
542 after(async function () {
543 await cleanupTests(servers)
544 })
545})