From 529b37527cff5203a0689a15ce73dcee6e1eece2 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 28 Jan 2021 09:37:26 +0100 Subject: [PATCH] Use a profile manager for transcoding --- scripts/print-transcode-command.ts | 4 +- server/helpers/ffmpeg-utils.ts | 51 ++++++++++---- server/initializers/constants.ts | 12 ---- server/lib/live-manager.ts | 4 +- server/lib/video-transcoding-profiles.ts | 85 +++++++++++++++++++----- server/lib/video-transcoding.ts | 12 ++-- 6 files changed, 116 insertions(+), 52 deletions(-) diff --git a/scripts/print-transcode-command.ts b/scripts/print-transcode-command.ts index b75b711a4..f3b169b32 100644 --- a/scripts/print-transcode-command.ts +++ b/scripts/print-transcode-command.ts @@ -3,9 +3,9 @@ registerTSPaths() import * as program from 'commander' import * as ffmpeg from 'fluent-ffmpeg' -import { availableEncoders } from '@server/lib/video-transcoding-profiles' import { buildx264VODCommand, runCommand, TranscodeOptions } from '@server/helpers/ffmpeg-utils' import { exit } from 'process' +import { VideoTranscodingProfilesManager } from '@server/lib/video-transcoding-profiles' program .arguments('') @@ -31,7 +31,7 @@ async function run (path: string, cmd: any) { inputPath: path, outputPath: '/dev/null', - availableEncoders, + availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), profile: 'default', resolution: +cmd.resolution, diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index f85b9f316..7d46130ec 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts @@ -2,7 +2,7 @@ import { Job } from 'bull' import * as ffmpeg from 'fluent-ffmpeg' import { readFile, remove, writeFile } from 'fs-extra' import { dirname, join } from 'path' -import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS } from '@server/initializers/constants' +import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' import { VideoResolution } from '../../shared/models/videos' import { checkFFmpegEncoders } from '../initializers/checker-before-init' import { CONFIG } from '../initializers/config' @@ -46,11 +46,22 @@ export interface EncoderProfile { } export type AvailableEncoders = { - [ id in 'live' | 'vod' ]: { - [ encoder in 'libx264' | 'aac' | 'libfdk_aac' ]?: EncoderProfile + live: { + [ encoder: string ]: EncoderProfile + } + + vod: { + [ encoder: string ]: EncoderProfile + } + + encodersToTry: { + video: string[] + audio: string[] } } +type StreamType = 'audio' | 'video' + // --------------------------------------------------------------------------- // Image manipulation // --------------------------------------------------------------------------- @@ -243,8 +254,10 @@ async function getLiveTranscodingCommand (options: { const baseEncoderBuilderParams = { input, + availableEncoders, profile, + fps: resolutionFPS, resolution, streamNum: i, @@ -252,7 +265,8 @@ async function getLiveTranscodingCommand (options: { } { - const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'VIDEO' })) + const streamType: StreamType = 'video' + const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType })) if (!builderResult) { throw new Error('No available live video encoder found') } @@ -268,7 +282,8 @@ async function getLiveTranscodingCommand (options: { } { - const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'AUDIO' })) + const streamType: StreamType = 'audio' + const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType })) if (!builderResult) { throw new Error('No available live audio encoder found') } @@ -480,8 +495,11 @@ function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptio // Transcoding presets // --------------------------------------------------------------------------- +// Run encoder builder depending on available encoders +// Try encoders by priority: if the encoder is available, run the chosen profile or fallback to the default one +// If the default one does not exist, check the next encoder async function getEncoderBuilderResult (options: { - streamType: string + streamType: 'video' | 'audio' input: string availableEncoders: AvailableEncoders @@ -495,17 +513,24 @@ async function getEncoderBuilderResult (options: { }) { const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options - const encodersToTry: string[] = VIDEO_TRANSCODING_ENCODERS[streamType] + const encodersToTry = availableEncoders.encodersToTry[streamType] + const encoders = availableEncoders[videoType] for (const encoder of encodersToTry) { - if (!(await checkFFmpegEncoders()).get(encoder) || !availableEncoders[videoType][encoder]) continue + if (!(await checkFFmpegEncoders()).get(encoder) || !encoders[encoder]) continue - const builderProfiles: EncoderProfile = availableEncoders[videoType][encoder] + // An object containing available profiles for this encoder + const builderProfiles: EncoderProfile = encoders[encoder] let builder = builderProfiles[profile] if (!builder) { logger.debug('Profile %s for encoder %s not available. Fallback to default.', profile, encoder) builder = builderProfiles.default + + if (!builder) { + logger.debug('Default profile for encoder %s not available. Try next available encoder.', encoder) + continue + } } const result = await builder({ input, resolution: resolution, fps, streamNum }) @@ -538,11 +563,11 @@ async function presetVideo ( // Audio encoder const parsedAudio = await getAudioStream(input) - let streamsToProcess = [ 'AUDIO', 'VIDEO' ] + let streamsToProcess: StreamType[] = [ 'audio', 'video' ] if (!parsedAudio.audioStream) { localCommand = localCommand.noAudio() - streamsToProcess = [ 'VIDEO' ] + streamsToProcess = [ 'audio' ] } for (const streamType of streamsToProcess) { @@ -564,9 +589,9 @@ async function presetVideo ( logger.debug('Apply ffmpeg params from %s.', builderResult.encoder, builderResult) - if (streamType === 'VIDEO') { + if (streamType === 'video') { localCommand.videoCodec(builderResult.encoder) - } else if (streamType === 'AUDIO') { + } else if (streamType === 'audio') { localCommand.audioCodec(builderResult.encoder) } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 89491708e..0fab872a9 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -341,17 +341,6 @@ const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = { KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum) } -const VIDEO_TRANSCODING_ENCODERS = { - VIDEO: [ 'libx264' ], - - // Try the first one, if not available try the second one etc - AUDIO: [ - // we favor VBR, if a good AAC encoder is available - 'libfdk_aac', - 'aac' - ] -} - const DEFAULT_AUDIO_RESOLUTION = VideoResolution.H_480P const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = { @@ -828,7 +817,6 @@ export { ACTOR_FOLLOW_SCORE, PREVIEWS_SIZE, REMOTE_SCHEME, - VIDEO_TRANSCODING_ENCODERS, FOLLOW_STATES, DEFAULT_USER_THEME_NAME, SERVER_ACTOR_NAME, diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts index d968f05da..c8e5bcb77 100644 --- a/server/lib/live-manager.ts +++ b/server/lib/live-manager.ts @@ -25,7 +25,7 @@ import { cleanupLive } from './job-queue/handlers/video-live-ending' import { PeerTubeSocket } from './peertube-socket' import { isAbleToUploadVideo } from './user' import { getHLSDirectory } from './video-paths' -import { availableEncoders } from './video-transcoding-profiles' +import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' import memoizee = require('memoizee') const NodeRtmpSession = require('node-media-server/node_rtmp_session') @@ -337,7 +337,7 @@ class LiveManager { outPath, resolutions: allResolutions, fps, - availableEncoders, + availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), profile: 'default' }) : getLiveMuxingCommand(rtmpUrl, outPath) diff --git a/server/lib/video-transcoding-profiles.ts b/server/lib/video-transcoding-profiles.ts index 338f4de4a..bbe556e75 100644 --- a/server/lib/video-transcoding-profiles.ts +++ b/server/lib/video-transcoding-profiles.ts @@ -78,32 +78,83 @@ const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum }) return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] } } -const availableEncoders: AvailableEncoders = { - vod: { - libx264: { - default: defaultX264VODOptionsBuilder +// Used to get and update available encoders +class VideoTranscodingProfilesManager { + private static instance: VideoTranscodingProfilesManager + + // 1 === less priority + private readonly encodersPriorities = { + video: [ + { name: 'libx264', priority: 100 } + ], + + // Try the first one, if not available try the second one etc + audio: [ + // we favor VBR, if a good AAC encoder is available + { name: 'libfdk_aac', priority: 200 }, + { name: 'aac', priority: 100 } + ] + } + + private readonly availableEncoders = { + vod: { + libx264: { + default: defaultX264VODOptionsBuilder + }, + aac: { + default: defaultAACOptionsBuilder + }, + libfdk_aac: { + default: defaultLibFDKAACVODOptionsBuilder + } }, - aac: { - default: defaultAACOptionsBuilder - }, - libfdk_aac: { - default: defaultLibFDKAACVODOptionsBuilder + live: { + libx264: { + default: defaultX264LiveOptionsBuilder + }, + aac: { + default: defaultAACOptionsBuilder + } } - }, - live: { - libx264: { - default: defaultX264LiveOptionsBuilder - }, - aac: { - default: defaultAACOptionsBuilder + } + + private constructor () { + + } + + getAvailableEncoders (): AvailableEncoders { + const encodersToTry = { + video: this.getEncodersByPriority('video'), + audio: this.getEncodersByPriority('audio') } + + return Object.assign({}, this.availableEncoders, { encodersToTry }) + } + + getAvailableProfiles (type: 'vod' | 'live') { + return this.availableEncoders[type] + } + + private getEncodersByPriority (type: 'video' | 'audio') { + return this.encodersPriorities[type] + .sort((e1, e2) => { + if (e1.priority > e2.priority) return -1 + else if (e1.priority === e2.priority) return 0 + + return 1 + }) + .map(e => e.name) + } + + static get Instance () { + return this.instance || (this.instance = new this()) } } // --------------------------------------------------------------------------- export { - availableEncoders + VideoTranscodingProfilesManager } // --------------------------------------------------------------------------- diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index 7af7a481c..c4b3425d1 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts @@ -14,7 +14,7 @@ import { VideoFileModel } from '../models/video/video-file' import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls' import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths' -import { availableEncoders } from './video-transcoding-profiles' +import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' /** * @@ -41,7 +41,7 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile: inputPath: videoInputPath, outputPath: videoTranscodedPath, - availableEncoders, + availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), profile: 'default', resolution: inputVideoFile.resolution, @@ -95,7 +95,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti inputPath: videoInputPath, outputPath: videoTranscodedPath, - availableEncoders, + availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), profile: 'default', resolution, @@ -107,7 +107,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti inputPath: videoInputPath, outputPath: videoTranscodedPath, - availableEncoders, + availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), profile: 'default', resolution, @@ -142,7 +142,7 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video inputPath: tmpPreviewPath, outputPath: videoTranscodedPath, - availableEncoders, + availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), profile: 'default', audioPath: audioInputPath, @@ -283,7 +283,7 @@ async function generateHlsPlaylistCommon (options: { inputPath, outputPath, - availableEncoders, + availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), profile: 'default', resolution,