live-command.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  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. }) {
  158. const { server, objectStorage, playlistNumber, segment, videoUUID } = options
  159. const segmentName = `${playlistNumber}-00000${segment}.ts`
  160. const baseUrl = objectStorage
  161. ? ObjectStorageCommand.getPlaylistBaseUrl() + 'hls'
  162. : server.url + '/static/streaming-playlists/hls'
  163. let error = true
  164. while (error) {
  165. try {
  166. await this.getRawRequest({
  167. ...options,
  168. url: `${baseUrl}/${videoUUID}/${segmentName}`,
  169. implicitToken: false,
  170. defaultExpectedStatus: HttpStatusCode.OK_200
  171. })
  172. const video = await server.videos.get({ id: videoUUID })
  173. const hlsPlaylist = video.streamingPlaylists[0]
  174. const shaBody = await server.streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
  175. if (!shaBody[segmentName]) {
  176. throw new Error('Segment SHA does not exist')
  177. }
  178. error = false
  179. } catch {
  180. error = true
  181. await wait(100)
  182. }
  183. }
  184. }
  185. async waitUntilReplacedByReplay (options: OverrideCommandOptions & {
  186. videoId: number | string
  187. }) {
  188. let video: VideoDetails
  189. do {
  190. video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
  191. await wait(500)
  192. } while (video.isLive === true || video.state.id !== VideoState.PUBLISHED)
  193. }
  194. // ---------------------------------------------------------------------------
  195. getSegmentFile (options: OverrideCommandOptions & {
  196. videoUUID: string
  197. playlistNumber: number
  198. segment: number
  199. objectStorage?: boolean // default false
  200. }) {
  201. const { playlistNumber, segment, videoUUID, objectStorage = false } = options
  202. const segmentName = `${playlistNumber}-00000${segment}.ts`
  203. const baseUrl = objectStorage
  204. ? ObjectStorageCommand.getPlaylistBaseUrl()
  205. : `${this.server.url}/static/streaming-playlists/hls`
  206. const url = `${baseUrl}/${videoUUID}/${segmentName}`
  207. return this.getRawRequest({
  208. ...options,
  209. url,
  210. implicitToken: false,
  211. defaultExpectedStatus: HttpStatusCode.OK_200
  212. })
  213. }
  214. getPlaylistFile (options: OverrideCommandOptions & {
  215. videoUUID: string
  216. playlistName: string
  217. objectStorage?: boolean // default false
  218. }) {
  219. const { playlistName, videoUUID, objectStorage = false } = options
  220. const baseUrl = objectStorage
  221. ? ObjectStorageCommand.getPlaylistBaseUrl()
  222. : `${this.server.url}/static/streaming-playlists/hls`
  223. const url = `${baseUrl}/${videoUUID}/${playlistName}`
  224. return this.getRawRequest({
  225. ...options,
  226. url,
  227. implicitToken: false,
  228. defaultExpectedStatus: HttpStatusCode.OK_200
  229. })
  230. }
  231. // ---------------------------------------------------------------------------
  232. async countPlaylists (options: OverrideCommandOptions & {
  233. videoUUID: string
  234. }) {
  235. const basePath = this.server.servers.buildDirectory('streaming-playlists')
  236. const hlsPath = join(basePath, 'hls', options.videoUUID)
  237. const files = await readdir(hlsPath)
  238. return files.filter(f => f.endsWith('.m3u8')).length
  239. }
  240. private async waitUntilState (options: OverrideCommandOptions & {
  241. videoId: number | string
  242. state: VideoState
  243. }) {
  244. let video: VideoDetails
  245. do {
  246. video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
  247. await wait(500)
  248. } while (video.state.id !== options.state)
  249. }
  250. }