aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-07-22 15:22:21 +0200
committerChocobozzz <me@florianbigard.com>2022-07-22 15:22:21 +0200
commitc8fa571f32b10c083fab07f28d2ef55895ef40af (patch)
treefd50f90cc7643333984ed3b19f6a06f2e9f54feb
parenta77c5ff3622ab75d0c22241d0ef72053deaa7926 (diff)
downloadPeerTube-c8fa571f32b10c083fab07f28d2ef55895ef40af.tar.gz
PeerTube-c8fa571f32b10c083fab07f28d2ef55895ef40af.tar.zst
PeerTube-c8fa571f32b10c083fab07f28d2ef55895ef40af.zip
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
-rw-r--r--client/src/app/shared/shared-video-live/live-stream-information.component.html1
-rw-r--r--client/src/app/shared/shared-video-live/live-stream-information.component.ts7
-rw-r--r--client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts2
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/migrations/0720-session-ending-processed.ts56
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts77
-rw-r--r--server/lib/live/live-manager.ts4
-rw-r--r--server/models/video/video-live-session.ts10
-rw-r--r--server/tests/api/live/live-save-replay.ts8
-rw-r--r--shared/models/videos/live/live-video-session.model.ts3
10 files changed, 134 insertions, 36 deletions
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 @@
42 <span i18n>Started on {{ session.startDate | date:'medium' }}</span> 42 <span i18n>Started on {{ session.startDate | date:'medium' }}</span>
43 <span i18n *ngIf="session.endDate">Ended on {{ session.endDate | date:'medium' }}</span> 43 <span i18n *ngIf="session.endDate">Ended on {{ session.endDate | date:'medium' }}</span>
44 <a i18n *ngIf="session.replayVideo" [routerLink]="getVideoUrl(session.replayVideo)" target="_blank">Go to replay</a> 44 <a i18n *ngIf="session.replayVideo" [routerLink]="getVideoUrl(session.replayVideo)" target="_blank">Go to replay</a>
45 <span i18n *ngIf="isReplayBeingProcessed(session)">Replay is being processed...</span>
45 </div> 46 </div>
46 </div> 47 </div>
47 </div> 48 </div>
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 {
49 return errors[session.error] 49 return errors[session.error]
50 } 50 }
51 51
52 isReplayBeingProcessed (session: LiveVideoSession) {
53 // Running live
54 if (!session.endDate) return false
55
56 return session.saveReplay && !session.endingProcessed
57 }
58
52 private loadLiveInfo (video: Video) { 59 private loadLiveInfo (video: Video) {
53 this.liveVideoService.getVideoLive(video.id) 60 this.liveVideoService.getVideoLive(video.id)
54 .subscribe(live => this.live = live) 61 .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 {
230 230
231 let message = $localize`Do you really want to delete ${this.video.name}?` 231 let message = $localize`Do you really want to delete ${this.video.name}?`
232 if (this.video.isLive) { 232 if (this.video.isLive) {
233 message += ' ' + $localize`The live stream will be automatically terminated.` 233 message += ' ' + $localize`The live stream will be automatically terminated and replays won't be saved.`
234 } 234 }
235 235
236 const res = await this.confirmService.confirm(message, $localize`Delete ${this.video.name}`) 236 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'
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26 26
27const LAST_MIGRATION_VERSION = 715 27const LAST_MIGRATION_VERSION = 720
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
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 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 const { transaction } = utils
10
11 {
12 const data = {
13 type: Sequelize.BOOLEAN,
14 defaultValue: null,
15 allowNull: true
16 }
17 await utils.queryInterface.addColumn('videoLiveSession', 'endingProcessed', data, { transaction })
18 await utils.queryInterface.addColumn('videoLiveSession', 'saveReplay', data, { transaction })
19 }
20
21 {
22 const query = `UPDATE "videoLiveSession" SET "saveReplay" = (
23 SELECT "videoLive"."saveReplay" FROM "videoLive" WHERE "videoLive"."videoId" = "videoLiveSession"."liveVideoId"
24 ) WHERE "videoLiveSession"."liveVideoId" IS NOT NULL`
25 await utils.sequelize.query(query, { transaction })
26 }
27
28 {
29 const query = `UPDATE "videoLiveSession" SET "saveReplay" = FALSE WHERE "saveReplay" IS NULL`
30 await utils.sequelize.query(query, { transaction })
31 }
32
33 {
34 const query = `UPDATE "videoLiveSession" SET "endingProcessed" = TRUE`
35 await utils.sequelize.query(query, { transaction })
36 }
37
38 {
39 const data = {
40 type: Sequelize.BOOLEAN,
41 defaultValue: null,
42 allowNull: false
43 }
44 await utils.queryInterface.changeColumn('videoLiveSession', 'endingProcessed', data, { transaction })
45 await utils.queryInterface.changeColumn('videoLiveSession', 'saveReplay', data, { transaction })
46 }
47}
48
49function down (options) {
50 throw new Error('Not implemented.')
51}
52
53export {
54 up,
55 down
56}
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) {
30 logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId, lTags()) 30 logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId, lTags())
31 } 31 }
32 32
33 const liveVideo = await VideoModel.load(payload.videoId) 33 const video = await VideoModel.load(payload.videoId)
34 const live = await VideoLiveModel.loadByVideoId(payload.videoId) 34 const live = await VideoLiveModel.loadByVideoId(payload.videoId)
35 const liveSession = await VideoLiveSessionModel.load(payload.liveSessionId) 35 const liveSession = await VideoLiveSessionModel.load(payload.liveSessionId)
36 36
37 if (!liveVideo || !live || !liveSession) { 37 const permanentLive = live.permanentLive
38
39 if (!video || !live || !liveSession) {
38 logError() 40 logError()
39 return 41 return
40 } 42 }
41 43
42 if (live.saveReplay !== true) { 44 liveSession.endingProcessed = true
43 return cleanupLiveAndFederate({ live, video: liveVideo, streamingPlaylistId: payload.streamingPlaylistId }) 45 await liveSession.save()
46
47 if (liveSession.saveReplay !== true) {
48 return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId })
44 } 49 }
45 50
46 if (live.permanentLive) { 51 if (permanentLive) {
47 await saveReplayToExternalVideo({ liveVideo, liveSession, publishedAt: payload.publishedAt, replayDirectory: payload.replayDirectory }) 52 await saveReplayToExternalVideo({
53 liveVideo: video,
54 liveSession,
55 publishedAt: payload.publishedAt,
56 replayDirectory: payload.replayDirectory
57 })
48 58
49 return cleanupLiveAndFederate({ live, video: liveVideo, streamingPlaylistId: payload.streamingPlaylistId }) 59 return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId })
50 } 60 }
51 61
52 return replaceLiveByReplay({ liveVideo, live, liveSession, replayDirectory: payload.replayDirectory }) 62 return replaceLiveByReplay({ video, liveSession, live, permanentLive, replayDirectory: payload.replayDirectory })
53} 63}
54 64
55// --------------------------------------------------------------------------- 65// ---------------------------------------------------------------------------
@@ -68,7 +78,7 @@ async function saveReplayToExternalVideo (options: {
68}) { 78}) {
69 const { liveVideo, liveSession, publishedAt, replayDirectory } = options 79 const { liveVideo, liveSession, publishedAt, replayDirectory } = options
70 80
71 const video = new VideoModel({ 81 const replayVideo = new VideoModel({
72 name: `${liveVideo.name} - ${new Date(publishedAt).toLocaleString()}`, 82 name: `${liveVideo.name} - ${new Date(publishedAt).toLocaleString()}`,
73 isLive: false, 83 isLive: false,
74 state: VideoState.TO_TRANSCODE, 84 state: VideoState.TO_TRANSCODE,
@@ -88,63 +98,64 @@ async function saveReplayToExternalVideo (options: {
88 channelId: liveVideo.channelId 98 channelId: liveVideo.channelId
89 }) as MVideoWithAllFiles 99 }) as MVideoWithAllFiles
90 100
91 video.Thumbnails = [] 101 replayVideo.Thumbnails = []
92 video.VideoFiles = [] 102 replayVideo.VideoFiles = []
93 video.VideoStreamingPlaylists = [] 103 replayVideo.VideoStreamingPlaylists = []
94 104
95 video.url = getLocalVideoActivityPubUrl(video) 105 replayVideo.url = getLocalVideoActivityPubUrl(replayVideo)
96 106
97 await video.save() 107 await replayVideo.save()
98 108
99 liveSession.replayVideoId = video.id 109 liveSession.replayVideoId = replayVideo.id
100 await liveSession.save() 110 await liveSession.save()
101 111
102 // If live is blacklisted, also blacklist the replay 112 // If live is blacklisted, also blacklist the replay
103 const blacklist = await VideoBlacklistModel.loadByVideoId(liveVideo.id) 113 const blacklist = await VideoBlacklistModel.loadByVideoId(liveVideo.id)
104 if (blacklist) { 114 if (blacklist) {
105 await VideoBlacklistModel.create({ 115 await VideoBlacklistModel.create({
106 videoId: video.id, 116 videoId: replayVideo.id,
107 unfederated: blacklist.unfederated, 117 unfederated: blacklist.unfederated,
108 reason: blacklist.reason, 118 reason: blacklist.reason,
109 type: blacklist.type 119 type: blacklist.type
110 }) 120 })
111 } 121 }
112 122
113 await assignReplayFilesToVideo({ video, replayDirectory }) 123 await assignReplayFilesToVideo({ video: replayVideo, replayDirectory })
114 124
115 await remove(replayDirectory) 125 await remove(replayDirectory)
116 126
117 for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) { 127 for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) {
118 const image = await generateVideoMiniature({ video, videoFile: video.getMaxQualityFile(), type }) 128 const image = await generateVideoMiniature({ video: replayVideo, videoFile: replayVideo.getMaxQualityFile(), type })
119 await video.addAndSaveThumbnail(image) 129 await replayVideo.addAndSaveThumbnail(image)
120 } 130 }
121 131
122 await moveToNextState({ video, isNewVideo: true }) 132 await moveToNextState({ video: replayVideo, isNewVideo: true })
123} 133}
124 134
125async function replaceLiveByReplay (options: { 135async function replaceLiveByReplay (options: {
126 liveVideo: MVideo 136 video: MVideo
127 liveSession: MVideoLiveSession 137 liveSession: MVideoLiveSession
128 live: MVideoLive 138 live: MVideoLive
139 permanentLive: boolean
129 replayDirectory: string 140 replayDirectory: string
130}) { 141}) {
131 const { liveVideo, liveSession, live, replayDirectory } = options 142 const { video, liveSession, live, permanentLive, replayDirectory } = options
132 143
133 await cleanupTMPLiveFiles(liveVideo) 144 await cleanupTMPLiveFiles(video)
134 145
135 await live.destroy() 146 await live.destroy()
136 147
137 liveVideo.isLive = false 148 video.isLive = false
138 liveVideo.waitTranscoding = true 149 video.waitTranscoding = true
139 liveVideo.state = VideoState.TO_TRANSCODE 150 video.state = VideoState.TO_TRANSCODE
140 151
141 await liveVideo.save() 152 await video.save()
142 153
143 liveSession.replayVideoId = liveVideo.id 154 liveSession.replayVideoId = video.id
144 await liveSession.save() 155 await liveSession.save()
145 156
146 // Remove old HLS playlist video files 157 // Remove old HLS playlist video files
147 const videoWithFiles = await VideoModel.loadFull(liveVideo.id) 158 const videoWithFiles = await VideoModel.loadFull(video.id)
148 159
149 const hlsPlaylist = videoWithFiles.getHLSPlaylist() 160 const hlsPlaylist = videoWithFiles.getHLSPlaylist()
150 await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) 161 await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id)
@@ -157,7 +168,7 @@ async function replaceLiveByReplay (options: {
157 168
158 await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory }) 169 await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory })
159 170
160 if (live.permanentLive) { // Remove session replay 171 if (permanentLive) { // Remove session replay
161 await remove(replayDirectory) 172 await remove(replayDirectory)
162 } else { // We won't stream again in this live, we can delete the base replay directory 173 } else { // We won't stream again in this live, we can delete the base replay directory
163 await remove(getLiveReplayBaseDirectory(videoWithFiles)) 174 await remove(getLiveReplayBaseDirectory(videoWithFiles))
@@ -224,16 +235,16 @@ async function assignReplayFilesToVideo (options: {
224} 235}
225 236
226async function cleanupLiveAndFederate (options: { 237async function cleanupLiveAndFederate (options: {
227 live: MVideoLive
228 video: MVideo 238 video: MVideo
239 permanentLive: boolean
229 streamingPlaylistId: number 240 streamingPlaylistId: number
230}) { 241}) {
231 const { live, video, streamingPlaylistId } = options 242 const { permanentLive, video, streamingPlaylistId } = options
232 243
233 const streamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(streamingPlaylistId) 244 const streamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(streamingPlaylistId)
234 245
235 if (streamingPlaylist) { 246 if (streamingPlaylist) {
236 if (live.permanentLive) { 247 if (permanentLive) {
237 await cleanupPermanentLive(video, streamingPlaylist) 248 await cleanupPermanentLive(video, streamingPlaylist)
238 } else { 249 } else {
239 await cleanupUnsavedNormalLive(video, streamingPlaylist) 250 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 {
475 private saveStartingSession (videoLive: MVideoLiveVideo) { 475 private saveStartingSession (videoLive: MVideoLiveVideo) {
476 const liveSession = new VideoLiveSessionModel({ 476 const liveSession = new VideoLiveSessionModel({
477 startDate: new Date(), 477 startDate: new Date(),
478 liveVideoId: videoLive.videoId 478 liveVideoId: videoLive.videoId,
479 saveReplay: videoLive.saveReplay,
480 endingProcessed: false
479 }) 481 })
480 482
481 return liveSession.save() 483 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<Partial<AttributesOnly<VideoLiv
53 @Column 53 @Column
54 error: LiveVideoError 54 error: LiveVideoError
55 55
56 @AllowNull(false)
57 @Column
58 saveReplay: boolean
59
60 @AllowNull(false)
61 @Column
62 endingProcessed: boolean
63
56 @ForeignKey(() => VideoModel) 64 @ForeignKey(() => VideoModel)
57 @Column 65 @Column
58 replayVideoId: number 66 replayVideoId: number
@@ -144,6 +152,8 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv
144 endDate: this.endDate 152 endDate: this.endDate
145 ? this.endDate.toISOString() 153 ? this.endDate.toISOString()
146 : null, 154 : null,
155 endingProcessed: this.endingProcessed,
156 saveReplay: this.saveReplay,
147 replayVideo, 157 replayVideo,
148 error: this.error 158 error: this.error
149 } 159 }
diff --git a/server/tests/api/live/live-save-replay.ts b/server/tests/api/live/live-save-replay.ts
index 99ad3b2e1..b89aed85a 100644
--- a/server/tests/api/live/live-save-replay.ts
+++ b/server/tests/api/live/live-save-replay.ts
@@ -206,6 +206,7 @@ describe('Save replay setting', function () {
206 expect(session.endDate).to.exist 206 expect(session.endDate).to.exist
207 expect(new Date(session.endDate)).to.be.above(sessionEndDateMin) 207 expect(new Date(session.endDate)).to.be.above(sessionEndDateMin)
208 208
209 expect(session.saveReplay).to.be.false
209 expect(session.error).to.not.exist 210 expect(session.error).to.not.exist
210 expect(session.replayVideo).to.not.exist 211 expect(session.replayVideo).to.not.exist
211 }) 212 })
@@ -272,6 +273,11 @@ describe('Save replay setting', function () {
272 it('Should correctly have saved the live and federated it after the streaming', async function () { 273 it('Should correctly have saved the live and federated it after the streaming', async function () {
273 this.timeout(30000) 274 this.timeout(30000)
274 275
276 const session = await servers[0].live.findLatestSession({ videoId: liveVideoUUID })
277 expect(session.endDate).to.not.exist
278 expect(session.endingProcessed).to.be.false
279 expect(session.saveReplay).to.be.true
280
275 await stopFfmpeg(ffmpegCommand) 281 await stopFfmpeg(ffmpegCommand)
276 282
277 await waitUntilLiveReplacedByReplayOnAllServers(servers, liveVideoUUID) 283 await waitUntilLiveReplacedByReplayOnAllServers(servers, liveVideoUUID)
@@ -291,6 +297,8 @@ describe('Save replay setting', function () {
291 expect(session.endDate).to.exist 297 expect(session.endDate).to.exist
292 298
293 expect(session.error).to.not.exist 299 expect(session.error).to.not.exist
300 expect(session.saveReplay).to.be.true
301 expect(session.endingProcessed).to.be.true
294 302
295 expect(session.replayVideo).to.exist 303 expect(session.replayVideo).to.exist
296 expect(session.replayVideo.id).to.exist 304 expect(session.replayVideo.id).to.exist
diff --git a/shared/models/videos/live/live-video-session.model.ts b/shared/models/videos/live/live-video-session.model.ts
index 7ff6afbe5..2464e2570 100644
--- a/shared/models/videos/live/live-video-session.model.ts
+++ b/shared/models/videos/live/live-video-session.model.ts
@@ -8,6 +8,9 @@ export interface LiveVideoSession {
8 8
9 error: LiveVideoError 9 error: LiveVideoError
10 10
11 saveReplay: boolean
12 endingProcessed: boolean
13
11 replayVideo: { 14 replayVideo: {
12 id: number 15 id: number
13 uuid: string 16 uuid: string