aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rwxr-xr-xscripts/ci.sh3
-rw-r--r--server/assets/default-audio-background.jpgbin14048 -> 7987 bytes
-rw-r--r--server/assets/default-live-background.jpgbin93634 -> 40557 bytes
-rw-r--r--server/controllers/activitypub/client.ts2
-rw-r--r--server/controllers/api/videos/index.ts2
-rw-r--r--server/controllers/api/videos/live.ts10
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts2
-rw-r--r--server/helpers/middlewares/videos.ts4
-rw-r--r--server/lib/activitypub/videos.ts36
-rw-r--r--server/middlewares/validators/videos/video-live.ts10
-rw-r--r--server/models/video/video-format-utils.ts11
-rw-r--r--server/models/video/video-live.ts6
-rw-r--r--server/models/video/video.ts30
-rw-r--r--server/tests/api/live/index.ts1
-rw-r--r--server/tests/api/live/live.ts351
-rw-r--r--server/types/models/video/video.ts9
-rw-r--r--shared/extra-utils/server/servers.ts5
-rw-r--r--shared/extra-utils/videos/live.ts14
-rw-r--r--shared/models/activitypub/objects/video-torrent-object.ts2
-rw-r--r--shared/models/users/user-right.enum.ts1
-rw-r--r--shared/models/videos/video-create.model.ts4
21 files changed, 472 insertions, 31 deletions
diff --git a/scripts/ci.sh b/scripts/ci.sh
index 486666c6a..e29b07ad7 100755
--- a/scripts/ci.sh
+++ b/scripts/ci.sh
@@ -58,8 +58,9 @@ elif [ "$1" = "api-2" ]; then
58 58
59 serverFiles=$(findTestFiles server/tests/api/server) 59 serverFiles=$(findTestFiles server/tests/api/server)
60 usersFiles=$(findTestFiles server/tests/api/users) 60 usersFiles=$(findTestFiles server/tests/api/users)
61 liveFiles=$(findTestFiles server/tests/api/live)
61 62
62 MOCHA_PARALLEL=true runTest 2 $serverFiles $usersFiles 63 MOCHA_PARALLEL=true runTest 2 $serverFiles $usersFiles liveFiles
63elif [ "$1" = "api-3" ]; then 64elif [ "$1" = "api-3" ]; then
64 npm run build:server 65 npm run build:server
65 66
diff --git a/server/assets/default-audio-background.jpg b/server/assets/default-audio-background.jpg
index a19173eac..0aef989a7 100644
--- a/server/assets/default-audio-background.jpg
+++ b/server/assets/default-audio-background.jpg
Binary files differ
diff --git a/server/assets/default-live-background.jpg b/server/assets/default-live-background.jpg
index 2743af7fc..1fd20e407 100644
--- a/server/assets/default-live-background.jpg
+++ b/server/assets/default-live-background.jpg
Binary files differ
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 1da44d096..df2a01d2c 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -223,7 +223,7 @@ function getAccountVideoRateFactory (rateType: VideoRateType) {
223 223
224async function videoController (req: express.Request, res: express.Response) { 224async function videoController (req: express.Request, res: express.Response) {
225 // We need more attributes 225 // We need more attributes
226 const video = await VideoModel.loadForGetAPI({ id: res.locals.onlyVideoWithRights.id }) as MVideoAPWithoutCaption 226 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(res.locals.onlyVideoWithRights.id)
227 227
228 if (video.url.startsWith(WEBSERVER.URL) === false) return res.redirect(video.url) 228 if (video.url.startsWith(WEBSERVER.URL) === false) return res.redirect(video.url)
229 229
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 6357062bc..50e769e77 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -189,7 +189,7 @@ async function addVideo (req: express.Request, res: express.Response) {
189 videoData.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED 189 videoData.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED
190 videoData.duration = videoPhysicalFile['duration'] // duration was added by a previous middleware 190 videoData.duration = videoPhysicalFile['duration'] // duration was added by a previous middleware
191 191
192 const video = new VideoModel(videoData) as MVideoDetails 192 const video = new VideoModel(videoData) as MVideoFullLight
193 video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object 193 video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
194 194
195 const videoFile = new VideoFileModel({ 195 const videoFile = new VideoFileModel({
diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts
index be46fb1c6..f980c7730 100644
--- a/server/controllers/api/videos/live.ts
+++ b/server/controllers/api/videos/live.ts
@@ -4,6 +4,7 @@ import { createReqFiles } from '@server/helpers/express-utils'
4import { CONFIG } from '@server/initializers/config' 4import { CONFIG } from '@server/initializers/config'
5import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants' 5import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants'
6import { getVideoActivityPubUrl } from '@server/lib/activitypub/url' 6import { getVideoActivityPubUrl } from '@server/lib/activitypub/url'
7import { federateVideoIfNeeded } from '@server/lib/activitypub/videos'
7import { buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' 8import { buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
8import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator } from '@server/middlewares/validators/videos/video-live' 9import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator } from '@server/middlewares/validators/videos/video-live'
9import { VideoLiveModel } from '@server/models/video/video-live' 10import { VideoLiveModel } from '@server/models/video/video-live'
@@ -63,10 +64,13 @@ async function getLiveVideo (req: express.Request, res: express.Response) {
63async function updateLiveVideo (req: express.Request, res: express.Response) { 64async function updateLiveVideo (req: express.Request, res: express.Response) {
64 const body: LiveVideoUpdate = req.body 65 const body: LiveVideoUpdate = req.body
65 66
67 const video = res.locals.videoAll
66 const videoLive = res.locals.videoLive 68 const videoLive = res.locals.videoLive
67 videoLive.saveReplay = body.saveReplay || false 69 videoLive.saveReplay = body.saveReplay || false
68 70
69 await videoLive.save() 71 video.VideoLive = await videoLive.save()
72
73 await federateVideoIfNeeded(video, false)
70 74
71 return res.sendStatus(204) 75 return res.sendStatus(204)
72} 76}
@@ -113,10 +117,12 @@ async function addLiveVideo (req: express.Request, res: express.Response) {
113 videoCreated.VideoChannel = res.locals.videoChannel 117 videoCreated.VideoChannel = res.locals.videoChannel
114 118
115 videoLive.videoId = videoCreated.id 119 videoLive.videoId = videoCreated.id
116 await videoLive.save(sequelizeOptions) 120 videoCreated.VideoLive = await videoLive.save(sequelizeOptions)
117 121
118 await setVideoTags({ video, tags: videoInfo.tags, transaction: t }) 122 await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
119 123
124 await federateVideoIfNeeded(videoCreated, true, t)
125
120 logger.info('Video live %s with uuid %s created.', videoInfo.name, videoCreated.uuid) 126 logger.info('Video live %s with uuid %s created.', videoInfo.name, videoCreated.uuid)
121 127
122 return { videoCreated } 128 return { videoCreated }
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index 7ff551ecd..cb385b07d 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -63,6 +63,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
63 if (!isBooleanValid(video.downloadEnabled)) video.downloadEnabled = true 63 if (!isBooleanValid(video.downloadEnabled)) video.downloadEnabled = true
64 if (!isBooleanValid(video.commentsEnabled)) video.commentsEnabled = false 64 if (!isBooleanValid(video.commentsEnabled)) video.commentsEnabled = false
65 if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false 65 if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false
66 if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false
66 67
67 return isActivityPubUrlValid(video.id) && 68 return isActivityPubUrlValid(video.id) &&
68 isVideoNameValid(video.name) && 69 isVideoNameValid(video.name) &&
@@ -79,7 +80,6 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
79 isDateValid(video.updated) && 80 isDateValid(video.updated) &&
80 (!video.originallyPublishedAt || isDateValid(video.originallyPublishedAt)) && 81 (!video.originallyPublishedAt || isDateValid(video.originallyPublishedAt)) &&
81 (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) && 82 (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) &&
82 video.url.length !== 0 &&
83 video.attributedTo.length !== 0 83 video.attributedTo.length !== 0
84} 84}
85 85
diff --git a/server/helpers/middlewares/videos.ts b/server/helpers/middlewares/videos.ts
index 77a48a467..3904f762a 100644
--- a/server/helpers/middlewares/videos.ts
+++ b/server/helpers/middlewares/videos.ts
@@ -92,9 +92,9 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
92 return true 92 return true
93} 93}
94 94
95function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response) { 95function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) {
96 // Retrieve the user who did the request 96 // Retrieve the user who did the request
97 if (video.isOwned() === false) { 97 if (onlyOwned && video.isOwned() === false) {
98 res.status(403) 98 res.status(403)
99 .json({ error: 'Cannot manage a video of another server.' }) 99 .json({ error: 'Cannot manage a video of another server.' })
100 .end() 100 .end()
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index ab23ff507..ea1e6a38f 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -1,3 +1,4 @@
1import { VideoLiveModel } from '@server/models/video/video-live'
1import * as Bluebird from 'bluebird' 2import * as Bluebird from 'bluebird'
2import { maxBy, minBy } from 'lodash' 3import { maxBy, minBy } from 'lodash'
3import * as magnetUtil from 'magnet-uri' 4import * as magnetUtil from 'magnet-uri'
@@ -84,7 +85,7 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid
84 // Check this is not a blacklisted video, or unfederated blacklisted video 85 // Check this is not a blacklisted video, or unfederated blacklisted video
85 (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) && 86 (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) &&
86 // Check the video is public/unlisted and published 87 // Check the video is public/unlisted and published
87 video.hasPrivacyForFederation() && video.state === VideoState.PUBLISHED 88 video.hasPrivacyForFederation() && (video.state === VideoState.PUBLISHED || video.state === VideoState.WAITING_FOR_LIVE)
88 ) { 89 ) {
89 // Fetch more attributes that we will need to serialize in AP object 90 // Fetch more attributes that we will need to serialize in AP object
90 if (isArray(video.VideoCaptions) === false) { 91 if (isArray(video.VideoCaptions) === false) {
@@ -424,6 +425,27 @@ async function updateVideoFromAP (options: {
424 await Promise.all(videoCaptionsPromises) 425 await Promise.all(videoCaptionsPromises)
425 } 426 }
426 427
428 {
429 // Create or update existing live
430 if (video.isLive) {
431 const [ videoLive ] = await VideoLiveModel.upsert({
432 saveReplay: videoObject.liveSaveReplay,
433 videoId: video.id
434 }, { transaction: t, returning: true })
435
436 videoUpdated.VideoLive = videoLive
437 } else { // Delete existing live if it exists
438 await VideoLiveModel.destroy({
439 where: {
440 videoId: video.id
441 },
442 transaction: t
443 })
444
445 videoUpdated.VideoLive = null
446 }
447 }
448
427 return videoUpdated 449 return videoUpdated
428 }) 450 })
429 451
@@ -436,7 +458,7 @@ async function updateVideoFromAP (options: {
436 }) 458 })
437 459
438 if (wasPrivateVideo || wasUnlistedVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) // Notify our users? 460 if (wasPrivateVideo || wasUnlistedVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) // Notify our users?
439 if (videoUpdated.isLive) PeerTubeSocket.Instance.sendVideoLiveNewState(video) 461 if (videoUpdated.isLive) PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated)
440 462
441 logger.info('Remote video with uuid %s updated', videoObject.uuid) 463 logger.info('Remote video with uuid %s updated', videoObject.uuid)
442 464
@@ -606,6 +628,16 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
606 628
607 videoCreated.VideoFiles = videoFiles 629 videoCreated.VideoFiles = videoFiles
608 630
631 if (videoCreated.isLive) {
632 const videoLive = new VideoLiveModel({
633 streamKey: null,
634 saveReplay: videoObject.liveSaveReplay,
635 videoId: videoCreated.id
636 })
637
638 videoCreated.VideoLive = await videoLive.save({ transaction: t })
639 }
640
609 const autoBlacklisted = await autoBlacklistVideoIfNeeded({ 641 const autoBlacklisted = await autoBlacklistVideoIfNeeded({
610 video: videoCreated, 642 video: videoCreated,
611 user: undefined, 643 user: undefined,
diff --git a/server/middlewares/validators/videos/video-live.ts b/server/middlewares/validators/videos/video-live.ts
index 69200cb60..cbc48fe93 100644
--- a/server/middlewares/validators/videos/video-live.ts
+++ b/server/middlewares/validators/videos/video-live.ts
@@ -16,14 +16,14 @@ const videoLiveGetValidator = [
16 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 16 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
17 17
18 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 18 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
19 logger.debug('Checking videoLiveGetValidator parameters', { parameters: req.body }) 19 logger.debug('Checking videoLiveGetValidator parameters', { parameters: req.params, user: res.locals.oauth.token.User.username })
20 20
21 if (areValidationErrors(req, res)) return 21 if (areValidationErrors(req, res)) return
22 if (!await doesVideoExist(req.params.videoId, res, 'all')) return 22 if (!await doesVideoExist(req.params.videoId, res, 'all')) return
23 23
24 // Check if the user who did the request is able to update the video 24 // Check if the user who did the request is able to get the live info
25 const user = res.locals.oauth.token.User 25 const user = res.locals.oauth.token.User
26 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return 26 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res, false)) return
27 27
28 const videoLive = await VideoLiveModel.loadByVideoId(res.locals.videoAll.id) 28 const videoLive = await VideoLiveModel.loadByVideoId(res.locals.videoAll.id)
29 if (!videoLive) return res.sendStatus(404) 29 if (!videoLive) return res.sendStatus(404)
@@ -122,6 +122,10 @@ const videoLiveUpdateValidator = [
122 .json({ error: 'Cannot update a live that has already started' }) 122 .json({ error: 'Cannot update a live that has already started' })
123 } 123 }
124 124
125 // Check the user can manage the live
126 const user = res.locals.oauth.token.User
127 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res)) return
128
125 return next() 129 return next()
126 } 130 }
127] 131]
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index 92bde7773..04e636a15 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -352,11 +352,20 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
352 sensitive: video.nsfw, 352 sensitive: video.nsfw,
353 waitTranscoding: video.waitTranscoding, 353 waitTranscoding: video.waitTranscoding,
354 isLiveBroadcast: video.isLive, 354 isLiveBroadcast: video.isLive,
355
356 liveSaveReplay: video.isLive
357 ? video.VideoLive.saveReplay
358 : null,
359
355 state: video.state, 360 state: video.state,
356 commentsEnabled: video.commentsEnabled, 361 commentsEnabled: video.commentsEnabled,
357 downloadEnabled: video.downloadEnabled, 362 downloadEnabled: video.downloadEnabled,
358 published: video.publishedAt.toISOString(), 363 published: video.publishedAt.toISOString(),
359 originallyPublishedAt: video.originallyPublishedAt ? video.originallyPublishedAt.toISOString() : null, 364
365 originallyPublishedAt: video.originallyPublishedAt
366 ? video.originallyPublishedAt.toISOString()
367 : null,
368
360 updated: video.updatedAt.toISOString(), 369 updated: video.updatedAt.toISOString(),
361 mediaType: 'text/markdown', 370 mediaType: 'text/markdown',
362 content: video.description, 371 content: video.description,
diff --git a/server/models/video/video-live.ts b/server/models/video/video-live.ts
index 345918cb9..f3bff74ea 100644
--- a/server/models/video/video-live.ts
+++ b/server/models/video/video-live.ts
@@ -93,7 +93,11 @@ export class VideoLiveModel extends Model<VideoLiveModel> {
93 93
94 toFormattedJSON (): LiveVideo { 94 toFormattedJSON (): LiveVideo {
95 return { 95 return {
96 rtmpUrl: WEBSERVER.RTMP_URL, 96 // If we don't have a stream key, it means this is a remote live so we don't specify the rtmp URL
97 rtmpUrl: this.streamKey
98 ? WEBSERVER.RTMP_URL
99 : null,
100
97 streamKey: this.streamKey, 101 streamKey: this.streamKey,
98 saveReplay: this.saveReplay 102 saveReplay: this.saveReplay
99 } 103 }
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index d094f19b0..aba8c8cf4 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -26,6 +26,7 @@ import {
26} from 'sequelize-typescript' 26} from 'sequelize-typescript'
27import { buildNSFWFilter } from '@server/helpers/express-utils' 27import { buildNSFWFilter } from '@server/helpers/express-utils'
28import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video' 28import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video'
29import { LiveManager } from '@server/lib/live-manager'
29import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 30import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
30import { getServerActor } from '@server/models/application/application' 31import { getServerActor } from '@server/models/application/application'
31import { ModelCache } from '@server/models/model-cache' 32import { ModelCache } from '@server/models/model-cache'
@@ -121,14 +122,13 @@ import {
121 videoModelToFormattedJSON 122 videoModelToFormattedJSON
122} from './video-format-utils' 123} from './video-format-utils'
123import { VideoImportModel } from './video-import' 124import { VideoImportModel } from './video-import'
125import { VideoLiveModel } from './video-live'
124import { VideoPlaylistElementModel } from './video-playlist-element' 126import { VideoPlaylistElementModel } from './video-playlist-element'
125import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder' 127import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
126import { VideoShareModel } from './video-share' 128import { VideoShareModel } from './video-share'
127import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 129import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
128import { VideoTagModel } from './video-tag' 130import { VideoTagModel } from './video-tag'
129import { VideoViewModel } from './video-view' 131import { VideoViewModel } from './video-view'
130import { LiveManager } from '@server/lib/live-manager'
131import { VideoLiveModel } from './video-live'
132 132
133export enum ScopeNames { 133export enum ScopeNames {
134 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', 134 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@@ -142,7 +142,8 @@ export enum ScopeNames {
142 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', 142 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
143 WITH_USER_ID = 'WITH_USER_ID', 143 WITH_USER_ID = 'WITH_USER_ID',
144 WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES', 144 WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES',
145 WITH_THUMBNAILS = 'WITH_THUMBNAILS' 145 WITH_THUMBNAILS = 'WITH_THUMBNAILS',
146 WITH_LIVE = 'WITH_LIVE'
146} 147}
147 148
148export type ForAPIOptions = { 149export type ForAPIOptions = {
@@ -245,6 +246,14 @@ export type AvailableForListIDsOptions = {
245 } 246 }
246 ] 247 ]
247 }, 248 },
249 [ScopeNames.WITH_LIVE]: {
250 include: [
251 {
252 model: VideoLiveModel,
253 required: false
254 }
255 ]
256 },
248 [ScopeNames.WITH_USER_ID]: { 257 [ScopeNames.WITH_USER_ID]: {
249 include: [ 258 include: [
250 { 259 {
@@ -943,6 +952,17 @@ export class VideoModel extends Model<VideoModel> {
943 } 952 }
944 ] 953 ]
945 }, 954 },
955 {
956 model: VideoStreamingPlaylistModel.unscoped(),
957 required: false,
958 include: [
959 {
960 model: VideoFileModel,
961 required: false
962 }
963 ]
964 },
965 VideoLiveModel,
946 VideoFileModel, 966 VideoFileModel,
947 TagModel 967 TagModel
948 ] 968 ]
@@ -1330,7 +1350,8 @@ export class VideoModel extends Model<VideoModel> {
1330 ScopeNames.WITH_SCHEDULED_UPDATE, 1350 ScopeNames.WITH_SCHEDULED_UPDATE,
1331 ScopeNames.WITH_WEBTORRENT_FILES, 1351 ScopeNames.WITH_WEBTORRENT_FILES,
1332 ScopeNames.WITH_STREAMING_PLAYLISTS, 1352 ScopeNames.WITH_STREAMING_PLAYLISTS,
1333 ScopeNames.WITH_THUMBNAILS 1353 ScopeNames.WITH_THUMBNAILS,
1354 ScopeNames.WITH_LIVE
1334 ] 1355 ]
1335 1356
1336 if (userId) { 1357 if (userId) {
@@ -1362,6 +1383,7 @@ export class VideoModel extends Model<VideoModel> {
1362 ScopeNames.WITH_ACCOUNT_DETAILS, 1383 ScopeNames.WITH_ACCOUNT_DETAILS,
1363 ScopeNames.WITH_SCHEDULED_UPDATE, 1384 ScopeNames.WITH_SCHEDULED_UPDATE,
1364 ScopeNames.WITH_THUMBNAILS, 1385 ScopeNames.WITH_THUMBNAILS,
1386 ScopeNames.WITH_LIVE,
1365 { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] }, 1387 { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
1366 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } 1388 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
1367 ] 1389 ]
diff --git a/server/tests/api/live/index.ts b/server/tests/api/live/index.ts
new file mode 100644
index 000000000..280daf423
--- /dev/null
+++ b/server/tests/api/live/index.ts
@@ -0,0 +1 @@
export * from './live'
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts
new file mode 100644
index 000000000..e66c0cb26
--- /dev/null
+++ b/server/tests/api/live/live.ts
@@ -0,0 +1,351 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import * as chai from 'chai'
5import { LiveVideo, LiveVideoCreate, VideoDetails, VideoPrivacy } from '@shared/models'
6import {
7 acceptChangeOwnership,
8 cleanupTests,
9 createLive,
10 doubleFollow,
11 flushAndRunMultipleServers,
12 getLive,
13 getVideo,
14 getVideosList,
15 makeRawRequest,
16 removeVideo,
17 ServerInfo,
18 setAccessTokensToServers,
19 setDefaultVideoChannel,
20 testImage,
21 updateCustomSubConfig,
22 updateLive,
23 waitJobs
24} from '../../../../shared/extra-utils'
25
26const expect = chai.expect
27
28describe('Test live', function () {
29 let servers: ServerInfo[] = []
30 let liveVideoUUID: string
31
32 before(async function () {
33 this.timeout(120000)
34
35 servers = await flushAndRunMultipleServers(2)
36
37 // Get the access tokens
38 await setAccessTokensToServers(servers)
39 await setDefaultVideoChannel(servers)
40
41 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
42 live: {
43 enabled: true,
44 allowReplay: true
45 }
46 })
47
48 // Server 1 and server 2 follow each other
49 await doubleFollow(servers[0], servers[1])
50 })
51
52 describe('Live creation, update and delete', function () {
53
54 it('Should create a live with the appropriate parameters', async function () {
55 this.timeout(20000)
56
57 const attributes: LiveVideoCreate = {
58 category: 1,
59 licence: 2,
60 language: 'fr',
61 description: 'super live description',
62 support: 'support field',
63 channelId: servers[0].videoChannel.id,
64 nsfw: false,
65 waitTranscoding: false,
66 name: 'my super live',
67 tags: [ 'tag1', 'tag2' ],
68 commentsEnabled: false,
69 downloadEnabled: false,
70 saveReplay: true,
71 privacy: VideoPrivacy.PUBLIC,
72 previewfile: 'video_short1-preview.webm.jpg',
73 thumbnailfile: 'video_short1.webm.jpg'
74 }
75
76 const res = await createLive(servers[0].url, servers[0].accessToken, attributes)
77 liveVideoUUID = res.body.video.uuid
78
79 await waitJobs(servers)
80
81 for (const server of servers) {
82 const resVideo = await getVideo(server.url, liveVideoUUID)
83 const video: VideoDetails = resVideo.body
84
85 expect(video.category.id).to.equal(1)
86 expect(video.licence.id).to.equal(2)
87 expect(video.language.id).to.equal('fr')
88 expect(video.description).to.equal('super live description')
89 expect(video.support).to.equal('support field')
90
91 expect(video.channel.name).to.equal(servers[0].videoChannel.name)
92 expect(video.channel.host).to.equal(servers[0].videoChannel.host)
93
94 expect(video.nsfw).to.be.false
95 expect(video.waitTranscoding).to.be.false
96 expect(video.name).to.equal('my super live')
97 expect(video.tags).to.deep.equal([ 'tag1', 'tag2' ])
98 expect(video.commentsEnabled).to.be.false
99 expect(video.downloadEnabled).to.be.false
100 expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC)
101
102 await testImage(server.url, 'video_short1-preview.webm', video.previewPath)
103 await testImage(server.url, 'video_short1.webm', video.thumbnailPath)
104
105 const resLive = await getLive(server.url, server.accessToken, liveVideoUUID)
106 const live: LiveVideo = resLive.body
107
108 if (server.url === servers[0].url) {
109 expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':1936/live')
110 expect(live.streamKey).to.not.be.empty
111 } else {
112 expect(live.rtmpUrl).to.be.null
113 expect(live.streamKey).to.be.null
114 }
115
116 expect(live.saveReplay).to.be.true
117 }
118 })
119
120 it('Should have a default preview and thumbnail', async function () {
121 this.timeout(20000)
122
123 const attributes: LiveVideoCreate = {
124 name: 'default live thumbnail',
125 channelId: servers[0].videoChannel.id,
126 privacy: VideoPrivacy.UNLISTED,
127 nsfw: true
128 }
129
130 const res = await createLive(servers[0].url, servers[0].accessToken, attributes)
131 const videoId = res.body.video.uuid
132
133 await waitJobs(servers)
134
135 for (const server of servers) {
136 const resVideo = await getVideo(server.url, videoId)
137 const video: VideoDetails = resVideo.body
138
139 expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED)
140 expect(video.nsfw).to.be.true
141
142 await makeRawRequest(server.url + video.thumbnailPath, 200)
143 await makeRawRequest(server.url + video.previewPath, 200)
144 }
145 })
146
147 it('Should not have the live listed since nobody streams into', async function () {
148 for (const server of servers) {
149 const res = await getVideosList(server.url)
150
151 expect(res.body.total).to.equal(0)
152 expect(res.body.data).to.have.lengthOf(0)
153 }
154 })
155
156 it('Should not be able to update a live of another server', async function () {
157 await updateLive(servers[1].url, servers[1].accessToken, liveVideoUUID, { saveReplay: false }, 403)
158 })
159
160 it('Should update the live', async function () {
161 this.timeout(10000)
162
163 await updateLive(servers[0].url, servers[0].accessToken, liveVideoUUID, { saveReplay: false })
164 await waitJobs(servers)
165 })
166
167 it('Have the live updated', async function () {
168 for (const server of servers) {
169 const res = await getLive(server.url, server.accessToken, liveVideoUUID)
170 const live: LiveVideo = res.body
171
172 if (server.url === servers[0].url) {
173 expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':1936/live')
174 expect(live.streamKey).to.not.be.empty
175 } else {
176 expect(live.rtmpUrl).to.be.null
177 expect(live.streamKey).to.be.null
178 }
179
180 expect(live.saveReplay).to.be.false
181 }
182 })
183
184 it('Delete the live', async function () {
185 this.timeout(10000)
186
187 await removeVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
188 await waitJobs(servers)
189 })
190
191 it('Should have the live deleted', async function () {
192 for (const server of servers) {
193 await getVideo(server.url, liveVideoUUID, 404)
194 await getLive(server.url, server.accessToken, liveVideoUUID, 404)
195 }
196 })
197 })
198
199 describe('Test live constraints', function () {
200
201 it('Should not have size limit if save replay is disabled', async function () {
202
203 })
204
205 it('Should have size limit if save replay is enabled', async function () {
206 // daily quota + total quota
207
208 })
209
210 it('Should have max duration limit', async function () {
211
212 })
213 })
214
215 describe('With save replay disabled', function () {
216
217 it('Should correctly create and federate the "waiting for stream" live', async function () {
218
219 })
220
221 it('Should correctly have updated the live and federated it when streaming in the live', async function () {
222
223 })
224
225 it('Should correctly delete the video and the live after the stream ended', async function () {
226 // Wait 10 seconds
227 // get video 404
228 // get video federation 404
229
230 // check cleanup
231 })
232
233 it('Should correctly terminate the stream on blacklist and delete the live', async function () {
234 // Wait 10 seconds
235 // get video 404
236 // get video federation 404
237
238 // check cleanup
239 })
240
241 it('Should correctly terminate the stream on delete and delete the video', async function () {
242 // Wait 10 seconds
243 // get video 404
244 // get video federation 404
245
246 // check cleanup
247 })
248 })
249
250 describe('With save replay enabled', function () {
251
252 it('Should correctly create and federate the "waiting for stream" live', async function () {
253
254 })
255
256 it('Should correctly have updated the live and federated it when streaming in the live', async function () {
257
258 })
259
260 it('Should correctly have saved the live and federated it after the streaming', async function () {
261
262 })
263
264 it('Should update the saved live and correctly federate the updated attributes', async function () {
265
266 })
267
268 it('Should have cleaned up the live files', async function () {
269
270 })
271
272 it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () {
273 // Wait 10 seconds
274 // get video -> blacklisted
275 // get video federation -> blacklisted
276
277 // check cleanup live files quand meme
278 })
279
280 it('Should correctly terminate the stream on delete and delete the video', async function () {
281 // Wait 10 seconds
282 // get video 404
283 // get video federation 404
284
285 // check cleanup
286 })
287 })
288
289 describe('Stream checks', function () {
290
291 it('Should not allow a stream without the appropriate path', async function () {
292
293 })
294
295 it('Should not allow a stream without the appropriate stream key', async function () {
296
297 })
298
299 it('Should not allow a stream on a live that was blacklisted', async function () {
300
301 })
302
303 it('Should not allow a stream on a live that was deleted', async function () {
304
305 })
306 })
307
308 describe('Live transcoding', function () {
309
310 it('Should enable transcoding without additional resolutions', async function () {
311 // enable
312 // stream
313 // wait federation + test
314
315 })
316
317 it('Should enable transcoding with some resolutions', async function () {
318 // enable
319 // stream
320 // wait federation + test
321 })
322
323 it('Should enable transcoding with some resolutions and correctly save them', async function () {
324 // enable
325 // stream
326 // end stream
327 // wait federation + test
328 })
329
330 it('Should correctly have cleaned up the live files', async function () {
331 // check files
332 })
333 })
334
335 describe('Live socket messages', function () {
336
337 it('Should correctly send a message when the live starts', async function () {
338 // local
339 // federation
340 })
341
342 it('Should correctly send a message when the live ends', async function () {
343 // local
344 // federation
345 })
346 })
347
348 after(async function () {
349 await cleanupTests(servers)
350 })
351})
diff --git a/server/types/models/video/video.ts b/server/types/models/video/video.ts
index 3d8f85b3d..ae23cc30f 100644
--- a/server/types/models/video/video.ts
+++ b/server/types/models/video/video.ts
@@ -21,6 +21,7 @@ import { MThumbnail } from './thumbnail'
21import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist' 21import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist'
22import { MScheduleVideoUpdate } from './schedule-video-update' 22import { MScheduleVideoUpdate } from './schedule-video-update'
23import { MUserVideoHistoryTime } from '../user/user-video-history' 23import { MUserVideoHistoryTime } from '../user/user-video-history'
24import { MVideoLive } from './video-live'
24 25
25type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M> 26type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M>
26 27
@@ -29,7 +30,7 @@ type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M>
29export type MVideo = 30export type MVideo =
30 Omit<VideoModel, 'VideoChannel' | 'Tags' | 'Thumbnails' | 'VideoPlaylistElements' | 'VideoAbuses' | 31 Omit<VideoModel, 'VideoChannel' | 'Tags' | 'Thumbnails' | 'VideoPlaylistElements' | 'VideoAbuses' |
31 'VideoFiles' | 'VideoStreamingPlaylists' | 'VideoShares' | 'AccountVideoRates' | 'VideoComments' | 'VideoViews' | 'UserVideoHistories' | 32 'VideoFiles' | 'VideoStreamingPlaylists' | 'VideoShares' | 'AccountVideoRates' | 'VideoComments' | 'VideoViews' | 'UserVideoHistories' |
32 'ScheduleVideoUpdate' | 'VideoBlacklist' | 'VideoImport' | 'VideoCaptions'> 33 'ScheduleVideoUpdate' | 'VideoBlacklist' | 'VideoImport' | 'VideoCaptions' | 'VideoLive'>
33 34
34// ############################################################################ 35// ############################################################################
35 36
@@ -151,7 +152,8 @@ export type MVideoFullLight =
151 Use<'UserVideoHistories', MUserVideoHistoryTime[]> & 152 Use<'UserVideoHistories', MUserVideoHistoryTime[]> &
152 Use<'VideoFiles', MVideoFile[]> & 153 Use<'VideoFiles', MVideoFile[]> &
153 Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> & 154 Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> &
154 Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> 155 Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> &
156 Use<'VideoLive', MVideoLive>
155 157
156// ############################################################################ 158// ############################################################################
157 159
@@ -165,7 +167,8 @@ export type MVideoAP =
165 Use<'VideoCaptions', MVideoCaptionLanguageUrl[]> & 167 Use<'VideoCaptions', MVideoCaptionLanguageUrl[]> &
166 Use<'VideoBlacklist', MVideoBlacklistUnfederated> & 168 Use<'VideoBlacklist', MVideoBlacklistUnfederated> &
167 Use<'VideoFiles', MVideoFileRedundanciesOpt[]> & 169 Use<'VideoFiles', MVideoFileRedundanciesOpt[]> &
168 Use<'Thumbnails', MThumbnail[]> 170 Use<'Thumbnails', MThumbnail[]> &
171 Use<'VideoLive', MVideoLive>
169 172
170export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'> 173export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'>
171 174
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts
index 994aac628..b4bd55968 100644
--- a/shared/extra-utils/server/servers.ts
+++ b/shared/extra-utils/server/servers.ts
@@ -10,10 +10,12 @@ import { randomInt } from '../../core-utils/miscs/miscs'
10 10
11interface ServerInfo { 11interface ServerInfo {
12 app: ChildProcess 12 app: ChildProcess
13
13 url: string 14 url: string
14 host: string 15 host: string
15 16 hostname: string
16 port: number 17 port: number
18
17 parallel: boolean 19 parallel: boolean
18 internalServerNumber: number 20 internalServerNumber: number
19 serverNumber: number 21 serverNumber: number
@@ -109,6 +111,7 @@ async function flushAndRunServer (serverNumber: number, configOverride?: Object,
109 serverNumber, 111 serverNumber,
110 url: `http://localhost:${port}`, 112 url: `http://localhost:${port}`,
111 host: `localhost:${port}`, 113 host: `localhost:${port}`,
114 hostname: 'localhost',
112 client: { 115 client: {
113 id: null, 116 id: null,
114 secret: null 117 secret: null
diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts
index f500fdc3e..65942db0a 100644
--- a/shared/extra-utils/videos/live.ts
+++ b/shared/extra-utils/videos/live.ts
@@ -2,8 +2,8 @@ import * as ffmpeg from 'fluent-ffmpeg'
2import { LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models' 2import { LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models'
3import { buildAbsoluteFixturePath, wait } from '../miscs/miscs' 3import { buildAbsoluteFixturePath, wait } from '../miscs/miscs'
4import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests' 4import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
5import { ServerInfo } from '../server/servers' 5import { getVideoWithToken } from './videos'
6import { getVideo, getVideoWithToken } from './videos' 6import { omit } from 'lodash'
7 7
8function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = 200) { 8function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = 200) {
9 const path = '/api/v1/videos/live' 9 const path = '/api/v1/videos/live'
@@ -31,16 +31,18 @@ function updateLive (url: string, token: string, videoId: number | string, field
31function createLive (url: string, token: string, fields: LiveVideoCreate, statusCodeExpected = 200) { 31function createLive (url: string, token: string, fields: LiveVideoCreate, statusCodeExpected = 200) {
32 const path = '/api/v1/videos/live' 32 const path = '/api/v1/videos/live'
33 33
34 let attaches: any = {} 34 const attaches: any = {}
35 if (fields.thumbnailfile) attaches = { thumbnailfile: fields.thumbnailfile } 35 if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
36 if (fields.previewfile) attaches = { previewfile: fields.previewfile } 36 if (fields.previewfile) attaches.previewfile = fields.previewfile
37
38 const updatedFields = omit(fields, 'thumbnailfile', 'previewfile')
37 39
38 return makeUploadRequest({ 40 return makeUploadRequest({
39 url, 41 url,
40 path, 42 path,
41 token, 43 token,
42 attaches, 44 attaches,
43 fields, 45 fields: updatedFields,
44 statusCodeExpected 46 statusCodeExpected
45 }) 47 })
46} 48}
diff --git a/shared/models/activitypub/objects/video-torrent-object.ts b/shared/models/activitypub/objects/video-torrent-object.ts
index 5b035a371..d99d273c3 100644
--- a/shared/models/activitypub/objects/video-torrent-object.ts
+++ b/shared/models/activitypub/objects/video-torrent-object.ts
@@ -21,7 +21,9 @@ export interface VideoObject {
21 views: number 21 views: number
22 22
23 sensitive: boolean 23 sensitive: boolean
24
24 isLiveBroadcast: boolean 25 isLiveBroadcast: boolean
26 liveSaveReplay: boolean
25 27
26 commentsEnabled: boolean 28 commentsEnabled: boolean
27 downloadEnabled: boolean 29 downloadEnabled: boolean
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts
index 4c3d9e7c8..e815fa893 100644
--- a/shared/models/users/user-right.enum.ts
+++ b/shared/models/users/user-right.enum.ts
@@ -30,6 +30,7 @@ export const enum UserRight {
30 UPDATE_ANY_VIDEO, 30 UPDATE_ANY_VIDEO,
31 UPDATE_ANY_VIDEO_PLAYLIST, 31 UPDATE_ANY_VIDEO_PLAYLIST,
32 32
33 GET_ANY_LIVE,
33 SEE_ALL_VIDEOS, 34 SEE_ALL_VIDEOS,
34 CHANGE_VIDEO_OWNERSHIP, 35 CHANGE_VIDEO_OWNERSHIP,
35 36
diff --git a/shared/models/videos/video-create.model.ts b/shared/models/videos/video-create.model.ts
index 175327afa..9e980529d 100644
--- a/shared/models/videos/video-create.model.ts
+++ b/shared/models/videos/video-create.model.ts
@@ -18,6 +18,6 @@ export interface VideoCreate {
18 scheduleUpdate?: VideoScheduleUpdate 18 scheduleUpdate?: VideoScheduleUpdate
19 originallyPublishedAt?: Date | string 19 originallyPublishedAt?: Date | string
20 20
21 thumbnailfile?: Blob 21 thumbnailfile?: Blob | string
22 previewfile?: Blob 22 previewfile?: Blob | string
23} 23}