]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - shared/extra-utils/videos/live.ts
Update dependencies.md
[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 waitUntilLivePublishedOnAllServers (servers: ServerInfo[], videoId: string) {
179 for (const server of servers) {
180 await waitUntilLivePublished(server.url, server.accessToken, videoId)
181 }
182 }
183
184 async function checkLiveCleanup (server: ServerInfo, videoUUID: string, resolutions: number[] = []) {
185 const basePath = buildServerDirectory(server, 'streaming-playlists')
186 const hlsPath = join(basePath, 'hls', videoUUID)
187
188 if (resolutions.length === 0) {
189 const result = await pathExists(hlsPath)
190 expect(result).to.be.false
191
192 return
193 }
194
195 const files = await readdir(hlsPath)
196
197 // fragmented file and playlist per resolution + master playlist + segments sha256 json file
198 expect(files).to.have.lengthOf(resolutions.length * 2 + 2)
199
200 for (const resolution of resolutions) {
201 expect(files).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`)
202 expect(files).to.contain(`${resolution}.m3u8`)
203 }
204
205 expect(files).to.contain('master.m3u8')
206 expect(files).to.contain('segments-sha256.json')
207 }
208
209 async function getPlaylistsCount (server: ServerInfo, videoUUID: string) {
210 const basePath = buildServerDirectory(server, 'streaming-playlists')
211 const hlsPath = join(basePath, 'hls', videoUUID)
212
213 const files = await readdir(hlsPath)
214
215 return files.filter(f => f.endsWith('.m3u8')).length
216 }
217
218 // ---------------------------------------------------------------------------
219
220 export {
221 getLive,
222 getPlaylistsCount,
223 waitUntilLiveSaved,
224 waitUntilLivePublished,
225 updateLive,
226 createLive,
227 runAndTestFfmpegStreamError,
228 checkLiveCleanup,
229 waitUntilLiveSegmentGeneration,
230 stopFfmpeg,
231 waitUntilLiveWaiting,
232 sendRTMPStreamInVideo,
233 waitUntilLiveEnded,
234 waitFfmpegUntilError,
235 waitUntilLivePublishedOnAllServers,
236 sendRTMPStream,
237 testFfmpegStreamError
238 }