]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - shared/extra-utils/videos/live.ts
Allow user to search through their watch history (#3576)
[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((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, 25000)
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 checkLiveCleanup (server: ServerInfo, videoUUID: string, resolutions: number[] = []) {
168 const basePath = buildServerDirectory(server, 'streaming-playlists')
169 const hlsPath = join(basePath, 'hls', videoUUID)
170
171 if (resolutions.length === 0) {
172 const result = await pathExists(hlsPath)
173 expect(result).to.be.false
174
175 return
176 }
177
178 const files = await readdir(hlsPath)
179
180 // fragmented file and playlist per resolution + master playlist + segments sha256 json file
181 expect(files).to.have.lengthOf(resolutions.length * 2 + 2)
182
183 for (const resolution of resolutions) {
184 expect(files).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`)
185 expect(files).to.contain(`${resolution}.m3u8`)
186 }
187
188 expect(files).to.contain('master.m3u8')
189 expect(files).to.contain('segments-sha256.json')
190 }
191
192 async function getPlaylistsCount (server: ServerInfo, videoUUID: string) {
193 const basePath = buildServerDirectory(server, 'streaming-playlists')
194 const hlsPath = join(basePath, 'hls', videoUUID)
195
196 const files = await readdir(hlsPath)
197
198 return files.filter(f => f.endsWith('.m3u8')).length
199 }
200
201 // ---------------------------------------------------------------------------
202
203 export {
204 getLive,
205 getPlaylistsCount,
206 waitUntilLivePublished,
207 updateLive,
208 createLive,
209 runAndTestFfmpegStreamError,
210 checkLiveCleanup,
211 waitUntilLiveSegmentGeneration,
212 stopFfmpeg,
213 waitUntilLiveWaiting,
214 sendRTMPStreamInVideo,
215 waitUntilLiveEnded,
216 waitFfmpegUntilError,
217 sendRTMPStream,
218 testFfmpegStreamError
219 }