live.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
  2. import { omit } from '@peertube/peertube-core-utils'
  3. import { HttpStatusCode, LiveVideoLatencyMode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models'
  4. import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
  5. import {
  6. LiveCommand,
  7. PeerTubeServer,
  8. cleanupTests,
  9. createSingleServer,
  10. makePostBodyRequest,
  11. makeUploadRequest,
  12. sendRTMPStream,
  13. setAccessTokensToServers,
  14. stopFfmpeg
  15. } from '@peertube/peertube-server-commands'
  16. import { expect } from 'chai'
  17. describe('Test video lives API validator', function () {
  18. const path = '/api/v1/videos/live'
  19. let server: PeerTubeServer
  20. let userAccessToken = ''
  21. let channelId: number
  22. let video: VideoCreateResult
  23. let videoIdNotLive: number
  24. let command: LiveCommand
  25. // ---------------------------------------------------------------
  26. before(async function () {
  27. this.timeout(30000)
  28. server = await createSingleServer(1)
  29. await setAccessTokensToServers([ server ])
  30. await server.config.enableMinimumTranscoding()
  31. await server.config.updateExistingConfig({
  32. newConfig: {
  33. live: {
  34. enabled: true,
  35. latencySetting: {
  36. enabled: false
  37. },
  38. maxInstanceLives: 20,
  39. maxUserLives: 20,
  40. allowReplay: true
  41. }
  42. }
  43. })
  44. const username = 'user1'
  45. const password = 'my super password'
  46. await server.users.create({ username, password })
  47. userAccessToken = await server.login.getAccessToken({ username, password })
  48. {
  49. const { videoChannels } = await server.users.getMyInfo()
  50. channelId = videoChannels[0].id
  51. }
  52. {
  53. videoIdNotLive = (await server.videos.quickUpload({ name: 'not live' })).id
  54. }
  55. command = server.live
  56. })
  57. describe('When creating a live', function () {
  58. let baseCorrectParams
  59. before(function () {
  60. baseCorrectParams = {
  61. name: 'my super name',
  62. category: 5,
  63. licence: 1,
  64. language: 'pt',
  65. nsfw: false,
  66. commentsEnabled: true,
  67. downloadEnabled: true,
  68. waitTranscoding: true,
  69. description: 'my super description',
  70. support: 'my super support text',
  71. tags: [ 'tag1', 'tag2' ],
  72. privacy: VideoPrivacy.PUBLIC,
  73. channelId,
  74. saveReplay: false,
  75. replaySettings: undefined,
  76. permanentLive: true,
  77. latencyMode: LiveVideoLatencyMode.DEFAULT
  78. }
  79. })
  80. it('Should fail with nothing', async function () {
  81. const fields = {}
  82. await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
  83. })
  84. it('Should fail with a long name', async function () {
  85. const fields = { ...baseCorrectParams, name: 'super'.repeat(65) }
  86. await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
  87. })
  88. it('Should fail with a bad category', async function () {
  89. const fields = { ...baseCorrectParams, category: 125 }
  90. await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
  91. })
  92. it('Should fail with a bad licence', async function () {
  93. const fields = { ...baseCorrectParams, licence: 125 }
  94. await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
  95. })
  96. it('Should fail with a bad language', async function () {
  97. const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
  98. await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
  99. })
  100. it('Should fail with a long description', async function () {
  101. const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) }
  102. await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
  103. })
  104. it('Should fail with a long support text', async function () {
  105. const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
  106. await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
  107. })
  108. it('Should fail without a channel', async function () {
  109. const fields = omit(baseCorrectParams, [ 'channelId' ])
  110. await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
  111. })
  112. it('Should fail with a bad channel', async function () {
  113. const fields = { ...baseCorrectParams, channelId: 545454 }
  114. await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
  115. })
  116. it('Should fail with a bad privacy for replay settings', async function () {
  117. const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: 999 } }
  118. await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
  119. })
  120. it('Should fail with another user channel', async function () {
  121. const user = {
  122. username: 'fake',
  123. password: 'fake_password'
  124. }
  125. await server.users.create({ username: user.username, password: user.password })
  126. const accessTokenUser = await server.login.getAccessToken(user)
  127. const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser })
  128. const customChannelId = videoChannels[0].id
  129. const fields = { ...baseCorrectParams, channelId: customChannelId }
  130. await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
  131. })
  132. it('Should fail with too many tags', async function () {
  133. const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }
  134. await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
  135. })
  136. it('Should fail with a tag length too low', async function () {
  137. const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] }
  138. await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
  139. })
  140. it('Should fail with a tag length too big', async function () {
  141. const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }
  142. await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
  143. })
  144. it('Should fail with an incorrect thumbnail file', async function () {
  145. const fields = baseCorrectParams
  146. const attaches = {
  147. thumbnailfile: buildAbsoluteFixturePath('video_short.mp4')
  148. }
  149. await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
  150. })
  151. it('Should fail with a big thumbnail file', async function () {
  152. const fields = baseCorrectParams
  153. const attaches = {
  154. thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png')
  155. }
  156. await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
  157. })
  158. it('Should fail with an incorrect preview file', async function () {
  159. const fields = baseCorrectParams
  160. const attaches = {
  161. previewfile: buildAbsoluteFixturePath('video_short.mp4')
  162. }
  163. await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
  164. })
  165. it('Should fail with a big preview file', async function () {
  166. const fields = baseCorrectParams
  167. const attaches = {
  168. previewfile: buildAbsoluteFixturePath('custom-preview-big.png')
  169. }
  170. await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
  171. })
  172. it('Should fail with bad latency setting', async function () {
  173. const fields = { ...baseCorrectParams, latencyMode: 42 }
  174. await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
  175. })
  176. it('Should fail to set latency if the server does not allow it', async function () {
  177. const fields = { ...baseCorrectParams, latencyMode: LiveVideoLatencyMode.HIGH_LATENCY }
  178. await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
  179. })
  180. it('Should succeed with the correct parameters', async function () {
  181. this.timeout(30000)
  182. const res = await makePostBodyRequest({
  183. url: server.url,
  184. path,
  185. token: server.accessToken,
  186. fields: baseCorrectParams,
  187. expectedStatus: HttpStatusCode.OK_200
  188. })
  189. video = res.body.video
  190. })
  191. it('Should forbid if live is disabled', async function () {
  192. await server.config.updateExistingConfig({
  193. newConfig: {
  194. live: {
  195. enabled: false
  196. }
  197. }
  198. })
  199. await makePostBodyRequest({
  200. url: server.url,
  201. path,
  202. token: server.accessToken,
  203. fields: baseCorrectParams,
  204. expectedStatus: HttpStatusCode.FORBIDDEN_403
  205. })
  206. })
  207. it('Should forbid to save replay if not enabled by the admin', async function () {
  208. const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }
  209. await server.config.enableLive({ allowReplay: false, transcoding: false })
  210. await makePostBodyRequest({
  211. url: server.url,
  212. path,
  213. token: server.accessToken,
  214. fields,
  215. expectedStatus: HttpStatusCode.FORBIDDEN_403
  216. })
  217. })
  218. it('Should allow to save replay if enabled by the admin', async function () {
  219. const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }
  220. await server.config.enableLive({ allowReplay: true, transcoding: false })
  221. await makePostBodyRequest({
  222. url: server.url,
  223. path,
  224. token: server.accessToken,
  225. fields,
  226. expectedStatus: HttpStatusCode.OK_200
  227. })
  228. })
  229. it('Should not allow live if max instance lives is reached', async function () {
  230. await server.config.updateExistingConfig({
  231. newConfig: {
  232. live: {
  233. enabled: true,
  234. maxInstanceLives: 1
  235. }
  236. }
  237. })
  238. await makePostBodyRequest({
  239. url: server.url,
  240. path,
  241. token: server.accessToken,
  242. fields: baseCorrectParams,
  243. expectedStatus: HttpStatusCode.FORBIDDEN_403
  244. })
  245. })
  246. it('Should not allow live if max user lives is reached', async function () {
  247. await server.config.updateExistingConfig({
  248. newConfig: {
  249. live: {
  250. enabled: true,
  251. maxInstanceLives: 20,
  252. maxUserLives: 1
  253. }
  254. }
  255. })
  256. await makePostBodyRequest({
  257. url: server.url,
  258. path,
  259. token: server.accessToken,
  260. fields: baseCorrectParams,
  261. expectedStatus: HttpStatusCode.FORBIDDEN_403
  262. })
  263. })
  264. })
  265. describe('When getting live information', function () {
  266. it('Should fail with a bad access token', async function () {
  267. await command.get({ token: 'toto', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
  268. })
  269. it('Should not display private information without access token', async function () {
  270. const live = await command.get({ token: '', videoId: video.id })
  271. expect(live.rtmpUrl).to.not.exist
  272. expect(live.streamKey).to.not.exist
  273. expect(live.latencyMode).to.exist
  274. })
  275. it('Should not display private information with token of another user', async function () {
  276. const live = await command.get({ token: userAccessToken, videoId: video.id })
  277. expect(live.rtmpUrl).to.not.exist
  278. expect(live.streamKey).to.not.exist
  279. expect(live.latencyMode).to.exist
  280. })
  281. it('Should display private information with appropriate token', async function () {
  282. const live = await command.get({ videoId: video.id })
  283. expect(live.rtmpUrl).to.exist
  284. expect(live.streamKey).to.exist
  285. expect(live.latencyMode).to.exist
  286. })
  287. it('Should fail with a bad video id', async function () {
  288. await command.get({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  289. })
  290. it('Should fail with an unknown video id', async function () {
  291. await command.get({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
  292. })
  293. it('Should fail with a non live video', async function () {
  294. await command.get({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
  295. })
  296. it('Should succeed with the correct params', async function () {
  297. await command.get({ videoId: video.id })
  298. await command.get({ videoId: video.uuid })
  299. await command.get({ videoId: video.shortUUID })
  300. })
  301. })
  302. describe('When getting live sessions', function () {
  303. it('Should fail with a bad access token', async function () {
  304. await command.listSessions({ token: 'toto', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
  305. })
  306. it('Should fail without token', async function () {
  307. await command.listSessions({ token: null, videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
  308. })
  309. it('Should fail with the token of another user', async function () {
  310. await command.listSessions({ token: userAccessToken, videoId: video.id, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
  311. })
  312. it('Should fail with a bad video id', async function () {
  313. await command.listSessions({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  314. })
  315. it('Should fail with an unknown video id', async function () {
  316. await command.listSessions({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
  317. })
  318. it('Should fail with a non live video', async function () {
  319. await command.listSessions({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
  320. })
  321. it('Should succeed with the correct params', async function () {
  322. await command.listSessions({ videoId: video.id })
  323. })
  324. })
  325. describe('When getting live session of a replay', function () {
  326. it('Should fail with a bad video id', async function () {
  327. await command.getReplaySession({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  328. })
  329. it('Should fail with an unknown video id', async function () {
  330. await command.getReplaySession({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
  331. })
  332. it('Should fail with a non replay video', async function () {
  333. await command.getReplaySession({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
  334. })
  335. })
  336. describe('When updating live information', async function () {
  337. it('Should fail without access token', async function () {
  338. await command.update({ token: '', videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
  339. })
  340. it('Should fail with a bad access token', async function () {
  341. await command.update({ token: 'toto', videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
  342. })
  343. it('Should fail with access token of another user', async function () {
  344. await command.update({ token: userAccessToken, videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
  345. })
  346. it('Should fail with a bad video id', async function () {
  347. await command.update({ videoId: 'toto', fields: {}, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  348. })
  349. it('Should fail with an unknown video id', async function () {
  350. await command.update({ videoId: 454555, fields: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
  351. })
  352. it('Should fail with a non live video', async function () {
  353. await command.update({ videoId: videoIdNotLive, fields: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
  354. })
  355. it('Should fail with bad latency setting', async function () {
  356. const fields = { latencyMode: 42 as any }
  357. await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  358. })
  359. it('Should fail with a bad privacy for replay settings', async function () {
  360. const fields = { saveReplay: true, replaySettings: { privacy: 999 as any } }
  361. await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  362. })
  363. it('Should fail with save replay enabled but without replay settings', async function () {
  364. await server.config.enableLive({ allowReplay: true, transcoding: false })
  365. const fields = { saveReplay: true }
  366. await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  367. })
  368. it('Should fail with save replay disabled and replay settings', async function () {
  369. const fields = { saveReplay: false, replaySettings: { privacy: VideoPrivacy.INTERNAL } }
  370. await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  371. })
  372. it('Should fail with only replay settings when save replay is disabled', async function () {
  373. const fields = { replaySettings: { privacy: VideoPrivacy.INTERNAL } }
  374. await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  375. })
  376. it('Should fail to set latency if the server does not allow it', async function () {
  377. const fields = { latencyMode: LiveVideoLatencyMode.HIGH_LATENCY }
  378. await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
  379. })
  380. it('Should succeed with the correct params', async function () {
  381. await command.update({ videoId: video.id, fields: { saveReplay: false } })
  382. await command.update({ videoId: video.uuid, fields: { saveReplay: false } })
  383. await command.update({ videoId: video.shortUUID, fields: { saveReplay: false } })
  384. await command.update({ videoId: video.id, fields: { saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } })
  385. })
  386. it('Should fail to update replay status if replay is not allowed on the instance', async function () {
  387. await server.config.enableLive({ allowReplay: false, transcoding: false })
  388. await command.update({ videoId: video.id, fields: { saveReplay: true }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
  389. })
  390. it('Should succeed to live attributes if it has already started', async function () {
  391. this.timeout(40000)
  392. const live = await command.get({ videoId: video.id })
  393. const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
  394. await command.waitUntilPublished({ videoId: video.id })
  395. await command.update({ videoId: video.id, fields: { permanentLive: false }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  396. await stopFfmpeg(ffmpegCommand)
  397. })
  398. it('Should fail to change live privacy if it has already started', async function () {
  399. this.timeout(40000)
  400. const live = await command.get({ videoId: video.id })
  401. const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
  402. await command.waitUntilPublished({ videoId: video.id })
  403. await server.videos.update({
  404. id: video.id,
  405. attributes: { privacy: VideoPrivacy.PUBLIC } // Same privacy, it's fine
  406. })
  407. await server.videos.update({
  408. id: video.id,
  409. attributes: { privacy: VideoPrivacy.UNLISTED },
  410. expectedStatus: HttpStatusCode.BAD_REQUEST_400
  411. })
  412. await stopFfmpeg(ffmpegCommand)
  413. })
  414. it('Should fail to stream twice in the save live', async function () {
  415. this.timeout(40000)
  416. const live = await command.get({ videoId: video.id })
  417. const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
  418. await command.waitUntilPublished({ videoId: video.id })
  419. await command.runAndTestStreamError({ videoId: video.id, shouldHaveError: true })
  420. await stopFfmpeg(ffmpegCommand)
  421. })
  422. })
  423. after(async function () {
  424. await cleanupTests([ server ])
  425. })
  426. })