diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-31 14:34:36 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-11 15:02:33 +0200 |
commit | 3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch) | |
tree | e4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/tests/api/transcoding | |
parent | 04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff) | |
download | PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip |
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge
conflicts, but it's a major step forward:
* Server can be faster at startup because imports() are async and we can
easily lazy import big modules
* Angular doesn't seem to support ES import (with .js extension), so we
had to correctly organize peertube into a monorepo:
* Use yarn workspace feature
* Use typescript reference projects for dependencies
* Shared projects have been moved into "packages", each one is now a
node module (with a dedicated package.json/tsconfig.json)
* server/tools have been moved into apps/ and is now a dedicated app
bundled and published on NPM so users don't have to build peertube
cli tools manually
* server/tests have been moved into packages/ so we don't compile
them every time we want to run the server
* Use isolatedModule option:
* Had to move from const enum to const
(https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums)
* Had to explictely specify "type" imports when used in decorators
* Prefer tsx (that uses esbuild under the hood) instead of ts-node to
load typescript files (tests with mocha or scripts):
* To reduce test complexity as esbuild doesn't support decorator
metadata, we only test server files that do not import server
models
* We still build tests files into js files for a faster CI
* Remove unmaintained peertube CLI import script
* Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'server/tests/api/transcoding')
-rw-r--r-- | server/tests/api/transcoding/audio-only.ts | 104 | ||||
-rw-r--r-- | server/tests/api/transcoding/create-transcoding.ts | 266 | ||||
-rw-r--r-- | server/tests/api/transcoding/hls.ts | 175 | ||||
-rw-r--r-- | server/tests/api/transcoding/index.ts | 6 | ||||
-rw-r--r-- | server/tests/api/transcoding/transcoder.ts | 800 | ||||
-rw-r--r-- | server/tests/api/transcoding/update-while-transcoding.ts | 160 | ||||
-rw-r--r-- | server/tests/api/transcoding/video-studio.ts | 377 |
7 files changed, 0 insertions, 1888 deletions
diff --git a/server/tests/api/transcoding/audio-only.ts b/server/tests/api/transcoding/audio-only.ts deleted file mode 100644 index f4cc012ef..000000000 --- a/server/tests/api/transcoding/audio-only.ts +++ /dev/null | |||
@@ -1,104 +0,0 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { getAudioStream, getVideoStreamDimensionsInfo } from '@shared/ffmpeg' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@shared/server-commands' | ||
13 | |||
14 | describe('Test audio only video transcoding', function () { | ||
15 | let servers: PeerTubeServer[] = [] | ||
16 | let videoUUID: string | ||
17 | let webVideoAudioFileUrl: string | ||
18 | let fragmentedAudioFileUrl: string | ||
19 | |||
20 | before(async function () { | ||
21 | this.timeout(120000) | ||
22 | |||
23 | const configOverride = { | ||
24 | transcoding: { | ||
25 | enabled: true, | ||
26 | resolutions: { | ||
27 | '0p': true, | ||
28 | '144p': false, | ||
29 | '240p': true, | ||
30 | '360p': false, | ||
31 | '480p': false, | ||
32 | '720p': false, | ||
33 | '1080p': false, | ||
34 | '1440p': false, | ||
35 | '2160p': false | ||
36 | }, | ||
37 | hls: { | ||
38 | enabled: true | ||
39 | }, | ||
40 | web_videos: { | ||
41 | enabled: true | ||
42 | } | ||
43 | } | ||
44 | } | ||
45 | servers = await createMultipleServers(2, configOverride) | ||
46 | |||
47 | // Get the access tokens | ||
48 | await setAccessTokensToServers(servers) | ||
49 | |||
50 | // Server 1 and server 2 follow each other | ||
51 | await doubleFollow(servers[0], servers[1]) | ||
52 | }) | ||
53 | |||
54 | it('Should upload a video and transcode it', async function () { | ||
55 | this.timeout(120000) | ||
56 | |||
57 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'audio only' } }) | ||
58 | videoUUID = uuid | ||
59 | |||
60 | await waitJobs(servers) | ||
61 | |||
62 | for (const server of servers) { | ||
63 | const video = await server.videos.get({ id: videoUUID }) | ||
64 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
65 | |||
66 | for (const files of [ video.files, video.streamingPlaylists[0].files ]) { | ||
67 | expect(files).to.have.lengthOf(3) | ||
68 | expect(files[0].resolution.id).to.equal(720) | ||
69 | expect(files[1].resolution.id).to.equal(240) | ||
70 | expect(files[2].resolution.id).to.equal(0) | ||
71 | } | ||
72 | |||
73 | if (server.serverNumber === 1) { | ||
74 | webVideoAudioFileUrl = video.files[2].fileUrl | ||
75 | fragmentedAudioFileUrl = video.streamingPlaylists[0].files[2].fileUrl | ||
76 | } | ||
77 | } | ||
78 | }) | ||
79 | |||
80 | it('0p transcoded video should not have video', async function () { | ||
81 | const paths = [ | ||
82 | servers[0].servers.buildWebVideoFilePath(webVideoAudioFileUrl), | ||
83 | servers[0].servers.buildFragmentedFilePath(videoUUID, fragmentedAudioFileUrl) | ||
84 | ] | ||
85 | |||
86 | for (const path of paths) { | ||
87 | const { audioStream } = await getAudioStream(path) | ||
88 | expect(audioStream['codec_name']).to.be.equal('aac') | ||
89 | expect(audioStream['bit_rate']).to.be.at.most(384 * 8000) | ||
90 | |||
91 | const size = await getVideoStreamDimensionsInfo(path) | ||
92 | |||
93 | expect(size.height).to.equal(0) | ||
94 | expect(size.width).to.equal(0) | ||
95 | expect(size.isPortraitMode).to.be.false | ||
96 | expect(size.ratio).to.equal(0) | ||
97 | expect(size.resolution).to.equal(0) | ||
98 | } | ||
99 | }) | ||
100 | |||
101 | after(async function () { | ||
102 | await cleanupTests(servers) | ||
103 | }) | ||
104 | }) | ||
diff --git a/server/tests/api/transcoding/create-transcoding.ts b/server/tests/api/transcoding/create-transcoding.ts deleted file mode 100644 index 9a891043c..000000000 --- a/server/tests/api/transcoding/create-transcoding.ts +++ /dev/null | |||
@@ -1,266 +0,0 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { checkResolutionsInMasterPlaylist, expectStartWith } from '@server/tests/shared' | ||
5 | import { areMockObjectStorageTestsDisabled } from '@shared/core-utils' | ||
6 | import { HttpStatusCode, VideoDetails } from '@shared/models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | ConfigCommand, | ||
10 | createMultipleServers, | ||
11 | doubleFollow, | ||
12 | expectNoFailedTranscodingJob, | ||
13 | makeRawRequest, | ||
14 | ObjectStorageCommand, | ||
15 | PeerTubeServer, | ||
16 | setAccessTokensToServers, | ||
17 | waitJobs | ||
18 | } from '@shared/server-commands' | ||
19 | |||
20 | async function checkFilesInObjectStorage (objectStorage: ObjectStorageCommand, video: VideoDetails) { | ||
21 | for (const file of video.files) { | ||
22 | expectStartWith(file.fileUrl, objectStorage.getMockWebVideosBaseUrl()) | ||
23 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
24 | } | ||
25 | |||
26 | if (video.streamingPlaylists.length === 0) return | ||
27 | |||
28 | const hlsPlaylist = video.streamingPlaylists[0] | ||
29 | for (const file of hlsPlaylist.files) { | ||
30 | expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl()) | ||
31 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
32 | } | ||
33 | |||
34 | expectStartWith(hlsPlaylist.playlistUrl, objectStorage.getMockPlaylistBaseUrl()) | ||
35 | await makeRawRequest({ url: hlsPlaylist.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
36 | |||
37 | expectStartWith(hlsPlaylist.segmentsSha256Url, objectStorage.getMockPlaylistBaseUrl()) | ||
38 | await makeRawRequest({ url: hlsPlaylist.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) | ||
39 | } | ||
40 | |||
41 | function runTests (enableObjectStorage: boolean) { | ||
42 | let servers: PeerTubeServer[] = [] | ||
43 | let videoUUID: string | ||
44 | let publishedAt: string | ||
45 | |||
46 | let shouldBeDeleted: string[] | ||
47 | const objectStorage = new ObjectStorageCommand() | ||
48 | |||
49 | before(async function () { | ||
50 | this.timeout(120000) | ||
51 | |||
52 | const config = enableObjectStorage | ||
53 | ? objectStorage.getDefaultMockConfig() | ||
54 | : {} | ||
55 | |||
56 | // Run server 2 to have transcoding enabled | ||
57 | servers = await createMultipleServers(2, config) | ||
58 | await setAccessTokensToServers(servers) | ||
59 | |||
60 | await servers[0].config.disableTranscoding() | ||
61 | |||
62 | await doubleFollow(servers[0], servers[1]) | ||
63 | |||
64 | if (enableObjectStorage) await objectStorage.prepareDefaultMockBuckets() | ||
65 | |||
66 | const { shortUUID } = await servers[0].videos.quickUpload({ name: 'video' }) | ||
67 | videoUUID = shortUUID | ||
68 | |||
69 | await waitJobs(servers) | ||
70 | |||
71 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
72 | publishedAt = video.publishedAt as string | ||
73 | |||
74 | await servers[0].config.enableTranscoding() | ||
75 | }) | ||
76 | |||
77 | it('Should generate HLS', async function () { | ||
78 | this.timeout(60000) | ||
79 | |||
80 | await servers[0].videos.runTranscoding({ | ||
81 | videoId: videoUUID, | ||
82 | transcodingType: 'hls' | ||
83 | }) | ||
84 | |||
85 | await waitJobs(servers) | ||
86 | await expectNoFailedTranscodingJob(servers[0]) | ||
87 | |||
88 | for (const server of servers) { | ||
89 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
90 | |||
91 | expect(videoDetails.files).to.have.lengthOf(1) | ||
92 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
93 | expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5) | ||
94 | |||
95 | if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails) | ||
96 | } | ||
97 | }) | ||
98 | |||
99 | it('Should generate Web Video', async function () { | ||
100 | this.timeout(60000) | ||
101 | |||
102 | await servers[0].videos.runTranscoding({ | ||
103 | videoId: videoUUID, | ||
104 | transcodingType: 'web-video' | ||
105 | }) | ||
106 | |||
107 | await waitJobs(servers) | ||
108 | |||
109 | for (const server of servers) { | ||
110 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
111 | |||
112 | expect(videoDetails.files).to.have.lengthOf(5) | ||
113 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
114 | expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5) | ||
115 | |||
116 | if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails) | ||
117 | } | ||
118 | }) | ||
119 | |||
120 | it('Should generate Web Video from HLS only video', async function () { | ||
121 | this.timeout(60000) | ||
122 | |||
123 | await servers[0].videos.removeAllWebVideoFiles({ videoId: videoUUID }) | ||
124 | await waitJobs(servers) | ||
125 | |||
126 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'web-video' }) | ||
127 | await waitJobs(servers) | ||
128 | |||
129 | for (const server of servers) { | ||
130 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
131 | |||
132 | expect(videoDetails.files).to.have.lengthOf(5) | ||
133 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
134 | expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5) | ||
135 | |||
136 | if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails) | ||
137 | } | ||
138 | }) | ||
139 | |||
140 | it('Should only generate Web Video', async function () { | ||
141 | this.timeout(60000) | ||
142 | |||
143 | await servers[0].videos.removeHLSPlaylist({ videoId: videoUUID }) | ||
144 | await waitJobs(servers) | ||
145 | |||
146 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'web-video' }) | ||
147 | await waitJobs(servers) | ||
148 | |||
149 | for (const server of servers) { | ||
150 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
151 | |||
152 | expect(videoDetails.files).to.have.lengthOf(5) | ||
153 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(0) | ||
154 | |||
155 | if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails) | ||
156 | } | ||
157 | }) | ||
158 | |||
159 | it('Should correctly update HLS playlist on resolution change', async function () { | ||
160 | this.timeout(120000) | ||
161 | |||
162 | await servers[0].config.updateExistingSubConfig({ | ||
163 | newConfig: { | ||
164 | transcoding: { | ||
165 | enabled: true, | ||
166 | resolutions: ConfigCommand.getCustomConfigResolutions(false), | ||
167 | |||
168 | webVideos: { | ||
169 | enabled: true | ||
170 | }, | ||
171 | hls: { | ||
172 | enabled: true | ||
173 | } | ||
174 | } | ||
175 | } | ||
176 | }) | ||
177 | |||
178 | const { uuid } = await servers[0].videos.quickUpload({ name: 'quick' }) | ||
179 | |||
180 | await waitJobs(servers) | ||
181 | |||
182 | for (const server of servers) { | ||
183 | const videoDetails = await server.videos.get({ id: uuid }) | ||
184 | |||
185 | expect(videoDetails.files).to.have.lengthOf(1) | ||
186 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
187 | expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(1) | ||
188 | |||
189 | if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails) | ||
190 | |||
191 | shouldBeDeleted = [ | ||
192 | videoDetails.streamingPlaylists[0].files[0].fileUrl, | ||
193 | videoDetails.streamingPlaylists[0].playlistUrl, | ||
194 | videoDetails.streamingPlaylists[0].segmentsSha256Url | ||
195 | ] | ||
196 | } | ||
197 | |||
198 | await servers[0].config.updateExistingSubConfig({ | ||
199 | newConfig: { | ||
200 | transcoding: { | ||
201 | enabled: true, | ||
202 | resolutions: ConfigCommand.getCustomConfigResolutions(true), | ||
203 | |||
204 | webVideos: { | ||
205 | enabled: true | ||
206 | }, | ||
207 | hls: { | ||
208 | enabled: true | ||
209 | } | ||
210 | } | ||
211 | } | ||
212 | }) | ||
213 | |||
214 | await servers[0].videos.runTranscoding({ videoId: uuid, transcodingType: 'hls' }) | ||
215 | await waitJobs(servers) | ||
216 | |||
217 | for (const server of servers) { | ||
218 | const videoDetails = await server.videos.get({ id: uuid }) | ||
219 | |||
220 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
221 | expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5) | ||
222 | |||
223 | if (enableObjectStorage) { | ||
224 | await checkFilesInObjectStorage(objectStorage, videoDetails) | ||
225 | |||
226 | const hlsPlaylist = videoDetails.streamingPlaylists[0] | ||
227 | const resolutions = hlsPlaylist.files.map(f => f.resolution.id) | ||
228 | await checkResolutionsInMasterPlaylist({ server: servers[0], playlistUrl: hlsPlaylist.playlistUrl, resolutions }) | ||
229 | |||
230 | const shaBody = await servers[0].streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry: true }) | ||
231 | expect(Object.keys(shaBody)).to.have.lengthOf(5) | ||
232 | } | ||
233 | } | ||
234 | }) | ||
235 | |||
236 | it('Should have correctly deleted previous files', async function () { | ||
237 | for (const fileUrl of shouldBeDeleted) { | ||
238 | await makeRawRequest({ url: fileUrl, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
239 | } | ||
240 | }) | ||
241 | |||
242 | it('Should not have updated published at attributes', async function () { | ||
243 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
244 | |||
245 | expect(video.publishedAt).to.equal(publishedAt) | ||
246 | }) | ||
247 | |||
248 | after(async function () { | ||
249 | if (objectStorage) await objectStorage.cleanupMock() | ||
250 | |||
251 | await cleanupTests(servers) | ||
252 | }) | ||
253 | } | ||
254 | |||
255 | describe('Test create transcoding jobs from API', function () { | ||
256 | |||
257 | describe('On filesystem', function () { | ||
258 | runTests(false) | ||
259 | }) | ||
260 | |||
261 | describe('On object storage', function () { | ||
262 | if (areMockObjectStorageTestsDisabled()) return | ||
263 | |||
264 | runTests(true) | ||
265 | }) | ||
266 | }) | ||
diff --git a/server/tests/api/transcoding/hls.ts b/server/tests/api/transcoding/hls.ts deleted file mode 100644 index d67043c2a..000000000 --- a/server/tests/api/transcoding/hls.ts +++ /dev/null | |||
@@ -1,175 +0,0 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { join } from 'path' | ||
4 | import { checkDirectoryIsEmpty, checkTmpIsEmpty, completeCheckHlsPlaylist } from '@server/tests/shared' | ||
5 | import { areMockObjectStorageTestsDisabled } from '@shared/core-utils' | ||
6 | import { HttpStatusCode } from '@shared/models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createMultipleServers, | ||
10 | doubleFollow, | ||
11 | ObjectStorageCommand, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers, | ||
14 | waitJobs | ||
15 | } from '@shared/server-commands' | ||
16 | import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants' | ||
17 | |||
18 | describe('Test HLS videos', function () { | ||
19 | let servers: PeerTubeServer[] = [] | ||
20 | |||
21 | function runTestSuite (hlsOnly: boolean, objectStorageBaseUrl?: string) { | ||
22 | const videoUUIDs: string[] = [] | ||
23 | |||
24 | it('Should upload a video and transcode it to HLS', async function () { | ||
25 | this.timeout(120000) | ||
26 | |||
27 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 1', fixture: 'video_short.webm' } }) | ||
28 | videoUUIDs.push(uuid) | ||
29 | |||
30 | await waitJobs(servers) | ||
31 | |||
32 | await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl }) | ||
33 | }) | ||
34 | |||
35 | it('Should upload an audio file and transcode it to HLS', async function () { | ||
36 | this.timeout(120000) | ||
37 | |||
38 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video audio', fixture: 'sample.ogg' } }) | ||
39 | videoUUIDs.push(uuid) | ||
40 | |||
41 | await waitJobs(servers) | ||
42 | |||
43 | await completeCheckHlsPlaylist({ | ||
44 | servers, | ||
45 | videoUUID: uuid, | ||
46 | hlsOnly, | ||
47 | resolutions: [ DEFAULT_AUDIO_RESOLUTION, 360, 240 ], | ||
48 | objectStorageBaseUrl | ||
49 | }) | ||
50 | }) | ||
51 | |||
52 | it('Should update the video', async function () { | ||
53 | this.timeout(30000) | ||
54 | |||
55 | await servers[0].videos.update({ id: videoUUIDs[0], attributes: { name: 'video 1 updated' } }) | ||
56 | |||
57 | await waitJobs(servers) | ||
58 | |||
59 | await completeCheckHlsPlaylist({ servers, videoUUID: videoUUIDs[0], hlsOnly, objectStorageBaseUrl }) | ||
60 | }) | ||
61 | |||
62 | it('Should delete videos', async function () { | ||
63 | for (const uuid of videoUUIDs) { | ||
64 | await servers[0].videos.remove({ id: uuid }) | ||
65 | } | ||
66 | |||
67 | await waitJobs(servers) | ||
68 | |||
69 | for (const server of servers) { | ||
70 | for (const uuid of videoUUIDs) { | ||
71 | await server.videos.get({ id: uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
72 | } | ||
73 | } | ||
74 | }) | ||
75 | |||
76 | it('Should have the playlists/segment deleted from the disk', async function () { | ||
77 | for (const server of servers) { | ||
78 | await checkDirectoryIsEmpty(server, 'web-videos', [ 'private' ]) | ||
79 | await checkDirectoryIsEmpty(server, join('web-videos', 'private')) | ||
80 | |||
81 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls'), [ 'private' ]) | ||
82 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls', 'private')) | ||
83 | } | ||
84 | }) | ||
85 | |||
86 | it('Should have an empty tmp directory', async function () { | ||
87 | for (const server of servers) { | ||
88 | await checkTmpIsEmpty(server) | ||
89 | } | ||
90 | }) | ||
91 | } | ||
92 | |||
93 | before(async function () { | ||
94 | this.timeout(120000) | ||
95 | |||
96 | const configOverride = { | ||
97 | transcoding: { | ||
98 | enabled: true, | ||
99 | allow_audio_files: true, | ||
100 | hls: { | ||
101 | enabled: true | ||
102 | } | ||
103 | } | ||
104 | } | ||
105 | servers = await createMultipleServers(2, configOverride) | ||
106 | |||
107 | // Get the access tokens | ||
108 | await setAccessTokensToServers(servers) | ||
109 | |||
110 | // Server 1 and server 2 follow each other | ||
111 | await doubleFollow(servers[0], servers[1]) | ||
112 | }) | ||
113 | |||
114 | describe('With Web Video & HLS enabled', function () { | ||
115 | runTestSuite(false) | ||
116 | }) | ||
117 | |||
118 | describe('With only HLS enabled', function () { | ||
119 | |||
120 | before(async function () { | ||
121 | await servers[0].config.updateCustomSubConfig({ | ||
122 | newConfig: { | ||
123 | transcoding: { | ||
124 | enabled: true, | ||
125 | allowAudioFiles: true, | ||
126 | resolutions: { | ||
127 | '144p': false, | ||
128 | '240p': true, | ||
129 | '360p': true, | ||
130 | '480p': true, | ||
131 | '720p': true, | ||
132 | '1080p': true, | ||
133 | '1440p': true, | ||
134 | '2160p': true | ||
135 | }, | ||
136 | hls: { | ||
137 | enabled: true | ||
138 | }, | ||
139 | webVideos: { | ||
140 | enabled: false | ||
141 | } | ||
142 | } | ||
143 | } | ||
144 | }) | ||
145 | }) | ||
146 | |||
147 | runTestSuite(true) | ||
148 | }) | ||
149 | |||
150 | describe('With object storage enabled', function () { | ||
151 | if (areMockObjectStorageTestsDisabled()) return | ||
152 | |||
153 | const objectStorage = new ObjectStorageCommand() | ||
154 | |||
155 | before(async function () { | ||
156 | this.timeout(120000) | ||
157 | |||
158 | const configOverride = objectStorage.getDefaultMockConfig() | ||
159 | await objectStorage.prepareDefaultMockBuckets() | ||
160 | |||
161 | await servers[0].kill() | ||
162 | await servers[0].run(configOverride) | ||
163 | }) | ||
164 | |||
165 | runTestSuite(true, objectStorage.getMockPlaylistBaseUrl()) | ||
166 | |||
167 | after(async function () { | ||
168 | await objectStorage.cleanupMock() | ||
169 | }) | ||
170 | }) | ||
171 | |||
172 | after(async function () { | ||
173 | await cleanupTests(servers) | ||
174 | }) | ||
175 | }) | ||
diff --git a/server/tests/api/transcoding/index.ts b/server/tests/api/transcoding/index.ts deleted file mode 100644 index 9866418d6..000000000 --- a/server/tests/api/transcoding/index.ts +++ /dev/null | |||
@@ -1,6 +0,0 @@ | |||
1 | export * from './audio-only' | ||
2 | export * from './create-transcoding' | ||
3 | export * from './hls' | ||
4 | export * from './transcoder' | ||
5 | export * from './update-while-transcoding' | ||
6 | export * from './video-studio' | ||
diff --git a/server/tests/api/transcoding/transcoder.ts b/server/tests/api/transcoding/transcoder.ts deleted file mode 100644 index 5386d236f..000000000 --- a/server/tests/api/transcoding/transcoder.ts +++ /dev/null | |||
@@ -1,800 +0,0 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { canDoQuickTranscode } from '@server/lib/transcoding/transcoding-quick-transcode' | ||
5 | import { checkWebTorrentWorks, generateHighBitrateVideo, generateVideoWithFramerate } from '@server/tests/shared' | ||
6 | import { buildAbsoluteFixturePath, getAllFiles, getMaxTheoreticalBitrate, getMinTheoreticalBitrate, omit } from '@shared/core-utils' | ||
7 | import { | ||
8 | ffprobePromise, | ||
9 | getAudioStream, | ||
10 | getVideoStreamBitrate, | ||
11 | getVideoStreamDimensionsInfo, | ||
12 | getVideoStreamFPS, | ||
13 | hasAudioStream | ||
14 | } from '@shared/ffmpeg' | ||
15 | import { HttpStatusCode, VideoFileMetadata, VideoState } from '@shared/models' | ||
16 | import { | ||
17 | cleanupTests, | ||
18 | createMultipleServers, | ||
19 | doubleFollow, | ||
20 | makeGetRequest, | ||
21 | PeerTubeServer, | ||
22 | setAccessTokensToServers, | ||
23 | waitJobs | ||
24 | } from '@shared/server-commands' | ||
25 | |||
26 | function updateConfigForTranscoding (server: PeerTubeServer) { | ||
27 | return server.config.updateCustomSubConfig({ | ||
28 | newConfig: { | ||
29 | transcoding: { | ||
30 | enabled: true, | ||
31 | allowAdditionalExtensions: true, | ||
32 | allowAudioFiles: true, | ||
33 | hls: { enabled: true }, | ||
34 | webVideos: { enabled: true }, | ||
35 | resolutions: { | ||
36 | '0p': false, | ||
37 | '144p': true, | ||
38 | '240p': true, | ||
39 | '360p': true, | ||
40 | '480p': true, | ||
41 | '720p': true, | ||
42 | '1080p': true, | ||
43 | '1440p': true, | ||
44 | '2160p': true | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | }) | ||
49 | } | ||
50 | |||
51 | describe('Test video transcoding', function () { | ||
52 | let servers: PeerTubeServer[] = [] | ||
53 | let video4k: string | ||
54 | |||
55 | before(async function () { | ||
56 | this.timeout(30_000) | ||
57 | |||
58 | // Run servers | ||
59 | servers = await createMultipleServers(2) | ||
60 | |||
61 | await setAccessTokensToServers(servers) | ||
62 | |||
63 | await doubleFollow(servers[0], servers[1]) | ||
64 | |||
65 | await updateConfigForTranscoding(servers[1]) | ||
66 | }) | ||
67 | |||
68 | describe('Basic transcoding (or not)', function () { | ||
69 | |||
70 | it('Should not transcode video on server 1', async function () { | ||
71 | this.timeout(60_000) | ||
72 | |||
73 | const attributes = { | ||
74 | name: 'my super name for server 1', | ||
75 | description: 'my super description for server 1', | ||
76 | fixture: 'video_short.webm' | ||
77 | } | ||
78 | await servers[0].videos.upload({ attributes }) | ||
79 | |||
80 | await waitJobs(servers) | ||
81 | |||
82 | for (const server of servers) { | ||
83 | const { data } = await server.videos.list() | ||
84 | const video = data[0] | ||
85 | |||
86 | const videoDetails = await server.videos.get({ id: video.id }) | ||
87 | expect(videoDetails.files).to.have.lengthOf(1) | ||
88 | |||
89 | const magnetUri = videoDetails.files[0].magnetUri | ||
90 | expect(magnetUri).to.match(/\.webm/) | ||
91 | |||
92 | await checkWebTorrentWorks(magnetUri, /\.webm$/) | ||
93 | } | ||
94 | }) | ||
95 | |||
96 | it('Should transcode video on server 2', async function () { | ||
97 | this.timeout(120_000) | ||
98 | |||
99 | const attributes = { | ||
100 | name: 'my super name for server 2', | ||
101 | description: 'my super description for server 2', | ||
102 | fixture: 'video_short.webm' | ||
103 | } | ||
104 | await servers[1].videos.upload({ attributes }) | ||
105 | |||
106 | await waitJobs(servers) | ||
107 | |||
108 | for (const server of servers) { | ||
109 | const { data } = await server.videos.list() | ||
110 | |||
111 | const video = data.find(v => v.name === attributes.name) | ||
112 | const videoDetails = await server.videos.get({ id: video.id }) | ||
113 | |||
114 | expect(videoDetails.files).to.have.lengthOf(5) | ||
115 | |||
116 | const magnetUri = videoDetails.files[0].magnetUri | ||
117 | expect(magnetUri).to.match(/\.mp4/) | ||
118 | |||
119 | await checkWebTorrentWorks(magnetUri, /\.mp4$/) | ||
120 | } | ||
121 | }) | ||
122 | |||
123 | it('Should wait for transcoding before publishing the video', async function () { | ||
124 | this.timeout(160_000) | ||
125 | |||
126 | { | ||
127 | // Upload the video, but wait transcoding | ||
128 | const attributes = { | ||
129 | name: 'waiting video', | ||
130 | fixture: 'video_short1.webm', | ||
131 | waitTranscoding: true | ||
132 | } | ||
133 | const { uuid } = await servers[1].videos.upload({ attributes }) | ||
134 | const videoId = uuid | ||
135 | |||
136 | // Should be in transcode state | ||
137 | const body = await servers[1].videos.get({ id: videoId }) | ||
138 | expect(body.name).to.equal('waiting video') | ||
139 | expect(body.state.id).to.equal(VideoState.TO_TRANSCODE) | ||
140 | expect(body.state.label).to.equal('To transcode') | ||
141 | expect(body.waitTranscoding).to.be.true | ||
142 | |||
143 | { | ||
144 | // Should have my video | ||
145 | const { data } = await servers[1].videos.listMyVideos() | ||
146 | const videoToFindInMine = data.find(v => v.name === attributes.name) | ||
147 | expect(videoToFindInMine).not.to.be.undefined | ||
148 | expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE) | ||
149 | expect(videoToFindInMine.state.label).to.equal('To transcode') | ||
150 | expect(videoToFindInMine.waitTranscoding).to.be.true | ||
151 | } | ||
152 | |||
153 | { | ||
154 | // Should not list this video | ||
155 | const { data } = await servers[1].videos.list() | ||
156 | const videoToFindInList = data.find(v => v.name === attributes.name) | ||
157 | expect(videoToFindInList).to.be.undefined | ||
158 | } | ||
159 | |||
160 | // Server 1 should not have the video yet | ||
161 | await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
162 | } | ||
163 | |||
164 | await waitJobs(servers) | ||
165 | |||
166 | for (const server of servers) { | ||
167 | const { data } = await server.videos.list() | ||
168 | const videoToFind = data.find(v => v.name === 'waiting video') | ||
169 | expect(videoToFind).not.to.be.undefined | ||
170 | |||
171 | const videoDetails = await server.videos.get({ id: videoToFind.id }) | ||
172 | |||
173 | expect(videoDetails.state.id).to.equal(VideoState.PUBLISHED) | ||
174 | expect(videoDetails.state.label).to.equal('Published') | ||
175 | expect(videoDetails.waitTranscoding).to.be.true | ||
176 | } | ||
177 | }) | ||
178 | |||
179 | it('Should accept and transcode additional extensions', async function () { | ||
180 | this.timeout(300_000) | ||
181 | |||
182 | for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) { | ||
183 | const attributes = { | ||
184 | name: fixture, | ||
185 | fixture | ||
186 | } | ||
187 | |||
188 | await servers[1].videos.upload({ attributes }) | ||
189 | |||
190 | await waitJobs(servers) | ||
191 | |||
192 | for (const server of servers) { | ||
193 | const { data } = await server.videos.list() | ||
194 | |||
195 | const video = data.find(v => v.name === attributes.name) | ||
196 | const videoDetails = await server.videos.get({ id: video.id }) | ||
197 | expect(videoDetails.files).to.have.lengthOf(5) | ||
198 | |||
199 | const magnetUri = videoDetails.files[0].magnetUri | ||
200 | expect(magnetUri).to.contain('.mp4') | ||
201 | } | ||
202 | } | ||
203 | }) | ||
204 | |||
205 | it('Should transcode a 4k video', async function () { | ||
206 | this.timeout(200_000) | ||
207 | |||
208 | const attributes = { | ||
209 | name: '4k video', | ||
210 | fixture: 'video_short_4k.mp4' | ||
211 | } | ||
212 | |||
213 | const { uuid } = await servers[1].videos.upload({ attributes }) | ||
214 | video4k = uuid | ||
215 | |||
216 | await waitJobs(servers) | ||
217 | |||
218 | const resolutions = [ 144, 240, 360, 480, 720, 1080, 1440, 2160 ] | ||
219 | |||
220 | for (const server of servers) { | ||
221 | const videoDetails = await server.videos.get({ id: video4k }) | ||
222 | expect(videoDetails.files).to.have.lengthOf(resolutions.length) | ||
223 | |||
224 | for (const r of resolutions) { | ||
225 | expect(videoDetails.files.find(f => f.resolution.id === r)).to.not.be.undefined | ||
226 | expect(videoDetails.streamingPlaylists[0].files.find(f => f.resolution.id === r)).to.not.be.undefined | ||
227 | } | ||
228 | } | ||
229 | }) | ||
230 | }) | ||
231 | |||
232 | describe('Audio transcoding', function () { | ||
233 | |||
234 | it('Should transcode high bit rate mp3 to proper bit rate', async function () { | ||
235 | this.timeout(60_000) | ||
236 | |||
237 | const attributes = { | ||
238 | name: 'mp3_256k', | ||
239 | fixture: 'video_short_mp3_256k.mp4' | ||
240 | } | ||
241 | await servers[1].videos.upload({ attributes }) | ||
242 | |||
243 | await waitJobs(servers) | ||
244 | |||
245 | for (const server of servers) { | ||
246 | const { data } = await server.videos.list() | ||
247 | |||
248 | const video = data.find(v => v.name === attributes.name) | ||
249 | const videoDetails = await server.videos.get({ id: video.id }) | ||
250 | |||
251 | expect(videoDetails.files).to.have.lengthOf(5) | ||
252 | |||
253 | const file = videoDetails.files.find(f => f.resolution.id === 240) | ||
254 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
255 | const probe = await getAudioStream(path) | ||
256 | |||
257 | if (probe.audioStream) { | ||
258 | expect(probe.audioStream['codec_name']).to.be.equal('aac') | ||
259 | expect(probe.audioStream['bit_rate']).to.be.at.most(384 * 8000) | ||
260 | } else { | ||
261 | this.fail('Could not retrieve the audio stream on ' + probe.absolutePath) | ||
262 | } | ||
263 | } | ||
264 | }) | ||
265 | |||
266 | it('Should transcode video with no audio and have no audio itself', async function () { | ||
267 | this.timeout(60_000) | ||
268 | |||
269 | const attributes = { | ||
270 | name: 'no_audio', | ||
271 | fixture: 'video_short_no_audio.mp4' | ||
272 | } | ||
273 | await servers[1].videos.upload({ attributes }) | ||
274 | |||
275 | await waitJobs(servers) | ||
276 | |||
277 | for (const server of servers) { | ||
278 | const { data } = await server.videos.list() | ||
279 | |||
280 | const video = data.find(v => v.name === attributes.name) | ||
281 | const videoDetails = await server.videos.get({ id: video.id }) | ||
282 | |||
283 | const file = videoDetails.files.find(f => f.resolution.id === 240) | ||
284 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
285 | |||
286 | expect(await hasAudioStream(path)).to.be.false | ||
287 | } | ||
288 | }) | ||
289 | |||
290 | it('Should leave the audio untouched, but properly transcode the video', async function () { | ||
291 | this.timeout(60_000) | ||
292 | |||
293 | const attributes = { | ||
294 | name: 'untouched_audio', | ||
295 | fixture: 'video_short.mp4' | ||
296 | } | ||
297 | await servers[1].videos.upload({ attributes }) | ||
298 | |||
299 | await waitJobs(servers) | ||
300 | |||
301 | for (const server of servers) { | ||
302 | const { data } = await server.videos.list() | ||
303 | |||
304 | const video = data.find(v => v.name === attributes.name) | ||
305 | const videoDetails = await server.videos.get({ id: video.id }) | ||
306 | |||
307 | expect(videoDetails.files).to.have.lengthOf(5) | ||
308 | |||
309 | const fixturePath = buildAbsoluteFixturePath(attributes.fixture) | ||
310 | const fixtureVideoProbe = await getAudioStream(fixturePath) | ||
311 | |||
312 | const file = videoDetails.files.find(f => f.resolution.id === 240) | ||
313 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
314 | |||
315 | const videoProbe = await getAudioStream(path) | ||
316 | |||
317 | if (videoProbe.audioStream && fixtureVideoProbe.audioStream) { | ||
318 | const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ] | ||
319 | expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit)) | ||
320 | } else { | ||
321 | this.fail('Could not retrieve the audio stream on ' + videoProbe.absolutePath) | ||
322 | } | ||
323 | } | ||
324 | }) | ||
325 | }) | ||
326 | |||
327 | describe('Audio upload', function () { | ||
328 | |||
329 | function runSuite (mode: 'legacy' | 'resumable') { | ||
330 | |||
331 | before(async function () { | ||
332 | await servers[1].config.updateCustomSubConfig({ | ||
333 | newConfig: { | ||
334 | transcoding: { | ||
335 | hls: { enabled: true }, | ||
336 | webVideos: { enabled: true }, | ||
337 | resolutions: { | ||
338 | '0p': false, | ||
339 | '144p': false, | ||
340 | '240p': false, | ||
341 | '360p': false, | ||
342 | '480p': false, | ||
343 | '720p': false, | ||
344 | '1080p': false, | ||
345 | '1440p': false, | ||
346 | '2160p': false | ||
347 | } | ||
348 | } | ||
349 | } | ||
350 | }) | ||
351 | }) | ||
352 | |||
353 | it('Should merge an audio file with the preview file', async function () { | ||
354 | this.timeout(60_000) | ||
355 | |||
356 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } | ||
357 | await servers[1].videos.upload({ attributes, mode }) | ||
358 | |||
359 | await waitJobs(servers) | ||
360 | |||
361 | for (const server of servers) { | ||
362 | const { data } = await server.videos.list() | ||
363 | |||
364 | const video = data.find(v => v.name === 'audio_with_preview') | ||
365 | const videoDetails = await server.videos.get({ id: video.id }) | ||
366 | |||
367 | expect(videoDetails.files).to.have.lengthOf(1) | ||
368 | |||
369 | await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
370 | await makeGetRequest({ url: server.url, path: videoDetails.previewPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
371 | |||
372 | const magnetUri = videoDetails.files[0].magnetUri | ||
373 | expect(magnetUri).to.contain('.mp4') | ||
374 | } | ||
375 | }) | ||
376 | |||
377 | it('Should upload an audio file and choose a default background image', async function () { | ||
378 | this.timeout(60_000) | ||
379 | |||
380 | const attributes = { name: 'audio_without_preview', fixture: 'sample.ogg' } | ||
381 | await servers[1].videos.upload({ attributes, mode }) | ||
382 | |||
383 | await waitJobs(servers) | ||
384 | |||
385 | for (const server of servers) { | ||
386 | const { data } = await server.videos.list() | ||
387 | |||
388 | const video = data.find(v => v.name === 'audio_without_preview') | ||
389 | const videoDetails = await server.videos.get({ id: video.id }) | ||
390 | |||
391 | expect(videoDetails.files).to.have.lengthOf(1) | ||
392 | |||
393 | await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
394 | await makeGetRequest({ url: server.url, path: videoDetails.previewPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
395 | |||
396 | const magnetUri = videoDetails.files[0].magnetUri | ||
397 | expect(magnetUri).to.contain('.mp4') | ||
398 | } | ||
399 | }) | ||
400 | |||
401 | it('Should upload an audio file and create an audio version only', async function () { | ||
402 | this.timeout(60_000) | ||
403 | |||
404 | await servers[1].config.updateCustomSubConfig({ | ||
405 | newConfig: { | ||
406 | transcoding: { | ||
407 | hls: { enabled: true }, | ||
408 | webVideos: { enabled: true }, | ||
409 | resolutions: { | ||
410 | '0p': true, | ||
411 | '144p': false, | ||
412 | '240p': false, | ||
413 | '360p': false | ||
414 | } | ||
415 | } | ||
416 | } | ||
417 | }) | ||
418 | |||
419 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } | ||
420 | const { id } = await servers[1].videos.upload({ attributes, mode }) | ||
421 | |||
422 | await waitJobs(servers) | ||
423 | |||
424 | for (const server of servers) { | ||
425 | const videoDetails = await server.videos.get({ id }) | ||
426 | |||
427 | for (const files of [ videoDetails.files, videoDetails.streamingPlaylists[0].files ]) { | ||
428 | expect(files).to.have.lengthOf(2) | ||
429 | expect(files.find(f => f.resolution.id === 0)).to.not.be.undefined | ||
430 | } | ||
431 | } | ||
432 | |||
433 | await updateConfigForTranscoding(servers[1]) | ||
434 | }) | ||
435 | } | ||
436 | |||
437 | describe('Legacy upload', function () { | ||
438 | runSuite('legacy') | ||
439 | }) | ||
440 | |||
441 | describe('Resumable upload', function () { | ||
442 | runSuite('resumable') | ||
443 | }) | ||
444 | }) | ||
445 | |||
446 | describe('Framerate', function () { | ||
447 | |||
448 | it('Should transcode a 60 FPS video', async function () { | ||
449 | this.timeout(60_000) | ||
450 | |||
451 | const attributes = { | ||
452 | name: 'my super 30fps name for server 2', | ||
453 | description: 'my super 30fps description for server 2', | ||
454 | fixture: '60fps_720p_small.mp4' | ||
455 | } | ||
456 | await servers[1].videos.upload({ attributes }) | ||
457 | |||
458 | await waitJobs(servers) | ||
459 | |||
460 | for (const server of servers) { | ||
461 | const { data } = await server.videos.list() | ||
462 | |||
463 | const video = data.find(v => v.name === attributes.name) | ||
464 | const videoDetails = await server.videos.get({ id: video.id }) | ||
465 | |||
466 | expect(videoDetails.files).to.have.lengthOf(5) | ||
467 | expect(videoDetails.files[0].fps).to.be.above(58).and.below(62) | ||
468 | expect(videoDetails.files[1].fps).to.be.below(31) | ||
469 | expect(videoDetails.files[2].fps).to.be.below(31) | ||
470 | expect(videoDetails.files[3].fps).to.be.below(31) | ||
471 | expect(videoDetails.files[4].fps).to.be.below(31) | ||
472 | |||
473 | for (const resolution of [ 144, 240, 360, 480 ]) { | ||
474 | const file = videoDetails.files.find(f => f.resolution.id === resolution) | ||
475 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
476 | const fps = await getVideoStreamFPS(path) | ||
477 | |||
478 | expect(fps).to.be.below(31) | ||
479 | } | ||
480 | |||
481 | const file = videoDetails.files.find(f => f.resolution.id === 720) | ||
482 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
483 | const fps = await getVideoStreamFPS(path) | ||
484 | |||
485 | expect(fps).to.be.above(58).and.below(62) | ||
486 | } | ||
487 | }) | ||
488 | |||
489 | it('Should downscale to the closest divisor standard framerate', async function () { | ||
490 | this.timeout(200_000) | ||
491 | |||
492 | let tempFixturePath: string | ||
493 | |||
494 | { | ||
495 | tempFixturePath = await generateVideoWithFramerate(59) | ||
496 | |||
497 | const fps = await getVideoStreamFPS(tempFixturePath) | ||
498 | expect(fps).to.be.equal(59) | ||
499 | } | ||
500 | |||
501 | const attributes = { | ||
502 | name: '59fps video', | ||
503 | description: '59fps video', | ||
504 | fixture: tempFixturePath | ||
505 | } | ||
506 | |||
507 | await servers[1].videos.upload({ attributes }) | ||
508 | |||
509 | await waitJobs(servers) | ||
510 | |||
511 | for (const server of servers) { | ||
512 | const { data } = await server.videos.list() | ||
513 | |||
514 | const { id } = data.find(v => v.name === attributes.name) | ||
515 | const video = await server.videos.get({ id }) | ||
516 | |||
517 | { | ||
518 | const file = video.files.find(f => f.resolution.id === 240) | ||
519 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
520 | const fps = await getVideoStreamFPS(path) | ||
521 | expect(fps).to.be.equal(25) | ||
522 | } | ||
523 | |||
524 | { | ||
525 | const file = video.files.find(f => f.resolution.id === 720) | ||
526 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
527 | const fps = await getVideoStreamFPS(path) | ||
528 | expect(fps).to.be.equal(59) | ||
529 | } | ||
530 | } | ||
531 | }) | ||
532 | }) | ||
533 | |||
534 | describe('Bitrate control', function () { | ||
535 | |||
536 | it('Should respect maximum bitrate values', async function () { | ||
537 | this.timeout(160_000) | ||
538 | |||
539 | const tempFixturePath = await generateHighBitrateVideo() | ||
540 | |||
541 | const attributes = { | ||
542 | name: 'high bitrate video', | ||
543 | description: 'high bitrate video', | ||
544 | fixture: tempFixturePath | ||
545 | } | ||
546 | |||
547 | await servers[1].videos.upload({ attributes }) | ||
548 | |||
549 | await waitJobs(servers) | ||
550 | |||
551 | for (const server of servers) { | ||
552 | const { data } = await server.videos.list() | ||
553 | |||
554 | const { id } = data.find(v => v.name === attributes.name) | ||
555 | const video = await server.videos.get({ id }) | ||
556 | |||
557 | for (const resolution of [ 240, 360, 480, 720, 1080 ]) { | ||
558 | const file = video.files.find(f => f.resolution.id === resolution) | ||
559 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
560 | |||
561 | const bitrate = await getVideoStreamBitrate(path) | ||
562 | const fps = await getVideoStreamFPS(path) | ||
563 | const dataResolution = await getVideoStreamDimensionsInfo(path) | ||
564 | |||
565 | expect(resolution).to.equal(resolution) | ||
566 | |||
567 | const maxBitrate = getMaxTheoreticalBitrate({ ...dataResolution, fps }) | ||
568 | expect(bitrate).to.be.below(maxBitrate) | ||
569 | } | ||
570 | } | ||
571 | }) | ||
572 | |||
573 | it('Should not transcode to an higher bitrate than the original file but above our low limit', async function () { | ||
574 | this.timeout(160_000) | ||
575 | |||
576 | const newConfig = { | ||
577 | transcoding: { | ||
578 | enabled: true, | ||
579 | resolutions: { | ||
580 | '144p': true, | ||
581 | '240p': true, | ||
582 | '360p': true, | ||
583 | '480p': true, | ||
584 | '720p': true, | ||
585 | '1080p': true, | ||
586 | '1440p': true, | ||
587 | '2160p': true | ||
588 | }, | ||
589 | webVideos: { enabled: true }, | ||
590 | hls: { enabled: true } | ||
591 | } | ||
592 | } | ||
593 | await servers[1].config.updateCustomSubConfig({ newConfig }) | ||
594 | |||
595 | const attributes = { | ||
596 | name: 'low bitrate', | ||
597 | fixture: 'low-bitrate.mp4' | ||
598 | } | ||
599 | |||
600 | const { id } = await servers[1].videos.upload({ attributes }) | ||
601 | |||
602 | await waitJobs(servers) | ||
603 | |||
604 | const video = await servers[1].videos.get({ id }) | ||
605 | |||
606 | const resolutions = [ 240, 360, 480, 720, 1080 ] | ||
607 | for (const r of resolutions) { | ||
608 | const file = video.files.find(f => f.resolution.id === r) | ||
609 | |||
610 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
611 | const bitrate = await getVideoStreamBitrate(path) | ||
612 | |||
613 | const inputBitrate = 60_000 | ||
614 | const limit = getMinTheoreticalBitrate({ fps: 10, ratio: 1, resolution: r }) | ||
615 | let belowValue = Math.max(inputBitrate, limit) | ||
616 | belowValue += belowValue * 0.20 // Apply 20% margin because bitrate control is not very precise | ||
617 | |||
618 | expect(bitrate, `${path} not below ${limit}`).to.be.below(belowValue) | ||
619 | } | ||
620 | }) | ||
621 | }) | ||
622 | |||
623 | describe('FFprobe', function () { | ||
624 | |||
625 | it('Should provide valid ffprobe data', async function () { | ||
626 | this.timeout(160_000) | ||
627 | |||
628 | const videoUUID = (await servers[1].videos.quickUpload({ name: 'ffprobe data' })).uuid | ||
629 | await waitJobs(servers) | ||
630 | |||
631 | { | ||
632 | const video = await servers[1].videos.get({ id: videoUUID }) | ||
633 | const file = video.files.find(f => f.resolution.id === 240) | ||
634 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
635 | |||
636 | const probe = await ffprobePromise(path) | ||
637 | const metadata = new VideoFileMetadata(probe) | ||
638 | |||
639 | // expected format properties | ||
640 | for (const p of [ | ||
641 | 'tags.encoder', | ||
642 | 'format_long_name', | ||
643 | 'size', | ||
644 | 'bit_rate' | ||
645 | ]) { | ||
646 | expect(metadata.format).to.have.nested.property(p) | ||
647 | } | ||
648 | |||
649 | // expected stream properties | ||
650 | for (const p of [ | ||
651 | 'codec_long_name', | ||
652 | 'profile', | ||
653 | 'width', | ||
654 | 'height', | ||
655 | 'display_aspect_ratio', | ||
656 | 'avg_frame_rate', | ||
657 | 'pix_fmt' | ||
658 | ]) { | ||
659 | expect(metadata.streams[0]).to.have.nested.property(p) | ||
660 | } | ||
661 | |||
662 | expect(metadata).to.not.have.nested.property('format.filename') | ||
663 | } | ||
664 | |||
665 | for (const server of servers) { | ||
666 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
667 | |||
668 | const videoFiles = getAllFiles(videoDetails) | ||
669 | expect(videoFiles).to.have.lengthOf(10) | ||
670 | |||
671 | for (const file of videoFiles) { | ||
672 | expect(file.metadata).to.be.undefined | ||
673 | expect(file.metadataUrl).to.exist | ||
674 | expect(file.metadataUrl).to.contain(servers[1].url) | ||
675 | expect(file.metadataUrl).to.contain(videoUUID) | ||
676 | |||
677 | const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl }) | ||
678 | expect(metadata).to.have.nested.property('format.size') | ||
679 | } | ||
680 | } | ||
681 | }) | ||
682 | |||
683 | it('Should correctly detect if quick transcode is possible', async function () { | ||
684 | this.timeout(10_000) | ||
685 | |||
686 | expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true | ||
687 | expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false | ||
688 | }) | ||
689 | }) | ||
690 | |||
691 | describe('Transcoding job queue', function () { | ||
692 | |||
693 | it('Should have the appropriate priorities for transcoding jobs', async function () { | ||
694 | const body = await servers[1].jobs.list({ | ||
695 | start: 0, | ||
696 | count: 100, | ||
697 | sort: 'createdAt', | ||
698 | jobType: 'video-transcoding' | ||
699 | }) | ||
700 | |||
701 | const jobs = body.data | ||
702 | const transcodingJobs = jobs.filter(j => j.data.videoUUID === video4k) | ||
703 | |||
704 | expect(transcodingJobs).to.have.lengthOf(16) | ||
705 | |||
706 | const hlsJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-hls') | ||
707 | const webVideoJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-web-video') | ||
708 | const optimizeJobs = transcodingJobs.filter(j => j.data.type === 'optimize-to-web-video') | ||
709 | |||
710 | expect(hlsJobs).to.have.lengthOf(8) | ||
711 | expect(webVideoJobs).to.have.lengthOf(7) | ||
712 | expect(optimizeJobs).to.have.lengthOf(1) | ||
713 | |||
714 | for (const j of optimizeJobs.concat(hlsJobs.concat(webVideoJobs))) { | ||
715 | expect(j.priority).to.be.greaterThan(100) | ||
716 | expect(j.priority).to.be.lessThan(150) | ||
717 | } | ||
718 | }) | ||
719 | }) | ||
720 | |||
721 | describe('Bounded transcoding', function () { | ||
722 | |||
723 | it('Should not generate an upper resolution than original file', async function () { | ||
724 | this.timeout(120_000) | ||
725 | |||
726 | await servers[0].config.updateExistingSubConfig({ | ||
727 | newConfig: { | ||
728 | transcoding: { | ||
729 | enabled: true, | ||
730 | hls: { enabled: true }, | ||
731 | webVideos: { enabled: true }, | ||
732 | resolutions: { | ||
733 | '0p': false, | ||
734 | '144p': false, | ||
735 | '240p': true, | ||
736 | '360p': false, | ||
737 | '480p': true, | ||
738 | '720p': false, | ||
739 | '1080p': false, | ||
740 | '1440p': false, | ||
741 | '2160p': false | ||
742 | }, | ||
743 | alwaysTranscodeOriginalResolution: false | ||
744 | } | ||
745 | } | ||
746 | }) | ||
747 | |||
748 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' }) | ||
749 | await waitJobs(servers) | ||
750 | |||
751 | const video = await servers[0].videos.get({ id: uuid }) | ||
752 | const hlsFiles = video.streamingPlaylists[0].files | ||
753 | |||
754 | expect(video.files).to.have.lengthOf(2) | ||
755 | expect(hlsFiles).to.have.lengthOf(2) | ||
756 | |||
757 | // eslint-disable-next-line @typescript-eslint/require-array-sort-compare | ||
758 | const resolutions = getAllFiles(video).map(f => f.resolution.id).sort() | ||
759 | expect(resolutions).to.deep.equal([ 240, 240, 480, 480 ]) | ||
760 | }) | ||
761 | |||
762 | it('Should only keep the original resolution if all resolutions are disabled', async function () { | ||
763 | this.timeout(120_000) | ||
764 | |||
765 | await servers[0].config.updateExistingSubConfig({ | ||
766 | newConfig: { | ||
767 | transcoding: { | ||
768 | resolutions: { | ||
769 | '0p': false, | ||
770 | '144p': false, | ||
771 | '240p': false, | ||
772 | '360p': false, | ||
773 | '480p': false, | ||
774 | '720p': false, | ||
775 | '1080p': false, | ||
776 | '1440p': false, | ||
777 | '2160p': false | ||
778 | } | ||
779 | } | ||
780 | } | ||
781 | }) | ||
782 | |||
783 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' }) | ||
784 | await waitJobs(servers) | ||
785 | |||
786 | const video = await servers[0].videos.get({ id: uuid }) | ||
787 | const hlsFiles = video.streamingPlaylists[0].files | ||
788 | |||
789 | expect(video.files).to.have.lengthOf(1) | ||
790 | expect(hlsFiles).to.have.lengthOf(1) | ||
791 | |||
792 | expect(video.files[0].resolution.id).to.equal(720) | ||
793 | expect(hlsFiles[0].resolution.id).to.equal(720) | ||
794 | }) | ||
795 | }) | ||
796 | |||
797 | after(async function () { | ||
798 | await cleanupTests(servers) | ||
799 | }) | ||
800 | }) | ||
diff --git a/server/tests/api/transcoding/update-while-transcoding.ts b/server/tests/api/transcoding/update-while-transcoding.ts deleted file mode 100644 index cfb4fa0cc..000000000 --- a/server/tests/api/transcoding/update-while-transcoding.ts +++ /dev/null | |||
@@ -1,160 +0,0 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { completeCheckHlsPlaylist } from '@server/tests/shared' | ||
4 | import { areMockObjectStorageTestsDisabled, wait } from '@shared/core-utils' | ||
5 | import { VideoPrivacy } from '@shared/models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | ObjectStorageCommand, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | waitJobs | ||
14 | } from '@shared/server-commands' | ||
15 | |||
16 | describe('Test update video privacy while transcoding', function () { | ||
17 | let servers: PeerTubeServer[] = [] | ||
18 | |||
19 | const videoUUIDs: string[] = [] | ||
20 | |||
21 | function runTestSuite (hlsOnly: boolean, objectStorageBaseUrl?: string) { | ||
22 | |||
23 | it('Should not have an error while quickly updating a private video to public after upload #1', async function () { | ||
24 | this.timeout(360_000) | ||
25 | |||
26 | const attributes = { | ||
27 | name: 'quick update', | ||
28 | privacy: VideoPrivacy.PRIVATE | ||
29 | } | ||
30 | |||
31 | const { uuid } = await servers[0].videos.upload({ attributes, waitTorrentGeneration: false }) | ||
32 | await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
33 | videoUUIDs.push(uuid) | ||
34 | |||
35 | await waitJobs(servers) | ||
36 | |||
37 | await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl }) | ||
38 | }) | ||
39 | |||
40 | it('Should not have an error while quickly updating a private video to public after upload #2', async function () { | ||
41 | this.timeout(60000) | ||
42 | |||
43 | { | ||
44 | const attributes = { | ||
45 | name: 'quick update 2', | ||
46 | privacy: VideoPrivacy.PRIVATE | ||
47 | } | ||
48 | |||
49 | const { uuid } = await servers[0].videos.upload({ attributes, waitTorrentGeneration: true }) | ||
50 | await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
51 | videoUUIDs.push(uuid) | ||
52 | |||
53 | await waitJobs(servers) | ||
54 | |||
55 | await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl }) | ||
56 | } | ||
57 | }) | ||
58 | |||
59 | it('Should not have an error while quickly updating a private video to public after upload #3', async function () { | ||
60 | this.timeout(60000) | ||
61 | |||
62 | const attributes = { | ||
63 | name: 'quick update 3', | ||
64 | privacy: VideoPrivacy.PRIVATE | ||
65 | } | ||
66 | |||
67 | const { uuid } = await servers[0].videos.upload({ attributes, waitTorrentGeneration: true }) | ||
68 | await wait(1000) | ||
69 | await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
70 | videoUUIDs.push(uuid) | ||
71 | |||
72 | await waitJobs(servers) | ||
73 | |||
74 | await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl }) | ||
75 | }) | ||
76 | } | ||
77 | |||
78 | before(async function () { | ||
79 | this.timeout(120000) | ||
80 | |||
81 | const configOverride = { | ||
82 | transcoding: { | ||
83 | enabled: true, | ||
84 | allow_audio_files: true, | ||
85 | hls: { | ||
86 | enabled: true | ||
87 | } | ||
88 | } | ||
89 | } | ||
90 | servers = await createMultipleServers(2, configOverride) | ||
91 | |||
92 | // Get the access tokens | ||
93 | await setAccessTokensToServers(servers) | ||
94 | |||
95 | // Server 1 and server 2 follow each other | ||
96 | await doubleFollow(servers[0], servers[1]) | ||
97 | }) | ||
98 | |||
99 | describe('With Web Video & HLS enabled', function () { | ||
100 | runTestSuite(false) | ||
101 | }) | ||
102 | |||
103 | describe('With only HLS enabled', function () { | ||
104 | |||
105 | before(async function () { | ||
106 | await servers[0].config.updateCustomSubConfig({ | ||
107 | newConfig: { | ||
108 | transcoding: { | ||
109 | enabled: true, | ||
110 | allowAudioFiles: true, | ||
111 | resolutions: { | ||
112 | '144p': false, | ||
113 | '240p': true, | ||
114 | '360p': true, | ||
115 | '480p': true, | ||
116 | '720p': true, | ||
117 | '1080p': true, | ||
118 | '1440p': true, | ||
119 | '2160p': true | ||
120 | }, | ||
121 | hls: { | ||
122 | enabled: true | ||
123 | }, | ||
124 | webVideos: { | ||
125 | enabled: false | ||
126 | } | ||
127 | } | ||
128 | } | ||
129 | }) | ||
130 | }) | ||
131 | |||
132 | runTestSuite(true) | ||
133 | }) | ||
134 | |||
135 | describe('With object storage enabled', function () { | ||
136 | if (areMockObjectStorageTestsDisabled()) return | ||
137 | |||
138 | const objectStorage = new ObjectStorageCommand() | ||
139 | |||
140 | before(async function () { | ||
141 | this.timeout(120000) | ||
142 | |||
143 | const configOverride = objectStorage.getDefaultMockConfig() | ||
144 | await objectStorage.prepareDefaultMockBuckets() | ||
145 | |||
146 | await servers[0].kill() | ||
147 | await servers[0].run(configOverride) | ||
148 | }) | ||
149 | |||
150 | runTestSuite(true, objectStorage.getMockPlaylistBaseUrl()) | ||
151 | |||
152 | after(async function () { | ||
153 | await objectStorage.cleanupMock() | ||
154 | }) | ||
155 | }) | ||
156 | |||
157 | after(async function () { | ||
158 | await cleanupTests(servers) | ||
159 | }) | ||
160 | }) | ||
diff --git a/server/tests/api/transcoding/video-studio.ts b/server/tests/api/transcoding/video-studio.ts deleted file mode 100644 index ba68f8e24..000000000 --- a/server/tests/api/transcoding/video-studio.ts +++ /dev/null | |||
@@ -1,377 +0,0 @@ | |||
1 | import { expect } from 'chai' | ||
2 | import { checkPersistentTmpIsEmpty, checkVideoDuration, expectStartWith } from '@server/tests/shared' | ||
3 | import { areMockObjectStorageTestsDisabled, getAllFiles } from '@shared/core-utils' | ||
4 | import { VideoStudioTask } from '@shared/models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | ObjectStorageCommand, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | VideoStudioCommand, | ||
14 | waitJobs | ||
15 | } from '@shared/server-commands' | ||
16 | |||
17 | describe('Test video studio', function () { | ||
18 | let servers: PeerTubeServer[] = [] | ||
19 | let videoUUID: string | ||
20 | |||
21 | async function renewVideo (fixture = 'video_short.webm') { | ||
22 | const video = await servers[0].videos.quickUpload({ name: 'video', fixture }) | ||
23 | videoUUID = video.uuid | ||
24 | |||
25 | await waitJobs(servers) | ||
26 | } | ||
27 | |||
28 | async function createTasks (tasks: VideoStudioTask[]) { | ||
29 | await servers[0].videoStudio.createEditionTasks({ videoId: videoUUID, tasks }) | ||
30 | await waitJobs(servers) | ||
31 | } | ||
32 | |||
33 | before(async function () { | ||
34 | this.timeout(120_000) | ||
35 | |||
36 | servers = await createMultipleServers(2) | ||
37 | |||
38 | await setAccessTokensToServers(servers) | ||
39 | await setDefaultVideoChannel(servers) | ||
40 | |||
41 | await doubleFollow(servers[0], servers[1]) | ||
42 | |||
43 | await servers[0].config.enableMinimumTranscoding() | ||
44 | |||
45 | await servers[0].config.enableStudio() | ||
46 | }) | ||
47 | |||
48 | describe('Cutting', function () { | ||
49 | |||
50 | it('Should cut the beginning of the video', async function () { | ||
51 | this.timeout(120_000) | ||
52 | |||
53 | await renewVideo() | ||
54 | await waitJobs(servers) | ||
55 | |||
56 | const beforeTasks = new Date() | ||
57 | |||
58 | await createTasks([ | ||
59 | { | ||
60 | name: 'cut', | ||
61 | options: { | ||
62 | start: 2 | ||
63 | } | ||
64 | } | ||
65 | ]) | ||
66 | |||
67 | for (const server of servers) { | ||
68 | await checkVideoDuration(server, videoUUID, 3) | ||
69 | |||
70 | const video = await server.videos.get({ id: videoUUID }) | ||
71 | expect(new Date(video.publishedAt)).to.be.below(beforeTasks) | ||
72 | } | ||
73 | }) | ||
74 | |||
75 | it('Should cut the end of the video', async function () { | ||
76 | this.timeout(120_000) | ||
77 | await renewVideo() | ||
78 | |||
79 | await createTasks([ | ||
80 | { | ||
81 | name: 'cut', | ||
82 | options: { | ||
83 | end: 2 | ||
84 | } | ||
85 | } | ||
86 | ]) | ||
87 | |||
88 | for (const server of servers) { | ||
89 | await checkVideoDuration(server, videoUUID, 2) | ||
90 | } | ||
91 | }) | ||
92 | |||
93 | it('Should cut start/end of the video', async function () { | ||
94 | this.timeout(120_000) | ||
95 | await renewVideo('video_short1.webm') // 10 seconds video duration | ||
96 | |||
97 | await createTasks([ | ||
98 | { | ||
99 | name: 'cut', | ||
100 | options: { | ||
101 | start: 2, | ||
102 | end: 6 | ||
103 | } | ||
104 | } | ||
105 | ]) | ||
106 | |||
107 | for (const server of servers) { | ||
108 | await checkVideoDuration(server, videoUUID, 4) | ||
109 | } | ||
110 | }) | ||
111 | }) | ||
112 | |||
113 | describe('Intro/Outro', function () { | ||
114 | |||
115 | it('Should add an intro', async function () { | ||
116 | this.timeout(120_000) | ||
117 | await renewVideo() | ||
118 | |||
119 | await createTasks([ | ||
120 | { | ||
121 | name: 'add-intro', | ||
122 | options: { | ||
123 | file: 'video_short.webm' | ||
124 | } | ||
125 | } | ||
126 | ]) | ||
127 | |||
128 | for (const server of servers) { | ||
129 | await checkVideoDuration(server, videoUUID, 10) | ||
130 | } | ||
131 | }) | ||
132 | |||
133 | it('Should add an outro', async function () { | ||
134 | this.timeout(120_000) | ||
135 | await renewVideo() | ||
136 | |||
137 | await createTasks([ | ||
138 | { | ||
139 | name: 'add-outro', | ||
140 | options: { | ||
141 | file: 'video_very_short_240p.mp4' | ||
142 | } | ||
143 | } | ||
144 | ]) | ||
145 | |||
146 | for (const server of servers) { | ||
147 | await checkVideoDuration(server, videoUUID, 7) | ||
148 | } | ||
149 | }) | ||
150 | |||
151 | it('Should add an intro/outro', async function () { | ||
152 | this.timeout(120_000) | ||
153 | await renewVideo() | ||
154 | |||
155 | await createTasks([ | ||
156 | { | ||
157 | name: 'add-intro', | ||
158 | options: { | ||
159 | file: 'video_very_short_240p.mp4' | ||
160 | } | ||
161 | }, | ||
162 | { | ||
163 | name: 'add-outro', | ||
164 | options: { | ||
165 | // Different frame rate | ||
166 | file: 'video_short2.webm' | ||
167 | } | ||
168 | } | ||
169 | ]) | ||
170 | |||
171 | for (const server of servers) { | ||
172 | await checkVideoDuration(server, videoUUID, 12) | ||
173 | } | ||
174 | }) | ||
175 | |||
176 | it('Should add an intro to a video without audio', async function () { | ||
177 | this.timeout(120_000) | ||
178 | await renewVideo('video_short_no_audio.mp4') | ||
179 | |||
180 | await createTasks([ | ||
181 | { | ||
182 | name: 'add-intro', | ||
183 | options: { | ||
184 | file: 'video_very_short_240p.mp4' | ||
185 | } | ||
186 | } | ||
187 | ]) | ||
188 | |||
189 | for (const server of servers) { | ||
190 | await checkVideoDuration(server, videoUUID, 7) | ||
191 | } | ||
192 | }) | ||
193 | |||
194 | it('Should add an outro without audio to a video with audio', async function () { | ||
195 | this.timeout(120_000) | ||
196 | await renewVideo() | ||
197 | |||
198 | await createTasks([ | ||
199 | { | ||
200 | name: 'add-outro', | ||
201 | options: { | ||
202 | file: 'video_short_no_audio.mp4' | ||
203 | } | ||
204 | } | ||
205 | ]) | ||
206 | |||
207 | for (const server of servers) { | ||
208 | await checkVideoDuration(server, videoUUID, 10) | ||
209 | } | ||
210 | }) | ||
211 | |||
212 | it('Should add an outro without audio to a video with audio', async function () { | ||
213 | this.timeout(120_000) | ||
214 | await renewVideo('video_short_no_audio.mp4') | ||
215 | |||
216 | await createTasks([ | ||
217 | { | ||
218 | name: 'add-outro', | ||
219 | options: { | ||
220 | file: 'video_short_no_audio.mp4' | ||
221 | } | ||
222 | } | ||
223 | ]) | ||
224 | |||
225 | for (const server of servers) { | ||
226 | await checkVideoDuration(server, videoUUID, 10) | ||
227 | } | ||
228 | }) | ||
229 | }) | ||
230 | |||
231 | describe('Watermark', function () { | ||
232 | |||
233 | it('Should add a watermark to the video', async function () { | ||
234 | this.timeout(120_000) | ||
235 | await renewVideo() | ||
236 | |||
237 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
238 | const oldFileUrls = getAllFiles(video).map(f => f.fileUrl) | ||
239 | |||
240 | await createTasks([ | ||
241 | { | ||
242 | name: 'add-watermark', | ||
243 | options: { | ||
244 | file: 'custom-thumbnail.png' | ||
245 | } | ||
246 | } | ||
247 | ]) | ||
248 | |||
249 | for (const server of servers) { | ||
250 | const video = await server.videos.get({ id: videoUUID }) | ||
251 | const fileUrls = getAllFiles(video).map(f => f.fileUrl) | ||
252 | |||
253 | for (const oldUrl of oldFileUrls) { | ||
254 | expect(fileUrls).to.not.include(oldUrl) | ||
255 | } | ||
256 | } | ||
257 | }) | ||
258 | }) | ||
259 | |||
260 | describe('Complex tasks', function () { | ||
261 | it('Should run a complex task', async function () { | ||
262 | this.timeout(240_000) | ||
263 | await renewVideo() | ||
264 | |||
265 | await createTasks(VideoStudioCommand.getComplexTask()) | ||
266 | |||
267 | for (const server of servers) { | ||
268 | await checkVideoDuration(server, videoUUID, 9) | ||
269 | } | ||
270 | }) | ||
271 | }) | ||
272 | |||
273 | describe('HLS only studio edition', function () { | ||
274 | |||
275 | before(async function () { | ||
276 | // Disable Web Videos | ||
277 | await servers[0].config.updateExistingSubConfig({ | ||
278 | newConfig: { | ||
279 | transcoding: { | ||
280 | webVideos: { | ||
281 | enabled: false | ||
282 | } | ||
283 | } | ||
284 | } | ||
285 | }) | ||
286 | }) | ||
287 | |||
288 | it('Should run a complex task on HLS only video', async function () { | ||
289 | this.timeout(240_000) | ||
290 | await renewVideo() | ||
291 | |||
292 | await createTasks(VideoStudioCommand.getComplexTask()) | ||
293 | |||
294 | for (const server of servers) { | ||
295 | const video = await server.videos.get({ id: videoUUID }) | ||
296 | expect(video.files).to.have.lengthOf(0) | ||
297 | |||
298 | await checkVideoDuration(server, videoUUID, 9) | ||
299 | } | ||
300 | }) | ||
301 | }) | ||
302 | |||
303 | describe('Server restart', function () { | ||
304 | |||
305 | it('Should still be able to run video edition after a server restart', async function () { | ||
306 | this.timeout(240_000) | ||
307 | |||
308 | await renewVideo() | ||
309 | await servers[0].videoStudio.createEditionTasks({ videoId: videoUUID, tasks: VideoStudioCommand.getComplexTask() }) | ||
310 | |||
311 | await servers[0].kill() | ||
312 | await servers[0].run() | ||
313 | |||
314 | await waitJobs(servers) | ||
315 | |||
316 | for (const server of servers) { | ||
317 | await checkVideoDuration(server, videoUUID, 9) | ||
318 | } | ||
319 | }) | ||
320 | |||
321 | it('Should have an empty persistent tmp directory', async function () { | ||
322 | await checkPersistentTmpIsEmpty(servers[0]) | ||
323 | }) | ||
324 | }) | ||
325 | |||
326 | describe('Object storage studio edition', function () { | ||
327 | if (areMockObjectStorageTestsDisabled()) return | ||
328 | |||
329 | const objectStorage = new ObjectStorageCommand() | ||
330 | |||
331 | before(async function () { | ||
332 | await objectStorage.prepareDefaultMockBuckets() | ||
333 | |||
334 | await servers[0].kill() | ||
335 | await servers[0].run(objectStorage.getDefaultMockConfig()) | ||
336 | |||
337 | await servers[0].config.enableMinimumTranscoding() | ||
338 | }) | ||
339 | |||
340 | it('Should run a complex task on a video in object storage', async function () { | ||
341 | this.timeout(240_000) | ||
342 | await renewVideo() | ||
343 | |||
344 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
345 | const oldFileUrls = getAllFiles(video).map(f => f.fileUrl) | ||
346 | |||
347 | await createTasks(VideoStudioCommand.getComplexTask()) | ||
348 | |||
349 | for (const server of servers) { | ||
350 | const video = await server.videos.get({ id: videoUUID }) | ||
351 | const files = getAllFiles(video) | ||
352 | |||
353 | for (const f of files) { | ||
354 | expect(oldFileUrls).to.not.include(f.fileUrl) | ||
355 | } | ||
356 | |||
357 | for (const webVideoFile of video.files) { | ||
358 | expectStartWith(webVideoFile.fileUrl, objectStorage.getMockWebVideosBaseUrl()) | ||
359 | } | ||
360 | |||
361 | for (const hlsFile of video.streamingPlaylists[0].files) { | ||
362 | expectStartWith(hlsFile.fileUrl, objectStorage.getMockPlaylistBaseUrl()) | ||
363 | } | ||
364 | |||
365 | await checkVideoDuration(server, videoUUID, 9) | ||
366 | } | ||
367 | }) | ||
368 | |||
369 | after(async function () { | ||
370 | await objectStorage.cleanupMock() | ||
371 | }) | ||
372 | }) | ||
373 | |||
374 | after(async function () { | ||
375 | await cleanupTests(servers) | ||
376 | }) | ||
377 | }) | ||