Fix high CPU with long live when save replay is true

pull/3385/head
Chocobozzz 2020-11-30 15:59:22 +01:00
parent d605328a30
commit 937581b8f6
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
4 changed files with 55 additions and 29 deletions

View File

@ -190,12 +190,11 @@ async function getLiveTranscodingCommand (options: {
outPath: string outPath: string
resolutions: number[] resolutions: number[]
fps: number fps: number
deleteSegments: boolean
availableEncoders: AvailableEncoders availableEncoders: AvailableEncoders
profile: string profile: string
}) { }) {
const { rtmpUrl, outPath, resolutions, fps, deleteSegments, availableEncoders, profile } = options const { rtmpUrl, outPath, resolutions, fps, availableEncoders, profile } = options
const input = rtmpUrl const input = rtmpUrl
const command = getFFmpeg(input) const command = getFFmpeg(input)
@ -272,14 +271,14 @@ async function getLiveTranscodingCommand (options: {
varStreamMap.push(`v:${i},a:${i}`) varStreamMap.push(`v:${i},a:${i}`)
} }
addDefaultLiveHLSParams(command, outPath, deleteSegments) addDefaultLiveHLSParams(command, outPath)
command.outputOption('-var_stream_map', varStreamMap.join(' ')) command.outputOption('-var_stream_map', varStreamMap.join(' '))
return command return command
} }
function getLiveMuxingCommand (rtmpUrl: string, outPath: string, deleteSegments: boolean) { function getLiveMuxingCommand (rtmpUrl: string, outPath: string) {
const command = getFFmpeg(rtmpUrl) const command = getFFmpeg(rtmpUrl)
command.inputOption('-fflags nobuffer') command.inputOption('-fflags nobuffer')
@ -288,17 +287,17 @@ function getLiveMuxingCommand (rtmpUrl: string, outPath: string, deleteSegments:
command.outputOption('-map 0:a?') command.outputOption('-map 0:a?')
command.outputOption('-map 0:v?') command.outputOption('-map 0:v?')
addDefaultLiveHLSParams(command, outPath, deleteSegments) addDefaultLiveHLSParams(command, outPath)
return command return command
} }
async function hlsPlaylistToFragmentedMP4 (hlsDirectory: string, segmentFiles: string[], outputPath: string) { async function hlsPlaylistToFragmentedMP4 (replayDirectory: string, segmentFiles: string[], outputPath: string) {
const concatFilePath = join(hlsDirectory, 'concat.txt') const concatFilePath = join(replayDirectory, 'concat.txt')
function cleaner () { function cleaner () {
remove(concatFilePath) remove(concatFilePath)
.catch(err => logger.error('Cannot remove concat file in %s.', hlsDirectory, { err })) .catch(err => logger.error('Cannot remove concat file in %s.', replayDirectory, { err }))
} }
// First concat the ts files to a mp4 file // First concat the ts files to a mp4 file
@ -385,14 +384,10 @@ function addDefaultEncoderParams (options: {
} }
} }
function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string, deleteSegments: boolean) { function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string) {
command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS) command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS)
command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE) command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE)
if (deleteSegments === true) {
command.outputOption('-hls_flags delete_segments') command.outputOption('-hls_flags delete_segments')
}
command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`) command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`)
command.outputOption('-master_pl_name master.m3u8') command.outputOption('-master_pl_name master.m3u8')
command.outputOption(`-f hls`) command.outputOption(`-f hls`)

View File

@ -634,6 +634,7 @@ const VIDEO_LIVE = {
CLEANUP_DELAY: 1000 * 60 * 5, // 5 minutes CLEANUP_DELAY: 1000 * 60 * 5, // 5 minutes
SEGMENT_TIME_SECONDS: 4, // 4 seconds SEGMENT_TIME_SECONDS: 4, // 4 seconds
SEGMENTS_LIST_SIZE: 15, // 15 maximum segments in live playlist SEGMENTS_LIST_SIZE: 15, // 15 maximum segments in live playlist
REPLAY_DIRECTORY: 'replay',
EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION: 4, EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION: 4,
RTMP: { RTMP: {
CHUNK_SIZE: 60000, CHUNK_SIZE: 60000,

View File

@ -1,5 +1,5 @@
import * as Bull from 'bull' import * as Bull from 'bull'
import { readdir, remove } from 'fs-extra' import { move, readdir, remove } from 'fs-extra'
import { join } from 'path' import { join } from 'path'
import { hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils' import { hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils'
import { getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils' import { getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils'
@ -14,6 +14,7 @@ import { VideoStreamingPlaylistModel } from '@server/models/video/video-streamin
import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models'
import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { VIDEO_LIVE } from '@server/initializers/constants'
async function processVideoLiveEnding (job: Bull.Job) { async function processVideoLiveEnding (job: Bull.Job) {
const payload = job.data as VideoLiveEndingPayload const payload = job.data as VideoLiveEndingPayload
@ -53,24 +54,40 @@ export {
async function saveLive (video: MVideo, live: MVideoLive) { async function saveLive (video: MVideo, live: MVideoLive) {
const hlsDirectory = getHLSDirectory(video, false) const hlsDirectory = getHLSDirectory(video, false)
const files = await readdir(hlsDirectory) const replayDirectory = join(hlsDirectory, VIDEO_LIVE.REPLAY_DIRECTORY)
const rootFiles = await readdir(hlsDirectory)
const playlistFiles: string[] = []
for (const file of rootFiles) {
if (file.endsWith('.m3u8') !== true) continue
await move(join(hlsDirectory, file), join(replayDirectory, file))
if (file !== 'master.m3u8') {
playlistFiles.push(file)
}
}
const replayFiles = await readdir(replayDirectory)
const playlistFiles = files.filter(f => f.endsWith('.m3u8') && f !== 'master.m3u8')
const resolutions: number[] = [] const resolutions: number[] = []
let duration: number let duration: number
for (const playlistFile of playlistFiles) { for (const playlistFile of playlistFiles) {
const playlistPath = join(hlsDirectory, playlistFile) const playlistPath = join(replayDirectory, playlistFile)
const { videoFileResolution } = await getVideoFileResolution(playlistPath) const { videoFileResolution } = await getVideoFileResolution(playlistPath)
// Put the final mp4 in the hls directory, and not in the replay directory
const mp4TmpPath = buildMP4TmpPath(hlsDirectory, videoFileResolution) const mp4TmpPath = buildMP4TmpPath(hlsDirectory, videoFileResolution)
// Playlist name is for example 3.m3u8 // Playlist name is for example 3.m3u8
// Segments names are 3-0.ts 3-1.ts etc // Segments names are 3-0.ts 3-1.ts etc
const shouldStartWith = playlistFile.replace(/\.m3u8$/, '') + '-' const shouldStartWith = playlistFile.replace(/\.m3u8$/, '') + '-'
const segmentFiles = files.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts')) const segmentFiles = replayFiles.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts'))
await hlsPlaylistToFragmentedMP4(hlsDirectory, segmentFiles, mp4TmpPath) await hlsPlaylistToFragmentedMP4(replayDirectory, segmentFiles, mp4TmpPath)
if (!duration) { if (!duration) {
duration = await getDurationFromVideoFile(mp4TmpPath) duration = await getDurationFromVideoFile(mp4TmpPath)
@ -143,7 +160,8 @@ async function cleanupLiveFiles (hlsDirectory: string) {
filename.endsWith('.m3u8') || filename.endsWith('.m3u8') ||
filename.endsWith('.mpd') || filename.endsWith('.mpd') ||
filename.endsWith('.m4s') || filename.endsWith('.m4s') ||
filename.endsWith('.tmp') filename.endsWith('.tmp') ||
filename === VIDEO_LIVE.REPLAY_DIRECTORY
) { ) {
const p = join(hlsDirectory, filename) const p = join(hlsDirectory, filename)

View File

@ -1,8 +1,8 @@
import * as chokidar from 'chokidar' import * as chokidar from 'chokidar'
import { FfmpegCommand } from 'fluent-ffmpeg' import { FfmpegCommand } from 'fluent-ffmpeg'
import { ensureDir, stat } from 'fs-extra' import { copy, ensureDir, stat } from 'fs-extra'
import { basename } from 'path' import { basename, join } from 'path'
import { isTestInstance } from '@server/helpers/core-utils' import { isTestInstance } from '@server/helpers/core-utils'
import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils' import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils'
import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils'
@ -25,6 +25,7 @@ import { getHLSDirectory } from './video-paths'
import { availableEncoders } from './video-transcoding-profiles' import { availableEncoders } from './video-transcoding-profiles'
import memoizee = require('memoizee') import memoizee = require('memoizee')
import { mkdir } from 'fs'
const NodeRtmpServer = require('node-media-server/node_rtmp_server') const NodeRtmpServer = require('node-media-server/node_rtmp_server')
const context = require('node-media-server/node_core_ctx') const context = require('node-media-server/node_core_ctx')
const nodeMediaServerLogger = require('node-media-server/node_core_logger') const nodeMediaServerLogger = require('node-media-server/node_core_logger')
@ -261,8 +262,13 @@ class LiveManager {
const outPath = getHLSDirectory(videoLive.Video) const outPath = getHLSDirectory(videoLive.Video)
await ensureDir(outPath) await ensureDir(outPath)
const replayDirectory = join(outPath, VIDEO_LIVE.REPLAY_DIRECTORY)
if (videoLive.saveReplay === true) {
await ensureDir(replayDirectory)
}
const videoUUID = videoLive.Video.uuid const videoUUID = videoLive.Video.uuid
const deleteSegments = videoLive.saveReplay === false
const ffmpegExec = CONFIG.LIVE.TRANSCODING.ENABLED const ffmpegExec = CONFIG.LIVE.TRANSCODING.ENABLED
? await getLiveTranscodingCommand({ ? await getLiveTranscodingCommand({
@ -270,11 +276,10 @@ class LiveManager {
outPath, outPath,
resolutions: allResolutions, resolutions: allResolutions,
fps, fps,
deleteSegments,
availableEncoders, availableEncoders,
profile: 'default' profile: 'default'
}) })
: getLiveMuxingCommand(rtmpUrl, outPath, deleteSegments) : getLiveMuxingCommand(rtmpUrl, outPath)
logger.info('Running live muxing/transcoding for %s.', videoUUID) logger.info('Running live muxing/transcoding for %s.', videoUUID)
this.transSessions.set(sessionId, ffmpegExec) this.transSessions.set(sessionId, ffmpegExec)
@ -284,11 +289,18 @@ class LiveManager {
const segmentsToProcessPerPlaylist: { [playlistId: string]: string[] } = {} const segmentsToProcessPerPlaylist: { [playlistId: string]: string[] } = {}
const playlistIdMatcher = /^([\d+])-/ const playlistIdMatcher = /^([\d+])-/
const processHashSegments = (segmentsToProcess: string[]) => { const processSegments = (segmentsToProcess: string[]) => {
// Add sha hash of previous segments, because ffmpeg should have finished generating them // Add sha hash of previous segments, because ffmpeg should have finished generating them
for (const previousSegment of segmentsToProcess) { for (const previousSegment of segmentsToProcess) {
this.addSegmentSha(videoUUID, previousSegment) this.addSegmentSha(videoUUID, previousSegment)
.catch(err => logger.error('Cannot add sha segment of video %s -> %s.', videoUUID, previousSegment, { err })) .catch(err => logger.error('Cannot add sha segment of video %s -> %s.', videoUUID, previousSegment, { err }))
if (videoLive.saveReplay) {
const segmentName = basename(previousSegment)
copy(previousSegment, join(outPath, VIDEO_LIVE.REPLAY_DIRECTORY, segmentName))
.catch(err => logger.error('Cannot copy segment %s to repay directory.', previousSegment, { err }))
}
} }
} }
@ -298,7 +310,7 @@ class LiveManager {
const playlistId = basename(segmentPath).match(playlistIdMatcher)[0] const playlistId = basename(segmentPath).match(playlistIdMatcher)[0]
const segmentsToProcess = segmentsToProcessPerPlaylist[playlistId] || [] const segmentsToProcess = segmentsToProcessPerPlaylist[playlistId] || []
processHashSegments(segmentsToProcess) processSegments(segmentsToProcess)
segmentsToProcessPerPlaylist[playlistId] = [ segmentPath ] segmentsToProcessPerPlaylist[playlistId] = [ segmentPath ]
@ -369,7 +381,7 @@ class LiveManager {
.then(() => { .then(() => {
// Process remaining segments hash // Process remaining segments hash
for (const key of Object.keys(segmentsToProcessPerPlaylist)) { for (const key of Object.keys(segmentsToProcessPerPlaylist)) {
processHashSegments(segmentsToProcessPerPlaylist[key]) processSegments(segmentsToProcessPerPlaylist[key])
} }
}) })
.catch(err => logger.error('Cannot close watchers of %s or process remaining hash segments.', outPath, { err })) .catch(err => logger.error('Cannot close watchers of %s or process remaining hash segments.', outPath, { err }))