]>
Commit | Line | Data |
---|---|---|
41fb13c3 | 1 | import { Job } from 'bull' |
5333788c | 2 | import { readdir, remove } from 'fs-extra' |
a5cf76af | 3 | import { join } from 'path' |
4ec52d04 C |
4 | import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamDuration } from '@server/helpers/ffmpeg' |
5 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | |
6 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | |
5333788c | 7 | import { cleanupNormalLive, cleanupPermanentLive, cleanupTMPLiveFiles, LiveSegmentShaStore } from '@server/lib/live' |
4ec52d04 C |
8 | import { |
9 | generateHLSMasterPlaylistFilename, | |
10 | generateHlsSha256SegmentsFilename, | |
11 | getLiveDirectory, | |
12 | getLiveReplayBaseDirectory | |
13 | } from '@server/lib/paths' | |
daf6e480 | 14 | import { generateVideoMiniature } from '@server/lib/thumbnail' |
c729caf6 | 15 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/transcoding' |
0305db28 | 16 | import { moveToNextState } from '@server/lib/video-state' |
a5cf76af | 17 | import { VideoModel } from '@server/models/video/video' |
26e3e98f | 18 | import { VideoBlacklistModel } from '@server/models/video/video-blacklist' |
68e70a74 | 19 | import { VideoFileModel } from '@server/models/video/video-file' |
b5b68755 | 20 | import { VideoLiveModel } from '@server/models/video/video-live' |
26e3e98f | 21 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' |
a5cf76af | 22 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
26e3e98f | 23 | import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@server/types/models' |
053aed43 | 24 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' |
a5cf76af C |
25 | import { logger } from '../../../helpers/logger' |
26 | ||
41fb13c3 | 27 | async function processVideoLiveEnding (job: Job) { |
a5cf76af C |
28 | const payload = job.data as VideoLiveEndingPayload |
29 | ||
4ec52d04 C |
30 | logger.info('Processing video live ending for %s.', payload.videoId, { payload }) |
31 | ||
68e70a74 C |
32 | function logError () { |
33 | logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId) | |
34 | } | |
35 | ||
26e3e98f | 36 | const liveVideo = await VideoModel.load(payload.videoId) |
b5b68755 | 37 | const live = await VideoLiveModel.loadByVideoId(payload.videoId) |
26e3e98f | 38 | const liveSession = await VideoLiveSessionModel.load(payload.liveSessionId) |
b5b68755 | 39 | |
26e3e98f | 40 | if (!liveVideo || !live || !liveSession) { |
68e70a74 C |
41 | logError() |
42 | return | |
43 | } | |
44 | ||
26e3e98f | 45 | LiveSegmentShaStore.Instance.cleanupShaSegments(liveVideo.uuid) |
bb4ba6d9 | 46 | |
b5b68755 | 47 | if (live.saveReplay !== true) { |
5333788c | 48 | return cleanupLiveAndFederate({ live, video: liveVideo }) |
b5b68755 C |
49 | } |
50 | ||
4ec52d04 | 51 | if (live.permanentLive) { |
26e3e98f | 52 | await saveReplayToExternalVideo({ liveVideo, liveSession, publishedAt: payload.publishedAt, replayDirectory: payload.replayDirectory }) |
4ec52d04 | 53 | |
5333788c | 54 | return cleanupLiveAndFederate({ live, video: liveVideo }) |
4ec52d04 C |
55 | } |
56 | ||
26e3e98f | 57 | return replaceLiveByReplay({ liveVideo, live, liveSession, replayDirectory: payload.replayDirectory }) |
b5b68755 C |
58 | } |
59 | ||
60 | // --------------------------------------------------------------------------- | |
61 | ||
62 | export { | |
8ebf2a5d | 63 | processVideoLiveEnding |
b5b68755 C |
64 | } |
65 | ||
66 | // --------------------------------------------------------------------------- | |
67 | ||
26e3e98f C |
68 | async function saveReplayToExternalVideo (options: { |
69 | liveVideo: MVideo | |
70 | liveSession: MVideoLiveSession | |
71 | publishedAt: string | |
72 | replayDirectory: string | |
73 | }) { | |
74 | const { liveVideo, liveSession, publishedAt, replayDirectory } = options | |
75 | ||
4ec52d04 C |
76 | await cleanupTMPLiveFiles(getLiveDirectory(liveVideo)) |
77 | ||
78 | const video = new VideoModel({ | |
79 | name: `${liveVideo.name} - ${new Date(publishedAt).toLocaleString()}`, | |
80 | isLive: false, | |
81 | state: VideoState.TO_TRANSCODE, | |
82 | duration: 0, | |
83 | ||
84 | remote: liveVideo.remote, | |
85 | category: liveVideo.category, | |
86 | licence: liveVideo.licence, | |
87 | language: liveVideo.language, | |
88 | commentsEnabled: liveVideo.commentsEnabled, | |
89 | downloadEnabled: liveVideo.downloadEnabled, | |
26e3e98f | 90 | waitTranscoding: true, |
4ec52d04 C |
91 | nsfw: liveVideo.nsfw, |
92 | description: liveVideo.description, | |
93 | support: liveVideo.support, | |
94 | privacy: liveVideo.privacy, | |
95 | channelId: liveVideo.channelId | |
96 | }) as MVideoWithAllFiles | |
97 | ||
98 | video.Thumbnails = [] | |
99 | video.VideoFiles = [] | |
100 | video.VideoStreamingPlaylists = [] | |
101 | ||
102 | video.url = getLocalVideoActivityPubUrl(video) | |
103 | ||
104 | await video.save() | |
105 | ||
26e3e98f C |
106 | liveSession.replayVideoId = video.id |
107 | await liveSession.save() | |
108 | ||
4ec52d04 C |
109 | // If live is blacklisted, also blacklist the replay |
110 | const blacklist = await VideoBlacklistModel.loadByVideoId(liveVideo.id) | |
111 | if (blacklist) { | |
112 | await VideoBlacklistModel.create({ | |
113 | videoId: video.id, | |
114 | unfederated: blacklist.unfederated, | |
115 | reason: blacklist.reason, | |
116 | type: blacklist.type | |
117 | }) | |
118 | } | |
119 | ||
26e3e98f | 120 | await assignReplayFilesToVideo({ video, replayDirectory }) |
937581b8 | 121 | |
4ec52d04 C |
122 | await remove(replayDirectory) |
123 | ||
124 | for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) { | |
125 | const image = await generateVideoMiniature({ video, videoFile: video.getMaxQualityFile(), type }) | |
126 | await video.addAndSaveThumbnail(image) | |
127 | } | |
937581b8 | 128 | |
4ec52d04 C |
129 | await moveToNextState({ video, isNewVideo: true }) |
130 | } | |
937581b8 | 131 | |
26e3e98f C |
132 | async function replaceLiveByReplay (options: { |
133 | liveVideo: MVideo | |
134 | liveSession: MVideoLiveSession | |
135 | live: MVideoLive | |
136 | replayDirectory: string | |
137 | }) { | |
138 | const { liveVideo, liveSession, live, replayDirectory } = options | |
139 | ||
140 | await cleanupTMPLiveFiles(getLiveDirectory(liveVideo)) | |
b5b68755 | 141 | |
31c82cd9 C |
142 | await live.destroy() |
143 | ||
26e3e98f C |
144 | liveVideo.isLive = false |
145 | liveVideo.waitTranscoding = true | |
146 | liveVideo.state = VideoState.TO_TRANSCODE | |
d846d99c | 147 | |
26e3e98f C |
148 | await liveVideo.save() |
149 | ||
150 | liveSession.replayVideoId = liveVideo.id | |
151 | await liveSession.save() | |
b5b68755 | 152 | |
97969c4e | 153 | // Remove old HLS playlist video files |
26e3e98f | 154 | const videoWithFiles = await VideoModel.loadAndPopulateAccountAndServerAndTags(liveVideo.id) |
b5b68755 | 155 | |
97969c4e C |
156 | const hlsPlaylist = videoWithFiles.getHLSPlaylist() |
157 | await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) | |
764b1a14 C |
158 | |
159 | // Reset playlist | |
97969c4e | 160 | hlsPlaylist.VideoFiles = [] |
764b1a14 C |
161 | hlsPlaylist.playlistFilename = generateHLSMasterPlaylistFilename() |
162 | hlsPlaylist.segmentsSha256Filename = generateHlsSha256SegmentsFilename() | |
163 | await hlsPlaylist.save() | |
97969c4e | 164 | |
26e3e98f | 165 | await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory }) |
4ec52d04 | 166 | |
5333788c C |
167 | if (live.permanentLive) { // Remove session replay |
168 | await remove(replayDirectory) | |
169 | } else { // We won't stream again in this live, we can delete the base replay directory | |
170 | await remove(getLiveReplayBaseDirectory(videoWithFiles)) | |
171 | } | |
4ec52d04 C |
172 | |
173 | // Regenerate the thumbnail & preview? | |
174 | if (videoWithFiles.getMiniature().automaticallyGenerated === true) { | |
175 | const miniature = await generateVideoMiniature({ | |
176 | video: videoWithFiles, | |
177 | videoFile: videoWithFiles.getMaxQualityFile(), | |
178 | type: ThumbnailType.MINIATURE | |
179 | }) | |
26e3e98f | 180 | await videoWithFiles.addAndSaveThumbnail(miniature) |
4ec52d04 C |
181 | } |
182 | ||
183 | if (videoWithFiles.getPreview().automaticallyGenerated === true) { | |
184 | const preview = await generateVideoMiniature({ | |
185 | video: videoWithFiles, | |
186 | videoFile: videoWithFiles.getMaxQualityFile(), | |
187 | type: ThumbnailType.PREVIEW | |
188 | }) | |
26e3e98f | 189 | await videoWithFiles.addAndSaveThumbnail(preview) |
4ec52d04 C |
190 | } |
191 | ||
26e3e98f C |
192 | // We consider this is a new video |
193 | await moveToNextState({ video: videoWithFiles, isNewVideo: true }) | |
4ec52d04 C |
194 | } |
195 | ||
26e3e98f C |
196 | async function assignReplayFilesToVideo (options: { |
197 | video: MVideo | |
198 | replayDirectory: string | |
199 | }) { | |
200 | const { video, replayDirectory } = options | |
201 | ||
a800dbf3 | 202 | let durationDone = false |
b5b68755 | 203 | |
4ec52d04 C |
204 | const concatenatedTsFiles = await readdir(replayDirectory) |
205 | ||
206 | for (const concatenatedTsFile of concatenatedTsFiles) { | |
3851e732 | 207 | const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile) |
2650d6d4 | 208 | |
e772bdf1 C |
209 | const probe = await ffprobePromise(concatenatedTsFilePath) |
210 | const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) | |
211 | ||
c729caf6 | 212 | const { resolution, isPortraitMode } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe) |
2650d6d4 | 213 | |
0305db28 | 214 | const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({ |
4ec52d04 | 215 | video, |
3851e732 | 216 | concatenatedTsFilePath, |
679c12e6 | 217 | resolution, |
e772bdf1 C |
218 | isPortraitMode, |
219 | isAAC: audioStream?.codec_name === 'aac' | |
b5b68755 | 220 | }) |
b5b68755 | 221 | |
4a54a939 | 222 | if (!durationDone) { |
4ec52d04 C |
223 | video.duration = await getVideoStreamDuration(outputPath) |
224 | await video.save() | |
4a54a939 C |
225 | |
226 | durationDone = true | |
2650d6d4 | 227 | } |
97969c4e | 228 | } |
d846d99c | 229 | |
4ec52d04 C |
230 | return video |
231 | } | |
053aed43 | 232 | |
26e3e98f | 233 | async function cleanupLiveAndFederate (options: { |
5333788c C |
234 | live: MVideoLive |
235 | video: MVideo | |
26e3e98f | 236 | }) { |
5333788c | 237 | const { live, video } = options |
bb4ba6d9 | 238 | |
5333788c | 239 | const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) |
a5cf76af | 240 | |
5333788c C |
241 | if (live.permanentLive) { |
242 | await cleanupPermanentLive(video, streamingPlaylist) | |
243 | } else { | |
244 | await cleanupNormalLive(video, streamingPlaylist) | |
a5cf76af | 245 | } |
5333788c | 246 | |
c8fdfab0 C |
247 | try { |
248 | const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id) | |
249 | return federateVideoIfNeeded(fullVideo, false, undefined) | |
250 | } catch (err) { | |
251 | logger.warn('Cannot federate live after cleanup', { videoId: video.id, err }) | |
252 | } | |
a5cf76af | 253 | } |