aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/helpers/ffmpeg-utils.ts23
-rw-r--r--server/initializers/constants.ts1
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts32
-rw-r--r--server/lib/live-manager.ts28
4 files changed, 55 insertions, 29 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 7c997877c..085635b5a 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -190,12 +190,11 @@ async function getLiveTranscodingCommand (options: {
190 outPath: string 190 outPath: string
191 resolutions: number[] 191 resolutions: number[]
192 fps: number 192 fps: number
193 deleteSegments: boolean
194 193
195 availableEncoders: AvailableEncoders 194 availableEncoders: AvailableEncoders
196 profile: string 195 profile: string
197}) { 196}) {
198 const { rtmpUrl, outPath, resolutions, fps, deleteSegments, availableEncoders, profile } = options 197 const { rtmpUrl, outPath, resolutions, fps, availableEncoders, profile } = options
199 const input = rtmpUrl 198 const input = rtmpUrl
200 199
201 const command = getFFmpeg(input) 200 const command = getFFmpeg(input)
@@ -272,14 +271,14 @@ async function getLiveTranscodingCommand (options: {
272 varStreamMap.push(`v:${i},a:${i}`) 271 varStreamMap.push(`v:${i},a:${i}`)
273 } 272 }
274 273
275 addDefaultLiveHLSParams(command, outPath, deleteSegments) 274 addDefaultLiveHLSParams(command, outPath)
276 275
277 command.outputOption('-var_stream_map', varStreamMap.join(' ')) 276 command.outputOption('-var_stream_map', varStreamMap.join(' '))
278 277
279 return command 278 return command
280} 279}
281 280
282function getLiveMuxingCommand (rtmpUrl: string, outPath: string, deleteSegments: boolean) { 281function getLiveMuxingCommand (rtmpUrl: string, outPath: string) {
283 const command = getFFmpeg(rtmpUrl) 282 const command = getFFmpeg(rtmpUrl)
284 command.inputOption('-fflags nobuffer') 283 command.inputOption('-fflags nobuffer')
285 284
@@ -288,17 +287,17 @@ function getLiveMuxingCommand (rtmpUrl: string, outPath: string, deleteSegments:
288 command.outputOption('-map 0:a?') 287 command.outputOption('-map 0:a?')
289 command.outputOption('-map 0:v?') 288 command.outputOption('-map 0:v?')
290 289
291 addDefaultLiveHLSParams(command, outPath, deleteSegments) 290 addDefaultLiveHLSParams(command, outPath)
292 291
293 return command 292 return command
294} 293}
295 294
296async function hlsPlaylistToFragmentedMP4 (hlsDirectory: string, segmentFiles: string[], outputPath: string) { 295async function hlsPlaylistToFragmentedMP4 (replayDirectory: string, segmentFiles: string[], outputPath: string) {
297 const concatFilePath = join(hlsDirectory, 'concat.txt') 296 const concatFilePath = join(replayDirectory, 'concat.txt')
298 297
299 function cleaner () { 298 function cleaner () {
300 remove(concatFilePath) 299 remove(concatFilePath)
301 .catch(err => logger.error('Cannot remove concat file in %s.', hlsDirectory, { err })) 300 .catch(err => logger.error('Cannot remove concat file in %s.', replayDirectory, { err }))
302 } 301 }
303 302
304 // First concat the ts files to a mp4 file 303 // First concat the ts files to a mp4 file
@@ -385,14 +384,10 @@ function addDefaultEncoderParams (options: {
385 } 384 }
386} 385}
387 386
388function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string, deleteSegments: boolean) { 387function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string) {
389 command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS) 388 command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS)
390 command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE) 389 command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE)
391 390 command.outputOption('-hls_flags delete_segments')
392 if (deleteSegments === true) {
393 command.outputOption('-hls_flags delete_segments')
394 }
395
396 command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`) 391 command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`)
397 command.outputOption('-master_pl_name master.m3u8') 392 command.outputOption('-master_pl_name master.m3u8')
398 command.outputOption(`-f hls`) 393 command.outputOption(`-f hls`)
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 6c44d703e..da837837e 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -634,6 +634,7 @@ const VIDEO_LIVE = {
634 CLEANUP_DELAY: 1000 * 60 * 5, // 5 minutes 634 CLEANUP_DELAY: 1000 * 60 * 5, // 5 minutes
635 SEGMENT_TIME_SECONDS: 4, // 4 seconds 635 SEGMENT_TIME_SECONDS: 4, // 4 seconds
636 SEGMENTS_LIST_SIZE: 15, // 15 maximum segments in live playlist 636 SEGMENTS_LIST_SIZE: 15, // 15 maximum segments in live playlist
637 REPLAY_DIRECTORY: 'replay',
637 EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION: 4, 638 EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION: 4,
638 RTMP: { 639 RTMP: {
639 CHUNK_SIZE: 60000, 640 CHUNK_SIZE: 60000,
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 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { readdir, remove } from 'fs-extra' 2import { move, readdir, remove } from 'fs-extra'
3import { join } from 'path' 3import { join } from 'path'
4import { hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils' 4import { hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils'
5import { getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils' 5import { getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils'
@@ -14,6 +14,7 @@ import { VideoStreamingPlaylistModel } from '@server/models/video/video-streamin
14import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' 14import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models'
15import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' 15import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models'
16import { logger } from '../../../helpers/logger' 16import { logger } from '../../../helpers/logger'
17import { VIDEO_LIVE } from '@server/initializers/constants'
17 18
18async function processVideoLiveEnding (job: Bull.Job) { 19async 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
54async function saveLive (video: MVideo, live: MVideoLive) { 55async 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
2import * as chokidar from 'chokidar' 2import * as chokidar from 'chokidar'
3import { FfmpegCommand } from 'fluent-ffmpeg' 3import { FfmpegCommand } from 'fluent-ffmpeg'
4import { ensureDir, stat } from 'fs-extra' 4import { copy, ensureDir, stat } from 'fs-extra'
5import { basename } from 'path' 5import { basename, join } from 'path'
6import { isTestInstance } from '@server/helpers/core-utils' 6import { isTestInstance } from '@server/helpers/core-utils'
7import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils' 7import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils'
8import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' 8import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils'
@@ -25,6 +25,7 @@ import { getHLSDirectory } from './video-paths'
25import { availableEncoders } from './video-transcoding-profiles' 25import { availableEncoders } from './video-transcoding-profiles'
26 26
27import memoizee = require('memoizee') 27import memoizee = require('memoizee')
28import { mkdir } from 'fs'
28const NodeRtmpServer = require('node-media-server/node_rtmp_server') 29const NodeRtmpServer = require('node-media-server/node_rtmp_server')
29const context = require('node-media-server/node_core_ctx') 30const context = require('node-media-server/node_core_ctx')
30const nodeMediaServerLogger = require('node-media-server/node_core_logger') 31const 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 }))