diff options
author | Chocobozzz <me@florianbigard.com> | 2021-02-02 09:45:42 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-02-02 09:45:42 +0100 |
commit | 40930fda8615a9e0196e99602419cb74dda75e02 (patch) | |
tree | 5eafa64ec07938e44193175b97d5587b75e05f55 | |
parent | 89613cb444b4e1601d202153d0ec8635392ec872 (diff) | |
download | PeerTube-40930fda8615a9e0196e99602419cb74dda75e02.tar.gz PeerTube-40930fda8615a9e0196e99602419cb74dda75e02.tar.zst PeerTube-40930fda8615a9e0196e99602419cb74dda75e02.zip |
Transcode audio uploads to lower resolutions
Better consistency
-rw-r--r-- | client/src/app/shared/shared-video-miniature/abstract-video-list.ts | 3 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-transcoding.ts | 2 | ||||
-rw-r--r-- | server/tests/api/videos/video-transcoder.ts | 942 |
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 | ||
116 | async function handleWebTorrentOptimizeJob (job: Bull.Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight, user: MUserId) { | 118 | async 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 | ||
44 | const expect = chai.expect | 44 | const expect = chai.expect |
45 | 45 | ||
46 | function 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 | |||
46 | describe('Test video transcoding', function () { | 68 | describe('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 () { |