diff --git a/client/src/app/shared/shared-video-live/live-stream-information.component.html b/client/src/app/shared/shared-video-live/live-stream-information.component.html index 99c7dbd4c..cf30c1ce1 100644 --- a/client/src/app/shared/shared-video-live/live-stream-information.component.html +++ b/client/src/app/shared/shared-video-live/live-stream-information.component.html @@ -42,6 +42,7 @@ Started on {{ session.startDate | date:'medium' }} Ended on {{ session.endDate | date:'medium' }} Go to replay + Replay is being processed... diff --git a/client/src/app/shared/shared-video-live/live-stream-information.component.ts b/client/src/app/shared/shared-video-live/live-stream-information.component.ts index c60f7fe2f..3dd59bb57 100644 --- a/client/src/app/shared/shared-video-live/live-stream-information.component.ts +++ b/client/src/app/shared/shared-video-live/live-stream-information.component.ts @@ -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) diff --git a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts index ed6a4afc0..56527ddfa 100644 --- a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts @@ -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}`) diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 8cb4d5f4a..e3f7ceb4a 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 715 +const LAST_MIGRATION_VERSION = 720 // --------------------------------------------------------------------------- diff --git a/server/initializers/migrations/0720-session-ending-processed.ts b/server/initializers/migrations/0720-session-ending-processed.ts new file mode 100644 index 000000000..74ffb39a0 --- /dev/null +++ b/server/initializers/migrations/0720-session-ending-processed.ts @@ -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 { + 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 +} diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 0e1bfb240..10507fb83 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts @@ -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) diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts index 649ad5195..41f89a2a4 100644 --- a/server/lib/live/live-manager.ts +++ b/server/lib/live/live-manager.ts @@ -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() diff --git a/server/models/video/video-live-session.ts b/server/models/video/video-live-session.ts index 758906a42..ed386052b 100644 --- a/server/models/video/video-live-session.ts +++ b/server/models/video/video-live-session.ts @@ -53,6 +53,14 @@ export class VideoLiveSessionModel extends Model VideoModel) @Column replayVideoId: number @@ -144,6 +152,8 @@ export class VideoLiveSessionModel extends Model