]>
Commit | Line | Data |
---|---|---|
68e70a74 C |
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | ||
3 | import { expect } from 'chai' | |
77e9f859 | 4 | import * as ffmpeg from 'fluent-ffmpeg' |
68e70a74 | 5 | import { pathExists, readdir } from 'fs-extra' |
97969c4e | 6 | import { omit } from 'lodash' |
68e70a74 | 7 | import { join } from 'path' |
97969c4e | 8 | import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models' |
0d8de275 | 9 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
68e70a74 | 10 | import { buildAbsoluteFixturePath, buildServerDirectory, wait } from '../miscs/miscs' |
77e9f859 | 11 | import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests' |
0d8de275 | 12 | import { ServerInfo, waitUntilLog } from '../server/servers' |
af4ae64f | 13 | import { getVideoWithToken } from './videos' |
77e9f859 | 14 | |
2d53be02 | 15 | function 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 |
26 | function 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 | 44 | function 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 | 63 | async 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 |
70 | function 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 | 100 | function 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 | 112 | async 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 | ||
118 | async 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 |
133 | async function stopFfmpeg (command: ffmpeg.FfmpegCommand) { |
134 | command.kill('SIGINT') | |
135 | ||
136 | await wait(500) | |
137 | } | |
138 | ||
6b67897e | 139 | function waitUntilLivePublished (url: string, token: string, videoId: number | string) { |
0d8de275 | 140 | return waitUntilLiveState(url, token, videoId, VideoState.PUBLISHED) |
6b67897e C |
141 | } |
142 | ||
59fd824c C |
143 | function waitUntilLiveWaiting (url: string, token: string, videoId: number | string) { |
144 | return waitUntilLiveState(url, token, videoId, VideoState.WAITING_FOR_LIVE) | |
145 | } | |
146 | ||
0e856b78 | 147 | function waitUntilLiveEnded (url: string, token: string, videoId: number | string) { |
0d8de275 C |
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) | |
0e856b78 C |
154 | } |
155 | ||
0d8de275 | 156 | async 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 |
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 | ||
68e70a74 | 178 | async 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 |
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 | ||
77e9f859 C |
212 | // --------------------------------------------------------------------------- |
213 | ||
214 | export { | |
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 | } |