diff options
Diffstat (limited to 'server/lib')
-rw-r--r-- | server/lib/activitypub/videos.ts | 6 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-live-ending.ts | 33 | ||||
-rw-r--r-- | server/lib/live-manager.ts | 42 |
3 files changed, 50 insertions, 31 deletions
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index ea1e6a38f..ab4aac0a1 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -85,7 +85,7 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid | |||
85 | // Check this is not a blacklisted video, or unfederated blacklisted video | 85 | // Check this is not a blacklisted video, or unfederated blacklisted video |
86 | (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) && | 86 | (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) && |
87 | // Check the video is public/unlisted and published | 87 | // Check the video is public/unlisted and published |
88 | video.hasPrivacyForFederation() && (video.state === VideoState.PUBLISHED || video.state === VideoState.WAITING_FOR_LIVE) | 88 | video.hasPrivacyForFederation() && video.hasStateForFederation() |
89 | ) { | 89 | ) { |
90 | // Fetch more attributes that we will need to serialize in AP object | 90 | // Fetch more attributes that we will need to serialize in AP object |
91 | if (isArray(video.VideoCaptions) === false) { | 91 | if (isArray(video.VideoCaptions) === false) { |
@@ -302,7 +302,7 @@ async function updateVideoFromAP (options: { | |||
302 | }) { | 302 | }) { |
303 | const { video, videoObject, account, channel, overrideTo } = options | 303 | const { video, videoObject, account, channel, overrideTo } = options |
304 | 304 | ||
305 | logger.debug('Updating remote video "%s".', options.videoObject.uuid, { account, channel }) | 305 | logger.debug('Updating remote video "%s".', options.videoObject.uuid, { videoObject: options.videoObject, account, channel }) |
306 | 306 | ||
307 | let videoFieldsSave: any | 307 | let videoFieldsSave: any |
308 | const wasPrivateVideo = video.privacy === VideoPrivacy.PRIVATE | 308 | const wasPrivateVideo = video.privacy === VideoPrivacy.PRIVATE |
@@ -562,6 +562,8 @@ function isAPHashTagObject (url: any): url is ActivityHashTagObject { | |||
562 | return url && url.type === 'Hashtag' | 562 | return url && url.type === 'Hashtag' |
563 | } | 563 | } |
564 | 564 | ||
565 | |||
566 | |||
565 | async function createVideo (videoObject: VideoObject, channel: MChannelAccountLight, waitThumbnail = false) { | 567 | async function createVideo (videoObject: VideoObject, channel: MChannelAccountLight, waitThumbnail = false) { |
566 | logger.debug('Adding remote video %s.', videoObject.id) | 568 | logger.debug('Adding remote video %s.', videoObject.id) |
567 | 569 | ||
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 1e964726e..2b900998a 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -6,22 +6,31 @@ import { publishAndFederateIfNeeded } from '@server/lib/video' | |||
6 | import { getHLSDirectory } from '@server/lib/video-paths' | 6 | import { getHLSDirectory } from '@server/lib/video-paths' |
7 | import { generateHlsPlaylist } from '@server/lib/video-transcoding' | 7 | import { generateHlsPlaylist } from '@server/lib/video-transcoding' |
8 | import { VideoModel } from '@server/models/video/video' | 8 | import { VideoModel } from '@server/models/video/video' |
9 | import { VideoFileModel } from '@server/models/video/video-file' | ||
9 | import { VideoLiveModel } from '@server/models/video/video-live' | 10 | import { VideoLiveModel } from '@server/models/video/video-live' |
10 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 11 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
11 | import { MStreamingPlaylist, MVideo, MVideoLive, MVideoWithFile } from '@server/types/models' | 12 | import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' |
12 | import { VideoLiveEndingPayload, VideoState } from '@shared/models' | 13 | import { VideoLiveEndingPayload, VideoState } from '@shared/models' |
13 | import { logger } from '../../../helpers/logger' | 14 | import { logger } from '../../../helpers/logger' |
14 | import { VideoFileModel } from '@server/models/video/video-file' | ||
15 | 15 | ||
16 | async function processVideoLiveEnding (job: Bull.Job) { | 16 | async function processVideoLiveEnding (job: Bull.Job) { |
17 | const payload = job.data as VideoLiveEndingPayload | 17 | const payload = job.data as VideoLiveEndingPayload |
18 | 18 | ||
19 | function logError () { | ||
20 | logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId) | ||
21 | } | ||
22 | |||
19 | const video = await VideoModel.load(payload.videoId) | 23 | const video = await VideoModel.load(payload.videoId) |
20 | const live = await VideoLiveModel.loadByVideoId(payload.videoId) | 24 | const live = await VideoLiveModel.loadByVideoId(payload.videoId) |
21 | 25 | ||
26 | if (!video || !live) { | ||
27 | logError() | ||
28 | return | ||
29 | } | ||
30 | |||
22 | const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) | 31 | const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) |
23 | if (!video || !streamingPlaylist || !live) { | 32 | if (!streamingPlaylist) { |
24 | logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId) | 33 | logError() |
25 | return | 34 | return |
26 | } | 35 | } |
27 | 36 | ||
@@ -52,21 +61,21 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
52 | const playlistPath = join(hlsDirectory, playlistFile) | 61 | const playlistPath = join(hlsDirectory, playlistFile) |
53 | const { videoFileResolution } = await getVideoFileResolution(playlistPath) | 62 | const { videoFileResolution } = await getVideoFileResolution(playlistPath) |
54 | 63 | ||
55 | const mp4TmpName = buildMP4TmpName(videoFileResolution) | 64 | const mp4TmpPath = buildMP4TmpPath(hlsDirectory, videoFileResolution) |
56 | 65 | ||
57 | // Playlist name is for example 3.m3u8 | 66 | // Playlist name is for example 3.m3u8 |
58 | // Segments names are 3-0.ts 3-1.ts etc | 67 | // Segments names are 3-0.ts 3-1.ts etc |
59 | const shouldStartWith = playlistFile.replace(/\.m3u8$/, '') + '-' | 68 | const shouldStartWith = playlistFile.replace(/\.m3u8$/, '') + '-' |
60 | 69 | ||
61 | const segmentFiles = files.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts')) | 70 | const segmentFiles = files.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts')) |
62 | await hlsPlaylistToFragmentedMP4(hlsDirectory, segmentFiles, mp4TmpName) | 71 | await hlsPlaylistToFragmentedMP4(hlsDirectory, segmentFiles, mp4TmpPath) |
63 | 72 | ||
64 | for (const file of segmentFiles) { | 73 | for (const file of segmentFiles) { |
65 | await remove(join(hlsDirectory, file)) | 74 | await remove(join(hlsDirectory, file)) |
66 | } | 75 | } |
67 | 76 | ||
68 | if (!duration) { | 77 | if (!duration) { |
69 | duration = await getDurationFromVideoFile(mp4TmpName) | 78 | duration = await getDurationFromVideoFile(mp4TmpPath) |
70 | } | 79 | } |
71 | 80 | ||
72 | resolutions.push(videoFileResolution) | 81 | resolutions.push(videoFileResolution) |
@@ -90,7 +99,7 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
90 | hlsPlaylist.VideoFiles = [] | 99 | hlsPlaylist.VideoFiles = [] |
91 | 100 | ||
92 | for (const resolution of resolutions) { | 101 | for (const resolution of resolutions) { |
93 | const videoInputPath = buildMP4TmpName(resolution) | 102 | const videoInputPath = buildMP4TmpPath(hlsDirectory, resolution) |
94 | const { isPortraitMode } = await getVideoFileResolution(videoInputPath) | 103 | const { isPortraitMode } = await getVideoFileResolution(videoInputPath) |
95 | 104 | ||
96 | await generateHlsPlaylist({ | 105 | await generateHlsPlaylist({ |
@@ -101,7 +110,7 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
101 | isPortraitMode | 110 | isPortraitMode |
102 | }) | 111 | }) |
103 | 112 | ||
104 | await remove(join(hlsDirectory, videoInputPath)) | 113 | await remove(videoInputPath) |
105 | } | 114 | } |
106 | 115 | ||
107 | await publishAndFederateIfNeeded(video, true) | 116 | await publishAndFederateIfNeeded(video, true) |
@@ -110,7 +119,7 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
110 | async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { | 119 | async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { |
111 | const hlsDirectory = getHLSDirectory(video, false) | 120 | const hlsDirectory = getHLSDirectory(video, false) |
112 | 121 | ||
113 | await cleanupLiveFiles(hlsDirectory) | 122 | await remove(hlsDirectory) |
114 | 123 | ||
115 | streamingPlaylist.destroy() | 124 | streamingPlaylist.destroy() |
116 | .catch(err => logger.error('Cannot remove live streaming playlist.', { err })) | 125 | .catch(err => logger.error('Cannot remove live streaming playlist.', { err })) |
@@ -135,6 +144,6 @@ async function cleanupLiveFiles (hlsDirectory: string) { | |||
135 | } | 144 | } |
136 | } | 145 | } |
137 | 146 | ||
138 | function buildMP4TmpName (resolution: number) { | 147 | function buildMP4TmpPath (basePath: string, resolution: number) { |
139 | return resolution + '-tmp.mp4' | 148 | return join(basePath, resolution + '-tmp.mp4') |
140 | } | 149 | } |
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts index 2d8f906e9..6eb05c9d6 100644 --- a/server/lib/live-manager.ts +++ b/server/lib/live-manager.ts | |||
@@ -4,7 +4,7 @@ import * as chokidar from 'chokidar' | |||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | 4 | import { FfmpegCommand } from 'fluent-ffmpeg' |
5 | import { ensureDir, stat } from 'fs-extra' | 5 | import { ensureDir, stat } from 'fs-extra' |
6 | import { basename } from 'path' | 6 | import { basename } from 'path' |
7 | import { computeResolutionsToTranscode, runLiveMuxing, runLiveTranscoding } from '@server/helpers/ffmpeg-utils' | 7 | import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution, getVideoStreamCodec, getVideoStreamSize, runLiveMuxing, runLiveTranscoding } from '@server/helpers/ffmpeg-utils' |
8 | import { logger } from '@server/helpers/logger' | 8 | import { logger } from '@server/helpers/logger' |
9 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' | 9 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' |
10 | import { MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, WEBSERVER } from '@server/initializers/constants' | 10 | import { MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, WEBSERVER } from '@server/initializers/constants' |
@@ -137,6 +137,13 @@ class LiveManager { | |||
137 | this.abortSession(sessionId) | 137 | this.abortSession(sessionId) |
138 | } | 138 | } |
139 | 139 | ||
140 | getLiveQuotaUsedByUser (userId: number) { | ||
141 | const currentLives = this.livesPerUser.get(userId) | ||
142 | if (!currentLives) return 0 | ||
143 | |||
144 | return currentLives.reduce((sum, obj) => sum + obj.size, 0) | ||
145 | } | ||
146 | |||
140 | private getContext () { | 147 | private getContext () { |
141 | return context | 148 | return context |
142 | } | 149 | } |
@@ -173,8 +180,15 @@ class LiveManager { | |||
173 | const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) | 180 | const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) |
174 | 181 | ||
175 | const session = this.getContext().sessions.get(sessionId) | 182 | const session = this.getContext().sessions.get(sessionId) |
183 | const rtmpUrl = 'rtmp://127.0.0.1:' + config.rtmp.port + streamPath | ||
184 | |||
185 | const [ resolutionResult, fps ] = await Promise.all([ | ||
186 | getVideoFileResolution(rtmpUrl), | ||
187 | getVideoFileFPS(rtmpUrl) | ||
188 | ]) | ||
189 | |||
176 | const resolutionsEnabled = CONFIG.LIVE.TRANSCODING.ENABLED | 190 | const resolutionsEnabled = CONFIG.LIVE.TRANSCODING.ENABLED |
177 | ? computeResolutionsToTranscode(session.videoHeight, 'live') | 191 | ? computeResolutionsToTranscode(resolutionResult.videoFileResolution, 'live') |
178 | : [] | 192 | : [] |
179 | 193 | ||
180 | logger.info('Will mux/transcode live video of original resolution %d.', session.videoHeight, { resolutionsEnabled }) | 194 | logger.info('Will mux/transcode live video of original resolution %d.', session.videoHeight, { resolutionsEnabled }) |
@@ -193,8 +207,9 @@ class LiveManager { | |||
193 | sessionId, | 207 | sessionId, |
194 | videoLive, | 208 | videoLive, |
195 | playlist: videoStreamingPlaylist, | 209 | playlist: videoStreamingPlaylist, |
196 | streamPath, | ||
197 | originalResolution: session.videoHeight, | 210 | originalResolution: session.videoHeight, |
211 | rtmpUrl, | ||
212 | fps, | ||
198 | resolutionsEnabled | 213 | resolutionsEnabled |
199 | }) | 214 | }) |
200 | } | 215 | } |
@@ -203,11 +218,12 @@ class LiveManager { | |||
203 | sessionId: string | 218 | sessionId: string |
204 | videoLive: MVideoLiveVideo | 219 | videoLive: MVideoLiveVideo |
205 | playlist: MStreamingPlaylist | 220 | playlist: MStreamingPlaylist |
206 | streamPath: string | 221 | rtmpUrl: string |
222 | fps: number | ||
207 | resolutionsEnabled: number[] | 223 | resolutionsEnabled: number[] |
208 | originalResolution: number | 224 | originalResolution: number |
209 | }) { | 225 | }) { |
210 | const { sessionId, videoLive, playlist, streamPath, resolutionsEnabled, originalResolution } = options | 226 | const { sessionId, videoLive, playlist, resolutionsEnabled, originalResolution, fps, rtmpUrl } = options |
211 | const startStreamDateTime = new Date().getTime() | 227 | const startStreamDateTime = new Date().getTime() |
212 | const allResolutions = resolutionsEnabled.concat([ originalResolution ]) | 228 | const allResolutions = resolutionsEnabled.concat([ originalResolution ]) |
213 | 229 | ||
@@ -238,17 +254,16 @@ class LiveManager { | |||
238 | const outPath = getHLSDirectory(videoLive.Video) | 254 | const outPath = getHLSDirectory(videoLive.Video) |
239 | await ensureDir(outPath) | 255 | await ensureDir(outPath) |
240 | 256 | ||
257 | const videoUUID = videoLive.Video.uuid | ||
241 | const deleteSegments = videoLive.saveReplay === false | 258 | const deleteSegments = videoLive.saveReplay === false |
242 | 259 | ||
243 | const rtmpUrl = 'rtmp://127.0.0.1:' + config.rtmp.port + streamPath | ||
244 | const ffmpegExec = CONFIG.LIVE.TRANSCODING.ENABLED | 260 | const ffmpegExec = CONFIG.LIVE.TRANSCODING.ENABLED |
245 | ? runLiveTranscoding(rtmpUrl, outPath, allResolutions, deleteSegments) | 261 | ? runLiveTranscoding(rtmpUrl, outPath, allResolutions, fps, deleteSegments) |
246 | : runLiveMuxing(rtmpUrl, outPath, deleteSegments) | 262 | : runLiveMuxing(rtmpUrl, outPath, deleteSegments) |
247 | 263 | ||
248 | logger.info('Running live muxing/transcoding.') | 264 | logger.info('Running live muxing/transcoding for %s.', videoUUID) |
249 | this.transSessions.set(sessionId, ffmpegExec) | 265 | this.transSessions.set(sessionId, ffmpegExec) |
250 | 266 | ||
251 | const videoUUID = videoLive.Video.uuid | ||
252 | const tsWatcher = chokidar.watch(outPath + '/*.ts') | 267 | const tsWatcher = chokidar.watch(outPath + '/*.ts') |
253 | 268 | ||
254 | const updateSegment = segmentPath => this.segmentsSha256Queue.push({ operation: 'update', segmentPath, videoUUID }) | 269 | const updateSegment = segmentPath => this.segmentsSha256Queue.push({ operation: 'update', segmentPath, videoUUID }) |
@@ -307,7 +322,7 @@ class LiveManager { | |||
307 | }) | 322 | }) |
308 | 323 | ||
309 | const onFFmpegEnded = () => { | 324 | const onFFmpegEnded = () => { |
310 | logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', streamPath) | 325 | logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', rtmpUrl) |
311 | 326 | ||
312 | this.transSessions.delete(sessionId) | 327 | this.transSessions.delete(sessionId) |
313 | 328 | ||
@@ -332,13 +347,6 @@ class LiveManager { | |||
332 | ffmpegExec.on('end', () => onFFmpegEnded()) | 347 | ffmpegExec.on('end', () => onFFmpegEnded()) |
333 | } | 348 | } |
334 | 349 | ||
335 | getLiveQuotaUsedByUser (userId: number) { | ||
336 | const currentLives = this.livesPerUser.get(userId) | ||
337 | if (!currentLives) return 0 | ||
338 | |||
339 | return currentLives.reduce((sum, obj) => sum + obj.size, 0) | ||
340 | } | ||
341 | |||
342 | private async onEndTransmuxing (videoId: number, cleanupNow = false) { | 350 | private async onEndTransmuxing (videoId: number, cleanupNow = false) { |
343 | try { | 351 | try { |
344 | const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | 352 | const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) |