aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared/server-commands/videos/live-command.ts
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-07-31 14:34:36 +0200
committerChocobozzz <me@florianbigard.com>2023-08-11 15:02:33 +0200
commit3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch)
treee4510b39bdac9c318fdb4b47018d08f15368b8f0 /shared/server-commands/videos/live-command.ts
parent04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff)
downloadPeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'shared/server-commands/videos/live-command.ts')
-rw-r--r--shared/server-commands/videos/live-command.ts337
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
3import { readdir } from 'fs-extra'
4import { join } from 'path'
5import { omit, wait } from '@shared/core-utils'
6import {
7 HttpStatusCode,
8 LiveVideo,
9 LiveVideoCreate,
10 LiveVideoSession,
11 LiveVideoUpdate,
12 ResultList,
13 VideoCreateResult,
14 VideoDetails,
15 VideoPrivacy,
16 VideoState
17} from '@shared/models'
18import { unwrapBody } from '../requests'
19import { ObjectStorageCommand, PeerTubeServer } from '../server'
20import { AbstractCommand, OverrideCommandOptions } from '../shared'
21import { sendRTMPStream, testFfmpegStreamError } from './live'
22
23export 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}