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 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 diff --git a/server/assets/default-audio-background.jpg b/server/assets/default-audio-background.jpg index a19173eac..0aef989a7 100644 Binary files a/server/assets/default-audio-background.jpg and b/server/assets/default-audio-background.jpg differ diff --git a/server/assets/default-live-background.jpg b/server/assets/default-live-background.jpg index 2743af7fc..1fd20e407 100644 Binary files a/server/assets/default-live-background.jpg and b/server/assets/default-live-background.jpg 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) { 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) 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) { 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({ 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' 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 } 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) { 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 } 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 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() 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 @@ +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, 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 = [ 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() } ] 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 { 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, 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 { 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 } 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 { } 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 { } ] }, + { + model: VideoStreamingPlaylistModel.unscoped(), + required: false, + include: [ + { + model: VideoFileModel, + required: false + } + ] + }, + VideoLiveModel, VideoFileModel, TagModel ] @@ -1330,7 +1350,8 @@ export class VideoModel extends Model { 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 { 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 ] } ] 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 @@ +/* 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) + }) +}) 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' 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 = PickWith @@ -29,7 +30,7 @@ type Use = PickWith export type MVideo = Omit + '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 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' 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 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' 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 }) } 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 { views: number sensitive: boolean + isLiveBroadcast: boolean + liveSaveReplay: boolean commentsEnabled: boolean 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 { UPDATE_ANY_VIDEO, UPDATE_ANY_VIDEO_PLAYLIST, + GET_ANY_LIVE, SEE_ALL_VIDEOS, CHANGE_VIDEO_OWNERSHIP, 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 { scheduleUpdate?: VideoScheduleUpdate originallyPublishedAt?: Date | string - thumbnailfile?: Blob - previewfile?: Blob + thumbnailfile?: Blob | string + previewfile?: Blob | string }