]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - shared/extra-utils/videos/live.ts
Remove unused class in download modal
[github/Chocobozzz/PeerTube.git] / shared / extra-utils / videos / live.ts
CommitLineData
68e70a74
C
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
77e9f859 4import * as ffmpeg from 'fluent-ffmpeg'
68e70a74 5import { pathExists, readdir } from 'fs-extra'
97969c4e 6import { omit } from 'lodash'
68e70a74 7import { join } from 'path'
97969c4e 8import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models'
0d8de275 9import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
68e70a74 10import { buildAbsoluteFixturePath, buildServerDirectory, wait } from '../miscs/miscs'
77e9f859 11import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
0d8de275 12import { ServerInfo, waitUntilLog } from '../server/servers'
af4ae64f 13import { getVideoWithToken } from './videos'
77e9f859 14
2d53be02 15function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = HttpStatusCode.OK_200) {
77e9f859
C
16 const path = '/api/v1/videos/live'
17
18 return makeGetRequest({
19 url,
20 token,
21 path: path + '/' + videoId,
22 statusCodeExpected
23 })
24}
25
2d53be02
RK
26function updateLive (
27 url: string,
28 token: string,
29 videoId: number | string,
30 fields: LiveVideoUpdate,
31 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
32) {
77e9f859
C
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
2d53be02 44function createLive (url: string, token: string, fields: LiveVideoCreate, statusCodeExpected = HttpStatusCode.OK_200) {
77e9f859
C
45 const path = '/api/v1/videos/live'
46
af4ae64f
C
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')
77e9f859
C
52
53 return makeUploadRequest({
54 url,
55 path,
56 token,
57 attaches,
af4ae64f 58 fields: updatedFields,
77e9f859
C
59 statusCodeExpected
60 })
61}
62
ca5c612b 63async function sendRTMPStreamInVideo (url: string, token: string, videoId: number | string, fixtureName?: string) {
97969c4e
C
64 const res = await getLive(url, token, videoId)
65 const videoLive = res.body as LiveVideo
66
ca5c612b 67 return sendRTMPStream(videoLive.rtmpUrl, videoLive.streamKey, fixtureName)
97969c4e
C
68}
69
ca5c612b
C
70function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = 'video_short.mp4') {
71 const fixture = buildAbsoluteFixturePath(fixtureName)
77e9f859
C
72
73 const command = ffmpeg(fixture)
74 command.inputOption('-stream_loop -1')
75 command.inputOption('-re')
68e70a74
C
76 command.outputOption('-c:v libx264')
77 command.outputOption('-g 50')
78 command.outputOption('-keyint_min 2')
884d2c39 79 command.outputOption('-r 60')
77e9f859
C
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
68e70a74 88 if (process.env.DEBUG) console.error(err)
77e9f859
C
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
97969c4e 100function waitFfmpegUntilError (command: ffmpeg.FfmpegCommand, successAfterMS = 10000) {
ba5a8d89 101 return new Promise<void>((res, rej) => {
97969c4e
C
102 command.on('error', err => {
103 return rej(err)
104 })
105
106 setTimeout(() => {
107 res()
108 }, successAfterMS)
109 })
110}
111
68e70a74 112async function runAndTestFfmpegStreamError (url: string, token: string, videoId: number | string, shouldHaveError: boolean) {
97969c4e 113 const command = await sendRTMPStreamInVideo(url, token, videoId)
68e70a74
C
114
115 return testFfmpegStreamError(command, shouldHaveError)
116}
117
118async function testFfmpegStreamError (command: ffmpeg.FfmpegCommand, shouldHaveError: boolean) {
97969c4e
C
119 let error: Error
120
121 try {
59fd824c 122 await waitFfmpegUntilError(command, 25000)
97969c4e
C
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
77e9f859
C
133async function stopFfmpeg (command: ffmpeg.FfmpegCommand) {
134 command.kill('SIGINT')
135
136 await wait(500)
137}
138
6b67897e 139function waitUntilLivePublished (url: string, token: string, videoId: number | string) {
0d8de275 140 return waitUntilLiveState(url, token, videoId, VideoState.PUBLISHED)
6b67897e
C
141}
142
59fd824c
C
143function waitUntilLiveWaiting (url: string, token: string, videoId: number | string) {
144 return waitUntilLiveState(url, token, videoId, VideoState.WAITING_FOR_LIVE)
145}
146
0e856b78 147function waitUntilLiveEnded (url: string, token: string, videoId: number | string) {
0d8de275
C
148 return waitUntilLiveState(url, token, videoId, VideoState.LIVE_ENDED)
149}
150
151function waitUntilLiveSegmentGeneration (server: ServerInfo, videoUUID: string, resolutionNum: number, segmentNum: number) {
152 const segmentName = `${resolutionNum}-00000${segmentNum}.ts`
153 return waitUntilLog(server, `${videoUUID}/${segmentName}`, 2, false)
0e856b78
C
154}
155
0d8de275 156async function waitUntilLiveState (url: string, token: string, videoId: number | string, state: VideoState) {
77e9f859
C
157 let video: VideoDetails
158
159 do {
160 const res = await getVideoWithToken(url, token, videoId)
161 video = res.body
162
163 await wait(500)
0d8de275 164 } while (video.state.id !== state)
77e9f859
C
165}
166
94d721ef
C
167async 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
68e70a74 178async function checkLiveCleanup (server: ServerInfo, videoUUID: string, resolutions: number[] = []) {
ca5c612b 179 const basePath = buildServerDirectory(server, 'streaming-playlists')
68e70a74
C
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
bb4ba6d9
C
203async 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
77e9f859
C
212// ---------------------------------------------------------------------------
213
214export {
215 getLive,
bb4ba6d9 216 getPlaylistsCount,
94d721ef 217 waitUntilLiveSaved,
6b67897e 218 waitUntilLivePublished,
77e9f859 219 updateLive,
77e9f859 220 createLive,
68e70a74
C
221 runAndTestFfmpegStreamError,
222 checkLiveCleanup,
0d8de275 223 waitUntilLiveSegmentGeneration,
77e9f859 224 stopFfmpeg,
59fd824c 225 waitUntilLiveWaiting,
97969c4e 226 sendRTMPStreamInVideo,
0e856b78 227 waitUntilLiveEnded,
97969c4e 228 waitFfmpegUntilError,
68e70a74
C
229 sendRTMPStream,
230 testFfmpegStreamError
77e9f859 231}