// Transcode meta function
// ---------------------------------------------------------------------------
-type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio'
+type TranscodeOptionsType = 'hls' | 'hls-from-ts' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio'
interface BaseTranscodeOptions {
type: TranscodeOptionsType
}
}
+interface HLSFromTSTranscodeOptions extends BaseTranscodeOptions {
+ type: 'hls-from-ts'
+
+ isAAC: boolean
+
+ hlsPlaylist: {
+ videoFilename: string
+ }
+}
+
interface QuickTranscodeOptions extends BaseTranscodeOptions {
type: 'quick-transcode'
}
type TranscodeOptions =
HLSTranscodeOptions
+ | HLSFromTSTranscodeOptions
| VideoTranscodeOptions
| MergeAudioTranscodeOptions
| OnlyAudioTranscodeOptions
} = {
'quick-transcode': buildQuickTranscodeCommand,
'hls': buildHLSVODCommand,
+ 'hls-from-ts': buildHLSVODFromTSCommand,
'merge-audio': buildAudioMergeCommand,
'only-audio': buildOnlyAudioCommand,
'video': buildx264VODCommand
async function transcode (options: TranscodeOptions) {
logger.debug('Will run transcode.', { options })
- let command = getFFmpeg(options.inputPath)
+ let command = getFFmpeg(options.inputPath, 'vod')
.output(options.outputPath)
command = await builders[options.type](command, options)
outPath: string
resolutions: number[]
fps: number
- deleteSegments: boolean
availableEncoders: AvailableEncoders
profile: string
}) {
- const { rtmpUrl, outPath, resolutions, fps, deleteSegments, availableEncoders, profile } = options
+ const { rtmpUrl, outPath, resolutions, fps, availableEncoders, profile } = options
const input = rtmpUrl
- const command = getFFmpeg(input)
- command.inputOption('-fflags nobuffer')
+ const command = getFFmpeg(input, 'live')
const varStreamMap: string[] = []
])
command.outputOption('-preset superfast')
+ command.outputOption('-sc_threshold 0')
+
+ addDefaultEncoderGlobalParams({ command })
for (let i = 0; i < resolutions.length; i++) {
const resolution = resolutions[i]
varStreamMap.push(`v:${i},a:${i}`)
}
- addDefaultLiveHLSParams(command, outPath, deleteSegments)
+ addDefaultLiveHLSParams(command, outPath)
command.outputOption('-var_stream_map', varStreamMap.join(' '))
return command
}
-function getLiveMuxingCommand (rtmpUrl: string, outPath: string, deleteSegments: boolean) {
- const command = getFFmpeg(rtmpUrl)
- command.inputOption('-fflags nobuffer')
+function getLiveMuxingCommand (rtmpUrl: string, outPath: string) {
+ const command = getFFmpeg(rtmpUrl, 'live')
command.outputOption('-c:v copy')
command.outputOption('-c:a copy')
command.outputOption('-map 0:a?')
command.outputOption('-map 0:v?')
- addDefaultLiveHLSParams(command, outPath, deleteSegments)
+ addDefaultLiveHLSParams(command, outPath)
return command
}
-async function hlsPlaylistToFragmentedMP4 (hlsDirectory: string, segmentFiles: string[], outputPath: string) {
- const concatFilePath = join(hlsDirectory, 'concat.txt')
-
- function cleaner () {
- remove(concatFilePath)
- .catch(err => logger.error('Cannot remove concat file in %s.', hlsDirectory, { err }))
- }
-
- // First concat the ts files to a mp4 file
- const content = segmentFiles.map(f => 'file ' + f)
- .join('\n')
-
- await writeFile(concatFilePath, content + '\n')
-
- const command = getFFmpeg(concatFilePath)
- command.inputOption('-safe 0')
- command.inputOption('-f concat')
-
- command.outputOption('-c:v copy')
- command.audioFilter('aresample=async=1:first_pts=0')
- command.output(outputPath)
-
- return runCommand(command, cleaner)
-}
-
function buildStreamSuffix (base: string, streamNum?: number) {
if (streamNum !== undefined) {
return `${base}:${streamNum}`
generateImageFromVideoFile,
TranscodeOptions,
TranscodeOptionsType,
- transcode,
- hlsPlaylistToFragmentedMP4
+ transcode
}
// ---------------------------------------------------------------------------
// Default options
// ---------------------------------------------------------------------------
+function addDefaultEncoderGlobalParams (options: {
+ command: ffmpeg.FfmpegCommand
+}) {
+ const { command } = options
+
+ // 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')
+ // NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it
+ .outputOption('-b_strategy 1')
+ // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
+ .outputOption('-bf 16')
+ // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
+ .outputOption('-pix_fmt yuv420p')
+}
+
function addDefaultEncoderParams (options: {
command: ffmpeg.FfmpegCommand
encoder: 'libx264' | string
if (encoder === 'libx264') {
// 3.1 is the minimal resource allocation for our highest supported resolution
- command.outputOption('-level 3.1')
- // NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it
- .outputOption('-b_strategy 1')
- // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
- .outputOption('-bf 16')
- // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
- .outputOption(buildStreamSuffix('-pix_fmt', streamNum) + ' yuv420p')
- // strip all metadata
- .outputOption('-map_metadata -1')
- // avoid issues when transcoding some files: https://trac.ffmpeg.org/ticket/6375
- .outputOption(buildStreamSuffix('-max_muxing_queue_size', streamNum) + ' 1024')
+ 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('-g ' + (fps * 2))
+ command.outputOption(buildStreamSuffix('-g:v', streamNum) + ' ' + (fps * 2))
}
}
}
-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_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE)
-
- if (deleteSegments === true) {
- command.outputOption('-hls_flags delete_segments')
- }
-
+ command.outputOption('-hls_flags delete_segments+independent_segments')
command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`)
command.outputOption('-master_pl_name master.m3u8')
command.outputOption(`-f hls`)
return command
}
+function addCommonHLSVODCommandOptions (command: ffmpeg.FfmpegCommand, outputPath: string) {
+ return command.outputOption('-hls_time 4')
+ .outputOption('-hls_list_size 0')
+ .outputOption('-hls_playlist_type vod')
+ .outputOption('-hls_segment_filename ' + outputPath)
+ .outputOption('-hls_segment_type fmp4')
+ .outputOption('-f hls')
+ .outputOption('-hls_flags single_file')
+}
+
async function buildHLSVODCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) {
const videoPath = getHLSVideoPath(options)
else if (options.resolution === VideoResolution.H_NOVIDEO) command = presetOnlyAudio(command)
else command = await buildx264VODCommand(command, options)
- command = command.outputOption('-hls_time 4')
- .outputOption('-hls_list_size 0')
- .outputOption('-hls_playlist_type vod')
- .outputOption('-hls_segment_filename ' + videoPath)
- .outputOption('-hls_segment_type fmp4')
- .outputOption('-f hls')
- .outputOption('-hls_flags single_file')
+ addCommonHLSVODCommandOptions(command, videoPath)
+
+ return command
+}
+
+async function buildHLSVODFromTSCommand (command: ffmpeg.FfmpegCommand, options: HLSFromTSTranscodeOptions) {
+ const videoPath = getHLSVideoPath(options)
+
+ command.outputOption('-c copy')
+
+ if (options.isAAC) {
+ // Required for example when copying an AAC stream from an MPEG-TS
+ // Since it's a bitstream filter, we don't need to reencode the audio
+ command.outputOption('-bsf:a aac_adtstoasc')
+ }
+
+ addCommonHLSVODCommandOptions(command, videoPath)
return command
}
async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) {
- if (options.type !== 'hls') return
+ if (options.type !== 'hls' && options.type !== 'hls-from-ts') return
const fileContent = await readFile(options.outputPath)
await writeFile(options.outputPath, newContent)
}
-function getHLSVideoPath (options: HLSTranscodeOptions) {
+function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) {
return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
}
.format('mp4')
.outputOption('-movflags faststart')
+ addDefaultEncoderGlobalParams({ command })
+
// Audio encoder
const parsedAudio = await getAudioStream(input)
// Utils
// ---------------------------------------------------------------------------
-function getFFmpeg (input: string) {
+function getFFmpeg (input: string, type: 'live' | 'vod') {
// We set cwd explicitly because ffmpeg appears to create temporary files when trancoding which fails in read-only file systems
const command = ffmpeg(input, { niceness: FFMPEG_NICE.TRANSCODING, cwd: CONFIG.STORAGE.TMP_DIR })
- if (CONFIG.TRANSCODING.THREADS > 0) {
+ const threads = type === 'live'
+ ? CONFIG.LIVE.TRANSCODING.THREADS
+ : CONFIG.TRANSCODING.THREADS
+
+ if (threads > 0) {
// If we don't set any threads ffmpeg will chose automatically
- command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
+ command.outputOption('-threads ' + threads)
}
return command