PeerTube/server/helpers/ffmpeg/ffmpeg-live.ts

205 lines
5.7 KiB
TypeScript

import { FfmpegCommand, FilterSpecification } from 'fluent-ffmpeg'
import { join } from 'path'
import { VIDEO_LIVE } from '@server/initializers/constants'
import { AvailableEncoders, LiveVideoLatencyMode } from '@shared/models'
import { logger, loggerTagsFactory } from '../logger'
import { buildStreamSuffix, getFFmpeg, getScaleFilter, StreamType } from './ffmpeg-commons'
import { getEncoderBuilderResult } from './ffmpeg-encoders'
import { addDefaultEncoderGlobalParams, addDefaultEncoderParams, applyEncoderOptions } from './ffmpeg-presets'
import { computeFPS } from './ffprobe-utils'
const lTags = loggerTagsFactory('ffmpeg')
async function getLiveTranscodingCommand (options: {
inputUrl: string
outPath: string
masterPlaylistName: string
latencyMode: LiveVideoLatencyMode
resolutions: number[]
// Input information
fps: number
bitrate: number
ratio: number
hasAudio: boolean
availableEncoders: AvailableEncoders
profile: string
}) {
const {
inputUrl,
outPath,
resolutions,
fps,
bitrate,
availableEncoders,
profile,
masterPlaylistName,
ratio,
latencyMode,
hasAudio
} = options
const command = getFFmpeg(inputUrl, 'live')
const varStreamMap: string[] = []
const complexFilter: FilterSpecification[] = [
{
inputs: '[v:0]',
filter: 'split',
options: resolutions.length,
outputs: resolutions.map(r => `vtemp${r}`)
}
]
command.outputOption('-sc_threshold 0')
addDefaultEncoderGlobalParams(command)
for (let i = 0; i < resolutions.length; i++) {
const streamMap: string[] = []
const resolution = resolutions[i]
const resolutionFPS = computeFPS(fps, resolution)
const baseEncoderBuilderParams = {
input: inputUrl,
availableEncoders,
profile,
canCopyAudio: true,
canCopyVideo: true,
inputBitrate: bitrate,
inputRatio: ratio,
resolution,
fps: resolutionFPS,
streamNum: i,
videoType: 'live' as 'live'
}
{
const streamType: StreamType = 'video'
const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType })
if (!builderResult) {
throw new Error('No available live video encoder found')
}
command.outputOption(`-map [vout${resolution}]`)
addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
logger.debug(
'Apply ffmpeg live video params from %s using %s profile.', builderResult.encoder, profile,
{ builderResult, fps: resolutionFPS, resolution, ...lTags() }
)
command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`)
applyEncoderOptions(command, builderResult.result)
complexFilter.push({
inputs: `vtemp${resolution}`,
filter: getScaleFilter(builderResult.result),
options: `w=-2:h=${resolution}`,
outputs: `vout${resolution}`
})
streamMap.push(`v:${i}`)
}
if (hasAudio) {
const streamType: StreamType = 'audio'
const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType })
if (!builderResult) {
throw new Error('No available live audio encoder found')
}
command.outputOption('-map a:0')
addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
logger.debug(
'Apply ffmpeg live audio params from %s using %s profile.', builderResult.encoder, profile,
{ builderResult, fps: resolutionFPS, resolution, ...lTags() }
)
command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`)
applyEncoderOptions(command, builderResult.result)
streamMap.push(`a:${i}`)
}
varStreamMap.push(streamMap.join(','))
}
command.complexFilter(complexFilter)
addDefaultLiveHLSParams({ command, outPath, masterPlaylistName, latencyMode })
command.outputOption('-var_stream_map', varStreamMap.join(' '))
return command
}
function getLiveMuxingCommand (options: {
inputUrl: string
outPath: string
masterPlaylistName: string
latencyMode: LiveVideoLatencyMode
}) {
const { inputUrl, outPath, masterPlaylistName, latencyMode } = options
const command = getFFmpeg(inputUrl, 'live')
command.outputOption('-c:v copy')
command.outputOption('-c:a copy')
command.outputOption('-map 0:a?')
command.outputOption('-map 0:v?')
addDefaultLiveHLSParams({ command, outPath, masterPlaylistName, latencyMode })
return command
}
function getLiveSegmentTime (latencyMode: LiveVideoLatencyMode) {
if (latencyMode === LiveVideoLatencyMode.SMALL_LATENCY) {
return VIDEO_LIVE.SEGMENT_TIME_SECONDS.SMALL_LATENCY
}
return VIDEO_LIVE.SEGMENT_TIME_SECONDS.DEFAULT_LATENCY
}
// ---------------------------------------------------------------------------
export {
getLiveSegmentTime,
getLiveTranscodingCommand,
getLiveMuxingCommand
}
// ---------------------------------------------------------------------------
function addDefaultLiveHLSParams (options: {
command: FfmpegCommand
outPath: string
masterPlaylistName: string
latencyMode: LiveVideoLatencyMode
}) {
const { command, outPath, masterPlaylistName, latencyMode } = options
command.outputOption('-hls_time ' + getLiveSegmentTime(latencyMode))
command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE)
command.outputOption('-hls_flags delete_segments+independent_segments+program_date_time')
command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`)
command.outputOption('-master_pl_name ' + masterPlaylistName)
command.outputOption(`-f hls`)
command.output(join(outPath, '%v.m3u8'))
}