]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/tests/api/videos/video-transcoder.ts
Merge branch 'release/2.1.0' into develop
[github/Chocobozzz/PeerTube.git] / server / tests / api / videos / video-transcoder.ts
CommitLineData
a1587156 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
0e1dc3e7 2
0e1dc3e7 3import * as chai from 'chai'
a7ba16b6 4import 'mocha'
7160878c 5import { omit } from 'lodash'
c1c86c15 6import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos'
1600235a 7import { audio, canDoQuickTranscode, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
0e1dc3e7 8import {
7243f84d
C
9 buildAbsoluteFixturePath,
10 cleanupTests,
2186386c
C
11 doubleFollow,
12 flushAndRunMultipleServers,
d175a6f7 13 generateHighBitrateVideo,
837666fe 14 generateVideoWithFramerate,
2186386c
C
15 getMyVideos,
16 getVideo,
17 getVideosList,
b345a804 18 makeGetRequest,
2186386c
C
19 root,
20 ServerInfo,
21 setAccessTokensToServers,
22 uploadVideo,
1600235a 23 waitJobs,
d175a6f7 24 webtorrentAdd
94565d52 25} from '../../../../shared/extra-utils'
7243f84d 26import { join } from 'path'
edb4ffc7 27import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
a7ba16b6
C
28
29const expect = chai.expect
0e1dc3e7
C
30
31describe('Test video transcoding', function () {
32 let servers: ServerInfo[] = []
33
34 before(async function () {
e212f887 35 this.timeout(30000)
0e1dc3e7
C
36
37 // Run servers
38 servers = await flushAndRunMultipleServers(2)
39
40 await setAccessTokensToServers(servers)
b2977eec
C
41
42 await doubleFollow(servers[0], servers[1])
0e1dc3e7
C
43 })
44
45 it('Should not transcode video on server 1', async function () {
46 this.timeout(60000)
47
48 const videoAttributes = {
975e6e0e
C
49 name: 'my super name for server 1',
50 description: 'my super description for server 1',
0e1dc3e7
C
51 fixture: 'video_short.webm'
52 }
53 await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
54
3cd0734f 55 await waitJobs(servers)
0e1dc3e7 56
b2977eec
C
57 for (const server of servers) {
58 const res = await getVideosList(server.url)
a1587156 59 const video = res.body.data[0]
40298b02 60
b2977eec
C
61 const res2 = await getVideo(server.url, video.id)
62 const videoDetails = res2.body
63 expect(videoDetails.files).to.have.lengthOf(1)
5f04dd2f 64
a1587156 65 const magnetUri = videoDetails.files[0].magnetUri
b2977eec 66 expect(magnetUri).to.match(/\.webm/)
0e1dc3e7 67
b2977eec
C
68 const torrent = await webtorrentAdd(magnetUri, true)
69 expect(torrent.files).to.be.an('array')
70 expect(torrent.files.length).to.equal(1)
a1587156 71 expect(torrent.files[0].path).match(/\.webm$/)
b2977eec 72 }
0e1dc3e7
C
73 })
74
75 it('Should transcode video on server 2', async function () {
76 this.timeout(60000)
77
78 const videoAttributes = {
975e6e0e
C
79 name: 'my super name for server 2',
80 description: 'my super description for server 2',
0e1dc3e7
C
81 fixture: 'video_short.webm'
82 }
83 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
84
3cd0734f 85 await waitJobs(servers)
0e1dc3e7 86
b2977eec
C
87 for (const server of servers) {
88 const res = await getVideosList(server.url)
0e1dc3e7 89
b2977eec
C
90 const video = res.body.data.find(v => v.name === videoAttributes.name)
91 const res2 = await getVideo(server.url, video.id)
92 const videoDetails = res2.body
5f04dd2f 93
b2977eec 94 expect(videoDetails.files).to.have.lengthOf(4)
40298b02 95
a1587156 96 const magnetUri = videoDetails.files[0].magnetUri
b2977eec 97 expect(magnetUri).to.match(/\.mp4/)
0e1dc3e7 98
b2977eec
C
99 const torrent = await webtorrentAdd(magnetUri, true)
100 expect(torrent.files).to.be.an('array')
101 expect(torrent.files.length).to.equal(1)
a1587156 102 expect(torrent.files[0].path).match(/\.mp4$/)
b2977eec 103 }
0e1dc3e7
C
104 })
105
7160878c
RK
106 it('Should transcode high bit rate mp3 to proper bit rate', async function () {
107 this.timeout(60000)
108
109 const videoAttributes = {
110 name: 'mp3_256k',
111 fixture: 'video_short_mp3_256k.mp4'
112 }
113 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
114
115 await waitJobs(servers)
116
b2977eec
C
117 for (const server of servers) {
118 const res = await getVideosList(server.url)
7160878c 119
b2977eec
C
120 const video = res.body.data.find(v => v.name === videoAttributes.name)
121 const res2 = await getVideo(server.url, video.id)
122 const videoDetails: VideoDetails = res2.body
7160878c 123
b2977eec 124 expect(videoDetails.files).to.have.lengthOf(4)
7160878c 125
7243f84d 126 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-240.mp4')
cdf4cb9e 127 const probe = await audio.get(path)
7160878c 128
b2977eec 129 if (probe.audioStream) {
a1587156
C
130 expect(probe.audioStream['codec_name']).to.be.equal('aac')
131 expect(probe.audioStream['bit_rate']).to.be.at.most(384 * 8000)
b2977eec
C
132 } else {
133 this.fail('Could not retrieve the audio stream on ' + probe.absolutePath)
134 }
7160878c
RK
135 }
136 })
137
138 it('Should transcode video with no audio and have no audio itself', async function () {
139 this.timeout(60000)
140
141 const videoAttributes = {
142 name: 'no_audio',
143 fixture: 'video_short_no_audio.mp4'
144 }
145 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
146
147 await waitJobs(servers)
148
b2977eec
C
149 for (const server of servers) {
150 const res = await getVideosList(server.url)
7160878c 151
b2977eec
C
152 const video = res.body.data.find(v => v.name === videoAttributes.name)
153 const res2 = await getVideo(server.url, video.id)
154 const videoDetails: VideoDetails = res2.body
7160878c 155
b2977eec 156 expect(videoDetails.files).to.have.lengthOf(4)
7243f84d 157 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-240.mp4')
cdf4cb9e 158 const probe = await audio.get(path)
b2977eec
C
159 expect(probe).to.not.have.property('audioStream')
160 }
7160878c
RK
161 })
162
163 it('Should leave the audio untouched, but properly transcode the video', async function () {
164 this.timeout(60000)
165
166 const videoAttributes = {
167 name: 'untouched_audio',
168 fixture: 'video_short.mp4'
169 }
170 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
171
172 await waitJobs(servers)
173
b2977eec
C
174 for (const server of servers) {
175 const res = await getVideosList(server.url)
176
177 const video = res.body.data.find(v => v.name === videoAttributes.name)
178 const res2 = await getVideo(server.url, video.id)
179 const videoDetails: VideoDetails = res2.body
180
181 expect(videoDetails.files).to.have.lengthOf(4)
182 const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture)
cdf4cb9e 183 const fixtureVideoProbe = await audio.get(fixturePath)
7243f84d 184 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-240.mp4')
cdf4cb9e 185 const videoProbe = await audio.get(path)
b2977eec
C
186 if (videoProbe.audioStream && fixtureVideoProbe.audioStream) {
187 const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ]
188 expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit))
189 } else {
190 this.fail('Could not retrieve the audio stream on ' + videoProbe.absolutePath)
191 }
7160878c
RK
192 }
193 })
194
3a6f351b 195 it('Should transcode a 60 FPS video', async function () {
73c69591
C
196 this.timeout(60000)
197
198 const videoAttributes = {
199 name: 'my super 30fps name for server 2',
200 description: 'my super 30fps description for server 2',
3a6f351b 201 fixture: '60fps_720p_small.mp4'
73c69591
C
202 }
203 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
204
3cd0734f 205 await waitJobs(servers)
73c69591 206
b2977eec
C
207 for (const server of servers) {
208 const res = await getVideosList(server.url)
73c69591 209
b2977eec
C
210 const video = res.body.data.find(v => v.name === videoAttributes.name)
211 const res2 = await getVideo(server.url, video.id)
212 const videoDetails: VideoDetails = res2.body
73c69591 213
b2977eec 214 expect(videoDetails.files).to.have.lengthOf(4)
a1587156
C
215 expect(videoDetails.files[0].fps).to.be.above(58).and.below(62)
216 expect(videoDetails.files[1].fps).to.be.below(31)
217 expect(videoDetails.files[2].fps).to.be.below(31)
218 expect(videoDetails.files[3].fps).to.be.below(31)
73c69591 219
b2977eec 220 for (const resolution of [ '240', '360', '480' ]) {
7243f84d 221 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-' + resolution + '.mp4')
b2977eec 222 const fps = await getVideoFileFPS(path)
73c69591 223
b2977eec
C
224 expect(fps).to.be.below(31)
225 }
3a6f351b 226
7243f84d 227 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-720.mp4')
b2977eec 228 const fps = await getVideoFileFPS(path)
3a6f351b 229
b2977eec
C
230 expect(fps).to.be.above(58).and.below(62)
231 }
73c69591
C
232 })
233
edb4ffc7 234 it('Should wait for transcoding before publishing the video', async function () {
2186386c
C
235 this.timeout(80000)
236
2186386c
C
237 {
238 // Upload the video, but wait transcoding
239 const videoAttributes = {
240 name: 'waiting video',
241 fixture: 'video_short1.webm',
242 waitTranscoding: true
243 }
a1587156 244 const resVideo = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
2186386c
C
245 const videoId = resVideo.body.video.uuid
246
247 // Should be in transcode state
a1587156 248 const { body } = await getVideo(servers[1].url, videoId)
2186386c
C
249 expect(body.name).to.equal('waiting video')
250 expect(body.state.id).to.equal(VideoState.TO_TRANSCODE)
251 expect(body.state.label).to.equal('To transcode')
252 expect(body.waitTranscoding).to.be.true
253
254 // Should have my video
255 const resMyVideos = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 10)
7160878c 256 const videoToFindInMine = resMyVideos.body.data.find(v => v.name === videoAttributes.name)
2186386c
C
257 expect(videoToFindInMine).not.to.be.undefined
258 expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE)
259 expect(videoToFindInMine.state.label).to.equal('To transcode')
260 expect(videoToFindInMine.waitTranscoding).to.be.true
261
262 // Should not list this video
263 const resVideos = await getVideosList(servers[1].url)
7160878c 264 const videoToFindInList = resVideos.body.data.find(v => v.name === videoAttributes.name)
2186386c
C
265 expect(videoToFindInList).to.be.undefined
266
267 // Server 1 should not have the video yet
268 await getVideo(servers[0].url, videoId, 404)
269 }
270
3cd0734f 271 await waitJobs(servers)
2186386c
C
272
273 for (const server of servers) {
274 const res = await getVideosList(server.url)
275 const videoToFind = res.body.data.find(v => v.name === 'waiting video')
276 expect(videoToFind).not.to.be.undefined
277
278 const res2 = await getVideo(server.url, videoToFind.id)
279 const videoDetails: VideoDetails = res2.body
280
281 expect(videoDetails.state.id).to.equal(VideoState.PUBLISHED)
282 expect(videoDetails.state.label).to.equal('Published')
283 expect(videoDetails.waitTranscoding).to.be.true
284 }
285 })
286
edb4ffc7
FA
287 it('Should respect maximum bitrate values', async function () {
288 this.timeout(160000)
289
74cd011b
C
290 let tempFixturePath: string
291
edb4ffc7 292 {
74cd011b 293 tempFixturePath = await generateHighBitrateVideo()
edb4ffc7
FA
294
295 const bitrate = await getVideoFileBitrate(tempFixturePath)
941c5eac 296 expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 25, VIDEO_TRANSCODING_FPS))
c1c86c15 297 }
edb4ffc7 298
c1c86c15
C
299 const videoAttributes = {
300 name: 'high bitrate video',
301 description: 'high bitrate video',
302 fixture: tempFixturePath
303 }
edb4ffc7 304
c1c86c15 305 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
edb4ffc7 306
c1c86c15 307 await waitJobs(servers)
edb4ffc7 308
c1c86c15
C
309 for (const server of servers) {
310 const res = await getVideosList(server.url)
edb4ffc7 311
c1c86c15 312 const video = res.body.data.find(v => v.name === videoAttributes.name)
edb4ffc7 313
a1587156 314 for (const resolution of [ '240', '360', '480', '720', '1080' ]) {
7243f84d 315 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-' + resolution + '.mp4')
c1c86c15
C
316 const bitrate = await getVideoFileBitrate(path)
317 const fps = await getVideoFileFPS(path)
318 const resolution2 = await getVideoFileResolution(path)
edb4ffc7 319
c1c86c15
C
320 expect(resolution2.videoFileResolution.toString()).to.equal(resolution)
321 expect(bitrate).to.be.below(getMaxBitrate(resolution2.videoFileResolution, fps, VIDEO_TRANSCODING_FPS))
edb4ffc7
FA
322 }
323 }
324 })
325
14e2014a
C
326 it('Should accept and transcode additional extensions', async function () {
327 this.timeout(300000)
328
7ed2c1a4
FA
329 let tempFixturePath: string
330
331 {
332 tempFixturePath = await generateHighBitrateVideo()
333
334 const bitrate = await getVideoFileBitrate(tempFixturePath)
941c5eac 335 expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 25, VIDEO_TRANSCODING_FPS))
7ed2c1a4
FA
336 }
337
14e2014a
C
338 for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) {
339 const videoAttributes = {
340 name: fixture,
341 fixture
342 }
343
a1587156 344 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
14e2014a
C
345
346 await waitJobs(servers)
347
348 for (const server of servers) {
349 const res = await getVideosList(server.url)
350
351 const video = res.body.data.find(v => v.name === videoAttributes.name)
352 const res2 = await getVideo(server.url, video.id)
353 const videoDetails = res2.body
354
355 expect(videoDetails.files).to.have.lengthOf(4)
356
a1587156 357 const magnetUri = videoDetails.files[0].magnetUri
14e2014a
C
358 expect(magnetUri).to.contain('.mp4')
359 }
360 }
361 })
362
7ed2c1a4
FA
363 it('Should correctly detect if quick transcode is possible', async function () {
364 this.timeout(10000)
365
366 expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true
367 expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false
368 })
369
b345a804
C
370 it('Should merge an audio file with the preview file', async function () {
371 this.timeout(60000)
372
373 const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
a1587156 374 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg)
b345a804
C
375
376 await waitJobs(servers)
377
378 for (const server of servers) {
379 const res = await getVideosList(server.url)
380
381 const video = res.body.data.find(v => v.name === 'audio_with_preview')
382 const res2 = await getVideo(server.url, video.id)
383 const videoDetails: VideoDetails = res2.body
384
385 expect(videoDetails.files).to.have.lengthOf(1)
386
387 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: 200 })
388 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: 200 })
389
a1587156 390 const magnetUri = videoDetails.files[0].magnetUri
b345a804
C
391 expect(magnetUri).to.contain('.mp4')
392 }
393 })
394
395 it('Should upload an audio file and choose a default background image', async function () {
396 this.timeout(60000)
397
398 const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' }
a1587156 399 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg)
b345a804
C
400
401 await waitJobs(servers)
402
403 for (const server of servers) {
404 const res = await getVideosList(server.url)
405
406 const video = res.body.data.find(v => v.name === 'audio_without_preview')
407 const res2 = await getVideo(server.url, video.id)
408 const videoDetails = res2.body
409
410 expect(videoDetails.files).to.have.lengthOf(1)
411
412 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: 200 })
413 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: 200 })
414
a1587156 415 const magnetUri = videoDetails.files[0].magnetUri
b345a804
C
416 expect(magnetUri).to.contain('.mp4')
417 }
418 })
419
837666fe
RK
420 it('Should downscale to the closest divisor standard framerate', async function () {
421 this.timeout(160000)
422
423 let tempFixturePath: string
424
425 {
c7f36e4f 426 tempFixturePath = await generateVideoWithFramerate(59)
837666fe
RK
427
428 const fps = await getVideoFileFPS(tempFixturePath)
429 expect(fps).to.be.equal(59)
430 }
431
432 const videoAttributes = {
433 name: '59fps video',
434 description: '59fps video',
435 fixture: tempFixturePath
436 }
437
438 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
439
440 await waitJobs(servers)
441
442 for (const server of servers) {
443 const res = await getVideosList(server.url)
444
445 const video = res.body.data.find(v => v.name === videoAttributes.name)
837666fe 446
c7f36e4f 447 {
a1587156 448 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-240.mp4')
c7f36e4f
C
449 const fps = await getVideoFileFPS(path)
450 expect(fps).to.be.equal(25)
451 }
452
453 {
a1587156 454 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-720.mp4')
c7f36e4f
C
455 const fps = await getVideoFileFPS(path)
456 expect(fps).to.be.equal(59)
457 }
837666fe
RK
458 }
459 })
460
7c3b7976
C
461 after(async function () {
462 await cleanupTests(servers)
0e1dc3e7
C
463 })
464})