live-command.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
  2. import { readdir } from 'fs-extra'
  3. import { join } from 'path'
  4. import { omit, wait } from '@shared/core-utils'
  5. import {
  6. HttpStatusCode,
  7. LiveVideo,
  8. LiveVideoCreate,
  9. LiveVideoSession,
  10. LiveVideoUpdate,
  11. ResultList,
  12. VideoCreateResult,
  13. VideoDetails,
  14. VideoPrivacy,
  15. VideoState
  16. } from '@shared/models'
  17. import { unwrapBody } from '../requests'
  18. import { ObjectStorageCommand, PeerTubeServer } from '../server'
  19. import { AbstractCommand, OverrideCommandOptions } from '../shared'
  20. import { sendRTMPStream, testFfmpegStreamError } from './live'
  21. export class LiveCommand extends AbstractCommand {
  22. get (options: OverrideCommandOptions & {
  23. videoId: number | string
  24. }) {
  25. const path = '/api/v1/videos/live'
  26. return this.getRequestBody<LiveVideo>({
  27. ...options,
  28. path: path + '/' + options.videoId,
  29. implicitToken: true,
  30. defaultExpectedStatus: HttpStatusCode.OK_200
  31. })
  32. }
  33. // ---------------------------------------------------------------------------
  34. listSessions (options: OverrideCommandOptions & {
  35. videoId: number | string
  36. }) {
  37. const path = `/api/v1/videos/live/${options.videoId}/sessions`
  38. return this.getRequestBody<ResultList<LiveVideoSession>>({
  39. ...options,
  40. path,
  41. implicitToken: true,
  42. defaultExpectedStatus: HttpStatusCode.OK_200
  43. })
  44. }
  45. async findLatestSession (options: OverrideCommandOptions & {
  46. videoId: number | string
  47. }) {
  48. const { data: sessions } = await this.listSessions(options)
  49. return sessions[sessions.length - 1]
  50. }
  51. getReplaySession (options: OverrideCommandOptions & {
  52. videoId: number | string
  53. }) {
  54. const path = `/api/v1/videos/${options.videoId}/live-session`
  55. return this.getRequestBody<LiveVideoSession>({
  56. ...options,
  57. path,
  58. implicitToken: true,
  59. defaultExpectedStatus: HttpStatusCode.OK_200
  60. })
  61. }
  62. // ---------------------------------------------------------------------------
  63. update (options: OverrideCommandOptions & {
  64. videoId: number | string
  65. fields: LiveVideoUpdate
  66. }) {
  67. const { videoId, fields } = options
  68. const path = '/api/v1/videos/live'
  69. return this.putBodyRequest({
  70. ...options,
  71. path: path + '/' + videoId,
  72. fields,
  73. implicitToken: true,
  74. defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
  75. })
  76. }
  77. async create (options: OverrideCommandOptions & {
  78. fields: LiveVideoCreate
  79. }) {
  80. const { fields } = options
  81. const path = '/api/v1/videos/live'
  82. const attaches: any = {}
  83. if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
  84. if (fields.previewfile) attaches.previewfile = fields.previewfile
  85. const body = await unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
  86. ...options,
  87. path,
  88. attaches,
  89. fields: omit(fields, [ 'thumbnailfile', 'previewfile' ]),
  90. implicitToken: true,
  91. defaultExpectedStatus: HttpStatusCode.OK_200
  92. }))
  93. return body.video
  94. }
  95. async quickCreate (options: OverrideCommandOptions & {
  96. saveReplay: boolean
  97. permanentLive: boolean
  98. privacy?: VideoPrivacy
  99. }) {
  100. const { saveReplay, permanentLive, privacy } = options
  101. const { uuid } = await this.create({
  102. ...options,
  103. fields: {
  104. name: 'live',
  105. permanentLive,
  106. saveReplay,
  107. channelId: this.server.store.channel.id,
  108. privacy
  109. }
  110. })
  111. const video = await this.server.videos.getWithToken({ id: uuid })
  112. const live = await this.get({ videoId: uuid })
  113. return { video, live }
  114. }
  115. // ---------------------------------------------------------------------------
  116. async sendRTMPStreamInVideo (options: OverrideCommandOptions & {
  117. videoId: number | string
  118. fixtureName?: string
  119. copyCodecs?: boolean
  120. }) {
  121. const { videoId, fixtureName, copyCodecs } = options
  122. const videoLive = await this.get({ videoId })
  123. return sendRTMPStream({ rtmpBaseUrl: videoLive.rtmpUrl, streamKey: videoLive.streamKey, fixtureName, copyCodecs })
  124. }
  125. async runAndTestStreamError (options: OverrideCommandOptions & {
  126. videoId: number | string
  127. shouldHaveError: boolean
  128. }) {
  129. const command = await this.sendRTMPStreamInVideo(options)
  130. return testFfmpegStreamError(command, options.shouldHaveError)
  131. }
  132. // ---------------------------------------------------------------------------
  133. waitUntilPublished (options: OverrideCommandOptions & {
  134. videoId: number | string
  135. }) {
  136. const { videoId } = options
  137. return this.waitUntilState({ videoId, state: VideoState.PUBLISHED })
  138. }
  139. waitUntilWaiting (options: OverrideCommandOptions & {
  140. videoId: number | string
  141. }) {
  142. const { videoId } = options
  143. return this.waitUntilState({ videoId, state: VideoState.WAITING_FOR_LIVE })
  144. }
  145. waitUntilEnded (options: OverrideCommandOptions & {
  146. videoId: number | string
  147. }) {
  148. const { videoId } = options
  149. return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED })
  150. }
  151. async waitUntilSegmentGeneration (options: OverrideCommandOptions & {
  152. server: PeerTubeServer
  153. videoUUID: string
  154. playlistNumber: number
  155. segment: number
  156. objectStorage: boolean
  157. objectStorageBaseUrl?: string
  158. }) {
  159. const {
  160. server,
  161. objectStorage,
  162. playlistNumber,
  163. segment,
  164. videoUUID,
  165. objectStorageBaseUrl = ObjectStorageCommand.getMockPlaylistBaseUrl()
  166. } = options
  167. const segmentName = `${playlistNumber}-00000${segment}.ts`
  168. const baseUrl = objectStorage
  169. ? join(objectStorageBaseUrl, 'hls')
  170. : server.url + '/static/streaming-playlists/hls'
  171. let error = true
  172. while (error) {
  173. try {
  174. await this.getRawRequest({
  175. ...options,
  176. url: `${baseUrl}/${videoUUID}/${segmentName}`,
  177. implicitToken: false,
  178. defaultExpectedStatus: HttpStatusCode.OK_200
  179. })
  180. const video = await server.videos.get({ id: videoUUID })
  181. const hlsPlaylist = video.streamingPlaylists[0]
  182. const shaBody = await server.streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
  183. if (!shaBody[segmentName]) {
  184. throw new Error('Segment SHA does not exist')
  185. }
  186. error = false
  187. } catch {
  188. error = true
  189. await wait(100)
  190. }
  191. }
  192. }
  193. async waitUntilReplacedByReplay (options: OverrideCommandOptions & {
  194. videoId: number | string
  195. }) {
  196. let video: VideoDetails
  197. do {
  198. video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
  199. await wait(500)
  200. } while (video.isLive === true || video.state.id !== VideoState.PUBLISHED)
  201. }
  202. // ---------------------------------------------------------------------------
  203. getSegmentFile (options: OverrideCommandOptions & {
  204. videoUUID: string
  205. playlistNumber: number
  206. segment: number
  207. objectStorage?: boolean // default false
  208. }) {
  209. const { playlistNumber, segment, videoUUID, objectStorage = false } = options
  210. const segmentName = `${playlistNumber}-00000${segment}.ts`
  211. const baseUrl = objectStorage
  212. ? ObjectStorageCommand.getMockPlaylistBaseUrl()
  213. : `${this.server.url}/static/streaming-playlists/hls`
  214. const url = `${baseUrl}/${videoUUID}/${segmentName}`
  215. return this.getRawRequest({
  216. ...options,
  217. url,
  218. implicitToken: false,
  219. defaultExpectedStatus: HttpStatusCode.OK_200
  220. })
  221. }
  222. getPlaylistFile (options: OverrideCommandOptions & {
  223. videoUUID: string
  224. playlistName: string
  225. objectStorage?: boolean // default false
  226. }) {
  227. const { playlistName, videoUUID, objectStorage = false } = options
  228. const baseUrl = objectStorage
  229. ? ObjectStorageCommand.getMockPlaylistBaseUrl()
  230. : `${this.server.url}/static/streaming-playlists/hls`
  231. const url = `${baseUrl}/${videoUUID}/${playlistName}`
  232. return this.getRawRequest({
  233. ...options,
  234. url,
  235. implicitToken: false,
  236. defaultExpectedStatus: HttpStatusCode.OK_200
  237. })
  238. }
  239. // ---------------------------------------------------------------------------
  240. async countPlaylists (options: OverrideCommandOptions & {
  241. videoUUID: string
  242. }) {
  243. const basePath = this.server.servers.buildDirectory('streaming-playlists')
  244. const hlsPath = join(basePath, 'hls', options.videoUUID)
  245. const files = await readdir(hlsPath)
  246. return files.filter(f => f.endsWith('.m3u8')).length
  247. }
  248. private async waitUntilState (options: OverrideCommandOptions & {
  249. videoId: number | string
  250. state: VideoState
  251. }) {
  252. let video: VideoDetails
  253. do {
  254. video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
  255. await wait(500)
  256. } while (video.state.id !== options.state)
  257. }
  258. }