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