Browse Source

Begin live tests

Chocobozzz 3 years ago
parent
commit
af4ae64f6f

+ 2 - 1
scripts/ci.sh

@@ -58,8 +58,9 @@ elif [ "$1" = "api-2" ]; then
 
     serverFiles=$(findTestFiles server/tests/api/server)
     usersFiles=$(findTestFiles server/tests/api/users)
+    liveFiles=$(findTestFiles server/tests/api/live)
 
-    MOCHA_PARALLEL=true runTest 2 $serverFiles $usersFiles
+    MOCHA_PARALLEL=true runTest 2 $serverFiles $usersFiles liveFiles
 elif [ "$1" = "api-3" ]; then
     npm run build:server
 

BIN
server/assets/default-audio-background.jpg


BIN
server/assets/default-live-background.jpg


+ 1 - 1
server/controllers/activitypub/client.ts

@@ -223,7 +223,7 @@ function getAccountVideoRateFactory (rateType: VideoRateType) {
 
 async function videoController (req: express.Request, res: express.Response) {
   // We need more attributes
-  const video = await VideoModel.loadForGetAPI({ id: res.locals.onlyVideoWithRights.id }) as MVideoAPWithoutCaption
+  const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(res.locals.onlyVideoWithRights.id)
 
   if (video.url.startsWith(WEBSERVER.URL) === false) return res.redirect(video.url)
 

+ 1 - 1
server/controllers/api/videos/index.ts

@@ -189,7 +189,7 @@ async function addVideo (req: express.Request, res: express.Response) {
   videoData.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED
   videoData.duration = videoPhysicalFile['duration'] // duration was added by a previous middleware
 
-  const video = new VideoModel(videoData) as MVideoDetails
+  const video = new VideoModel(videoData) as MVideoFullLight
   video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
 
   const videoFile = new VideoFileModel({

+ 8 - 2
server/controllers/api/videos/live.ts

@@ -4,6 +4,7 @@ import { createReqFiles } from '@server/helpers/express-utils'
 import { CONFIG } from '@server/initializers/config'
 import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants'
 import { getVideoActivityPubUrl } from '@server/lib/activitypub/url'
+import { federateVideoIfNeeded } from '@server/lib/activitypub/videos'
 import { buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
 import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator } from '@server/middlewares/validators/videos/video-live'
 import { VideoLiveModel } from '@server/models/video/video-live'
@@ -63,10 +64,13 @@ async function getLiveVideo (req: express.Request, res: express.Response) {
 async function updateLiveVideo (req: express.Request, res: express.Response) {
   const body: LiveVideoUpdate = req.body
 
+  const video = res.locals.videoAll
   const videoLive = res.locals.videoLive
   videoLive.saveReplay = body.saveReplay || false
 
-  await videoLive.save()
+  video.VideoLive = await videoLive.save()
+
+  await federateVideoIfNeeded(video, false)
 
   return res.sendStatus(204)
 }
@@ -113,10 +117,12 @@ async function addLiveVideo (req: express.Request, res: express.Response) {
     videoCreated.VideoChannel = res.locals.videoChannel
 
     videoLive.videoId = videoCreated.id
-    await videoLive.save(sequelizeOptions)
+    videoCreated.VideoLive = await videoLive.save(sequelizeOptions)
 
     await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
 
+    await federateVideoIfNeeded(videoCreated, true, t)
+
     logger.info('Video live %s with uuid %s created.', videoInfo.name, videoCreated.uuid)
 
     return { videoCreated }

+ 1 - 1
server/helpers/custom-validators/activitypub/videos.ts

@@ -63,6 +63,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
   if (!isBooleanValid(video.downloadEnabled)) video.downloadEnabled = true
   if (!isBooleanValid(video.commentsEnabled)) video.commentsEnabled = false
   if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false
+  if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false
 
   return isActivityPubUrlValid(video.id) &&
     isVideoNameValid(video.name) &&
@@ -79,7 +80,6 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
     isDateValid(video.updated) &&
     (!video.originallyPublishedAt || isDateValid(video.originallyPublishedAt)) &&
     (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) &&
-    video.url.length !== 0 &&
     video.attributedTo.length !== 0
 }
 

+ 2 - 2
server/helpers/middlewares/videos.ts

@@ -92,9 +92,9 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
   return true
 }
 
-function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response) {
+function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) {
   // Retrieve the user who did the request
-  if (video.isOwned() === false) {
+  if (onlyOwned && video.isOwned() === false) {
     res.status(403)
        .json({ error: 'Cannot manage a video of another server.' })
        .end()

+ 34 - 2
server/lib/activitypub/videos.ts

@@ -1,3 +1,4 @@
+import { VideoLiveModel } from '@server/models/video/video-live'
 import * as Bluebird from 'bluebird'
 import { maxBy, minBy } from 'lodash'
 import * as magnetUtil from 'magnet-uri'
@@ -84,7 +85,7 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid
     // Check this is not a blacklisted video, or unfederated blacklisted video
     (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) &&
     // Check the video is public/unlisted and published
-    video.hasPrivacyForFederation() && video.state === VideoState.PUBLISHED
+    video.hasPrivacyForFederation() && (video.state === VideoState.PUBLISHED || video.state === VideoState.WAITING_FOR_LIVE)
   ) {
     // Fetch more attributes that we will need to serialize in AP object
     if (isArray(video.VideoCaptions) === false) {
@@ -424,6 +425,27 @@ async function updateVideoFromAP (options: {
         await Promise.all(videoCaptionsPromises)
       }
 
+      {
+        // Create or update existing live
+        if (video.isLive) {
+          const [ videoLive ] = await VideoLiveModel.upsert({
+            saveReplay: videoObject.liveSaveReplay,
+            videoId: video.id
+          }, { transaction: t, returning: true })
+
+          videoUpdated.VideoLive = videoLive
+        } else { // Delete existing live if it exists
+          await VideoLiveModel.destroy({
+            where: {
+              videoId: video.id
+            },
+            transaction: t
+          })
+
+          videoUpdated.VideoLive = null
+        }
+      }
+
       return videoUpdated
     })
 
@@ -436,7 +458,7 @@ async function updateVideoFromAP (options: {
     })
 
     if (wasPrivateVideo || wasUnlistedVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) // Notify our users?
-    if (videoUpdated.isLive) PeerTubeSocket.Instance.sendVideoLiveNewState(video)
+    if (videoUpdated.isLive) PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated)
 
     logger.info('Remote video with uuid %s updated', videoObject.uuid)
 
@@ -606,6 +628,16 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
 
     videoCreated.VideoFiles = videoFiles
 
+    if (videoCreated.isLive) {
+      const videoLive = new VideoLiveModel({
+        streamKey: null,
+        saveReplay: videoObject.liveSaveReplay,
+        videoId: videoCreated.id
+      })
+
+      videoCreated.VideoLive = await videoLive.save({ transaction: t })
+    }
+
     const autoBlacklisted = await autoBlacklistVideoIfNeeded({
       video: videoCreated,
       user: undefined,

+ 7 - 3
server/middlewares/validators/videos/video-live.ts

@@ -16,14 +16,14 @@ const videoLiveGetValidator = [
   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking videoLiveGetValidator parameters', { parameters: req.body })
+    logger.debug('Checking videoLiveGetValidator parameters', { parameters: req.params, user: res.locals.oauth.token.User.username })
 
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res, 'all')) return
 
-    // Check if the user who did the request is able to update the video
+    // Check if the user who did the request is able to get the live info
     const user = res.locals.oauth.token.User
-    if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return
+    if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res, false)) return
 
     const videoLive = await VideoLiveModel.loadByVideoId(res.locals.videoAll.id)
     if (!videoLive) return res.sendStatus(404)
@@ -122,6 +122,10 @@ const videoLiveUpdateValidator = [
         .json({ error: 'Cannot update a live that has already started' })
     }
 
+    // Check the user can manage the live
+    const user = res.locals.oauth.token.User
+    if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res)) return
+
     return next()
   }
 ]

+ 10 - 1
server/models/video/video-format-utils.ts

@@ -352,11 +352,20 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
     sensitive: video.nsfw,
     waitTranscoding: video.waitTranscoding,
     isLiveBroadcast: video.isLive,
+
+    liveSaveReplay: video.isLive
+      ? video.VideoLive.saveReplay
+      : null,
+
     state: video.state,
     commentsEnabled: video.commentsEnabled,
     downloadEnabled: video.downloadEnabled,
     published: video.publishedAt.toISOString(),
-    originallyPublishedAt: video.originallyPublishedAt ? video.originallyPublishedAt.toISOString() : null,
+
+    originallyPublishedAt: video.originallyPublishedAt
+      ? video.originallyPublishedAt.toISOString()
+      : null,
+
     updated: video.updatedAt.toISOString(),
     mediaType: 'text/markdown',
     content: video.description,

+ 5 - 1
server/models/video/video-live.ts

@@ -93,7 +93,11 @@ export class VideoLiveModel extends Model<VideoLiveModel> {
 
   toFormattedJSON (): LiveVideo {
     return {
-      rtmpUrl: WEBSERVER.RTMP_URL,
+      // If we don't have a stream key, it means this is a remote live so we don't specify the rtmp URL
+      rtmpUrl: this.streamKey
+        ? WEBSERVER.RTMP_URL
+        : null,
+
       streamKey: this.streamKey,
       saveReplay: this.saveReplay
     }

+ 26 - 4
server/models/video/video.ts

@@ -26,6 +26,7 @@ import {
 } from 'sequelize-typescript'
 import { buildNSFWFilter } from '@server/helpers/express-utils'
 import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video'
+import { LiveManager } from '@server/lib/live-manager'
 import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
 import { getServerActor } from '@server/models/application/application'
 import { ModelCache } from '@server/models/model-cache'
@@ -121,14 +122,13 @@ import {
   videoModelToFormattedJSON
 } from './video-format-utils'
 import { VideoImportModel } from './video-import'
+import { VideoLiveModel } from './video-live'
 import { VideoPlaylistElementModel } from './video-playlist-element'
 import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
 import { VideoShareModel } from './video-share'
 import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
 import { VideoTagModel } from './video-tag'
 import { VideoViewModel } from './video-view'
-import { LiveManager } from '@server/lib/live-manager'
-import { VideoLiveModel } from './video-live'
 
 export enum ScopeNames {
   AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@@ -142,7 +142,8 @@ export enum ScopeNames {
   WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
   WITH_USER_ID = 'WITH_USER_ID',
   WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES',
-  WITH_THUMBNAILS = 'WITH_THUMBNAILS'
+  WITH_THUMBNAILS = 'WITH_THUMBNAILS',
+  WITH_LIVE = 'WITH_LIVE'
 }
 
 export type ForAPIOptions = {
@@ -245,6 +246,14 @@ export type AvailableForListIDsOptions = {
       }
     ]
   },
+  [ScopeNames.WITH_LIVE]: {
+    include: [
+      {
+        model: VideoLiveModel,
+        required: false
+      }
+    ]
+  },
   [ScopeNames.WITH_USER_ID]: {
     include: [
       {
@@ -943,6 +952,17 @@ export class VideoModel extends Model<VideoModel> {
             }
           ]
         },
+        {
+          model: VideoStreamingPlaylistModel.unscoped(),
+          required: false,
+          include: [
+            {
+              model: VideoFileModel,
+              required: false
+            }
+          ]
+        },
+        VideoLiveModel,
         VideoFileModel,
         TagModel
       ]
@@ -1330,7 +1350,8 @@ export class VideoModel extends Model<VideoModel> {
       ScopeNames.WITH_SCHEDULED_UPDATE,
       ScopeNames.WITH_WEBTORRENT_FILES,
       ScopeNames.WITH_STREAMING_PLAYLISTS,
-      ScopeNames.WITH_THUMBNAILS
+      ScopeNames.WITH_THUMBNAILS,
+      ScopeNames.WITH_LIVE
     ]
 
     if (userId) {
@@ -1362,6 +1383,7 @@ export class VideoModel extends Model<VideoModel> {
       ScopeNames.WITH_ACCOUNT_DETAILS,
       ScopeNames.WITH_SCHEDULED_UPDATE,
       ScopeNames.WITH_THUMBNAILS,
+      ScopeNames.WITH_LIVE,
       { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
       { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
     ]

+ 1 - 0
server/tests/api/live/index.ts

@@ -0,0 +1 @@
+export * from './live'

+ 351 - 0
server/tests/api/live/live.ts

@@ -0,0 +1,351 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import 'mocha'
+import * as chai from 'chai'
+import { LiveVideo, LiveVideoCreate, VideoDetails, VideoPrivacy } from '@shared/models'
+import {
+  acceptChangeOwnership,
+  cleanupTests,
+  createLive,
+  doubleFollow,
+  flushAndRunMultipleServers,
+  getLive,
+  getVideo,
+  getVideosList,
+  makeRawRequest,
+  removeVideo,
+  ServerInfo,
+  setAccessTokensToServers,
+  setDefaultVideoChannel,
+  testImage,
+  updateCustomSubConfig,
+  updateLive,
+  waitJobs
+} from '../../../../shared/extra-utils'
+
+const expect = chai.expect
+
+describe('Test live', function () {
+  let servers: ServerInfo[] = []
+  let liveVideoUUID: string
+
+  before(async function () {
+    this.timeout(120000)
+
+    servers = await flushAndRunMultipleServers(2)
+
+    // Get the access tokens
+    await setAccessTokensToServers(servers)
+    await setDefaultVideoChannel(servers)
+
+    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
+      live: {
+        enabled: true,
+        allowReplay: true
+      }
+    })
+
+    // Server 1 and server 2 follow each other
+    await doubleFollow(servers[0], servers[1])
+  })
+
+  describe('Live creation, update and delete', function () {
+
+    it('Should create a live with the appropriate parameters', async function () {
+      this.timeout(20000)
+
+      const attributes: LiveVideoCreate = {
+        category: 1,
+        licence: 2,
+        language: 'fr',
+        description: 'super live description',
+        support: 'support field',
+        channelId: servers[0].videoChannel.id,
+        nsfw: false,
+        waitTranscoding: false,
+        name: 'my super live',
+        tags: [ 'tag1', 'tag2' ],
+        commentsEnabled: false,
+        downloadEnabled: false,
+        saveReplay: true,
+        privacy: VideoPrivacy.PUBLIC,
+        previewfile: 'video_short1-preview.webm.jpg',
+        thumbnailfile: 'video_short1.webm.jpg'
+      }
+
+      const res = await createLive(servers[0].url, servers[0].accessToken, attributes)
+      liveVideoUUID = res.body.video.uuid
+
+      await waitJobs(servers)
+
+      for (const server of servers) {
+        const resVideo = await getVideo(server.url, liveVideoUUID)
+        const video: VideoDetails = resVideo.body
+
+        expect(video.category.id).to.equal(1)
+        expect(video.licence.id).to.equal(2)
+        expect(video.language.id).to.equal('fr')
+        expect(video.description).to.equal('super live description')
+        expect(video.support).to.equal('support field')
+
+        expect(video.channel.name).to.equal(servers[0].videoChannel.name)
+        expect(video.channel.host).to.equal(servers[0].videoChannel.host)
+
+        expect(video.nsfw).to.be.false
+        expect(video.waitTranscoding).to.be.false
+        expect(video.name).to.equal('my super live')
+        expect(video.tags).to.deep.equal([ 'tag1', 'tag2' ])
+        expect(video.commentsEnabled).to.be.false
+        expect(video.downloadEnabled).to.be.false
+        expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC)
+
+        await testImage(server.url, 'video_short1-preview.webm', video.previewPath)
+        await testImage(server.url, 'video_short1.webm', video.thumbnailPath)
+
+        const resLive = await getLive(server.url, server.accessToken, liveVideoUUID)
+        const live: LiveVideo = resLive.body
+
+        if (server.url === servers[0].url) {
+          expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':1936/live')
+          expect(live.streamKey).to.not.be.empty
+        } else {
+          expect(live.rtmpUrl).to.be.null
+          expect(live.streamKey).to.be.null
+        }
+
+        expect(live.saveReplay).to.be.true
+      }
+    })
+
+    it('Should have a default preview and thumbnail', async function () {
+      this.timeout(20000)
+
+      const attributes: LiveVideoCreate = {
+        name: 'default live thumbnail',
+        channelId: servers[0].videoChannel.id,
+        privacy: VideoPrivacy.UNLISTED,
+        nsfw: true
+      }
+
+      const res = await createLive(servers[0].url, servers[0].accessToken, attributes)
+      const videoId = res.body.video.uuid
+
+      await waitJobs(servers)
+
+      for (const server of servers) {
+        const resVideo = await getVideo(server.url, videoId)
+        const video: VideoDetails = resVideo.body
+
+        expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED)
+        expect(video.nsfw).to.be.true
+
+        await makeRawRequest(server.url + video.thumbnailPath, 200)
+        await makeRawRequest(server.url + video.previewPath, 200)
+      }
+    })
+
+    it('Should not have the live listed since nobody streams into', async function () {
+      for (const server of servers) {
+        const res = await getVideosList(server.url)
+
+        expect(res.body.total).to.equal(0)
+        expect(res.body.data).to.have.lengthOf(0)
+      }
+    })
+
+    it('Should not be able to update a live of another server', async function () {
+      await updateLive(servers[1].url, servers[1].accessToken, liveVideoUUID, { saveReplay: false }, 403)
+    })
+
+    it('Should update the live', async function () {
+      this.timeout(10000)
+
+      await updateLive(servers[0].url, servers[0].accessToken, liveVideoUUID, { saveReplay: false })
+      await waitJobs(servers)
+    })
+
+    it('Have the live updated', async function () {
+      for (const server of servers) {
+        const res = await getLive(server.url, server.accessToken, liveVideoUUID)
+        const live: LiveVideo = res.body
+
+        if (server.url === servers[0].url) {
+          expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':1936/live')
+          expect(live.streamKey).to.not.be.empty
+        } else {
+          expect(live.rtmpUrl).to.be.null
+          expect(live.streamKey).to.be.null
+        }
+
+        expect(live.saveReplay).to.be.false
+      }
+    })
+
+    it('Delete the live', async function () {
+      this.timeout(10000)
+
+      await removeVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
+      await waitJobs(servers)
+    })
+
+    it('Should have the live deleted', async function () {
+      for (const server of servers) {
+        await getVideo(server.url, liveVideoUUID, 404)
+        await getLive(server.url, server.accessToken, liveVideoUUID, 404)
+      }
+    })
+  })
+
+  describe('Test live constraints', function () {
+
+    it('Should not have size limit if save replay is disabled', async function () {
+
+    })
+
+    it('Should have size limit if save replay is enabled', async function () {
+      // daily quota + total quota
+
+    })
+
+    it('Should have max duration limit', async function () {
+
+    })
+  })
+
+  describe('With save replay disabled', function () {
+
+    it('Should correctly create and federate the "waiting for stream" live', async function () {
+
+    })
+
+    it('Should correctly have updated the live and federated it when streaming in the live', async function () {
+
+    })
+
+    it('Should correctly delete the video and the live after the stream ended', async function () {
+      // Wait 10 seconds
+      // get video 404
+      // get video federation 404
+
+      // check cleanup
+    })
+
+    it('Should correctly terminate the stream on blacklist and delete the live', async function () {
+      // Wait 10 seconds
+      // get video 404
+      // get video federation 404
+
+      // check cleanup
+    })
+
+    it('Should correctly terminate the stream on delete and delete the video', async function () {
+      // Wait 10 seconds
+      // get video 404
+      // get video federation 404
+
+      // check cleanup
+    })
+  })
+
+  describe('With save replay enabled', function () {
+
+    it('Should correctly create and federate the "waiting for stream" live', async function () {
+
+    })
+
+    it('Should correctly have updated the live and federated it when streaming in the live', async function () {
+
+    })
+
+    it('Should correctly have saved the live and federated it after the streaming', async function () {
+
+    })
+
+    it('Should update the saved live and correctly federate the updated attributes', async function () {
+
+    })
+
+    it('Should have cleaned up the live files', async function () {
+
+    })
+
+    it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () {
+      // Wait 10 seconds
+      // get video -> blacklisted
+      // get video federation -> blacklisted
+
+      // check cleanup live files quand meme
+    })
+
+    it('Should correctly terminate the stream on delete and delete the video', async function () {
+      // Wait 10 seconds
+      // get video 404
+      // get video federation 404
+
+      // check cleanup
+    })
+  })
+
+  describe('Stream checks', function () {
+
+    it('Should not allow a stream without the appropriate path', async function () {
+
+    })
+
+    it('Should not allow a stream without the appropriate stream key', async function () {
+
+    })
+
+    it('Should not allow a stream on a live that was blacklisted', async function () {
+
+    })
+
+    it('Should not allow a stream on a live that was deleted', async function () {
+
+    })
+  })
+
+  describe('Live transcoding', function () {
+
+    it('Should enable transcoding without additional resolutions', async function () {
+      // enable
+      // stream
+      // wait federation + test
+
+    })
+
+    it('Should enable transcoding with some resolutions', async function () {
+      // enable
+      // stream
+      // wait federation + test
+    })
+
+    it('Should enable transcoding with some resolutions and correctly save them', async function () {
+      // enable
+      // stream
+      // end stream
+      // wait federation + test
+    })
+
+    it('Should correctly have cleaned up the live files', async function () {
+      // check files
+    })
+  })
+
+  describe('Live socket messages', function () {
+
+    it('Should correctly send a message when the live starts', async function () {
+      // local
+      // federation
+    })
+
+    it('Should correctly send a message when the live ends', async function () {
+      // local
+      // federation
+    })
+  })
+
+  after(async function () {
+    await cleanupTests(servers)
+  })
+})

+ 6 - 3
server/types/models/video/video.ts

@@ -21,6 +21,7 @@ import { MThumbnail } from './thumbnail'
 import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist'
 import { MScheduleVideoUpdate } from './schedule-video-update'
 import { MUserVideoHistoryTime } from '../user/user-video-history'
+import { MVideoLive } from './video-live'
 
 type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M>
 
@@ -29,7 +30,7 @@ type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M>
 export type MVideo =
   Omit<VideoModel, 'VideoChannel' | 'Tags' | 'Thumbnails' | 'VideoPlaylistElements' | 'VideoAbuses' |
   'VideoFiles' | 'VideoStreamingPlaylists' | 'VideoShares' | 'AccountVideoRates' | 'VideoComments' | 'VideoViews' | 'UserVideoHistories' |
-  'ScheduleVideoUpdate' | 'VideoBlacklist' | 'VideoImport' | 'VideoCaptions'>
+  'ScheduleVideoUpdate' | 'VideoBlacklist' | 'VideoImport' | 'VideoCaptions' | 'VideoLive'>
 
 // ############################################################################
 
@@ -151,7 +152,8 @@ export type MVideoFullLight =
   Use<'UserVideoHistories', MUserVideoHistoryTime[]> &
   Use<'VideoFiles', MVideoFile[]> &
   Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> &
-  Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]>
+  Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> &
+  Use<'VideoLive', MVideoLive>
 
 // ############################################################################
 
@@ -165,7 +167,8 @@ export type MVideoAP =
   Use<'VideoCaptions', MVideoCaptionLanguageUrl[]> &
   Use<'VideoBlacklist', MVideoBlacklistUnfederated> &
   Use<'VideoFiles', MVideoFileRedundanciesOpt[]> &
-  Use<'Thumbnails', MThumbnail[]>
+  Use<'Thumbnails', MThumbnail[]> &
+  Use<'VideoLive', MVideoLive>
 
 export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'>
 

+ 4 - 1
shared/extra-utils/server/servers.ts

@@ -10,10 +10,12 @@ import { randomInt } from '../../core-utils/miscs/miscs'
 
 interface ServerInfo {
   app: ChildProcess
+
   url: string
   host: string
-
+  hostname: string
   port: number
+
   parallel: boolean
   internalServerNumber: number
   serverNumber: number
@@ -109,6 +111,7 @@ async function flushAndRunServer (serverNumber: number, configOverride?: Object,
     serverNumber,
     url: `http://localhost:${port}`,
     host: `localhost:${port}`,
+    hostname: 'localhost',
     client: {
       id: null,
       secret: null

+ 8 - 6
shared/extra-utils/videos/live.ts

@@ -2,8 +2,8 @@ import * as ffmpeg from 'fluent-ffmpeg'
 import { LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models'
 import { buildAbsoluteFixturePath, wait } from '../miscs/miscs'
 import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
-import { ServerInfo } from '../server/servers'
-import { getVideo, getVideoWithToken } from './videos'
+import { getVideoWithToken } from './videos'
+import { omit } from 'lodash'
 
 function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = 200) {
   const path = '/api/v1/videos/live'
@@ -31,16 +31,18 @@ function updateLive (url: string, token: string, videoId: number | string, field
 function createLive (url: string, token: string, fields: LiveVideoCreate, statusCodeExpected = 200) {
   const path = '/api/v1/videos/live'
 
-  let attaches: any = {}
-  if (fields.thumbnailfile) attaches = { thumbnailfile: fields.thumbnailfile }
-  if (fields.previewfile) attaches = { previewfile: fields.previewfile }
+  const attaches: any = {}
+  if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
+  if (fields.previewfile) attaches.previewfile = fields.previewfile
+
+  const updatedFields = omit(fields, 'thumbnailfile', 'previewfile')
 
   return makeUploadRequest({
     url,
     path,
     token,
     attaches,
-    fields,
+    fields: updatedFields,
     statusCodeExpected
   })
 }

+ 2 - 0
shared/models/activitypub/objects/video-torrent-object.ts

@@ -21,7 +21,9 @@ export interface VideoObject {
   views: number
 
   sensitive: boolean
+
   isLiveBroadcast: boolean
+  liveSaveReplay: boolean
 
   commentsEnabled: boolean
   downloadEnabled: boolean

+ 1 - 0
shared/models/users/user-right.enum.ts

@@ -30,6 +30,7 @@ export const enum UserRight {
   UPDATE_ANY_VIDEO,
   UPDATE_ANY_VIDEO_PLAYLIST,
 
+  GET_ANY_LIVE,
   SEE_ALL_VIDEOS,
   CHANGE_VIDEO_OWNERSHIP,
 

+ 2 - 2
shared/models/videos/video-create.model.ts

@@ -18,6 +18,6 @@ export interface VideoCreate {
   scheduleUpdate?: VideoScheduleUpdate
   originallyPublishedAt?: Date | string
 
-  thumbnailfile?: Blob
-  previewfile?: Blob
+  thumbnailfile?: Blob | string
+  previewfile?: Blob | string
 }