aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/job-queue/handlers/video-live-ending.ts
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-04-21 09:06:52 +0200
committerChocobozzz <me@florianbigard.com>2022-04-21 11:47:57 +0200
commit4ec52d04dcc5d664612331f8e08d7d90da990415 (patch)
tree4b193f9f8f210caaf2dbe05ef3e37fa3a6fc28f0 /server/lib/job-queue/handlers/video-live-ending.ts
parent2024a3b9338d667640aa115da6071ea83d088c50 (diff)
downloadPeerTube-4ec52d04dcc5d664612331f8e08d7d90da990415.tar.gz
PeerTube-4ec52d04dcc5d664612331f8e08d7d90da990415.tar.zst
PeerTube-4ec52d04dcc5d664612331f8e08d7d90da990415.zip
Add ability to save replay of permanent lives
Diffstat (limited to 'server/lib/job-queue/handlers/video-live-ending.ts')
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts159
1 files changed, 114 insertions, 45 deletions
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts
index f4de4b47c..1e290338c 100644
--- a/server/lib/job-queue/handlers/video-live-ending.ts
+++ b/server/lib/job-queue/handlers/video-live-ending.ts
@@ -1,25 +1,33 @@
1import { Job } from 'bull' 1import { Job } from 'bull'
2import { pathExists, readdir, remove } from 'fs-extra' 2import { pathExists, readdir, remove } from 'fs-extra'
3import { join } from 'path' 3import { join } from 'path'
4import { ffprobePromise, getAudioStream, getVideoStreamDuration, getVideoStreamDimensionsInfo } from '@server/helpers/ffmpeg' 4import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamDuration } from '@server/helpers/ffmpeg'
5import { VIDEO_LIVE } from '@server/initializers/constants' 5import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
6import { buildConcatenatedName, cleanupLive, LiveSegmentShaStore } from '@server/lib/live' 6import { federateVideoIfNeeded } from '@server/lib/activitypub/videos'
7import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveDirectory } from '@server/lib/paths' 7import { cleanupLive, LiveSegmentShaStore } from '@server/lib/live'
8import {
9 generateHLSMasterPlaylistFilename,
10 generateHlsSha256SegmentsFilename,
11 getLiveDirectory,
12 getLiveReplayBaseDirectory
13} from '@server/lib/paths'
8import { generateVideoMiniature } from '@server/lib/thumbnail' 14import { generateVideoMiniature } from '@server/lib/thumbnail'
9import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/transcoding' 15import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/transcoding'
10import { VideoPathManager } from '@server/lib/video-path-manager'
11import { moveToNextState } from '@server/lib/video-state' 16import { moveToNextState } from '@server/lib/video-state'
12import { VideoModel } from '@server/models/video/video' 17import { VideoModel } from '@server/models/video/video'
13import { VideoFileModel } from '@server/models/video/video-file' 18import { VideoFileModel } from '@server/models/video/video-file'
14import { VideoLiveModel } from '@server/models/video/video-live' 19import { VideoLiveModel } from '@server/models/video/video-live'
15import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 20import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
16import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' 21import { MVideo, MVideoLive, MVideoWithAllFiles } from '@server/types/models'
17import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' 22import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models'
18import { logger } from '../../../helpers/logger' 23import { logger } from '../../../helpers/logger'
24import { VideoBlacklistModel } from '@server/models/video/video-blacklist'
19 25
20async function processVideoLiveEnding (job: Job) { 26async function processVideoLiveEnding (job: Job) {
21 const payload = job.data as VideoLiveEndingPayload 27 const payload = job.data as VideoLiveEndingPayload
22 28
29 logger.info('Processing video live ending for %s.', payload.videoId, { payload })
30
23 function logError () { 31 function logError () {
24 logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId) 32 logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId)
25 } 33 }
@@ -32,19 +40,19 @@ async function processVideoLiveEnding (job: Job) {
32 return 40 return
33 } 41 }
34 42
35 const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id)
36 if (!streamingPlaylist) {
37 logError()
38 return
39 }
40
41 LiveSegmentShaStore.Instance.cleanupShaSegments(video.uuid) 43 LiveSegmentShaStore.Instance.cleanupShaSegments(video.uuid)
42 44
43 if (live.saveReplay !== true) { 45 if (live.saveReplay !== true) {
44 return cleanupLive(video, streamingPlaylist) 46 return cleanupLiveAndFederate(video)
45 } 47 }
46 48
47 return saveLive(video, live, streamingPlaylist) 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)
48} 56}
49 57
50// --------------------------------------------------------------------------- 58// ---------------------------------------------------------------------------
@@ -55,22 +63,66 @@ export {
55 63
56// --------------------------------------------------------------------------- 64// ---------------------------------------------------------------------------
57 65
58async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MStreamingPlaylist) { 66async function saveReplayToExternalVideo (liveVideo: MVideo, publishedAt: string, replayDirectory: string) {
59 const replayDirectory = VideoPathManager.Instance.getFSHLSOutputPath(video, VIDEO_LIVE.REPLAY_DIRECTORY) 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)
60 109
61 const rootFiles = await readdir(getLiveDirectory(video)) 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 }
62 116
63 const playlistFiles = rootFiles.filter(file => { 117 await moveToNextState({ video, isNewVideo: true })
64 return file.endsWith('.m3u8') && file !== streamingPlaylist.playlistFilename 118}
65 })
66 119
120async function replaceLiveByReplay (video: MVideo, live: MVideoLive, replayDirectory: string) {
67 await cleanupTMPLiveFiles(getLiveDirectory(video)) 121 await cleanupTMPLiveFiles(getLiveDirectory(video))
68 122
69 await live.destroy() 123 await live.destroy()
70 124
71 video.isLive = false 125 video.isLive = false
72 // Reinit views
73 video.views = 0
74 video.state = VideoState.TO_TRANSCODE 126 video.state = VideoState.TO_TRANSCODE
75 127
76 await video.save() 128 await video.save()
@@ -87,10 +139,38 @@ async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MSt
87 hlsPlaylist.segmentsSha256Filename = generateHlsSha256SegmentsFilename() 139 hlsPlaylist.segmentsSha256Filename = generateHlsSha256SegmentsFilename()
88 await hlsPlaylist.save() 140 await hlsPlaylist.save()
89 141
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) {
90 let durationDone = false 169 let durationDone = false
91 170
92 for (const playlistFile of playlistFiles) { 171 const concatenatedTsFiles = await readdir(replayDirectory)
93 const concatenatedTsFile = buildConcatenatedName(playlistFile) 172
173 for (const concatenatedTsFile of concatenatedTsFiles) {
94 const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile) 174 const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile)
95 175
96 const probe = await ffprobePromise(concatenatedTsFilePath) 176 const probe = await ffprobePromise(concatenatedTsFilePath)
@@ -99,7 +179,7 @@ async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MSt
99 const { resolution, isPortraitMode } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe) 179 const { resolution, isPortraitMode } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe)
100 180
101 const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({ 181 const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({
102 video: videoWithFiles, 182 video,
103 concatenatedTsFilePath, 183 concatenatedTsFilePath,
104 resolution, 184 resolution,
105 isPortraitMode, 185 isPortraitMode,
@@ -107,33 +187,22 @@ async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MSt
107 }) 187 })
108 188
109 if (!durationDone) { 189 if (!durationDone) {
110 videoWithFiles.duration = await getVideoStreamDuration(outputPath) 190 video.duration = await getVideoStreamDuration(outputPath)
111 await videoWithFiles.save() 191 await video.save()
112 192
113 durationDone = true 193 durationDone = true
114 } 194 }
115 } 195 }
116 196
117 await remove(replayDirectory) 197 return video
118 198}
119 // Regenerate the thumbnail & preview?
120 if (videoWithFiles.getMiniature().automaticallyGenerated === true) {
121 await generateVideoMiniature({
122 video: videoWithFiles,
123 videoFile: videoWithFiles.getMaxQualityFile(),
124 type: ThumbnailType.MINIATURE
125 })
126 }
127 199
128 if (videoWithFiles.getPreview().automaticallyGenerated === true) { 200async function cleanupLiveAndFederate (video: MVideo) {
129 await generateVideoMiniature({ 201 const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id)
130 video: videoWithFiles, 202 await cleanupLive(video, streamingPlaylist)
131 videoFile: videoWithFiles.getMaxQualityFile(),
132 type: ThumbnailType.PREVIEW
133 })
134 }
135 203
136 await moveToNextState({ video: videoWithFiles, isNewVideo: false }) 204 const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)
205 return federateVideoIfNeeded(fullVideo, false, undefined)
137} 206}
138 207
139async function cleanupTMPLiveFiles (hlsDirectory: string) { 208async function cleanupTMPLiveFiles (hlsDirectory: string) {