]>
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 } = options | |
125 | ||
126 | const { uuid } = await this.create({ | |
127 | ...options, | |
128 | ||
129 | fields: { | |
130 | name: 'live', | |
131 | permanentLive, | |
132 | saveReplay, | |
133 | channelId: this.server.store.channel.id, | |
134 | privacy | |
135 | } | |
136 | }) | |
137 | ||
138 | const video = await this.server.videos.getWithToken({ id: uuid }) | |
139 | const live = await this.get({ videoId: uuid }) | |
140 | ||
141 | return { video, live } | |
142 | } | |
143 | ||
144 | // --------------------------------------------------------------------------- | |
145 | ||
146 | async sendRTMPStreamInVideo (options: OverrideCommandOptions & { | |
147 | videoId: number | string | |
148 | fixtureName?: string | |
149 | copyCodecs?: boolean | |
150 | }) { | |
151 | const { videoId, fixtureName, copyCodecs } = options | |
152 | const videoLive = await this.get({ videoId }) | |
153 | ||
154 | return sendRTMPStream({ rtmpBaseUrl: videoLive.rtmpUrl, streamKey: videoLive.streamKey, fixtureName, copyCodecs }) | |
155 | } | |
156 | ||
157 | async runAndTestStreamError (options: OverrideCommandOptions & { | |
158 | videoId: number | string | |
159 | shouldHaveError: boolean | |
160 | }) { | |
161 | const command = await this.sendRTMPStreamInVideo(options) | |
162 | ||
163 | return testFfmpegStreamError(command, options.shouldHaveError) | |
164 | } | |
165 | ||
166 | // --------------------------------------------------------------------------- | |
167 | ||
168 | waitUntilPublished (options: OverrideCommandOptions & { | |
169 | videoId: number | string | |
170 | }) { | |
171 | const { videoId } = options | |
172 | return this.waitUntilState({ videoId, state: VideoState.PUBLISHED }) | |
173 | } | |
174 | ||
175 | waitUntilWaiting (options: OverrideCommandOptions & { | |
176 | videoId: number | string | |
177 | }) { | |
178 | const { videoId } = options | |
179 | return this.waitUntilState({ videoId, state: VideoState.WAITING_FOR_LIVE }) | |
180 | } | |
181 | ||
182 | waitUntilEnded (options: OverrideCommandOptions & { | |
183 | videoId: number | string | |
184 | }) { | |
185 | const { videoId } = options | |
186 | return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED }) | |
187 | } | |
188 | ||
189 | async waitUntilSegmentGeneration (options: OverrideCommandOptions & { | |
190 | server: PeerTubeServer | |
191 | videoUUID: string | |
192 | playlistNumber: number | |
193 | segment: number | |
194 | objectStorage: boolean | |
195 | objectStorageBaseUrl?: string | |
196 | }) { | |
197 | const { | |
198 | server, | |
199 | objectStorage, | |
200 | playlistNumber, | |
201 | segment, | |
202 | videoUUID, | |
203 | objectStorageBaseUrl = ObjectStorageCommand.getMockPlaylistBaseUrl() | |
204 | } = options | |
205 | ||
206 | const segmentName = `${playlistNumber}-00000${segment}.ts` | |
207 | const baseUrl = objectStorage | |
208 | ? join(objectStorageBaseUrl, 'hls') | |
209 | : server.url + '/static/streaming-playlists/hls' | |
210 | ||
211 | let error = true | |
212 | ||
213 | while (error) { | |
214 | try { | |
215 | await this.getRawRequest({ | |
216 | ...options, | |
217 | ||
218 | url: `${baseUrl}/${videoUUID}/${segmentName}`, | |
219 | implicitToken: false, | |
220 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
221 | }) | |
222 | ||
223 | const video = await server.videos.get({ id: videoUUID }) | |
224 | const hlsPlaylist = video.streamingPlaylists[0] | |
225 | ||
226 | const shaBody = await server.streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url }) | |
227 | ||
228 | if (!shaBody[segmentName]) { | |
229 | throw new Error('Segment SHA does not exist') | |
230 | } | |
231 | ||
232 | error = false | |
233 | } catch { | |
234 | error = true | |
235 | await wait(100) | |
236 | } | |
237 | } | |
238 | } | |
239 | ||
240 | async waitUntilReplacedByReplay (options: OverrideCommandOptions & { | |
241 | videoId: number | string | |
242 | }) { | |
243 | let video: VideoDetails | |
244 | ||
245 | do { | |
246 | video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId }) | |
247 | ||
248 | await wait(500) | |
249 | } while (video.isLive === true || video.state.id !== VideoState.PUBLISHED) | |
250 | } | |
251 | ||
252 | // --------------------------------------------------------------------------- | |
253 | ||
254 | getSegmentFile (options: OverrideCommandOptions & { | |
255 | videoUUID: string | |
256 | playlistNumber: number | |
257 | segment: number | |
258 | objectStorage?: boolean // default false | |
259 | }) { | |
260 | const { playlistNumber, segment, videoUUID, objectStorage = false } = options | |
261 | ||
262 | const segmentName = `${playlistNumber}-00000${segment}.ts` | |
263 | const baseUrl = objectStorage | |
264 | ? ObjectStorageCommand.getMockPlaylistBaseUrl() | |
265 | : `${this.server.url}/static/streaming-playlists/hls` | |
266 | ||
267 | const url = `${baseUrl}/${videoUUID}/${segmentName}` | |
268 | ||
269 | return this.getRawRequest({ | |
270 | ...options, | |
271 | ||
272 | url, | |
273 | implicitToken: false, | |
274 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
275 | }) | |
276 | } | |
277 | ||
278 | getPlaylistFile (options: OverrideCommandOptions & { | |
279 | videoUUID: string | |
280 | playlistName: string | |
281 | objectStorage?: boolean // default false | |
282 | }) { | |
283 | const { playlistName, videoUUID, objectStorage = false } = options | |
284 | ||
285 | const baseUrl = objectStorage | |
286 | ? ObjectStorageCommand.getMockPlaylistBaseUrl() | |
287 | : `${this.server.url}/static/streaming-playlists/hls` | |
288 | ||
289 | const url = `${baseUrl}/${videoUUID}/${playlistName}` | |
290 | ||
291 | return this.getRawRequest({ | |
292 | ...options, | |
293 | ||
294 | url, | |
295 | implicitToken: false, | |
296 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
297 | }) | |
298 | } | |
299 | ||
300 | // --------------------------------------------------------------------------- | |
301 | ||
302 | async countPlaylists (options: OverrideCommandOptions & { | |
303 | videoUUID: string | |
304 | }) { | |
305 | const basePath = this.server.servers.buildDirectory('streaming-playlists') | |
306 | const hlsPath = join(basePath, 'hls', options.videoUUID) | |
307 | ||
308 | const files = await readdir(hlsPath) | |
309 | ||
310 | return files.filter(f => f.endsWith('.m3u8')).length | |
311 | } | |
312 | ||
313 | private async waitUntilState (options: OverrideCommandOptions & { | |
314 | videoId: number | string | |
315 | state: VideoState | |
316 | }) { | |
317 | let video: VideoDetails | |
318 | ||
319 | do { | |
320 | video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId }) | |
321 | ||
322 | await wait(500) | |
323 | } while (video.state.id !== options.state) | |
324 | } | |
325 | } |