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:
Wicklow 2023-03-31 07:12:21 +00:00 committed by GitHub
parent ebd61437c1
commit 05a60d8599
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 746 additions and 120 deletions

View File

@ -272,7 +272,7 @@
</div> </div>
</div> </div>
<div class="form-group" *ngIf="isSaveReplayEnabled()"> <div class="form-group" *ngIf="isSaveReplayAllowed()">
<my-peertube-checkbox inputName="liveVideoSaveReplay" formControlName="saveReplay"> <my-peertube-checkbox inputName="liveVideoSaveReplay" formControlName="saveReplay">
<ng-template ptTemplate="label"> <ng-template ptTemplate="label">
<ng-container i18n>Automatically publish a replay when your live ends</ng-container> <ng-container i18n>Automatically publish a replay when your live ends</ng-container>
@ -284,6 +284,13 @@
</my-peertube-checkbox> </my-peertube-checkbox>
</div> </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()"> <div class="form-group" *ngIf="isLatencyModeEnabled()">
<label i18n for="latencyMode">Latency mode</label> <label i18n for="latencyMode">Latency mode</label>
<my-select-options <my-select-options

View File

@ -165,7 +165,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
liveStreamKey: null, liveStreamKey: null,
permanentLive: null, permanentLive: null,
latencyMode: null, latencyMode: null,
saveReplay: null saveReplay: null,
replayPrivacy: null
} }
this.formValidatorService.updateFormGroup( this.formValidatorService.updateFormGroup(
@ -303,10 +304,14 @@ export class VideoEditComponent implements OnInit, OnDestroy {
modalRef.componentInstance.captionEdited.subscribe(this.onCaptionEdited.bind(this)) modalRef.componentInstance.captionEdited.subscribe(this.onCaptionEdited.bind(this))
} }
isSaveReplayEnabled () { isSaveReplayAllowed () {
return this.serverConfig.live.allowReplay return this.serverConfig.live.allowReplay
} }
isSaveReplayEnabled () {
return this.form.value['saveReplay'] === true
}
isPermanentLiveEnabled () { isPermanentLiveEnabled () {
return this.form.value['permanentLive'] === true return this.form.value['permanentLive'] === true
} }

View File

@ -8,7 +8,15 @@ import { Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared
import { LiveVideoService } from '@app/shared/shared-video-live' import { LiveVideoService } from '@app/shared/shared-video-live'
import { LoadingBarService } from '@ngx-loading-bar/core' import { LoadingBarService } from '@ngx-loading-bar/core'
import { logger } from '@root-helpers/logger' 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' import { VideoSend } from './video-send'
@Component({ @Component({
@ -79,11 +87,12 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
permanentLive: this.firstStepPermanentLive, permanentLive: this.firstStepPermanentLive,
latencyMode: LiveVideoLatencyMode.DEFAULT, latencyMode: LiveVideoLatencyMode.DEFAULT,
saveReplay: this.isReplayAllowed(), saveReplay: this.isReplayAllowed(),
replaySettings: { privacy: VideoPrivacy.PRIVATE },
channelId: this.firstStepChannelId channelId: this.firstStepChannelId
} }
// Go live in private mode, but correctly fill the update form with the first user choice // 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.form.patchValue(toPatch)
this.liveVideoService.goLive(video) this.liveVideoService.goLive(video)
@ -130,6 +139,7 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
const liveVideoUpdate: LiveVideoUpdate = { const liveVideoUpdate: LiveVideoUpdate = {
saveReplay: this.form.value.saveReplay, saveReplay: this.form.value.saveReplay,
replaySettings: { privacy: this.form.value.replayPrivacy },
latencyMode: this.form.value.latencyMode, latencyMode: this.form.value.latencyMode,
permanentLive: this.form.value.permanentLive permanentLive: this.form.value.permanentLive
} }

View File

@ -67,6 +67,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
if (this.liveVideo) { if (this.liveVideo) {
this.form.patchValue({ this.form.patchValue({
saveReplay: this.liveVideo.saveReplay, saveReplay: this.liveVideo.saveReplay,
replayPrivacy: this.liveVideo.replaySettings ? this.liveVideo.replaySettings.privacy : VideoPrivacy.PRIVATE,
latencyMode: this.liveVideo.latencyMode, latencyMode: this.liveVideo.latencyMode,
permanentLive: this.liveVideo.permanentLive permanentLive: this.liveVideo.permanentLive
}) })
@ -127,6 +128,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
const liveVideoUpdate: LiveVideoUpdate = { const liveVideoUpdate: LiveVideoUpdate = {
saveReplay: !!this.form.value.saveReplay, saveReplay: !!this.form.value.saveReplay,
replaySettings: { privacy: this.form.value.replayPrivacy },
permanentLive: !!this.form.value.permanentLive, permanentLive: !!this.form.value.permanentLive,
latencyMode: this.form.value.latencyMode latencyMode: this.form.value.latencyMode
} }

View File

@ -16,7 +16,7 @@ import {
} from '@server/middlewares/validators/videos/video-live' } from '@server/middlewares/validators/videos/video-live'
import { VideoLiveModel } from '@server/models/video/video-live' import { VideoLiveModel } from '@server/models/video/video-live'
import { VideoLiveSessionModel } from '@server/models/video/video-live-session' 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 { buildUUID, uuidToShort } from '@shared/extra-utils'
import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoState } from '@shared/models' import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoState } from '@shared/models'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
@ -24,6 +24,7 @@ import { sequelizeTypescript } from '../../../initializers/database'
import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail'
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares' import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
const liveRouter = express.Router() const liveRouter = express.Router()
@ -105,7 +106,10 @@ async function updateLiveVideo (req: express.Request, res: express.Response) {
const video = res.locals.videoAll const video = res.locals.videoAll
const videoLive = res.locals.videoLive 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.permanentLive)) videoLive.permanentLive = body.permanentLive
if (exists(body.latencyMode)) videoLive.latencyMode = body.latencyMode 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() 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) { async function addLiveVideo (req: express.Request, res: express.Response) {
const videoInfo: LiveVideoCreate = req.body 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 // Do not forget to add video channel information to the created video
videoCreated.VideoChannel = res.locals.videoChannel 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 videoLive.videoId = videoCreated.id
videoCreated.VideoLive = await videoLive.save(sequelizeOptions) videoCreated.VideoLive = await videoLive.save(sequelizeOptions)

View File

@ -26,7 +26,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 755 const LAST_MIGRATION_VERSION = 760
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -52,6 +52,7 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla
import { VideoTagModel } from '../models/video/video-tag' import { VideoTagModel } from '../models/video/video-tag'
import { VideoViewModel } from '../models/view/video-view' import { VideoViewModel } from '../models/view/video-view'
import { CONFIG } from './config' 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 require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
@ -141,6 +142,7 @@ async function initDatabaseModels (silent: boolean) {
UserVideoHistoryModel, UserVideoHistoryModel,
VideoLiveModel, VideoLiveModel,
VideoLiveSessionModel, VideoLiveSessionModel,
VideoLiveReplaySettingModel,
AccountBlocklistModel, AccountBlocklistModel,
ServerBlocklistModel, ServerBlocklistModel,
UserNotificationModel, UserNotificationModel,

View File

@ -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
}

View File

@ -19,6 +19,7 @@ import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@serv
import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models'
import { logger, loggerTagsFactory } from '../../../helpers/logger' import { logger, loggerTagsFactory } from '../../../helpers/logger'
import { VideoPathManager } from '@server/lib/video-path-manager' import { VideoPathManager } from '@server/lib/video-path-manager'
import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
const lTags = loggerTagsFactory('live', 'job') const lTags = loggerTagsFactory('live', 'job')
@ -60,7 +61,13 @@ async function processVideoLiveEnding (job: Job) {
return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId }) 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 { liveVideo, liveSession, publishedAt, replayDirectory } = options
const replaySettings = await VideoLiveReplaySettingModel.load(liveSession.replaySettingId)
const replayVideo = new VideoModel({ const replayVideo = new VideoModel({
name: `${liveVideo.name} - ${new Date(publishedAt).toLocaleString()}`, name: `${liveVideo.name} - ${new Date(publishedAt).toLocaleString()}`,
isLive: false, isLive: false,
@ -95,7 +104,7 @@ async function saveReplayToExternalVideo (options: {
nsfw: liveVideo.nsfw, nsfw: liveVideo.nsfw,
description: liveVideo.description, description: liveVideo.description,
support: liveVideo.support, support: liveVideo.support,
privacy: liveVideo.privacy, privacy: replaySettings.privacy,
channelId: liveVideo.channelId channelId: liveVideo.channelId
}) as MVideoWithAllFiles }) as MVideoWithAllFiles
@ -142,6 +151,7 @@ async function replaceLiveByReplay (options: {
}) { }) {
const { video, liveSession, live, permanentLive, replayDirectory } = options const { video, liveSession, live, permanentLive, replayDirectory } = options
const replaySettings = await VideoLiveReplaySettingModel.load(liveSession.replaySettingId)
const videoWithFiles = await VideoModel.loadFull(video.id) const videoWithFiles = await VideoModel.loadFull(video.id)
const hlsPlaylist = videoWithFiles.getHLSPlaylist() const hlsPlaylist = videoWithFiles.getHLSPlaylist()
@ -150,6 +160,7 @@ async function replaceLiveByReplay (options: {
await live.destroy() await live.destroy()
videoWithFiles.isLive = false videoWithFiles.isLive = false
videoWithFiles.privacy = replaySettings.privacy
videoWithFiles.waitTranscoding = true videoWithFiles.waitTranscoding = true
videoWithFiles.state = VideoState.TO_TRANSCODE videoWithFiles.state = VideoState.TO_TRANSCODE

View File

@ -19,7 +19,7 @@ import { VideoModel } from '@server/models/video/video'
import { VideoLiveModel } from '@server/models/video/video-live' import { VideoLiveModel } from '@server/models/video/video-live'
import { VideoLiveSessionModel } from '@server/models/video/video-live-session' import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 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 { pick, wait } from '@shared/core-utils'
import { LiveVideoError, VideoState } from '@shared/models' import { LiveVideoError, VideoState } from '@shared/models'
import { federateVideoIfNeeded } from '../activitypub/videos' import { federateVideoIfNeeded } from '../activitypub/videos'
@ -30,6 +30,8 @@ import { Hooks } from '../plugins/hooks'
import { LiveQuotaStore } from './live-quota-store' import { LiveQuotaStore } from './live-quota-store'
import { cleanupAndDestroyPermanentLive } from './live-utils' import { cleanupAndDestroyPermanentLive } from './live-utils'
import { MuxingSession } from './shared' 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 NodeRtmpSession = require('node-media-server/src/node_rtmp_session')
const context = require('node-media-server/src/node_core_ctx') const context = require('node-media-server/src/node_core_ctx')
@ -270,7 +272,7 @@ class LiveManager {
private async runMuxingSession (options: { private async runMuxingSession (options: {
sessionId: string sessionId: string
videoLive: MVideoLiveVideo videoLive: MVideoLiveVideoWithSetting
inputUrl: string inputUrl: string
fps: number fps: number
@ -470,15 +472,26 @@ class LiveManager {
return resolutionsEnabled return resolutionsEnabled
} }
private saveStartingSession (videoLive: MVideoLiveVideo) { private async saveStartingSession (videoLive: MVideoLiveVideoWithSetting) {
const liveSession = new VideoLiveSessionModel({ const replaySettings = videoLive.saveReplay
startDate: new Date(), ? new VideoLiveReplaySettingModel({
liveVideoId: videoLive.videoId, privacy: videoLive.ReplaySetting.privacy
saveReplay: videoLive.saveReplay, })
endingProcessed: false : 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) { private async saveEndingSession (videoId: number, error: LiveVideoError | null) {

View File

@ -17,7 +17,7 @@ import {
VideoState VideoState
} from '@shared/models' } from '@shared/models'
import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' 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 { cleanUpReqFiles } from '../../../helpers/express-utils'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { CONFIG } from '../../../initializers/config' import { CONFIG } from '../../../initializers/config'
@ -66,6 +66,11 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
.customSanitizer(toBooleanOrNull) .customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'), .custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'),
body('replaySettings.privacy')
.optional()
.customSanitizer(toIntOrNull)
.custom(isVideoPrivacyValid),
body('permanentLive') body('permanentLive')
.optional() .optional()
.customSanitizer(toBooleanOrNull) .customSanitizer(toBooleanOrNull)
@ -153,6 +158,11 @@ const videoLiveUpdateValidator = [
.customSanitizer(toBooleanOrNull) .customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'), .custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'),
body('replaySettings.privacy')
.optional()
.customSanitizer(toIntOrNull)
.custom(isVideoPrivacyValid),
body('latencyMode') body('latencyMode')
.optional() .optional()
.customSanitizer(toIntOrNull) .customSanitizer(toIntOrNull)
@ -177,6 +187,8 @@ const videoLiveUpdateValidator = [
}) })
} }
if (!checkLiveSettingsReplayConsistency({ res, body })) return
if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) { if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) {
return res.fail({ message: 'Cannot update a live that has already started' }) return res.fail({ message: 'Cannot update a live that has already started' })
} }
@ -272,3 +284,43 @@ function hasValidLatencyMode (body: LiveVideoUpdate | LiveVideoCreate) {
return true 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
}

View File

@ -160,6 +160,7 @@ export class VideoTableAttributes {
'permanentLive', 'permanentLive',
'latencyMode', 'latencyMode',
'videoId', 'videoId',
'replaySettingId',
'createdAt', 'createdAt',
'updatedAt' 'updatedAt'
] ]

View File

@ -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
}
}
}

View File

@ -1,10 +1,23 @@
import { FindOptions } from 'sequelize' 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 { MVideoLiveSession, MVideoLiveSessionReplay } from '@server/types/models'
import { uuidToShort } from '@shared/extra-utils' import { uuidToShort } from '@shared/extra-utils'
import { LiveVideoError, LiveVideoSession } from '@shared/models' import { LiveVideoError, LiveVideoSession } from '@shared/models'
import { AttributesOnly } from '@shared/typescript-utils' import { AttributesOnly } from '@shared/typescript-utils'
import { VideoModel } from './video' import { VideoModel } from './video'
import { VideoLiveReplaySettingModel } from './video-live-replay-setting'
export enum ScopeNames { export enum ScopeNames {
WITH_REPLAY = 'WITH_REPLAY' WITH_REPLAY = 'WITH_REPLAY'
@ -17,6 +30,10 @@ export enum ScopeNames {
model: VideoModel.unscoped(), model: VideoModel.unscoped(),
as: 'ReplayVideo', as: 'ReplayVideo',
required: false required: false
},
{
model: VideoLiveReplaySettingModel,
required: false
} }
] ]
} }
@ -30,6 +47,10 @@ export enum ScopeNames {
}, },
{ {
fields: [ 'liveVideoId' ] fields: [ 'liveVideoId' ]
},
{
fields: [ 'replaySettingId' ],
unique: true
} }
] ]
}) })
@ -89,6 +110,27 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv
}) })
LiveVideo: VideoModel 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> { static load (id: number): Promise<MVideoLiveSession> {
return VideoLiveSessionModel.findOne({ return VideoLiveSessionModel.findOne({
where: { id } where: { id }
@ -146,6 +188,10 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv
} }
: undefined : undefined
const replaySettings = this.replaySettingId
? this.ReplaySetting.toFormattedJSON()
: undefined
return { return {
id: this.id, id: this.id,
startDate: this.startDate.toISOString(), startDate: this.startDate.toISOString(),
@ -154,6 +200,7 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv
: null, : null,
endingProcessed: this.endingProcessed, endingProcessed: this.endingProcessed,
saveReplay: this.saveReplay, saveReplay: this.saveReplay,
replaySettings,
replayVideo, replayVideo,
error: this.error error: this.error
} }

View File

@ -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 { CONFIG } from '@server/initializers/config'
import { WEBSERVER } from '@server/initializers/constants' 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 { LiveVideo, LiveVideoLatencyMode, VideoState } from '@shared/models'
import { AttributesOnly } from '@shared/typescript-utils' import { AttributesOnly } from '@shared/typescript-utils'
import { VideoModel } from './video' import { VideoModel } from './video'
import { VideoBlacklistModel } from './video-blacklist' import { VideoBlacklistModel } from './video-blacklist'
import { VideoLiveReplaySettingModel } from './video-live-replay-setting'
@DefaultScope(() => ({ @DefaultScope(() => ({
include: [ include: [
@ -18,6 +31,10 @@ import { VideoBlacklistModel } from './video-blacklist'
required: false required: false
} }
] ]
},
{
model: VideoLiveReplaySettingModel,
required: false
} }
] ]
})) }))
@ -27,6 +44,10 @@ import { VideoBlacklistModel } from './video-blacklist'
{ {
fields: [ 'videoId' ], fields: [ 'videoId' ],
unique: true unique: true
},
{
fields: [ 'replaySettingId' ],
unique: true
} }
] ]
}) })
@ -66,6 +87,27 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>
}) })
Video: VideoModel 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) { static loadByStreamKey (streamKey: string) {
const query = { const query = {
where: { where: {
@ -84,11 +126,15 @@ export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>
required: false required: false
} }
] ]
},
{
model: VideoLiveReplaySettingModel.unscoped(),
required: false
} }
] ]
} }
return VideoLiveModel.findOne<MVideoLiveVideo>(query) return VideoLiveModel.findOne<MVideoLiveVideoWithSetting>(query)
} }
static loadByVideoId (videoId: number) { 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 { return {
...privateInformation, ...privateInformation,
permanentLive: this.permanentLive, permanentLive: this.permanentLive,
saveReplay: this.saveReplay, saveReplay: this.saveReplay,
replaySettings,
latencyMode: this.latencyMode latencyMode: this.latencyMode
} }
} }

View File

@ -706,6 +706,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
name: 'videoId', name: 'videoId',
allowNull: false allowNull: false
}, },
hooks: true,
onDelete: 'cascade' onDelete: 'cascade'
}) })
VideoLive: VideoLiveModel VideoLive: VideoLiveModel

View File

@ -83,6 +83,7 @@ describe('Test video lives API validator', function () {
privacy: VideoPrivacy.PUBLIC, privacy: VideoPrivacy.PUBLIC,
channelId, channelId,
saveReplay: false, saveReplay: false,
replaySettings: undefined,
permanentLive: false, permanentLive: false,
latencyMode: LiveVideoLatencyMode.DEFAULT latencyMode: LiveVideoLatencyMode.DEFAULT
} }
@ -141,6 +142,12 @@ describe('Test video lives API validator', function () {
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 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 () { it('Should fail with another user channel', async function () {
const user = { const user = {
username: 'fake', 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 () { 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({ await server.config.updateCustomSubConfig({
newConfig: { 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 () { 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({ await server.config.updateCustomSubConfig({
newConfig: { newConfig: {
@ -464,6 +471,39 @@ describe('Test video lives API validator', function () {
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) 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 () { it('Should fail to set latency if the server does not allow it', async function () {
const fields = { latencyMode: LiveVideoLatencyMode.HIGH_LATENCY } 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.id, fields: { saveReplay: false } })
await command.update({ videoId: video.uuid, 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.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 () { it('Should fail to update replay status if replay is not allowed on the instance', async function () {

View File

@ -24,10 +24,7 @@ describe('Test live constraints', function () {
let userAccessToken: string let userAccessToken: string
let userChannelId: number let userChannelId: number
async function createLiveWrapper (options: { async function createLiveWrapper (options: { replay: boolean, permanent: boolean }) {
replay: boolean
permanent: boolean
}) {
const { replay, permanent } = options const { replay, permanent } = options
const liveAttributes = { const liveAttributes = {
@ -35,6 +32,7 @@ describe('Test live constraints', function () {
channelId: userChannelId, channelId: userChannelId,
privacy: VideoPrivacy.PUBLIC, privacy: VideoPrivacy.PUBLIC,
saveReplay: replay, saveReplay: replay,
replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined,
permanentLive: permanent permanentLive: permanent
} }

View File

@ -23,6 +23,7 @@ describe('Fast restream in live', function () {
privacy: VideoPrivacy.PUBLIC, privacy: VideoPrivacy.PUBLIC,
name: 'my super live', name: 'my super live',
saveReplay: options.replay, saveReplay: options.replay,
replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined,
permanentLive: options.permanent permanentLive: options.permanent
} }

View File

@ -27,7 +27,7 @@ describe('Save replay setting', function () {
let liveVideoUUID: string let liveVideoUUID: string
let ffmpegCommand: FfmpegCommand let ffmpegCommand: FfmpegCommand
async function createLiveWrapper (options: { permanent: boolean, replay: boolean }) { async function createLiveWrapper (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacy } }) {
if (liveVideoUUID) { if (liveVideoUUID) {
try { try {
await servers[0].videos.remove({ id: liveVideoUUID }) await servers[0].videos.remove({ id: liveVideoUUID })
@ -40,6 +40,7 @@ describe('Save replay setting', function () {
privacy: VideoPrivacy.PUBLIC, privacy: VideoPrivacy.PUBLIC,
name: 'my super live', name: 'my super live',
saveReplay: options.replay, saveReplay: options.replay,
replaySettings: options.replaySettings,
permanentLive: options.permanent permanentLive: options.permanent
} }
@ -47,7 +48,7 @@ describe('Save replay setting', function () {
return uuid 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) liveVideoUUID = await createLiveWrapper(options)
const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
@ -61,7 +62,7 @@ describe('Save replay setting', function () {
return { ffmpegCommand, liveDetails } 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) const { ffmpegCommand, liveDetails } = await publishLive(options)
await Promise.all([ await Promise.all([
@ -76,7 +77,7 @@ describe('Save replay setting', function () {
return { liveDetails } 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) const { ffmpegCommand, liveDetails } = await publishLive(options)
await Promise.all([ 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 () { before(async function () {
this.timeout(120000) 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 () { it('Should correctly create and federate the "waiting for stream" live', async function () {
this.timeout(20000) 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 waitJobs(servers)
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) 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 () { 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 checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) 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 () { 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.endDate).to.not.exist
expect(session.endingProcessed).to.be.false expect(session.endingProcessed).to.be.false
expect(session.saveReplay).to.be.true expect(session.saveReplay).to.be.true
expect(session.replaySettings).to.exist
expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED)
await stopFfmpeg(ffmpegCommand) await stopFfmpeg(ffmpegCommand)
@ -281,8 +293,9 @@ describe('Save replay setting', function () {
await waitJobs(servers) await waitJobs(servers)
// Live has been transcoded // Live has been transcoded
await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.UNLISTED)
}) })
it('Should find the replay live session', async function () { 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.error).to.not.exist
expect(session.saveReplay).to.be.true expect(session.saveReplay).to.be.true
expect(session.endingProcessed).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).to.exist
expect(session.replayVideo.id).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 () { it('Should update the saved live and correctly federate the updated attributes', async function () {
this.timeout(30000) 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) await waitJobs(servers)
for (const server of servers) { for (const server of servers) {
const video = await server.videos.get({ id: liveVideoUUID }) const video = await server.videos.get({ id: liveVideoUUID })
expect(video.name).to.equal('video updated') expect(video.name).to.equal('video updated')
expect(video.isLive).to.be.false 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 () { it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () {
this.timeout(120000) this.timeout(120000)
await publishLiveAndBlacklist({ permanent: false, replay: true }) await publishLiveAndBlacklist({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } })
await checkVideosExist(liveVideoUUID, false) 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 () { it('Should correctly terminate the stream on delete and delete the video', async function () {
this.timeout(40000) 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 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 })
@ -348,103 +364,201 @@ describe('Save replay setting', function () {
describe('With save replay enabled on permanent live', function () { describe('With save replay enabled on permanent live', function () {
let lastReplayUUID: string let lastReplayUUID: string
it('Should correctly create and federate the "waiting for stream" live', async function () { describe('With a first live and its replay', function () {
this.timeout(20000)
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 waitJobs(servers)
await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
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 () { describe('With a second live and its replay', function () {
this.timeout(20000) 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 }) expect(live.saveReplay).to.be.true
await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) expect(live.replaySettings).to.exist
expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC)
await waitJobs(servers) })
await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) it('Should correctly have updated the live and federated it when streaming in the live', async function () {
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) this.timeout(20000)
})
it('Should correctly have saved the live and federated it after the streaming', async function () { ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
this.timeout(30000) 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) it('Should correctly have saved the live and federated it after the streaming', async function () {
await waitJobs(servers) this.timeout(30000)
const liveDetails = await servers[0].videos.get({ id: liveVideoUUID })
const video = await findExternalSavedVideo(servers[0], liveDetails) await stopFfmpeg(ffmpegCommand)
expect(video).to.exist
for (const server of servers) { await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID)
await server.videos.get({ id: video.uuid }) 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 () { for (const server of servers) {
const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID }) await server.videos.get({ id: video.uuid })
expect(total).to.equal(1) }
expect(data).to.have.lengthOf(1)
const sessionFromLive = data[0] lastReplayUUID = video.uuid
const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID }) })
for (const session of [ sessionFromLive, sessionFromReplay ]) { it('Should have appropriate ended session and replay live session', async function () {
expect(session.startDate).to.exist const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID })
expect(session.endDate).to.exist 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 for (const session of [ sessionFromLive, sessionFromReplay ]) {
expect(session.replayVideo.id).to.exist expect(session.startDate).to.exist
expect(session.replayVideo.shortUUID).to.exist expect(session.endDate).to.exist
expect(session.replayVideo.uuid).to.equal(lastReplayUUID)
}
})
it('Should have cleaned up the live files', async function () { expect(session.replaySettings).to.exist
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) expect(session.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC)
})
it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { expect(session.error).to.not.exist
this.timeout(120000)
await servers[0].videos.remove({ id: lastReplayUUID }) expect(session.replayVideo).to.exist
const { liveDetails } = await publishLiveAndBlacklist({ permanent: true, replay: true }) 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) it('Should have the first live replay with correct settings', async function () {
expect(replay).to.exist await checkVideosExist(lastReplayUUID, true, HttpStatusCode.OK_200)
await checkVideoState(lastReplayUUID, VideoState.PUBLISHED)
await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.PUBLIC)
})
for (const videoId of [ liveVideoUUID, replay.uuid ]) { it('Should have cleaned up the live files', async function () {
await checkVideosExist(videoId, false) await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
})
await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () {
await servers[1].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) 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 () { const replay = await findExternalSavedVideo(servers[0], liveDetails)
this.timeout(40000) 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) await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
expect(replay).to.not.exist 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 })
})
}) })
}) })

View File

@ -87,6 +87,7 @@ describe('Test live', function () {
commentsEnabled: false, commentsEnabled: false,
downloadEnabled: false, downloadEnabled: false,
saveReplay: true, saveReplay: true,
replaySettings: { privacy: VideoPrivacy.PUBLIC },
latencyMode: LiveVideoLatencyMode.SMALL_LATENCY, latencyMode: LiveVideoLatencyMode.SMALL_LATENCY,
privacy: VideoPrivacy.PUBLIC, privacy: VideoPrivacy.PUBLIC,
previewfile: 'video_short1-preview.webm.jpg', previewfile: 'video_short1-preview.webm.jpg',
@ -128,6 +129,9 @@ describe('Test live', function () {
if (server.url === servers[0].url) { if (server.url === servers[0].url) {
expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live') expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live')
expect(live.streamKey).to.not.be.empty expect(live.streamKey).to.not.be.empty
expect(live.replaySettings).to.exist
expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC)
} else { } else {
expect(live.rtmpUrl).to.not.exist expect(live.rtmpUrl).to.not.exist
expect(live.streamKey).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.saveReplay).to.be.false
expect(live.replaySettings).to.not.exist
expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT) expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT)
} }
}) })
@ -366,7 +371,10 @@ describe('Test live', function () {
name: 'live video', name: 'live video',
channelId: servers[0].store.channel.id, channelId: servers[0].store.channel.id,
privacy: VideoPrivacy.PUBLIC, privacy: VideoPrivacy.PUBLIC,
saveReplay saveReplay,
replaySettings: saveReplay
? { privacy: VideoPrivacy.PUBLIC }
: undefined
} }
const { uuid } = await commands[0].create({ fields: liveAttributes }) const { uuid } = await commands[0].create({ fields: liveAttributes })
@ -670,6 +678,9 @@ describe('Test live', function () {
channelId: servers[0].store.channel.id, channelId: servers[0].store.channel.id,
privacy: VideoPrivacy.PUBLIC, privacy: VideoPrivacy.PUBLIC,
saveReplay: options.saveReplay, saveReplay: options.saveReplay,
replaySettings: options.saveReplay
? { privacy: VideoPrivacy.PUBLIC }
: undefined,
permanentLive: options.permanent permanentLive: options.permanent
} }

View File

@ -342,6 +342,7 @@ describe('Test user notifications', function () {
privacy: VideoPrivacy.PUBLIC, privacy: VideoPrivacy.PUBLIC,
channelId: servers[1].store.channel.id, channelId: servers[1].store.channel.id,
saveReplay: true, saveReplay: true,
replaySettings: { privacy: VideoPrivacy.PUBLIC },
permanentLive: false permanentLive: false
} }
}) })
@ -367,6 +368,7 @@ describe('Test user notifications', function () {
privacy: VideoPrivacy.PUBLIC, privacy: VideoPrivacy.PUBLIC,
channelId: servers[1].store.channel.id, channelId: servers[1].store.channel.id,
saveReplay: true, saveReplay: true,
replaySettings: { privacy: VideoPrivacy.PUBLIC },
permanentLive: true permanentLive: true
} }
}) })

View File

@ -27,6 +27,7 @@ async function createLive (server: PeerTubeServer, permanent: boolean) {
privacy: VideoPrivacy.PUBLIC, privacy: VideoPrivacy.PUBLIC,
name: 'my super live', name: 'my super live',
saveReplay: true, saveReplay: true,
replaySettings: { privacy: VideoPrivacy.PUBLIC },
permanentLive: permanent permanentLive: permanent
} }

View File

@ -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 normalLiveId = video.uuid
normalLive = live 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 permanentLiveId = video.uuid
permanentLive = live permanentLive = live
} }

View File

@ -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 normalLiveId = video.uuid
normalLive = live 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 permanentLiveId = video.uuid
permanentLive = live permanentLive = live
} }

View File

@ -1,4 +1,5 @@
import { OutgoingHttpHeaders } from 'http' import { OutgoingHttpHeaders } from 'http'
import { Writable } from 'stream'
import { RegisterServerAuthExternalOptions } from '@server/types' import { RegisterServerAuthExternalOptions } from '@server/types'
import { import {
MAbuseMessage, MAbuseMessage,
@ -16,7 +17,7 @@ import {
MVideoFormattableDetails, MVideoFormattableDetails,
MVideoId, MVideoId,
MVideoImmutable, MVideoImmutable,
MVideoLive, MVideoLiveFormattable,
MVideoPlaylistFull, MVideoPlaylistFull,
MVideoPlaylistFullSummary MVideoPlaylistFullSummary
} from '@server/types/models' } from '@server/types/models'
@ -43,7 +44,6 @@ import {
MVideoShareActor, MVideoShareActor,
MVideoThumbnail MVideoThumbnail
} from './models' } from './models'
import { Writable } from 'stream'
import { MVideoSource } from './models/video/video-source' import { MVideoSource } from './models/video/video-source'
declare module 'express' { declare module 'express' {
@ -124,7 +124,7 @@ declare module 'express' {
onlyVideo?: MVideoThumbnail onlyVideo?: MVideoThumbnail
videoId?: MVideoId videoId?: MVideoId
videoLive?: MVideoLive videoLive?: MVideoLiveFormattable
videoLiveSession?: MVideoLiveSession videoLiveSession?: MVideoLiveSession
videoShare?: MVideoShareActor videoShare?: MVideoShareActor

View File

@ -13,6 +13,7 @@ export * from './video-channels'
export * from './video-comment' export * from './video-comment'
export * from './video-file' export * from './video-file'
export * from './video-import' export * from './video-import'
export * from './video-live-replay-setting'
export * from './video-live-session' export * from './video-live-session'
export * from './video-live' export * from './video-live'
export * from './video-playlist' export * from './video-playlist'

View File

@ -0,0 +1,3 @@
import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
export type MLiveReplaySetting = Omit<VideoLiveReplaySettingModel, 'VideoLive' | 'VideoLiveSession'>

View File

@ -1,15 +1,17 @@
import { VideoLiveSessionModel } from '@server/models/video/video-live-session' import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
import { PickWith } from '@shared/typescript-utils' import { PickWith } from '@shared/typescript-utils'
import { MVideo } from './video' import { MVideo } from './video'
import { MLiveReplaySetting } from './video-live-replay-setting'
type Use<K extends keyof VideoLiveSessionModel, M> = PickWith<VideoLiveSessionModel, K, M> 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 = export type MVideoLiveSessionReplay =
MVideoLiveSession & MVideoLiveSession &
Use<'ReplayVideo', MVideo> Use<'ReplayVideo', MVideo> &
Use<'ReplaySetting', MLiveReplaySetting>

View File

@ -1,15 +1,22 @@
import { VideoLiveModel } from '@server/models/video/video-live' import { VideoLiveModel } from '@server/models/video/video-live'
import { PickWith } from '@shared/typescript-utils' import { PickWith } from '@shared/typescript-utils'
import { MVideo } from './video' import { MVideo } from './video'
import { MLiveReplaySetting } from './video-live-replay-setting'
type Use<K extends keyof VideoLiveModel, M> = PickWith<VideoLiveModel, K, M> 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 = export type MVideoLiveVideo =
MVideoLive & MVideoLive &
Use<'Video', MVideo> Use<'Video', MVideo>
// ############################################################################
export type MVideoLiveVideoWithSetting =
MVideoLiveVideo &
Use<'ReplaySetting', MLiveReplaySetting>

View File

@ -1,4 +1,5 @@
import { VideoCreate } from '../video-create.model' import { VideoCreate } from '../video-create.model'
import { VideoPrivacy } from '../video-privacy.enum'
import { LiveVideoLatencyMode } from './live-video-latency-mode.enum' import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
export interface LiveVideoCreate extends VideoCreate { export interface LiveVideoCreate extends VideoCreate {
@ -6,4 +7,5 @@ export interface LiveVideoCreate extends VideoCreate {
latencyMode?: LiveVideoLatencyMode latencyMode?: LiveVideoLatencyMode
saveReplay?: boolean saveReplay?: boolean
replaySettings?: { privacy: VideoPrivacy }
} }

View File

@ -1,3 +1,4 @@
import { VideoPrivacy } from '../video-privacy.enum'
import { LiveVideoError } from './live-video-error.enum' import { LiveVideoError } from './live-video-error.enum'
export interface LiveVideoSession { export interface LiveVideoSession {
@ -11,6 +12,8 @@ export interface LiveVideoSession {
saveReplay: boolean saveReplay: boolean
endingProcessed: boolean endingProcessed: boolean
replaySettings?: { privacy: VideoPrivacy }
replayVideo: { replayVideo: {
id: number id: number
uuid: string uuid: string

View File

@ -1,7 +1,9 @@
import { VideoPrivacy } from '../video-privacy.enum'
import { LiveVideoLatencyMode } from './live-video-latency-mode.enum' import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
export interface LiveVideoUpdate { export interface LiveVideoUpdate {
permanentLive?: boolean permanentLive?: boolean
saveReplay?: boolean saveReplay?: boolean
replaySettings?: { privacy: VideoPrivacy }
latencyMode?: LiveVideoLatencyMode latencyMode?: LiveVideoLatencyMode
} }

View File

@ -1,3 +1,4 @@
import { VideoPrivacy } from '../video-privacy.enum'
import { LiveVideoLatencyMode } from './live-video-latency-mode.enum' import { LiveVideoLatencyMode } from './live-video-latency-mode.enum'
export interface LiveVideo { export interface LiveVideo {
@ -7,6 +8,7 @@ export interface LiveVideo {
streamKey?: string streamKey?: string
saveReplay: boolean saveReplay: boolean
replaySettings?: { privacy: VideoPrivacy }
permanentLive: boolean permanentLive: boolean
latencyMode: LiveVideoLatencyMode latencyMode: LiveVideoLatencyMode
} }

View File

@ -130,6 +130,7 @@ export class LiveCommand extends AbstractCommand {
name: 'live', name: 'live',
permanentLive, permanentLive,
saveReplay, saveReplay,
replaySettings: { privacy },
channelId: this.server.store.channel.id, channelId: this.server.store.channel.id,
privacy privacy
} }

View File

@ -2446,7 +2446,7 @@ paths:
/api/v1/videos/privacies: /api/v1/videos/privacies:
get: get:
summary: List available video privacy policies summary: List available video privacy policies
operationId: getPrivacyPolicies operationId: getVideoPrivacyPolicies
tags: tags:
- Video - Video
responses: responses:
@ -3087,6 +3087,8 @@ paths:
type: integer type: integer
saveReplay: saveReplay:
type: boolean type: boolean
replaySettings:
$ref: '#/components/schemas/LiveVideoReplaySettings'
permanentLive: permanentLive:
description: User can stream multiple times in a permanent live description: User can stream multiple times in a permanent live
type: boolean type: boolean
@ -6088,7 +6090,7 @@ components:
- 1 - 1
- 2 - 2
- 3 - 3
description: Video playlist privacy policy (see [/video-playlists/privacies]) description: Video playlist privacy policy (see [/video-playlists/privacies](#operation/getPlaylistPrivacyPolicies))
VideoPlaylistPrivacyConstant: VideoPlaylistPrivacyConstant:
properties: properties:
id: id:
@ -6116,7 +6118,7 @@ components:
- 2 - 2
- 3 - 3
- 4 - 4
description: privacy id of the video (see [/videos/privacies](#operation/getPrivacyPolicies)) description: privacy id of the video (see [/videos/privacies](#operation/getVideoPrivacyPolicies))
VideoPrivacyConstant: VideoPrivacyConstant:
properties: properties:
id: id:
@ -6177,6 +6179,14 @@ components:
- 2 - 2
- 3 - 3
description: 'The live latency mode (Default = `1`, High latency = `2`, Small Latency = `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: VideoStateConstant:
properties: properties:
@ -8693,6 +8703,8 @@ components:
properties: properties:
saveReplay: saveReplay:
type: boolean type: boolean
replaySettings:
$ref: '#/components/schemas/LiveVideoReplaySettings'
permanentLive: permanentLive:
description: User can stream multiple times in a permanent live description: User can stream multiple times in a permanent live
type: boolean 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 description: RTMP stream key to use to stream into this live video. Included in the response if an appropriate token is provided
saveReplay: saveReplay:
type: boolean type: boolean
replaySettings:
$ref: '#/components/schemas/LiveVideoReplaySettings'
permanentLive: permanentLive:
description: User can stream multiple times in a permanent live description: User can stream multiple times in a permanent live
type: boolean type: boolean