Feature/Add replay privacy (#5692)
* Add replay settings feature * Fix replay settings behaviour * Fix tests * Fix tests * Fix tests * Update openapi doc and fix tests * Add tests and fix code * Models correction * Add migration and update controller and middleware * Add check params tests * Fix video live middleware * Updated code based on review comments
This commit is contained in:
parent
ebd61437c1
commit
05a60d8599
|
@ -272,7 +272,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LAST_MIGRATION_VERSION = 755
|
const LAST_MIGRATION_VERSION = 760
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,7 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla
|
||||||
import { VideoTagModel } from '../models/video/video-tag'
|
import { 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,
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
|
|
||||||
|
async function up (utils: {
|
||||||
|
transaction: Sequelize.Transaction
|
||||||
|
queryInterface: Sequelize.QueryInterface
|
||||||
|
sequelize: Sequelize.Sequelize
|
||||||
|
}): Promise<void> {
|
||||||
|
{
|
||||||
|
const query = `
|
||||||
|
CREATE TABLE IF NOT EXISTS "videoLiveReplaySetting" (
|
||||||
|
"id" SERIAL ,
|
||||||
|
"privacy" INTEGER NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
await utils.sequelize.query(query, { transaction : utils.transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await utils.queryInterface.addColumn('videoLive', 'replaySettingId', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
defaultValue: null,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'videoLiveReplaySetting',
|
||||||
|
key: 'id'
|
||||||
|
},
|
||||||
|
onDelete: 'SET NULL'
|
||||||
|
}, { transaction: utils.transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await utils.queryInterface.addColumn('videoLiveSession', 'replaySettingId', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
defaultValue: null,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'videoLiveReplaySetting',
|
||||||
|
key: 'id'
|
||||||
|
},
|
||||||
|
onDelete: 'SET NULL'
|
||||||
|
}, { transaction: utils.transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const query = `
|
||||||
|
SELECT live."id", v."privacy"
|
||||||
|
FROM "videoLive" live
|
||||||
|
INNER JOIN "video" v ON live."videoId" = v."id"
|
||||||
|
WHERE live."saveReplay" = true
|
||||||
|
`
|
||||||
|
|
||||||
|
const videoLives = await utils.sequelize.query<{ id: number, privacy: number }>(
|
||||||
|
query,
|
||||||
|
{ type: Sequelize.QueryTypes.SELECT, transaction: utils.transaction }
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const videoLive of videoLives) {
|
||||||
|
const query = `
|
||||||
|
WITH new_replay_setting AS (
|
||||||
|
INSERT INTO "videoLiveReplaySetting" ("privacy", "createdAt", "updatedAt")
|
||||||
|
VALUES (:privacy, NOW(), NOW())
|
||||||
|
RETURNING id
|
||||||
|
)
|
||||||
|
UPDATE "videoLive" SET "replaySettingId" = (SELECT id FROM new_replay_setting)
|
||||||
|
WHERE "id" = :id
|
||||||
|
`
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
replacements: { privacy: videoLive.privacy, id: videoLive.id },
|
||||||
|
type: Sequelize.QueryTypes.UPDATE,
|
||||||
|
transaction: utils.transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.sequelize.query(query, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const query = `
|
||||||
|
SELECT session."id", v."privacy"
|
||||||
|
FROM "videoLiveSession" session
|
||||||
|
INNER JOIN "video" v ON session."liveVideoId" = v."id"
|
||||||
|
WHERE session."saveReplay" = true
|
||||||
|
AND session."liveVideoId" IS NOT NULL;
|
||||||
|
`
|
||||||
|
|
||||||
|
const videoLiveSessions = await utils.sequelize.query<{ id: number, privacy: number }>(
|
||||||
|
query,
|
||||||
|
{ type: Sequelize.QueryTypes.SELECT, transaction: utils.transaction }
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const videoLive of videoLiveSessions) {
|
||||||
|
const query = `
|
||||||
|
WITH new_replay_setting AS (
|
||||||
|
INSERT INTO "videoLiveReplaySetting" ("privacy", "createdAt", "updatedAt")
|
||||||
|
VALUES (:privacy, NOW(), NOW())
|
||||||
|
RETURNING id
|
||||||
|
)
|
||||||
|
UPDATE "videoLiveSession" SET "replaySettingId" = (SELECT id FROM new_replay_setting)
|
||||||
|
WHERE "id" = :id
|
||||||
|
`
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
replacements: { privacy: videoLive.privacy, id: videoLive.id },
|
||||||
|
type: Sequelize.QueryTypes.UPDATE,
|
||||||
|
transaction: utils.transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.sequelize.query(query, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function down (options) {
|
||||||
|
throw new Error('Not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
up,
|
||||||
|
down
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@serv
|
||||||
import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models'
|
import { 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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
? new VideoLiveReplaySettingModel({
|
||||||
|
privacy: videoLive.ReplaySetting.privacy
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
|
||||||
|
return sequelizeTypescript.transaction(async t => {
|
||||||
|
if (videoLive.saveReplay) {
|
||||||
|
await replaySettings.save({ transaction: t })
|
||||||
|
}
|
||||||
|
|
||||||
|
return VideoLiveSessionModel.create({
|
||||||
startDate: new Date(),
|
startDate: new Date(),
|
||||||
liveVideoId: videoLive.videoId,
|
liveVideoId: videoLive.videoId,
|
||||||
saveReplay: videoLive.saveReplay,
|
saveReplay: videoLive.saveReplay,
|
||||||
|
replaySettingId: videoLive.saveReplay ? replaySettings.id : null,
|
||||||
endingProcessed: false
|
endingProcessed: false
|
||||||
|
}, { transaction: t })
|
||||||
})
|
})
|
||||||
|
|
||||||
return liveSession.save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async saveEndingSession (videoId: number, error: LiveVideoError | null) {
|
private async saveEndingSession (videoId: number, error: LiveVideoError | null) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -160,6 +160,7 @@ export class VideoTableAttributes {
|
||||||
'permanentLive',
|
'permanentLive',
|
||||||
'latencyMode',
|
'latencyMode',
|
||||||
'videoId',
|
'videoId',
|
||||||
|
'replaySettingId',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt'
|
'updatedAt'
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { isVideoPrivacyValid } from '@server/helpers/custom-validators/videos'
|
||||||
|
import { MLiveReplaySetting } from '@server/types/models/video/video-live-replay-setting'
|
||||||
|
import { VideoPrivacy } from '@shared/models/videos/video-privacy.enum'
|
||||||
|
import { Transaction } from 'sequelize'
|
||||||
|
import { AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||||
|
import { throwIfNotValid } from '../shared/sequelize-helpers'
|
||||||
|
|
||||||
|
@Table({
|
||||||
|
tableName: 'videoLiveReplaySetting'
|
||||||
|
})
|
||||||
|
export class VideoLiveReplaySettingModel extends Model<VideoLiveReplaySettingModel> {
|
||||||
|
|
||||||
|
@CreatedAt
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@UpdatedAt
|
||||||
|
updatedAt: Date
|
||||||
|
|
||||||
|
@AllowNull(false)
|
||||||
|
@Is('VideoPrivacy', value => throwIfNotValid(value, isVideoPrivacyValid, 'privacy'))
|
||||||
|
@Column
|
||||||
|
privacy: VideoPrivacy
|
||||||
|
|
||||||
|
static load (id: number, transaction?: Transaction): Promise<MLiveReplaySetting> {
|
||||||
|
return VideoLiveReplaySettingModel.findOne({
|
||||||
|
where: { id },
|
||||||
|
transaction
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeSettings (id: number) {
|
||||||
|
return VideoLiveReplaySettingModel.destroy({
|
||||||
|
where: { id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toFormattedJSON () {
|
||||||
|
return {
|
||||||
|
privacy: this.privacy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,23 @@
|
||||||
import { FindOptions } from 'sequelize'
|
import { 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,15 +364,18 @@ 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
|
||||||
|
|
||||||
|
describe('With a first live and its replay', 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: true, replay: true })
|
liveVideoUUID = await createLiveWrapper({ permanent: true, 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 () {
|
||||||
|
@ -369,6 +388,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 () {
|
||||||
|
@ -403,6 +423,9 @@ describe('Save replay setting', function () {
|
||||||
expect(session.startDate).to.exist
|
expect(session.startDate).to.exist
|
||||||
expect(session.endDate).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.error).to.not.exist
|
||||||
|
|
||||||
expect(session.replayVideo).to.exist
|
expect(session.replayVideo).to.exist
|
||||||
|
@ -412,6 +435,88 @@ describe('Save replay setting', function () {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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 })
|
||||||
|
|
||||||
|
expect(live.saveReplay).to.be.true
|
||||||
|
expect(live.replaySettings).to.exist
|
||||||
|
expect(live.replaySettings.privacy).to.equal(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(2)
|
||||||
|
expect(data).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
const sessionFromLive = data[1]
|
||||||
|
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.PUBLIC)
|
||||||
|
|
||||||
|
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, true, HttpStatusCode.OK_200)
|
||||||
|
await checkVideoState(lastReplayUUID, VideoState.PUBLISHED)
|
||||||
|
await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.PUBLIC)
|
||||||
|
})
|
||||||
|
|
||||||
it('Should have cleaned up the live files', async function () {
|
it('Should have cleaned up the live files', async function () {
|
||||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||||
})
|
})
|
||||||
|
@ -420,7 +525,11 @@ describe('Save replay setting', function () {
|
||||||
this.timeout(120000)
|
this.timeout(120000)
|
||||||
|
|
||||||
await servers[0].videos.remove({ id: lastReplayUUID })
|
await servers[0].videos.remove({ id: lastReplayUUID })
|
||||||
const { liveDetails } = await publishLiveAndBlacklist({ permanent: true, replay: true })
|
const { liveDetails } = await publishLiveAndBlacklist({
|
||||||
|
permanent: true,
|
||||||
|
replay: true,
|
||||||
|
replaySettings: { privacy: VideoPrivacy.PUBLIC }
|
||||||
|
})
|
||||||
|
|
||||||
const replay = await findExternalSavedVideo(servers[0], liveDetails)
|
const replay = await findExternalSavedVideo(servers[0], liveDetails)
|
||||||
expect(replay).to.exist
|
expect(replay).to.exist
|
||||||
|
@ -438,7 +547,11 @@ describe('Save replay setting', function () {
|
||||||
it('Should correctly terminate the stream on delete and not save the video', async function () {
|
it('Should correctly terminate the stream on delete and not save the video', async function () {
|
||||||
this.timeout(40000)
|
this.timeout(40000)
|
||||||
|
|
||||||
const { liveDetails } = await publishLiveAndDelete({ permanent: true, replay: true })
|
const { liveDetails } = await publishLiveAndDelete({
|
||||||
|
permanent: true,
|
||||||
|
replay: true,
|
||||||
|
replaySettings: { privacy: VideoPrivacy.PUBLIC }
|
||||||
|
})
|
||||||
|
|
||||||
const replay = await findExternalSavedVideo(servers[0], liveDetails)
|
const replay = await findExternalSavedVideo(servers[0], liveDetails)
|
||||||
expect(replay).to.not.exist
|
expect(replay).to.not.exist
|
||||||
|
@ -447,6 +560,7 @@ describe('Save replay setting', function () {
|
||||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await cleanupTests(servers)
|
await cleanupTests(servers)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
|
||||||
|
|
||||||
|
export type MLiveReplaySetting = Omit<VideoLiveReplaySettingModel, 'VideoLive' | 'VideoLiveSession'>
|
|
@ -1,15 +1,17 @@
|
||||||
import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
|
import { 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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -6178,6 +6180,14 @@ components:
|
||||||
- 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:
|
||||||
id:
|
id:
|
||||||
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue