diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/assets/default-audio-background.jpg | bin | 14048 -> 7987 bytes | |||
-rw-r--r-- | server/assets/default-live-background.jpg | bin | 93634 -> 40557 bytes | |||
-rw-r--r-- | server/controllers/activitypub/client.ts | 2 | ||||
-rw-r--r-- | server/controllers/api/videos/index.ts | 2 | ||||
-rw-r--r-- | server/controllers/api/videos/live.ts | 10 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/videos.ts | 2 | ||||
-rw-r--r-- | server/helpers/middlewares/videos.ts | 4 | ||||
-rw-r--r-- | server/lib/activitypub/videos.ts | 36 | ||||
-rw-r--r-- | server/middlewares/validators/videos/video-live.ts | 10 | ||||
-rw-r--r-- | server/models/video/video-format-utils.ts | 11 | ||||
-rw-r--r-- | server/models/video/video-live.ts | 6 | ||||
-rw-r--r-- | server/models/video/video.ts | 30 | ||||
-rw-r--r-- | server/tests/api/live/index.ts | 1 | ||||
-rw-r--r-- | server/tests/api/live/live.ts | 351 | ||||
-rw-r--r-- | server/types/models/video/video.ts | 9 |
15 files changed, 453 insertions, 21 deletions
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 | ||
224 | async function videoController (req: express.Request, res: express.Response) { | 224 | async 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' | |||
4 | import { CONFIG } from '@server/initializers/config' | 4 | import { CONFIG } from '@server/initializers/config' |
5 | import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants' | 5 | import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants' |
6 | import { getVideoActivityPubUrl } from '@server/lib/activitypub/url' | 6 | import { getVideoActivityPubUrl } from '@server/lib/activitypub/url' |
7 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | ||
7 | import { buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | 8 | import { buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' |
8 | import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator } from '@server/middlewares/validators/videos/video-live' | 9 | import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator } from '@server/middlewares/validators/videos/video-live' |
9 | import { VideoLiveModel } from '@server/models/video/video-live' | 10 | import { VideoLiveModel } from '@server/models/video/video-live' |
@@ -63,10 +64,13 @@ async function getLiveVideo (req: express.Request, res: express.Response) { | |||
63 | async function updateLiveVideo (req: express.Request, res: express.Response) { | 64 | async 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 | ||
95 | function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response) { | 95 | function 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 @@ | |||
1 | import { VideoLiveModel } from '@server/models/video/video-live' | ||
1 | import * as Bluebird from 'bluebird' | 2 | import * as Bluebird from 'bluebird' |
2 | import { maxBy, minBy } from 'lodash' | 3 | import { maxBy, minBy } from 'lodash' |
3 | import * as magnetUtil from 'magnet-uri' | 4 | import * 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' |
27 | import { buildNSFWFilter } from '@server/helpers/express-utils' | 27 | import { buildNSFWFilter } from '@server/helpers/express-utils' |
28 | import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video' | 28 | import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video' |
29 | import { LiveManager } from '@server/lib/live-manager' | ||
29 | import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | 30 | import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' |
30 | import { getServerActor } from '@server/models/application/application' | 31 | import { getServerActor } from '@server/models/application/application' |
31 | import { ModelCache } from '@server/models/model-cache' | 32 | import { 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' |
123 | import { VideoImportModel } from './video-import' | 124 | import { VideoImportModel } from './video-import' |
125 | import { VideoLiveModel } from './video-live' | ||
124 | import { VideoPlaylistElementModel } from './video-playlist-element' | 126 | import { VideoPlaylistElementModel } from './video-playlist-element' |
125 | import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder' | 127 | import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder' |
126 | import { VideoShareModel } from './video-share' | 128 | import { VideoShareModel } from './video-share' |
127 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 129 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
128 | import { VideoTagModel } from './video-tag' | 130 | import { VideoTagModel } from './video-tag' |
129 | import { VideoViewModel } from './video-view' | 131 | import { VideoViewModel } from './video-view' |
130 | import { LiveManager } from '@server/lib/live-manager' | ||
131 | import { VideoLiveModel } from './video-live' | ||
132 | 132 | ||
133 | export enum ScopeNames { | 133 | export 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 | ||
148 | export type ForAPIOptions = { | 149 | export 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 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { LiveVideo, LiveVideoCreate, VideoDetails, VideoPrivacy } from '@shared/models' | ||
6 | import { | ||
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 | |||
26 | const expect = chai.expect | ||
27 | |||
28 | describe('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' | |||
21 | import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist' | 21 | import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist' |
22 | import { MScheduleVideoUpdate } from './schedule-video-update' | 22 | import { MScheduleVideoUpdate } from './schedule-video-update' |
23 | import { MUserVideoHistoryTime } from '../user/user-video-history' | 23 | import { MUserVideoHistoryTime } from '../user/user-video-history' |
24 | import { MVideoLive } from './video-live' | ||
24 | 25 | ||
25 | type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M> | 26 | type 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> | |||
29 | export type MVideo = | 30 | export 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 | ||
170 | export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'> | 173 | export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'> |
171 | 174 | ||