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