import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils'
import { processImage } from './image-utils'
import { logger } from './logger'
+import { FilterSpecification } from 'fluent-ffmpeg'
/**
*
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')
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}`
+ })
}
{
varStreamMap.push(`v:${i},a:${i}`)
}
+ command.complexFilter(complexFilter)
+
addDefaultLiveHLSParams(command, outPath)
command.outputOption('-var_stream_map', varStreamMap.join(' '))
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
+ ? `${options.resolution}:-2`
+ : `-2:${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')
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')
if (streamType === 'video') {
localCommand.videoCodec(builderResult.encoder)
+
+ if (scaleFilterValue) {
+ localCommand.outputOption(`-vf ${getScaleFilter(builderResult.result)}=${scaleFilterValue}`)
+ }
} else if (streamType === 'audio') {
localCommand.audioCodec(builderResult.encoder)
}
+
applyEncoderOptions(localCommand, builderResult.result)
addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps })
}
function applyEncoderOptions (command: ffmpeg.FfmpegCommand, options: EncoderOptions): ffmpeg.FfmpegCommand {
return command
.inputOptions(options.inputOptions ?? [])
- .videoFilters(options.videoFilters ?? [])
.outputOptions(options.outputOptions ?? [])
}
+function getScaleFilter (options: EncoderOptions): string {
+ if (options.scaleFilter) return options.scaleFilter.name
+
+ return 'scale'
+}
+
// ---------------------------------------------------------------------------
// Utils
// ---------------------------------------------------------------------------
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 = () => {
- return {
- videoFilters: [
- 'fps=10'
- ]
+ {
+ const builder = (options) => {
+ return {
+ outputOptions: [
+ '-r:' + options.streamNum + ' 5'
+ ]
+ }
}
- }
- transcodingManager.addVODProfile('libx264', 'video-filters-vod', builder)
+ transcodingManager.addLiveProfile('libx264', 'low-live', builder)
+ }
}
+ // Input options
{
- const builder = () => {
- return {
- inputOptions: [
- '-r 5'
- ]
+ {
+ const builder = () => {
+ return {
+ inputOptions: [
+ '-r 5'
+ ]
+ }
}
- }
- transcodingManager.addVODProfile('libx264', 'input-options-vod', builder)
- }
+ transcodingManager.addVODProfile('libx264', 'input-options-vod', builder)
+ }
- {
- const builder = (options) => {
- return {
- outputOptions: [
- '-r:' + options.streamNum + ' 5'
- ]
+ {
+ const builder = () => {
+ return {
+ inputOptions: [
+ '-r 5'
+ ]
+ }
}
- }
- transcodingManager.addLiveProfile('libx264', 'low-live', builder)
+ transcodingManager.addLiveProfile('libx264', 'input-options-live', builder)
+ }
}
+ // Scale filters
{
- const builder = () => {
- return {
- inputOptions: [
- '-r 5'
- ]
+ {
+ const builder = () => {
+ return {
+ scaleFilter: {
+ name: 'Glomgold'
+ }
+ }
}
+
+ transcodingManager.addVODProfile('libx264', 'bad-scale-vod', builder)
}
- transcodingManager.addLiveProfile('libx264', 'input-options-live', builder)
+ {
+ const builder = () => {
+ return {
+ scaleFilter: {
+ name: 'Flintheart'
+ }
+ }
+ }
+
+ transcodingManager.addLiveProfile('libx264', 'bad-scale-live', builder)
+ }
}
}
sendRTMPStreamInVideo,
setAccessTokensToServers,
setDefaultVideoChannel,
+ testFfmpegStreamError,
uninstallPlugin,
updateCustomSubConfig,
uploadVideoAndGetId,
+ waitFfmpegUntilError,
waitJobs,
waitUntilLivePublished
} from '../../../shared/extra-utils'
const res = await getConfig(server.url)
const config = res.body as ServerConfig
- expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod', 'video-filters-vod', 'input-options-vod' ])
- expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'low-live', 'input-options-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 () {
await checkVideoFPS(videoUUID, 'below', 12)
})
- it('Should apply video filters in vod profile', async function () {
+ it('Should apply input options in vod profile', async function () {
this.timeout(120000)
- await updateConf(server, 'video-filters-vod', 'default')
+ await updateConf(server, 'input-options-vod', 'default')
const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
await waitJobs([ server ])
- await checkVideoFPS(videoUUID, 'below', 12)
+ await checkVideoFPS(videoUUID, 'below', 6)
})
- it('Should apply input options in vod profile', async function () {
+ it('Should apply the scale filter in vod profile', async function () {
this.timeout(120000)
- await updateConf(server, 'input-options-vod', 'default')
+ await updateConf(server, 'bad-scale-vod', 'default')
const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
await waitJobs([ server ])
- await checkVideoFPS(videoUUID, 'below', 6)
+ // 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 () {
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)
export interface EncoderOptions {
copy?: boolean // Copy stream? Default to false
+ scaleFilter?: {
+ name: string
+ }
+
inputOptions?: string[]
- videoFilters?: string[]
outputOptions?: string[]
}
Adding transcoding profiles allow admins to change ffmpeg encoding parameters and/or encoders.
A transcoding profile has to be chosen by the admin of the instance using the admin configuration.
-Transcoding profiles used for live transcoding must not provide any `videoFilters`.
-
```js
async function register ({
transcodingManager
// All these options are optional and defaults to []
return {
inputOptions: [],
- videoFilters: [
- 'vflip' // flip the video vertically
- ],
outputOptions: [
// Use a custom bitrate
'-b' + streamString + ' 10K'
// And/Or support this profile for live transcoding
transcodingManager.addLiveProfile(encoder, profileName, builder)
- // Note: this profile will fail for live transcode because it specifies videoFilters
}
{
const builder = () => {
return {
inputOptions: [],
- videoFilters: [],
outputOptions: []
}
}