Begin live tests
This commit is contained in:
parent
77e9f859c6
commit
af4ae64f6f
|
@ -58,8 +58,9 @@ elif [ "$1" = "api-2" ]; then
|
||||||
|
|
||||||
serverFiles=$(findTestFiles server/tests/api/server)
|
serverFiles=$(findTestFiles server/tests/api/server)
|
||||||
usersFiles=$(findTestFiles server/tests/api/users)
|
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
|
elif [ "$1" = "api-3" ]; then
|
||||||
npm run build:server
|
npm run build:server
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 40 KiB |
|
@ -223,7 +223,7 @@ function getAccountVideoRateFactory (rateType: VideoRateType) {
|
||||||
|
|
||||||
async function videoController (req: express.Request, res: express.Response) {
|
async function videoController (req: express.Request, res: express.Response) {
|
||||||
// We need more attributes
|
// 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)
|
if (video.url.startsWith(WEBSERVER.URL) === false) return res.redirect(video.url)
|
||||||
|
|
||||||
|
|
|
@ -189,7 +189,7 @@ async function addVideo (req: express.Request, res: express.Response) {
|
||||||
videoData.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED
|
videoData.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED
|
||||||
videoData.duration = videoPhysicalFile['duration'] // duration was added by a previous middleware
|
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
|
video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
|
||||||
|
|
||||||
const videoFile = new VideoFileModel({
|
const videoFile = new VideoFileModel({
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { createReqFiles } from '@server/helpers/express-utils'
|
||||||
import { CONFIG } from '@server/initializers/config'
|
import { CONFIG } from '@server/initializers/config'
|
||||||
import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants'
|
import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants'
|
||||||
import { getVideoActivityPubUrl } from '@server/lib/activitypub/url'
|
import { getVideoActivityPubUrl } from '@server/lib/activitypub/url'
|
||||||
|
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos'
|
||||||
import { buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
|
import { buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
|
||||||
import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator } from '@server/middlewares/validators/videos/video-live'
|
import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator } from '@server/middlewares/validators/videos/video-live'
|
||||||
import { VideoLiveModel } from '@server/models/video/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) {
|
async function updateLiveVideo (req: express.Request, res: express.Response) {
|
||||||
const body: LiveVideoUpdate = req.body
|
const body: LiveVideoUpdate = req.body
|
||||||
|
|
||||||
|
const video = res.locals.videoAll
|
||||||
const videoLive = res.locals.videoLive
|
const videoLive = res.locals.videoLive
|
||||||
videoLive.saveReplay = body.saveReplay || false
|
videoLive.saveReplay = body.saveReplay || false
|
||||||
|
|
||||||
await videoLive.save()
|
video.VideoLive = await videoLive.save()
|
||||||
|
|
||||||
|
await federateVideoIfNeeded(video, false)
|
||||||
|
|
||||||
return res.sendStatus(204)
|
return res.sendStatus(204)
|
||||||
}
|
}
|
||||||
|
@ -113,10 +117,12 @@ async function addLiveVideo (req: express.Request, res: express.Response) {
|
||||||
videoCreated.VideoChannel = res.locals.videoChannel
|
videoCreated.VideoChannel = res.locals.videoChannel
|
||||||
|
|
||||||
videoLive.videoId = videoCreated.id
|
videoLive.videoId = videoCreated.id
|
||||||
await videoLive.save(sequelizeOptions)
|
videoCreated.VideoLive = await videoLive.save(sequelizeOptions)
|
||||||
|
|
||||||
await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
|
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)
|
logger.info('Video live %s with uuid %s created.', videoInfo.name, videoCreated.uuid)
|
||||||
|
|
||||||
return { videoCreated }
|
return { videoCreated }
|
||||||
|
|
|
@ -63,6 +63,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
|
||||||
if (!isBooleanValid(video.downloadEnabled)) video.downloadEnabled = true
|
if (!isBooleanValid(video.downloadEnabled)) video.downloadEnabled = true
|
||||||
if (!isBooleanValid(video.commentsEnabled)) video.commentsEnabled = false
|
if (!isBooleanValid(video.commentsEnabled)) video.commentsEnabled = false
|
||||||
if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false
|
if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false
|
||||||
|
if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false
|
||||||
|
|
||||||
return isActivityPubUrlValid(video.id) &&
|
return isActivityPubUrlValid(video.id) &&
|
||||||
isVideoNameValid(video.name) &&
|
isVideoNameValid(video.name) &&
|
||||||
|
@ -79,7 +80,6 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
|
||||||
isDateValid(video.updated) &&
|
isDateValid(video.updated) &&
|
||||||
(!video.originallyPublishedAt || isDateValid(video.originallyPublishedAt)) &&
|
(!video.originallyPublishedAt || isDateValid(video.originallyPublishedAt)) &&
|
||||||
(!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) &&
|
(!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) &&
|
||||||
video.url.length !== 0 &&
|
|
||||||
video.attributedTo.length !== 0
|
video.attributedTo.length !== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -92,9 +92,9 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
|
||||||
return true
|
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
|
// Retrieve the user who did the request
|
||||||
if (video.isOwned() === false) {
|
if (onlyOwned && video.isOwned() === false) {
|
||||||
res.status(403)
|
res.status(403)
|
||||||
.json({ error: 'Cannot manage a video of another server.' })
|
.json({ error: 'Cannot manage a video of another server.' })
|
||||||
.end()
|
.end()
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { VideoLiveModel } from '@server/models/video/video-live'
|
||||||
import * as Bluebird from 'bluebird'
|
import * as Bluebird from 'bluebird'
|
||||||
import { maxBy, minBy } from 'lodash'
|
import { maxBy, minBy } from 'lodash'
|
||||||
import * as magnetUtil from 'magnet-uri'
|
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
|
// Check this is not a blacklisted video, or unfederated blacklisted video
|
||||||
(video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) &&
|
(video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) &&
|
||||||
// Check the video is public/unlisted and published
|
// 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
|
// Fetch more attributes that we will need to serialize in AP object
|
||||||
if (isArray(video.VideoCaptions) === false) {
|
if (isArray(video.VideoCaptions) === false) {
|
||||||
|
@ -424,6 +425,27 @@ async function updateVideoFromAP (options: {
|
||||||
await Promise.all(videoCaptionsPromises)
|
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
|
return videoUpdated
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -436,7 +458,7 @@ async function updateVideoFromAP (options: {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (wasPrivateVideo || wasUnlistedVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) // Notify our users?
|
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)
|
logger.info('Remote video with uuid %s updated', videoObject.uuid)
|
||||||
|
|
||||||
|
@ -606,6 +628,16 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
|
||||||
|
|
||||||
videoCreated.VideoFiles = videoFiles
|
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({
|
const autoBlacklisted = await autoBlacklistVideoIfNeeded({
|
||||||
video: videoCreated,
|
video: videoCreated,
|
||||||
user: undefined,
|
user: undefined,
|
||||||
|
|
|
@ -16,14 +16,14 @@ const videoLiveGetValidator = [
|
||||||
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
|
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
|
||||||
|
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
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 (areValidationErrors(req, res)) return
|
||||||
if (!await doesVideoExist(req.params.videoId, res, 'all')) 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
|
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)
|
const videoLive = await VideoLiveModel.loadByVideoId(res.locals.videoAll.id)
|
||||||
if (!videoLive) return res.sendStatus(404)
|
if (!videoLive) return res.sendStatus(404)
|
||||||
|
@ -122,6 +122,10 @@ const videoLiveUpdateValidator = [
|
||||||
.json({ error: 'Cannot update a live that has already started' })
|
.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()
|
return next()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -352,11 +352,20 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
||||||
sensitive: video.nsfw,
|
sensitive: video.nsfw,
|
||||||
waitTranscoding: video.waitTranscoding,
|
waitTranscoding: video.waitTranscoding,
|
||||||
isLiveBroadcast: video.isLive,
|
isLiveBroadcast: video.isLive,
|
||||||
|
|
||||||
|
liveSaveReplay: video.isLive
|
||||||
|
? video.VideoLive.saveReplay
|
||||||
|
: null,
|
||||||
|
|
||||||
state: video.state,
|
state: video.state,
|
||||||
commentsEnabled: video.commentsEnabled,
|
commentsEnabled: video.commentsEnabled,
|
||||||
downloadEnabled: video.downloadEnabled,
|
downloadEnabled: video.downloadEnabled,
|
||||||
published: video.publishedAt.toISOString(),
|
published: video.publishedAt.toISOString(),
|
||||||
originallyPublishedAt: video.originallyPublishedAt ? video.originallyPublishedAt.toISOString() : null,
|
|
||||||
|
originallyPublishedAt: video.originallyPublishedAt
|
||||||
|
? video.originallyPublishedAt.toISOString()
|
||||||
|
: null,
|
||||||
|
|
||||||
updated: video.updatedAt.toISOString(),
|
updated: video.updatedAt.toISOString(),
|
||||||
mediaType: 'text/markdown',
|
mediaType: 'text/markdown',
|
||||||
content: video.description,
|
content: video.description,
|
||||||
|
|
|
@ -93,7 +93,11 @@ export class VideoLiveModel extends Model<VideoLiveModel> {
|
||||||
|
|
||||||
toFormattedJSON (): LiveVideo {
|
toFormattedJSON (): LiveVideo {
|
||||||
return {
|
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,
|
streamKey: this.streamKey,
|
||||||
saveReplay: this.saveReplay
|
saveReplay: this.saveReplay
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
import { buildNSFWFilter } from '@server/helpers/express-utils'
|
import { buildNSFWFilter } from '@server/helpers/express-utils'
|
||||||
import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video'
|
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 { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
|
||||||
import { getServerActor } from '@server/models/application/application'
|
import { getServerActor } from '@server/models/application/application'
|
||||||
import { ModelCache } from '@server/models/model-cache'
|
import { ModelCache } from '@server/models/model-cache'
|
||||||
|
@ -121,14 +122,13 @@ import {
|
||||||
videoModelToFormattedJSON
|
videoModelToFormattedJSON
|
||||||
} from './video-format-utils'
|
} from './video-format-utils'
|
||||||
import { VideoImportModel } from './video-import'
|
import { VideoImportModel } from './video-import'
|
||||||
|
import { VideoLiveModel } from './video-live'
|
||||||
import { VideoPlaylistElementModel } from './video-playlist-element'
|
import { VideoPlaylistElementModel } from './video-playlist-element'
|
||||||
import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
|
import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
|
||||||
import { VideoShareModel } from './video-share'
|
import { VideoShareModel } from './video-share'
|
||||||
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
|
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
|
||||||
import { VideoTagModel } from './video-tag'
|
import { VideoTagModel } from './video-tag'
|
||||||
import { VideoViewModel } from './video-view'
|
import { VideoViewModel } from './video-view'
|
||||||
import { LiveManager } from '@server/lib/live-manager'
|
|
||||||
import { VideoLiveModel } from './video-live'
|
|
||||||
|
|
||||||
export enum ScopeNames {
|
export enum ScopeNames {
|
||||||
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
|
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
|
||||||
|
@ -142,7 +142,8 @@ export enum ScopeNames {
|
||||||
WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
|
WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
|
||||||
WITH_USER_ID = 'WITH_USER_ID',
|
WITH_USER_ID = 'WITH_USER_ID',
|
||||||
WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES',
|
WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES',
|
||||||
WITH_THUMBNAILS = 'WITH_THUMBNAILS'
|
WITH_THUMBNAILS = 'WITH_THUMBNAILS',
|
||||||
|
WITH_LIVE = 'WITH_LIVE'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ForAPIOptions = {
|
export type ForAPIOptions = {
|
||||||
|
@ -245,6 +246,14 @@ export type AvailableForListIDsOptions = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
[ScopeNames.WITH_LIVE]: {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: VideoLiveModel,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
[ScopeNames.WITH_USER_ID]: {
|
[ScopeNames.WITH_USER_ID]: {
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
|
@ -943,6 +952,17 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
model: VideoStreamingPlaylistModel.unscoped(),
|
||||||
|
required: false,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: VideoFileModel,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
VideoLiveModel,
|
||||||
VideoFileModel,
|
VideoFileModel,
|
||||||
TagModel
|
TagModel
|
||||||
]
|
]
|
||||||
|
@ -1330,7 +1350,8 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
ScopeNames.WITH_SCHEDULED_UPDATE,
|
ScopeNames.WITH_SCHEDULED_UPDATE,
|
||||||
ScopeNames.WITH_WEBTORRENT_FILES,
|
ScopeNames.WITH_WEBTORRENT_FILES,
|
||||||
ScopeNames.WITH_STREAMING_PLAYLISTS,
|
ScopeNames.WITH_STREAMING_PLAYLISTS,
|
||||||
ScopeNames.WITH_THUMBNAILS
|
ScopeNames.WITH_THUMBNAILS,
|
||||||
|
ScopeNames.WITH_LIVE
|
||||||
]
|
]
|
||||||
|
|
||||||
if (userId) {
|
if (userId) {
|
||||||
|
@ -1362,6 +1383,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
ScopeNames.WITH_ACCOUNT_DETAILS,
|
ScopeNames.WITH_ACCOUNT_DETAILS,
|
||||||
ScopeNames.WITH_SCHEDULED_UPDATE,
|
ScopeNames.WITH_SCHEDULED_UPDATE,
|
||||||
ScopeNames.WITH_THUMBNAILS,
|
ScopeNames.WITH_THUMBNAILS,
|
||||||
|
ScopeNames.WITH_LIVE,
|
||||||
{ method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
|
{ method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
|
||||||
{ method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
|
{ method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './live'
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -21,6 +21,7 @@ import { MThumbnail } from './thumbnail'
|
||||||
import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist'
|
import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist'
|
||||||
import { MScheduleVideoUpdate } from './schedule-video-update'
|
import { MScheduleVideoUpdate } from './schedule-video-update'
|
||||||
import { MUserVideoHistoryTime } from '../user/user-video-history'
|
import { MUserVideoHistoryTime } from '../user/user-video-history'
|
||||||
|
import { MVideoLive } from './video-live'
|
||||||
|
|
||||||
type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M>
|
type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M>
|
||||||
|
|
||||||
|
@ -29,7 +30,7 @@ type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M>
|
||||||
export type MVideo =
|
export type MVideo =
|
||||||
Omit<VideoModel, 'VideoChannel' | 'Tags' | 'Thumbnails' | 'VideoPlaylistElements' | 'VideoAbuses' |
|
Omit<VideoModel, 'VideoChannel' | 'Tags' | 'Thumbnails' | 'VideoPlaylistElements' | 'VideoAbuses' |
|
||||||
'VideoFiles' | 'VideoStreamingPlaylists' | 'VideoShares' | 'AccountVideoRates' | 'VideoComments' | 'VideoViews' | 'UserVideoHistories' |
|
'VideoFiles' | 'VideoStreamingPlaylists' | 'VideoShares' | 'AccountVideoRates' | 'VideoComments' | 'VideoViews' | 'UserVideoHistories' |
|
||||||
'ScheduleVideoUpdate' | 'VideoBlacklist' | 'VideoImport' | 'VideoCaptions'>
|
'ScheduleVideoUpdate' | 'VideoBlacklist' | 'VideoImport' | 'VideoCaptions' | 'VideoLive'>
|
||||||
|
|
||||||
// ############################################################################
|
// ############################################################################
|
||||||
|
|
||||||
|
@ -151,7 +152,8 @@ export type MVideoFullLight =
|
||||||
Use<'UserVideoHistories', MUserVideoHistoryTime[]> &
|
Use<'UserVideoHistories', MUserVideoHistoryTime[]> &
|
||||||
Use<'VideoFiles', MVideoFile[]> &
|
Use<'VideoFiles', MVideoFile[]> &
|
||||||
Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> &
|
Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> &
|
||||||
Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]>
|
Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> &
|
||||||
|
Use<'VideoLive', MVideoLive>
|
||||||
|
|
||||||
// ############################################################################
|
// ############################################################################
|
||||||
|
|
||||||
|
@ -165,7 +167,8 @@ export type MVideoAP =
|
||||||
Use<'VideoCaptions', MVideoCaptionLanguageUrl[]> &
|
Use<'VideoCaptions', MVideoCaptionLanguageUrl[]> &
|
||||||
Use<'VideoBlacklist', MVideoBlacklistUnfederated> &
|
Use<'VideoBlacklist', MVideoBlacklistUnfederated> &
|
||||||
Use<'VideoFiles', MVideoFileRedundanciesOpt[]> &
|
Use<'VideoFiles', MVideoFileRedundanciesOpt[]> &
|
||||||
Use<'Thumbnails', MThumbnail[]>
|
Use<'Thumbnails', MThumbnail[]> &
|
||||||
|
Use<'VideoLive', MVideoLive>
|
||||||
|
|
||||||
export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'>
|
export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'>
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,12 @@ import { randomInt } from '../../core-utils/miscs/miscs'
|
||||||
|
|
||||||
interface ServerInfo {
|
interface ServerInfo {
|
||||||
app: ChildProcess
|
app: ChildProcess
|
||||||
|
|
||||||
url: string
|
url: string
|
||||||
host: string
|
host: string
|
||||||
|
hostname: string
|
||||||
port: number
|
port: number
|
||||||
|
|
||||||
parallel: boolean
|
parallel: boolean
|
||||||
internalServerNumber: number
|
internalServerNumber: number
|
||||||
serverNumber: number
|
serverNumber: number
|
||||||
|
@ -109,6 +111,7 @@ async function flushAndRunServer (serverNumber: number, configOverride?: Object,
|
||||||
serverNumber,
|
serverNumber,
|
||||||
url: `http://localhost:${port}`,
|
url: `http://localhost:${port}`,
|
||||||
host: `localhost:${port}`,
|
host: `localhost:${port}`,
|
||||||
|
hostname: 'localhost',
|
||||||
client: {
|
client: {
|
||||||
id: null,
|
id: null,
|
||||||
secret: null
|
secret: null
|
||||||
|
|
|
@ -2,8 +2,8 @@ import * as ffmpeg from 'fluent-ffmpeg'
|
||||||
import { LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models'
|
import { LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models'
|
||||||
import { buildAbsoluteFixturePath, wait } from '../miscs/miscs'
|
import { buildAbsoluteFixturePath, wait } from '../miscs/miscs'
|
||||||
import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
|
import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
|
||||||
import { ServerInfo } from '../server/servers'
|
import { getVideoWithToken } from './videos'
|
||||||
import { getVideo, getVideoWithToken } from './videos'
|
import { omit } from 'lodash'
|
||||||
|
|
||||||
function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = 200) {
|
function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = 200) {
|
||||||
const path = '/api/v1/videos/live'
|
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) {
|
function createLive (url: string, token: string, fields: LiveVideoCreate, statusCodeExpected = 200) {
|
||||||
const path = '/api/v1/videos/live'
|
const path = '/api/v1/videos/live'
|
||||||
|
|
||||||
let attaches: any = {}
|
const attaches: any = {}
|
||||||
if (fields.thumbnailfile) attaches = { thumbnailfile: fields.thumbnailfile }
|
if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
|
||||||
if (fields.previewfile) attaches = { previewfile: fields.previewfile }
|
if (fields.previewfile) attaches.previewfile = fields.previewfile
|
||||||
|
|
||||||
|
const updatedFields = omit(fields, 'thumbnailfile', 'previewfile')
|
||||||
|
|
||||||
return makeUploadRequest({
|
return makeUploadRequest({
|
||||||
url,
|
url,
|
||||||
path,
|
path,
|
||||||
token,
|
token,
|
||||||
attaches,
|
attaches,
|
||||||
fields,
|
fields: updatedFields,
|
||||||
statusCodeExpected
|
statusCodeExpected
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,9 @@ export interface VideoObject {
|
||||||
views: number
|
views: number
|
||||||
|
|
||||||
sensitive: boolean
|
sensitive: boolean
|
||||||
|
|
||||||
isLiveBroadcast: boolean
|
isLiveBroadcast: boolean
|
||||||
|
liveSaveReplay: boolean
|
||||||
|
|
||||||
commentsEnabled: boolean
|
commentsEnabled: boolean
|
||||||
downloadEnabled: boolean
|
downloadEnabled: boolean
|
||||||
|
|
|
@ -30,6 +30,7 @@ export const enum UserRight {
|
||||||
UPDATE_ANY_VIDEO,
|
UPDATE_ANY_VIDEO,
|
||||||
UPDATE_ANY_VIDEO_PLAYLIST,
|
UPDATE_ANY_VIDEO_PLAYLIST,
|
||||||
|
|
||||||
|
GET_ANY_LIVE,
|
||||||
SEE_ALL_VIDEOS,
|
SEE_ALL_VIDEOS,
|
||||||
CHANGE_VIDEO_OWNERSHIP,
|
CHANGE_VIDEO_OWNERSHIP,
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,6 @@ export interface VideoCreate {
|
||||||
scheduleUpdate?: VideoScheduleUpdate
|
scheduleUpdate?: VideoScheduleUpdate
|
||||||
originallyPublishedAt?: Date | string
|
originallyPublishedAt?: Date | string
|
||||||
|
|
||||||
thumbnailfile?: Blob
|
thumbnailfile?: Blob | string
|
||||||
previewfile?: Blob
|
previewfile?: Blob | string
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue