diff options
Diffstat (limited to 'server/tests/api/transcoding/transcoder.ts')
-rw-r--r-- | server/tests/api/transcoding/transcoder.ts | 800 |
1 files changed, 0 insertions, 800 deletions
diff --git a/server/tests/api/transcoding/transcoder.ts b/server/tests/api/transcoding/transcoder.ts deleted file mode 100644 index 5386d236f..000000000 --- a/server/tests/api/transcoding/transcoder.ts +++ /dev/null | |||
@@ -1,800 +0,0 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { canDoQuickTranscode } from '@server/lib/transcoding/transcoding-quick-transcode' | ||
5 | import { checkWebTorrentWorks, generateHighBitrateVideo, generateVideoWithFramerate } from '@server/tests/shared' | ||
6 | import { buildAbsoluteFixturePath, getAllFiles, getMaxTheoreticalBitrate, getMinTheoreticalBitrate, omit } from '@shared/core-utils' | ||
7 | import { | ||
8 | ffprobePromise, | ||
9 | getAudioStream, | ||
10 | getVideoStreamBitrate, | ||
11 | getVideoStreamDimensionsInfo, | ||
12 | getVideoStreamFPS, | ||
13 | hasAudioStream | ||
14 | } from '@shared/ffmpeg' | ||
15 | import { HttpStatusCode, VideoFileMetadata, VideoState } from '@shared/models' | ||
16 | import { | ||
17 | cleanupTests, | ||
18 | createMultipleServers, | ||
19 | doubleFollow, | ||
20 | makeGetRequest, | ||
21 | PeerTubeServer, | ||
22 | setAccessTokensToServers, | ||
23 | waitJobs | ||
24 | } from '@shared/server-commands' | ||
25 | |||
26 | function updateConfigForTranscoding (server: PeerTubeServer) { | ||
27 | return server.config.updateCustomSubConfig({ | ||
28 | newConfig: { | ||
29 | transcoding: { | ||
30 | enabled: true, | ||
31 | allowAdditionalExtensions: true, | ||
32 | allowAudioFiles: true, | ||
33 | hls: { enabled: true }, | ||
34 | webVideos: { enabled: true }, | ||
35 | resolutions: { | ||
36 | '0p': false, | ||
37 | '144p': true, | ||
38 | '240p': true, | ||
39 | '360p': true, | ||
40 | '480p': true, | ||
41 | '720p': true, | ||
42 | '1080p': true, | ||
43 | '1440p': true, | ||
44 | '2160p': true | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | }) | ||
49 | } | ||
50 | |||
51 | describe('Test video transcoding', function () { | ||
52 | let servers: PeerTubeServer[] = [] | ||
53 | let video4k: string | ||
54 | |||
55 | before(async function () { | ||
56 | this.timeout(30_000) | ||
57 | |||
58 | // Run servers | ||
59 | servers = await createMultipleServers(2) | ||
60 | |||
61 | await setAccessTokensToServers(servers) | ||
62 | |||
63 | await doubleFollow(servers[0], servers[1]) | ||
64 | |||
65 | await updateConfigForTranscoding(servers[1]) | ||
66 | }) | ||
67 | |||
68 | describe('Basic transcoding (or not)', function () { | ||
69 | |||
70 | it('Should not transcode video on server 1', async function () { | ||
71 | this.timeout(60_000) | ||
72 | |||
73 | const attributes = { | ||
74 | name: 'my super name for server 1', | ||
75 | description: 'my super description for server 1', | ||
76 | fixture: 'video_short.webm' | ||
77 | } | ||
78 | await servers[0].videos.upload({ attributes }) | ||
79 | |||
80 | await waitJobs(servers) | ||
81 | |||
82 | for (const server of servers) { | ||
83 | const { data } = await server.videos.list() | ||
84 | const video = data[0] | ||
85 | |||
86 | const videoDetails = await server.videos.get({ id: video.id }) | ||
87 | expect(videoDetails.files).to.have.lengthOf(1) | ||
88 | |||
89 | const magnetUri = videoDetails.files[0].magnetUri | ||
90 | expect(magnetUri).to.match(/\.webm/) | ||
91 | |||
92 | await checkWebTorrentWorks(magnetUri, /\.webm$/) | ||
93 | } | ||
94 | }) | ||
95 | |||
96 | it('Should transcode video on server 2', async function () { | ||
97 | this.timeout(120_000) | ||
98 | |||
99 | const attributes = { | ||
100 | name: 'my super name for server 2', | ||
101 | description: 'my super description for server 2', | ||
102 | fixture: 'video_short.webm' | ||
103 | } | ||
104 | await servers[1].videos.upload({ attributes }) | ||
105 | |||
106 | await waitJobs(servers) | ||
107 | |||
108 | for (const server of servers) { | ||
109 | const { data } = await server.videos.list() | ||
110 | |||
111 | const video = data.find(v => v.name === attributes.name) | ||
112 | const videoDetails = await server.videos.get({ id: video.id }) | ||
113 | |||
114 | expect(videoDetails.files).to.have.lengthOf(5) | ||
115 | |||
116 | const magnetUri = videoDetails.files[0].magnetUri | ||
117 | expect(magnetUri).to.match(/\.mp4/) | ||
118 | |||
119 | await checkWebTorrentWorks(magnetUri, /\.mp4$/) | ||
120 | } | ||
121 | }) | ||
122 | |||
123 | it('Should wait for transcoding before publishing the video', async function () { | ||
124 | this.timeout(160_000) | ||
125 | |||
126 | { | ||
127 | // Upload the video, but wait transcoding | ||
128 | const attributes = { | ||
129 | name: 'waiting video', | ||
130 | fixture: 'video_short1.webm', | ||
131 | waitTranscoding: true | ||
132 | } | ||
133 | const { uuid } = await servers[1].videos.upload({ attributes }) | ||
134 | const videoId = uuid | ||
135 | |||
136 | // Should be in transcode state | ||
137 | const body = await servers[1].videos.get({ id: videoId }) | ||
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 | |||
143 | { | ||
144 | // Should have my video | ||
145 | const { data } = await servers[1].videos.listMyVideos() | ||
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 | } | ||
152 | |||
153 | { | ||
154 | // Should not list this video | ||
155 | const { data } = await servers[1].videos.list() | ||
156 | const videoToFindInList = data.find(v => v.name === attributes.name) | ||
157 | expect(videoToFindInList).to.be.undefined | ||
158 | } | ||
159 | |||
160 | // Server 1 should not have the video yet | ||
161 | await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
162 | } | ||
163 | |||
164 | await waitJobs(servers) | ||
165 | |||
166 | for (const server of servers) { | ||
167 | const { data } = await server.videos.list() | ||
168 | const videoToFind = data.find(v => v.name === 'waiting video') | ||
169 | expect(videoToFind).not.to.be.undefined | ||
170 | |||
171 | const videoDetails = await server.videos.get({ id: videoToFind.id }) | ||
172 | |||
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 | }) | ||
178 | |||
179 | it('Should accept and transcode additional extensions', async function () { | ||
180 | this.timeout(300_000) | ||
181 | |||
182 | for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) { | ||
183 | const attributes = { | ||
184 | name: fixture, | ||
185 | fixture | ||
186 | } | ||
187 | |||
188 | await servers[1].videos.upload({ attributes }) | ||
189 | |||
190 | await waitJobs(servers) | ||
191 | |||
192 | for (const server of servers) { | ||
193 | const { data } = await server.videos.list() | ||
194 | |||
195 | const video = data.find(v => v.name === attributes.name) | ||
196 | const videoDetails = await server.videos.get({ id: video.id }) | ||
197 | expect(videoDetails.files).to.have.lengthOf(5) | ||
198 | |||
199 | const magnetUri = videoDetails.files[0].magnetUri | ||
200 | expect(magnetUri).to.contain('.mp4') | ||
201 | } | ||
202 | } | ||
203 | }) | ||
204 | |||
205 | it('Should transcode a 4k video', async function () { | ||
206 | this.timeout(200_000) | ||
207 | |||
208 | const attributes = { | ||
209 | name: '4k video', | ||
210 | fixture: 'video_short_4k.mp4' | ||
211 | } | ||
212 | |||
213 | const { uuid } = await servers[1].videos.upload({ attributes }) | ||
214 | video4k = uuid | ||
215 | |||
216 | await waitJobs(servers) | ||
217 | |||
218 | const resolutions = [ 144, 240, 360, 480, 720, 1080, 1440, 2160 ] | ||
219 | |||
220 | for (const server of servers) { | ||
221 | const videoDetails = await server.videos.get({ id: video4k }) | ||
222 | expect(videoDetails.files).to.have.lengthOf(resolutions.length) | ||
223 | |||
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 | } | ||
228 | } | ||
229 | }) | ||
230 | }) | ||
231 | |||
232 | describe('Audio transcoding', function () { | ||
233 | |||
234 | it('Should transcode high bit rate mp3 to proper bit rate', async function () { | ||
235 | this.timeout(60_000) | ||
236 | |||
237 | const attributes = { | ||
238 | name: 'mp3_256k', | ||
239 | fixture: 'video_short_mp3_256k.mp4' | ||
240 | } | ||
241 | await servers[1].videos.upload({ attributes }) | ||
242 | |||
243 | await waitJobs(servers) | ||
244 | |||
245 | for (const server of servers) { | ||
246 | const { data } = await server.videos.list() | ||
247 | |||
248 | const video = data.find(v => v.name === attributes.name) | ||
249 | const videoDetails = await server.videos.get({ id: video.id }) | ||
250 | |||
251 | expect(videoDetails.files).to.have.lengthOf(5) | ||
252 | |||
253 | const file = videoDetails.files.find(f => f.resolution.id === 240) | ||
254 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
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 | } | ||
263 | } | ||
264 | }) | ||
265 | |||
266 | it('Should transcode video with no audio and have no audio itself', async function () { | ||
267 | this.timeout(60_000) | ||
268 | |||
269 | const attributes = { | ||
270 | name: 'no_audio', | ||
271 | fixture: 'video_short_no_audio.mp4' | ||
272 | } | ||
273 | await servers[1].videos.upload({ attributes }) | ||
274 | |||
275 | await waitJobs(servers) | ||
276 | |||
277 | for (const server of servers) { | ||
278 | const { data } = await server.videos.list() | ||
279 | |||
280 | const video = data.find(v => v.name === attributes.name) | ||
281 | const videoDetails = await server.videos.get({ id: video.id }) | ||
282 | |||
283 | const file = videoDetails.files.find(f => f.resolution.id === 240) | ||
284 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
285 | |||
286 | expect(await hasAudioStream(path)).to.be.false | ||
287 | } | ||
288 | }) | ||
289 | |||
290 | it('Should leave the audio untouched, but properly transcode the video', async function () { | ||
291 | this.timeout(60_000) | ||
292 | |||
293 | const attributes = { | ||
294 | name: 'untouched_audio', | ||
295 | fixture: 'video_short.mp4' | ||
296 | } | ||
297 | await servers[1].videos.upload({ attributes }) | ||
298 | |||
299 | await waitJobs(servers) | ||
300 | |||
301 | for (const server of servers) { | ||
302 | const { data } = await server.videos.list() | ||
303 | |||
304 | const video = data.find(v => v.name === attributes.name) | ||
305 | const videoDetails = await server.videos.get({ id: video.id }) | ||
306 | |||
307 | expect(videoDetails.files).to.have.lengthOf(5) | ||
308 | |||
309 | const fixturePath = buildAbsoluteFixturePath(attributes.fixture) | ||
310 | const fixtureVideoProbe = await getAudioStream(fixturePath) | ||
311 | |||
312 | const file = videoDetails.files.find(f => f.resolution.id === 240) | ||
313 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
314 | |||
315 | const videoProbe = await getAudioStream(path) | ||
316 | |||
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 | }) | ||
326 | |||
327 | describe('Audio upload', function () { | ||
328 | |||
329 | function runSuite (mode: 'legacy' | 'resumable') { | ||
330 | |||
331 | before(async function () { | ||
332 | await servers[1].config.updateCustomSubConfig({ | ||
333 | newConfig: { | ||
334 | transcoding: { | ||
335 | hls: { enabled: true }, | ||
336 | webVideos: { enabled: true }, | ||
337 | resolutions: { | ||
338 | '0p': false, | ||
339 | '144p': false, | ||
340 | '240p': false, | ||
341 | '360p': false, | ||
342 | '480p': false, | ||
343 | '720p': false, | ||
344 | '1080p': false, | ||
345 | '1440p': false, | ||
346 | '2160p': false | ||
347 | } | ||
348 | } | ||
349 | } | ||
350 | }) | ||
351 | }) | ||
352 | |||
353 | it('Should merge an audio file with the preview file', async function () { | ||
354 | this.timeout(60_000) | ||
355 | |||
356 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } | ||
357 | await servers[1].videos.upload({ attributes, mode }) | ||
358 | |||
359 | await waitJobs(servers) | ||
360 | |||
361 | for (const server of servers) { | ||
362 | const { data } = await server.videos.list() | ||
363 | |||
364 | const video = data.find(v => v.name === 'audio_with_preview') | ||
365 | const videoDetails = await server.videos.get({ id: video.id }) | ||
366 | |||
367 | expect(videoDetails.files).to.have.lengthOf(1) | ||
368 | |||
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 }) | ||
371 | |||
372 | const magnetUri = videoDetails.files[0].magnetUri | ||
373 | expect(magnetUri).to.contain('.mp4') | ||
374 | } | ||
375 | }) | ||
376 | |||
377 | it('Should upload an audio file and choose a default background image', async function () { | ||
378 | this.timeout(60_000) | ||
379 | |||
380 | const attributes = { name: 'audio_without_preview', fixture: 'sample.ogg' } | ||
381 | await servers[1].videos.upload({ attributes, mode }) | ||
382 | |||
383 | await waitJobs(servers) | ||
384 | |||
385 | for (const server of servers) { | ||
386 | const { data } = await server.videos.list() | ||
387 | |||
388 | const video = data.find(v => v.name === 'audio_without_preview') | ||
389 | const videoDetails = await server.videos.get({ id: video.id }) | ||
390 | |||
391 | expect(videoDetails.files).to.have.lengthOf(1) | ||
392 | |||
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 }) | ||
395 | |||
396 | const magnetUri = videoDetails.files[0].magnetUri | ||
397 | expect(magnetUri).to.contain('.mp4') | ||
398 | } | ||
399 | }) | ||
400 | |||
401 | it('Should upload an audio file and create an audio version only', async function () { | ||
402 | this.timeout(60_000) | ||
403 | |||
404 | await servers[1].config.updateCustomSubConfig({ | ||
405 | newConfig: { | ||
406 | transcoding: { | ||
407 | hls: { enabled: true }, | ||
408 | webVideos: { enabled: true }, | ||
409 | resolutions: { | ||
410 | '0p': true, | ||
411 | '144p': false, | ||
412 | '240p': false, | ||
413 | '360p': false | ||
414 | } | ||
415 | } | ||
416 | } | ||
417 | }) | ||
418 | |||
419 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } | ||
420 | const { id } = await servers[1].videos.upload({ attributes, mode }) | ||
421 | |||
422 | await waitJobs(servers) | ||
423 | |||
424 | for (const server of servers) { | ||
425 | const videoDetails = await server.videos.get({ id }) | ||
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 | } | ||
431 | } | ||
432 | |||
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') | ||
443 | }) | ||
444 | }) | ||
445 | |||
446 | describe('Framerate', function () { | ||
447 | |||
448 | it('Should transcode a 60 FPS video', async function () { | ||
449 | this.timeout(60_000) | ||
450 | |||
451 | const attributes = { | ||
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 | } | ||
456 | await servers[1].videos.upload({ attributes }) | ||
457 | |||
458 | await waitJobs(servers) | ||
459 | |||
460 | for (const server of servers) { | ||
461 | const { data } = await server.videos.list() | ||
462 | |||
463 | const video = data.find(v => v.name === attributes.name) | ||
464 | const videoDetails = await server.videos.get({ id: video.id }) | ||
465 | |||
466 | expect(videoDetails.files).to.have.lengthOf(5) | ||
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) | ||
471 | expect(videoDetails.files[4].fps).to.be.below(31) | ||
472 | |||
473 | for (const resolution of [ 144, 240, 360, 480 ]) { | ||
474 | const file = videoDetails.files.find(f => f.resolution.id === resolution) | ||
475 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
476 | const fps = await getVideoStreamFPS(path) | ||
477 | |||
478 | expect(fps).to.be.below(31) | ||
479 | } | ||
480 | |||
481 | const file = videoDetails.files.find(f => f.resolution.id === 720) | ||
482 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
483 | const fps = await getVideoStreamFPS(path) | ||
484 | |||
485 | expect(fps).to.be.above(58).and.below(62) | ||
486 | } | ||
487 | }) | ||
488 | |||
489 | it('Should downscale to the closest divisor standard framerate', async function () { | ||
490 | this.timeout(200_000) | ||
491 | |||
492 | let tempFixturePath: string | ||
493 | |||
494 | { | ||
495 | tempFixturePath = await generateVideoWithFramerate(59) | ||
496 | |||
497 | const fps = await getVideoStreamFPS(tempFixturePath) | ||
498 | expect(fps).to.be.equal(59) | ||
499 | } | ||
500 | |||
501 | const attributes = { | ||
502 | name: '59fps video', | ||
503 | description: '59fps video', | ||
504 | fixture: tempFixturePath | ||
505 | } | ||
506 | |||
507 | await servers[1].videos.upload({ attributes }) | ||
508 | |||
509 | await waitJobs(servers) | ||
510 | |||
511 | for (const server of servers) { | ||
512 | const { data } = await server.videos.list() | ||
513 | |||
514 | const { id } = data.find(v => v.name === attributes.name) | ||
515 | const video = await server.videos.get({ id }) | ||
516 | |||
517 | { | ||
518 | const file = video.files.find(f => f.resolution.id === 240) | ||
519 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
520 | const fps = await getVideoStreamFPS(path) | ||
521 | expect(fps).to.be.equal(25) | ||
522 | } | ||
523 | |||
524 | { | ||
525 | const file = video.files.find(f => f.resolution.id === 720) | ||
526 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
527 | const fps = await getVideoStreamFPS(path) | ||
528 | expect(fps).to.be.equal(59) | ||
529 | } | ||
530 | } | ||
531 | }) | ||
532 | }) | ||
533 | |||
534 | describe('Bitrate control', function () { | ||
535 | |||
536 | it('Should respect maximum bitrate values', async function () { | ||
537 | this.timeout(160_000) | ||
538 | |||
539 | const tempFixturePath = await generateHighBitrateVideo() | ||
540 | |||
541 | const attributes = { | ||
542 | name: 'high bitrate video', | ||
543 | description: 'high bitrate video', | ||
544 | fixture: tempFixturePath | ||
545 | } | ||
546 | |||
547 | await servers[1].videos.upload({ attributes }) | ||
548 | |||
549 | await waitJobs(servers) | ||
550 | |||
551 | for (const server of servers) { | ||
552 | const { data } = await server.videos.list() | ||
553 | |||
554 | const { id } = data.find(v => v.name === attributes.name) | ||
555 | const video = await server.videos.get({ id }) | ||
556 | |||
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.buildWebVideoFilePath(file.fileUrl) | ||
560 | |||
561 | const bitrate = await getVideoStreamBitrate(path) | ||
562 | const fps = await getVideoStreamFPS(path) | ||
563 | const dataResolution = await getVideoStreamDimensionsInfo(path) | ||
564 | |||
565 | expect(resolution).to.equal(resolution) | ||
566 | |||
567 | const maxBitrate = getMaxTheoreticalBitrate({ ...dataResolution, fps }) | ||
568 | expect(bitrate).to.be.below(maxBitrate) | ||
569 | } | ||
570 | } | ||
571 | }) | ||
572 | |||
573 | it('Should not transcode to an higher bitrate than the original file but above our low limit', async function () { | ||
574 | this.timeout(160_000) | ||
575 | |||
576 | const newConfig = { | ||
577 | transcoding: { | ||
578 | enabled: true, | ||
579 | resolutions: { | ||
580 | '144p': true, | ||
581 | '240p': true, | ||
582 | '360p': true, | ||
583 | '480p': true, | ||
584 | '720p': true, | ||
585 | '1080p': true, | ||
586 | '1440p': true, | ||
587 | '2160p': true | ||
588 | }, | ||
589 | webVideos: { enabled: true }, | ||
590 | hls: { enabled: true } | ||
591 | } | ||
592 | } | ||
593 | await servers[1].config.updateCustomSubConfig({ newConfig }) | ||
594 | |||
595 | const attributes = { | ||
596 | name: 'low bitrate', | ||
597 | fixture: 'low-bitrate.mp4' | ||
598 | } | ||
599 | |||
600 | const { id } = await servers[1].videos.upload({ attributes }) | ||
601 | |||
602 | await waitJobs(servers) | ||
603 | |||
604 | const video = await servers[1].videos.get({ id }) | ||
605 | |||
606 | const resolutions = [ 240, 360, 480, 720, 1080 ] | ||
607 | for (const r of resolutions) { | ||
608 | const file = video.files.find(f => f.resolution.id === r) | ||
609 | |||
610 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
611 | const bitrate = await getVideoStreamBitrate(path) | ||
612 | |||
613 | const inputBitrate = 60_000 | ||
614 | const limit = getMinTheoreticalBitrate({ 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) | ||
619 | } | ||
620 | }) | ||
621 | }) | ||
622 | |||
623 | describe('FFprobe', function () { | ||
624 | |||
625 | it('Should provide valid ffprobe data', async function () { | ||
626 | this.timeout(160_000) | ||
627 | |||
628 | const videoUUID = (await servers[1].videos.quickUpload({ name: 'ffprobe data' })).uuid | ||
629 | await waitJobs(servers) | ||
630 | |||
631 | { | ||
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.buildWebVideoFilePath(file.fileUrl) | ||
635 | |||
636 | const probe = await ffprobePromise(path) | ||
637 | const metadata = new VideoFileMetadata(probe) | ||
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 | } | ||
664 | |||
665 | for (const server of servers) { | ||
666 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
667 | |||
668 | const videoFiles = getAllFiles(videoDetails) | ||
669 | expect(videoFiles).to.have.lengthOf(10) | ||
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 | |||
677 | const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl }) | ||
678 | expect(metadata).to.have.nested.property('format.size') | ||
679 | } | ||
680 | } | ||
681 | }) | ||
682 | |||
683 | it('Should correctly detect if quick transcode is possible', async function () { | ||
684 | this.timeout(10_000) | ||
685 | |||
686 | expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true | ||
687 | expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false | ||
688 | }) | ||
689 | }) | ||
690 | |||
691 | describe('Transcoding job queue', function () { | ||
692 | |||
693 | it('Should have the appropriate priorities for transcoding jobs', async function () { | ||
694 | const body = await servers[1].jobs.list({ | ||
695 | start: 0, | ||
696 | count: 100, | ||
697 | sort: 'createdAt', | ||
698 | jobType: 'video-transcoding' | ||
699 | }) | ||
700 | |||
701 | const jobs = body.data | ||
702 | const transcodingJobs = jobs.filter(j => j.data.videoUUID === video4k) | ||
703 | |||
704 | expect(transcodingJobs).to.have.lengthOf(16) | ||
705 | |||
706 | const hlsJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-hls') | ||
707 | const webVideoJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-web-video') | ||
708 | const optimizeJobs = transcodingJobs.filter(j => j.data.type === 'optimize-to-web-video') | ||
709 | |||
710 | expect(hlsJobs).to.have.lengthOf(8) | ||
711 | expect(webVideoJobs).to.have.lengthOf(7) | ||
712 | expect(optimizeJobs).to.have.lengthOf(1) | ||
713 | |||
714 | for (const j of optimizeJobs.concat(hlsJobs.concat(webVideoJobs))) { | ||
715 | expect(j.priority).to.be.greaterThan(100) | ||
716 | expect(j.priority).to.be.lessThan(150) | ||
717 | } | ||
718 | }) | ||
719 | }) | ||
720 | |||
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 | webVideos: { 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 | |||
797 | after(async function () { | ||
798 | await cleanupTests(servers) | ||
799 | }) | ||
800 | }) | ||