From 0c9668f77901e7540e2c7045eb0f2974a4842a69 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 21 Apr 2023 14:55:10 +0200 Subject: Implement remote runner jobs in server Move ffmpeg functions to @shared --- shared/ffmpeg/ffmpeg-live.ts | 184 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 shared/ffmpeg/ffmpeg-live.ts (limited to 'shared/ffmpeg/ffmpeg-live.ts') diff --git a/shared/ffmpeg/ffmpeg-live.ts b/shared/ffmpeg/ffmpeg-live.ts new file mode 100644 index 000000000..cca4c6474 --- /dev/null +++ b/shared/ffmpeg/ffmpeg-live.ts @@ -0,0 +1,184 @@ +import { FilterSpecification } from 'fluent-ffmpeg' +import { join } from 'path' +import { pick } from '@shared/core-utils' +import { FFmpegCommandWrapper, FFmpegCommandWrapperOptions } from './ffmpeg-command-wrapper' +import { buildStreamSuffix, getScaleFilter, StreamType } from './ffmpeg-utils' +import { addDefaultEncoderGlobalParams, addDefaultEncoderParams, applyEncoderOptions } from './shared' + +export class FFmpegLive { + private readonly commandWrapper: FFmpegCommandWrapper + + constructor (options: FFmpegCommandWrapperOptions) { + this.commandWrapper = new FFmpegCommandWrapper(options) + } + + async getLiveTranscodingCommand (options: { + inputUrl: string + + outPath: string + masterPlaylistName: string + + toTranscode: { + resolution: number + fps: number + }[] + + // Input information + bitrate: number + ratio: number + hasAudio: boolean + + segmentListSize: number + segmentDuration: number + }) { + const { + inputUrl, + outPath, + toTranscode, + bitrate, + masterPlaylistName, + ratio, + hasAudio + } = options + const command = this.commandWrapper.buildCommand(inputUrl) + + const varStreamMap: string[] = [] + + const complexFilter: FilterSpecification[] = [ + { + inputs: '[v:0]', + filter: 'split', + options: toTranscode.length, + outputs: toTranscode.map(t => `vtemp${t.resolution}`) + } + ] + + command.outputOption('-sc_threshold 0') + + addDefaultEncoderGlobalParams(command) + + for (let i = 0; i < toTranscode.length; i++) { + const streamMap: string[] = [] + const { resolution, fps } = toTranscode[i] + + const baseEncoderBuilderParams = { + input: inputUrl, + + canCopyAudio: true, + canCopyVideo: true, + + inputBitrate: bitrate, + inputRatio: ratio, + + resolution, + fps, + + streamNum: i, + videoType: 'live' as 'live' + } + + { + const streamType: StreamType = 'video' + const builderResult = await this.commandWrapper.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, streamNum: i }) + + this.commandWrapper.debugLog( + `Apply ffmpeg live video params from ${builderResult.encoder} using ${this.commandWrapper.getProfile()} profile.`, + { builderResult, fps, toTranscode } + ) + + 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 this.commandWrapper.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, streamNum: i }) + + this.commandWrapper.debugLog( + `Apply ffmpeg live audio params from ${builderResult.encoder} using ${this.commandWrapper.getProfile()} profile.`, + { builderResult, fps, resolution } + ) + + command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`) + applyEncoderOptions(command, builderResult.result) + + streamMap.push(`a:${i}`) + } + + varStreamMap.push(streamMap.join(',')) + } + + command.complexFilter(complexFilter) + + this.addDefaultLiveHLSParams({ ...pick(options, [ 'segmentDuration', 'segmentListSize' ]), outPath, masterPlaylistName }) + + command.outputOption('-var_stream_map', varStreamMap.join(' ')) + + return command + } + + getLiveMuxingCommand (options: { + inputUrl: string + outPath: string + masterPlaylistName: string + + segmentListSize: number + segmentDuration: number + }) { + const { inputUrl, outPath, masterPlaylistName } = options + + const command = this.commandWrapper.buildCommand(inputUrl) + + command.outputOption('-c:v copy') + command.outputOption('-c:a copy') + command.outputOption('-map 0:a?') + command.outputOption('-map 0:v?') + + this.addDefaultLiveHLSParams({ ...pick(options, [ 'segmentDuration', 'segmentListSize' ]), outPath, masterPlaylistName }) + + return command + } + + private addDefaultLiveHLSParams (options: { + outPath: string + masterPlaylistName: string + segmentListSize: number + segmentDuration: number + }) { + const { outPath, masterPlaylistName, segmentListSize, segmentDuration } = options + + const command = this.commandWrapper.getCommand() + + command.outputOption('-hls_time ' + segmentDuration) + command.outputOption('-hls_list_size ' + segmentListSize) + 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')) + } +} -- cgit v1.2.3