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