2022-02-11 10:51:33 +01:00
|
|
|
import { FfmpegCommand } from 'fluent-ffmpeg'
|
|
|
|
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
2022-08-17 14:52:23 +02:00
|
|
|
import { pick } from '@shared/core-utils'
|
2022-02-11 10:51:33 +01:00
|
|
|
import { AvailableEncoders, EncoderOptions } from '@shared/models'
|
|
|
|
import { buildStreamSuffix, getScaleFilter, StreamType } from './ffmpeg-commons'
|
|
|
|
import { getEncoderBuilderResult } from './ffmpeg-encoders'
|
|
|
|
import { ffprobePromise, getVideoStreamBitrate, getVideoStreamDimensionsInfo, hasAudioStream } from './ffprobe-utils'
|
|
|
|
|
|
|
|
const lTags = loggerTagsFactory('ffmpeg')
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
function addDefaultEncoderGlobalParams (command: FfmpegCommand) {
|
|
|
|
// avoid issues when transcoding some files: https://trac.ffmpeg.org/ticket/6375
|
|
|
|
command.outputOption('-max_muxing_queue_size 1024')
|
|
|
|
// strip all metadata
|
|
|
|
.outputOption('-map_metadata -1')
|
|
|
|
// allows import of source material with incompatible pixel formats (e.g. MJPEG video)
|
|
|
|
.outputOption('-pix_fmt yuv420p')
|
|
|
|
}
|
|
|
|
|
|
|
|
function addDefaultEncoderParams (options: {
|
|
|
|
command: FfmpegCommand
|
|
|
|
encoder: 'libx264' | string
|
|
|
|
fps: number
|
|
|
|
|
|
|
|
streamNum?: number
|
|
|
|
}) {
|
|
|
|
const { command, encoder, fps, streamNum } = options
|
|
|
|
|
|
|
|
if (encoder === 'libx264') {
|
|
|
|
// 3.1 is the minimal resource allocation for our highest supported resolution
|
|
|
|
command.outputOption(buildStreamSuffix('-level:v', streamNum) + ' 3.1')
|
|
|
|
|
|
|
|
if (fps) {
|
|
|
|
// Keyframe interval of 2 seconds for faster seeking and resolution switching.
|
|
|
|
// https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
|
|
|
|
// https://superuser.com/a/908325
|
|
|
|
command.outputOption(buildStreamSuffix('-g:v', streamNum) + ' ' + (fps * 2))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
async function presetVOD (options: {
|
|
|
|
command: FfmpegCommand
|
|
|
|
input: string
|
|
|
|
|
|
|
|
availableEncoders: AvailableEncoders
|
|
|
|
profile: string
|
|
|
|
|
|
|
|
canCopyAudio: boolean
|
|
|
|
canCopyVideo: boolean
|
|
|
|
|
|
|
|
resolution: number
|
|
|
|
fps: number
|
|
|
|
|
|
|
|
scaleFilterValue?: string
|
|
|
|
}) {
|
|
|
|
const { command, input, profile, resolution, fps, scaleFilterValue } = options
|
|
|
|
|
|
|
|
let localCommand = command
|
|
|
|
.format('mp4')
|
|
|
|
.outputOption('-movflags faststart')
|
|
|
|
|
|
|
|
addDefaultEncoderGlobalParams(command)
|
|
|
|
|
|
|
|
const probe = await ffprobePromise(input)
|
|
|
|
|
|
|
|
// Audio encoder
|
|
|
|
const bitrate = await getVideoStreamBitrate(input, probe)
|
|
|
|
const videoStreamDimensions = await getVideoStreamDimensionsInfo(input, probe)
|
|
|
|
|
|
|
|
let streamsToProcess: StreamType[] = [ 'audio', 'video' ]
|
|
|
|
|
|
|
|
if (!await hasAudioStream(input, probe)) {
|
|
|
|
localCommand = localCommand.noAudio()
|
|
|
|
streamsToProcess = [ 'video' ]
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const streamType of streamsToProcess) {
|
|
|
|
const builderResult = await getEncoderBuilderResult({
|
|
|
|
...pick(options, [ 'availableEncoders', 'canCopyAudio', 'canCopyVideo' ]),
|
|
|
|
|
|
|
|
input,
|
|
|
|
inputBitrate: bitrate,
|
|
|
|
inputRatio: videoStreamDimensions?.ratio || 0,
|
|
|
|
|
|
|
|
profile,
|
|
|
|
resolution,
|
|
|
|
fps,
|
|
|
|
streamType,
|
|
|
|
|
|
|
|
videoType: 'vod' as 'vod'
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!builderResult) {
|
|
|
|
throw new Error('No available encoder found for stream ' + streamType)
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.debug(
|
|
|
|
'Apply ffmpeg params from %s for %s stream of input %s using %s profile.',
|
|
|
|
builderResult.encoder, streamType, input, profile,
|
|
|
|
{ builderResult, resolution, fps, ...lTags() }
|
|
|
|
)
|
|
|
|
|
|
|
|
if (streamType === 'video') {
|
|
|
|
localCommand.videoCodec(builderResult.encoder)
|
|
|
|
|
|
|
|
if (scaleFilterValue) {
|
|
|
|
localCommand.outputOption(`-vf ${getScaleFilter(builderResult.result)}=${scaleFilterValue}`)
|
|
|
|
}
|
|
|
|
} else if (streamType === 'audio') {
|
|
|
|
localCommand.audioCodec(builderResult.encoder)
|
|
|
|
}
|
|
|
|
|
|
|
|
applyEncoderOptions(localCommand, builderResult.result)
|
|
|
|
addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps })
|
|
|
|
}
|
|
|
|
|
|
|
|
return localCommand
|
|
|
|
}
|
|
|
|
|
|
|
|
function presetCopy (command: FfmpegCommand): FfmpegCommand {
|
|
|
|
return command
|
|
|
|
.format('mp4')
|
|
|
|
.videoCodec('copy')
|
|
|
|
.audioCodec('copy')
|
|
|
|
}
|
|
|
|
|
|
|
|
function presetOnlyAudio (command: FfmpegCommand): FfmpegCommand {
|
|
|
|
return command
|
|
|
|
.format('mp4')
|
|
|
|
.audioCodec('copy')
|
|
|
|
.noVideo()
|
|
|
|
}
|
|
|
|
|
|
|
|
function applyEncoderOptions (command: FfmpegCommand, options: EncoderOptions): FfmpegCommand {
|
|
|
|
return command
|
|
|
|
.inputOptions(options.inputOptions ?? [])
|
|
|
|
.outputOptions(options.outputOptions ?? [])
|
|
|
|
}
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
export {
|
|
|
|
presetVOD,
|
|
|
|
presetCopy,
|
|
|
|
presetOnlyAudio,
|
|
|
|
|
|
|
|
addDefaultEncoderGlobalParams,
|
|
|
|
addDefaultEncoderParams,
|
|
|
|
|
|
|
|
applyEncoderOptions
|
|
|
|
}
|