Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for inputOptions and videoFilters in transcode plugin #3917

Merged
merged 7 commits into from Apr 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 50 additions & 28 deletions server/helpers/ffmpeg-utils.ts
Expand Up @@ -3,12 +3,13 @@ import * as ffmpeg from 'fluent-ffmpeg'
import { readFile, remove, writeFile } from 'fs-extra'
import { dirname, join } from 'path'
import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants'
import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos'
import { AvailableEncoders, EncoderOptionsBuilder, EncoderOptions, EncoderProfile, VideoResolution } from '../../shared/models/videos'
import { CONFIG } from '../initializers/config'
import { execPromise, promisify0 } from './core-utils'
import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils'
import { processImage } from './image-utils'
import { logger } from './logger'
import { FilterSpecification } from 'fluent-ffmpeg'

/**
*
Expand Down Expand Up @@ -226,21 +227,14 @@ async function getLiveTranscodingCommand (options: {

const varStreamMap: string[] = []

command.complexFilter([
const complexFilter: FilterSpecification[] = [
{
inputs: '[v:0]',
filter: 'split',
options: resolutions.length,
outputs: resolutions.map(r => `vtemp${r}`)
},

...resolutions.map(r => ({
inputs: `vtemp${r}`,
filter: 'scale',
options: `w=-2:h=${r}`,
outputs: `vout${r}`
}))
])
}
]

command.outputOption('-preset superfast')
command.outputOption('-sc_threshold 0')
Expand Down Expand Up @@ -277,7 +271,14 @@ async function getLiveTranscodingCommand (options: {
logger.debug('Apply ffmpeg live video params from %s using %s profile.', builderResult.encoder, profile, builderResult)

command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`)
command.addOutputOptions(builderResult.result.outputOptions)
applyEncoderOptions(command, builderResult.result)

complexFilter.push({
inputs: `vtemp${resolution}`,
filter: getScaleFilter(builderResult.result),
options: `w=-2:h=${resolution}`,
Chocobozzz marked this conversation as resolved.
Show resolved Hide resolved
outputs: `vout${resolution}`
})
}

{
Expand All @@ -294,12 +295,14 @@ async function getLiveTranscodingCommand (options: {
logger.debug('Apply ffmpeg live audio params from %s using %s profile.', builderResult.encoder, profile, builderResult)

command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`)
command.addOutputOptions(builderResult.result.outputOptions)
applyEncoderOptions(command, builderResult.result)
}

varStreamMap.push(`v:${i},a:${i}`)
}

command.complexFilter(complexFilter)

addDefaultLiveHLSParams(command, outPath)

command.outputOption('-var_stream_map', varStreamMap.join(' '))
Expand Down Expand Up @@ -389,29 +392,29 @@ async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: Tran
let fps = await getVideoFileFPS(options.inputPath)
fps = computeFPS(fps, options.resolution)

command = await presetVideo(command, options.inputPath, options, fps)
let scaleFilterValue: string

if (options.resolution !== undefined) {
// '?x720' or '720x?' for example
const size = options.isPortraitMode === true
? `${options.resolution}x?`
: `?x${options.resolution}`

command = command.size(size)
scaleFilterValue = options.isPortraitMode === true
? `w=${options.resolution}:h=-2`
: `w=-2:h=${options.resolution}`
}

command = await presetVideo({ command, input: options.inputPath, transcodeOptions: options, fps, scaleFilterValue })

return command
}

async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) {
command = command.loop(undefined)

command = await presetVideo(command, options.audioPath, options)
// Avoid "height not divisible by 2" error
const scaleFilterValue = 'trunc(iw/2)*2:trunc(ih/2)*2'
command = await presetVideo({ command, input: options.audioPath, transcodeOptions: options, scaleFilterValue })

command.outputOption('-preset:v veryfast')

command = command.input(options.audioPath)
.videoFilter('scale=trunc(iw/2)*2:trunc(ih/2)*2') // Avoid "height not divisible by 2" error
.outputOption('-tune stillimage')
.outputOption('-shortest')

Expand Down Expand Up @@ -555,12 +558,15 @@ async function getEncoderBuilderResult (options: {
return null
}

async function presetVideo (
command: ffmpeg.FfmpegCommand,
input: string,
transcodeOptions: TranscodeOptions,
async function presetVideo (options: {
command: ffmpeg.FfmpegCommand
input: string
transcodeOptions: TranscodeOptions
fps?: number
) {
scaleFilterValue?: string
}) {
const { command, input, transcodeOptions, fps, scaleFilterValue } = options

let localCommand = command
.format('mp4')
.outputOption('-movflags faststart')
Expand Down Expand Up @@ -601,11 +607,15 @@ async function presetVideo (

if (streamType === 'video') {
localCommand.videoCodec(builderResult.encoder)

if (scaleFilterValue) {
localCommand.outputOption(`-vf ${getScaleFilter(builderResult.result)}=${scaleFilterValue}`)
}
} else if (streamType === 'audio') {
localCommand.audioCodec(builderResult.encoder)
}

command.addOutputOptions(builderResult.result.outputOptions)
applyEncoderOptions(localCommand, builderResult.result)
addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps })
}

Expand All @@ -626,6 +636,18 @@ function presetOnlyAudio (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand {
.noVideo()
}

function applyEncoderOptions (command: ffmpeg.FfmpegCommand, options: EncoderOptions): ffmpeg.FfmpegCommand {
return command
.inputOptions(options.inputOptions ?? [])
.outputOptions(options.outputOptions ?? [])
}

function getScaleFilter (options: EncoderOptions): string {
if (options.scaleFilter) return options.scaleFilter.name

return 'scale'
}

// ---------------------------------------------------------------------------
// Utils
// ---------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion server/lib/video-transcoding-profiles.ts
Expand Up @@ -55,7 +55,7 @@ const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNu

if (await canDoQuickAudioTranscode(input, probe)) {
logger.debug('Copy audio stream %s by AAC encoder.', input)
return { copy: true, outputOptions: [] }
return { copy: true, outputOptions: [ ] }
}

const parsedAudio = await getAudioStream(input, probe)
Expand Down
3 changes: 2 additions & 1 deletion server/tests/cli/print-transcode-command.ts
Expand Up @@ -22,7 +22,8 @@ describe('Test create transcoding jobs', function () {
const command = await execCLI(`npm run print-transcode-command -- ${fixturePath} -r ${resolution}`)
const targetBitrate = Math.min(getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS), bitrate)

expect(command).to.includes(`-y -acodec aac -vcodec libx264 -filter:v scale=w=trunc(oh*a/2)*2:h=${resolution}`)
expect(command).to.includes(`-vf scale=w=-2:h=${resolution}`)
expect(command).to.includes(`-y -acodec aac -vcodec libx264`)
expect(command).to.includes('-f mp4')
expect(command).to.includes('-movflags faststart')
expect(command).to.includes('-b:a 256k')
Expand Down
82 changes: 70 additions & 12 deletions server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js
@@ -1,30 +1,88 @@
async function register ({ transcodingManager }) {

// Output options
{
const builder = () => {
return {
outputOptions: [
'-r 10'
]
{
const builder = () => {
return {
outputOptions: [
'-r 10'
]
}
}

transcodingManager.addVODProfile('libx264', 'low-vod', builder)
}

transcodingManager.addVODProfile('libx264', 'low-vod', builder)
{
const builder = (options) => {
return {
outputOptions: [
'-r:' + options.streamNum + ' 5'
]
}
}

transcodingManager.addLiveProfile('libx264', 'low-live', builder)
}
}

// Input options
{
const builder = (options) => {
return {
outputOptions: [
'-r:' + options.streamNum + ' 5'
]
{
const builder = () => {
return {
inputOptions: [
'-r 5'
]
}
}

transcodingManager.addVODProfile('libx264', 'input-options-vod', builder)
}

transcodingManager.addLiveProfile('libx264', 'low-live', builder)
{
const builder = () => {
return {
inputOptions: [
'-r 5'
]
}
}

transcodingManager.addLiveProfile('libx264', 'input-options-live', builder)
}
}

// Scale filters
{
{
const builder = () => {
return {
scaleFilter: {
name: 'Glomgold'
}
}
}

transcodingManager.addVODProfile('libx264', 'bad-scale-vod', builder)
}

{
const builder = () => {
return {
scaleFilter: {
name: 'Flintheart'
}
}
}

transcodingManager.addLiveProfile('libx264', 'bad-scale-live', builder)
}
}
}


async function unregister () {
return
}
Expand Down
57 changes: 55 additions & 2 deletions server/tests/plugins/plugin-transcoding.ts
Expand Up @@ -15,6 +15,7 @@ import {
sendRTMPStreamInVideo,
setAccessTokensToServers,
setDefaultVideoChannel,
testFfmpegStreamError,
uninstallPlugin,
updateCustomSubConfig,
uploadVideoAndGetId,
Expand Down Expand Up @@ -119,8 +120,8 @@ describe('Test transcoding plugins', function () {
const res = await getConfig(server.url)
const config = res.body as ServerConfig

expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod' ])
expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'low-live' ])
expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod', 'input-options-vod', 'bad-scale-vod' ])
expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'low-live', 'input-options-live', 'bad-scale-live' ])
})

it('Should not use the plugin profile if not chosen by the admin', async function () {
Expand All @@ -143,6 +144,33 @@ describe('Test transcoding plugins', function () {
await checkVideoFPS(videoUUID, 'below', 12)
})

it('Should apply input options in vod profile', async function () {
this.timeout(120000)

await updateConf(server, 'input-options-vod', 'default')

const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
await waitJobs([ server ])

await checkVideoFPS(videoUUID, 'below', 6)
})

it('Should apply the scale filter in vod profile', async function () {
this.timeout(120000)

await updateConf(server, 'bad-scale-vod', 'default')

const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
await waitJobs([ server ])

// Transcoding failed
const res = await getVideo(server.url, videoUUID)
const video: VideoDetails = res.body

expect(video.files).to.have.lengthOf(1)
expect(video.streamingPlaylists).to.have.lengthOf(0)
})

it('Should not use the plugin profile if not chosen by the admin', async function () {
this.timeout(120000)

Expand All @@ -169,6 +197,31 @@ describe('Test transcoding plugins', function () {
await checkLiveFPS(liveVideoId, 'below', 12)
})

it('Should apply the input options on live profile', async function () {
this.timeout(120000)

await updateConf(server, 'low-vod', 'input-options-live')

const liveVideoId = await createLiveWrapper(server)

await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm')
await waitUntilLivePublished(server.url, server.accessToken, liveVideoId)
await waitJobs([ server ])

await checkLiveFPS(liveVideoId, 'below', 6)
})

it('Should apply the scale filter name on live profile', async function () {
this.timeout(120000)

await updateConf(server, 'low-vod', 'bad-scale-live')

const liveVideoId = await createLiveWrapper(server)

const command = await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm')
await testFfmpegStreamError(command, true)
})

it('Should default to the default profile if the specified profile does not exist', async function () {
this.timeout(120000)

Expand Down
7 changes: 6 additions & 1 deletion shared/models/videos/video-transcoding.model.ts
Expand Up @@ -12,7 +12,12 @@ export type EncoderOptionsBuilder = (params: {
export interface EncoderOptions {
copy?: boolean // Copy stream? Default to false

outputOptions: string[]
scaleFilter?: {
name: string
}

inputOptions?: string[]
outputOptions?: string[]
}

// All our encoders
Expand Down