1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
3 import { readdir } from 'fs-extra'
4 import { join } from 'path'
5 import { omit, wait } from '@shared/core-utils'
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'
23 export class LiveCommand extends AbstractCommand {
25 get (options: OverrideCommandOptions & {
26 videoId: number | string
28 const path = '/api/v1/videos/live'
30 return this.getRequestBody<LiveVideo>({
33 path: path + '/' + options.videoId,
35 defaultExpectedStatus: HttpStatusCode.OK_200
39 // ---------------------------------------------------------------------------
41 listSessions (options: OverrideCommandOptions & {
42 videoId: number | string
44 const path = `/api/v1/videos/live/${options.videoId}/sessions`
46 return this.getRequestBody<ResultList<LiveVideoSession>>({
51 defaultExpectedStatus: HttpStatusCode.OK_200
55 async findLatestSession (options: OverrideCommandOptions & {
56 videoId: number | string
58 const { data: sessions } = await this.listSessions(options)
60 return sessions[sessions.length - 1]
63 getReplaySession (options: OverrideCommandOptions & {
64 videoId: number | string
66 const path = `/api/v1/videos/${options.videoId}/live-session`
68 return this.getRequestBody<LiveVideoSession>({
73 defaultExpectedStatus: HttpStatusCode.OK_200
77 // ---------------------------------------------------------------------------
79 update (options: OverrideCommandOptions & {
80 videoId: number | string
81 fields: LiveVideoUpdate
83 const { videoId, fields } = options
84 const path = '/api/v1/videos/live'
86 return this.putBodyRequest({
89 path: path + '/' + videoId,
92 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
96 async create (options: OverrideCommandOptions & {
97 fields: LiveVideoCreate
99 const { fields } = options
100 const path = '/api/v1/videos/live'
102 const attaches: any = {}
103 if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
104 if (fields.previewfile) attaches.previewfile = fields.previewfile
106 const body = await unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
111 fields: omit(fields, [ 'thumbnailfile', 'previewfile' ]),
113 defaultExpectedStatus: HttpStatusCode.OK_200
119 async quickCreate (options: OverrideCommandOptions & {
121 permanentLive: boolean
122 privacy?: VideoPrivacy
124 const { saveReplay, permanentLive, privacy = VideoPrivacy.PUBLIC } = options
126 const { uuid } = await this.create({
133 replaySettings: { privacy },
134 channelId: this.server.store.channel.id,
139 const video = await this.server.videos.getWithToken({ id: uuid })
140 const live = await this.get({ videoId: uuid })
142 return { video, live }
145 // ---------------------------------------------------------------------------
147 async sendRTMPStreamInVideo (options: OverrideCommandOptions & {
148 videoId: number | string
152 const { videoId, fixtureName, copyCodecs } = options
153 const videoLive = await this.get({ videoId })
155 return sendRTMPStream({ rtmpBaseUrl: videoLive.rtmpUrl, streamKey: videoLive.streamKey, fixtureName, copyCodecs })
158 async runAndTestStreamError (options: OverrideCommandOptions & {
159 videoId: number | string
160 shouldHaveError: boolean
162 const command = await this.sendRTMPStreamInVideo(options)
164 return testFfmpegStreamError(command, options.shouldHaveError)
167 // ---------------------------------------------------------------------------
169 waitUntilPublished (options: OverrideCommandOptions & {
170 videoId: number | string
172 const { videoId } = options
173 return this.waitUntilState({ videoId, state: VideoState.PUBLISHED })
176 waitUntilWaiting (options: OverrideCommandOptions & {
177 videoId: number | string
179 const { videoId } = options
180 return this.waitUntilState({ videoId, state: VideoState.WAITING_FOR_LIVE })
183 waitUntilEnded (options: OverrideCommandOptions & {
184 videoId: number | string
186 const { videoId } = options
187 return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED })
190 async waitUntilSegmentGeneration (options: OverrideCommandOptions & {
191 server: PeerTubeServer
193 playlistNumber: number
195 objectStorage?: ObjectStorageCommand
196 objectStorageBaseUrl?: string
207 const segmentName = `${playlistNumber}-00000${segment}.ts`
208 const baseUrl = objectStorage
209 ? join(objectStorageBaseUrl || objectStorage.getMockPlaylistBaseUrl(), 'hls')
210 : server.url + '/static/streaming-playlists/hls'
216 // Check fragment exists
217 await this.getRawRequest({
220 url: `${baseUrl}/${videoUUID}/${segmentName}`,
221 implicitToken: false,
222 defaultExpectedStatus: HttpStatusCode.OK_200
225 const video = await server.videos.get({ id: videoUUID })
226 const hlsPlaylist = video.streamingPlaylists[0]
228 // Check SHA generation
229 const shaBody = await server.streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry: !!objectStorage })
230 if (!shaBody[segmentName]) {
231 throw new Error('Segment SHA does not exist')
234 // Check fragment is in m3u8 playlist
235 const subPlaylist = await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/${playlistNumber}.m3u8` })
236 if (!subPlaylist.includes(segmentName)) throw new Error('Fragment does not exist in playlist')
246 async waitUntilReplacedByReplay (options: OverrideCommandOptions & {
247 videoId: number | string
249 let video: VideoDetails
252 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
255 } while (video.isLive === true || video.state.id !== VideoState.PUBLISHED)
258 // ---------------------------------------------------------------------------
260 getSegmentFile (options: OverrideCommandOptions & {
262 playlistNumber: number
264 objectStorage?: ObjectStorageCommand
266 const { playlistNumber, segment, videoUUID, objectStorage } = options
268 const segmentName = `${playlistNumber}-00000${segment}.ts`
269 const baseUrl = objectStorage
270 ? objectStorage.getMockPlaylistBaseUrl()
271 : `${this.server.url}/static/streaming-playlists/hls`
273 const url = `${baseUrl}/${videoUUID}/${segmentName}`
275 return this.getRawRequest({
279 implicitToken: false,
280 defaultExpectedStatus: HttpStatusCode.OK_200
284 getPlaylistFile (options: OverrideCommandOptions & {
287 objectStorage?: ObjectStorageCommand
289 const { playlistName, videoUUID, objectStorage } = options
291 const baseUrl = objectStorage
292 ? objectStorage.getMockPlaylistBaseUrl()
293 : `${this.server.url}/static/streaming-playlists/hls`
295 const url = `${baseUrl}/${videoUUID}/${playlistName}`
297 return this.getRawRequest({
301 implicitToken: false,
302 defaultExpectedStatus: HttpStatusCode.OK_200
306 // ---------------------------------------------------------------------------
308 async countPlaylists (options: OverrideCommandOptions & {
311 const basePath = this.server.servers.buildDirectory('streaming-playlists')
312 const hlsPath = join(basePath, 'hls', options.videoUUID)
314 const files = await readdir(hlsPath)
316 return files.filter(f => f.endsWith('.m3u8')).length
319 private async waitUntilState (options: OverrideCommandOptions & {
320 videoId: number | string
323 let video: VideoDetails
326 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
329 } while (video.state.id !== options.state)