Feature/Add replay privacy (#5692)
* Add replay settings feature * Fix replay settings behaviour * Fix tests * Fix tests * Fix tests * Update openapi doc and fix tests * Add tests and fix code * Models correction * Add migration and update controller and middleware * Add check params tests * Fix video live middleware * Updated code based on review comments
This commit is contained in:
parent
ebd61437c1
commit
05a60d8599
|
@ -272,7 +272,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" *ngIf="isSaveReplayEnabled()">
|
||||
<div class="form-group" *ngIf="isSaveReplayAllowed()">
|
||||
<my-peertube-checkbox inputName="liveVideoSaveReplay" formControlName="saveReplay">
|
||||
<ng-template ptTemplate="label">
|
||||
<ng-container i18n>Automatically publish a replay when your live ends</ng-container>
|
||||
|
@ -284,6 +284,13 @@
|
|||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group mx-4" *ngIf="isSaveReplayEnabled()">
|
||||
<label i18n for="replayPrivacy">Privacy of the new replay</label>
|
||||
<my-select-options
|
||||
labelForId="replayPrivacy" [items]="videoPrivacies" [clearable]="false" formControlName="replayPrivacy"
|
||||
></my-select-options>
|
||||
</div>
|
||||
|
||||
<div class="form-group" *ngIf="isLatencyModeEnabled()">
|
||||
<label i18n for="latencyMode">Latency mode</label>
|
||||
<my-select-options
|
||||
|
|
|
@ -165,7 +165,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
|||
liveStreamKey: null,
|
||||
permanentLive: null,
|
||||
latencyMode: null,
|
||||
saveReplay: null
|
||||
saveReplay: null,
|
||||
replayPrivacy: null
|
||||
}
|
||||
|
||||
this.formValidatorService.updateFormGroup(
|
||||
|
@ -303,10 +304,14 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
|||
modalRef.componentInstance.captionEdited.subscribe(this.onCaptionEdited.bind(this))
|
||||
}
|
||||
|
||||
isSaveReplayEnabled () {
|
||||
isSaveReplayAllowed () {
|
||||
return this.serverConfig.live.allowReplay
|
||||
}
|
||||
|
||||
isSaveReplayEnabled () {
|
||||
return this.form.value['saveReplay'] === true
|
||||
}
|
||||
|
||||
isPermanentLiveEnabled () {
|
||||
return this.form.value['permanentLive'] === true
|
||||
}
|
||||
|
|
|
@ -8,7 +8,15 @@ import { Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared
|
|||
import { LiveVideoService } from '@app/shared/shared-video-live'
|
||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import { LiveVideo, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, PeerTubeProblemDocument, ServerErrorCode } from '@shared/models'
|
||||
import {
|
||||
LiveVideo,
|
||||
LiveVideoCreate,
|
||||
LiveVideoLatencyMode,
|
||||
LiveVideoUpdate,
|
||||
PeerTubeProblemDocument,
|
||||
ServerErrorCode,
|
||||
VideoPrivacy
|
||||
} from '@shared/models'
|
||||
import { VideoSend } from './video-send'
|
||||
|
||||
@Component({
|
||||
|
@ -79,11 +87,12 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
|
|||
permanentLive: this.firstStepPermanentLive,
|
||||
latencyMode: LiveVideoLatencyMode.DEFAULT,
|
||||
saveReplay: this.isReplayAllowed(),
|
||||
replaySettings: { privacy: VideoPrivacy.PRIVATE },
|
||||
channelId: this.firstStepChannelId
|
||||
}
|
||||
|
||||
// Go live in private mode, but correctly fill the update form with the first user choice
|
||||
const toPatch = { ...video, privacy: this.firstStepPrivacyId }
|
||||
const toPatch = { ...video, privacy: this.firstStepPrivacyId, replayPrivacy: video.replaySettings.privacy }
|
||||
this.form.patchValue(toPatch)
|
||||
|
||||
this.liveVideoService.goLive(video)
|
||||
|
@ -130,6 +139,7 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
|
|||
|
||||
const liveVideoUpdate: LiveVideoUpdate = {
|
||||
saveReplay: this.form.value.saveReplay,
|
||||
replaySettings: { privacy: this.form.value.replayPrivacy },
|
||||
latencyMode: this.form.value.latencyMode,
|
||||
permanentLive: this.form.value.permanentLive
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||
if (this.liveVideo) {
|
||||
this.form.patchValue({
|
||||
saveReplay: this.liveVideo.saveReplay,
|
||||
replayPrivacy: this.liveVideo.replaySettings ? this.liveVideo.replaySettings.privacy : VideoPrivacy.PRIVATE,
|
||||
latencyMode: this.liveVideo.latencyMode,
|
||||
permanentLive: this.liveVideo.permanentLive
|
||||
})
|
||||
|
@ -127,6 +128,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||
|
||||
const liveVideoUpdate: LiveVideoUpdate = {
|
||||
saveReplay: !!this.form.value.saveReplay,
|
||||
replaySettings: { privacy: this.form.value.replayPrivacy },
|
||||
permanentLive: !!this.form.value.permanentLive,
|
||||
latencyMode: this.form.value.latencyMode
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from '@server/middlewares/validators/videos/video-live'
|
||||
import { VideoLiveModel } from '@server/models/video/video-live'
|
||||
import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
|
||||
import { MVideoDetails, MVideoFullLight } from '@server/types/models'
|
||||
import { MVideoDetails, MVideoFullLight, MVideoLive } from '@server/types/models'
|
||||
import { buildUUID, uuidToShort } from '@shared/extra-utils'
|
||||
import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoState } from '@shared/models'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
|
@ -24,6 +24,7 @@ import { sequelizeTypescript } from '../../../initializers/database'
|
|||
import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail'
|
||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares'
|
||||
import { VideoModel } from '../../../models/video/video'
|
||||
import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
|
||||
|
||||
const liveRouter = express.Router()
|
||||
|
||||
|
@ -105,7 +106,10 @@ async function updateLiveVideo (req: express.Request, res: express.Response) {
|
|||
const video = res.locals.videoAll
|
||||
const videoLive = res.locals.videoLive
|
||||
|
||||
if (exists(body.saveReplay)) videoLive.saveReplay = body.saveReplay
|
||||
const newReplaySettingModel = await updateReplaySettings(videoLive, body)
|
||||
if (newReplaySettingModel) videoLive.replaySettingId = newReplaySettingModel.id
|
||||
else videoLive.replaySettingId = null
|
||||
|
||||
if (exists(body.permanentLive)) videoLive.permanentLive = body.permanentLive
|
||||
if (exists(body.latencyMode)) videoLive.latencyMode = body.latencyMode
|
||||
|
||||
|
@ -116,6 +120,27 @@ async function updateLiveVideo (req: express.Request, res: express.Response) {
|
|||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
}
|
||||
|
||||
async function updateReplaySettings (videoLive: MVideoLive, body: LiveVideoUpdate) {
|
||||
if (exists(body.saveReplay)) videoLive.saveReplay = body.saveReplay
|
||||
|
||||
// The live replay is not saved anymore, destroy the old model if it existed
|
||||
if (!videoLive.saveReplay) {
|
||||
if (videoLive.replaySettingId) {
|
||||
await VideoLiveReplaySettingModel.removeSettings(videoLive.replaySettingId)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const settingModel = videoLive.replaySettingId
|
||||
? await VideoLiveReplaySettingModel.load(videoLive.replaySettingId)
|
||||
: new VideoLiveReplaySettingModel()
|
||||
|
||||
if (exists(body.replaySettings.privacy)) settingModel.privacy = body.replaySettings.privacy
|
||||
|
||||
return settingModel.save()
|
||||
}
|
||||
|
||||
async function addLiveVideo (req: express.Request, res: express.Response) {
|
||||
const videoInfo: LiveVideoCreate = req.body
|
||||
|
||||
|
@ -161,6 +186,15 @@ async function addLiveVideo (req: express.Request, res: express.Response) {
|
|||
// Do not forget to add video channel information to the created video
|
||||
videoCreated.VideoChannel = res.locals.videoChannel
|
||||
|
||||
if (videoLive.saveReplay) {
|
||||
const replaySettings = new VideoLiveReplaySettingModel({
|
||||
privacy: videoInfo.replaySettings.privacy
|
||||
})
|
||||
await replaySettings.save(sequelizeOptions)
|
||||
|
||||
videoLive.replaySettingId = replaySettings.id
|
||||
}
|
||||
|
||||
videoLive.videoId = videoCreated.id
|
||||
videoCreated.VideoLive = await videoLive.save(sequelizeOptions)
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LAST_MIGRATION_VERSION = 755
|
||||
const LAST_MIGRATION_VERSION = 760
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla
|
|||
import { VideoTagModel } from '../models/video/video-tag'
|
||||
import { VideoViewModel } from '../models/view/video-view'
|
||||
import { CONFIG } from './config'
|
||||
import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
|
||||
|
||||
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
||||
|
||||
|
@ -141,6 +142,7 @@ async function initDatabaseModels (silent: boolean) {
|
|||
UserVideoHistoryModel,
|
||||
VideoLiveModel,
|
||||
VideoLiveSessionModel,
|
||||
VideoLiveReplaySettingModel,
|
||||
AccountBlocklistModel,
|
||||
ServerBlocklistModel,
|
||||
UserNotificationModel,
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
import * as Sequelize from 'sequelize'
|
||||
|
||||
async function up (utils: {
|
||||
transaction: Sequelize.Transaction
|
||||
queryInterface: Sequelize.QueryInterface
|
||||
sequelize: Sequelize.Sequelize
|
||||
}): Promise<void> {
|
||||
{
|
||||
const query = `
|
||||
CREATE TABLE IF NOT EXISTS "videoLiveReplaySetting" (
|
||||
"id" SERIAL ,
|
||||
"privacy" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
`
|
||||
|
||||
await utils.sequelize.query(query, { transaction : utils.transaction })
|
||||
}
|
||||
|
||||
{
|
||||
await utils.queryInterface.addColumn('videoLive', 'replaySettingId', {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: null,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'videoLiveReplaySetting',
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'SET NULL'
|
||||
}, { transaction: utils.transaction })
|
||||
}
|
||||
|
||||
{
|
||||
await utils.queryInterface.addColumn('videoLiveSession', 'replaySettingId', {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: null,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'videoLiveReplaySetting',
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'SET NULL'
|
||||
}, { transaction: utils.transaction })
|
||||
}
|
||||
|
||||
{
|
||||
const query = `
|
||||
SELECT live."id", v."privacy"
|
||||
FROM "videoLive" live
|
||||
INNER JOIN "video" v ON live."videoId" = v."id"
|
||||
WHERE live."saveReplay" = true
|
||||
`
|
||||
|
||||
const videoLives = await utils.sequelize.query<{ id: number, privacy: number }>(
|
||||
query,
|
||||
{ type: Sequelize.QueryTypes.SELECT, transaction: utils.transaction }
|
||||
)
|
||||
|
||||
for (const videoLive of videoLives) {
|
||||
const query = `
|
||||
WITH new_replay_setting AS (
|
||||
INSERT INTO "videoLiveReplaySetting" ("privacy", "createdAt", "updatedAt")
|
||||
VALUES (:privacy, NOW(), NOW())
|
||||
RETURNING id
|
||||
)
|
||||
UPDATE "videoLive" SET "replaySettingId" = (SELECT id FROM new_replay_setting)
|
||||
WHERE "id" = :id
|
||||
`
|
||||
|
||||
const options = {
|
||||
replacements: { privacy: videoLive.privacy, id: videoLive.id },
|
||||
type: Sequelize.QueryTypes.UPDATE,
|
||||
transaction: utils.transaction
|
||||
}
|
||||
|
||||
await utils.sequelize.query(query, options)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const query = `
|
||||
SELECT session."id", v."privacy"
|
||||
FROM "videoLiveSession" session
|
||||
INNER JOIN "video" v ON session."liveVideoId" = v."id"
|
||||
WHERE session."saveReplay" = true
|
||||
AND session."liveVideoId" IS NOT NULL;
|
||||
`
|
||||
|
||||
const videoLiveSessions = await utils.sequelize.query<{ id: number, privacy: number }>(
|
||||
query,
|
||||
{ type: Sequelize.QueryTypes.SELECT, transaction: utils.transaction }
|
||||
)
|
||||
|
||||
for (const videoLive of videoLiveSessions) {
|
||||
const query = `
|
||||
WITH new_replay_setting AS (
|
||||
INSERT INTO "videoLiveReplaySetting" ("privacy", "createdAt", "updatedAt")
|
||||
VALUES (:privacy, NOW(), NOW())
|
||||
RETURNING id
|
||||
)
|
||||
UPDATE "videoLiveSession" SET "replaySettingId" = (SELECT id FROM new_replay_setting)
|
||||
WHERE "id" = :id
|
||||
`
|
||||
|
||||
const options = {
|
||||
replacements: { privacy: videoLive.privacy, id: videoLive.id },
|
||||
type: Sequelize.QueryTypes.UPDATE,
|
||||
transaction: utils.transaction
|
||||
}
|
||||
|
||||
await utils.sequelize.query(query, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function down (options) {
|
||||
throw new Error('Not implemented.')
|
||||
}
|
||||
|
||||
export {
|
||||
up,
|
||||
down
|
||||
}
|
|
@ -19,6 +19,7 @@ import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@serv
|
|||
import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models'
|
||||
import { logger, loggerTagsFactory } from '../../../helpers/logger'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager'
|
||||
import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
|
||||
|
||||
const lTags = loggerTagsFactory('live', 'job')
|
||||
|
||||
|
@ -60,7 +61,13 @@ async function processVideoLiveEnding (job: Job) {
|
|||
return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId })
|
||||
}
|
||||
|
||||
return replaceLiveByReplay({ video, liveSession, live, permanentLive, replayDirectory: payload.replayDirectory })
|
||||
return replaceLiveByReplay({
|
||||
video,
|
||||
liveSession,
|
||||
live,
|
||||
permanentLive,
|
||||
replayDirectory: payload.replayDirectory
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -79,6 +86,8 @@ async function saveReplayToExternalVideo (options: {
|
|||
}) {
|
||||
const { liveVideo, liveSession, publishedAt, replayDirectory } = options
|
||||
|
||||
const replaySettings = await VideoLiveReplaySettingModel.load(liveSession.replaySettingId)
|
||||
|
||||
const replayVideo = new VideoModel({
|
||||
name: `${liveVideo.name} - ${new Date(publishedAt).toLocaleString()}`,
|
||||
isLive: false,
|
||||
|
@ -95,7 +104,7 @@ async function saveReplayToExternalVideo (options: {
|
|||
nsfw: liveVideo.nsfw,
|
||||
description: liveVideo.description,
|
||||
support: liveVideo.support,
|
||||
privacy: liveVideo.privacy,
|
||||
privacy: replaySettings.privacy,
|
||||
channelId: liveVideo.channelId
|
||||
}) as MVideoWithAllFiles
|
||||
|
||||
|
@ -142,6 +151,7 @@ async function replaceLiveByReplay (options: {
|
|||
}) {
|
||||
const { video, liveSession, live, permanentLive, replayDirectory } = options
|
||||
|
||||
const replaySettings = await VideoLiveReplaySettingModel.load(liveSession.replaySettingId)
|
||||
const videoWithFiles = await VideoModel.loadFull(video.id)
|
||||
const hlsPlaylist = videoWithFiles.getHLSPlaylist()
|
||||
|
||||
|
@ -150,6 +160,7 @@ async function replaceLiveByReplay (options: {
|
|||
await live.destroy()
|
||||
|
||||
videoWithFiles.isLive = false
|
||||
videoWithFiles.privacy = replaySettings.privacy
|
||||
videoWithFiles.waitTranscoding = true
|
||||
videoWithFiles.state = VideoState.TO_TRANSCODE
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import { VideoModel } from '@server/models/video/video'
|
|||
import { VideoLiveModel } from '@server/models/video/video-live'
|
||||
import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
|
||||
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
|
||||
import { MVideo, MVideoLiveSession, MVideoLiveVideo } from '@server/types/models'
|
||||
import { MVideo, MVideoLiveSession, MVideoLiveVideo, MVideoLiveVideoWithSetting } from '@server/types/models'
|
||||
import { pick, wait } from '@shared/core-utils'
|
||||
import { LiveVideoError, VideoState } from '@shared/models'
|
||||
import { federateVideoIfNeeded } from '../activitypub/videos'
|
||||
|
@ -30,6 +30,8 @@ import { Hooks } from '../plugins/hooks'
|
|||
import { LiveQuotaStore } from './live-quota-store'
|
||||
import { cleanupAndDestroyPermanentLive } from './live-utils'
|
||||
import { MuxingSession } from './shared'
|
||||
import { sequelizeTypescript } from '@server/initializers/database'
|
||||
import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
|
||||
|
||||
const NodeRtmpSession = require('node-media-server/src/node_rtmp_session')
|
||||
const context = require('node-media-server/src/node_core_ctx')
|
||||
|
@ -270,7 +272,7 @@ class LiveManager {
|
|||
|
||||
private async runMuxingSession (options: {
|
||||
sessionId: string
|
||||
videoLive: MVideoLiveVideo
|
||||
videoLive: MVideoLiveVideoWithSetting
|
||||
|
||||
inputUrl: string
|
||||
fps: number
|
||||
|
@ -470,15 +472,26 @@ class LiveManager {
|
|||
return resolutionsEnabled
|
||||
}
|
||||
|
||||
private saveStartingSession (videoLive: MVideoLiveVideo) {
|
||||
const liveSession = new VideoLiveSessionModel({
|
||||
startDate: new Date(),
|
||||
liveVideoId: videoLive.videoId,
|
||||
saveReplay: videoLive.saveReplay,
|
||||
endingProcessed: false
|
||||
})
|
||||
private async saveStartingSession (videoLive: MVideoLiveVideoWithSetting) {
|
||||
const replaySettings = videoLive.saveReplay
|
||||
? new VideoLiveReplaySettingModel({
|
||||
privacy: videoLive.ReplaySetting.privacy
|
||||
})
|
||||
: null
|
||||
|
||||
return liveSession.save()
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
if (videoLive.saveReplay) {
|
||||
await replaySettings.save({ transaction: t })
|
||||
}
|
||||
|
||||
return VideoLiveSessionModel.create({
|
||||
startDate: new Date(),
|
||||
liveVideoId: videoLive.videoId,
|
||||
saveReplay: videoLive.saveReplay,
|
||||
replaySettingId: videoLive.saveReplay ? replaySettings.id : null,
|
||||
endingProcessed: false
|
||||
}, { transaction: t })
|
||||
})
|
||||
}
|
||||
|
||||
private async saveEndingSession (videoId: number, error: LiveVideoError | null) {
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
VideoState
|
||||
} from '@shared/models'
|
||||
import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
|
||||
import { isVideoNameValid } from '../../../helpers/custom-validators/videos'
|
||||
import { isVideoNameValid, isVideoPrivacyValid } from '../../../helpers/custom-validators/videos'
|
||||
import { cleanUpReqFiles } from '../../../helpers/express-utils'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { CONFIG } from '../../../initializers/config'
|
||||
|
@ -66,6 +66,11 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
|
|||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'),
|
||||
|
||||
body('replaySettings.privacy')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isVideoPrivacyValid),
|
||||
|
||||
body('permanentLive')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
|
@ -153,6 +158,11 @@ const videoLiveUpdateValidator = [
|
|||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'),
|
||||
|
||||
body('replaySettings.privacy')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isVideoPrivacyValid),
|
||||
|
||||
body('latencyMode')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
|
@ -177,6 +187,8 @@ const videoLiveUpdateValidator = [
|
|||
})
|
||||
}
|
||||
|
||||
if (!checkLiveSettingsReplayConsistency({ res, body })) return
|
||||
|
||||
if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) {
|
||||
return res.fail({ message: 'Cannot update a live that has already started' })
|
||||
}
|
||||
|
@ -272,3 +284,43 @@ function hasValidLatencyMode (body: LiveVideoUpdate | LiveVideoCreate) {
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkLiveSettingsReplayConsistency (options: {
|
||||
res: express.Response
|
||||
body: LiveVideoUpdate
|
||||
}) {
|
||||
const { res, body } = options
|
||||
|
||||
// We now save replays of this live, so replay settings are mandatory
|
||||
if (res.locals.videoLive.saveReplay !== true && body.saveReplay === true) {
|
||||
|
||||
if (!exists(body.replaySettings)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Replay settings are missing now the live replay is saved'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!exists(body.replaySettings.privacy)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Privacy replay setting is missing now the live replay is saved'
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Save replay was and is not enabled, so send an error the user if it specified replay settings
|
||||
if ((!exists(body.saveReplay) && res.locals.videoLive.saveReplay === false) || body.saveReplay === false) {
|
||||
if (exists(body.replaySettings)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot save replay settings since live replay is not enabled'
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -160,6 +160,7 @@ export class VideoTableAttributes {
|
|||
'permanentLive',
|
||||
'latencyMode',
|
||||
'videoId',
|
||||
'replaySettingId',
|
||||
'createdAt',
|
||||
'updatedAt'
|
||||
]
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import { isVideoPrivacyValid } from '@server/helpers/custom-validators/videos'
|
||||
import { MLiveReplaySetting } from '@server/types/models/video/video-live-replay-setting'
|
||||
import { VideoPrivacy } from '@shared/models/videos/video-privacy.enum'
|
||||
import { Transaction } from 'sequelize'
|
||||
import { AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { throwIfNotValid } from '../shared/sequelize-helpers'
|
||||
|
||||
@Table({
|
||||
tableName: 'videoLiveReplaySetting'
|
||||
})
|
||||
export class VideoLiveReplaySettingModel extends Model<VideoLiveReplaySettingModel> {
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date
|
||||
|
||||
@AllowNull(false)
|
||||
@Is('VideoPrivacy', value => throwIfNotValid(value, isVideoPrivacyValid, 'privacy'))
|
||||
@Column
|
||||
privacy: VideoPrivacy
|
||||
|
||||
static load (id: number, transaction?: Transaction): Promise<MLiveReplaySetting> {
|
||||
return VideoLiveReplaySettingModel.findOne({
|
||||
where: { id },
|
||||
transaction
|
||||
})
|
||||
}
|
||||
|
||||
static removeSettings (id: number) {
|
||||
return VideoLiveReplaySettingModel.destroy({
|
||||
where: { id }
|
||||
})
|
||||
}
|
||||
|
||||
toFormattedJSON () {
|
||||
return {
|
||||
privacy: this.privacy
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,23 @@
|
|||
import { FindOptions } from 'sequelize'
|
||||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import {
|
||||
AllowNull,
|
||||
BeforeDestroy,
|
||||
BelongsTo,
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
Model,
|
||||
Scopes,
|
||||
Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { MVideoLiveSession, MVideoLiveSessionReplay } from '@server/types/models'
|
||||
import { uuidToShort } from '@shared/extra-utils'
|
||||
import { LiveVideoError, LiveVideoSession } from '@shared/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
import { VideoModel } from './video'
|
||||
import { VideoLiveReplaySettingModel } from './video-live-replay-setting'
|
||||
|
||||
export enum ScopeNames {
|
||||
WITH_REPLAY = 'WITH_REPLAY'
|
||||
|
@ -17,6 +30,10 @@ export enum ScopeNames {
|
|||
model: VideoModel.unscoped(),
|
||||
as: 'ReplayVideo',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: VideoLiveReplaySettingModel,
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -30,6 +47,10 @@ export enum ScopeNames {
|
|||
},
|
||||
{
|
||||
fields: [ 'liveVideoId' ]
|
||||
},
|
||||
{
|
||||
fields: [ 'replaySettingId' ],
|
||||
unique: true
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -89,6 +110,27 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv
|
|||
})
|
||||
LiveVideo: VideoModel
|
||||
|
||||
@ForeignKey(() => VideoLiveReplaySettingModel)
|
||||
@Column
|
||||
replaySettingId: number
|
||||
|
||||
@BelongsTo(() => VideoLiveReplaySettingModel, {
|
||||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'set null'
|
||||
})
|
||||
ReplaySetting: VideoLiveReplaySettingModel
|
||||
|
||||
@BeforeDestroy
|
||||
static deleteReplaySetting (instance: VideoLiveSessionModel) {
|
||||
return VideoLiveReplaySettingModel.destroy({
|
||||
where: {
|
||||
id: instance.replaySettingId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static load (id: number): Promise<MVideoLiveSession> {
|
||||
return VideoLiveSessionModel.findOne({
|
||||
where: { id }
|
||||
|
@ -146,6 +188,10 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv
|
|||
}
|
||||
: undefined
|
||||
|
||||
const replaySettings = this.replaySettingId
|
||||
? this.ReplaySetting.toFormattedJSON()
|
||||
: undefined
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
startDate: this.startDate.toISOString(),
|
||||
|
@ -154,6 +200,7 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv
|
|||
: null,
|
||||
endingProcessed: this.endingProcessed,
|
||||
saveReplay: this.saveReplay,
|
||||
replaySettings,
|
||||
replayVideo,
|
||||
error: this.error
|
||||
}
|
||||
|
|
|
@ -1,11 +1,24 @@
|
|||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import {
|
||||
BeforeDestroy,
|
||||
AllowNull,
|
||||
BelongsTo,
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
DefaultScope,
|
||||
ForeignKey,
|
||||
Model,
|
||||
Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { CONFIG } from '@server/initializers/config'
|
||||
import { WEBSERVER } from '@server/initializers/constants'
|
||||
import { MVideoLive, MVideoLiveVideo } from '@server/types/models'
|
||||
import { MVideoLive, MVideoLiveVideoWithSetting } from '@server/types/models'
|
||||
import { LiveVideo, LiveVideoLatencyMode, VideoState } from '@shared/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
import { VideoModel } from './video'
|
||||
import { VideoBlacklistModel } from './video-blacklist'
|
||||
import { VideoLiveReplaySettingModel } from './video-live-replay-setting'
|
||||
|
||||
@DefaultScope(() => ({
|
||||
include: [
|
||||
|
@ -18,6 +31,10 @@ import { VideoBlacklistModel } from './video-blacklist'
|
|||
required: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: VideoLiveReplaySettingModel,
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}))
|
||||
|
@ -27,6 +44,10 @@ import { VideoBlacklistModel } from './video-blacklist'
|
|||
{
|
||||
fields: [ 'videoId' ],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: [ 'replaySettingId' ],
|
||||
unique: true
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -66,6 +87,27 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>
|
|||
})
|
||||
Video: VideoModel
|
||||
|
||||
@ForeignKey(() => VideoLiveReplaySettingModel)
|
||||
@Column
|
||||
replaySettingId: number
|
||||
|
||||
@BelongsTo(() => VideoLiveReplaySettingModel, {
|
||||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'set null'
|
||||
})
|
||||
ReplaySetting: VideoLiveReplaySettingModel
|
||||
|
||||
@BeforeDestroy
|
||||
static deleteReplaySetting (instance: VideoLiveModel) {
|
||||
return VideoLiveReplaySettingModel.destroy({
|
||||
where: {
|
||||
id: instance.replaySettingId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static loadByStreamKey (streamKey: string) {
|
||||
const query = {
|
||||
where: {
|
||||
|
@ -84,11 +126,15 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>
|
|||
required: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: VideoLiveReplaySettingModel.unscoped(),
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return VideoLiveModel.findOne<MVideoLiveVideo>(query)
|
||||
return VideoLiveModel.findOne<MVideoLiveVideoWithSetting>(query)
|
||||
}
|
||||
|
||||
static loadByVideoId (videoId: number) {
|
||||
|
@ -120,11 +166,16 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>
|
|||
}
|
||||
}
|
||||
|
||||
const replaySettings = this.replaySettingId
|
||||
? this.ReplaySetting.toFormattedJSON()
|
||||
: undefined
|
||||
|
||||
return {
|
||||
...privateInformation,
|
||||
|
||||
permanentLive: this.permanentLive,
|
||||
saveReplay: this.saveReplay,
|
||||
replaySettings,
|
||||
latencyMode: this.latencyMode
|
||||
}
|
||||
}
|
||||
|
|
|
@ -706,6 +706,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
|
|||
name: 'videoId',
|
||||
allowNull: false
|
||||
},
|
||||
hooks: true,
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
VideoLive: VideoLiveModel
|
||||
|
|
|
@ -83,6 +83,7 @@ describe('Test video lives API validator', function () {
|
|||
privacy: VideoPrivacy.PUBLIC,
|
||||
channelId,
|
||||
saveReplay: false,
|
||||
replaySettings: undefined,
|
||||
permanentLive: false,
|
||||
latencyMode: LiveVideoLatencyMode.DEFAULT
|
||||
}
|
||||
|
@ -141,6 +142,12 @@ describe('Test video lives API validator', function () {
|
|||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad privacy for replay settings', async function () {
|
||||
const fields = { ...baseCorrectParams, replaySettings: { privacy: 5 } }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with another user channel', async function () {
|
||||
const user = {
|
||||
username: 'fake',
|
||||
|
@ -256,7 +263,7 @@ describe('Test video lives API validator', function () {
|
|||
})
|
||||
|
||||
it('Should forbid to save replay if not enabled by the admin', async function () {
|
||||
const fields = { ...baseCorrectParams, saveReplay: true }
|
||||
const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }
|
||||
|
||||
await server.config.updateCustomSubConfig({
|
||||
newConfig: {
|
||||
|
@ -277,7 +284,7 @@ describe('Test video lives API validator', function () {
|
|||
})
|
||||
|
||||
it('Should allow to save replay if enabled by the admin', async function () {
|
||||
const fields = { ...baseCorrectParams, saveReplay: true }
|
||||
const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }
|
||||
|
||||
await server.config.updateCustomSubConfig({
|
||||
newConfig: {
|
||||
|
@ -464,6 +471,39 @@ describe('Test video lives API validator', function () {
|
|||
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad privacy for replay settings', async function () {
|
||||
const fields = { saveReplay: true, replaySettings: { privacy: 5 } }
|
||||
|
||||
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with save replay enabled but without replay settings', async function () {
|
||||
await server.config.updateCustomSubConfig({
|
||||
newConfig: {
|
||||
live: {
|
||||
enabled: true,
|
||||
allowReplay: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const fields = { saveReplay: true }
|
||||
|
||||
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with save replay disabled and replay settings', async function () {
|
||||
const fields = { saveReplay: false, replaySettings: { privacy: VideoPrivacy.INTERNAL } }
|
||||
|
||||
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with only replay settings when save replay is disabled', async function () {
|
||||
const fields = { replaySettings: { privacy: VideoPrivacy.INTERNAL } }
|
||||
|
||||
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail to set latency if the server does not allow it', async function () {
|
||||
const fields = { latencyMode: LiveVideoLatencyMode.HIGH_LATENCY }
|
||||
|
||||
|
@ -474,6 +514,9 @@ describe('Test video lives API validator', function () {
|
|||
await command.update({ videoId: video.id, fields: { saveReplay: false } })
|
||||
await command.update({ videoId: video.uuid, fields: { saveReplay: false } })
|
||||
await command.update({ videoId: video.shortUUID, fields: { saveReplay: false } })
|
||||
|
||||
await command.update({ videoId: video.id, fields: { saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } })
|
||||
|
||||
})
|
||||
|
||||
it('Should fail to update replay status if replay is not allowed on the instance', async function () {
|
||||
|
|
|
@ -24,10 +24,7 @@ describe('Test live constraints', function () {
|
|||
let userAccessToken: string
|
||||
let userChannelId: number
|
||||
|
||||
async function createLiveWrapper (options: {
|
||||
replay: boolean
|
||||
permanent: boolean
|
||||
}) {
|
||||
async function createLiveWrapper (options: { replay: boolean, permanent: boolean }) {
|
||||
const { replay, permanent } = options
|
||||
|
||||
const liveAttributes = {
|
||||
|
@ -35,6 +32,7 @@ describe('Test live constraints', function () {
|
|||
channelId: userChannelId,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
saveReplay: replay,
|
||||
replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined,
|
||||
permanentLive: permanent
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ describe('Fast restream in live', function () {
|
|||
privacy: VideoPrivacy.PUBLIC,
|
||||
name: 'my super live',
|
||||
saveReplay: options.replay,
|
||||
replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined,
|
||||
permanentLive: options.permanent
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ describe('Save replay setting', function () {
|
|||
let liveVideoUUID: string
|
||||
let ffmpegCommand: FfmpegCommand
|
||||
|
||||
async function createLiveWrapper (options: { permanent: boolean, replay: boolean }) {
|
||||
async function createLiveWrapper (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacy } }) {
|
||||
if (liveVideoUUID) {
|
||||
try {
|
||||
await servers[0].videos.remove({ id: liveVideoUUID })
|
||||
|
@ -40,6 +40,7 @@ describe('Save replay setting', function () {
|
|||
privacy: VideoPrivacy.PUBLIC,
|
||||
name: 'my super live',
|
||||
saveReplay: options.replay,
|
||||
replaySettings: options.replaySettings,
|
||||
permanentLive: options.permanent
|
||||
}
|
||||
|
||||
|
@ -47,7 +48,7 @@ describe('Save replay setting', function () {
|
|||
return uuid
|
||||
}
|
||||
|
||||
async function publishLive (options: { permanent: boolean, replay: boolean }) {
|
||||
async function publishLive (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacy } }) {
|
||||
liveVideoUUID = await createLiveWrapper(options)
|
||||
|
||||
const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
|
||||
|
@ -61,7 +62,7 @@ describe('Save replay setting', function () {
|
|||
return { ffmpegCommand, liveDetails }
|
||||
}
|
||||
|
||||
async function publishLiveAndDelete (options: { permanent: boolean, replay: boolean }) {
|
||||
async function publishLiveAndDelete (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacy } }) {
|
||||
const { ffmpegCommand, liveDetails } = await publishLive(options)
|
||||
|
||||
await Promise.all([
|
||||
|
@ -76,7 +77,7 @@ describe('Save replay setting', function () {
|
|||
return { liveDetails }
|
||||
}
|
||||
|
||||
async function publishLiveAndBlacklist (options: { permanent: boolean, replay: boolean }) {
|
||||
async function publishLiveAndBlacklist (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacy } }) {
|
||||
const { ffmpegCommand, liveDetails } = await publishLive(options)
|
||||
|
||||
await Promise.all([
|
||||
|
@ -112,6 +113,13 @@ describe('Save replay setting', function () {
|
|||
}
|
||||
}
|
||||
|
||||
async function checkVideoPrivacy (videoId: string, privacy: VideoPrivacy) {
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoId })
|
||||
expect(video.privacy.id).to.equal(privacy)
|
||||
}
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
|
@ -247,12 +255,13 @@ describe('Save replay setting', function () {
|
|||
it('Should correctly create and federate the "waiting for stream" live', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
liveVideoUUID = await createLiveWrapper({ permanent: false, replay: true })
|
||||
liveVideoUUID = await createLiveWrapper({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
|
||||
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
|
||||
})
|
||||
|
||||
it('Should correctly have updated the live and federated it when streaming in the live', async function () {
|
||||
|
@ -265,6 +274,7 @@ describe('Save replay setting', function () {
|
|||
|
||||
await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
|
||||
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
|
||||
})
|
||||
|
||||
it('Should correctly have saved the live and federated it after the streaming', async function () {
|
||||
|
@ -274,6 +284,8 @@ describe('Save replay setting', function () {
|
|||
expect(session.endDate).to.not.exist
|
||||
expect(session.endingProcessed).to.be.false
|
||||
expect(session.saveReplay).to.be.true
|
||||
expect(session.replaySettings).to.exist
|
||||
expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED)
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
|
||||
|
@ -281,8 +293,9 @@ describe('Save replay setting', function () {
|
|||
await waitJobs(servers)
|
||||
|
||||
// Live has been transcoded
|
||||
await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
|
||||
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
|
||||
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.UNLISTED)
|
||||
})
|
||||
|
||||
it('Should find the replay live session', async function () {
|
||||
|
@ -296,6 +309,8 @@ describe('Save replay setting', function () {
|
|||
expect(session.error).to.not.exist
|
||||
expect(session.saveReplay).to.be.true
|
||||
expect(session.endingProcessed).to.be.true
|
||||
expect(session.replaySettings).to.exist
|
||||
expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED)
|
||||
|
||||
expect(session.replayVideo).to.exist
|
||||
expect(session.replayVideo.id).to.exist
|
||||
|
@ -306,13 +321,14 @@ describe('Save replay setting', function () {
|
|||
it('Should update the saved live and correctly federate the updated attributes', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await servers[0].videos.update({ id: liveVideoUUID, attributes: { name: 'video updated' } })
|
||||
await servers[0].videos.update({ id: liveVideoUUID, attributes: { name: 'video updated', privacy: VideoPrivacy.PUBLIC } })
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: liveVideoUUID })
|
||||
expect(video.name).to.equal('video updated')
|
||||
expect(video.isLive).to.be.false
|
||||
expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -323,7 +339,7 @@ describe('Save replay setting', function () {
|
|||
it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await publishLiveAndBlacklist({ permanent: false, replay: true })
|
||||
await publishLiveAndBlacklist({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } })
|
||||
|
||||
await checkVideosExist(liveVideoUUID, false)
|
||||
|
||||
|
@ -338,7 +354,7 @@ describe('Save replay setting', function () {
|
|||
it('Should correctly terminate the stream on delete and delete the video', async function () {
|
||||
this.timeout(40000)
|
||||
|
||||
await publishLiveAndDelete({ permanent: false, replay: true })
|
||||
await publishLiveAndDelete({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } })
|
||||
|
||||
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404)
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||
|
@ -348,103 +364,201 @@ describe('Save replay setting', function () {
|
|||
describe('With save replay enabled on permanent live', function () {
|
||||
let lastReplayUUID: string
|
||||
|
||||
it('Should correctly create and federate the "waiting for stream" live', async function () {
|
||||
this.timeout(20000)
|
||||
describe('With a first live and its replay', function () {
|
||||
|
||||
liveVideoUUID = await createLiveWrapper({ permanent: true, replay: true })
|
||||
it('Should correctly create and federate the "waiting for stream" live', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await waitJobs(servers)
|
||||
liveVideoUUID = await createLiveWrapper({ permanent: true, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } })
|
||||
|
||||
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
|
||||
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
|
||||
})
|
||||
|
||||
it('Should correctly have updated the live and federated it when streaming in the live', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
|
||||
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
|
||||
})
|
||||
|
||||
it('Should correctly have saved the live and federated it after the streaming', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
const liveDetails = await servers[0].videos.get({ id: liveVideoUUID })
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
|
||||
await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID)
|
||||
await waitJobs(servers)
|
||||
|
||||
const video = await findExternalSavedVideo(servers[0], liveDetails)
|
||||
expect(video).to.exist
|
||||
|
||||
for (const server of servers) {
|
||||
await server.videos.get({ id: video.uuid })
|
||||
}
|
||||
|
||||
lastReplayUUID = video.uuid
|
||||
})
|
||||
|
||||
it('Should have appropriate ended session and replay live session', async function () {
|
||||
const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID })
|
||||
expect(total).to.equal(1)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
|
||||
const sessionFromLive = data[0]
|
||||
const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID })
|
||||
|
||||
for (const session of [ sessionFromLive, sessionFromReplay ]) {
|
||||
expect(session.startDate).to.exist
|
||||
expect(session.endDate).to.exist
|
||||
|
||||
expect(session.replaySettings).to.exist
|
||||
expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED)
|
||||
|
||||
expect(session.error).to.not.exist
|
||||
|
||||
expect(session.replayVideo).to.exist
|
||||
expect(session.replayVideo.id).to.exist
|
||||
expect(session.replayVideo.shortUUID).to.exist
|
||||
expect(session.replayVideo.uuid).to.equal(lastReplayUUID)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have the first live replay with correct settings', async function () {
|
||||
await checkVideosExist(lastReplayUUID, false, HttpStatusCode.OK_200)
|
||||
await checkVideoState(lastReplayUUID, VideoState.PUBLISHED)
|
||||
await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.UNLISTED)
|
||||
})
|
||||
})
|
||||
|
||||
it('Should correctly have updated the live and federated it when streaming in the live', async function () {
|
||||
this.timeout(20000)
|
||||
describe('With a second live and its replay', function () {
|
||||
it('Should update the replay settings', async function () {
|
||||
await servers[0].live.update(
|
||||
{ videoId: liveVideoUUID, fields: { replaySettings: { privacy: VideoPrivacy.PUBLIC } } })
|
||||
await waitJobs(servers)
|
||||
const live = await servers[0].live.get({ videoId: liveVideoUUID })
|
||||
|
||||
ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
|
||||
expect(live.saveReplay).to.be.true
|
||||
expect(live.replaySettings).to.exist
|
||||
expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC)
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
|
||||
})
|
||||
it('Should correctly have updated the live and federated it when streaming in the live', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
it('Should correctly have saved the live and federated it after the streaming', async function () {
|
||||
this.timeout(30000)
|
||||
ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
|
||||
|
||||
const liveDetails = await servers[0].videos.get({ id: liveVideoUUID })
|
||||
await waitJobs(servers)
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
|
||||
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
|
||||
})
|
||||
|
||||
await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID)
|
||||
await waitJobs(servers)
|
||||
it('Should correctly have saved the live and federated it after the streaming', async function () {
|
||||
this.timeout(30000)
|
||||
const liveDetails = await servers[0].videos.get({ id: liveVideoUUID })
|
||||
|
||||
const video = await findExternalSavedVideo(servers[0], liveDetails)
|
||||
expect(video).to.exist
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
|
||||
for (const server of servers) {
|
||||
await server.videos.get({ id: video.uuid })
|
||||
}
|
||||
await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID)
|
||||
await waitJobs(servers)
|
||||
|
||||
lastReplayUUID = video.uuid
|
||||
})
|
||||
const video = await findExternalSavedVideo(servers[0], liveDetails)
|
||||
expect(video).to.exist
|
||||
|
||||
it('Should have appropriate ended session and replay live session', async function () {
|
||||
const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID })
|
||||
expect(total).to.equal(1)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
for (const server of servers) {
|
||||
await server.videos.get({ id: video.uuid })
|
||||
}
|
||||
|
||||
const sessionFromLive = data[0]
|
||||
const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID })
|
||||
lastReplayUUID = video.uuid
|
||||
})
|
||||
|
||||
for (const session of [ sessionFromLive, sessionFromReplay ]) {
|
||||
expect(session.startDate).to.exist
|
||||
expect(session.endDate).to.exist
|
||||
it('Should have appropriate ended session and replay live session', async function () {
|
||||
const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID })
|
||||
expect(total).to.equal(2)
|
||||
expect(data).to.have.lengthOf(2)
|
||||
|
||||
expect(session.error).to.not.exist
|
||||
const sessionFromLive = data[1]
|
||||
const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID })
|
||||
|
||||
expect(session.replayVideo).to.exist
|
||||
expect(session.replayVideo.id).to.exist
|
||||
expect(session.replayVideo.shortUUID).to.exist
|
||||
expect(session.replayVideo.uuid).to.equal(lastReplayUUID)
|
||||
}
|
||||
})
|
||||
for (const session of [ sessionFromLive, sessionFromReplay ]) {
|
||||
expect(session.startDate).to.exist
|
||||
expect(session.endDate).to.exist
|
||||
|
||||
it('Should have cleaned up the live files', async function () {
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||
})
|
||||
expect(session.replaySettings).to.exist
|
||||
expect(session.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC)
|
||||
|
||||
it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () {
|
||||
this.timeout(120000)
|
||||
expect(session.error).to.not.exist
|
||||
|
||||
await servers[0].videos.remove({ id: lastReplayUUID })
|
||||
const { liveDetails } = await publishLiveAndBlacklist({ permanent: true, replay: true })
|
||||
expect(session.replayVideo).to.exist
|
||||
expect(session.replayVideo.id).to.exist
|
||||
expect(session.replayVideo.shortUUID).to.exist
|
||||
expect(session.replayVideo.uuid).to.equal(lastReplayUUID)
|
||||
}
|
||||
})
|
||||
|
||||
const replay = await findExternalSavedVideo(servers[0], liveDetails)
|
||||
expect(replay).to.exist
|
||||
it('Should have the first live replay with correct settings', async function () {
|
||||
await checkVideosExist(lastReplayUUID, true, HttpStatusCode.OK_200)
|
||||
await checkVideoState(lastReplayUUID, VideoState.PUBLISHED)
|
||||
await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.PUBLIC)
|
||||
})
|
||||
|
||||
for (const videoId of [ liveVideoUUID, replay.uuid ]) {
|
||||
await checkVideosExist(videoId, false)
|
||||
it('Should have cleaned up the live files', async function () {
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||
})
|
||||
|
||||
await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
await servers[1].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
}
|
||||
it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||
})
|
||||
await servers[0].videos.remove({ id: lastReplayUUID })
|
||||
const { liveDetails } = await publishLiveAndBlacklist({
|
||||
permanent: true,
|
||||
replay: true,
|
||||
replaySettings: { privacy: VideoPrivacy.PUBLIC }
|
||||
})
|
||||
|
||||
it('Should correctly terminate the stream on delete and not save the video', async function () {
|
||||
this.timeout(40000)
|
||||
const replay = await findExternalSavedVideo(servers[0], liveDetails)
|
||||
expect(replay).to.exist
|
||||
|
||||
const { liveDetails } = await publishLiveAndDelete({ permanent: true, replay: true })
|
||||
for (const videoId of [ liveVideoUUID, replay.uuid ]) {
|
||||
await checkVideosExist(videoId, false)
|
||||
|
||||
const replay = await findExternalSavedVideo(servers[0], liveDetails)
|
||||
expect(replay).to.not.exist
|
||||
await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
await servers[1].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
}
|
||||
|
||||
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404)
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||
})
|
||||
|
||||
it('Should correctly terminate the stream on delete and not save the video', async function () {
|
||||
this.timeout(40000)
|
||||
|
||||
const { liveDetails } = await publishLiveAndDelete({
|
||||
permanent: true,
|
||||
replay: true,
|
||||
replaySettings: { privacy: VideoPrivacy.PUBLIC }
|
||||
})
|
||||
|
||||
const replay = await findExternalSavedVideo(servers[0], liveDetails)
|
||||
expect(replay).to.not.exist
|
||||
|
||||
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404)
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -87,6 +87,7 @@ describe('Test live', function () {
|
|||
commentsEnabled: false,
|
||||
downloadEnabled: false,
|
||||
saveReplay: true,
|
||||
replaySettings: { privacy: VideoPrivacy.PUBLIC },
|
||||
latencyMode: LiveVideoLatencyMode.SMALL_LATENCY,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
previewfile: 'video_short1-preview.webm.jpg',
|
||||
|
@ -128,6 +129,9 @@ describe('Test live', function () {
|
|||
if (server.url === servers[0].url) {
|
||||
expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live')
|
||||
expect(live.streamKey).to.not.be.empty
|
||||
|
||||
expect(live.replaySettings).to.exist
|
||||
expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC)
|
||||
} else {
|
||||
expect(live.rtmpUrl).to.not.exist
|
||||
expect(live.streamKey).to.not.exist
|
||||
|
@ -196,6 +200,7 @@ describe('Test live', function () {
|
|||
}
|
||||
|
||||
expect(live.saveReplay).to.be.false
|
||||
expect(live.replaySettings).to.not.exist
|
||||
expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT)
|
||||
}
|
||||
})
|
||||
|
@ -366,7 +371,10 @@ describe('Test live', function () {
|
|||
name: 'live video',
|
||||
channelId: servers[0].store.channel.id,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
saveReplay
|
||||
saveReplay,
|
||||
replaySettings: saveReplay
|
||||
? { privacy: VideoPrivacy.PUBLIC }
|
||||
: undefined
|
||||
}
|
||||
|
||||
const { uuid } = await commands[0].create({ fields: liveAttributes })
|
||||
|
@ -670,6 +678,9 @@ describe('Test live', function () {
|
|||
channelId: servers[0].store.channel.id,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
saveReplay: options.saveReplay,
|
||||
replaySettings: options.saveReplay
|
||||
? { privacy: VideoPrivacy.PUBLIC }
|
||||
: undefined,
|
||||
permanentLive: options.permanent
|
||||
}
|
||||
|
||||
|
|
|
@ -342,6 +342,7 @@ describe('Test user notifications', function () {
|
|||
privacy: VideoPrivacy.PUBLIC,
|
||||
channelId: servers[1].store.channel.id,
|
||||
saveReplay: true,
|
||||
replaySettings: { privacy: VideoPrivacy.PUBLIC },
|
||||
permanentLive: false
|
||||
}
|
||||
})
|
||||
|
@ -367,6 +368,7 @@ describe('Test user notifications', function () {
|
|||
privacy: VideoPrivacy.PUBLIC,
|
||||
channelId: servers[1].store.channel.id,
|
||||
saveReplay: true,
|
||||
replaySettings: { privacy: VideoPrivacy.PUBLIC },
|
||||
permanentLive: true
|
||||
}
|
||||
})
|
||||
|
|
|
@ -27,6 +27,7 @@ async function createLive (server: PeerTubeServer, permanent: boolean) {
|
|||
privacy: VideoPrivacy.PUBLIC,
|
||||
name: 'my super live',
|
||||
saveReplay: true,
|
||||
replaySettings: { privacy: VideoPrivacy.PUBLIC },
|
||||
permanentLive: permanent
|
||||
}
|
||||
|
||||
|
|
|
@ -305,13 +305,21 @@ describe('Object storage for video static file privacy', function () {
|
|||
})
|
||||
|
||||
{
|
||||
const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: false, privacy: VideoPrivacy.PRIVATE })
|
||||
const { video, live } = await server.live.quickCreate({
|
||||
saveReplay: true,
|
||||
permanentLive: false,
|
||||
privacy: VideoPrivacy.PRIVATE
|
||||
})
|
||||
normalLiveId = video.uuid
|
||||
normalLive = live
|
||||
}
|
||||
|
||||
{
|
||||
const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: true, privacy: VideoPrivacy.PRIVATE })
|
||||
const { video, live } = await server.live.quickCreate({
|
||||
saveReplay: true,
|
||||
permanentLive: true,
|
||||
privacy: VideoPrivacy.PRIVATE
|
||||
})
|
||||
permanentLiveId = video.uuid
|
||||
permanentLive = live
|
||||
}
|
||||
|
|
|
@ -364,13 +364,21 @@ describe('Test video static file privacy', function () {
|
|||
})
|
||||
|
||||
{
|
||||
const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: false, privacy: VideoPrivacy.PRIVATE })
|
||||
const { video, live } = await server.live.quickCreate({
|
||||
saveReplay: true,
|
||||
permanentLive: false,
|
||||
privacy: VideoPrivacy.PRIVATE
|
||||
})
|
||||
normalLiveId = video.uuid
|
||||
normalLive = live
|
||||
}
|
||||
|
||||
{
|
||||
const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: true, privacy: VideoPrivacy.PRIVATE })
|
||||
const { video, live } = await server.live.quickCreate({
|
||||
saveReplay: true,
|
||||
permanentLive: true,
|
||||
privacy: VideoPrivacy.PRIVATE
|
||||
})
|
||||
permanentLiveId = video.uuid
|
||||
permanentLive = live
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { OutgoingHttpHeaders } from 'http'
|
||||
import { Writable } from 'stream'
|
||||
import { RegisterServerAuthExternalOptions } from '@server/types'
|
||||
import {
|
||||
MAbuseMessage,
|
||||
|
@ -16,7 +17,7 @@ import {
|
|||
MVideoFormattableDetails,
|
||||
MVideoId,
|
||||
MVideoImmutable,
|
||||
MVideoLive,
|
||||
MVideoLiveFormattable,
|
||||
MVideoPlaylistFull,
|
||||
MVideoPlaylistFullSummary
|
||||
} from '@server/types/models'
|
||||
|
@ -43,7 +44,6 @@ import {
|
|||
MVideoShareActor,
|
||||
MVideoThumbnail
|
||||
} from './models'
|
||||
import { Writable } from 'stream'
|
||||
import { MVideoSource } from './models/video/video-source'
|
||||
|
||||
declare module 'express' {
|
||||
|
@ -124,7 +124,7 @@ declare module 'express' {
|
|||
onlyVideo?: MVideoThumbnail
|
||||
videoId?: MVideoId
|
||||
|
||||
videoLive?: MVideoLive
|
||||
videoLive?: MVideoLiveFormattable
|
||||
videoLiveSession?: MVideoLiveSession
|
||||
|
||||
videoShare?: MVideoShareActor
|
||||
|
|
|
@ -13,6 +13,7 @@ export * from './video-channels'
|
|||
export * from './video-comment'
|
||||
export * from './video-file'
|
||||
export * from './video-import'
|
||||
export * from './video-live-replay-setting'
|
||||
export * from './video-live-session'
|
||||
export * from './video-live'
|
||||
export * from './video-playlist'
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
|
||||
|
||||
export type MLiveReplaySetting = Omit<VideoLiveReplaySettingModel, 'VideoLive' | 'VideoLiveSession'>
|
|
@ -1,15 +1,17 @@
|
|||
import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
|
||||
import { PickWith } from '@shared/typescript-utils'
|
||||
import { MVideo } from './video'
|
||||
import { MLiveReplaySetting } from './video-live-replay-setting'
|
||||
|
||||
type Use<K extends keyof VideoLiveSessionModel, M> = PickWith<VideoLiveSessionModel, K, M>
|
||||
|
||||
// ############################################################################
|
||||
|
||||
export type MVideoLiveSession = Omit<VideoLiveSessionModel, 'Video' | 'VideoLive'>
|
||||
export type MVideoLiveSession = Omit<VideoLiveSessionModel, 'Video' | 'VideoLive' | 'ReplaySetting'>
|
||||
|
||||
// ############################################################################
|
||||
|
||||
export type MVideoLiveSessionReplay =
|
||||
MVideoLiveSession &
|
||||
Use<'ReplayVideo', MVideo>
|
||||
Use<'ReplayVideo', MVideo> &
|
||||
Use<'ReplaySetting', MLiveReplaySetting>
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
import { VideoLiveModel } from '@server/models/video/video-live'
|
||||
import { PickWith } from '@shared/typescript-utils'
|
||||
import { MVideo } from './video'
|
||||
import { MLiveReplaySetting } from './video-live-replay-setting'
|
||||
|
||||
type Use<K extends keyof VideoLiveModel, M> = PickWith<VideoLiveModel, K, M>
|
||||
|
||||
// ############################################################################
|
||||
|
||||
export type MVideoLive = Omit<VideoLiveModel, 'Video'>
|
||||
export type MVideoLive = Omit<VideoLiveModel, 'Video' | 'ReplaySetting'>
|
||||
|
||||
// ############################################################################
|
||||
|
||||
export type MVideoLiveVideo =
|
||||
MVideoLive &
|
||||
Use<'Video', MVideo>
|
||||
|
||||
// ############################################################################
|
||||
|
||||
export type MVideoLiveVideoWithSetting =
|
||||
MVideoLiveVideo &
|
||||
Use<'ReplaySetting', MLiveReplaySetting>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { VideoCreate } from '../video-create.model'
|
||||
import { VideoPrivacy } from '../video-privacy.enum'
|
||||
import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
|
||||
|
||||
export interface LiveVideoCreate extends VideoCreate {
|
||||
|
@ -6,4 +7,5 @@ export interface LiveVideoCreate extends VideoCreate {
|
|||
latencyMode?: LiveVideoLatencyMode
|
||||
|
||||
saveReplay?: boolean
|
||||
replaySettings?: { privacy: VideoPrivacy }
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { VideoPrivacy } from '../video-privacy.enum'
|
||||
import { LiveVideoError } from './live-video-error.enum'
|
||||
|
||||
export interface LiveVideoSession {
|
||||
|
@ -11,6 +12,8 @@ export interface LiveVideoSession {
|
|||
saveReplay: boolean
|
||||
endingProcessed: boolean
|
||||
|
||||
replaySettings?: { privacy: VideoPrivacy }
|
||||
|
||||
replayVideo: {
|
||||
id: number
|
||||
uuid: string
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { VideoPrivacy } from '../video-privacy.enum'
|
||||
import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
|
||||
|
||||
export interface LiveVideoUpdate {
|
||||
permanentLive?: boolean
|
||||
saveReplay?: boolean
|
||||
replaySettings?: { privacy: VideoPrivacy }
|
||||
latencyMode?: LiveVideoLatencyMode
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { VideoPrivacy } from '../video-privacy.enum'
|
||||
import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
|
||||
|
||||
export interface LiveVideo {
|
||||
|
@ -7,6 +8,7 @@ export interface LiveVideo {
|
|||
streamKey?: string
|
||||
|
||||
saveReplay: boolean
|
||||
replaySettings?: { privacy: VideoPrivacy }
|
||||
permanentLive: boolean
|
||||
latencyMode: LiveVideoLatencyMode
|
||||
}
|
||||
|
|
|
@ -130,6 +130,7 @@ export class LiveCommand extends AbstractCommand {
|
|||
name: 'live',
|
||||
permanentLive,
|
||||
saveReplay,
|
||||
replaySettings: { privacy },
|
||||
channelId: this.server.store.channel.id,
|
||||
privacy
|
||||
}
|
||||
|
|
|
@ -2446,7 +2446,7 @@ paths:
|
|||
/api/v1/videos/privacies:
|
||||
get:
|
||||
summary: List available video privacy policies
|
||||
operationId: getPrivacyPolicies
|
||||
operationId: getVideoPrivacyPolicies
|
||||
tags:
|
||||
- Video
|
||||
responses:
|
||||
|
@ -3087,6 +3087,8 @@ paths:
|
|||
type: integer
|
||||
saveReplay:
|
||||
type: boolean
|
||||
replaySettings:
|
||||
$ref: '#/components/schemas/LiveVideoReplaySettings'
|
||||
permanentLive:
|
||||
description: User can stream multiple times in a permanent live
|
||||
type: boolean
|
||||
|
@ -6088,7 +6090,7 @@ components:
|
|||
- 1
|
||||
- 2
|
||||
- 3
|
||||
description: Video playlist privacy policy (see [/video-playlists/privacies])
|
||||
description: Video playlist privacy policy (see [/video-playlists/privacies](#operation/getPlaylistPrivacyPolicies))
|
||||
VideoPlaylistPrivacyConstant:
|
||||
properties:
|
||||
id:
|
||||
|
@ -6116,7 +6118,7 @@ components:
|
|||
- 2
|
||||
- 3
|
||||
- 4
|
||||
description: privacy id of the video (see [/videos/privacies](#operation/getPrivacyPolicies))
|
||||
description: privacy id of the video (see [/videos/privacies](#operation/getVideoPrivacyPolicies))
|
||||
VideoPrivacyConstant:
|
||||
properties:
|
||||
id:
|
||||
|
@ -6177,6 +6179,14 @@ components:
|
|||
- 2
|
||||
- 3
|
||||
description: 'The live latency mode (Default = `1`, High latency = `2`, Small Latency = `3`)'
|
||||
|
||||
LiveVideoReplaySettings:
|
||||
type: object
|
||||
properties:
|
||||
privacy:
|
||||
# description: Video playlist privacy policy (see [../video-playlists/privacies])
|
||||
$ref: '#/components/schemas/VideoPrivacySet'
|
||||
|
||||
|
||||
VideoStateConstant:
|
||||
properties:
|
||||
|
@ -8693,6 +8703,8 @@ components:
|
|||
properties:
|
||||
saveReplay:
|
||||
type: boolean
|
||||
replaySettings:
|
||||
$ref: '#/components/schemas/LiveVideoReplaySettings'
|
||||
permanentLive:
|
||||
description: User can stream multiple times in a permanent live
|
||||
type: boolean
|
||||
|
@ -8713,6 +8725,8 @@ components:
|
|||
description: RTMP stream key to use to stream into this live video. Included in the response if an appropriate token is provided
|
||||
saveReplay:
|
||||
type: boolean
|
||||
replaySettings:
|
||||
$ref: '#/components/schemas/LiveVideoReplaySettings'
|
||||
permanentLive:
|
||||
description: User can stream multiple times in a permanent live
|
||||
type: boolean
|
||||
|
|
Loading…
Reference in New Issue