From 5a547f69d5dc5867e253f7721513479c754b4f15 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 24 Nov 2020 14:08:23 +0100 Subject: [PATCH] Support encoding profiles --- scripts/dev/cli.sh | 2 +- server/helpers/ffmpeg-utils.ts | 221 +++++++++++++----- server/helpers/ffprobe-utils.ts | 19 +- server/lib/live-manager.ts | 12 +- server/lib/video-transcoding-profiles.ts | 95 ++++++++ server/lib/video-transcoding.ts | 91 +------- server/tools/test.ts | 105 +++++++++ shared/extra-utils/server/servers.ts | 12 +- shared/models/videos/video-resolution.enum.ts | 2 +- 9 files changed, 402 insertions(+), 157 deletions(-) create mode 100644 server/lib/video-transcoding-profiles.ts create mode 100644 server/tools/test.ts diff --git a/scripts/dev/cli.sh b/scripts/dev/cli.sh index dc6f0af0d..4bf4808b8 100755 --- a/scripts/dev/cli.sh +++ b/scripts/dev/cli.sh @@ -12,4 +12,4 @@ rm -rf ./dist/server/tools/ mkdir -p "./dist/server/tools" cp -r "./server/tools/node_modules" "./dist/server/tools" -npm run tsc -- --watch --project ./server/tools/tsconfig.json +npm run tsc -- --watch --sourceMap --project ./server/tools/tsconfig.json diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index e297108df..712ec757e 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts @@ -1,10 +1,10 @@ import * as ffmpeg from 'fluent-ffmpeg' import { readFile, remove, writeFile } from 'fs-extra' import { dirname, join } from 'path' -import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' +import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants' +import { VideoResolution } from '../../shared/models/videos' import { checkFFmpegEncoders } from '../initializers/checker-before-init' import { CONFIG } from '../initializers/config' -import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS, VIDEO_TRANSCODING_FPS } from '../initializers/constants' import { getAudioStream, getClosestFramerateStandard, getVideoFileFPS } from './ffprobe-utils' import { processImage } from './image-utils' import { logger } from './logger' @@ -19,11 +19,13 @@ export type EncoderOptionsBuilder = (params: { input: string resolution: VideoResolution fps?: number + streamNum?: number }) => Promise | EncoderOptions // Options types export interface EncoderOptions { + copy?: boolean outputOptions: string[] } @@ -37,7 +39,7 @@ export interface EncoderProfile { export type AvailableEncoders = { [ id in 'live' | 'vod' ]: { - [ encoder in 'libx264' | 'aac' | 'libfdkAAC' ]: EncoderProfile + [ encoder in 'libx264' | 'aac' | 'libfdk_aac' ]?: EncoderProfile } } @@ -197,8 +199,20 @@ async function transcode (options: TranscodeOptions) { // Live muxing/transcoding functions // --------------------------------------------------------------------------- -function getLiveTranscodingCommand (rtmpUrl: string, outPath: string, resolutions: number[], fps: number, deleteSegments: boolean) { - const command = getFFmpeg(rtmpUrl) +async function getLiveTranscodingCommand (options: { + rtmpUrl: string + outPath: string + resolutions: number[] + fps: number + deleteSegments: boolean + + availableEncoders: AvailableEncoders + profile: string +}) { + const { rtmpUrl, outPath, resolutions, fps, deleteSegments, availableEncoders, profile } = options + const input = rtmpUrl + + const command = getFFmpeg(input) command.inputOption('-fflags nobuffer') const varStreamMap: string[] = [] @@ -219,19 +233,43 @@ function getLiveTranscodingCommand (rtmpUrl: string, outPath: string, resolution })) ]) - addEncoderDefaultParams(command, 'libx264', fps) - command.outputOption('-preset superfast') for (let i = 0; i < resolutions.length; i++) { const resolution = resolutions[i] + const baseEncoderBuilderParams = { input, availableEncoders, profile, fps, resolution, streamNum: i, videoType: 'live' as 'live' } - command.outputOption(`-map [vout${resolution}]`) - command.outputOption(`-c:v:${i} libx264`) - command.outputOption(`-b:v:${i} ${getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)}`) + { + const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'VIDEO' })) + if (!builderResult) { + throw new Error('No available live video encoder found') + } - command.outputOption(`-map a:0`) - command.outputOption(`-c:a:${i} aac`) + command.outputOption(`-map [vout${resolution}]`) + + addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps, streamNum: i }) + + logger.debug('Apply ffmpeg live video params from %s.', builderResult.encoder, builderResult) + + command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`) + command.addOutputOptions(builderResult.result.outputOptions) + } + + { + const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'AUDIO' })) + if (!builderResult) { + throw new Error('No available live audio encoder found') + } + + command.outputOption('-map a:0') + + addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps, streamNum: i }) + + logger.debug('Apply ffmpeg live audio params from %s.', builderResult.encoder, builderResult) + + command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`) + command.addOutputOptions(builderResult.result.outputOptions) + } varStreamMap.push(`v:${i},a:${i}`) } @@ -282,11 +320,20 @@ async function hlsPlaylistToFragmentedMP4 (hlsDirectory: string, segmentFiles: s return runCommand(command, cleaner) } +function buildStreamSuffix (base: string, streamNum?: number) { + if (streamNum !== undefined) { + return `${base}:${streamNum}` + } + + return base +} + // --------------------------------------------------------------------------- export { getLiveTranscodingCommand, getLiveMuxingCommand, + buildStreamSuffix, convertWebPToJPG, processGIF, generateImageFromVideoFile, @@ -302,19 +349,35 @@ export { // Default options // --------------------------------------------------------------------------- -function addEncoderDefaultParams (command: ffmpeg.FfmpegCommand, encoder: 'libx264' | string, fps?: number) { - if (encoder !== 'libx264') return +function addDefaultEncoderParams (options: { + command: ffmpeg.FfmpegCommand + encoder: 'libx264' | string + streamNum?: number + fps?: number +}) { + const { command, encoder, fps, streamNum } = options - command.outputOption('-level 3.1') // 3.1 is the minimal resource allocation for our highest supported resolution - .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it - .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 - .outputOption('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video) - .outputOption('-map_metadata -1') // strip all metadata - .outputOption('-max_muxing_queue_size 1024') // avoid issues when transcoding some files: https://trac.ffmpeg.org/ticket/6375 - // 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 - .outputOption('-g ' + (fps * 2)) + 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') + + 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)) + } + } } function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string, deleteSegments: boolean) { @@ -352,17 +415,18 @@ async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: Tran if (options.resolution !== undefined) { // '?x720' or '720x?' for example - const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` + const size = options.isPortraitMode === true + ? `${options.resolution}x?` + : `?x${options.resolution}` + command = command.size(size) } - if (fps) { - // Hard FPS limits - if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD') - else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN + // Hard FPS limits + if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD') + else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN - command = command.withFPS(fps) - } + command = command.withFPS(fps) return command } @@ -445,6 +509,49 @@ function getHLSVideoPath (options: HLSTranscodeOptions) { // Transcoding presets // --------------------------------------------------------------------------- +async function getEncoderBuilderResult (options: { + streamType: string + input: string + + availableEncoders: AvailableEncoders + profile: string + + videoType: 'vod' | 'live' + + resolution: number + fps?: number + streamNum?: number +}) { + const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options + + const encodersToTry: string[] = VIDEO_TRANSCODING_ENCODERS[streamType] + + for (const encoder of encodersToTry) { + if (!(await checkFFmpegEncoders()).get(encoder) || !availableEncoders[videoType][encoder]) continue + + const builderProfiles: EncoderProfile = availableEncoders[videoType][encoder] + let builder = builderProfiles[profile] + + if (!builder) { + logger.debug('Profile %s for encoder %s not available. Fallback to default.', profile, encoder) + builder = builderProfiles.default + } + + const result = await builder({ input, resolution: resolution, fps, streamNum }) + + return { + result, + + // If we don't have output options, then copy the input stream + encoder: result.copy === true + ? 'copy' + : encoder + } + } + + return null +} + async function presetVideo ( command: ffmpeg.FfmpegCommand, input: string, @@ -459,49 +566,41 @@ async function presetVideo ( const parsedAudio = await getAudioStream(input) let streamsToProcess = [ 'AUDIO', 'VIDEO' ] - const streamsFound = { - AUDIO: '', - VIDEO: '' - } if (!parsedAudio.audioStream) { localCommand = localCommand.noAudio() streamsToProcess = [ 'VIDEO' ] } - for (const stream of streamsToProcess) { - const encodersToTry: string[] = VIDEO_TRANSCODING_ENCODERS[stream] + for (const streamType of streamsToProcess) { + const { profile, resolution, availableEncoders } = transcodeOptions - for (const encoder of encodersToTry) { - if (!(await checkFFmpegEncoders()).get(encoder)) continue + const builderResult = await getEncoderBuilderResult({ + streamType, + input, + resolution, + availableEncoders, + profile, + fps, + videoType: 'vod' as 'vod' + }) - const builderProfiles: EncoderProfile = transcodeOptions.availableEncoders.vod[encoder] - let builder = builderProfiles[transcodeOptions.profile] - - if (!builder) { - logger.debug('Profile %s for encoder %s not available. Fallback to default.', transcodeOptions.profile, encoder) - builder = builderProfiles.default - } - - const builderResult = await builder({ input, resolution: transcodeOptions.resolution, fps }) - - logger.debug('Apply ffmpeg params from %s.', encoder, builderResult) - - localCommand.outputOptions(builderResult.outputOptions) - - addEncoderDefaultParams(localCommand, encoder) - - streamsFound[stream] = encoder - break + if (!builderResult) { + throw new Error('No available encoder found for stream ' + streamType) } - if (!streamsFound[stream]) { - throw new Error('No available encoder found ' + encodersToTry.join(', ')) + logger.debug('Apply ffmpeg params from %s.', builderResult.encoder, builderResult) + + if (streamType === 'VIDEO') { + localCommand.videoCodec(builderResult.encoder) + } else if (streamType === 'AUDIO') { + localCommand.audioCodec(builderResult.encoder) } + + command.addOutputOptions(builderResult.result.outputOptions) + addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps }) } - localCommand.videoCodec(streamsFound.VIDEO) - return localCommand } diff --git a/server/helpers/ffprobe-utils.ts b/server/helpers/ffprobe-utils.ts index 6159d3963..5545ddbbf 100644 --- a/server/helpers/ffprobe-utils.ts +++ b/server/helpers/ffprobe-utils.ts @@ -198,9 +198,12 @@ function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod' async function canDoQuickTranscode (path: string): Promise { const probe = await ffprobePromise(path) - // NOTE: This could be optimized by running ffprobe only once (but it runs fast anyway) + return await canDoQuickVideoTranscode(path, probe) && + await canDoQuickAudioTranscode(path, probe) +} + +async function canDoQuickVideoTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise { const videoStream = await getVideoStreamFromFile(path, probe) - const parsedAudio = await getAudioStream(path, probe) const fps = await getVideoFileFPS(path, probe) const bitRate = await getVideoFileBitrate(path, probe) const resolution = await getVideoFileResolution(path, probe) @@ -212,6 +215,12 @@ async function canDoQuickTranscode (path: string): Promise { if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false + return true +} + +async function canDoQuickAudioTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise { + const parsedAudio = await getAudioStream(path, probe) + // check audio params (if audio stream exists) if (parsedAudio.audioStream) { if (parsedAudio.audioStream['codec_name'] !== 'aac') return false @@ -239,11 +248,15 @@ export { getVideoFileResolution, getMetadataFromFile, getMaxAudioBitrate, + getVideoStreamFromFile, getDurationFromVideoFile, getAudioStream, getVideoFileFPS, + ffprobePromise, getClosestFramerateStandard, computeResolutionsToTranscode, getVideoFileBitrate, - canDoQuickTranscode + canDoQuickTranscode, + canDoQuickVideoTranscode, + canDoQuickAudioTranscode } diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts index b9e5d4561..ee0e4de37 100644 --- a/server/lib/live-manager.ts +++ b/server/lib/live-manager.ts @@ -22,6 +22,7 @@ import { JobQueue } from './job-queue' import { PeerTubeSocket } from './peertube-socket' import { isAbleToUploadVideo } from './user' import { getHLSDirectory } from './video-paths' +import { availableEncoders } from './video-transcoding-profiles' import memoizee = require('memoizee') const NodeRtmpServer = require('node-media-server/node_rtmp_server') @@ -264,7 +265,16 @@ class LiveManager { const deleteSegments = videoLive.saveReplay === false const ffmpegExec = CONFIG.LIVE.TRANSCODING.ENABLED - ? getLiveTranscodingCommand(rtmpUrl, outPath, allResolutions, fps, deleteSegments) + ? await getLiveTranscodingCommand({ + rtmpUrl, + outPath, + resolutions: + allResolutions, + fps, + deleteSegments, + availableEncoders, + profile: 'default' + }) : getLiveMuxingCommand(rtmpUrl, outPath, deleteSegments) logger.info('Running live muxing/transcoding for %s.', videoUUID) diff --git a/server/lib/video-transcoding-profiles.ts b/server/lib/video-transcoding-profiles.ts new file mode 100644 index 000000000..12e22a19d --- /dev/null +++ b/server/lib/video-transcoding-profiles.ts @@ -0,0 +1,95 @@ +import { getTargetBitrate } from '../../shared/models/videos' +import { AvailableEncoders, buildStreamSuffix, EncoderOptionsBuilder } from '../helpers/ffmpeg-utils' +import { ffprobePromise, getAudioStream, getMaxAudioBitrate, getVideoFileBitrate, getVideoStreamFromFile } from '../helpers/ffprobe-utils' +import { VIDEO_TRANSCODING_FPS } from '../initializers/constants' + +// --------------------------------------------------------------------------- +// Available encoders profiles +// --------------------------------------------------------------------------- + +// Resources: +// * https://slhck.info/video/2017/03/01/rate-control.html +// * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate + +const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ input, resolution, fps }) => { + let targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) + + const probe = await ffprobePromise(input) + + const videoStream = await getVideoStreamFromFile(input, probe) + if (!videoStream) { + return { outputOptions: [ ] } + } + + // Don't transcode to an higher bitrate than the original file + const fileBitrate = await getVideoFileBitrate(input, probe) + targetBitrate = Math.min(targetBitrate, fileBitrate) + + return { + outputOptions: [ + `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}` + ] + } +} + +const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = async ({ resolution, fps, streamNum }) => { + const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) + + return { + outputOptions: [ + `${buildStreamSuffix('-b:v', streamNum)} ${targetBitrate}`, + `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}` + ] + } +} + +const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNum }) => { + const parsedAudio = await getAudioStream(input) + + // We try to reduce the ceiling bitrate by making rough matches of bitrates + // Of course this is far from perfect, but it might save some space in the end + + const audioCodecName = parsedAudio.audioStream['codec_name'] + + const bitrate = getMaxAudioBitrate(audioCodecName, parsedAudio.bitrate) + + if (bitrate !== undefined && bitrate !== -1) { + return { outputOptions: [ buildStreamSuffix('-b:a', streamNum), bitrate + 'k' ] } + } + + return { copy: true, outputOptions: [] } +} + +const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum }) => { + return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] } +} + +const availableEncoders: AvailableEncoders = { + vod: { + libx264: { + default: defaultX264VODOptionsBuilder + }, + aac: { + default: defaultAACOptionsBuilder + }, + libfdk_aac: { + default: defaultLibFDKAACVODOptionsBuilder + } + }, + live: { + libx264: { + default: defaultX264LiveOptionsBuilder + }, + aac: { + default: defaultAACOptionsBuilder + } + } +} + +// --------------------------------------------------------------------------- + +export { + availableEncoders +} + +// --------------------------------------------------------------------------- diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index 0e6a0e984..aaad219dd 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts @@ -2,30 +2,18 @@ import { copyFile, ensureDir, move, remove, stat } from 'fs-extra' import { basename, extname as extnameUtil, join } from 'path' import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models' -import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' +import { VideoResolution } from '../../shared/models/videos' import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' -import { AvailableEncoders, EncoderOptionsBuilder, transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils' -import { - canDoQuickTranscode, - getAudioStream, - getDurationFromVideoFile, - getMaxAudioBitrate, - getMetadataFromFile, - getVideoFileBitrate, - getVideoFileFPS -} from '../helpers/ffprobe-utils' +import { transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils' +import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../helpers/ffprobe-utils' import { logger } from '../helpers/logger' import { CONFIG } from '../initializers/config' -import { - HLS_STREAMING_PLAYLIST_DIRECTORY, - P2P_MEDIA_LOADER_PEER_VERSION, - VIDEO_TRANSCODING_FPS, - WEBSERVER -} from '../initializers/constants' +import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants' 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' /** * Optimize the original video file and replace it. The resolution is not changed. @@ -252,75 +240,6 @@ async function generateHlsPlaylist (options: { return video } -// --------------------------------------------------------------------------- -// Available encoders profiles -// --------------------------------------------------------------------------- - -const defaultX264OptionsBuilder: EncoderOptionsBuilder = async ({ input, resolution, fps }) => { - if (!fps) return { outputOptions: [] } - - let targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) - - // Don't transcode to an higher bitrate than the original file - const fileBitrate = await getVideoFileBitrate(input) - targetBitrate = Math.min(targetBitrate, fileBitrate) - - return { - outputOptions: [ - // Constrained Encoding (VBV) - // https://slhck.info/video/2017/03/01/rate-control.html - // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate - `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}` - ] - } -} - -const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input }) => { - const parsedAudio = await getAudioStream(input) - - // we try to reduce the ceiling bitrate by making rough matches of bitrates - // of course this is far from perfect, but it might save some space in the end - - const audioCodecName = parsedAudio.audioStream['codec_name'] - - const bitrate = getMaxAudioBitrate(audioCodecName, parsedAudio.bitrate) - - if (bitrate !== undefined && bitrate !== -1) { - return { outputOptions: [ '-b:a', bitrate + 'k' ] } - } - - return { outputOptions: [] } -} - -const defaultLibFDKAACOptionsBuilder: EncoderOptionsBuilder = () => { - return { outputOptions: [ '-aq', '5' ] } -} - -const availableEncoders: AvailableEncoders = { - vod: { - libx264: { - default: defaultX264OptionsBuilder - }, - aac: { - default: defaultAACOptionsBuilder - }, - libfdkAAC: { - default: defaultLibFDKAACOptionsBuilder - } - }, - live: { - libx264: { - default: defaultX264OptionsBuilder - }, - aac: { - default: defaultAACOptionsBuilder - }, - libfdkAAC: { - default: defaultLibFDKAACOptionsBuilder - } - } -} - // --------------------------------------------------------------------------- export { diff --git a/server/tools/test.ts b/server/tools/test.ts new file mode 100644 index 000000000..23bf0120f --- /dev/null +++ b/server/tools/test.ts @@ -0,0 +1,105 @@ +import { registerTSPaths } from '../helpers/register-ts-paths' +registerTSPaths() + +import { LiveVideo, LiveVideoCreate, VideoPrivacy } from '@shared/models' +import * as program from 'commander' +import { + createLive, + flushAndRunServer, + getLive, + killallServers, + sendRTMPStream, + ServerInfo, + setAccessTokensToServers, + setDefaultVideoChannel, + updateCustomSubConfig +} from '../../shared/extra-utils' + +type CommandType = 'live-mux' | 'live-transcoding' + +registerTSPaths() + +const command = program + .name('test') + .option('-t, --type ', 'live-muxing|live-transcoding') + .parse(process.argv) + +run() + .catch(err => { + console.error(err) + process.exit(-1) + }) + +async function run () { + const commandType: CommandType = command['type'] + if (!commandType) { + console.error('Miss command type') + process.exit(-1) + } + + console.log('Starting server.') + + const server = await flushAndRunServer(1, {}, [], false) + + const cleanup = () => { + console.log('Killing server') + killallServers([ server ]) + } + + process.on('exit', cleanup) + process.on('SIGINT', cleanup) + + await setAccessTokensToServers([ server ]) + await setDefaultVideoChannel([ server ]) + + await buildConfig(server, commandType) + + const attributes: LiveVideoCreate = { + name: 'live', + saveReplay: true, + channelId: server.videoChannel.id, + privacy: VideoPrivacy.PUBLIC + } + + console.log('Creating live.') + + const res = await createLive(server.url, server.accessToken, attributes) + const liveVideoUUID = res.body.video.uuid + + const resLive = await getLive(server.url, server.accessToken, liveVideoUUID) + const live: LiveVideo = resLive.body + + console.log('Sending RTMP stream.') + + const ffmpegCommand = sendRTMPStream(live.rtmpUrl, live.streamKey) + + ffmpegCommand.on('error', err => { + console.error(err) + process.exit(-1) + }) + + ffmpegCommand.on('end', () => { + console.log('ffmpeg ended') + process.exit(0) + }) +} + +// ---------------------------------------------------------------------------- + +async function buildConfig (server: ServerInfo, commandType: CommandType) { + await updateCustomSubConfig(server.url, server.accessToken, { + instance: { + customizations: { + javascript: '', + css: '' + } + }, + live: { + enabled: true, + allowReplay: true, + transcoding: { + enabled: commandType === 'live-transcoding' + } + } + }) +} diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts index a647b0eb4..75e79cc41 100644 --- a/shared/extra-utils/server/servers.ts +++ b/shared/extra-utils/server/servers.ts @@ -104,7 +104,7 @@ function randomRTMP () { return randomInt(low, high) } -async function flushAndRunServer (serverNumber: number, configOverride?: Object, args = []) { +async function flushAndRunServer (serverNumber: number, configOverride?: Object, args = [], silent = true) { const parallel = parallelTests() const internalServerNumber = parallel ? randomServer() : serverNumber @@ -133,10 +133,10 @@ async function flushAndRunServer (serverNumber: number, configOverride?: Object, } } - return runServer(server, configOverride, args) + return runServer(server, configOverride, args, silent) } -async function runServer (server: ServerInfo, configOverrideArg?: any, args = []) { +async function runServer (server: ServerInfo, configOverrideArg?: any, args = [], silent?: boolean) { // These actions are async so we need to be sure that they have both been done const serverRunString = { 'Server listening': false @@ -240,7 +240,11 @@ async function runServer (server: ServerInfo, configOverrideArg?: any, args = [] // If no, there is maybe one thing not already initialized (client/user credentials generation...) if (dontContinue === true) return - server.app.stdout.removeListener('data', onStdout) + if (silent === false) { + console.log(data.toString()) + } else { + server.app.stdout.removeListener('data', onStdout) + } process.on('exit', () => { try { diff --git a/shared/models/videos/video-resolution.enum.ts b/shared/models/videos/video-resolution.enum.ts index 571ab5d8f..dcd55dad8 100644 --- a/shared/models/videos/video-resolution.enum.ts +++ b/shared/models/videos/video-resolution.enum.ts @@ -81,7 +81,7 @@ export function getTargetBitrate (resolution: number, fps: number, fpsTranscodin // Example outputs: // 1080p10: 2420 kbps, 1080p30: 3300 kbps, 1080p60: 4620 kbps // 720p10: 1283 kbps, 720p30: 1750 kbps, 720p60: 2450 kbps - return baseBitrate + (fps - fpsTranscodingConstants.AVERAGE) * (maxBitrateDifference / maxFpsDifference) + return Math.floor(baseBitrate + (fps - fpsTranscodingConstants.AVERAGE) * (maxBitrateDifference / maxFpsDifference)) } /**