]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/tests/api/transcoding/transcoder.ts
Add runner server tests
[github/Chocobozzz/PeerTube.git] / server / tests / api / transcoding / transcoder.ts
CommitLineData
a1587156 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
0e1dc3e7 2
bbd5aa7e 3import { expect } from 'chai'
d102de1b
C
4import { canDoQuickTranscode } from '@server/lib/transcoding/transcoding-quick-transcode'
5import { checkWebTorrentWorks, generateHighBitrateVideo, generateVideoWithFramerate } from '@server/tests/shared'
367a9dc6 6import { buildAbsoluteFixturePath, getAllFiles, getMaxBitrate, getMinLimitBitrate, omit } from '@shared/core-utils'
c729caf6 7import {
d102de1b 8 ffprobePromise,
84cae54e 9 getAudioStream,
c729caf6 10 getVideoStreamBitrate,
c729caf6 11 getVideoStreamDimensionsInfo,
84cae54e 12 getVideoStreamFPS,
c729caf6 13 hasAudioStream
d102de1b
C
14} from '@shared/ffmpeg'
15import { HttpStatusCode, VideoFileMetadata, VideoState } from '@shared/models'
0e1dc3e7 16import {
7243f84d 17 cleanupTests,
254d3579 18 createMultipleServers,
4c7e60bc 19 doubleFollow,
b345a804 20 makeGetRequest,
254d3579 21 PeerTubeServer,
2186386c 22 setAccessTokensToServers,
d102de1b 23 waitJobs
bf54587a 24} from '@shared/server-commands'
a7ba16b6 25
254d3579 26function updateConfigForTranscoding (server: PeerTubeServer) {
89d241a7 27 return server.config.updateCustomSubConfig({
65e6e260
C
28 newConfig: {
29 transcoding: {
30 enabled: true,
31 allowAdditionalExtensions: true,
32 allowAudioFiles: true,
33 hls: { enabled: true },
34 webtorrent: { enabled: true },
35 resolutions: {
36 '0p': false,
8dd754c7 37 '144p': true,
65e6e260
C
38 '240p': true,
39 '360p': true,
40 '480p': true,
41 '720p': true,
42 '1080p': true,
43 '1440p': true,
44 '2160p': true
45 }
40930fda
C
46 }
47 }
48 })
49}
50
0e1dc3e7 51describe('Test video transcoding', function () {
254d3579 52 let servers: PeerTubeServer[] = []
6939cbac 53 let video4k: string
0e1dc3e7
C
54
55 before(async function () {
454c20fa 56 this.timeout(30_000)
0e1dc3e7
C
57
58 // Run servers
254d3579 59 servers = await createMultipleServers(2)
0e1dc3e7
C
60
61 await setAccessTokensToServers(servers)
b2977eec
C
62
63 await doubleFollow(servers[0], servers[1])
40930fda
C
64
65 await updateConfigForTranscoding(servers[1])
0e1dc3e7
C
66 })
67
40930fda 68 describe('Basic transcoding (or not)', function () {
0e1dc3e7 69
40930fda
C
70 it('Should not transcode video on server 1', async function () {
71 this.timeout(60_000)
0e1dc3e7 72
d23dd9fb 73 const attributes = {
40930fda
C
74 name: 'my super name for server 1',
75 description: 'my super description for server 1',
76 fixture: 'video_short.webm'
77 }
89d241a7 78 await servers[0].videos.upload({ attributes })
0e1dc3e7 79
40930fda 80 await waitJobs(servers)
40298b02 81
40930fda 82 for (const server of servers) {
89d241a7 83 const { data } = await server.videos.list()
d23dd9fb 84 const video = data[0]
5f04dd2f 85
89d241a7 86 const videoDetails = await server.videos.get({ id: video.id })
40930fda 87 expect(videoDetails.files).to.have.lengthOf(1)
0e1dc3e7 88
40930fda
C
89 const magnetUri = videoDetails.files[0].magnetUri
90 expect(magnetUri).to.match(/\.webm/)
0e1dc3e7 91
d102de1b 92 await checkWebTorrentWorks(magnetUri, /\.webm$/)
40930fda
C
93 }
94 })
0e1dc3e7 95
40930fda
C
96 it('Should transcode video on server 2', async function () {
97 this.timeout(120_000)
98
d23dd9fb 99 const attributes = {
40930fda
C
100 name: 'my super name for server 2',
101 description: 'my super description for server 2',
102 fixture: 'video_short.webm'
103 }
89d241a7 104 await servers[1].videos.upload({ attributes })
40930fda
C
105
106 await waitJobs(servers)
107
108 for (const server of servers) {
89d241a7 109 const { data } = await server.videos.list()
0e1dc3e7 110
d23dd9fb 111 const video = data.find(v => v.name === attributes.name)
89d241a7 112 const videoDetails = await server.videos.get({ id: video.id })
0e1dc3e7 113
8dd754c7 114 expect(videoDetails.files).to.have.lengthOf(5)
0e1dc3e7 115
40930fda
C
116 const magnetUri = videoDetails.files[0].magnetUri
117 expect(magnetUri).to.match(/\.mp4/)
5f04dd2f 118
d102de1b 119 await checkWebTorrentWorks(magnetUri, /\.mp4$/)
40930fda
C
120 }
121 })
40298b02 122
40930fda
C
123 it('Should wait for transcoding before publishing the video', async function () {
124 this.timeout(160_000)
0e1dc3e7 125
40930fda
C
126 {
127 // Upload the video, but wait transcoding
d23dd9fb 128 const attributes = {
40930fda
C
129 name: 'waiting video',
130 fixture: 'video_short1.webm',
131 waitTranscoding: true
132 }
89d241a7 133 const { uuid } = await servers[1].videos.upload({ attributes })
d23dd9fb 134 const videoId = uuid
40930fda
C
135
136 // Should be in transcode state
89d241a7 137 const body = await servers[1].videos.get({ id: videoId })
40930fda
C
138 expect(body.name).to.equal('waiting video')
139 expect(body.state.id).to.equal(VideoState.TO_TRANSCODE)
140 expect(body.state.label).to.equal('To transcode')
141 expect(body.waitTranscoding).to.be.true
142
d23dd9fb
C
143 {
144 // Should have my video
89d241a7 145 const { data } = await servers[1].videos.listMyVideos()
d23dd9fb
C
146 const videoToFindInMine = data.find(v => v.name === attributes.name)
147 expect(videoToFindInMine).not.to.be.undefined
148 expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE)
149 expect(videoToFindInMine.state.label).to.equal('To transcode')
150 expect(videoToFindInMine.waitTranscoding).to.be.true
151 }
40930fda 152
d23dd9fb
C
153 {
154 // Should not list this video
89d241a7 155 const { data } = await servers[1].videos.list()
d23dd9fb
C
156 const videoToFindInList = data.find(v => v.name === attributes.name)
157 expect(videoToFindInList).to.be.undefined
158 }
40930fda
C
159
160 // Server 1 should not have the video yet
89d241a7 161 await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
40930fda 162 }
0e1dc3e7 163
40930fda 164 await waitJobs(servers)
7160878c 165
40930fda 166 for (const server of servers) {
89d241a7 167 const { data } = await server.videos.list()
d23dd9fb 168 const videoToFind = data.find(v => v.name === 'waiting video')
40930fda 169 expect(videoToFind).not.to.be.undefined
7160878c 170
89d241a7 171 const videoDetails = await server.videos.get({ id: videoToFind.id })
7160878c 172
40930fda
C
173 expect(videoDetails.state.id).to.equal(VideoState.PUBLISHED)
174 expect(videoDetails.state.label).to.equal('Published')
175 expect(videoDetails.waitTranscoding).to.be.true
176 }
177 })
7160878c 178
40930fda
C
179 it('Should accept and transcode additional extensions', async function () {
180 this.timeout(300_000)
7160878c 181
40930fda 182 for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) {
d23dd9fb 183 const attributes = {
40930fda
C
184 name: fixture,
185 fixture
186 }
7160878c 187
89d241a7 188 await servers[1].videos.upload({ attributes })
7160878c 189
40930fda 190 await waitJobs(servers)
7160878c 191
40930fda 192 for (const server of servers) {
89d241a7 193 const { data } = await server.videos.list()
7160878c 194
d23dd9fb 195 const video = data.find(v => v.name === attributes.name)
89d241a7 196 const videoDetails = await server.videos.get({ id: video.id })
8dd754c7 197 expect(videoDetails.files).to.have.lengthOf(5)
7160878c 198
40930fda
C
199 const magnetUri = videoDetails.files[0].magnetUri
200 expect(magnetUri).to.contain('.mp4')
201 }
202 }
203 })
7160878c 204
40930fda
C
205 it('Should transcode a 4k video', async function () {
206 this.timeout(200_000)
7160878c 207
d23dd9fb 208 const attributes = {
40930fda
C
209 name: '4k video',
210 fixture: 'video_short_4k.mp4'
211 }
7160878c 212
89d241a7 213 const { uuid } = await servers[1].videos.upload({ attributes })
d23dd9fb 214 video4k = uuid
b2977eec 215
40930fda 216 await waitJobs(servers)
b2977eec 217
8dd754c7 218 const resolutions = [ 144, 240, 360, 480, 720, 1080, 1440, 2160 ]
ca5c612b 219
40930fda 220 for (const server of servers) {
89d241a7 221 const videoDetails = await server.videos.get({ id: video4k })
40930fda 222 expect(videoDetails.files).to.have.lengthOf(resolutions.length)
ca5c612b 223
40930fda
C
224 for (const r of resolutions) {
225 expect(videoDetails.files.find(f => f.resolution.id === r)).to.not.be.undefined
226 expect(videoDetails.streamingPlaylists[0].files.find(f => f.resolution.id === r)).to.not.be.undefined
227 }
b2977eec 228 }
40930fda 229 })
7160878c
RK
230 })
231
40930fda 232 describe('Audio transcoding', function () {
73c69591 233
40930fda
C
234 it('Should transcode high bit rate mp3 to proper bit rate', async function () {
235 this.timeout(60_000)
73c69591 236
d23dd9fb 237 const attributes = {
40930fda
C
238 name: 'mp3_256k',
239 fixture: 'video_short_mp3_256k.mp4'
240 }
89d241a7 241 await servers[1].videos.upload({ attributes })
73c69591 242
40930fda 243 await waitJobs(servers)
73c69591 244
40930fda 245 for (const server of servers) {
89d241a7 246 const { data } = await server.videos.list()
73c69591 247
d23dd9fb 248 const video = data.find(v => v.name === attributes.name)
89d241a7 249 const videoDetails = await server.videos.get({ id: video.id })
73c69591 250
8dd754c7 251 expect(videoDetails.files).to.have.lengthOf(5)
73c69591 252
83903cb6
C
253 const file = videoDetails.files.find(f => f.resolution.id === 240)
254 const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
40930fda
C
255 const probe = await getAudioStream(path)
256
257 if (probe.audioStream) {
258 expect(probe.audioStream['codec_name']).to.be.equal('aac')
259 expect(probe.audioStream['bit_rate']).to.be.at.most(384 * 8000)
260 } else {
261 this.fail('Could not retrieve the audio stream on ' + probe.absolutePath)
262 }
b2977eec 263 }
40930fda 264 })
3a6f351b 265
40930fda
C
266 it('Should transcode video with no audio and have no audio itself', async function () {
267 this.timeout(60_000)
2186386c 268
d23dd9fb 269 const attributes = {
40930fda
C
270 name: 'no_audio',
271 fixture: 'video_short_no_audio.mp4'
272 }
89d241a7 273 await servers[1].videos.upload({ attributes })
2186386c 274
40930fda 275 await waitJobs(servers)
2186386c 276
40930fda 277 for (const server of servers) {
89d241a7 278 const { data } = await server.videos.list()
2186386c 279
d23dd9fb 280 const video = data.find(v => v.name === attributes.name)
89d241a7 281 const videoDetails = await server.videos.get({ id: video.id })
2186386c 282
83903cb6
C
283 const file = videoDetails.files.find(f => f.resolution.id === 240)
284 const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
285
c729caf6 286 expect(await hasAudioStream(path)).to.be.false
40930fda
C
287 }
288 })
2186386c 289
40930fda
C
290 it('Should leave the audio untouched, but properly transcode the video', async function () {
291 this.timeout(60_000)
edb4ffc7 292
d23dd9fb 293 const attributes = {
40930fda
C
294 name: 'untouched_audio',
295 fixture: 'video_short.mp4'
296 }
89d241a7 297 await servers[1].videos.upload({ attributes })
74cd011b 298
40930fda 299 await waitJobs(servers)
edb4ffc7 300
40930fda 301 for (const server of servers) {
89d241a7 302 const { data } = await server.videos.list()
edb4ffc7 303
d23dd9fb 304 const video = data.find(v => v.name === attributes.name)
89d241a7 305 const videoDetails = await server.videos.get({ id: video.id })
edb4ffc7 306
8dd754c7 307 expect(videoDetails.files).to.have.lengthOf(5)
edb4ffc7 308
d23dd9fb 309 const fixturePath = buildAbsoluteFixturePath(attributes.fixture)
40930fda 310 const fixtureVideoProbe = await getAudioStream(fixturePath)
83903cb6
C
311
312 const file = videoDetails.files.find(f => f.resolution.id === 240)
313 const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
edb4ffc7 314
40930fda 315 const videoProbe = await getAudioStream(path)
edb4ffc7 316
40930fda
C
317 if (videoProbe.audioStream && fixtureVideoProbe.audioStream) {
318 const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ]
319 expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit))
320 } else {
321 this.fail('Could not retrieve the audio stream on ' + videoProbe.absolutePath)
322 }
323 }
324 })
325 })
edb4ffc7 326
40930fda
C
327 describe('Audio upload', function () {
328
f6d6e7f8 329 function runSuite (mode: 'legacy' | 'resumable') {
330
331 before(async function () {
89d241a7 332 await servers[1].config.updateCustomSubConfig({
65e6e260
C
333 newConfig: {
334 transcoding: {
335 hls: { enabled: true },
336 webtorrent: { enabled: true },
337 resolutions: {
338 '0p': false,
8dd754c7 339 '144p': false,
65e6e260
C
340 '240p': false,
341 '360p': false,
342 '480p': false,
343 '720p': false,
344 '1080p': false,
345 '1440p': false,
346 '2160p': false
347 }
f6d6e7f8 348 }
40930fda 349 }
f6d6e7f8 350 })
40930fda 351 })
edb4ffc7 352
f6d6e7f8 353 it('Should merge an audio file with the preview file', async function () {
354 this.timeout(60_000)
edb4ffc7 355
d23dd9fb 356 const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
89d241a7 357 await servers[1].videos.upload({ attributes, mode })
14e2014a 358
f6d6e7f8 359 await waitJobs(servers)
7ed2c1a4 360
f6d6e7f8 361 for (const server of servers) {
89d241a7 362 const { data } = await server.videos.list()
7ed2c1a4 363
d23dd9fb 364 const video = data.find(v => v.name === 'audio_with_preview')
89d241a7 365 const videoDetails = await server.videos.get({ id: video.id })
7ed2c1a4 366
f6d6e7f8 367 expect(videoDetails.files).to.have.lengthOf(1)
14e2014a 368
c0e8b12e
C
369 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
370 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, expectedStatus: HttpStatusCode.OK_200 })
40930fda 371
f6d6e7f8 372 const magnetUri = videoDetails.files[0].magnetUri
373 expect(magnetUri).to.contain('.mp4')
374 }
375 })
14e2014a 376
f6d6e7f8 377 it('Should upload an audio file and choose a default background image', async function () {
378 this.timeout(60_000)
14e2014a 379
d23dd9fb 380 const attributes = { name: 'audio_without_preview', fixture: 'sample.ogg' }
89d241a7 381 await servers[1].videos.upload({ attributes, mode })
14e2014a 382
f6d6e7f8 383 await waitJobs(servers)
14e2014a 384
f6d6e7f8 385 for (const server of servers) {
89d241a7 386 const { data } = await server.videos.list()
40930fda 387
d23dd9fb 388 const video = data.find(v => v.name === 'audio_without_preview')
89d241a7 389 const videoDetails = await server.videos.get({ id: video.id })
14e2014a 390
f6d6e7f8 391 expect(videoDetails.files).to.have.lengthOf(1)
14e2014a 392
c0e8b12e
C
393 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
394 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, expectedStatus: HttpStatusCode.OK_200 })
7ed2c1a4 395
f6d6e7f8 396 const magnetUri = videoDetails.files[0].magnetUri
397 expect(magnetUri).to.contain('.mp4')
40930fda
C
398 }
399 })
7ed2c1a4 400
f6d6e7f8 401 it('Should upload an audio file and create an audio version only', async function () {
402 this.timeout(60_000)
403
89d241a7 404 await servers[1].config.updateCustomSubConfig({
65e6e260
C
405 newConfig: {
406 transcoding: {
407 hls: { enabled: true },
408 webtorrent: { enabled: true },
409 resolutions: {
410 '0p': true,
8dd754c7 411 '144p': false,
65e6e260
C
412 '240p': false,
413 '360p': false
414 }
f6d6e7f8 415 }
416 }
417 })
b345a804 418
d23dd9fb 419 const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
89d241a7 420 const { id } = await servers[1].videos.upload({ attributes, mode })
b345a804 421
f6d6e7f8 422 await waitJobs(servers)
b345a804 423
f6d6e7f8 424 for (const server of servers) {
89d241a7 425 const videoDetails = await server.videos.get({ id })
f6d6e7f8 426
427 for (const files of [ videoDetails.files, videoDetails.streamingPlaylists[0].files ]) {
428 expect(files).to.have.lengthOf(2)
429 expect(files.find(f => f.resolution.id === 0)).to.not.be.undefined
430 }
40930fda 431 }
b345a804 432
f6d6e7f8 433 await updateConfigForTranscoding(servers[1])
434 })
435 }
436
437 describe('Legacy upload', function () {
438 runSuite('legacy')
439 })
440
441 describe('Resumable upload', function () {
442 runSuite('resumable')
40930fda
C
443 })
444 })
b345a804 445
40930fda 446 describe('Framerate', function () {
b345a804 447
40930fda
C
448 it('Should transcode a 60 FPS video', async function () {
449 this.timeout(60_000)
b345a804 450
d23dd9fb 451 const attributes = {
40930fda
C
452 name: 'my super 30fps name for server 2',
453 description: 'my super 30fps description for server 2',
454 fixture: '60fps_720p_small.mp4'
455 }
89d241a7 456 await servers[1].videos.upload({ attributes })
b345a804 457
40930fda 458 await waitJobs(servers)
b345a804 459
40930fda 460 for (const server of servers) {
89d241a7 461 const { data } = await server.videos.list()
b345a804 462
d23dd9fb 463 const video = data.find(v => v.name === attributes.name)
89d241a7 464 const videoDetails = await server.videos.get({ id: video.id })
b345a804 465
8dd754c7 466 expect(videoDetails.files).to.have.lengthOf(5)
40930fda
C
467 expect(videoDetails.files[0].fps).to.be.above(58).and.below(62)
468 expect(videoDetails.files[1].fps).to.be.below(31)
469 expect(videoDetails.files[2].fps).to.be.below(31)
470 expect(videoDetails.files[3].fps).to.be.below(31)
8dd754c7 471 expect(videoDetails.files[4].fps).to.be.below(31)
b345a804 472
8dd754c7 473 for (const resolution of [ 144, 240, 360, 480 ]) {
83903cb6
C
474 const file = videoDetails.files.find(f => f.resolution.id === resolution)
475 const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
c729caf6 476 const fps = await getVideoStreamFPS(path)
b345a804 477
40930fda
C
478 expect(fps).to.be.below(31)
479 }
b345a804 480
83903cb6
C
481 const file = videoDetails.files.find(f => f.resolution.id === 720)
482 const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
c729caf6 483 const fps = await getVideoStreamFPS(path)
b345a804 484
40930fda
C
485 expect(fps).to.be.above(58).and.below(62)
486 }
487 })
b345a804 488
40930fda
C
489 it('Should downscale to the closest divisor standard framerate', async function () {
490 this.timeout(200_000)
837666fe 491
40930fda 492 let tempFixturePath: string
837666fe 493
40930fda
C
494 {
495 tempFixturePath = await generateVideoWithFramerate(59)
837666fe 496
c729caf6 497 const fps = await getVideoStreamFPS(tempFixturePath)
40930fda
C
498 expect(fps).to.be.equal(59)
499 }
837666fe 500
d23dd9fb 501 const attributes = {
40930fda
C
502 name: '59fps video',
503 description: '59fps video',
504 fixture: tempFixturePath
505 }
837666fe 506
89d241a7 507 await servers[1].videos.upload({ attributes })
837666fe 508
40930fda 509 await waitJobs(servers)
837666fe 510
40930fda 511 for (const server of servers) {
89d241a7 512 const { data } = await server.videos.list()
837666fe 513
83903cb6
C
514 const { id } = data.find(v => v.name === attributes.name)
515 const video = await server.videos.get({ id })
837666fe 516
40930fda 517 {
83903cb6
C
518 const file = video.files.find(f => f.resolution.id === 240)
519 const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
c729caf6 520 const fps = await getVideoStreamFPS(path)
40930fda
C
521 expect(fps).to.be.equal(25)
522 }
523
524 {
83903cb6
C
525 const file = video.files.find(f => f.resolution.id === 720)
526 const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
c729caf6 527 const fps = await getVideoStreamFPS(path)
40930fda
C
528 expect(fps).to.be.equal(59)
529 }
c7f36e4f 530 }
40930fda
C
531 })
532 })
533
534 describe('Bitrate control', function () {
83903cb6 535
40930fda
C
536 it('Should respect maximum bitrate values', async function () {
537 this.timeout(160_000)
538
679c12e6 539 const tempFixturePath = await generateHighBitrateVideo()
d218e7de 540
d23dd9fb 541 const attributes = {
40930fda
C
542 name: 'high bitrate video',
543 description: 'high bitrate video',
544 fixture: tempFixturePath
545 }
d218e7de 546
89d241a7 547 await servers[1].videos.upload({ attributes })
d218e7de 548
40930fda 549 await waitJobs(servers)
d218e7de 550
40930fda 551 for (const server of servers) {
89d241a7 552 const { data } = await server.videos.list()
d218e7de 553
83903cb6
C
554 const { id } = data.find(v => v.name === attributes.name)
555 const video = await server.videos.get({ id })
8319d6ae 556
83903cb6
C
557 for (const resolution of [ 240, 360, 480, 720, 1080 ]) {
558 const file = video.files.find(f => f.resolution.id === resolution)
559 const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
8319d6ae 560
c729caf6
C
561 const bitrate = await getVideoStreamBitrate(path)
562 const fps = await getVideoStreamFPS(path)
563 const dataResolution = await getVideoStreamDimensionsInfo(path)
679c12e6
C
564
565 expect(resolution).to.equal(resolution)
8319d6ae 566
679c12e6
C
567 const maxBitrate = getMaxBitrate({ ...dataResolution, fps })
568 expect(bitrate).to.be.below(maxBitrate)
40930fda 569 }
7b81edc8 570 }
40930fda 571 })
8319d6ae 572
d78b51aa 573 it('Should not transcode to an higher bitrate than the original file but above our low limit', async function () {
40930fda
C
574 this.timeout(160_000)
575
65e6e260 576 const newConfig = {
40930fda
C
577 transcoding: {
578 enabled: true,
579 resolutions: {
8dd754c7 580 '144p': true,
40930fda
C
581 '240p': true,
582 '360p': true,
583 '480p': true,
584 '720p': true,
585 '1080p': true,
586 '1440p': true,
587 '2160p': true
588 },
589 webtorrent: { enabled: true },
590 hls: { enabled: true }
591 }
8319d6ae 592 }
89d241a7 593 await servers[1].config.updateCustomSubConfig({ newConfig })
7b81edc8 594
d23dd9fb 595 const attributes = {
40930fda
C
596 name: 'low bitrate',
597 fixture: 'low-bitrate.mp4'
598 }
8319d6ae 599
83903cb6 600 const { id } = await servers[1].videos.upload({ attributes })
7b81edc8 601
40930fda 602 await waitJobs(servers)
8319d6ae 603
83903cb6
C
604 const video = await servers[1].videos.get({ id })
605
40930fda
C
606 const resolutions = [ 240, 360, 480, 720, 1080 ]
607 for (const r of resolutions) {
83903cb6
C
608 const file = video.files.find(f => f.resolution.id === r)
609
610 const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
c729caf6 611 const bitrate = await getVideoStreamBitrate(path)
d78b51aa
C
612
613 const inputBitrate = 60_000
614 const limit = getMinLimitBitrate({ fps: 10, ratio: 1, resolution: r })
615 let belowValue = Math.max(inputBitrate, limit)
616 belowValue += belowValue * 0.20 // Apply 20% margin because bitrate control is not very precise
617
618 expect(bitrate, `${path} not below ${limit}`).to.be.below(belowValue)
8319d6ae 619 }
40930fda 620 })
8319d6ae
RK
621 })
622
40930fda 623 describe('FFprobe', function () {
f5961a8c 624
40930fda
C
625 it('Should provide valid ffprobe data', async function () {
626 this.timeout(160_000)
f5961a8c 627
89d241a7 628 const videoUUID = (await servers[1].videos.quickUpload({ name: 'ffprobe data' })).uuid
40930fda 629 await waitJobs(servers)
f5961a8c 630
40930fda 631 {
83903cb6
C
632 const video = await servers[1].videos.get({ id: videoUUID })
633 const file = video.files.find(f => f.resolution.id === 240)
634 const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
d102de1b
C
635
636 const probe = await ffprobePromise(path)
637 const metadata = new VideoFileMetadata(probe)
40930fda
C
638
639 // expected format properties
640 for (const p of [
641 'tags.encoder',
642 'format_long_name',
643 'size',
644 'bit_rate'
645 ]) {
646 expect(metadata.format).to.have.nested.property(p)
647 }
648
649 // expected stream properties
650 for (const p of [
651 'codec_long_name',
652 'profile',
653 'width',
654 'height',
655 'display_aspect_ratio',
656 'avg_frame_rate',
657 'pix_fmt'
658 ]) {
659 expect(metadata.streams[0]).to.have.nested.property(p)
660 }
661
662 expect(metadata).to.not.have.nested.property('format.filename')
663 }
f5961a8c 664
40930fda 665 for (const server of servers) {
89d241a7 666 const videoDetails = await server.videos.get({ id: videoUUID })
40930fda 667
c729caf6 668 const videoFiles = getAllFiles(videoDetails)
8dd754c7 669 expect(videoFiles).to.have.lengthOf(10)
40930fda
C
670
671 for (const file of videoFiles) {
672 expect(file.metadata).to.be.undefined
673 expect(file.metadataUrl).to.exist
674 expect(file.metadataUrl).to.contain(servers[1].url)
675 expect(file.metadataUrl).to.contain(videoUUID)
676
89d241a7 677 const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl })
40930fda
C
678 expect(metadata).to.have.nested.property('format.size')
679 }
680 }
681 })
f5961a8c 682
40930fda
C
683 it('Should correctly detect if quick transcode is possible', async function () {
684 this.timeout(10_000)
f5961a8c 685
40930fda
C
686 expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true
687 expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false
688 })
f5961a8c
C
689 })
690
40930fda 691 describe('Transcoding job queue', function () {
6939cbac 692
40930fda 693 it('Should have the appropriate priorities for transcoding jobs', async function () {
851675c5 694 const body = await servers[1].jobs.list({
40930fda
C
695 start: 0,
696 count: 100,
8dd754c7 697 sort: 'createdAt',
40930fda
C
698 jobType: 'video-transcoding'
699 })
6939cbac 700
9c6327f8 701 const jobs = body.data
40930fda 702 const transcodingJobs = jobs.filter(j => j.data.videoUUID === video4k)
6939cbac 703
8dd754c7 704 expect(transcodingJobs).to.have.lengthOf(16)
6939cbac 705
40930fda
C
706 const hlsJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-hls')
707 const webtorrentJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-webtorrent')
708 const optimizeJobs = transcodingJobs.filter(j => j.data.type === 'optimize-to-webtorrent')
6939cbac 709
8dd754c7
FC
710 expect(hlsJobs).to.have.lengthOf(8)
711 expect(webtorrentJobs).to.have.lengthOf(7)
40930fda 712 expect(optimizeJobs).to.have.lengthOf(1)
6939cbac 713
a6e37eeb 714 for (const j of optimizeJobs.concat(hlsJobs.concat(webtorrentJobs))) {
40930fda
C
715 expect(j.priority).to.be.greaterThan(100)
716 expect(j.priority).to.be.lessThan(150)
717 }
718 })
6939cbac
C
719 })
720
84cae54e
C
721 describe('Bounded transcoding', function () {
722
723 it('Should not generate an upper resolution than original file', async function () {
724 this.timeout(120_000)
725
726 await servers[0].config.updateExistingSubConfig({
727 newConfig: {
728 transcoding: {
729 enabled: true,
730 hls: { enabled: true },
731 webtorrent: { enabled: true },
732 resolutions: {
733 '0p': false,
734 '144p': false,
735 '240p': true,
736 '360p': false,
737 '480p': true,
738 '720p': false,
739 '1080p': false,
740 '1440p': false,
741 '2160p': false
742 },
743 alwaysTranscodeOriginalResolution: false
744 }
745 }
746 })
747
748 const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' })
749 await waitJobs(servers)
750
751 const video = await servers[0].videos.get({ id: uuid })
752 const hlsFiles = video.streamingPlaylists[0].files
753
754 expect(video.files).to.have.lengthOf(2)
755 expect(hlsFiles).to.have.lengthOf(2)
756
757 // eslint-disable-next-line @typescript-eslint/require-array-sort-compare
758 const resolutions = getAllFiles(video).map(f => f.resolution.id).sort()
759 expect(resolutions).to.deep.equal([ 240, 240, 480, 480 ])
760 })
761
762 it('Should only keep the original resolution if all resolutions are disabled', async function () {
763 this.timeout(120_000)
764
765 await servers[0].config.updateExistingSubConfig({
766 newConfig: {
767 transcoding: {
768 resolutions: {
769 '0p': false,
770 '144p': false,
771 '240p': false,
772 '360p': false,
773 '480p': false,
774 '720p': false,
775 '1080p': false,
776 '1440p': false,
777 '2160p': false
778 }
779 }
780 }
781 })
782
783 const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' })
784 await waitJobs(servers)
785
786 const video = await servers[0].videos.get({ id: uuid })
787 const hlsFiles = video.streamingPlaylists[0].files
788
789 expect(video.files).to.have.lengthOf(1)
790 expect(hlsFiles).to.have.lengthOf(1)
791
792 expect(video.files[0].resolution.id).to.equal(720)
793 expect(hlsFiles[0].resolution.id).to.equal(720)
794 })
795 })
796
7c3b7976
C
797 after(async function () {
798 await cleanupTests(servers)
0e1dc3e7
C
799 })
800})