]>
Commit | Line | Data |
---|---|---|
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | |
2 | ||
3 | import { readdir } from 'fs-extra' | |
4 | import { join } from 'path' | |
5 | import { omit, wait } from '@shared/core-utils' | |
6 | import { | |
7 | HttpStatusCode, | |
8 | LiveVideo, | |
9 | LiveVideoCreate, | |
10 | LiveVideoSession, | |
11 | LiveVideoUpdate, | |
12 | ResultList, | |
13 | VideoCreateResult, | |
14 | VideoDetails, | |
15 | VideoPrivacy, | |
16 | VideoState | |
17 | } from '@shared/models' | |
18 | import { unwrapBody } from '../requests' | |
19 | import { ObjectStorageCommand, PeerTubeServer } from '../server' | |
20 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | |
21 | import { sendRTMPStream, testFfmpegStreamError } from './live' | |
22 | ||
23 | export class LiveCommand extends AbstractCommand { | |
24 | ||
25 | get (options: OverrideCommandOptions & { | |
26 | videoId: number | string | |
27 | }) { | |
28 | const path = '/api/v1/videos/live' | |
29 | ||
30 | return this.getRequestBody<LiveVideo>({ | |
31 | ...options, | |
32 | ||
33 | path: path + '/' + options.videoId, | |
34 | implicitToken: true, | |
35 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
36 | }) | |
37 | } | |
38 | ||
39 | // --------------------------------------------------------------------------- | |
40 | ||
41 | listSessions (options: OverrideCommandOptions & { | |
42 | videoId: number | string | |
43 | }) { | |
44 | const path = `/api/v1/videos/live/${options.videoId}/sessions` | |
45 | ||
46 | return this.getRequestBody<ResultList<LiveVideoSession>>({ | |
47 | ...options, | |
48 | ||
49 | path, | |
50 | implicitToken: true, | |
51 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
52 | }) | |
53 | } | |
54 | ||
55 | async findLatestSession (options: OverrideCommandOptions & { | |
56 | videoId: number | string | |
57 | }) { | |
58 | const { data: sessions } = await this.listSessions(options) | |
59 | ||
60 | return sessions[sessions.length - 1] | |
61 | } | |
62 | ||
63 | getReplaySession (options: OverrideCommandOptions & { | |
64 | videoId: number | string | |
65 | }) { | |
66 | const path = `/api/v1/videos/${options.videoId}/live-session` | |
67 | ||
68 | return this.getRequestBody<LiveVideoSession>({ | |
69 | ...options, | |
70 | ||
71 | path, | |
72 | implicitToken: true, | |
73 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
74 | }) | |
75 | } | |
76 | ||
77 | // --------------------------------------------------------------------------- | |
78 | ||
79 | update (options: OverrideCommandOptions & { | |
80 | videoId: number | string | |
81 | fields: LiveVideoUpdate | |
82 | }) { | |
83 | const { videoId, fields } = options | |
84 | const path = '/api/v1/videos/live' | |
85 | ||
86 | return this.putBodyRequest({ | |
87 | ...options, | |
88 | ||
89 | path: path + '/' + videoId, | |
90 | fields, | |
91 | implicitToken: true, | |
92 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | |
93 | }) | |
94 | } | |
95 | ||
96 | async create (options: OverrideCommandOptions & { | |
97 | fields: LiveVideoCreate | |
98 | }) { | |
99 | const { fields } = options | |
100 | const path = '/api/v1/videos/live' | |
101 | ||
102 | const attaches: any = {} | |
103 | if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile | |
104 | if (fields.previewfile) attaches.previewfile = fields.previewfile | |
105 | ||
106 | const body = await unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({ | |
107 | ...options, | |
108 | ||
109 | path, | |
110 | attaches, | |
111 | fields: omit(fields, [ 'thumbnailfile', 'previewfile' ]), | |
112 | implicitToken: true, | |
113 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
114 | })) | |
115 | ||
116 | return body.video | |
117 | } | |
118 | ||
119 | async quickCreate (options: OverrideCommandOptions & { | |
120 | saveReplay: boolean | |
121 | permanentLive: boolean | |
122 | privacy?: VideoPrivacy | |
123 | }) { | |
124 | const { saveReplay, permanentLive, privacy = VideoPrivacy.PUBLIC } = options | |
125 | ||
126 | const { uuid } = await this.create({ | |
127 | ...options, | |
128 | ||
129 | fields: { | |
130 | name: 'live', | |
131 | permanentLive, | |
132 | saveReplay, | |
133 | replaySettings: { privacy }, | |
134 | channelId: this.server.store.channel.id, | |
135 | privacy | |
136 | } | |
137 | }) | |
138 | ||
139 | const video = await this.server.videos.getWithToken({ id: uuid }) | |
140 | const live = await this.get({ videoId: uuid }) | |
141 | ||
142 | return { video, live } | |
143 | } | |
144 | ||
145 | // --------------------------------------------------------------------------- | |
146 | ||
147 | async sendRTMPStreamInVideo (options: OverrideCommandOptions & { | |
148 | videoId: number | string | |
149 | fixtureName?: string | |
150 | copyCodecs?: boolean | |
151 | }) { | |
152 | const { videoId, fixtureName, copyCodecs } = options | |
153 | const videoLive = await this.get({ videoId }) | |
154 | ||
155 | return sendRTMPStream({ rtmpBaseUrl: videoLive.rtmpUrl, streamKey: videoLive.streamKey, fixtureName, copyCodecs }) | |
156 | } | |
157 | ||
158 | async runAndTestStreamError (options: OverrideCommandOptions & { | |
159 | videoId: number | string | |
160 | shouldHaveError: boolean | |
161 | }) { | |
162 | const command = await this.sendRTMPStreamInVideo(options) | |
163 | ||
164 | return testFfmpegStreamError(command, options.shouldHaveError) | |
165 | } | |
166 | ||
167 | // --------------------------------------------------------------------------- | |
168 | ||
169 | waitUntilPublished (options: OverrideCommandOptions & { | |
170 | videoId: number | string | |
171 | }) { | |
172 | const { videoId } = options | |
173 | return this.waitUntilState({ videoId, state: VideoState.PUBLISHED }) | |
174 | } | |
175 | ||
176 | waitUntilWaiting (options: OverrideCommandOptions & { | |
177 | videoId: number | string | |
178 | }) { | |
179 | const { videoId } = options | |
180 | return this.waitUntilState({ videoId, state: VideoState.WAITING_FOR_LIVE }) | |
181 | } | |
182 | ||
183 | waitUntilEnded (options: OverrideCommandOptions & { | |
184 | videoId: number | string | |
185 | }) { | |
186 | const { videoId } = options | |
187 | return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED }) | |
188 | } | |
189 | ||
190 | async waitUntilSegmentGeneration (options: OverrideCommandOptions & { | |
191 | server: PeerTubeServer | |
192 | videoUUID: string | |
193 | playlistNumber: number | |
194 | segment: number | |
195 | objectStorage?: ObjectStorageCommand | |
196 | objectStorageBaseUrl?: string | |
197 | }) { | |
198 | const { | |
199 | server, | |
200 | objectStorage, | |
201 | playlistNumber, | |
202 | segment, | |
203 | videoUUID, | |
204 | objectStorageBaseUrl | |
205 | } = options | |
206 | ||
207 | const segmentName = `${playlistNumber}-00000${segment}.ts` | |
208 | const baseUrl = objectStorage | |
209 | ? join(objectStorageBaseUrl || objectStorage.getMockPlaylistBaseUrl(), 'hls') | |
210 | : server.url + '/static/streaming-playlists/hls' | |
211 | ||
212 | let error = true | |
213 | ||
214 | while (error) { | |
215 | try { | |
216 | // Check fragment exists | |
217 | await this.getRawRequest({ | |
218 | ...options, | |
219 | ||
220 | url: `${baseUrl}/${videoUUID}/${segmentName}`, | |
221 | implicitToken: false, | |
222 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
223 | }) | |
224 | ||
225 | const video = await server.videos.get({ id: videoUUID }) | |
226 | const hlsPlaylist = video.streamingPlaylists[0] | |
227 | ||
228 | // Check SHA generation | |
229 | const shaBody = await server.streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry: !!objectStorage }) | |
230 | if (!shaBody[segmentName]) { | |
231 | throw new Error('Segment SHA does not exist') | |
232 | } | |
233 | ||
234 | // Check fragment is in m3u8 playlist | |
235 | const subPlaylist = await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/${playlistNumber}.m3u8` }) | |
236 | if (!subPlaylist.includes(segmentName)) throw new Error('Fragment does not exist in playlist') | |
237 | ||
238 | error = false | |
239 | } catch { | |
240 | error = true | |
241 | await wait(100) | |
242 | } | |
243 | } | |
244 | } | |
245 | ||
246 | async waitUntilReplacedByReplay (options: OverrideCommandOptions & { | |
247 | videoId: number | string | |
248 | }) { | |
249 | let video: VideoDetails | |
250 | ||
251 | do { | |
252 | video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId }) | |
253 | ||
254 | await wait(500) | |
255 | } while (video.isLive === true || video.state.id !== VideoState.PUBLISHED) | |
256 | } | |
257 | ||
258 | // --------------------------------------------------------------------------- | |
259 | ||
260 | getSegmentFile (options: OverrideCommandOptions & { | |
261 | videoUUID: string | |
262 | playlistNumber: number | |
263 | segment: number | |
264 | objectStorage?: ObjectStorageCommand | |
265 | }) { | |
266 | const { playlistNumber, segment, videoUUID, objectStorage } = options | |
267 | ||
268 | const segmentName = `${playlistNumber}-00000${segment}.ts` | |
269 | const baseUrl = objectStorage | |
270 | ? objectStorage.getMockPlaylistBaseUrl() | |
271 | : `${this.server.url}/static/streaming-playlists/hls` | |
272 | ||
273 | const url = `${baseUrl}/${videoUUID}/${segmentName}` | |
274 | ||
275 | return this.getRawRequest({ | |
276 | ...options, | |
277 | ||
278 | url, | |
279 | implicitToken: false, | |
280 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
281 | }) | |
282 | } | |
283 | ||
284 | getPlaylistFile (options: OverrideCommandOptions & { | |
285 | videoUUID: string | |
286 | playlistName: string | |
287 | objectStorage?: ObjectStorageCommand | |
288 | }) { | |
289 | const { playlistName, videoUUID, objectStorage } = options | |
290 | ||
291 | const baseUrl = objectStorage | |
292 | ? objectStorage.getMockPlaylistBaseUrl() | |
293 | : `${this.server.url}/static/streaming-playlists/hls` | |
294 | ||
295 | const url = `${baseUrl}/${videoUUID}/${playlistName}` | |
296 | ||
297 | return this.getRawRequest({ | |
298 | ...options, | |
299 | ||
300 | url, | |
301 | implicitToken: false, | |
302 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
303 | }) | |
304 | } | |
305 | ||
306 | // --------------------------------------------------------------------------- | |
307 | ||
308 | async countPlaylists (options: OverrideCommandOptions & { | |
309 | videoUUID: string | |
310 | }) { | |
311 | const basePath = this.server.servers.buildDirectory('streaming-playlists') | |
312 | const hlsPath = join(basePath, 'hls', options.videoUUID) | |
313 | ||
314 | const files = await readdir(hlsPath) | |
315 | ||
316 | return files.filter(f => f.endsWith('.m3u8')).length | |
317 | } | |
318 | ||
319 | private async waitUntilState (options: OverrideCommandOptions & { | |
320 | videoId: number | string | |
321 | state: VideoState | |
322 | }) { | |
323 | let video: VideoDetails | |
324 | ||
325 | do { | |
326 | video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId }) | |
327 | ||
328 | await wait(500) | |
329 | } while (video.state.id !== options.state) | |
330 | } | |
331 | } |