]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Clearer live session
authorChocobozzz <me@florianbigard.com>
Fri, 22 Jul 2022 13:22:21 +0000 (15:22 +0200)
committerChocobozzz <me@florianbigard.com>
Fri, 22 Jul 2022 13:22:21 +0000 (15:22 +0200)
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

client/src/app/shared/shared-video-live/live-stream-information.component.html
client/src/app/shared/shared-video-live/live-stream-information.component.ts
client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts
server/initializers/constants.ts
server/initializers/migrations/0720-session-ending-processed.ts [new file with mode: 0644]
server/lib/job-queue/handlers/video-live-ending.ts
server/lib/live/live-manager.ts
server/models/video/video-live-session.ts
server/tests/api/live/live-save-replay.ts
shared/models/videos/live/live-video-session.model.ts

index 99c7dbd4c792d3d79650cadfd11bbad652519395..cf30c1ce1bd98aa3e1c8cc8abedefb49c6466f30 100644 (file)
@@ -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>
index c60f7fe2f2e7de6279160dad4a8d75e2c9231162..3dd59bb572933badb6dbae7a58f578ca14b2b66a 100644 (file)
@@ -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)
index ed6a4afc0f90c3980c0531f2fe223c91a33e38ef..56527ddfaf84baf8383c175207d12d727f0630bb 100644 (file)
@@ -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}`)
index 8cb4d5f4a4c5ee7a842babce0854f3f5beac5852..e3f7ceb4a07f7a2c2917992717c02ca2787df8e7 100644 (file)
@@ -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 (file)
index 0000000..74ffb39
--- /dev/null
@@ -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
+}
index 0e1bfb240fe835c787a01c8d60c2a28c8158d97d..10507fb837fe8af85e3bf777b97fe00ef1ea68b7 100644 (file)
@@ -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)
index 649ad5195eb3d27a1b75b4e1b87ab9cd8c992048..41f89a2a494ee4efefd103b7ca9be1402e8dcbe6 100644 (file)
@@ -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()
index 758906a42ae9cf375c5377fb36bf858a547ea9d9..ed386052bc37fd12877053a4b881f3e59f37354f 100644 (file)
@@ -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
     }
index 99ad3b2e1a76c216e70d5d2b57c2a553e7e5289e..b89aed85a60bf11c471e7b5f98d29c581918f3f5 100644 (file)
@@ -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
index 7ff6afbe51bd1d8260419ce7c6ca1ed6abae4618..2464e25703d8bcf3dc60176c40ac9d6981af8586 100644 (file)
@@ -8,6 +8,9 @@ export interface LiveVideoSession {
 
   error: LiveVideoError
 
+  saveReplay: boolean
+  endingProcessed: boolean
+
   replayVideo: {
     id: number
     uuid: string