]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/tests/api/videos/video-transcoder.ts
164843d322329e059bc0b6c69fc0d4105239c4c1
[github/Chocobozzz/PeerTube.git] / server / tests / api / videos / video-transcoder.ts
1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3 import 'mocha'
4 import * as chai from 'chai'
5 import { FfprobeData } from 'fluent-ffmpeg'
6 import { omit } from 'lodash'
7 import { join } from 'path'
8 import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
9 import {
10 buildAbsoluteFixturePath,
11 buildServerDirectory,
12 cleanupTests,
13 doubleFollow,
14 flushAndRunMultipleServers,
15 generateHighBitrateVideo,
16 generateVideoWithFramerate,
17 getMyVideos,
18 getServerFileSize,
19 getVideo,
20 getVideoFileMetadataUrl,
21 getVideosList,
22 makeGetRequest,
23 ServerInfo,
24 setAccessTokensToServers,
25 updateCustomSubConfig,
26 uploadVideo,
27 uploadVideoAndGetId,
28 waitJobs,
29 webtorrentAdd
30 } from '../../../../shared/extra-utils'
31 import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos'
32 import {
33 canDoQuickTranscode,
34 getAudioStream,
35 getMetadataFromFile,
36 getVideoFileBitrate,
37 getVideoFileFPS,
38 getVideoFileResolution
39 } from '../../../helpers/ffprobe-utils'
40
41 const expect = chai.expect
42
43 describe('Test video transcoding', function () {
44 let servers: ServerInfo[] = []
45
46 before(async function () {
47 this.timeout(30000)
48
49 // Run servers
50 servers = await flushAndRunMultipleServers(2)
51
52 await setAccessTokensToServers(servers)
53
54 await doubleFollow(servers[0], servers[1])
55 })
56
57 it('Should not transcode video on server 1', async function () {
58 this.timeout(60000)
59
60 const videoAttributes = {
61 name: 'my super name for server 1',
62 description: 'my super description for server 1',
63 fixture: 'video_short.webm'
64 }
65 await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
66
67 await waitJobs(servers)
68
69 for (const server of servers) {
70 const res = await getVideosList(server.url)
71 const video = res.body.data[0]
72
73 const res2 = await getVideo(server.url, video.id)
74 const videoDetails = res2.body
75 expect(videoDetails.files).to.have.lengthOf(1)
76
77 const magnetUri = videoDetails.files[0].magnetUri
78 expect(magnetUri).to.match(/\.webm/)
79
80 const torrent = await webtorrentAdd(magnetUri, true)
81 expect(torrent.files).to.be.an('array')
82 expect(torrent.files.length).to.equal(1)
83 expect(torrent.files[0].path).match(/\.webm$/)
84 }
85 })
86
87 it('Should transcode video on server 2', async function () {
88 this.timeout(120000)
89
90 const videoAttributes = {
91 name: 'my super name for server 2',
92 description: 'my super description for server 2',
93 fixture: 'video_short.webm'
94 }
95 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
96
97 await waitJobs(servers)
98
99 for (const server of servers) {
100 const res = await getVideosList(server.url)
101
102 const video = res.body.data.find(v => v.name === videoAttributes.name)
103 const res2 = await getVideo(server.url, video.id)
104 const videoDetails = res2.body
105
106 expect(videoDetails.files).to.have.lengthOf(4)
107
108 const magnetUri = videoDetails.files[0].magnetUri
109 expect(magnetUri).to.match(/\.mp4/)
110
111 const torrent = await webtorrentAdd(magnetUri, true)
112 expect(torrent.files).to.be.an('array')
113 expect(torrent.files.length).to.equal(1)
114 expect(torrent.files[0].path).match(/\.mp4$/)
115 }
116 })
117
118 it('Should transcode high bit rate mp3 to proper bit rate', async function () {
119 this.timeout(60000)
120
121 const videoAttributes = {
122 name: 'mp3_256k',
123 fixture: 'video_short_mp3_256k.mp4'
124 }
125 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
126
127 await waitJobs(servers)
128
129 for (const server of servers) {
130 const res = await getVideosList(server.url)
131
132 const video = res.body.data.find(v => v.name === videoAttributes.name)
133 const res2 = await getVideo(server.url, video.id)
134 const videoDetails: VideoDetails = res2.body
135
136 expect(videoDetails.files).to.have.lengthOf(4)
137
138 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4'))
139 const probe = await getAudioStream(path)
140
141 if (probe.audioStream) {
142 expect(probe.audioStream['codec_name']).to.be.equal('aac')
143 expect(probe.audioStream['bit_rate']).to.be.at.most(384 * 8000)
144 } else {
145 this.fail('Could not retrieve the audio stream on ' + probe.absolutePath)
146 }
147 }
148 })
149
150 it('Should transcode video with no audio and have no audio itself', async function () {
151 this.timeout(60000)
152
153 const videoAttributes = {
154 name: 'no_audio',
155 fixture: 'video_short_no_audio.mp4'
156 }
157 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
158
159 await waitJobs(servers)
160
161 for (const server of servers) {
162 const res = await getVideosList(server.url)
163
164 const video = res.body.data.find(v => v.name === videoAttributes.name)
165 const res2 = await getVideo(server.url, video.id)
166 const videoDetails: VideoDetails = res2.body
167
168 expect(videoDetails.files).to.have.lengthOf(4)
169 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4'))
170 const probe = await getAudioStream(path)
171 expect(probe).to.not.have.property('audioStream')
172 }
173 })
174
175 it('Should leave the audio untouched, but properly transcode the video', async function () {
176 this.timeout(60000)
177
178 const videoAttributes = {
179 name: 'untouched_audio',
180 fixture: 'video_short.mp4'
181 }
182 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
183
184 await waitJobs(servers)
185
186 for (const server of servers) {
187 const res = await getVideosList(server.url)
188
189 const video = res.body.data.find(v => v.name === videoAttributes.name)
190 const res2 = await getVideo(server.url, video.id)
191 const videoDetails: VideoDetails = res2.body
192
193 expect(videoDetails.files).to.have.lengthOf(4)
194
195 const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture)
196 const fixtureVideoProbe = await getAudioStream(fixturePath)
197 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4'))
198
199 const videoProbe = await getAudioStream(path)
200
201 if (videoProbe.audioStream && fixtureVideoProbe.audioStream) {
202 const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ]
203 expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit))
204 } else {
205 this.fail('Could not retrieve the audio stream on ' + videoProbe.absolutePath)
206 }
207 }
208 })
209
210 it('Should transcode a 60 FPS video', async function () {
211 this.timeout(60000)
212
213 const videoAttributes = {
214 name: 'my super 30fps name for server 2',
215 description: 'my super 30fps description for server 2',
216 fixture: '60fps_720p_small.mp4'
217 }
218 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
219
220 await waitJobs(servers)
221
222 for (const server of servers) {
223 const res = await getVideosList(server.url)
224
225 const video = res.body.data.find(v => v.name === videoAttributes.name)
226 const res2 = await getVideo(server.url, video.id)
227 const videoDetails: VideoDetails = res2.body
228
229 expect(videoDetails.files).to.have.lengthOf(4)
230 expect(videoDetails.files[0].fps).to.be.above(58).and.below(62)
231 expect(videoDetails.files[1].fps).to.be.below(31)
232 expect(videoDetails.files[2].fps).to.be.below(31)
233 expect(videoDetails.files[3].fps).to.be.below(31)
234
235 for (const resolution of [ '240', '360', '480' ]) {
236 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-' + resolution + '.mp4'))
237 const fps = await getVideoFileFPS(path)
238
239 expect(fps).to.be.below(31)
240 }
241
242 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-720.mp4'))
243 const fps = await getVideoFileFPS(path)
244
245 expect(fps).to.be.above(58).and.below(62)
246 }
247 })
248
249 it('Should wait for transcoding before publishing the video', async function () {
250 this.timeout(160000)
251
252 {
253 // Upload the video, but wait transcoding
254 const videoAttributes = {
255 name: 'waiting video',
256 fixture: 'video_short1.webm',
257 waitTranscoding: true
258 }
259 const resVideo = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
260 const videoId = resVideo.body.video.uuid
261
262 // Should be in transcode state
263 const { body } = await getVideo(servers[1].url, videoId)
264 expect(body.name).to.equal('waiting video')
265 expect(body.state.id).to.equal(VideoState.TO_TRANSCODE)
266 expect(body.state.label).to.equal('To transcode')
267 expect(body.waitTranscoding).to.be.true
268
269 // Should have my video
270 const resMyVideos = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 10)
271 const videoToFindInMine = resMyVideos.body.data.find(v => v.name === videoAttributes.name)
272 expect(videoToFindInMine).not.to.be.undefined
273 expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE)
274 expect(videoToFindInMine.state.label).to.equal('To transcode')
275 expect(videoToFindInMine.waitTranscoding).to.be.true
276
277 // Should not list this video
278 const resVideos = await getVideosList(servers[1].url)
279 const videoToFindInList = resVideos.body.data.find(v => v.name === videoAttributes.name)
280 expect(videoToFindInList).to.be.undefined
281
282 // Server 1 should not have the video yet
283 await getVideo(servers[0].url, videoId, 404)
284 }
285
286 await waitJobs(servers)
287
288 for (const server of servers) {
289 const res = await getVideosList(server.url)
290 const videoToFind = res.body.data.find(v => v.name === 'waiting video')
291 expect(videoToFind).not.to.be.undefined
292
293 const res2 = await getVideo(server.url, videoToFind.id)
294 const videoDetails: VideoDetails = res2.body
295
296 expect(videoDetails.state.id).to.equal(VideoState.PUBLISHED)
297 expect(videoDetails.state.label).to.equal('Published')
298 expect(videoDetails.waitTranscoding).to.be.true
299 }
300 })
301
302 it('Should respect maximum bitrate values', async function () {
303 this.timeout(160000)
304
305 let tempFixturePath: string
306
307 {
308 tempFixturePath = await generateHighBitrateVideo()
309
310 const bitrate = await getVideoFileBitrate(tempFixturePath)
311 expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 25, VIDEO_TRANSCODING_FPS))
312 }
313
314 const videoAttributes = {
315 name: 'high bitrate video',
316 description: 'high bitrate video',
317 fixture: tempFixturePath
318 }
319
320 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
321
322 await waitJobs(servers)
323
324 for (const server of servers) {
325 const res = await getVideosList(server.url)
326
327 const video = res.body.data.find(v => v.name === videoAttributes.name)
328
329 for (const resolution of [ '240', '360', '480', '720', '1080' ]) {
330 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-' + resolution + '.mp4'))
331
332 const bitrate = await getVideoFileBitrate(path)
333 const fps = await getVideoFileFPS(path)
334 const resolution2 = await getVideoFileResolution(path)
335
336 expect(resolution2.videoFileResolution.toString()).to.equal(resolution)
337 expect(bitrate).to.be.below(getMaxBitrate(resolution2.videoFileResolution, fps, VIDEO_TRANSCODING_FPS))
338 }
339 }
340 })
341
342 it('Should accept and transcode additional extensions', async function () {
343 this.timeout(300000)
344
345 let tempFixturePath: string
346
347 {
348 tempFixturePath = await generateHighBitrateVideo()
349
350 const bitrate = await getVideoFileBitrate(tempFixturePath)
351 expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 25, VIDEO_TRANSCODING_FPS))
352 }
353
354 for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) {
355 const videoAttributes = {
356 name: fixture,
357 fixture
358 }
359
360 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
361
362 await waitJobs(servers)
363
364 for (const server of servers) {
365 const res = await getVideosList(server.url)
366
367 const video = res.body.data.find(v => v.name === videoAttributes.name)
368 const res2 = await getVideo(server.url, video.id)
369 const videoDetails = res2.body
370
371 expect(videoDetails.files).to.have.lengthOf(4)
372
373 const magnetUri = videoDetails.files[0].magnetUri
374 expect(magnetUri).to.contain('.mp4')
375 }
376 }
377 })
378
379 it('Should correctly detect if quick transcode is possible', async function () {
380 this.timeout(10000)
381
382 expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true
383 expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false
384 })
385
386 it('Should merge an audio file with the preview file', async function () {
387 this.timeout(60000)
388
389 const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
390 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg)
391
392 await waitJobs(servers)
393
394 for (const server of servers) {
395 const res = await getVideosList(server.url)
396
397 const video = res.body.data.find(v => v.name === 'audio_with_preview')
398 const res2 = await getVideo(server.url, video.id)
399 const videoDetails: VideoDetails = res2.body
400
401 expect(videoDetails.files).to.have.lengthOf(1)
402
403 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: 200 })
404 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: 200 })
405
406 const magnetUri = videoDetails.files[0].magnetUri
407 expect(magnetUri).to.contain('.mp4')
408 }
409 })
410
411 it('Should upload an audio file and choose a default background image', async function () {
412 this.timeout(60000)
413
414 const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' }
415 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg)
416
417 await waitJobs(servers)
418
419 for (const server of servers) {
420 const res = await getVideosList(server.url)
421
422 const video = res.body.data.find(v => v.name === 'audio_without_preview')
423 const res2 = await getVideo(server.url, video.id)
424 const videoDetails = res2.body
425
426 expect(videoDetails.files).to.have.lengthOf(1)
427
428 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: 200 })
429 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: 200 })
430
431 const magnetUri = videoDetails.files[0].magnetUri
432 expect(magnetUri).to.contain('.mp4')
433 }
434 })
435
436 it('Should downscale to the closest divisor standard framerate', async function () {
437 this.timeout(160000)
438
439 let tempFixturePath: string
440
441 {
442 tempFixturePath = await generateVideoWithFramerate(59)
443
444 const fps = await getVideoFileFPS(tempFixturePath)
445 expect(fps).to.be.equal(59)
446 }
447
448 const videoAttributes = {
449 name: '59fps video',
450 description: '59fps video',
451 fixture: tempFixturePath
452 }
453
454 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
455
456 await waitJobs(servers)
457
458 for (const server of servers) {
459 const res = await getVideosList(server.url)
460
461 const video = res.body.data.find(v => v.name === videoAttributes.name)
462
463 {
464 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4'))
465 const fps = await getVideoFileFPS(path)
466 expect(fps).to.be.equal(25)
467 }
468
469 {
470 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-720.mp4'))
471 const fps = await getVideoFileFPS(path)
472 expect(fps).to.be.equal(59)
473 }
474 }
475 })
476
477 it('Should not transcode to an higher bitrate than the original file', async function () {
478 this.timeout(160000)
479
480 const config = {
481 transcoding: {
482 enabled: true,
483 resolutions: {
484 '240p': true,
485 '360p': true,
486 '480p': true,
487 '720p': true,
488 '1080p': true
489 },
490 webtorrent: { enabled: true },
491 hls: { enabled: true }
492 }
493 }
494 await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
495
496 const videoAttributes = {
497 name: 'low bitrate',
498 fixture: 'low-bitrate.mp4'
499 }
500
501 const resUpload = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
502 const videoUUID = resUpload.body.video.uuid
503
504 await waitJobs(servers)
505
506 const resolutions = [ 240, 360, 480, 720, 1080 ]
507 for (const r of resolutions) {
508 expect(await getServerFileSize(servers[1], `videos/${videoUUID}-${r}.mp4`)).to.be.below(60000)
509 }
510 })
511
512 it('Should provide valid ffprobe data', async function () {
513 this.timeout(160000)
514
515 const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'ffprobe data' })).uuid
516 await waitJobs(servers)
517
518 {
519 const path = buildServerDirectory(servers[1], join('videos', videoUUID + '-240.mp4'))
520 const metadata = await getMetadataFromFile(path)
521
522 // expected format properties
523 for (const p of [
524 'tags.encoder',
525 'format_long_name',
526 'size',
527 'bit_rate'
528 ]) {
529 expect(metadata.format).to.have.nested.property(p)
530 }
531
532 // expected stream properties
533 for (const p of [
534 'codec_long_name',
535 'profile',
536 'width',
537 'height',
538 'display_aspect_ratio',
539 'avg_frame_rate',
540 'pix_fmt'
541 ]) {
542 expect(metadata.streams[0]).to.have.nested.property(p)
543 }
544
545 expect(metadata).to.not.have.nested.property('format.filename')
546 }
547
548 for (const server of servers) {
549 const res2 = await getVideo(server.url, videoUUID)
550 const videoDetails: VideoDetails = res2.body
551
552 const videoFiles = videoDetails.files
553 .concat(videoDetails.streamingPlaylists[0].files)
554 expect(videoFiles).to.have.lengthOf(8)
555
556 for (const file of videoFiles) {
557 expect(file.metadata).to.be.undefined
558 expect(file.metadataUrl).to.exist
559 expect(file.metadataUrl).to.contain(servers[1].url)
560 expect(file.metadataUrl).to.contain(videoUUID)
561
562 const res3 = await getVideoFileMetadataUrl(file.metadataUrl)
563 const metadata: FfprobeData = res3.body
564 expect(metadata).to.have.nested.property('format.size')
565 }
566 }
567 })
568
569 after(async function () {
570 await cleanupTests(servers)
571 })
572 })