X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fhelpers%2Fffmpeg-utils.ts;h=bf6408d3e1c63b116c239b779b0622f47209766e;hb=040d6896a3cd5622e78cccdedd8cce2afcf49a31;hp=3cc062b8cd37acc7936749d7633ba9b96a3aa23b;hpb=884d2c39ae23b44d0d037aaff0f66ad9ae0807ba;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 3cc062b8c..bf6408d3e 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts @@ -110,7 +110,7 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima // 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 @@ -134,6 +134,16 @@ interface HLSTranscodeOptions extends BaseTranscodeOptions { } } +interface HLSFromTSTranscodeOptions extends BaseTranscodeOptions { + type: 'hls-from-ts' + + isAAC: boolean + + hlsPlaylist: { + videoFilename: string + } +} + interface QuickTranscodeOptions extends BaseTranscodeOptions { type: 'quick-transcode' } @@ -153,6 +163,7 @@ interface OnlyAudioTranscodeOptions extends BaseTranscodeOptions { type TranscodeOptions = HLSTranscodeOptions + | HLSFromTSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | OnlyAudioTranscodeOptions @@ -163,6 +174,7 @@ const builders: { } = { 'quick-transcode': buildQuickTranscodeCommand, 'hls': buildHLSVODCommand, + 'hls-from-ts': buildHLSVODFromTSCommand, 'merge-audio': buildAudioMergeCommand, 'only-audio': buildOnlyAudioCommand, 'video': buildx264VODCommand @@ -171,7 +183,7 @@ const builders: { 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) @@ -190,16 +202,14 @@ async function getLiveTranscodingCommand (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[] = [] @@ -220,6 +230,9 @@ async function getLiveTranscodingCommand (options: { ]) command.outputOption('-preset superfast') + command.outputOption('-sc_threshold 0') + + addDefaultEncoderGlobalParams({ command }) for (let i = 0; i < resolutions.length; i++) { const resolution = resolutions[i] @@ -270,52 +283,26 @@ async function getLiveTranscodingCommand (options: { 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}` @@ -335,8 +322,7 @@ export { generateImageFromVideoFile, TranscodeOptions, TranscodeOptionsType, - transcode, - hlsPlaylistToFragmentedMP4 + transcode } // --------------------------------------------------------------------------- @@ -345,6 +331,23 @@ export { // 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 @@ -355,35 +358,21 @@ function addDefaultEncoderParams (options: { 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`) @@ -443,6 +432,16 @@ function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { 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) @@ -450,19 +449,29 @@ async function buildHLSVODCommand (command: ffmpeg.FfmpegCommand, options: HLSTr 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) @@ -476,7 +485,7 @@ async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) { await writeFile(options.outputPath, newContent) } -function getHLSVideoPath (options: HLSTranscodeOptions) { +function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) { return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` } @@ -537,6 +546,8 @@ async function presetVideo ( .format('mp4') .outputOption('-movflags faststart') + addDefaultEncoderGlobalParams({ command }) + // Audio encoder const parsedAudio = await getAudioStream(input) @@ -597,13 +608,17 @@ function presetOnlyAudio (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand { // 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