Clearer live session
Get the save replay setting when the session started to prevent inconsistent behaviour when the setting changed before the session was processed by the live ending job Display more information about the potential session replay in live modal information
This commit is contained in:
parent
a77c5ff362
commit
c8fa571f32
10 changed files with 134 additions and 36 deletions
|
@ -42,6 +42,7 @@
|
|||
<span i18n>Started on {{ session.startDate | date:'medium' }}</span>
|
||||
<span i18n *ngIf="session.endDate">Ended on {{ session.endDate | date:'medium' }}</span>
|
||||
<a i18n *ngIf="session.replayVideo" [routerLink]="getVideoUrl(session.replayVideo)" target="_blank">Go to replay</a>
|
||||
<span i18n *ngIf="isReplayBeingProcessed(session)">Replay is being processed...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -49,6 +49,13 @@ export class LiveStreamInformationComponent {
|
|||
return errors[session.error]
|
||||
}
|
||||
|
||||
isReplayBeingProcessed (session: LiveVideoSession) {
|
||||
// Running live
|
||||
if (!session.endDate) return false
|
||||
|
||||
return session.saveReplay && !session.endingProcessed
|
||||
}
|
||||
|
||||
private loadLiveInfo (video: Video) {
|
||||
this.liveVideoService.getVideoLive(video.id)
|
||||
.subscribe(live => this.live = live)
|
||||
|
|
|
@ -230,7 +230,7 @@ export class VideoActionsDropdownComponent implements OnChanges {
|
|||
|
||||
let message = $localize`Do you really want to delete ${this.video.name}?`
|
||||
if (this.video.isLive) {
|
||||
message += ' ' + $localize`The live stream will be automatically terminated.`
|
||||
message += ' ' + $localize`The live stream will be automatically terminated and replays won't be saved.`
|
||||
}
|
||||
|
||||
const res = await this.confirmService.confirm(message, $localize`Delete ${this.video.name}`)
|
||||
|
|
|
@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LAST_MIGRATION_VERSION = 715
|
||||
const LAST_MIGRATION_VERSION = 720
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import * as Sequelize from 'sequelize'
|
||||
|
||||
async function up (utils: {
|
||||
transaction: Sequelize.Transaction
|
||||
queryInterface: Sequelize.QueryInterface
|
||||
sequelize: Sequelize.Sequelize
|
||||
db: any
|
||||
}): Promise<void> {
|
||||
const { transaction } = utils
|
||||
|
||||
{
|
||||
const data = {
|
||||
type: Sequelize.BOOLEAN,
|
||||
defaultValue: null,
|
||||
allowNull: true
|
||||
}
|
||||
await utils.queryInterface.addColumn('videoLiveSession', 'endingProcessed', data, { transaction })
|
||||
await utils.queryInterface.addColumn('videoLiveSession', 'saveReplay', data, { transaction })
|
||||
}
|
||||
|
||||
{
|
||||
const query = `UPDATE "videoLiveSession" SET "saveReplay" = (
|
||||
SELECT "videoLive"."saveReplay" FROM "videoLive" WHERE "videoLive"."videoId" = "videoLiveSession"."liveVideoId"
|
||||
) WHERE "videoLiveSession"."liveVideoId" IS NOT NULL`
|
||||
await utils.sequelize.query(query, { transaction })
|
||||
}
|
||||
|
||||
{
|
||||
const query = `UPDATE "videoLiveSession" SET "saveReplay" = FALSE WHERE "saveReplay" IS NULL`
|
||||
await utils.sequelize.query(query, { transaction })
|
||||
}
|
||||
|
||||
{
|
||||
const query = `UPDATE "videoLiveSession" SET "endingProcessed" = TRUE`
|
||||
await utils.sequelize.query(query, { transaction })
|
||||
}
|
||||
|
||||
{
|
||||
const data = {
|
||||
type: Sequelize.BOOLEAN,
|
||||
defaultValue: null,
|
||||
allowNull: false
|
||||
}
|
||||
await utils.queryInterface.changeColumn('videoLiveSession', 'endingProcessed', data, { transaction })
|
||||
await utils.queryInterface.changeColumn('videoLiveSession', 'saveReplay', data, { transaction })
|
||||
}
|
||||
}
|
||||
|
||||
function down (options) {
|
||||
throw new Error('Not implemented.')
|
||||
}
|
||||
|
||||
export {
|
||||
up,
|
||||
down
|
||||
}
|
|
@ -30,26 +30,36 @@ async function processVideoLiveEnding (job: Job) {
|
|||
logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId, lTags())
|
||||
}
|
||||
|
||||
const liveVideo = await VideoModel.load(payload.videoId)
|
||||
const video = await VideoModel.load(payload.videoId)
|
||||
const live = await VideoLiveModel.loadByVideoId(payload.videoId)
|
||||
const liveSession = await VideoLiveSessionModel.load(payload.liveSessionId)
|
||||
|
||||
if (!liveVideo || !live || !liveSession) {
|
||||
const permanentLive = live.permanentLive
|
||||
|
||||
if (!video || !live || !liveSession) {
|
||||
logError()
|
||||
return
|
||||
}
|
||||
|
||||
if (live.saveReplay !== true) {
|
||||
return cleanupLiveAndFederate({ live, video: liveVideo, streamingPlaylistId: payload.streamingPlaylistId })
|
||||
liveSession.endingProcessed = true
|
||||
await liveSession.save()
|
||||
|
||||
if (liveSession.saveReplay !== true) {
|
||||
return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId })
|
||||
}
|
||||
|
||||
if (live.permanentLive) {
|
||||
await saveReplayToExternalVideo({ liveVideo, liveSession, publishedAt: payload.publishedAt, replayDirectory: payload.replayDirectory })
|
||||
if (permanentLive) {
|
||||
await saveReplayToExternalVideo({
|
||||
liveVideo: video,
|
||||
liveSession,
|
||||
publishedAt: payload.publishedAt,
|
||||
replayDirectory: payload.replayDirectory
|
||||
})
|
||||
|
||||
return cleanupLiveAndFederate({ live, video: liveVideo, streamingPlaylistId: payload.streamingPlaylistId })
|
||||
return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId })
|
||||
}
|
||||
|
||||
return replaceLiveByReplay({ liveVideo, live, liveSession, replayDirectory: payload.replayDirectory })
|
||||
return replaceLiveByReplay({ video, liveSession, live, permanentLive, replayDirectory: payload.replayDirectory })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -68,7 +78,7 @@ async function saveReplayToExternalVideo (options: {
|
|||
}) {
|
||||
const { liveVideo, liveSession, publishedAt, replayDirectory } = options
|
||||
|
||||
const video = new VideoModel({
|
||||
const replayVideo = new VideoModel({
|
||||
name: `${liveVideo.name} - ${new Date(publishedAt).toLocaleString()}`,
|
||||
isLive: false,
|
||||
state: VideoState.TO_TRANSCODE,
|
||||
|
@ -88,63 +98,64 @@ async function saveReplayToExternalVideo (options: {
|
|||
channelId: liveVideo.channelId
|
||||
}) as MVideoWithAllFiles
|
||||
|
||||
video.Thumbnails = []
|
||||
video.VideoFiles = []
|
||||
video.VideoStreamingPlaylists = []
|
||||
replayVideo.Thumbnails = []
|
||||
replayVideo.VideoFiles = []
|
||||
replayVideo.VideoStreamingPlaylists = []
|
||||
|
||||
video.url = getLocalVideoActivityPubUrl(video)
|
||||
replayVideo.url = getLocalVideoActivityPubUrl(replayVideo)
|
||||
|
||||
await video.save()
|
||||
await replayVideo.save()
|
||||
|
||||
liveSession.replayVideoId = video.id
|
||||
liveSession.replayVideoId = replayVideo.id
|
||||
await liveSession.save()
|
||||
|
||||
// If live is blacklisted, also blacklist the replay
|
||||
const blacklist = await VideoBlacklistModel.loadByVideoId(liveVideo.id)
|
||||
if (blacklist) {
|
||||
await VideoBlacklistModel.create({
|
||||
videoId: video.id,
|
||||
videoId: replayVideo.id,
|
||||
unfederated: blacklist.unfederated,
|
||||
reason: blacklist.reason,
|
||||
type: blacklist.type
|
||||
})
|
||||
}
|
||||
|
||||
await assignReplayFilesToVideo({ video, replayDirectory })
|
||||
await assignReplayFilesToVideo({ video: replayVideo, replayDirectory })
|
||||
|
||||
await remove(replayDirectory)
|
||||
|
||||
for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) {
|
||||
const image = await generateVideoMiniature({ video, videoFile: video.getMaxQualityFile(), type })
|
||||
await video.addAndSaveThumbnail(image)
|
||||
const image = await generateVideoMiniature({ video: replayVideo, videoFile: replayVideo.getMaxQualityFile(), type })
|
||||
await replayVideo.addAndSaveThumbnail(image)
|
||||
}
|
||||
|
||||
await moveToNextState({ video, isNewVideo: true })
|
||||
await moveToNextState({ video: replayVideo, isNewVideo: true })
|
||||
}
|
||||
|
||||
async function replaceLiveByReplay (options: {
|
||||
liveVideo: MVideo
|
||||
video: MVideo
|
||||
liveSession: MVideoLiveSession
|
||||
live: MVideoLive
|
||||
permanentLive: boolean
|
||||
replayDirectory: string
|
||||
}) {
|
||||
const { liveVideo, liveSession, live, replayDirectory } = options
|
||||
const { video, liveSession, live, permanentLive, replayDirectory } = options
|
||||
|
||||
await cleanupTMPLiveFiles(liveVideo)
|
||||
await cleanupTMPLiveFiles(video)
|
||||
|
||||
await live.destroy()
|
||||
|
||||
liveVideo.isLive = false
|
||||
liveVideo.waitTranscoding = true
|
||||
liveVideo.state = VideoState.TO_TRANSCODE
|
||||
video.isLive = false
|
||||
video.waitTranscoding = true
|
||||
video.state = VideoState.TO_TRANSCODE
|
||||
|
||||
await liveVideo.save()
|
||||
await video.save()
|
||||
|
||||
liveSession.replayVideoId = liveVideo.id
|
||||
liveSession.replayVideoId = video.id
|
||||
await liveSession.save()
|
||||
|
||||
// Remove old HLS playlist video files
|
||||
const videoWithFiles = await VideoModel.loadFull(liveVideo.id)
|
||||
const videoWithFiles = await VideoModel.loadFull(video.id)
|
||||
|
||||
const hlsPlaylist = videoWithFiles.getHLSPlaylist()
|
||||
await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id)
|
||||
|
@ -157,7 +168,7 @@ async function replaceLiveByReplay (options: {
|
|||
|
||||
await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory })
|
||||
|
||||
if (live.permanentLive) { // Remove session replay
|
||||
if (permanentLive) { // Remove session replay
|
||||
await remove(replayDirectory)
|
||||
} else { // We won't stream again in this live, we can delete the base replay directory
|
||||
await remove(getLiveReplayBaseDirectory(videoWithFiles))
|
||||
|
@ -224,16 +235,16 @@ async function assignReplayFilesToVideo (options: {
|
|||
}
|
||||
|
||||
async function cleanupLiveAndFederate (options: {
|
||||
live: MVideoLive
|
||||
video: MVideo
|
||||
permanentLive: boolean
|
||||
streamingPlaylistId: number
|
||||
}) {
|
||||
const { live, video, streamingPlaylistId } = options
|
||||
const { permanentLive, video, streamingPlaylistId } = options
|
||||
|
||||
const streamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(streamingPlaylistId)
|
||||
|
||||
if (streamingPlaylist) {
|
||||
if (live.permanentLive) {
|
||||
if (permanentLive) {
|
||||
await cleanupPermanentLive(video, streamingPlaylist)
|
||||
} else {
|
||||
await cleanupUnsavedNormalLive(video, streamingPlaylist)
|
||||
|
|
|
@ -475,7 +475,9 @@ class LiveManager {
|
|||
private saveStartingSession (videoLive: MVideoLiveVideo) {
|
||||
const liveSession = new VideoLiveSessionModel({
|
||||
startDate: new Date(),
|
||||
liveVideoId: videoLive.videoId
|
||||
liveVideoId: videoLive.videoId,
|
||||
saveReplay: videoLive.saveReplay,
|
||||
endingProcessed: false
|
||||
})
|
||||
|
||||
return liveSession.save()
|
||||
|
|
|
@ -53,6 +53,14 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv
|
|||
@Column
|
||||
error: LiveVideoError
|
||||
|
||||
@AllowNull(false)
|
||||
@Column
|
||||
saveReplay: boolean
|
||||
|
||||
@AllowNull(false)
|
||||
@Column
|
||||
endingProcessed: boolean
|
||||
|
||||
@ForeignKey(() => VideoModel)
|
||||
@Column
|
||||
replayVideoId: number
|
||||
|
@ -144,6 +152,8 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv
|
|||
endDate: this.endDate
|
||||
? this.endDate.toISOString()
|
||||
: null,
|
||||
endingProcessed: this.endingProcessed,
|
||||
saveReplay: this.saveReplay,
|
||||
replayVideo,
|
||||
error: this.error
|
||||
}
|
||||
|
|
|
@ -206,6 +206,7 @@ describe('Save replay setting', function () {
|
|||
expect(session.endDate).to.exist
|
||||
expect(new Date(session.endDate)).to.be.above(sessionEndDateMin)
|
||||
|
||||
expect(session.saveReplay).to.be.false
|
||||
expect(session.error).to.not.exist
|
||||
expect(session.replayVideo).to.not.exist
|
||||
})
|
||||
|
@ -272,6 +273,11 @@ describe('Save replay setting', function () {
|
|||
it('Should correctly have saved the live and federated it after the streaming', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
const session = await servers[0].live.findLatestSession({ videoId: liveVideoUUID })
|
||||
expect(session.endDate).to.not.exist
|
||||
expect(session.endingProcessed).to.be.false
|
||||
expect(session.saveReplay).to.be.true
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
|
||||
await waitUntilLiveReplacedByReplayOnAllServers(servers, liveVideoUUID)
|
||||
|
@ -291,6 +297,8 @@ describe('Save replay setting', function () {
|
|||
expect(session.endDate).to.exist
|
||||
|
||||
expect(session.error).to.not.exist
|
||||
expect(session.saveReplay).to.be.true
|
||||
expect(session.endingProcessed).to.be.true
|
||||
|
||||
expect(session.replayVideo).to.exist
|
||||
expect(session.replayVideo.id).to.exist
|
||||
|
|
|
@ -8,6 +8,9 @@ export interface LiveVideoSession {
|
|||
|
||||
error: LiveVideoError
|
||||
|
||||
saveReplay: boolean
|
||||
endingProcessed: boolean
|
||||
|
||||
replayVideo: {
|
||||
id: number
|
||||
uuid: string
|
||||
|
|
Loading…
Reference in a new issue