aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/shared/shared-video-miniature/abstract-video-list.ts3
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts2
-rw-r--r--server/tests/api/videos/video-transcoder.ts942
3 files changed, 523 insertions, 424 deletions
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts
index 14a29d05f..5361f6d6c 100644
--- a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts
+++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts
@@ -382,8 +382,9 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, AfterConte
382 } 382 }
383 383
384 private getUrlWithoutParams () { 384 private getUrlWithoutParams () {
385 let urlTree = this.router.parseUrl(this.router.url) 385 const urlTree = this.router.parseUrl(this.router.url)
386 urlTree.queryParams = {} 386 urlTree.queryParams = {}
387
387 return urlTree.toString() 388 return urlTree.toString()
388 } 389 }
389} 390}
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index 8f88f0a8a..853cfebcd 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -111,6 +111,8 @@ async function handleWebTorrentMergeAudioJob (job: Bull.Job, payload: MergeAudio
111 await mergeAudioVideofile(video, payload.resolution, job) 111 await mergeAudioVideofile(video, payload.resolution, job)
112 112
113 await retryTransactionWrapper(onNewWebTorrentFileResolution, video, user, payload) 113 await retryTransactionWrapper(onNewWebTorrentFileResolution, video, user, payload)
114
115 await createLowerResolutionsJobs(video, user, payload.resolution, false)
114} 116}
115 117
116async function handleWebTorrentOptimizeJob (job: Bull.Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight, user: MUserId) { 118async function handleWebTorrentOptimizeJob (job: Bull.Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight, user: MUserId) {
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts
index 5ad02df2f..1058baaa3 100644
--- a/server/tests/api/videos/video-transcoder.ts
+++ b/server/tests/api/videos/video-transcoder.ts
@@ -43,6 +43,28 @@ import {
43 43
44const expect = chai.expect 44const expect = chai.expect
45 45
46function updateConfigForTranscoding (server: ServerInfo) {
47 return updateCustomSubConfig(server.url, server.accessToken, {
48 transcoding: {
49 enabled: true,
50 allowAdditionalExtensions: true,
51 allowAudioFiles: true,
52 hls: { enabled: true },
53 webtorrent: { enabled: true },
54 resolutions: {
55 '0p': false,
56 '240p': true,
57 '360p': true,
58 '480p': true,
59 '720p': true,
60 '1080p': true,
61 '1440p': true,
62 '2160p': true
63 }
64 }
65 })
66}
67
46describe('Test video transcoding', function () { 68describe('Test video transcoding', function () {
47 let servers: ServerInfo[] = [] 69 let servers: ServerInfo[] = []
48 let video4k: string 70 let video4k: string
@@ -56,585 +78,659 @@ describe('Test video transcoding', function () {
56 await setAccessTokensToServers(servers) 78 await setAccessTokensToServers(servers)
57 79
58 await doubleFollow(servers[0], servers[1]) 80 await doubleFollow(servers[0], servers[1])
81
82 await updateConfigForTranscoding(servers[1])
59 }) 83 })
60 84
61 it('Should not transcode video on server 1', async function () { 85 describe('Basic transcoding (or not)', function () {
62 this.timeout(60_000)
63 86
64 const videoAttributes = { 87 it('Should not transcode video on server 1', async function () {
65 name: 'my super name for server 1', 88 this.timeout(60_000)
66 description: 'my super description for server 1',
67 fixture: 'video_short.webm'
68 }
69 await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
70 89
71 await waitJobs(servers) 90 const videoAttributes = {
91 name: 'my super name for server 1',
92 description: 'my super description for server 1',
93 fixture: 'video_short.webm'
94 }
95 await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
72 96
73 for (const server of servers) { 97 await waitJobs(servers)
74 const res = await getVideosList(server.url)
75 const video = res.body.data[0]
76 98
77 const res2 = await getVideo(server.url, video.id) 99 for (const server of servers) {
78 const videoDetails = res2.body 100 const res = await getVideosList(server.url)
79 expect(videoDetails.files).to.have.lengthOf(1) 101 const video = res.body.data[0]
80 102
81 const magnetUri = videoDetails.files[0].magnetUri 103 const res2 = await getVideo(server.url, video.id)
82 expect(magnetUri).to.match(/\.webm/) 104 const videoDetails = res2.body
105 expect(videoDetails.files).to.have.lengthOf(1)
83 106
84 const torrent = await webtorrentAdd(magnetUri, true) 107 const magnetUri = videoDetails.files[0].magnetUri
85 expect(torrent.files).to.be.an('array') 108 expect(magnetUri).to.match(/\.webm/)
86 expect(torrent.files.length).to.equal(1)
87 expect(torrent.files[0].path).match(/\.webm$/)
88 }
89 })
90 109
91 it('Should transcode video on server 2', async function () { 110 const torrent = await webtorrentAdd(magnetUri, true)
92 this.timeout(120_000) 111 expect(torrent.files).to.be.an('array')
112 expect(torrent.files.length).to.equal(1)
113 expect(torrent.files[0].path).match(/\.webm$/)
114 }
115 })
93 116
94 const videoAttributes = { 117 it('Should transcode video on server 2', async function () {
95 name: 'my super name for server 2', 118 this.timeout(120_000)
96 description: 'my super description for server 2', 119
97 fixture: 'video_short.webm' 120 const videoAttributes = {
98 } 121 name: 'my super name for server 2',
99 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes) 122 description: 'my super description for server 2',
123 fixture: 'video_short.webm'
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)
100 131
101 await waitJobs(servers) 132 const video = res.body.data.find(v => v.name === videoAttributes.name)
133 const res2 = await getVideo(server.url, video.id)
134 const videoDetails = res2.body
102 135
103 for (const server of servers) { 136 expect(videoDetails.files).to.have.lengthOf(4)
104 const res = await getVideosList(server.url)
105 137
106 const video = res.body.data.find(v => v.name === videoAttributes.name) 138 const magnetUri = videoDetails.files[0].magnetUri
107 const res2 = await getVideo(server.url, video.id) 139 expect(magnetUri).to.match(/\.mp4/)
108 const videoDetails = res2.body
109 140
110 expect(videoDetails.files).to.have.lengthOf(4) 141 const torrent = await webtorrentAdd(magnetUri, true)
142 expect(torrent.files).to.be.an('array')
143 expect(torrent.files.length).to.equal(1)
144 expect(torrent.files[0].path).match(/\.mp4$/)
145 }
146 })
111 147
112 const magnetUri = videoDetails.files[0].magnetUri 148 it('Should wait for transcoding before publishing the video', async function () {
113 expect(magnetUri).to.match(/\.mp4/) 149 this.timeout(160_000)
114 150
115 const torrent = await webtorrentAdd(magnetUri, true) 151 {
116 expect(torrent.files).to.be.an('array') 152 // Upload the video, but wait transcoding
117 expect(torrent.files.length).to.equal(1) 153 const videoAttributes = {
118 expect(torrent.files[0].path).match(/\.mp4$/) 154 name: 'waiting video',
119 } 155 fixture: 'video_short1.webm',
120 }) 156 waitTranscoding: true
157 }
158 const resVideo = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
159 const videoId = resVideo.body.video.uuid
160
161 // Should be in transcode state
162 const { body } = await getVideo(servers[1].url, videoId)
163 expect(body.name).to.equal('waiting video')
164 expect(body.state.id).to.equal(VideoState.TO_TRANSCODE)
165 expect(body.state.label).to.equal('To transcode')
166 expect(body.waitTranscoding).to.be.true
167
168 // Should have my video
169 const resMyVideos = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 10)
170 const videoToFindInMine = resMyVideos.body.data.find(v => v.name === videoAttributes.name)
171 expect(videoToFindInMine).not.to.be.undefined
172 expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE)
173 expect(videoToFindInMine.state.label).to.equal('To transcode')
174 expect(videoToFindInMine.waitTranscoding).to.be.true
175
176 // Should not list this video
177 const resVideos = await getVideosList(servers[1].url)
178 const videoToFindInList = resVideos.body.data.find(v => v.name === videoAttributes.name)
179 expect(videoToFindInList).to.be.undefined
180
181 // Server 1 should not have the video yet
182 await getVideo(servers[0].url, videoId, HttpStatusCode.NOT_FOUND_404)
183 }
121 184
122 it('Should transcode high bit rate mp3 to proper bit rate', async function () { 185 await waitJobs(servers)
123 this.timeout(60_000)
124 186
125 const videoAttributes = { 187 for (const server of servers) {
126 name: 'mp3_256k', 188 const res = await getVideosList(server.url)
127 fixture: 'video_short_mp3_256k.mp4' 189 const videoToFind = res.body.data.find(v => v.name === 'waiting video')
128 } 190 expect(videoToFind).not.to.be.undefined
129 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
130 191
131 await waitJobs(servers) 192 const res2 = await getVideo(server.url, videoToFind.id)
193 const videoDetails: VideoDetails = res2.body
132 194
133 for (const server of servers) { 195 expect(videoDetails.state.id).to.equal(VideoState.PUBLISHED)
134 const res = await getVideosList(server.url) 196 expect(videoDetails.state.label).to.equal('Published')
197 expect(videoDetails.waitTranscoding).to.be.true
198 }
199 })
135 200
136 const video = res.body.data.find(v => v.name === videoAttributes.name) 201 it('Should accept and transcode additional extensions', async function () {
137 const res2 = await getVideo(server.url, video.id) 202 this.timeout(300_000)
138 const videoDetails: VideoDetails = res2.body
139 203
140 expect(videoDetails.files).to.have.lengthOf(4) 204 let tempFixturePath: string
141 205
142 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4')) 206 {
143 const probe = await getAudioStream(path) 207 tempFixturePath = await generateHighBitrateVideo()
144 208
145 if (probe.audioStream) { 209 const bitrate = await getVideoFileBitrate(tempFixturePath)
146 expect(probe.audioStream['codec_name']).to.be.equal('aac') 210 expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 25, VIDEO_TRANSCODING_FPS))
147 expect(probe.audioStream['bit_rate']).to.be.at.most(384 * 8000)
148 } else {
149 this.fail('Could not retrieve the audio stream on ' + probe.absolutePath)
150 } 211 }
151 }
152 })
153 212
154 it('Should transcode video with no audio and have no audio itself', async function () { 213 for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) {
155 this.timeout(60_000) 214 const videoAttributes = {
215 name: fixture,
216 fixture
217 }
156 218
157 const videoAttributes = { 219 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
158 name: 'no_audio',
159 fixture: 'video_short_no_audio.mp4'
160 }
161 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
162 220
163 await waitJobs(servers) 221 await waitJobs(servers)
164 222
165 for (const server of servers) { 223 for (const server of servers) {
166 const res = await getVideosList(server.url) 224 const res = await getVideosList(server.url)
167 225
168 const video = res.body.data.find(v => v.name === videoAttributes.name) 226 const video = res.body.data.find(v => v.name === videoAttributes.name)
169 const res2 = await getVideo(server.url, video.id) 227 const res2 = await getVideo(server.url, video.id)
170 const videoDetails: VideoDetails = res2.body 228 const videoDetails = res2.body
171 229
172 expect(videoDetails.files).to.have.lengthOf(4) 230 expect(videoDetails.files).to.have.lengthOf(4)
173 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4'))
174 const probe = await getAudioStream(path)
175 expect(probe).to.not.have.property('audioStream')
176 }
177 })
178 231
179 it('Should leave the audio untouched, but properly transcode the video', async function () { 232 const magnetUri = videoDetails.files[0].magnetUri
180 this.timeout(60_000) 233 expect(magnetUri).to.contain('.mp4')
234 }
235 }
236 })
181 237
182 const videoAttributes = { 238 it('Should transcode a 4k video', async function () {
183 name: 'untouched_audio', 239 this.timeout(200_000)
184 fixture: 'video_short.mp4'
185 }
186 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
187 240
188 await waitJobs(servers) 241 const videoAttributes = {
242 name: '4k video',
243 fixture: 'video_short_4k.mp4'
244 }
189 245
190 for (const server of servers) { 246 const resUpload = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
191 const res = await getVideosList(server.url) 247 video4k = resUpload.body.video.uuid
192 248
193 const video = res.body.data.find(v => v.name === videoAttributes.name) 249 await waitJobs(servers)
194 const res2 = await getVideo(server.url, video.id)
195 const videoDetails: VideoDetails = res2.body
196 250
197 expect(videoDetails.files).to.have.lengthOf(4) 251 const resolutions = [ 240, 360, 480, 720, 1080, 1440, 2160 ]
198 252
199 const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture) 253 for (const server of servers) {
200 const fixtureVideoProbe = await getAudioStream(fixturePath) 254 const res = await getVideo(server.url, video4k)
201 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4')) 255 const videoDetails: VideoDetails = res.body
202 256
203 const videoProbe = await getAudioStream(path) 257 expect(videoDetails.files).to.have.lengthOf(resolutions.length)
204 258
205 if (videoProbe.audioStream && fixtureVideoProbe.audioStream) { 259 for (const r of resolutions) {
206 const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ] 260 expect(videoDetails.files.find(f => f.resolution.id === r)).to.not.be.undefined
207 expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit)) 261 expect(videoDetails.streamingPlaylists[0].files.find(f => f.resolution.id === r)).to.not.be.undefined
208 } else { 262 }
209 this.fail('Could not retrieve the audio stream on ' + videoProbe.absolutePath)
210 } 263 }
211 } 264 })
212 }) 265 })
213 266
214 it('Should transcode a 60 FPS video', async function () { 267 describe('Audio transcoding', function () {
215 this.timeout(60_000)
216 268
217 const videoAttributes = { 269 it('Should transcode high bit rate mp3 to proper bit rate', async function () {
218 name: 'my super 30fps name for server 2', 270 this.timeout(60_000)
219 description: 'my super 30fps description for server 2',
220 fixture: '60fps_720p_small.mp4'
221 }
222 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
223 271
224 await waitJobs(servers) 272 const videoAttributes = {
273 name: 'mp3_256k',
274 fixture: 'video_short_mp3_256k.mp4'
275 }
276 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
225 277
226 for (const server of servers) { 278 await waitJobs(servers)
227 const res = await getVideosList(server.url)
228 279
229 const video = res.body.data.find(v => v.name === videoAttributes.name) 280 for (const server of servers) {
230 const res2 = await getVideo(server.url, video.id) 281 const res = await getVideosList(server.url)
231 const videoDetails: VideoDetails = res2.body
232 282
233 expect(videoDetails.files).to.have.lengthOf(4) 283 const video = res.body.data.find(v => v.name === videoAttributes.name)
234 expect(videoDetails.files[0].fps).to.be.above(58).and.below(62) 284 const res2 = await getVideo(server.url, video.id)
235 expect(videoDetails.files[1].fps).to.be.below(31) 285 const videoDetails: VideoDetails = res2.body
236 expect(videoDetails.files[2].fps).to.be.below(31)
237 expect(videoDetails.files[3].fps).to.be.below(31)
238 286
239 for (const resolution of [ '240', '360', '480' ]) { 287 expect(videoDetails.files).to.have.lengthOf(4)
240 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-' + resolution + '.mp4'))
241 const fps = await getVideoFileFPS(path)
242 288
243 expect(fps).to.be.below(31) 289 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4'))
290 const probe = await getAudioStream(path)
291
292 if (probe.audioStream) {
293 expect(probe.audioStream['codec_name']).to.be.equal('aac')
294 expect(probe.audioStream['bit_rate']).to.be.at.most(384 * 8000)
295 } else {
296 this.fail('Could not retrieve the audio stream on ' + probe.absolutePath)
297 }
244 } 298 }
299 })
245 300
246 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-720.mp4')) 301 it('Should transcode video with no audio and have no audio itself', async function () {
247 const fps = await getVideoFileFPS(path) 302 this.timeout(60_000)
248
249 expect(fps).to.be.above(58).and.below(62)
250 }
251 })
252
253 it('Should wait for transcoding before publishing the video', async function () {
254 this.timeout(160_000)
255 303
256 {
257 // Upload the video, but wait transcoding
258 const videoAttributes = { 304 const videoAttributes = {
259 name: 'waiting video', 305 name: 'no_audio',
260 fixture: 'video_short1.webm', 306 fixture: 'video_short_no_audio.mp4'
261 waitTranscoding: true 307 }
262 } 308 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
263 const resVideo = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
264 const videoId = resVideo.body.video.uuid
265
266 // Should be in transcode state
267 const { body } = await getVideo(servers[1].url, videoId)
268 expect(body.name).to.equal('waiting video')
269 expect(body.state.id).to.equal(VideoState.TO_TRANSCODE)
270 expect(body.state.label).to.equal('To transcode')
271 expect(body.waitTranscoding).to.be.true
272
273 // Should have my video
274 const resMyVideos = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 10)
275 const videoToFindInMine = resMyVideos.body.data.find(v => v.name === videoAttributes.name)
276 expect(videoToFindInMine).not.to.be.undefined
277 expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE)
278 expect(videoToFindInMine.state.label).to.equal('To transcode')
279 expect(videoToFindInMine.waitTranscoding).to.be.true
280
281 // Should not list this video
282 const resVideos = await getVideosList(servers[1].url)
283 const videoToFindInList = resVideos.body.data.find(v => v.name === videoAttributes.name)
284 expect(videoToFindInList).to.be.undefined
285
286 // Server 1 should not have the video yet
287 await getVideo(servers[0].url, videoId, HttpStatusCode.NOT_FOUND_404)
288 }
289 309
290 await waitJobs(servers) 310 await waitJobs(servers)
291 311
292 for (const server of servers) { 312 for (const server of servers) {
293 const res = await getVideosList(server.url) 313 const res = await getVideosList(server.url)
294 const videoToFind = res.body.data.find(v => v.name === 'waiting video')
295 expect(videoToFind).not.to.be.undefined
296 314
297 const res2 = await getVideo(server.url, videoToFind.id) 315 const video = res.body.data.find(v => v.name === videoAttributes.name)
298 const videoDetails: VideoDetails = res2.body 316 const res2 = await getVideo(server.url, video.id)
317 const videoDetails: VideoDetails = res2.body
299 318
300 expect(videoDetails.state.id).to.equal(VideoState.PUBLISHED) 319 expect(videoDetails.files).to.have.lengthOf(4)
301 expect(videoDetails.state.label).to.equal('Published') 320 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4'))
302 expect(videoDetails.waitTranscoding).to.be.true 321 const probe = await getAudioStream(path)
303 } 322 expect(probe).to.not.have.property('audioStream')
304 }) 323 }
324 })
305 325
306 it('Should respect maximum bitrate values', async function () { 326 it('Should leave the audio untouched, but properly transcode the video', async function () {
307 this.timeout(160_000) 327 this.timeout(60_000)
308 328
309 let tempFixturePath: string 329 const videoAttributes = {
330 name: 'untouched_audio',
331 fixture: 'video_short.mp4'
332 }
333 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
310 334
311 { 335 await waitJobs(servers)
312 tempFixturePath = await generateHighBitrateVideo()
313 336
314 const bitrate = await getVideoFileBitrate(tempFixturePath) 337 for (const server of servers) {
315 expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 25, VIDEO_TRANSCODING_FPS)) 338 const res = await getVideosList(server.url)
316 }
317 339
318 const videoAttributes = { 340 const video = res.body.data.find(v => v.name === videoAttributes.name)
319 name: 'high bitrate video', 341 const res2 = await getVideo(server.url, video.id)
320 description: 'high bitrate video', 342 const videoDetails: VideoDetails = res2.body
321 fixture: tempFixturePath
322 }
323 343
324 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes) 344 expect(videoDetails.files).to.have.lengthOf(4)
325 345
326 await waitJobs(servers) 346 const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture)
347 const fixtureVideoProbe = await getAudioStream(fixturePath)
348 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4'))
327 349
328 for (const server of servers) { 350 const videoProbe = await getAudioStream(path)
329 const res = await getVideosList(server.url)
330 351
331 const video = res.body.data.find(v => v.name === videoAttributes.name) 352 if (videoProbe.audioStream && fixtureVideoProbe.audioStream) {
353 const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ]
354 expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit))
355 } else {
356 this.fail('Could not retrieve the audio stream on ' + videoProbe.absolutePath)
357 }
358 }
359 })
360 })
332 361
333 for (const resolution of [ '240', '360', '480', '720', '1080' ]) { 362 describe('Audio upload', function () {
334 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-' + resolution + '.mp4')) 363
364 before(async function () {
365 await updateCustomSubConfig(servers[1].url, servers[1].accessToken, {
366 transcoding: {
367 hls: { enabled: true },
368 webtorrent: { enabled: true },
369 resolutions: {
370 '0p': false,
371 '240p': false,
372 '360p': false,
373 '480p': false,
374 '720p': false,
375 '1080p': false,
376 '1440p': false,
377 '2160p': false
378 }
379 }
380 })
381 })
335 382
336 const bitrate = await getVideoFileBitrate(path) 383 it('Should merge an audio file with the preview file', async function () {
337 const fps = await getVideoFileFPS(path) 384 this.timeout(60_000)
338 const resolution2 = await getVideoFileResolution(path)
339 385
340 expect(resolution2.videoFileResolution.toString()).to.equal(resolution) 386 const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
341 expect(bitrate).to.be.below(getMaxBitrate(resolution2.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) 387 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg)
342 }
343 }
344 })
345 388
346 it('Should accept and transcode additional extensions', async function () { 389 await waitJobs(servers)
347 this.timeout(300_000) 390
391 for (const server of servers) {
392 const res = await getVideosList(server.url)
348 393
349 let tempFixturePath: string 394 const video = res.body.data.find(v => v.name === 'audio_with_preview')
395 const res2 = await getVideo(server.url, video.id)
396 const videoDetails: VideoDetails = res2.body
350 397
351 { 398 expect(videoDetails.files).to.have.lengthOf(1)
352 tempFixturePath = await generateHighBitrateVideo()
353 399
354 const bitrate = await getVideoFileBitrate(tempFixturePath) 400 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 })
355 expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 25, VIDEO_TRANSCODING_FPS)) 401 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 })
356 }
357 402
358 for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) { 403 const magnetUri = videoDetails.files[0].magnetUri
359 const videoAttributes = { 404 expect(magnetUri).to.contain('.mp4')
360 name: fixture,
361 fixture
362 } 405 }
406 })
363 407
364 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes) 408 it('Should upload an audio file and choose a default background image', async function () {
409 this.timeout(60_000)
410
411 const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' }
412 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg)
365 413
366 await waitJobs(servers) 414 await waitJobs(servers)
367 415
368 for (const server of servers) { 416 for (const server of servers) {
369 const res = await getVideosList(server.url) 417 const res = await getVideosList(server.url)
370 418
371 const video = res.body.data.find(v => v.name === videoAttributes.name) 419 const video = res.body.data.find(v => v.name === 'audio_without_preview')
372 const res2 = await getVideo(server.url, video.id) 420 const res2 = await getVideo(server.url, video.id)
373 const videoDetails = res2.body 421 const videoDetails = res2.body
374 422
375 expect(videoDetails.files).to.have.lengthOf(4) 423 expect(videoDetails.files).to.have.lengthOf(1)
424
425 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 })
426 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 })
376 427
377 const magnetUri = videoDetails.files[0].magnetUri 428 const magnetUri = videoDetails.files[0].magnetUri
378 expect(magnetUri).to.contain('.mp4') 429 expect(magnetUri).to.contain('.mp4')
379 } 430 }
380 } 431 })
381 })
382 432
383 it('Should correctly detect if quick transcode is possible', async function () { 433 it('Should upload an audio file and create an audio version only', async function () {
384 this.timeout(10_000) 434 this.timeout(60_000)
385 435
386 expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true 436 await updateCustomSubConfig(servers[1].url, servers[1].accessToken, {
387 expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false 437 transcoding: {
388 }) 438 hls: { enabled: true },
439 webtorrent: { enabled: true },
440 resolutions: {
441 '0p': true,
442 '240p': false,
443 '360p': false
444 }
445 }
446 })
389 447
390 it('Should merge an audio file with the preview file', async function () { 448 const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
391 this.timeout(60_000) 449 const resVideo = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg)
392 450
393 const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } 451 await waitJobs(servers)
394 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg)
395 452
396 await waitJobs(servers) 453 for (const server of servers) {
454 const res2 = await getVideo(server.url, resVideo.body.video.id)
455 const videoDetails: VideoDetails = res2.body
397 456
398 for (const server of servers) { 457 for (const files of [ videoDetails.files, videoDetails.streamingPlaylists[0].files ]) {
399 const res = await getVideosList(server.url) 458 expect(files).to.have.lengthOf(2)
459 expect(files.find(f => f.resolution.id === 0)).to.not.be.undefined
460 }
461 }
400 462
401 const video = res.body.data.find(v => v.name === 'audio_with_preview') 463 await updateConfigForTranscoding(servers[1])
402 const res2 = await getVideo(server.url, video.id) 464 })
403 const videoDetails: VideoDetails = res2.body 465 })
404 466
405 expect(videoDetails.files).to.have.lengthOf(1) 467 describe('Framerate', function () {
406 468
407 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 }) 469 it('Should transcode a 60 FPS video', async function () {
408 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 }) 470 this.timeout(60_000)
409 471
410 const magnetUri = videoDetails.files[0].magnetUri 472 const videoAttributes = {
411 expect(magnetUri).to.contain('.mp4') 473 name: 'my super 30fps name for server 2',
412 } 474 description: 'my super 30fps description for server 2',
413 }) 475 fixture: '60fps_720p_small.mp4'
476 }
477 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
414 478
415 it('Should upload an audio file and choose a default background image', async function () { 479 await waitJobs(servers)
416 this.timeout(60_000)
417 480
418 const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' } 481 for (const server of servers) {
419 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg) 482 const res = await getVideosList(server.url)
420 483
421 await waitJobs(servers) 484 const video = res.body.data.find(v => v.name === videoAttributes.name)
485 const res2 = await getVideo(server.url, video.id)
486 const videoDetails: VideoDetails = res2.body
422 487
423 for (const server of servers) { 488 expect(videoDetails.files).to.have.lengthOf(4)
424 const res = await getVideosList(server.url) 489 expect(videoDetails.files[0].fps).to.be.above(58).and.below(62)
490 expect(videoDetails.files[1].fps).to.be.below(31)
491 expect(videoDetails.files[2].fps).to.be.below(31)
492 expect(videoDetails.files[3].fps).to.be.below(31)
425 493
426 const video = res.body.data.find(v => v.name === 'audio_without_preview') 494 for (const resolution of [ '240', '360', '480' ]) {
427 const res2 = await getVideo(server.url, video.id) 495 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-' + resolution + '.mp4'))
428 const videoDetails = res2.body 496 const fps = await getVideoFileFPS(path)
429 497
430 expect(videoDetails.files).to.have.lengthOf(1) 498 expect(fps).to.be.below(31)
499 }
431 500
432 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 }) 501 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-720.mp4'))
433 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 }) 502 const fps = await getVideoFileFPS(path)
434 503
435 const magnetUri = videoDetails.files[0].magnetUri 504 expect(fps).to.be.above(58).and.below(62)
436 expect(magnetUri).to.contain('.mp4') 505 }
437 } 506 })
438 })
439 507
440 it('Should downscale to the closest divisor standard framerate', async function () { 508 it('Should downscale to the closest divisor standard framerate', async function () {
441 this.timeout(200_000) 509 this.timeout(200_000)
442 510
443 let tempFixturePath: string 511 let tempFixturePath: string
444 512
445 { 513 {
446 tempFixturePath = await generateVideoWithFramerate(59) 514 tempFixturePath = await generateVideoWithFramerate(59)
447 515
448 const fps = await getVideoFileFPS(tempFixturePath) 516 const fps = await getVideoFileFPS(tempFixturePath)
449 expect(fps).to.be.equal(59) 517 expect(fps).to.be.equal(59)
450 } 518 }
451 519
452 const videoAttributes = { 520 const videoAttributes = {
453 name: '59fps video', 521 name: '59fps video',
454 description: '59fps video', 522 description: '59fps video',
455 fixture: tempFixturePath 523 fixture: tempFixturePath
456 } 524 }
457 525
458 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes) 526 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
459 527
460 await waitJobs(servers) 528 await waitJobs(servers)
461 529
462 for (const server of servers) { 530 for (const server of servers) {
463 const res = await getVideosList(server.url) 531 const res = await getVideosList(server.url)
464 532
465 const video = res.body.data.find(v => v.name === videoAttributes.name) 533 const video = res.body.data.find(v => v.name === videoAttributes.name)
466 534
467 { 535 {
468 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4')) 536 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4'))
469 const fps = await getVideoFileFPS(path) 537 const fps = await getVideoFileFPS(path)
470 expect(fps).to.be.equal(25) 538 expect(fps).to.be.equal(25)
539 }
540
541 {
542 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-720.mp4'))
543 const fps = await getVideoFileFPS(path)
544 expect(fps).to.be.equal(59)
545 }
471 } 546 }
547 })
548 })
549
550 describe('Bitrate control', function () {
551 it('Should respect maximum bitrate values', async function () {
552 this.timeout(160_000)
553
554 let tempFixturePath: string
472 555
473 { 556 {
474 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-720.mp4')) 557 tempFixturePath = await generateHighBitrateVideo()
475 const fps = await getVideoFileFPS(path)
476 expect(fps).to.be.equal(59)
477 }
478 }
479 })
480 558
481 it('Should not transcode to an higher bitrate than the original file', async function () { 559 const bitrate = await getVideoFileBitrate(tempFixturePath)
482 this.timeout(160_000) 560 expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 25, VIDEO_TRANSCODING_FPS))
483
484 const config = {
485 transcoding: {
486 enabled: true,
487 resolutions: {
488 '240p': true,
489 '360p': true,
490 '480p': true,
491 '720p': true,
492 '1080p': true,
493 '1440p': true,
494 '2160p': true
495 },
496 webtorrent: { enabled: true },
497 hls: { enabled: true }
498 } 561 }
499 }
500 await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
501 562
502 const videoAttributes = { 563 const videoAttributes = {
503 name: 'low bitrate', 564 name: 'high bitrate video',
504 fixture: 'low-bitrate.mp4' 565 description: 'high bitrate video',
505 } 566 fixture: tempFixturePath
567 }
506 568
507 const resUpload = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes) 569 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
508 const videoUUID = resUpload.body.video.uuid
509 570
510 await waitJobs(servers) 571 await waitJobs(servers)
511 572
512 const resolutions = [ 240, 360, 480, 720, 1080 ] 573 for (const server of servers) {
513 for (const r of resolutions) { 574 const res = await getVideosList(server.url)
514 const path = `videos/${videoUUID}-${r}.mp4`
515 const size = await getServerFileSize(servers[1], path)
516 expect(size, `${path} not below ${60_000}`).to.be.below(60_000)
517 }
518 })
519 575
520 it('Should provide valid ffprobe data', async function () { 576 const video = res.body.data.find(v => v.name === videoAttributes.name)
521 this.timeout(160_000)
522 577
523 const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'ffprobe data' })).uuid 578 for (const resolution of [ '240', '360', '480', '720', '1080' ]) {
524 await waitJobs(servers) 579 const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-' + resolution + '.mp4'))
525 580
526 { 581 const bitrate = await getVideoFileBitrate(path)
527 const path = buildServerDirectory(servers[1], join('videos', videoUUID + '-240.mp4')) 582 const fps = await getVideoFileFPS(path)
528 const metadata = await getMetadataFromFile(path) 583 const resolution2 = await getVideoFileResolution(path)
529 584
530 // expected format properties 585 expect(resolution2.videoFileResolution.toString()).to.equal(resolution)
531 for (const p of [ 586 expect(bitrate).to.be.below(getMaxBitrate(resolution2.videoFileResolution, fps, VIDEO_TRANSCODING_FPS))
532 'tags.encoder', 587 }
533 'format_long_name',
534 'size',
535 'bit_rate'
536 ]) {
537 expect(metadata.format).to.have.nested.property(p)
538 } 588 }
589 })
539 590
540 // expected stream properties 591 it('Should not transcode to an higher bitrate than the original file', async function () {
541 for (const p of [ 592 this.timeout(160_000)
542 'codec_long_name', 593
543 'profile', 594 const config = {
544 'width', 595 transcoding: {
545 'height', 596 enabled: true,
546 'display_aspect_ratio', 597 resolutions: {
547 'avg_frame_rate', 598 '240p': true,
548 'pix_fmt' 599 '360p': true,
549 ]) { 600 '480p': true,
550 expect(metadata.streams[0]).to.have.nested.property(p) 601 '720p': true,
602 '1080p': true,
603 '1440p': true,
604 '2160p': true
605 },
606 webtorrent: { enabled: true },
607 hls: { enabled: true }
608 }
551 } 609 }
610 await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
552 611
553 expect(metadata).to.not.have.nested.property('format.filename') 612 const videoAttributes = {
554 } 613 name: 'low bitrate',
555 614 fixture: 'low-bitrate.mp4'
556 for (const server of servers) { 615 }
557 const res2 = await getVideo(server.url, videoUUID)
558 const videoDetails: VideoDetails = res2.body
559 616
560 const videoFiles = videoDetails.files 617 const resUpload = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
561 .concat(videoDetails.streamingPlaylists[0].files) 618 const videoUUID = resUpload.body.video.uuid
562 expect(videoFiles).to.have.lengthOf(8)
563 619
564 for (const file of videoFiles) { 620 await waitJobs(servers)
565 expect(file.metadata).to.be.undefined
566 expect(file.metadataUrl).to.exist
567 expect(file.metadataUrl).to.contain(servers[1].url)
568 expect(file.metadataUrl).to.contain(videoUUID)
569 621
570 const res3 = await getVideoFileMetadataUrl(file.metadataUrl) 622 const resolutions = [ 240, 360, 480, 720, 1080 ]
571 const metadata: FfprobeData = res3.body 623 for (const r of resolutions) {
572 expect(metadata).to.have.nested.property('format.size') 624 const path = `videos/${videoUUID}-${r}.mp4`
625 const size = await getServerFileSize(servers[1], path)
626 expect(size, `${path} not below ${60_000}`).to.be.below(60_000)
573 } 627 }
574 } 628 })
575 }) 629 })
576 630
577 it('Should transcode a 4k video', async function () { 631 describe('FFprobe', function () {
578 this.timeout(200_000)
579
580 const videoAttributes = {
581 name: '4k video',
582 fixture: 'video_short_4k.mp4'
583 }
584 632
585 const resUpload = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes) 633 it('Should provide valid ffprobe data', async function () {
586 video4k = resUpload.body.video.uuid 634 this.timeout(160_000)
587 635
588 await waitJobs(servers) 636 const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'ffprobe data' })).uuid
637 await waitJobs(servers)
589 638
590 const resolutions = [ 240, 360, 480, 720, 1080, 1440, 2160 ] 639 {
640 const path = buildServerDirectory(servers[1], join('videos', videoUUID + '-240.mp4'))
641 const metadata = await getMetadataFromFile(path)
642
643 // expected format properties
644 for (const p of [
645 'tags.encoder',
646 'format_long_name',
647 'size',
648 'bit_rate'
649 ]) {
650 expect(metadata.format).to.have.nested.property(p)
651 }
652
653 // expected stream properties
654 for (const p of [
655 'codec_long_name',
656 'profile',
657 'width',
658 'height',
659 'display_aspect_ratio',
660 'avg_frame_rate',
661 'pix_fmt'
662 ]) {
663 expect(metadata.streams[0]).to.have.nested.property(p)
664 }
665
666 expect(metadata).to.not.have.nested.property('format.filename')
667 }
591 668
592 for (const server of servers) { 669 for (const server of servers) {
593 const res = await getVideo(server.url, video4k) 670 const res2 = await getVideo(server.url, videoUUID)
594 const videoDetails: VideoDetails = res.body 671 const videoDetails: VideoDetails = res2.body
672
673 const videoFiles = videoDetails.files
674 .concat(videoDetails.streamingPlaylists[0].files)
675 expect(videoFiles).to.have.lengthOf(8)
676
677 for (const file of videoFiles) {
678 expect(file.metadata).to.be.undefined
679 expect(file.metadataUrl).to.exist
680 expect(file.metadataUrl).to.contain(servers[1].url)
681 expect(file.metadataUrl).to.contain(videoUUID)
682
683 const res3 = await getVideoFileMetadataUrl(file.metadataUrl)
684 const metadata: FfprobeData = res3.body
685 expect(metadata).to.have.nested.property('format.size')
686 }
687 }
688 })
595 689
596 expect(videoDetails.files).to.have.lengthOf(resolutions.length) 690 it('Should correctly detect if quick transcode is possible', async function () {
691 this.timeout(10_000)
597 692
598 for (const r of resolutions) { 693 expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true
599 expect(videoDetails.files.find(f => f.resolution.id === r)).to.not.be.undefined 694 expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false
600 expect(videoDetails.streamingPlaylists[0].files.find(f => f.resolution.id === r)).to.not.be.undefined 695 })
601 }
602 }
603 }) 696 })
604 697
605 it('Should have the appropriate priorities for transcoding jobs', async function () { 698 describe('Transcoding job queue', function () {
606 const res = await getJobsListPaginationAndSort({
607 url: servers[1].url,
608 accessToken: servers[1].accessToken,
609 start: 0,
610 count: 100,
611 sort: '-createdAt',
612 jobType: 'video-transcoding'
613 })
614 699
615 const jobs = res.body.data as Job[] 700 it('Should have the appropriate priorities for transcoding jobs', async function () {
701 const res = await getJobsListPaginationAndSort({
702 url: servers[1].url,
703 accessToken: servers[1].accessToken,
704 start: 0,
705 count: 100,
706 sort: '-createdAt',
707 jobType: 'video-transcoding'
708 })
616 709
617 const transcodingJobs = jobs.filter(j => j.data.videoUUID === video4k) 710 const jobs = res.body.data as Job[]
618 711
619 expect(transcodingJobs).to.have.lengthOf(14) 712 const transcodingJobs = jobs.filter(j => j.data.videoUUID === video4k)
620 713
621 const hlsJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-hls') 714 expect(transcodingJobs).to.have.lengthOf(14)
622 const webtorrentJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-webtorrent')
623 const optimizeJobs = transcodingJobs.filter(j => j.data.type === 'optimize-to-webtorrent')
624 715
625 expect(hlsJobs).to.have.lengthOf(7) 716 const hlsJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-hls')
626 expect(webtorrentJobs).to.have.lengthOf(6) 717 const webtorrentJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-webtorrent')
627 expect(optimizeJobs).to.have.lengthOf(1) 718 const optimizeJobs = transcodingJobs.filter(j => j.data.type === 'optimize-to-webtorrent')
628 719
629 for (const j of optimizeJobs) { 720 expect(hlsJobs).to.have.lengthOf(7)
630 expect(j.priority).to.be.greaterThan(11) 721 expect(webtorrentJobs).to.have.lengthOf(6)
631 expect(j.priority).to.be.lessThan(50) 722 expect(optimizeJobs).to.have.lengthOf(1)
632 }
633 723
634 for (const j of hlsJobs.concat(webtorrentJobs)) { 724 for (const j of optimizeJobs) {
635 expect(j.priority).to.be.greaterThan(100) 725 expect(j.priority).to.be.greaterThan(11)
636 expect(j.priority).to.be.lessThan(150) 726 expect(j.priority).to.be.lessThan(50)
637 } 727 }
728
729 for (const j of hlsJobs.concat(webtorrentJobs)) {
730 expect(j.priority).to.be.greaterThan(100)
731 expect(j.priority).to.be.lessThan(150)
732 }
733 })
638 }) 734 })
639 735
640 after(async function () { 736 after(async function () {