]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - shared/server-commands/videos/live-command.ts
Correctly wait for live segment generation
[github/Chocobozzz/PeerTube.git] / shared / server-commands / videos / live-command.ts
index c24c7a5fcf48976af7daf74a4071fedb5c7ed0cd..73f4eefd331a94086c625b55b41c1898d0f6aeff 100644 (file)
@@ -1,11 +1,22 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import { readdir } from 'fs-extra'
-import { omit } from 'lodash'
 import { join } from 'path'
-import { wait } from '@shared/core-utils'
-import { HttpStatusCode, LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoCreateResult, VideoDetails, VideoState } from '@shared/models'
+import { omit, wait } from '@shared/core-utils'
+import {
+  HttpStatusCode,
+  LiveVideo,
+  LiveVideoCreate,
+  LiveVideoSession,
+  LiveVideoUpdate,
+  ResultList,
+  VideoCreateResult,
+  VideoDetails,
+  VideoPrivacy,
+  VideoState
+} from '@shared/models'
 import { unwrapBody } from '../requests'
+import { ObjectStorageCommand, PeerTubeServer } from '../server'
 import { AbstractCommand, OverrideCommandOptions } from '../shared'
 import { sendRTMPStream, testFfmpegStreamError } from './live'
 
@@ -25,6 +36,46 @@ export class LiveCommand extends AbstractCommand {
     })
   }
 
+  // ---------------------------------------------------------------------------
+
+  listSessions (options: OverrideCommandOptions & {
+    videoId: number | string
+  }) {
+    const path = `/api/v1/videos/live/${options.videoId}/sessions`
+
+    return this.getRequestBody<ResultList<LiveVideoSession>>({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  async findLatestSession (options: OverrideCommandOptions & {
+    videoId: number | string
+  }) {
+    const { data: sessions } = await this.listSessions(options)
+
+    return sessions[sessions.length - 1]
+  }
+
+  getReplaySession (options: OverrideCommandOptions & {
+    videoId: number | string
+  }) {
+    const path = `/api/v1/videos/${options.videoId}/live-session`
+
+    return this.getRequestBody<LiveVideoSession>({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
   update (options: OverrideCommandOptions & {
     videoId: number | string
     fields: LiveVideoUpdate
@@ -57,7 +108,7 @@ export class LiveCommand extends AbstractCommand {
 
       path,
       attaches,
-      fields: omit(fields, 'thumbnailfile', 'previewfile'),
+      fields: omit(fields, [ 'thumbnailfile', 'previewfile' ]),
       implicitToken: true,
       defaultExpectedStatus: HttpStatusCode.OK_200
     }))
@@ -65,6 +116,34 @@ export class LiveCommand extends AbstractCommand {
     return body.video
   }
 
+  async quickCreate (options: OverrideCommandOptions & {
+    saveReplay: boolean
+    permanentLive: boolean
+    privacy?: VideoPrivacy
+  }) {
+    const { saveReplay, permanentLive, privacy = VideoPrivacy.PUBLIC } = options
+
+    const { uuid } = await this.create({
+      ...options,
+
+      fields: {
+        name: 'live',
+        permanentLive,
+        saveReplay,
+        replaySettings: { privacy },
+        channelId: this.server.store.channel.id,
+        privacy
+      }
+    })
+
+    const video = await this.server.videos.getWithToken({ id: uuid })
+    const live = await this.get({ videoId: uuid })
+
+    return { video, live }
+  }
+
+  // ---------------------------------------------------------------------------
+
   async sendRTMPStreamInVideo (options: OverrideCommandOptions & {
     videoId: number | string
     fixtureName?: string
@@ -85,6 +164,8 @@ export class LiveCommand extends AbstractCommand {
     return testFfmpegStreamError(command, options.shouldHaveError)
   }
 
+  // ---------------------------------------------------------------------------
+
   waitUntilPublished (options: OverrideCommandOptions & {
     videoId: number | string
   }) {
@@ -106,15 +187,60 @@ export class LiveCommand extends AbstractCommand {
     return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED })
   }
 
-  waitUntilSegmentGeneration (options: OverrideCommandOptions & {
+  async waitUntilSegmentGeneration (options: OverrideCommandOptions & {
+    server: PeerTubeServer
     videoUUID: string
-    resolution: number
+    playlistNumber: number
     segment: number
+    objectStorage: boolean
+    objectStorageBaseUrl?: string
   }) {
-    const { resolution, segment, videoUUID } = options
-    const segmentName = `${resolution}-00000${segment}.ts`
+    const {
+      server,
+      objectStorage,
+      playlistNumber,
+      segment,
+      videoUUID,
+      objectStorageBaseUrl = ObjectStorageCommand.getMockPlaylistBaseUrl()
+    } = options
+
+    const segmentName = `${playlistNumber}-00000${segment}.ts`
+    const baseUrl = objectStorage
+      ? join(objectStorageBaseUrl, 'hls')
+      : server.url + '/static/streaming-playlists/hls'
+
+    let error = true
+
+    while (error) {
+      try {
+        // Check fragment exists
+        await this.getRawRequest({
+          ...options,
+
+          url: `${baseUrl}/${videoUUID}/${segmentName}`,
+          implicitToken: false,
+          defaultExpectedStatus: HttpStatusCode.OK_200
+        })
+
+        const video = await server.videos.get({ id: videoUUID })
+        const hlsPlaylist = video.streamingPlaylists[0]
 
-    return this.server.servers.waitUntilLog(`${videoUUID}/${segmentName}`, 2, false)
+        // Check SHA generation
+        const shaBody = await server.streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry: objectStorage })
+        if (!shaBody[segmentName]) {
+          throw new Error('Segment SHA does not exist')
+        }
+
+        // Check fragment is in m3u8 playlist
+        const subPlaylist = await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/${playlistNumber}.m3u8` })
+        if (!subPlaylist.includes(segmentName)) throw new Error('Fragment does not exist in playlist')
+
+        error = false
+      } catch {
+        error = true
+        await wait(100)
+      }
+    }
   }
 
   async waitUntilReplacedByReplay (options: OverrideCommandOptions & {
@@ -129,6 +255,56 @@ export class LiveCommand extends AbstractCommand {
     } while (video.isLive === true || video.state.id !== VideoState.PUBLISHED)
   }
 
+  // ---------------------------------------------------------------------------
+
+  getSegmentFile (options: OverrideCommandOptions & {
+    videoUUID: string
+    playlistNumber: number
+    segment: number
+    objectStorage?: boolean // default false
+  }) {
+    const { playlistNumber, segment, videoUUID, objectStorage = false } = options
+
+    const segmentName = `${playlistNumber}-00000${segment}.ts`
+    const baseUrl = objectStorage
+      ? ObjectStorageCommand.getMockPlaylistBaseUrl()
+      : `${this.server.url}/static/streaming-playlists/hls`
+
+    const url = `${baseUrl}/${videoUUID}/${segmentName}`
+
+    return this.getRawRequest({
+      ...options,
+
+      url,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getPlaylistFile (options: OverrideCommandOptions & {
+    videoUUID: string
+    playlistName: string
+    objectStorage?: boolean // default false
+  }) {
+    const { playlistName, videoUUID, objectStorage = false } = options
+
+    const baseUrl = objectStorage
+      ? ObjectStorageCommand.getMockPlaylistBaseUrl()
+      : `${this.server.url}/static/streaming-playlists/hls`
+
+    const url = `${baseUrl}/${videoUUID}/${playlistName}`
+
+    return this.getRawRequest({
+      ...options,
+
+      url,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
   async countPlaylists (options: OverrideCommandOptions & {
     videoUUID: string
   }) {