]>
Commit | Line | Data |
---|---|---|
4f219914 C |
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | ||
3 | import { readdir } from 'fs-extra' | |
4f219914 | 4 | import { join } from 'path' |
bbd5aa7e | 5 | import { omit, wait } from '@shared/core-utils' |
26e3e98f C |
6 | import { |
7 | HttpStatusCode, | |
8 | LiveVideo, | |
9 | LiveVideoCreate, | |
10 | LiveVideoSession, | |
11 | LiveVideoUpdate, | |
12 | ResultList, | |
13 | VideoCreateResult, | |
14 | VideoDetails, | |
3545e72c | 15 | VideoPrivacy, |
26e3e98f C |
16 | VideoState |
17 | } from '@shared/models' | |
4f219914 | 18 | import { unwrapBody } from '../requests' |
bbae45c3 | 19 | import { ObjectStorageCommand, PeerTubeServer } from '../server' |
4f219914 C |
20 | import { AbstractCommand, OverrideCommandOptions } from '../shared' |
21 | import { sendRTMPStream, testFfmpegStreamError } from './live' | |
4f219914 C |
22 | |
23 | export class LiveCommand extends AbstractCommand { | |
24 | ||
04aed767 | 25 | get (options: OverrideCommandOptions & { |
4f219914 C |
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, | |
a1637fa1 | 34 | implicitToken: true, |
4f219914 C |
35 | defaultExpectedStatus: HttpStatusCode.OK_200 |
36 | }) | |
37 | } | |
38 | ||
cfd57d2c C |
39 | // --------------------------------------------------------------------------- |
40 | ||
26e3e98f C |
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 | ||
cfd57d2c C |
77 | // --------------------------------------------------------------------------- |
78 | ||
04aed767 | 79 | update (options: OverrideCommandOptions & { |
4f219914 C |
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, | |
a1637fa1 | 91 | implicitToken: true, |
4f219914 C |
92 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 |
93 | }) | |
94 | } | |
95 | ||
04aed767 | 96 | async create (options: OverrideCommandOptions & { |
4f219914 C |
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, | |
bbd5aa7e | 111 | fields: omit(fields, [ 'thumbnailfile', 'previewfile' ]), |
a1637fa1 | 112 | implicitToken: true, |
4f219914 C |
113 | defaultExpectedStatus: HttpStatusCode.OK_200 |
114 | })) | |
115 | ||
116 | return body.video | |
117 | } | |
118 | ||
3545e72c C |
119 | async quickCreate (options: OverrideCommandOptions & { |
120 | saveReplay: boolean | |
121 | permanentLive: boolean | |
122 | privacy?: VideoPrivacy | |
123 | }) { | |
d102de1b | 124 | const { saveReplay, permanentLive, privacy = VideoPrivacy.PUBLIC } = options |
3545e72c C |
125 | |
126 | const { uuid } = await this.create({ | |
127 | ...options, | |
128 | ||
129 | fields: { | |
130 | name: 'live', | |
131 | permanentLive, | |
132 | saveReplay, | |
05a60d85 | 133 | replaySettings: { privacy }, |
3545e72c C |
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 | ||
cfd57d2c C |
145 | // --------------------------------------------------------------------------- |
146 | ||
4f219914 C |
147 | async sendRTMPStreamInVideo (options: OverrideCommandOptions & { |
148 | videoId: number | string | |
149 | fixtureName?: string | |
c826f34a | 150 | copyCodecs?: boolean |
4f219914 | 151 | }) { |
c826f34a | 152 | const { videoId, fixtureName, copyCodecs } = options |
04aed767 | 153 | const videoLive = await this.get({ videoId }) |
4f219914 | 154 | |
c826f34a | 155 | return sendRTMPStream({ rtmpBaseUrl: videoLive.rtmpUrl, streamKey: videoLive.streamKey, fixtureName, copyCodecs }) |
4f219914 C |
156 | } |
157 | ||
04aed767 | 158 | async runAndTestStreamError (options: OverrideCommandOptions & { |
4f219914 C |
159 | videoId: number | string |
160 | shouldHaveError: boolean | |
161 | }) { | |
162 | const command = await this.sendRTMPStreamInVideo(options) | |
163 | ||
164 | return testFfmpegStreamError(command, options.shouldHaveError) | |
165 | } | |
166 | ||
cfd57d2c C |
167 | // --------------------------------------------------------------------------- |
168 | ||
04aed767 | 169 | waitUntilPublished (options: OverrideCommandOptions & { |
4f219914 C |
170 | videoId: number | string |
171 | }) { | |
172 | const { videoId } = options | |
04aed767 | 173 | return this.waitUntilState({ videoId, state: VideoState.PUBLISHED }) |
4f219914 C |
174 | } |
175 | ||
04aed767 | 176 | waitUntilWaiting (options: OverrideCommandOptions & { |
4f219914 C |
177 | videoId: number | string |
178 | }) { | |
179 | const { videoId } = options | |
04aed767 | 180 | return this.waitUntilState({ videoId, state: VideoState.WAITING_FOR_LIVE }) |
4f219914 C |
181 | } |
182 | ||
04aed767 | 183 | waitUntilEnded (options: OverrideCommandOptions & { |
4f219914 C |
184 | videoId: number | string |
185 | }) { | |
186 | const { videoId } = options | |
04aed767 | 187 | return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED }) |
4f219914 C |
188 | } |
189 | ||
bbae45c3 C |
190 | async waitUntilSegmentGeneration (options: OverrideCommandOptions & { |
191 | server: PeerTubeServer | |
4f219914 | 192 | videoUUID: string |
53023be3 | 193 | playlistNumber: number |
4f219914 | 194 | segment: number |
f8918990 | 195 | objectStorage?: ObjectStorageCommand |
8059e050 | 196 | objectStorageBaseUrl?: string |
4f219914 | 197 | }) { |
8059e050 C |
198 | const { |
199 | server, | |
200 | objectStorage, | |
201 | playlistNumber, | |
202 | segment, | |
203 | videoUUID, | |
f8918990 | 204 | objectStorageBaseUrl |
8059e050 | 205 | } = options |
53023be3 | 206 | |
34aa316f | 207 | const segmentName = `${playlistNumber}-00000${segment}.ts` |
bbae45c3 | 208 | const baseUrl = objectStorage |
f8918990 | 209 | ? join(objectStorageBaseUrl || objectStorage.getMockPlaylistBaseUrl(), 'hls') |
bbae45c3 C |
210 | : server.url + '/static/streaming-playlists/hls' |
211 | ||
212 | let error = true | |
213 | ||
214 | while (error) { | |
215 | try { | |
81f14b91 | 216 | // Check fragment exists |
bbae45c3 C |
217 | await this.getRawRequest({ |
218 | ...options, | |
219 | ||
220 | url: `${baseUrl}/${videoUUID}/${segmentName}`, | |
221 | implicitToken: false, | |
222 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
223 | }) | |
224 | ||
dd84f4f2 C |
225 | const video = await server.videos.get({ id: videoUUID }) |
226 | const hlsPlaylist = video.streamingPlaylists[0] | |
227 | ||
81f14b91 | 228 | // Check SHA generation |
f8918990 | 229 | const shaBody = await server.streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry: !!objectStorage }) |
dd84f4f2 C |
230 | if (!shaBody[segmentName]) { |
231 | throw new Error('Segment SHA does not exist') | |
232 | } | |
233 | ||
81f14b91 C |
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 | ||
bbae45c3 C |
238 | error = false |
239 | } catch { | |
240 | error = true | |
241 | await wait(100) | |
242 | } | |
243 | } | |
34aa316f C |
244 | } |
245 | ||
cfd57d2c C |
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 & { | |
53023be3 C |
261 | videoUUID: string |
262 | playlistNumber: number | |
263 | segment: number | |
f8918990 | 264 | objectStorage?: ObjectStorageCommand |
53023be3 | 265 | }) { |
f8918990 | 266 | const { playlistNumber, segment, videoUUID, objectStorage } = options |
53023be3 C |
267 | |
268 | const segmentName = `${playlistNumber}-00000${segment}.ts` | |
cfd57d2c | 269 | const baseUrl = objectStorage |
f8918990 | 270 | ? objectStorage.getMockPlaylistBaseUrl() |
cfd57d2c C |
271 | : `${this.server.url}/static/streaming-playlists/hls` |
272 | ||
273 | const url = `${baseUrl}/${videoUUID}/${segmentName}` | |
53023be3 C |
274 | |
275 | return this.getRawRequest({ | |
276 | ...options, | |
277 | ||
278 | url, | |
279 | implicitToken: false, | |
280 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
281 | }) | |
4f219914 C |
282 | } |
283 | ||
cfd57d2c C |
284 | getPlaylistFile (options: OverrideCommandOptions & { |
285 | videoUUID: string | |
286 | playlistName: string | |
f8918990 | 287 | objectStorage?: ObjectStorageCommand |
4f219914 | 288 | }) { |
f8918990 | 289 | const { playlistName, videoUUID, objectStorage } = options |
4f219914 | 290 | |
cfd57d2c | 291 | const baseUrl = objectStorage |
f8918990 | 292 | ? objectStorage.getMockPlaylistBaseUrl() |
cfd57d2c | 293 | : `${this.server.url}/static/streaming-playlists/hls` |
4f219914 | 294 | |
cfd57d2c C |
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 | }) | |
4f219914 C |
304 | } |
305 | ||
cfd57d2c C |
306 | // --------------------------------------------------------------------------- |
307 | ||
04aed767 | 308 | async countPlaylists (options: OverrideCommandOptions & { |
4f219914 C |
309 | videoUUID: string |
310 | }) { | |
89d241a7 | 311 | const basePath = this.server.servers.buildDirectory('streaming-playlists') |
4f219914 C |
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 | ||
04aed767 | 319 | private async waitUntilState (options: OverrideCommandOptions & { |
4f219914 C |
320 | videoId: number | string |
321 | state: VideoState | |
322 | }) { | |
323 | let video: VideoDetails | |
324 | ||
325 | do { | |
89d241a7 | 326 | video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId }) |
4f219914 C |
327 | |
328 | await wait(500) | |
329 | } while (video.state.id !== options.state) | |
330 | } | |
331 | } |