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

View File

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

View File

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

View File

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

View File

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

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 { 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,

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 { 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

View File

@ -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) {

View File

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

View File

@ -160,6 +160,7 @@ export class VideoTableAttributes {
'permanentLive',
'latencyMode',
'videoId',
'replaySettingId',
'createdAt',
'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 { 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
}

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

View File

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

View File

@ -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 () {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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 { 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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