aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/tests/api/transcoding
diff options
context:
space:
mode:
Diffstat (limited to 'server/tests/api/transcoding')
-rw-r--r--server/tests/api/transcoding/audio-only.ts104
-rw-r--r--server/tests/api/transcoding/create-transcoding.ts266
-rw-r--r--server/tests/api/transcoding/hls.ts175
-rw-r--r--server/tests/api/transcoding/index.ts6
-rw-r--r--server/tests/api/transcoding/transcoder.ts800
-rw-r--r--server/tests/api/transcoding/update-while-transcoding.ts160
-rw-r--r--server/tests/api/transcoding/video-studio.ts377
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
3import { expect } from 'chai'
4import { getAudioStream, getVideoStreamDimensionsInfo } from '@shared/ffmpeg'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 waitJobs
12} from '@shared/server-commands'
13
14describe('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
3import { expect } from 'chai'
4import { checkResolutionsInMasterPlaylist, expectStartWith } from '@server/tests/shared'
5import { areMockObjectStorageTestsDisabled } from '@shared/core-utils'
6import { HttpStatusCode, VideoDetails } from '@shared/models'
7import {
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
20async 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
41function 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
255describe('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
3import { join } from 'path'
4import { checkDirectoryIsEmpty, checkTmpIsEmpty, completeCheckHlsPlaylist } from '@server/tests/shared'
5import { areMockObjectStorageTestsDisabled } from '@shared/core-utils'
6import { HttpStatusCode } from '@shared/models'
7import {
8 cleanupTests,
9 createMultipleServers,
10 doubleFollow,
11 ObjectStorageCommand,
12 PeerTubeServer,
13 setAccessTokensToServers,
14 waitJobs
15} from '@shared/server-commands'
16import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants'
17
18describe('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 @@
1export * from './audio-only'
2export * from './create-transcoding'
3export * from './hls'
4export * from './transcoder'
5export * from './update-while-transcoding'
6export * 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
3import { expect } from 'chai'
4import { canDoQuickTranscode } from '@server/lib/transcoding/transcoding-quick-transcode'
5import { checkWebTorrentWorks, generateHighBitrateVideo, generateVideoWithFramerate } from '@server/tests/shared'
6import { buildAbsoluteFixturePath, getAllFiles, getMaxTheoreticalBitrate, getMinTheoreticalBitrate, omit } from '@shared/core-utils'
7import {
8 ffprobePromise,
9 getAudioStream,
10 getVideoStreamBitrate,
11 getVideoStreamDimensionsInfo,
12 getVideoStreamFPS,
13 hasAudioStream
14} from '@shared/ffmpeg'
15import { HttpStatusCode, VideoFileMetadata, VideoState } from '@shared/models'
16import {
17 cleanupTests,
18 createMultipleServers,
19 doubleFollow,
20 makeGetRequest,
21 PeerTubeServer,
22 setAccessTokensToServers,
23 waitJobs
24} from '@shared/server-commands'
25
26function 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
51describe('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
3import { completeCheckHlsPlaylist } from '@server/tests/shared'
4import { areMockObjectStorageTestsDisabled, wait } from '@shared/core-utils'
5import { VideoPrivacy } from '@shared/models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 ObjectStorageCommand,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 waitJobs
14} from '@shared/server-commands'
15
16describe('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 @@
1import { expect } from 'chai'
2import { checkPersistentTmpIsEmpty, checkVideoDuration, expectStartWith } from '@server/tests/shared'
3import { areMockObjectStorageTestsDisabled, getAllFiles } from '@shared/core-utils'
4import { VideoStudioTask } from '@shared/models'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 ObjectStorageCommand,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultVideoChannel,
13 VideoStudioCommand,
14 waitJobs
15} from '@shared/server-commands'
16
17describe('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})