diff options
Diffstat (limited to 'server/lib')
-rw-r--r-- | server/lib/job-queue/handlers/video-live-ending.ts | 32 | ||||
-rw-r--r-- | server/lib/live-manager.ts | 28 |
2 files changed, 45 insertions, 15 deletions
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 447744224..0d2bcaa28 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { readdir, remove } from 'fs-extra' | 2 | import { move, readdir, remove } from 'fs-extra' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils' | 4 | import { hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils' |
5 | import { getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils' | 5 | import { getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils' |
@@ -14,6 +14,7 @@ import { VideoStreamingPlaylistModel } from '@server/models/video/video-streamin | |||
14 | import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' | 14 | import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' |
15 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' | 15 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' |
16 | import { logger } from '../../../helpers/logger' | 16 | import { logger } from '../../../helpers/logger' |
17 | import { VIDEO_LIVE } from '@server/initializers/constants' | ||
17 | 18 | ||
18 | async function processVideoLiveEnding (job: Bull.Job) { | 19 | async function processVideoLiveEnding (job: Bull.Job) { |
19 | const payload = job.data as VideoLiveEndingPayload | 20 | const payload = job.data as VideoLiveEndingPayload |
@@ -53,24 +54,40 @@ export { | |||
53 | 54 | ||
54 | async function saveLive (video: MVideo, live: MVideoLive) { | 55 | async function saveLive (video: MVideo, live: MVideoLive) { |
55 | const hlsDirectory = getHLSDirectory(video, false) | 56 | const hlsDirectory = getHLSDirectory(video, false) |
56 | const files = await readdir(hlsDirectory) | 57 | const replayDirectory = join(hlsDirectory, VIDEO_LIVE.REPLAY_DIRECTORY) |
58 | |||
59 | const rootFiles = await readdir(hlsDirectory) | ||
60 | |||
61 | const playlistFiles: string[] = [] | ||
62 | |||
63 | for (const file of rootFiles) { | ||
64 | if (file.endsWith('.m3u8') !== true) continue | ||
65 | |||
66 | await move(join(hlsDirectory, file), join(replayDirectory, file)) | ||
67 | |||
68 | if (file !== 'master.m3u8') { | ||
69 | playlistFiles.push(file) | ||
70 | } | ||
71 | } | ||
72 | |||
73 | const replayFiles = await readdir(replayDirectory) | ||
57 | 74 | ||
58 | const playlistFiles = files.filter(f => f.endsWith('.m3u8') && f !== 'master.m3u8') | ||
59 | const resolutions: number[] = [] | 75 | const resolutions: number[] = [] |
60 | let duration: number | 76 | let duration: number |
61 | 77 | ||
62 | for (const playlistFile of playlistFiles) { | 78 | for (const playlistFile of playlistFiles) { |
63 | const playlistPath = join(hlsDirectory, playlistFile) | 79 | const playlistPath = join(replayDirectory, playlistFile) |
64 | const { videoFileResolution } = await getVideoFileResolution(playlistPath) | 80 | const { videoFileResolution } = await getVideoFileResolution(playlistPath) |
65 | 81 | ||
82 | // Put the final mp4 in the hls directory, and not in the replay directory | ||
66 | const mp4TmpPath = buildMP4TmpPath(hlsDirectory, videoFileResolution) | 83 | const mp4TmpPath = buildMP4TmpPath(hlsDirectory, videoFileResolution) |
67 | 84 | ||
68 | // Playlist name is for example 3.m3u8 | 85 | // Playlist name is for example 3.m3u8 |
69 | // Segments names are 3-0.ts 3-1.ts etc | 86 | // Segments names are 3-0.ts 3-1.ts etc |
70 | const shouldStartWith = playlistFile.replace(/\.m3u8$/, '') + '-' | 87 | const shouldStartWith = playlistFile.replace(/\.m3u8$/, '') + '-' |
71 | 88 | ||
72 | const segmentFiles = files.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts')) | 89 | const segmentFiles = replayFiles.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts')) |
73 | await hlsPlaylistToFragmentedMP4(hlsDirectory, segmentFiles, mp4TmpPath) | 90 | await hlsPlaylistToFragmentedMP4(replayDirectory, segmentFiles, mp4TmpPath) |
74 | 91 | ||
75 | if (!duration) { | 92 | if (!duration) { |
76 | duration = await getDurationFromVideoFile(mp4TmpPath) | 93 | duration = await getDurationFromVideoFile(mp4TmpPath) |
@@ -143,7 +160,8 @@ async function cleanupLiveFiles (hlsDirectory: string) { | |||
143 | filename.endsWith('.m3u8') || | 160 | filename.endsWith('.m3u8') || |
144 | filename.endsWith('.mpd') || | 161 | filename.endsWith('.mpd') || |
145 | filename.endsWith('.m4s') || | 162 | filename.endsWith('.m4s') || |
146 | filename.endsWith('.tmp') | 163 | filename.endsWith('.tmp') || |
164 | filename === VIDEO_LIVE.REPLAY_DIRECTORY | ||
147 | ) { | 165 | ) { |
148 | const p = join(hlsDirectory, filename) | 166 | const p = join(hlsDirectory, filename) |
149 | 167 | ||
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts index d63e79dfc..d201465fa 100644 --- a/server/lib/live-manager.ts +++ b/server/lib/live-manager.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | 1 | ||
2 | import * as chokidar from 'chokidar' | 2 | import * as chokidar from 'chokidar' |
3 | import { FfmpegCommand } from 'fluent-ffmpeg' | 3 | import { FfmpegCommand } from 'fluent-ffmpeg' |
4 | import { ensureDir, stat } from 'fs-extra' | 4 | import { copy, ensureDir, stat } from 'fs-extra' |
5 | import { basename } from 'path' | 5 | import { basename, join } from 'path' |
6 | import { isTestInstance } from '@server/helpers/core-utils' | 6 | import { isTestInstance } from '@server/helpers/core-utils' |
7 | import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils' | 7 | import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils' |
8 | import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' | 8 | import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' |
@@ -25,6 +25,7 @@ import { getHLSDirectory } from './video-paths' | |||
25 | import { availableEncoders } from './video-transcoding-profiles' | 25 | import { availableEncoders } from './video-transcoding-profiles' |
26 | 26 | ||
27 | import memoizee = require('memoizee') | 27 | import memoizee = require('memoizee') |
28 | import { mkdir } from 'fs' | ||
28 | const NodeRtmpServer = require('node-media-server/node_rtmp_server') | 29 | const NodeRtmpServer = require('node-media-server/node_rtmp_server') |
29 | const context = require('node-media-server/node_core_ctx') | 30 | const context = require('node-media-server/node_core_ctx') |
30 | const nodeMediaServerLogger = require('node-media-server/node_core_logger') | 31 | const nodeMediaServerLogger = require('node-media-server/node_core_logger') |
@@ -261,8 +262,13 @@ class LiveManager { | |||
261 | const outPath = getHLSDirectory(videoLive.Video) | 262 | const outPath = getHLSDirectory(videoLive.Video) |
262 | await ensureDir(outPath) | 263 | await ensureDir(outPath) |
263 | 264 | ||
265 | const replayDirectory = join(outPath, VIDEO_LIVE.REPLAY_DIRECTORY) | ||
266 | |||
267 | if (videoLive.saveReplay === true) { | ||
268 | await ensureDir(replayDirectory) | ||
269 | } | ||
270 | |||
264 | const videoUUID = videoLive.Video.uuid | 271 | const videoUUID = videoLive.Video.uuid |
265 | const deleteSegments = videoLive.saveReplay === false | ||
266 | 272 | ||
267 | const ffmpegExec = CONFIG.LIVE.TRANSCODING.ENABLED | 273 | const ffmpegExec = CONFIG.LIVE.TRANSCODING.ENABLED |
268 | ? await getLiveTranscodingCommand({ | 274 | ? await getLiveTranscodingCommand({ |
@@ -270,11 +276,10 @@ class LiveManager { | |||
270 | outPath, | 276 | outPath, |
271 | resolutions: allResolutions, | 277 | resolutions: allResolutions, |
272 | fps, | 278 | fps, |
273 | deleteSegments, | ||
274 | availableEncoders, | 279 | availableEncoders, |
275 | profile: 'default' | 280 | profile: 'default' |
276 | }) | 281 | }) |
277 | : getLiveMuxingCommand(rtmpUrl, outPath, deleteSegments) | 282 | : getLiveMuxingCommand(rtmpUrl, outPath) |
278 | 283 | ||
279 | logger.info('Running live muxing/transcoding for %s.', videoUUID) | 284 | logger.info('Running live muxing/transcoding for %s.', videoUUID) |
280 | this.transSessions.set(sessionId, ffmpegExec) | 285 | this.transSessions.set(sessionId, ffmpegExec) |
@@ -284,11 +289,18 @@ class LiveManager { | |||
284 | const segmentsToProcessPerPlaylist: { [playlistId: string]: string[] } = {} | 289 | const segmentsToProcessPerPlaylist: { [playlistId: string]: string[] } = {} |
285 | const playlistIdMatcher = /^([\d+])-/ | 290 | const playlistIdMatcher = /^([\d+])-/ |
286 | 291 | ||
287 | const processHashSegments = (segmentsToProcess: string[]) => { | 292 | const processSegments = (segmentsToProcess: string[]) => { |
288 | // Add sha hash of previous segments, because ffmpeg should have finished generating them | 293 | // Add sha hash of previous segments, because ffmpeg should have finished generating them |
289 | for (const previousSegment of segmentsToProcess) { | 294 | for (const previousSegment of segmentsToProcess) { |
290 | this.addSegmentSha(videoUUID, previousSegment) | 295 | this.addSegmentSha(videoUUID, previousSegment) |
291 | .catch(err => logger.error('Cannot add sha segment of video %s -> %s.', videoUUID, previousSegment, { err })) | 296 | .catch(err => logger.error('Cannot add sha segment of video %s -> %s.', videoUUID, previousSegment, { err })) |
297 | |||
298 | if (videoLive.saveReplay) { | ||
299 | const segmentName = basename(previousSegment) | ||
300 | |||
301 | copy(previousSegment, join(outPath, VIDEO_LIVE.REPLAY_DIRECTORY, segmentName)) | ||
302 | .catch(err => logger.error('Cannot copy segment %s to repay directory.', previousSegment, { err })) | ||
303 | } | ||
292 | } | 304 | } |
293 | } | 305 | } |
294 | 306 | ||
@@ -298,7 +310,7 @@ class LiveManager { | |||
298 | const playlistId = basename(segmentPath).match(playlistIdMatcher)[0] | 310 | const playlistId = basename(segmentPath).match(playlistIdMatcher)[0] |
299 | 311 | ||
300 | const segmentsToProcess = segmentsToProcessPerPlaylist[playlistId] || [] | 312 | const segmentsToProcess = segmentsToProcessPerPlaylist[playlistId] || [] |
301 | processHashSegments(segmentsToProcess) | 313 | processSegments(segmentsToProcess) |
302 | 314 | ||
303 | segmentsToProcessPerPlaylist[playlistId] = [ segmentPath ] | 315 | segmentsToProcessPerPlaylist[playlistId] = [ segmentPath ] |
304 | 316 | ||
@@ -369,7 +381,7 @@ class LiveManager { | |||
369 | .then(() => { | 381 | .then(() => { |
370 | // Process remaining segments hash | 382 | // Process remaining segments hash |
371 | for (const key of Object.keys(segmentsToProcessPerPlaylist)) { | 383 | for (const key of Object.keys(segmentsToProcessPerPlaylist)) { |
372 | processHashSegments(segmentsToProcessPerPlaylist[key]) | 384 | processSegments(segmentsToProcessPerPlaylist[key]) |
373 | } | 385 | } |
374 | }) | 386 | }) |
375 | .catch(err => logger.error('Cannot close watchers of %s or process remaining hash segments.', outPath, { err })) | 387 | .catch(err => logger.error('Cannot close watchers of %s or process remaining hash segments.', outPath, { err })) |