Merge from upstream
[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 * as ffmpeg from 'fluent-ffmpeg'
7 import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos'
8 import { audio, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
9 import {
10   buildAbsoluteFixturePath,
11   doubleFollow,
12   flushAndRunMultipleServers,
13   getMyVideos,
14   getVideo,
15   getVideosList,
16   killallServers,
17   root,
18   ServerInfo,
19   setAccessTokensToServers,
20   uploadVideo,
21   webtorrentAdd,
22   generateHighBitrateVideo
23 } from '../../../../shared/utils'
24 import { join } from 'path'
25 import { waitJobs } from '../../../../shared/utils/server/jobs'
26 import { pathExists } from 'fs-extra'
27 import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
28
29 const expect = chai.expect
30
31 describe('Test video transcoding', function () {
32   let servers: ServerInfo[] = []
33
34   before(async function () {
35     this.timeout(30000)
36
37     // Run servers
38     servers = await flushAndRunMultipleServers(2)
39
40     await setAccessTokensToServers(servers)
41
42     await doubleFollow(servers[0], servers[1])
43   })
44
45   it('Should not transcode video on server 1', async function () {
46     this.timeout(60000)
47
48     const videoAttributes = {
49       name: 'my super name for server 1',
50       description: 'my super description for server 1',
51       fixture: 'video_short.webm'
52     }
53     await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
54
55     await waitJobs(servers)
56
57     for (const server of servers) {
58       const res = await getVideosList(server.url)
59       const video = res.body.data[ 0 ]
60
61       const res2 = await getVideo(server.url, video.id)
62       const videoDetails = res2.body
63       expect(videoDetails.files).to.have.lengthOf(1)
64
65       const magnetUri = videoDetails.files[ 0 ].magnetUri
66       expect(magnetUri).to.match(/\.webm/)
67
68       const torrent = await webtorrentAdd(magnetUri, true)
69       expect(torrent.files).to.be.an('array')
70       expect(torrent.files.length).to.equal(1)
71       expect(torrent.files[ 0 ].path).match(/\.webm$/)
72     }
73   })
74
75   it('Should transcode video on server 2', async function () {
76     this.timeout(60000)
77
78     const videoAttributes = {
79       name: 'my super name for server 2',
80       description: 'my super description for server 2',
81       fixture: 'video_short.webm'
82     }
83     await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
84
85     await waitJobs(servers)
86
87     for (const server of servers) {
88       const res = await getVideosList(server.url)
89
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
93
94       expect(videoDetails.files).to.have.lengthOf(4)
95
96       const magnetUri = videoDetails.files[ 0 ].magnetUri
97       expect(magnetUri).to.match(/\.mp4/)
98
99       const torrent = await webtorrentAdd(magnetUri, true)
100       expect(torrent.files).to.be.an('array')
101       expect(torrent.files.length).to.equal(1)
102       expect(torrent.files[ 0 ].path).match(/\.mp4$/)
103     }
104   })
105
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
117     for (const server of servers) {
118       const res = await getVideosList(server.url)
119
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
123
124       expect(videoDetails.files).to.have.lengthOf(4)
125
126       const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4')
127       const probe = await audio.get(path)
128
129       if (probe.audioStream) {
130         expect(probe.audioStream[ 'codec_name' ]).to.be.equal('aac')
131         expect(probe.audioStream[ 'bit_rate' ]).to.be.at.most(384 * 8000)
132       } else {
133         this.fail('Could not retrieve the audio stream on ' + probe.absolutePath)
134       }
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
149     for (const server of servers) {
150       const res = await getVideosList(server.url)
151
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
155
156       expect(videoDetails.files).to.have.lengthOf(4)
157       const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4')
158       const probe = await audio.get(path)
159       expect(probe).to.not.have.property('audioStream')
160     }
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
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)
183       const fixtureVideoProbe = await audio.get(fixturePath)
184       const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4')
185       const videoProbe = await audio.get(path)
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       }
192     }
193   })
194
195   it('Should transcode a 60 FPS video', async function () {
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',
201       fixture: '60fps_720p_small.mp4'
202     }
203     await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
204
205     await waitJobs(servers)
206
207     for (const server of servers) {
208       const res = await getVideosList(server.url)
209
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
213
214       expect(videoDetails.files).to.have.lengthOf(4)
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)
219
220       for (const resolution of [ '240', '360', '480' ]) {
221         const path = join(root(), 'test2', 'videos', video.uuid + '-' + resolution + '.mp4')
222         const fps = await getVideoFileFPS(path)
223
224         expect(fps).to.be.below(31)
225       }
226
227       const path = join(root(), 'test2', 'videos', video.uuid + '-720.mp4')
228       const fps = await getVideoFileFPS(path)
229
230       expect(fps).to.be.above(58).and.below(62)
231     }
232   })
233
234   it('Should wait for transcoding before publishing the video', async function () {
235     this.timeout(80000)
236
237     {
238       // Upload the video, but wait transcoding
239       const videoAttributes = {
240         name: 'waiting video',
241         fixture: 'video_short1.webm',
242         waitTranscoding: true
243       }
244       const resVideo = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributes)
245       const videoId = resVideo.body.video.uuid
246
247       // Should be in transcode state
248       const { body } = await getVideo(servers[ 1 ].url, videoId)
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)
256       const videoToFindInMine = resMyVideos.body.data.find(v => v.name === videoAttributes.name)
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)
264       const videoToFindInList = resVideos.body.data.find(v => v.name === videoAttributes.name)
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
271     await waitJobs(servers)
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
287   it('Should respect maximum bitrate values', async function () {
288     this.timeout(160000)
289
290     let tempFixturePath: string
291
292     {
293       tempFixturePath = await generateHighBitrateVideo()
294
295       const bitrate = await getVideoFileBitrate(tempFixturePath)
296       expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 60, VIDEO_TRANSCODING_FPS))
297     }
298
299     const videoAttributes = {
300       name: 'high bitrate video',
301       description: 'high bitrate video',
302       fixture: tempFixturePath
303     }
304
305     await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
306
307     await waitJobs(servers)
308
309     for (const server of servers) {
310       const res = await getVideosList(server.url)
311
312       const video = res.body.data.find(v => v.name === videoAttributes.name)
313
314       for (const resolution of ['240', '360', '480', '720', '1080']) {
315         const path = join(root(), 'test2', 'videos', video.uuid + '-' + resolution + '.mp4')
316         const bitrate = await getVideoFileBitrate(path)
317         const fps = await getVideoFileFPS(path)
318         const resolution2 = await getVideoFileResolution(path)
319
320         expect(resolution2.videoFileResolution.toString()).to.equal(resolution)
321         expect(bitrate).to.be.below(getMaxBitrate(resolution2.videoFileResolution, fps, VIDEO_TRANSCODING_FPS))
322       }
323     }
324   })
325
326   after(async function () {
327     killallServers(servers)
328   })
329 })