]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - shared/extra-utils/videos/live.ts
Increase ffmpeg error test timeout
[github/Chocobozzz/PeerTube.git] / shared / extra-utils / videos / live.ts
1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3 import { expect } from 'chai'
4 import * as ffmpeg from 'fluent-ffmpeg'
5 import { pathExists, readdir } from 'fs-extra'
6 import { omit } from 'lodash'
7 import { join } from 'path'
8 import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models'
9 import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
10 import { buildAbsoluteFixturePath, buildServerDirectory, wait } from '../miscs/miscs'
11 import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
12 import { ServerInfo, waitUntilLog } from '../server/servers'
13 import { getVideoWithToken } from './videos'
14
15 function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = HttpStatusCode.OK_200) {
16 const path = '/api/v1/videos/live'
17
18 return makeGetRequest({
19 url,
20 token,
21 path: path + '/' + videoId,
22 statusCodeExpected
23 })
24 }
25
26 function updateLive (
27 url: string,
28 token: string,
29 videoId: number | string,
30 fields: LiveVideoUpdate,
31 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
32 ) {
33 const path = '/api/v1/videos/live'
34
35 return makePutBodyRequest({
36 url,
37 token,
38 path: path + '/' + videoId,
39 fields,
40 statusCodeExpected
41 })
42 }
43
44 function createLive (url: string, token: string, fields: LiveVideoCreate, statusCodeExpected = HttpStatusCode.OK_200) {
45 const path = '/api/v1/videos/live'
46
47 const attaches: any = {}
48 if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
49 if (fields.previewfile) attaches.previewfile = fields.previewfile
50
51 const updatedFields = omit(fields, 'thumbnailfile', 'previewfile')
52
53 return makeUploadRequest({
54 url,
55 path,
56 token,
57 attaches,
58 fields: updatedFields,
59 statusCodeExpected
60 })
61 }
62
63 async function sendRTMPStreamInVideo (url: string, token: string, videoId: number | string, fixtureName?: string) {
64 const res = await getLive(url, token, videoId)
65 const videoLive = res.body as LiveVideo
66
67 return sendRTMPStream(videoLive.rtmpUrl, videoLive.streamKey, fixtureName)
68 }
69
70 function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = 'video_short.mp4') {
71 const fixture = buildAbsoluteFixturePath(fixtureName)
72
73 const command = ffmpeg(fixture)
74 command.inputOption('-stream_loop -1')
75 command.inputOption('-re')
76 command.outputOption('-c:v libx264')
77 command.outputOption('-g 50')
78 command.outputOption('-keyint_min 2')
79 command.outputOption('-r 60')
80 command.outputOption('-f flv')
81
82 const rtmpUrl = rtmpBaseUrl + '/' + streamKey
83 command.output(rtmpUrl)
84
85 command.on('error', err => {
86 if (err?.message?.includes('Exiting normally')) return
87
88 if (process.env.DEBUG) console.error(err)
89 })
90
91 if (process.env.DEBUG) {
92 command.on('stderr', data => console.log(data))
93 }
94
95 command.run()
96
97 return command
98 }
99
100 function waitFfmpegUntilError (command: ffmpeg.FfmpegCommand, successAfterMS = 10000) {
101 return new Promise<void>((res, rej) => {
102 command.on('error', err => {
103 return rej(err)
104 })
105
106 setTimeout(() => {
107 res()
108 }, successAfterMS)
109 })
110 }
111
112 async function runAndTestFfmpegStreamError (url: string, token: string, videoId: number | string, shouldHaveError: boolean) {
113 const command = await sendRTMPStreamInVideo(url, token, videoId)
114
115 return testFfmpegStreamError(command, shouldHaveError)
116 }
117
118 async function testFfmpegStreamError (command: ffmpeg.FfmpegCommand, shouldHaveError: boolean) {
119 let error: Error
120
121 try {
122 await waitFfmpegUntilError(command, 35000)
123 } catch (err) {
124 error = err
125 }
126
127 await stopFfmpeg(command)
128
129 if (shouldHaveError && !error) throw new Error('Ffmpeg did not have an error')
130 if (!shouldHaveError && error) throw error
131 }
132
133 async function stopFfmpeg (command: ffmpeg.FfmpegCommand) {
134 command.kill('SIGINT')
135
136 await wait(500)
137 }
138
139 function waitUntilLivePublished (url: string, token: string, videoId: number | string) {
140 return waitUntilLiveState(url, token, videoId, VideoState.PUBLISHED)
141 }
142
143 function waitUntilLiveWaiting (url: string, token: string, videoId: number | string) {
144 return waitUntilLiveState(url, token, videoId, VideoState.WAITING_FOR_LIVE)
145 }
146
147 function waitUntilLiveEnded (url: string, token: string, videoId: number | string) {
148 return waitUntilLiveState(url, token, videoId, VideoState.LIVE_ENDED)
149 }
150
151 function waitUntilLiveSegmentGeneration (server: ServerInfo, videoUUID: string, resolutionNum: number, segmentNum: number) {
152 const segmentName = `${resolutionNum}-00000${segmentNum}.ts`
153 return waitUntilLog(server, `${videoUUID}/${segmentName}`, 2, false)
154 }
155
156 async function waitUntilLiveState (url: string, token: string, videoId: number | string, state: VideoState) {
157 let video: VideoDetails
158
159 do {
160 const res = await getVideoWithToken(url, token, videoId)
161 video = res.body
162
163 await wait(500)
164 } while (video.state.id !== state)
165 }
166
167 async function waitUntilLiveSaved (url: string, token: string, videoId: number | string) {
168 let video: VideoDetails
169
170 do {
171 const res = await getVideoWithToken(url, token, videoId)
172 video = res.body
173
174 await wait(500)
175 } while (video.isLive === true && video.state.id !== VideoState.PUBLISHED)
176 }
177
178 async function checkLiveCleanup (server: ServerInfo, videoUUID: string, resolutions: number[] = []) {
179 const basePath = buildServerDirectory(server, 'streaming-playlists')
180 const hlsPath = join(basePath, 'hls', videoUUID)
181
182 if (resolutions.length === 0) {
183 const result = await pathExists(hlsPath)
184 expect(result).to.be.false
185
186 return
187 }
188
189 const files = await readdir(hlsPath)
190
191 // fragmented file and playlist per resolution + master playlist + segments sha256 json file
192 expect(files).to.have.lengthOf(resolutions.length * 2 + 2)
193
194 for (const resolution of resolutions) {
195 expect(files).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`)
196 expect(files).to.contain(`${resolution}.m3u8`)
197 }
198
199 expect(files).to.contain('master.m3u8')
200 expect(files).to.contain('segments-sha256.json')
201 }
202
203 async function getPlaylistsCount (server: ServerInfo, videoUUID: string) {
204 const basePath = buildServerDirectory(server, 'streaming-playlists')
205 const hlsPath = join(basePath, 'hls', videoUUID)
206
207 const files = await readdir(hlsPath)
208
209 return files.filter(f => f.endsWith('.m3u8')).length
210 }
211
212 // ---------------------------------------------------------------------------
213
214 export {
215 getLive,
216 getPlaylistsCount,
217 waitUntilLiveSaved,
218 waitUntilLivePublished,
219 updateLive,
220 createLive,
221 runAndTestFfmpegStreamError,
222 checkLiveCleanup,
223 waitUntilLiveSegmentGeneration,
224 stopFfmpeg,
225 waitUntilLiveWaiting,
226 sendRTMPStreamInVideo,
227 waitUntilLiveEnded,
228 waitFfmpegUntilError,
229 sendRTMPStream,
230 testFfmpegStreamError
231 }