diff options
Diffstat (limited to 'server/lib/job-queue/handlers/video-live-ending.ts')
-rw-r--r-- | server/lib/job-queue/handlers/video-live-ending.ts | 279 |
1 files changed, 0 insertions, 279 deletions
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts deleted file mode 100644 index 070d1d7a2..000000000 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ /dev/null | |||
@@ -1,279 +0,0 @@ | |||
1 | import { Job } from 'bullmq' | ||
2 | import { readdir, remove } from 'fs-extra' | ||
3 | import { join } from 'path' | ||
4 | import { peertubeTruncate } from '@server/helpers/core-utils' | ||
5 | import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' | ||
6 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | ||
7 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | ||
8 | import { cleanupAndDestroyPermanentLive, cleanupTMPLiveFiles, cleanupUnsavedNormalLive } from '@server/lib/live' | ||
9 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '@server/lib/paths' | ||
10 | import { generateLocalVideoMiniature, regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail' | ||
11 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/hls-transcoding' | ||
12 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
13 | import { moveToNextState } from '@server/lib/video-state' | ||
14 | import { VideoModel } from '@server/models/video/video' | ||
15 | import { VideoBlacklistModel } from '@server/models/video/video-blacklist' | ||
16 | import { VideoFileModel } from '@server/models/video/video-file' | ||
17 | import { VideoLiveModel } from '@server/models/video/video-live' | ||
18 | import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' | ||
19 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' | ||
20 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | ||
21 | import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@server/types/models' | ||
22 | import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@shared/ffmpeg' | ||
23 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' | ||
24 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | ||
25 | import { JobQueue } from '../job-queue' | ||
26 | |||
27 | const lTags = loggerTagsFactory('live', 'job') | ||
28 | |||
29 | async function processVideoLiveEnding (job: Job) { | ||
30 | const payload = job.data as VideoLiveEndingPayload | ||
31 | |||
32 | logger.info('Processing video live ending for %s.', payload.videoId, { payload, ...lTags() }) | ||
33 | |||
34 | function logError () { | ||
35 | logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId, lTags()) | ||
36 | } | ||
37 | |||
38 | const video = await VideoModel.load(payload.videoId) | ||
39 | const live = await VideoLiveModel.loadByVideoId(payload.videoId) | ||
40 | const liveSession = await VideoLiveSessionModel.load(payload.liveSessionId) | ||
41 | |||
42 | if (!video || !live || !liveSession) { | ||
43 | logError() | ||
44 | return | ||
45 | } | ||
46 | |||
47 | const permanentLive = live.permanentLive | ||
48 | |||
49 | liveSession.endingProcessed = true | ||
50 | await liveSession.save() | ||
51 | |||
52 | if (liveSession.saveReplay !== true) { | ||
53 | return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId }) | ||
54 | } | ||
55 | |||
56 | if (permanentLive) { | ||
57 | await saveReplayToExternalVideo({ | ||
58 | liveVideo: video, | ||
59 | liveSession, | ||
60 | publishedAt: payload.publishedAt, | ||
61 | replayDirectory: payload.replayDirectory | ||
62 | }) | ||
63 | |||
64 | return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId }) | ||
65 | } | ||
66 | |||
67 | return replaceLiveByReplay({ | ||
68 | video, | ||
69 | liveSession, | ||
70 | live, | ||
71 | permanentLive, | ||
72 | replayDirectory: payload.replayDirectory | ||
73 | }) | ||
74 | } | ||
75 | |||
76 | // --------------------------------------------------------------------------- | ||
77 | |||
78 | export { | ||
79 | processVideoLiveEnding | ||
80 | } | ||
81 | |||
82 | // --------------------------------------------------------------------------- | ||
83 | |||
84 | async function saveReplayToExternalVideo (options: { | ||
85 | liveVideo: MVideo | ||
86 | liveSession: MVideoLiveSession | ||
87 | publishedAt: string | ||
88 | replayDirectory: string | ||
89 | }) { | ||
90 | const { liveVideo, liveSession, publishedAt, replayDirectory } = options | ||
91 | |||
92 | const replaySettings = await VideoLiveReplaySettingModel.load(liveSession.replaySettingId) | ||
93 | |||
94 | const videoNameSuffix = ` - ${new Date(publishedAt).toLocaleString()}` | ||
95 | const truncatedVideoName = peertubeTruncate(liveVideo.name, { | ||
96 | length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max - videoNameSuffix.length | ||
97 | }) | ||
98 | |||
99 | const replayVideo = new VideoModel({ | ||
100 | name: truncatedVideoName + videoNameSuffix, | ||
101 | isLive: false, | ||
102 | state: VideoState.TO_TRANSCODE, | ||
103 | duration: 0, | ||
104 | |||
105 | remote: liveVideo.remote, | ||
106 | category: liveVideo.category, | ||
107 | licence: liveVideo.licence, | ||
108 | language: liveVideo.language, | ||
109 | commentsEnabled: liveVideo.commentsEnabled, | ||
110 | downloadEnabled: liveVideo.downloadEnabled, | ||
111 | waitTranscoding: true, | ||
112 | nsfw: liveVideo.nsfw, | ||
113 | description: liveVideo.description, | ||
114 | support: liveVideo.support, | ||
115 | privacy: replaySettings.privacy, | ||
116 | channelId: liveVideo.channelId | ||
117 | }) as MVideoWithAllFiles | ||
118 | |||
119 | replayVideo.Thumbnails = [] | ||
120 | replayVideo.VideoFiles = [] | ||
121 | replayVideo.VideoStreamingPlaylists = [] | ||
122 | |||
123 | replayVideo.url = getLocalVideoActivityPubUrl(replayVideo) | ||
124 | |||
125 | await replayVideo.save() | ||
126 | |||
127 | liveSession.replayVideoId = replayVideo.id | ||
128 | await liveSession.save() | ||
129 | |||
130 | // If live is blacklisted, also blacklist the replay | ||
131 | const blacklist = await VideoBlacklistModel.loadByVideoId(liveVideo.id) | ||
132 | if (blacklist) { | ||
133 | await VideoBlacklistModel.create({ | ||
134 | videoId: replayVideo.id, | ||
135 | unfederated: blacklist.unfederated, | ||
136 | reason: blacklist.reason, | ||
137 | type: blacklist.type | ||
138 | }) | ||
139 | } | ||
140 | |||
141 | await assignReplayFilesToVideo({ video: replayVideo, replayDirectory }) | ||
142 | |||
143 | await remove(replayDirectory) | ||
144 | |||
145 | for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) { | ||
146 | const image = await generateLocalVideoMiniature({ video: replayVideo, videoFile: replayVideo.getMaxQualityFile(), type }) | ||
147 | await replayVideo.addAndSaveThumbnail(image) | ||
148 | } | ||
149 | |||
150 | await moveToNextState({ video: replayVideo, isNewVideo: true }) | ||
151 | |||
152 | await createStoryboardJob(replayVideo) | ||
153 | } | ||
154 | |||
155 | async function replaceLiveByReplay (options: { | ||
156 | video: MVideo | ||
157 | liveSession: MVideoLiveSession | ||
158 | live: MVideoLive | ||
159 | permanentLive: boolean | ||
160 | replayDirectory: string | ||
161 | }) { | ||
162 | const { video, liveSession, live, permanentLive, replayDirectory } = options | ||
163 | |||
164 | const replaySettings = await VideoLiveReplaySettingModel.load(liveSession.replaySettingId) | ||
165 | const videoWithFiles = await VideoModel.loadFull(video.id) | ||
166 | const hlsPlaylist = videoWithFiles.getHLSPlaylist() | ||
167 | |||
168 | await cleanupTMPLiveFiles(videoWithFiles, hlsPlaylist) | ||
169 | |||
170 | await live.destroy() | ||
171 | |||
172 | videoWithFiles.isLive = false | ||
173 | videoWithFiles.privacy = replaySettings.privacy | ||
174 | videoWithFiles.waitTranscoding = true | ||
175 | videoWithFiles.state = VideoState.TO_TRANSCODE | ||
176 | |||
177 | await videoWithFiles.save() | ||
178 | |||
179 | liveSession.replayVideoId = videoWithFiles.id | ||
180 | await liveSession.save() | ||
181 | |||
182 | await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) | ||
183 | |||
184 | // Reset playlist | ||
185 | hlsPlaylist.VideoFiles = [] | ||
186 | hlsPlaylist.playlistFilename = generateHLSMasterPlaylistFilename() | ||
187 | hlsPlaylist.segmentsSha256Filename = generateHlsSha256SegmentsFilename() | ||
188 | await hlsPlaylist.save() | ||
189 | |||
190 | await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory }) | ||
191 | |||
192 | // Should not happen in this function, but we keep the code if in the future we can replace the permanent live by a replay | ||
193 | if (permanentLive) { // Remove session replay | ||
194 | await remove(replayDirectory) | ||
195 | } else { // We won't stream again in this live, we can delete the base replay directory | ||
196 | await remove(getLiveReplayBaseDirectory(videoWithFiles)) | ||
197 | } | ||
198 | |||
199 | // Regenerate the thumbnail & preview? | ||
200 | await regenerateMiniaturesIfNeeded(videoWithFiles) | ||
201 | |||
202 | // We consider this is a new video | ||
203 | await moveToNextState({ video: videoWithFiles, isNewVideo: true }) | ||
204 | |||
205 | await createStoryboardJob(videoWithFiles) | ||
206 | } | ||
207 | |||
208 | async function assignReplayFilesToVideo (options: { | ||
209 | video: MVideo | ||
210 | replayDirectory: string | ||
211 | }) { | ||
212 | const { video, replayDirectory } = options | ||
213 | |||
214 | const concatenatedTsFiles = await readdir(replayDirectory) | ||
215 | |||
216 | for (const concatenatedTsFile of concatenatedTsFiles) { | ||
217 | const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) | ||
218 | await video.reload() | ||
219 | |||
220 | const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile) | ||
221 | |||
222 | const probe = await ffprobePromise(concatenatedTsFilePath) | ||
223 | const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) | ||
224 | const { resolution } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe) | ||
225 | const fps = await getVideoStreamFPS(concatenatedTsFilePath, probe) | ||
226 | |||
227 | try { | ||
228 | await generateHlsPlaylistResolutionFromTS({ | ||
229 | video, | ||
230 | inputFileMutexReleaser, | ||
231 | concatenatedTsFilePath, | ||
232 | resolution, | ||
233 | fps, | ||
234 | isAAC: audioStream?.codec_name === 'aac' | ||
235 | }) | ||
236 | } catch (err) { | ||
237 | logger.error('Cannot generate HLS playlist resolution from TS files.', { err }) | ||
238 | } | ||
239 | |||
240 | inputFileMutexReleaser() | ||
241 | } | ||
242 | |||
243 | return video | ||
244 | } | ||
245 | |||
246 | async function cleanupLiveAndFederate (options: { | ||
247 | video: MVideo | ||
248 | permanentLive: boolean | ||
249 | streamingPlaylistId: number | ||
250 | }) { | ||
251 | const { permanentLive, video, streamingPlaylistId } = options | ||
252 | |||
253 | const streamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(streamingPlaylistId) | ||
254 | |||
255 | if (streamingPlaylist) { | ||
256 | if (permanentLive) { | ||
257 | await cleanupAndDestroyPermanentLive(video, streamingPlaylist) | ||
258 | } else { | ||
259 | await cleanupUnsavedNormalLive(video, streamingPlaylist) | ||
260 | } | ||
261 | } | ||
262 | |||
263 | try { | ||
264 | const fullVideo = await VideoModel.loadFull(video.id) | ||
265 | return federateVideoIfNeeded(fullVideo, false, undefined) | ||
266 | } catch (err) { | ||
267 | logger.warn('Cannot federate live after cleanup', { videoId: video.id, err }) | ||
268 | } | ||
269 | } | ||
270 | |||
271 | function createStoryboardJob (video: MVideo) { | ||
272 | return JobQueue.Instance.createJob({ | ||
273 | type: 'generate-video-storyboard' as 'generate-video-storyboard', | ||
274 | payload: { | ||
275 | videoUUID: video.uuid, | ||
276 | federate: true | ||
277 | } | ||
278 | }) | ||
279 | } | ||