aboutsummaryrefslogtreecommitdiffhomepage
path: root/packages/server-commands/src/videos/live-command.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/server-commands/src/videos/live-command.ts')
-rw-r--r--packages/server-commands/src/videos/live-command.ts339
1 files changed, 339 insertions, 0 deletions
diff --git a/packages/server-commands/src/videos/live-command.ts b/packages/server-commands/src/videos/live-command.ts
new file mode 100644
index 000000000..793b64f40
--- /dev/null
+++ b/packages/server-commands/src/videos/live-command.ts
@@ -0,0 +1,339 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { readdir } from 'fs/promises'
4import { join } from 'path'
5import { omit, wait } from '@peertube/peertube-core-utils'
6import {
7 HttpStatusCode,
8 LiveVideo,
9 LiveVideoCreate,
10 LiveVideoSession,
11 LiveVideoUpdate,
12 ResultList,
13 VideoCreateResult,
14 VideoDetails,
15 VideoPrivacy,
16 VideoPrivacyType,
17 VideoState,
18 VideoStateType
19} from '@peertube/peertube-models'
20import { unwrapBody } from '../requests/index.js'
21import { ObjectStorageCommand, PeerTubeServer } from '../server/index.js'
22import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
23import { sendRTMPStream, testFfmpegStreamError } from './live.js'
24
25export class LiveCommand extends AbstractCommand {
26
27 get (options: OverrideCommandOptions & {
28 videoId: number | string
29 }) {
30 const path = '/api/v1/videos/live'
31
32 return this.getRequestBody<LiveVideo>({
33 ...options,
34
35 path: path + '/' + options.videoId,
36 implicitToken: true,
37 defaultExpectedStatus: HttpStatusCode.OK_200
38 })
39 }
40
41 // ---------------------------------------------------------------------------
42
43 listSessions (options: OverrideCommandOptions & {
44 videoId: number | string
45 }) {
46 const path = `/api/v1/videos/live/${options.videoId}/sessions`
47
48 return this.getRequestBody<ResultList<LiveVideoSession>>({
49 ...options,
50
51 path,
52 implicitToken: true,
53 defaultExpectedStatus: HttpStatusCode.OK_200
54 })
55 }
56
57 async findLatestSession (options: OverrideCommandOptions & {
58 videoId: number | string
59 }) {
60 const { data: sessions } = await this.listSessions(options)
61
62 return sessions[sessions.length - 1]
63 }
64
65 getReplaySession (options: OverrideCommandOptions & {
66 videoId: number | string
67 }) {
68 const path = `/api/v1/videos/${options.videoId}/live-session`
69
70 return this.getRequestBody<LiveVideoSession>({
71 ...options,
72
73 path,
74 implicitToken: true,
75 defaultExpectedStatus: HttpStatusCode.OK_200
76 })
77 }
78
79 // ---------------------------------------------------------------------------
80
81 update (options: OverrideCommandOptions & {
82 videoId: number | string
83 fields: LiveVideoUpdate
84 }) {
85 const { videoId, fields } = options
86 const path = '/api/v1/videos/live'
87
88 return this.putBodyRequest({
89 ...options,
90
91 path: path + '/' + videoId,
92 fields,
93 implicitToken: true,
94 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
95 })
96 }
97
98 async create (options: OverrideCommandOptions & {
99 fields: LiveVideoCreate
100 }) {
101 const { fields } = options
102 const path = '/api/v1/videos/live'
103
104 const attaches: any = {}
105 if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
106 if (fields.previewfile) attaches.previewfile = fields.previewfile
107
108 const body = await unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
109 ...options,
110
111 path,
112 attaches,
113 fields: omit(fields, [ 'thumbnailfile', 'previewfile' ]),
114 implicitToken: true,
115 defaultExpectedStatus: HttpStatusCode.OK_200
116 }))
117
118 return body.video
119 }
120
121 async quickCreate (options: OverrideCommandOptions & {
122 saveReplay: boolean
123 permanentLive: boolean
124 privacy?: VideoPrivacyType
125 videoPasswords?: string[]
126 }) {
127 const { saveReplay, permanentLive, privacy = VideoPrivacy.PUBLIC, videoPasswords } = options
128
129 const replaySettings = privacy === VideoPrivacy.PASSWORD_PROTECTED
130 ? { privacy: VideoPrivacy.PRIVATE }
131 : { privacy }
132
133 const { uuid } = await this.create({
134 ...options,
135
136 fields: {
137 name: 'live',
138 permanentLive,
139 saveReplay,
140 replaySettings,
141 channelId: this.server.store.channel.id,
142 privacy,
143 videoPasswords
144 }
145 })
146
147 const video = await this.server.videos.getWithToken({ id: uuid })
148 const live = await this.get({ videoId: uuid })
149
150 return { video, live }
151 }
152
153 // ---------------------------------------------------------------------------
154
155 async sendRTMPStreamInVideo (options: OverrideCommandOptions & {
156 videoId: number | string
157 fixtureName?: string
158 copyCodecs?: boolean
159 }) {
160 const { videoId, fixtureName, copyCodecs } = options
161 const videoLive = await this.get({ videoId })
162
163 return sendRTMPStream({ rtmpBaseUrl: videoLive.rtmpUrl, streamKey: videoLive.streamKey, fixtureName, copyCodecs })
164 }
165
166 async runAndTestStreamError (options: OverrideCommandOptions & {
167 videoId: number | string
168 shouldHaveError: boolean
169 }) {
170 const command = await this.sendRTMPStreamInVideo(options)
171
172 return testFfmpegStreamError(command, options.shouldHaveError)
173 }
174
175 // ---------------------------------------------------------------------------
176
177 waitUntilPublished (options: OverrideCommandOptions & {
178 videoId: number | string
179 }) {
180 const { videoId } = options
181 return this.waitUntilState({ videoId, state: VideoState.PUBLISHED })
182 }
183
184 waitUntilWaiting (options: OverrideCommandOptions & {
185 videoId: number | string
186 }) {
187 const { videoId } = options
188 return this.waitUntilState({ videoId, state: VideoState.WAITING_FOR_LIVE })
189 }
190
191 waitUntilEnded (options: OverrideCommandOptions & {
192 videoId: number | string
193 }) {
194 const { videoId } = options
195 return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED })
196 }
197
198 async waitUntilSegmentGeneration (options: OverrideCommandOptions & {
199 server: PeerTubeServer
200 videoUUID: string
201 playlistNumber: number
202 segment: number
203 objectStorage?: ObjectStorageCommand
204 objectStorageBaseUrl?: string
205 }) {
206 const {
207 server,
208 objectStorage,
209 playlistNumber,
210 segment,
211 videoUUID,
212 objectStorageBaseUrl
213 } = options
214
215 const segmentName = `${playlistNumber}-00000${segment}.ts`
216 const baseUrl = objectStorage
217 ? join(objectStorageBaseUrl || objectStorage.getMockPlaylistBaseUrl(), 'hls')
218 : server.url + '/static/streaming-playlists/hls'
219
220 let error = true
221
222 while (error) {
223 try {
224 // Check fragment exists
225 await this.getRawRequest({
226 ...options,
227
228 url: `${baseUrl}/${videoUUID}/${segmentName}`,
229 implicitToken: false,
230 defaultExpectedStatus: HttpStatusCode.OK_200
231 })
232
233 const video = await server.videos.get({ id: videoUUID })
234 const hlsPlaylist = video.streamingPlaylists[0]
235
236 // Check SHA generation
237 const shaBody = await server.streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry: !!objectStorage })
238 if (!shaBody[segmentName]) {
239 throw new Error('Segment SHA does not exist')
240 }
241
242 // Check fragment is in m3u8 playlist
243 const subPlaylist = await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/${playlistNumber}.m3u8` })
244 if (!subPlaylist.includes(segmentName)) throw new Error('Fragment does not exist in playlist')
245
246 error = false
247 } catch {
248 error = true
249 await wait(100)
250 }
251 }
252 }
253
254 async waitUntilReplacedByReplay (options: OverrideCommandOptions & {
255 videoId: number | string
256 }) {
257 let video: VideoDetails
258
259 do {
260 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
261
262 await wait(500)
263 } while (video.isLive === true || video.state.id !== VideoState.PUBLISHED)
264 }
265
266 // ---------------------------------------------------------------------------
267
268 getSegmentFile (options: OverrideCommandOptions & {
269 videoUUID: string
270 playlistNumber: number
271 segment: number
272 objectStorage?: ObjectStorageCommand
273 }) {
274 const { playlistNumber, segment, videoUUID, objectStorage } = options
275
276 const segmentName = `${playlistNumber}-00000${segment}.ts`
277 const baseUrl = objectStorage
278 ? objectStorage.getMockPlaylistBaseUrl()
279 : `${this.server.url}/static/streaming-playlists/hls`
280
281 const url = `${baseUrl}/${videoUUID}/${segmentName}`
282
283 return this.getRawRequest({
284 ...options,
285
286 url,
287 implicitToken: false,
288 defaultExpectedStatus: HttpStatusCode.OK_200
289 })
290 }
291
292 getPlaylistFile (options: OverrideCommandOptions & {
293 videoUUID: string
294 playlistName: string
295 objectStorage?: ObjectStorageCommand
296 }) {
297 const { playlistName, videoUUID, objectStorage } = options
298
299 const baseUrl = objectStorage
300 ? objectStorage.getMockPlaylistBaseUrl()
301 : `${this.server.url}/static/streaming-playlists/hls`
302
303 const url = `${baseUrl}/${videoUUID}/${playlistName}`
304
305 return this.getRawRequest({
306 ...options,
307
308 url,
309 implicitToken: false,
310 defaultExpectedStatus: HttpStatusCode.OK_200
311 })
312 }
313
314 // ---------------------------------------------------------------------------
315
316 async countPlaylists (options: OverrideCommandOptions & {
317 videoUUID: string
318 }) {
319 const basePath = this.server.servers.buildDirectory('streaming-playlists')
320 const hlsPath = join(basePath, 'hls', options.videoUUID)
321
322 const files = await readdir(hlsPath)
323
324 return files.filter(f => f.endsWith('.m3u8')).length
325 }
326
327 private async waitUntilState (options: OverrideCommandOptions & {
328 videoId: number | string
329 state: VideoStateType
330 }) {
331 let video: VideoDetails
332
333 do {
334 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
335
336 await wait(500)
337 } while (video.state.id !== options.state)
338 }
339}