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