]>
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 |
bbae45c3 | 195 | objectStorage: boolean |
8059e050 | 196 | objectStorageBaseUrl?: string |
4f219914 | 197 | }) { |
8059e050 C |
198 | const { |
199 | server, | |
200 | objectStorage, | |
201 | playlistNumber, | |
202 | segment, | |
203 | videoUUID, | |
204 | objectStorageBaseUrl = ObjectStorageCommand.getMockPlaylistBaseUrl() | |
205 | } = options | |
53023be3 | 206 | |
34aa316f | 207 | const segmentName = `${playlistNumber}-00000${segment}.ts` |
bbae45c3 | 208 | const baseUrl = objectStorage |
8059e050 | 209 | ? join(objectStorageBaseUrl, 'hls') |
bbae45c3 C |
210 | : server.url + '/static/streaming-playlists/hls' |
211 | ||
212 | let error = true | |
213 | ||
214 | while (error) { | |
215 | try { | |
216 | await this.getRawRequest({ | |
217 | ...options, | |
218 | ||
219 | url: `${baseUrl}/${videoUUID}/${segmentName}`, | |
220 | implicitToken: false, | |
221 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
222 | }) | |
223 | ||
dd84f4f2 C |
224 | const video = await server.videos.get({ id: videoUUID }) |
225 | const hlsPlaylist = video.streamingPlaylists[0] | |
226 | ||
5170f492 | 227 | const shaBody = await server.streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry: objectStorage }) |
dd84f4f2 C |
228 | |
229 | if (!shaBody[segmentName]) { | |
230 | throw new Error('Segment SHA does not exist') | |
231 | } | |
232 | ||
bbae45c3 C |
233 | error = false |
234 | } catch { | |
235 | error = true | |
236 | await wait(100) | |
237 | } | |
238 | } | |
34aa316f C |
239 | } |
240 | ||
cfd57d2c C |
241 | async waitUntilReplacedByReplay (options: OverrideCommandOptions & { |
242 | videoId: number | string | |
243 | }) { | |
244 | let video: VideoDetails | |
245 | ||
246 | do { | |
247 | video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId }) | |
248 | ||
249 | await wait(500) | |
250 | } while (video.isLive === true || video.state.id !== VideoState.PUBLISHED) | |
251 | } | |
252 | ||
253 | // --------------------------------------------------------------------------- | |
254 | ||
255 | getSegmentFile (options: OverrideCommandOptions & { | |
53023be3 C |
256 | videoUUID: string |
257 | playlistNumber: number | |
258 | segment: number | |
cfd57d2c | 259 | objectStorage?: boolean // default false |
53023be3 | 260 | }) { |
cfd57d2c | 261 | const { playlistNumber, segment, videoUUID, objectStorage = false } = options |
53023be3 C |
262 | |
263 | const segmentName = `${playlistNumber}-00000${segment}.ts` | |
cfd57d2c | 264 | const baseUrl = objectStorage |
9ab330b9 | 265 | ? ObjectStorageCommand.getMockPlaylistBaseUrl() |
cfd57d2c C |
266 | : `${this.server.url}/static/streaming-playlists/hls` |
267 | ||
268 | const url = `${baseUrl}/${videoUUID}/${segmentName}` | |
53023be3 C |
269 | |
270 | return this.getRawRequest({ | |
271 | ...options, | |
272 | ||
273 | url, | |
274 | implicitToken: false, | |
275 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
276 | }) | |
4f219914 C |
277 | } |
278 | ||
cfd57d2c C |
279 | getPlaylistFile (options: OverrideCommandOptions & { |
280 | videoUUID: string | |
281 | playlistName: string | |
282 | objectStorage?: boolean // default false | |
4f219914 | 283 | }) { |
cfd57d2c | 284 | const { playlistName, videoUUID, objectStorage = false } = options |
4f219914 | 285 | |
cfd57d2c | 286 | const baseUrl = objectStorage |
9ab330b9 | 287 | ? ObjectStorageCommand.getMockPlaylistBaseUrl() |
cfd57d2c | 288 | : `${this.server.url}/static/streaming-playlists/hls` |
4f219914 | 289 | |
cfd57d2c C |
290 | const url = `${baseUrl}/${videoUUID}/${playlistName}` |
291 | ||
292 | return this.getRawRequest({ | |
293 | ...options, | |
294 | ||
295 | url, | |
296 | implicitToken: false, | |
297 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
298 | }) | |
4f219914 C |
299 | } |
300 | ||
cfd57d2c C |
301 | // --------------------------------------------------------------------------- |
302 | ||
04aed767 | 303 | async countPlaylists (options: OverrideCommandOptions & { |
4f219914 C |
304 | videoUUID: string |
305 | }) { | |
89d241a7 | 306 | const basePath = this.server.servers.buildDirectory('streaming-playlists') |
4f219914 C |
307 | const hlsPath = join(basePath, 'hls', options.videoUUID) |
308 | ||
309 | const files = await readdir(hlsPath) | |
310 | ||
311 | return files.filter(f => f.endsWith('.m3u8')).length | |
312 | } | |
313 | ||
04aed767 | 314 | private async waitUntilState (options: OverrideCommandOptions & { |
4f219914 C |
315 | videoId: number | string |
316 | state: VideoState | |
317 | }) { | |
318 | let video: VideoDetails | |
319 | ||
320 | do { | |
89d241a7 | 321 | video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId }) |
4f219914 C |
322 | |
323 | await wait(500) | |
324 | } while (video.state.id !== options.state) | |
325 | } | |
326 | } |