From c729caf6cc34630877a0e5a1bda1719384cd0c8a Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 11 Feb 2022 10:51:33 +0100 Subject: Add basic video editor support --- server/helpers/ffmpeg/ffmpeg-live.ts | 161 +++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 server/helpers/ffmpeg/ffmpeg-live.ts (limited to 'server/helpers/ffmpeg/ffmpeg-live.ts') diff --git a/server/helpers/ffmpeg/ffmpeg-live.ts b/server/helpers/ffmpeg/ffmpeg-live.ts new file mode 100644 index 000000000..ff571626c --- /dev/null +++ b/server/helpers/ffmpeg/ffmpeg-live.ts @@ -0,0 +1,161 @@ +import { FfmpegCommand, FilterSpecification } from 'fluent-ffmpeg' +import { join } from 'path' +import { VIDEO_LIVE } from '@server/initializers/constants' +import { AvailableEncoders } 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 + + resolutions: number[] + + // Input information + fps: number + bitrate: number + ratio: number + + availableEncoders: AvailableEncoders + profile: string +}) { + const { inputUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName, ratio } = 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 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}` + }) + } + + { + 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) + } + + varStreamMap.push(`v:${i},a:${i}`) + } + + command.complexFilter(complexFilter) + + addDefaultLiveHLSParams(command, outPath, masterPlaylistName) + + command.outputOption('-var_stream_map', varStreamMap.join(' ')) + + return command +} + +function getLiveMuxingCommand (inputUrl: string, outPath: string, masterPlaylistName: string) { + 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) + + return command +} + +// --------------------------------------------------------------------------- + +export { + getLiveTranscodingCommand, + getLiveMuxingCommand +} + +// --------------------------------------------------------------------------- + +function addDefaultLiveHLSParams (command: FfmpegCommand, outPath: string, masterPlaylistName: string) { + command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS) + command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE) + command.outputOption('-hls_flags delete_segments+independent_segments') + 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')) +} -- cgit v1.2.3