diff options
Diffstat (limited to 'server/tests/api/runners/runner-live-transcoding.ts')
-rw-r--r-- | server/tests/api/runners/runner-live-transcoding.ts | 330 |
1 files changed, 0 insertions, 330 deletions
diff --git a/server/tests/api/runners/runner-live-transcoding.ts b/server/tests/api/runners/runner-live-transcoding.ts deleted file mode 100644 index b11d54039..000000000 --- a/server/tests/api/runners/runner-live-transcoding.ts +++ /dev/null | |||
@@ -1,330 +0,0 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { readFile } from 'fs-extra' | ||
6 | import { buildAbsoluteFixturePath, wait } from '@shared/core-utils' | ||
7 | import { | ||
8 | HttpStatusCode, | ||
9 | LiveRTMPHLSTranscodingUpdatePayload, | ||
10 | LiveVideo, | ||
11 | LiveVideoError, | ||
12 | RunnerJob, | ||
13 | RunnerJobLiveRTMPHLSTranscodingPayload, | ||
14 | Video, | ||
15 | VideoPrivacy, | ||
16 | VideoState | ||
17 | } from '@shared/models' | ||
18 | import { | ||
19 | cleanupTests, | ||
20 | createSingleServer, | ||
21 | makeRawRequest, | ||
22 | PeerTubeServer, | ||
23 | sendRTMPStream, | ||
24 | setAccessTokensToServers, | ||
25 | setDefaultVideoChannel, | ||
26 | stopFfmpeg, | ||
27 | testFfmpegStreamError, | ||
28 | waitJobs | ||
29 | } from '@shared/server-commands' | ||
30 | |||
31 | describe('Test runner live transcoding', function () { | ||
32 | let server: PeerTubeServer | ||
33 | let runnerToken: string | ||
34 | let baseUrl: string | ||
35 | |||
36 | before(async function () { | ||
37 | this.timeout(120_000) | ||
38 | |||
39 | server = await createSingleServer(1) | ||
40 | |||
41 | await setAccessTokensToServers([ server ]) | ||
42 | await setDefaultVideoChannel([ server ]) | ||
43 | |||
44 | await server.config.enableRemoteTranscoding() | ||
45 | await server.config.enableTranscoding() | ||
46 | runnerToken = await server.runners.autoRegisterRunner() | ||
47 | |||
48 | baseUrl = server.url + '/static/streaming-playlists/hls' | ||
49 | }) | ||
50 | |||
51 | describe('Without transcoding enabled', function () { | ||
52 | |||
53 | before(async function () { | ||
54 | await server.config.enableLive({ | ||
55 | allowReplay: false, | ||
56 | resolutions: 'min', | ||
57 | transcoding: false | ||
58 | }) | ||
59 | }) | ||
60 | |||
61 | it('Should not have available jobs', async function () { | ||
62 | this.timeout(120000) | ||
63 | |||
64 | const { live, video } = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC }) | ||
65 | |||
66 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
67 | await server.live.waitUntilPublished({ videoId: video.id }) | ||
68 | |||
69 | await waitJobs([ server ]) | ||
70 | |||
71 | const { availableJobs } = await server.runnerJobs.requestLive({ runnerToken }) | ||
72 | expect(availableJobs).to.have.lengthOf(0) | ||
73 | |||
74 | await stopFfmpeg(ffmpegCommand) | ||
75 | }) | ||
76 | }) | ||
77 | |||
78 | describe('With transcoding enabled on classic live', function () { | ||
79 | let live: LiveVideo | ||
80 | let video: Video | ||
81 | let ffmpegCommand: FfmpegCommand | ||
82 | let jobUUID: string | ||
83 | let acceptedJob: RunnerJob & { jobToken: string } | ||
84 | |||
85 | async function testPlaylistFile (fixture: string, expected: string) { | ||
86 | const text = await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/${fixture}` }) | ||
87 | expect(await readFile(buildAbsoluteFixturePath(expected), 'utf-8')).to.equal(text) | ||
88 | |||
89 | } | ||
90 | |||
91 | async function testTSFile (fixture: string, expected: string) { | ||
92 | const { body } = await makeRawRequest({ url: `${baseUrl}/${video.uuid}/${fixture}`, expectedStatus: HttpStatusCode.OK_200 }) | ||
93 | expect(await readFile(buildAbsoluteFixturePath(expected))).to.deep.equal(body) | ||
94 | } | ||
95 | |||
96 | before(async function () { | ||
97 | await server.config.enableLive({ | ||
98 | allowReplay: true, | ||
99 | resolutions: 'max', | ||
100 | transcoding: true | ||
101 | }) | ||
102 | }) | ||
103 | |||
104 | it('Should publish a a live and have available jobs', async function () { | ||
105 | this.timeout(120000) | ||
106 | |||
107 | const data = await server.live.quickCreate({ permanentLive: false, saveReplay: false, privacy: VideoPrivacy.PUBLIC }) | ||
108 | live = data.live | ||
109 | video = data.video | ||
110 | |||
111 | ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
112 | await waitJobs([ server ]) | ||
113 | |||
114 | const job = await server.runnerJobs.requestLiveJob(runnerToken) | ||
115 | jobUUID = job.uuid | ||
116 | |||
117 | expect(job.type).to.equal('live-rtmp-hls-transcoding') | ||
118 | expect(job.payload.input.rtmpUrl).to.exist | ||
119 | |||
120 | expect(job.payload.output.toTranscode).to.have.lengthOf(5) | ||
121 | |||
122 | for (const { resolution, fps } of job.payload.output.toTranscode) { | ||
123 | expect([ 720, 480, 360, 240, 144 ]).to.contain(resolution) | ||
124 | |||
125 | expect(fps).to.be.above(25) | ||
126 | expect(fps).to.be.below(70) | ||
127 | } | ||
128 | }) | ||
129 | |||
130 | it('Should update the live with a new chunk', async function () { | ||
131 | this.timeout(120000) | ||
132 | |||
133 | const { job } = await server.runnerJobs.accept<RunnerJobLiveRTMPHLSTranscodingPayload>({ jobUUID, runnerToken }) | ||
134 | acceptedJob = job | ||
135 | |||
136 | { | ||
137 | const payload: LiveRTMPHLSTranscodingUpdatePayload = { | ||
138 | masterPlaylistFile: 'live/master.m3u8', | ||
139 | resolutionPlaylistFile: 'live/0.m3u8', | ||
140 | resolutionPlaylistFilename: '0.m3u8', | ||
141 | type: 'add-chunk', | ||
142 | videoChunkFile: 'live/0-000067.ts', | ||
143 | videoChunkFilename: '0-000067.ts' | ||
144 | } | ||
145 | await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: job.jobToken, payload, progress: 50 }) | ||
146 | |||
147 | const updatedJob = await server.runnerJobs.getJob({ uuid: job.uuid }) | ||
148 | expect(updatedJob.progress).to.equal(50) | ||
149 | } | ||
150 | |||
151 | { | ||
152 | const payload: LiveRTMPHLSTranscodingUpdatePayload = { | ||
153 | resolutionPlaylistFile: 'live/1.m3u8', | ||
154 | resolutionPlaylistFilename: '1.m3u8', | ||
155 | type: 'add-chunk', | ||
156 | videoChunkFile: 'live/1-000068.ts', | ||
157 | videoChunkFilename: '1-000068.ts' | ||
158 | } | ||
159 | await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: job.jobToken, payload }) | ||
160 | } | ||
161 | |||
162 | await wait(1000) | ||
163 | |||
164 | await testPlaylistFile('master.m3u8', 'live/master.m3u8') | ||
165 | await testPlaylistFile('0.m3u8', 'live/0.m3u8') | ||
166 | await testPlaylistFile('1.m3u8', 'live/1.m3u8') | ||
167 | |||
168 | await testTSFile('0-000067.ts', 'live/0-000067.ts') | ||
169 | await testTSFile('1-000068.ts', 'live/1-000068.ts') | ||
170 | }) | ||
171 | |||
172 | it('Should replace existing m3u8 on update', async function () { | ||
173 | this.timeout(120000) | ||
174 | |||
175 | const payload: LiveRTMPHLSTranscodingUpdatePayload = { | ||
176 | masterPlaylistFile: 'live/1.m3u8', | ||
177 | resolutionPlaylistFilename: '0.m3u8', | ||
178 | resolutionPlaylistFile: 'live/1.m3u8', | ||
179 | type: 'add-chunk', | ||
180 | videoChunkFile: 'live/1-000069.ts', | ||
181 | videoChunkFilename: '1-000068.ts' | ||
182 | } | ||
183 | await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload }) | ||
184 | await wait(1000) | ||
185 | |||
186 | await testPlaylistFile('master.m3u8', 'live/1.m3u8') | ||
187 | await testPlaylistFile('0.m3u8', 'live/1.m3u8') | ||
188 | await testTSFile('1-000068.ts', 'live/1-000069.ts') | ||
189 | }) | ||
190 | |||
191 | it('Should update the live with removed chunks', async function () { | ||
192 | this.timeout(120000) | ||
193 | |||
194 | const payload: LiveRTMPHLSTranscodingUpdatePayload = { | ||
195 | resolutionPlaylistFile: 'live/0.m3u8', | ||
196 | resolutionPlaylistFilename: '0.m3u8', | ||
197 | type: 'remove-chunk', | ||
198 | videoChunkFilename: '1-000068.ts' | ||
199 | } | ||
200 | await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload }) | ||
201 | |||
202 | await wait(1000) | ||
203 | |||
204 | await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/master.m3u8` }) | ||
205 | await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/0.m3u8` }) | ||
206 | await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/1.m3u8` }) | ||
207 | await makeRawRequest({ url: `${baseUrl}/${video.uuid}/0-000067.ts`, expectedStatus: HttpStatusCode.OK_200 }) | ||
208 | await makeRawRequest({ url: `${baseUrl}/${video.uuid}/1-000068.ts`, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
209 | }) | ||
210 | |||
211 | it('Should complete the live and save the replay', async function () { | ||
212 | this.timeout(120000) | ||
213 | |||
214 | for (const segment of [ '0-000069.ts', '0-000070.ts' ]) { | ||
215 | const payload: LiveRTMPHLSTranscodingUpdatePayload = { | ||
216 | masterPlaylistFile: 'live/master.m3u8', | ||
217 | resolutionPlaylistFilename: '0.m3u8', | ||
218 | resolutionPlaylistFile: 'live/0.m3u8', | ||
219 | type: 'add-chunk', | ||
220 | videoChunkFile: 'live/' + segment, | ||
221 | videoChunkFilename: segment | ||
222 | } | ||
223 | await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload }) | ||
224 | |||
225 | await wait(1000) | ||
226 | } | ||
227 | |||
228 | await waitJobs([ server ]) | ||
229 | |||
230 | { | ||
231 | const { state } = await server.videos.get({ id: video.uuid }) | ||
232 | expect(state.id).to.equal(VideoState.PUBLISHED) | ||
233 | } | ||
234 | |||
235 | await stopFfmpeg(ffmpegCommand) | ||
236 | |||
237 | await server.runnerJobs.success({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload: {} }) | ||
238 | |||
239 | await wait(1500) | ||
240 | await waitJobs([ server ]) | ||
241 | |||
242 | { | ||
243 | const { state } = await server.videos.get({ id: video.uuid }) | ||
244 | expect(state.id).to.equal(VideoState.LIVE_ENDED) | ||
245 | |||
246 | const session = await server.live.findLatestSession({ videoId: video.uuid }) | ||
247 | expect(session.error).to.be.null | ||
248 | } | ||
249 | }) | ||
250 | }) | ||
251 | |||
252 | describe('With transcoding enabled on cancelled/aborted/errored live', function () { | ||
253 | let live: LiveVideo | ||
254 | let video: Video | ||
255 | let ffmpegCommand: FfmpegCommand | ||
256 | |||
257 | async function prepare () { | ||
258 | ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
259 | await server.runnerJobs.requestLiveJob(runnerToken) | ||
260 | |||
261 | const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'live-rtmp-hls-transcoding' }) | ||
262 | |||
263 | return job | ||
264 | } | ||
265 | |||
266 | async function checkSessionError (error: LiveVideoError) { | ||
267 | await wait(1500) | ||
268 | await waitJobs([ server ]) | ||
269 | |||
270 | const session = await server.live.findLatestSession({ videoId: video.uuid }) | ||
271 | expect(session.error).to.equal(error) | ||
272 | } | ||
273 | |||
274 | before(async function () { | ||
275 | await server.config.enableLive({ | ||
276 | allowReplay: true, | ||
277 | resolutions: 'max', | ||
278 | transcoding: true | ||
279 | }) | ||
280 | |||
281 | const data = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC }) | ||
282 | live = data.live | ||
283 | video = data.video | ||
284 | }) | ||
285 | |||
286 | it('Should abort a running live', async function () { | ||
287 | this.timeout(120000) | ||
288 | |||
289 | const job = await prepare() | ||
290 | |||
291 | await Promise.all([ | ||
292 | server.runnerJobs.abort({ jobUUID: job.uuid, runnerToken, jobToken: job.jobToken, reason: 'abort' }), | ||
293 | testFfmpegStreamError(ffmpegCommand, true) | ||
294 | ]) | ||
295 | |||
296 | // Abort is not supported | ||
297 | await checkSessionError(LiveVideoError.RUNNER_JOB_ERROR) | ||
298 | }) | ||
299 | |||
300 | it('Should cancel a running live', async function () { | ||
301 | this.timeout(120000) | ||
302 | |||
303 | const job = await prepare() | ||
304 | |||
305 | await Promise.all([ | ||
306 | server.runnerJobs.cancelByAdmin({ jobUUID: job.uuid }), | ||
307 | testFfmpegStreamError(ffmpegCommand, true) | ||
308 | ]) | ||
309 | |||
310 | await checkSessionError(LiveVideoError.RUNNER_JOB_CANCEL) | ||
311 | }) | ||
312 | |||
313 | it('Should error a running live', async function () { | ||
314 | this.timeout(120000) | ||
315 | |||
316 | const job = await prepare() | ||
317 | |||
318 | await Promise.all([ | ||
319 | server.runnerJobs.error({ jobUUID: job.uuid, runnerToken, jobToken: job.jobToken, message: 'error' }), | ||
320 | testFfmpegStreamError(ffmpegCommand, true) | ||
321 | ]) | ||
322 | |||
323 | await checkSessionError(LiveVideoError.RUNNER_JOB_ERROR) | ||
324 | }) | ||
325 | }) | ||
326 | |||
327 | after(async function () { | ||
328 | await cleanupTests([ server ]) | ||
329 | }) | ||
330 | }) | ||