]>
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 | }) { | |
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 | ||
cfd57d2c C |
144 | // --------------------------------------------------------------------------- |
145 | ||
4f219914 C |
146 | async sendRTMPStreamInVideo (options: OverrideCommandOptions & { |
147 | videoId: number | string | |
148 | fixtureName?: string | |
c826f34a | 149 | copyCodecs?: boolean |
4f219914 | 150 | }) { |
c826f34a | 151 | const { videoId, fixtureName, copyCodecs } = options |
04aed767 | 152 | const videoLive = await this.get({ videoId }) |
4f219914 | 153 | |
c826f34a | 154 | return sendRTMPStream({ rtmpBaseUrl: videoLive.rtmpUrl, streamKey: videoLive.streamKey, fixtureName, copyCodecs }) |
4f219914 C |
155 | } |
156 | ||
04aed767 | 157 | async runAndTestStreamError (options: OverrideCommandOptions & { |
4f219914 C |
158 | videoId: number | string |
159 | shouldHaveError: boolean | |
160 | }) { | |
161 | const command = await this.sendRTMPStreamInVideo(options) | |
162 | ||
163 | return testFfmpegStreamError(command, options.shouldHaveError) | |
164 | } | |
165 | ||
cfd57d2c C |
166 | // --------------------------------------------------------------------------- |
167 | ||
04aed767 | 168 | waitUntilPublished (options: OverrideCommandOptions & { |
4f219914 C |
169 | videoId: number | string |
170 | }) { | |
171 | const { videoId } = options | |
04aed767 | 172 | return this.waitUntilState({ videoId, state: VideoState.PUBLISHED }) |
4f219914 C |
173 | } |
174 | ||
04aed767 | 175 | waitUntilWaiting (options: OverrideCommandOptions & { |
4f219914 C |
176 | videoId: number | string |
177 | }) { | |
178 | const { videoId } = options | |
04aed767 | 179 | return this.waitUntilState({ videoId, state: VideoState.WAITING_FOR_LIVE }) |
4f219914 C |
180 | } |
181 | ||
04aed767 | 182 | waitUntilEnded (options: OverrideCommandOptions & { |
4f219914 C |
183 | videoId: number | string |
184 | }) { | |
185 | const { videoId } = options | |
04aed767 | 186 | return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED }) |
4f219914 C |
187 | } |
188 | ||
bbae45c3 C |
189 | async waitUntilSegmentGeneration (options: OverrideCommandOptions & { |
190 | server: PeerTubeServer | |
4f219914 | 191 | videoUUID: string |
53023be3 | 192 | playlistNumber: number |
4f219914 | 193 | segment: number |
bbae45c3 | 194 | objectStorage: boolean |
8059e050 | 195 | objectStorageBaseUrl?: string |
4f219914 | 196 | }) { |
8059e050 C |
197 | const { |
198 | server, | |
199 | objectStorage, | |
200 | playlistNumber, | |
201 | segment, | |
202 | videoUUID, | |
203 | objectStorageBaseUrl = ObjectStorageCommand.getMockPlaylistBaseUrl() | |
204 | } = options | |
53023be3 | 205 | |
34aa316f | 206 | const segmentName = `${playlistNumber}-00000${segment}.ts` |
bbae45c3 | 207 | const baseUrl = objectStorage |
8059e050 | 208 | ? join(objectStorageBaseUrl, 'hls') |
bbae45c3 C |
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 | ||
dd84f4f2 C |
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 | ||
bbae45c3 C |
232 | error = false |
233 | } catch { | |
234 | error = true | |
235 | await wait(100) | |
236 | } | |
237 | } | |
34aa316f C |
238 | } |
239 | ||
cfd57d2c C |
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 & { | |
53023be3 C |
255 | videoUUID: string |
256 | playlistNumber: number | |
257 | segment: number | |
cfd57d2c | 258 | objectStorage?: boolean // default false |
53023be3 | 259 | }) { |
cfd57d2c | 260 | const { playlistNumber, segment, videoUUID, objectStorage = false } = options |
53023be3 C |
261 | |
262 | const segmentName = `${playlistNumber}-00000${segment}.ts` | |
cfd57d2c | 263 | const baseUrl = objectStorage |
9ab330b9 | 264 | ? ObjectStorageCommand.getMockPlaylistBaseUrl() |
cfd57d2c C |
265 | : `${this.server.url}/static/streaming-playlists/hls` |
266 | ||
267 | const url = `${baseUrl}/${videoUUID}/${segmentName}` | |
53023be3 C |
268 | |
269 | return this.getRawRequest({ | |
270 | ...options, | |
271 | ||
272 | url, | |
273 | implicitToken: false, | |
274 | defaultExpectedStatus: HttpStatusCode.OK_200 | |
275 | }) | |
4f219914 C |
276 | } |
277 | ||
cfd57d2c C |
278 | getPlaylistFile (options: OverrideCommandOptions & { |
279 | videoUUID: string | |
280 | playlistName: string | |
281 | objectStorage?: boolean // default false | |
4f219914 | 282 | }) { |
cfd57d2c | 283 | const { playlistName, videoUUID, objectStorage = false } = options |
4f219914 | 284 | |
cfd57d2c | 285 | const baseUrl = objectStorage |
9ab330b9 | 286 | ? ObjectStorageCommand.getMockPlaylistBaseUrl() |
cfd57d2c | 287 | : `${this.server.url}/static/streaming-playlists/hls` |
4f219914 | 288 | |
cfd57d2c C |
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 | }) | |
4f219914 C |
298 | } |
299 | ||
cfd57d2c C |
300 | // --------------------------------------------------------------------------- |
301 | ||
04aed767 | 302 | async countPlaylists (options: OverrideCommandOptions & { |
4f219914 C |
303 | videoUUID: string |
304 | }) { | |
89d241a7 | 305 | const basePath = this.server.servers.buildDirectory('streaming-playlists') |
4f219914 C |
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 | ||
04aed767 | 313 | private async waitUntilState (options: OverrideCommandOptions & { |
4f219914 C |
314 | videoId: number | string |
315 | state: VideoState | |
316 | }) { | |
317 | let video: VideoDetails | |
318 | ||
319 | do { | |
89d241a7 | 320 | video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId }) |
4f219914 C |
321 | |
322 | await wait(500) | |
323 | } while (video.state.id !== options.state) | |
324 | } | |
325 | } |