aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-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