]>
Commit | Line | Data |
---|---|---|
41fb13c3 | 1 | import { Job } from 'bull' |
e772bdf1 | 2 | import { pathExists, 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' | |
7 | import { cleanupLive, LiveSegmentShaStore } from '@server/lib/live' | |
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) { |
26e3e98f | 48 | return cleanupLiveAndFederate({ liveVideo }) |
b5b68755 C |
49 | } |
50 | ||
4ec52d04 | 51 | if (live.permanentLive) { |
26e3e98f | 52 | await saveReplayToExternalVideo({ liveVideo, liveSession, publishedAt: payload.publishedAt, replayDirectory: payload.replayDirectory }) |
4ec52d04 | 53 | |
26e3e98f | 54 | return cleanupLiveAndFederate({ 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 C |
166 | |
167 | await remove(getLiveReplayBaseDirectory(videoWithFiles)) | |
168 | ||
169 | // Regenerate the thumbnail & preview? | |
170 | if (videoWithFiles.getMiniature().automaticallyGenerated === true) { | |
171 | const miniature = await generateVideoMiniature({ | |
172 | video: videoWithFiles, | |
173 | videoFile: videoWithFiles.getMaxQualityFile(), | |
174 | type: ThumbnailType.MINIATURE | |
175 | }) | |
26e3e98f | 176 | await videoWithFiles.addAndSaveThumbnail(miniature) |
4ec52d04 C |
177 | } |
178 | ||
179 | if (videoWithFiles.getPreview().automaticallyGenerated === true) { | |
180 | const preview = await generateVideoMiniature({ | |
181 | video: videoWithFiles, | |
182 | videoFile: videoWithFiles.getMaxQualityFile(), | |
183 | type: ThumbnailType.PREVIEW | |
184 | }) | |
26e3e98f | 185 | await videoWithFiles.addAndSaveThumbnail(preview) |
4ec52d04 C |
186 | } |
187 | ||
26e3e98f C |
188 | // We consider this is a new video |
189 | await moveToNextState({ video: videoWithFiles, isNewVideo: true }) | |
4ec52d04 C |
190 | } |
191 | ||
26e3e98f C |
192 | async function assignReplayFilesToVideo (options: { |
193 | video: MVideo | |
194 | replayDirectory: string | |
195 | }) { | |
196 | const { video, replayDirectory } = options | |
197 | ||
a800dbf3 | 198 | let durationDone = false |
b5b68755 | 199 | |
4ec52d04 C |
200 | const concatenatedTsFiles = await readdir(replayDirectory) |
201 | ||
202 | for (const concatenatedTsFile of concatenatedTsFiles) { | |
3851e732 | 203 | const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile) |
2650d6d4 | 204 | |
e772bdf1 C |
205 | const probe = await ffprobePromise(concatenatedTsFilePath) |
206 | const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) | |
207 | ||
c729caf6 | 208 | const { resolution, isPortraitMode } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe) |
2650d6d4 | 209 | |
0305db28 | 210 | const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({ |
4ec52d04 | 211 | video, |
3851e732 | 212 | concatenatedTsFilePath, |
679c12e6 | 213 | resolution, |
e772bdf1 C |
214 | isPortraitMode, |
215 | isAAC: audioStream?.codec_name === 'aac' | |
b5b68755 | 216 | }) |
b5b68755 | 217 | |
4a54a939 | 218 | if (!durationDone) { |
4ec52d04 C |
219 | video.duration = await getVideoStreamDuration(outputPath) |
220 | await video.save() | |
4a54a939 C |
221 | |
222 | durationDone = true | |
2650d6d4 | 223 | } |
97969c4e | 224 | } |
d846d99c | 225 | |
4ec52d04 C |
226 | return video |
227 | } | |
053aed43 | 228 | |
26e3e98f C |
229 | async function cleanupLiveAndFederate (options: { |
230 | liveVideo: MVideo | |
231 | }) { | |
232 | const { liveVideo } = options | |
233 | ||
234 | const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(liveVideo.id) | |
235 | await cleanupLive(liveVideo, streamingPlaylist) | |
053aed43 | 236 | |
26e3e98f | 237 | const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(liveVideo.id) |
4ec52d04 | 238 | return federateVideoIfNeeded(fullVideo, false, undefined) |
b5b68755 C |
239 | } |
240 | ||
0305db28 | 241 | async function cleanupTMPLiveFiles (hlsDirectory: string) { |
bb4ba6d9 C |
242 | if (!await pathExists(hlsDirectory)) return |
243 | ||
a5cf76af C |
244 | const files = await readdir(hlsDirectory) |
245 | ||
246 | for (const filename of files) { | |
247 | if ( | |
248 | filename.endsWith('.ts') || | |
249 | filename.endsWith('.m3u8') || | |
250 | filename.endsWith('.mpd') || | |
251 | filename.endsWith('.m4s') || | |
2650d6d4 | 252 | filename.endsWith('.tmp') |
a5cf76af C |
253 | ) { |
254 | const p = join(hlsDirectory, filename) | |
255 | ||
256 | remove(p) | |
257 | .catch(err => logger.error('Cannot remove %s.', p, { err })) | |
258 | } | |
259 | } | |
a5cf76af | 260 | } |