1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
3 import { expect } from 'chai'
4 import { readFile } from 'fs-extra'
5 import { completeCheckHlsPlaylist } from '@server/tests/shared'
6 import { buildAbsoluteFixturePath } from '@shared/core-utils'
9 RunnerJobSuccessPayload,
10 RunnerJobVODAudioMergeTranscodingPayload,
11 RunnerJobVODHLSTranscodingPayload,
13 RunnerJobVODWebVideoTranscodingPayload,
15 VODAudioMergeTranscodingSuccess,
16 VODHLSTranscodingSuccess,
17 VODWebVideoTranscodingSuccess
18 } from '@shared/models'
21 createMultipleServers,
26 setAccessTokensToServers,
27 setDefaultVideoChannel,
29 } from '@shared/server-commands'
31 async function processAllJobs (server: PeerTubeServer, runnerToken: string) {
33 const { availableJobs } = await server.runnerJobs.requestVOD({ runnerToken })
34 if (availableJobs.length === 0) break
36 const { job } = await server.runnerJobs.accept<RunnerJobVODPayload>({ runnerToken, jobUUID: availableJobs[0].uuid })
38 const payload: RunnerJobSuccessPayload = {
39 videoFile: `video_short_${job.payload.output.resolution}p.mp4`,
40 resolutionPlaylistFile: `video_short_${job.payload.output.resolution}p.m3u8`
42 await server.runnerJobs.success({ runnerToken, jobUUID: job.uuid, jobToken: job.jobToken, payload })
45 await waitJobs([ server ])
48 describe('Test runner VOD transcoding', function () {
49 let servers: PeerTubeServer[] = []
50 let runnerToken: string
52 before(async function () {
55 servers = await createMultipleServers(2)
57 await setAccessTokensToServers(servers)
58 await setDefaultVideoChannel(servers)
60 await doubleFollow(servers[0], servers[1])
62 await servers[0].config.enableRemoteTranscoding()
63 runnerToken = await servers[0].runners.autoRegisterRunner()
66 describe('Without transcoding', function () {
68 before(async function () {
71 await servers[0].config.disableTranscoding()
72 await servers[0].videos.quickUpload({ name: 'video' })
74 await waitJobs(servers)
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)
83 describe('With classic transcoding enabled', function () {
85 before(async function () {
88 await servers[0].config.enableTranscoding(true, true)
91 it('Should error a transcoding job', async function () {
94 await servers[0].runnerJobs.cancelAllJobs()
95 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
96 await waitJobs(servers)
98 const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
99 const jobUUID = availableJobs[0].uuid
101 const { job } = await servers[0].runnerJobs.accept({ runnerToken, jobUUID })
102 const jobToken = job.jobToken
104 await servers[0].runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
106 const video = await servers[0].videos.get({ id: uuid })
107 expect(video.state.id).to.equal(VideoState.TRANSCODING_FAILED)
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)
115 const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
116 const jobUUID = availableJobs[0].uuid
118 await servers[0].runnerJobs.cancelByAdmin({ jobUUID })
120 const video = await servers[0].videos.get({ id: uuid })
121 expect(video.state.id).to.equal(VideoState.PUBLISHED)
125 describe('Web video transcoding only', function () {
126 let videoUUID: string
130 before(async function () {
133 await servers[0].runnerJobs.cancelAllJobs()
134 await servers[0].config.enableTranscoding(true, false)
136 const { uuid } = await servers[0].videos.quickUpload({ name: 'web video', fixture: 'video_short.webm' })
139 await waitJobs(servers)
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)
146 jobUUID = availableJobs[0].uuid
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
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)
158 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
159 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.webm'))
161 expect(body).to.deep.equal(inputFile)
164 it('Should transcode the max video resolution and send it back to the server', async function () {
167 const payload: VODWebVideoTranscodingSuccess = {
168 videoFile: 'video_short.mp4'
170 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
172 await waitJobs(servers)
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)
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')))
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)
190 for (const resolution of [ 480, 360, 240, 144 ]) {
191 const job = availableJobs.find(j => j.payload.output.resolution === resolution)
193 expect(job.type).to.equal('vod-web-video-transcoding')
195 if (resolution === 240) jobUUID = job.uuid
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
203 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
204 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
206 expect(body).to.deep.equal(inputFile)
208 const payload: VODWebVideoTranscodingSuccess = { videoFile: 'video_short_240p.mp4' }
209 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
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)
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
221 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
222 jobToken = job.jobToken
224 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
225 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
226 expect(body).to.deep.equal(inputFile)
228 const payload: VODWebVideoTranscodingSuccess = { videoFile: `video_short_${resolution}p.mp4` }
229 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
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)
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')))
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 })
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)
255 describe('HLS transcoding only', function () {
256 let videoUUID: string
260 before(async function () {
263 await servers[0].config.enableTranscoding(false, true)
265 const { uuid } = await servers[0].videos.quickUpload({ name: 'hls video', fixture: 'video_short.webm' })
268 await waitJobs(servers)
271 it('Should run the optimize job', async function () {
274 await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
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)
281 for (const resolution of [ 720, 480, 360, 240, 144 ]) {
282 const job = availableJobs.find(j => j.payload.output.resolution === resolution)
284 expect(job.type).to.equal('vod-hls-transcoding')
286 if (resolution === 480) jobUUID = job.uuid
290 it('Should process one of these transcoding jobs', async function () {
293 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
294 jobToken = job.jobToken
296 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
297 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
299 expect(body).to.deep.equal(inputFile)
301 const payload: VODHLSTranscodingSuccess = {
302 videoFile: 'video_short_480p.mp4',
303 resolutionPlaylistFile: 'video_short_480p.m3u8'
305 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
307 await waitJobs(servers)
310 it('Should have the video updated', async function () {
311 for (const server of servers) {
312 const video = await server.videos.get({ id: videoUUID })
314 expect(video.files).to.have.lengthOf(1)
315 expect(video.streamingPlaylists).to.have.lengthOf(1)
317 const hls = video.streamingPlaylists[0]
318 expect(hls.files).to.have.lengthOf(1)
320 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
324 it('Should process all other jobs', async function () {
327 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
328 expect(availableJobs).to.have.lengthOf(4)
330 let maxQualityFile = 'video_short.mp4'
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
337 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
338 jobToken = job.jobToken
340 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
341 const inputFile = await readFile(buildAbsoluteFixturePath(maxQualityFile))
342 expect(body).to.deep.equal(inputFile)
344 const payload: VODHLSTranscodingSuccess = {
345 videoFile: `video_short_${resolution}p.mp4`,
346 resolutionPlaylistFile: `video_short_${resolution}p.m3u8`
348 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
350 if (resolution === 720) {
351 maxQualityFile = 'video_short_720p.mp4'
355 await waitJobs(servers)
358 it('Should have the video updated', async function () {
359 for (const server of servers) {
360 const video = await server.videos.get({ id: videoUUID })
362 expect(video.files).to.have.lengthOf(0)
363 expect(video.streamingPlaylists).to.have.lengthOf(1)
365 const hls = video.streamingPlaylists[0]
366 expect(hls.files).to.have.lengthOf(5)
368 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: true, servers, resolutions: [ 720, 480, 360, 240, 144 ] })
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)
378 describe('Web video and HLS transcoding', function () {
380 before(async function () {
383 await servers[0].config.enableTranscoding(true, true)
385 await servers[0].videos.quickUpload({ name: 'web video and hls video', fixture: 'video_short.webm' })
387 await waitJobs(servers)
390 it('Should process the first optimize job', async function () {
393 await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
396 it('Should have 9 jobs to process', async function () {
397 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
399 expect(availableJobs).to.have.lengthOf(9)
401 const webVideoJobs = availableJobs.filter(j => j.type === 'vod-web-video-transcoding')
402 const hlsJobs = availableJobs.filter(j => j.type === 'vod-hls-transcoding')
404 expect(webVideoJobs).to.have.lengthOf(4)
405 expect(hlsJobs).to.have.lengthOf(5)
408 it('Should process all available jobs', async function () {
409 await processAllJobs(servers[0], runnerToken)
413 describe('Audio merge transcoding', function () {
414 let videoUUID: string
418 before(async function () {
421 await servers[0].config.enableTranscoding(true, true)
423 const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
424 const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' })
427 await waitJobs(servers)
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)
434 expect(availableJobs[0].type).to.equal('vod-audio-merge-transcoding')
436 jobUUID = availableJobs[0].uuid
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
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)
449 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.audioFileUrl, jobToken, runnerToken })
450 const inputFile = await readFile(buildAbsoluteFixturePath('sample.ogg'))
451 expect(body).to.deep.equal(inputFile)
455 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.previewFileUrl, jobToken, runnerToken })
457 const video = await servers[0].videos.get({ id: videoUUID })
458 const { body: inputFile } = await makeGetRequest({
460 path: video.previewPath,
461 expectedStatus: HttpStatusCode.OK_200
464 expect(body).to.deep.equal(inputFile)
468 it('Should merge the audio', async function () {
471 const payload: VODAudioMergeTranscodingSuccess = { videoFile: 'video_short_480p.mp4' }
472 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
474 await waitJobs(servers)
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)
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')))
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)
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)
497 jobUUID = availableJobs.find(j => j.payload.output.resolution === 480).uuid
500 it('Should process one other job', async function () {
503 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
504 jobToken = job.jobToken
506 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
507 const inputFile = await readFile(buildAbsoluteFixturePath('video_short_480p.mp4'))
508 expect(body).to.deep.equal(inputFile)
510 const payload: VODHLSTranscodingSuccess = {
511 videoFile: `video_short_480p.mp4`,
512 resolutionPlaylistFile: `video_short_480p.m3u8`
514 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
516 await waitJobs(servers)
519 it('Should have the video updated', async function () {
520 for (const server of servers) {
521 const video = await server.videos.get({ id: videoUUID })
523 expect(video.files).to.have.lengthOf(1)
524 expect(video.streamingPlaylists).to.have.lengthOf(1)
526 const hls = video.streamingPlaylists[0]
527 expect(hls.files).to.have.lengthOf(1)
529 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
533 it('Should process all available jobs', async function () {
534 await processAllJobs(servers[0], runnerToken)
538 after(async function () {
539 await cleanupTests(servers)