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 for (let i = 0; i < 5; i++) {
102 const { job } = await servers[0].runnerJobs.accept({ runnerToken, jobUUID })
103 const jobToken = job.jobToken
105 await servers[0].runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
108 const video = await servers[0].videos.get({ id: uuid })
109 expect(video.state.id).to.equal(VideoState.TRANSCODING_FAILED)
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)
117 const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
118 const jobUUID = availableJobs[0].uuid
120 await servers[0].runnerJobs.cancelByAdmin({ jobUUID })
122 const video = await servers[0].videos.get({ id: uuid })
123 expect(video.state.id).to.equal(VideoState.PUBLISHED)
127 describe('Web video transcoding only', function () {
128 let videoUUID: string
132 before(async function () {
135 await servers[0].runnerJobs.cancelAllJobs()
136 await servers[0].config.enableTranscoding(true, false)
138 const { uuid } = await servers[0].videos.quickUpload({ name: 'web video', fixture: 'video_short.webm' })
141 await waitJobs(servers)
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)
148 jobUUID = availableJobs[0].uuid
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
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)
160 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
161 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.webm'))
163 expect(body).to.deep.equal(inputFile)
166 it('Should transcode the max video resolution and send it back to the server', async function () {
169 const payload: VODWebVideoTranscodingSuccess = {
170 videoFile: 'video_short.mp4'
172 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
174 await waitJobs(servers)
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)
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')))
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)
192 for (const resolution of [ 480, 360, 240, 144 ]) {
193 const job = availableJobs.find(j => j.payload.output.resolution === resolution)
195 expect(job.type).to.equal('vod-web-video-transcoding')
197 if (resolution === 240) jobUUID = job.uuid
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
205 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
206 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
208 expect(body).to.deep.equal(inputFile)
210 const payload: VODWebVideoTranscodingSuccess = { videoFile: `video_short_${job.payload.output.resolution}p.mp4` }
211 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
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)
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
223 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
224 jobToken = job.jobToken
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)
230 const payload: VODWebVideoTranscodingSuccess = { videoFile: `video_short_${resolution}p.mp4` }
231 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
234 await waitJobs(servers)
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)
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')))
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 })
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)
259 describe('HLS transcoding only', function () {
260 let videoUUID: string
264 before(async function () {
267 await servers[0].config.enableTranscoding(false, true)
269 const { uuid } = await servers[0].videos.quickUpload({ name: 'hls video', fixture: 'video_short.webm' })
272 await waitJobs(servers)
275 it('Should run the optimize job', async function () {
278 await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
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)
285 for (const resolution of [ 720, 480, 360, 240, 144 ]) {
286 const job = availableJobs.find(j => j.payload.output.resolution === resolution)
288 expect(job.type).to.equal('vod-hls-transcoding')
290 if (resolution === 480) jobUUID = job.uuid
294 it('Should process one of these transcoding jobs', async function () {
297 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
298 jobToken = job.jobToken
300 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
301 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
303 expect(body).to.deep.equal(inputFile)
305 const payload: VODHLSTranscodingSuccess = {
306 videoFile: 'video_short_480p.mp4',
307 resolutionPlaylistFile: 'video_short_480p.m3u8'
309 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
311 await waitJobs(servers)
314 it('Should have the video updated', async function () {
315 for (const server of servers) {
316 const video = await server.videos.get({ id: videoUUID })
318 expect(video.files).to.have.lengthOf(1)
319 expect(video.streamingPlaylists).to.have.lengthOf(1)
321 const hls = video.streamingPlaylists[0]
322 expect(hls.files).to.have.lengthOf(1)
324 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
328 it('Should process all other jobs', async function () {
331 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
332 expect(availableJobs).to.have.lengthOf(4)
334 let maxQualityFile = 'video_short.mp4'
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
341 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
342 jobToken = job.jobToken
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)
348 const payload: VODHLSTranscodingSuccess = {
349 videoFile: `video_short_${resolution}p.mp4`,
350 resolutionPlaylistFile: `video_short_${resolution}p.m3u8`
352 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
354 if (resolution === 720) {
355 maxQualityFile = 'video_short_720p.mp4'
359 await waitJobs(servers)
362 it('Should have the video updated', async function () {
363 for (const server of servers) {
364 const video = await server.videos.get({ id: videoUUID })
366 expect(video.files).to.have.lengthOf(0)
367 expect(video.streamingPlaylists).to.have.lengthOf(1)
369 const hls = video.streamingPlaylists[0]
370 expect(hls.files).to.have.lengthOf(5)
372 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: true, servers, resolutions: [ 720, 480, 360, 240, 144 ] })
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)
382 describe('Web video and HLS transcoding', function () {
384 before(async function () {
387 await servers[0].config.enableTranscoding(true, true)
389 await servers[0].videos.quickUpload({ name: 'web video and hls video', fixture: 'video_short.webm' })
391 await waitJobs(servers)
394 it('Should process the first optimize job', async function () {
397 await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
400 it('Should have 9 jobs to process', async function () {
401 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
403 expect(availableJobs).to.have.lengthOf(9)
405 const webVideoJobs = availableJobs.filter(j => j.type === 'vod-web-video-transcoding')
406 const hlsJobs = availableJobs.filter(j => j.type === 'vod-hls-transcoding')
408 expect(webVideoJobs).to.have.lengthOf(4)
409 expect(hlsJobs).to.have.lengthOf(5)
412 it('Should process all available jobs', async function () {
413 await processAllJobs(servers[0], runnerToken)
417 describe('Audio merge transcoding', function () {
418 let videoUUID: string
422 before(async function () {
425 await servers[0].config.enableTranscoding(true, true)
427 const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
428 const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' })
431 await waitJobs(servers)
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)
438 expect(availableJobs[0].type).to.equal('vod-audio-merge-transcoding')
440 jobUUID = availableJobs[0].uuid
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
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)
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)
459 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.previewFileUrl, jobToken, runnerToken })
461 const video = await servers[0].videos.get({ id: videoUUID })
462 const { body: inputFile } = await makeGetRequest({
464 path: video.previewPath,
465 expectedStatus: HttpStatusCode.OK_200
468 expect(body).to.deep.equal(inputFile)
472 it('Should merge the audio', async function () {
475 const payload: VODAudioMergeTranscodingSuccess = { videoFile: 'video_short_480p.mp4' }
476 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
478 await waitJobs(servers)
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)
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')))
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)
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)
501 jobUUID = availableJobs.find(j => j.payload.output.resolution === 480).uuid
504 it('Should process one other job', async function () {
507 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
508 jobToken = job.jobToken
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)
514 const payload: VODHLSTranscodingSuccess = {
515 videoFile: `video_short_480p.mp4`,
516 resolutionPlaylistFile: `video_short_480p.m3u8`
518 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
520 await waitJobs(servers)
523 it('Should have the video updated', async function () {
524 for (const server of servers) {
525 const video = await server.videos.get({ id: videoUUID })
527 expect(video.files).to.have.lengthOf(1)
528 expect(video.streamingPlaylists).to.have.lengthOf(1)
530 const hls = video.streamingPlaylists[0]
531 expect(hls.files).to.have.lengthOf(1)
533 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
537 it('Should process all available jobs', async function () {
538 await processAllJobs(servers[0], runnerToken)
542 after(async function () {
543 await cleanupTests(servers)