]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add ability for plugins to specify scale filter
authorChocobozzz <me@florianbigard.com>
Fri, 9 Apr 2021 08:36:21 +0000 (10:36 +0200)
committerChocobozzz <chocobozzz@cpy.re>
Fri, 9 Apr 2021 13:53:18 +0000 (15:53 +0200)
server/helpers/ffmpeg-utils.ts
server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js
server/tests/plugins/plugin-transcoding.ts
shared/models/videos/video-transcoding.model.ts
support/doc/plugins/guide.md

index 685a35886e97437a8b1f1e3e01e43ebef0bb6a81..aa4223cdabdc9cc12eb798bcbe86f4d1506e4f77 100644 (file)
@@ -9,6 +9,7 @@ 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'
 
 /**
  *
@@ -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')
@@ -278,6 +272,13 @@ async function getLiveTranscodingCommand (options: {
 
       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}`
+      })
     }
 
     {
@@ -300,6 +301,8 @@ async function getLiveTranscodingCommand (options: {
     varStreamMap.push(`v:${i},a:${i}`)
   }
 
+  command.complexFilter(complexFilter)
+
   addDefaultLiveHLSParams(command, outPath)
 
   command.outputOption('-var_stream_map', varStreamMap.join(' '))
@@ -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
+      ? `${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')
 
@@ -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')
@@ -601,9 +607,14 @@ 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)
     }
+
     applyEncoderOptions(localCommand, builderResult.result)
     addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps })
   }
@@ -628,10 +639,15 @@ function presetOnlyAudio (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand {
 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
 // ---------------------------------------------------------------------------
index 366b827a94d04af0dcda39c8986a08d37d6c9ce4..59b1369471b2c86e31a0a6612fa6a8c703b07e83 100644 (file)
@@ -1,63 +1,84 @@
 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)
+    }
   }
 }
 
index 415705ca1773913c2295f1449998c6e3075aef7e..b6dff930e3dd57d4c5227022be9a8d890ea16282 100644 (file)
@@ -15,9 +15,11 @@ import {
   sendRTMPStreamInVideo,
   setAccessTokensToServers,
   setDefaultVideoChannel,
+  testFfmpegStreamError,
   uninstallPlugin,
   updateCustomSubConfig,
   uploadVideoAndGetId,
+  waitFfmpegUntilError,
   waitJobs,
   waitUntilLivePublished
 } from '../../../shared/extra-utils'
@@ -119,8 +121,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', '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 () {
@@ -143,26 +145,31 @@ describe('Test transcoding plugins', 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 () {
@@ -205,6 +212,17 @@ describe('Test transcoding plugins', 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)
 
index ffb0115dceafcef5e6c6fe1742200a4f13410863..3f2382ce8b133cd2fc663df825247b9a57a5fe49 100644 (file)
@@ -12,8 +12,11 @@ export type EncoderOptionsBuilder = (params: {
 export interface EncoderOptions {
   copy?: boolean // Copy stream? Default to false
 
+  scaleFilter?: {
+    name: string
+  }
+
   inputOptions?: string[]
-  videoFilters?: string[]
   outputOptions?: string[]
 }
 
index 331813e4f5b6110983055150853f50f8d183a4ca..ea33b8d9f6907f92b20496d9538bb5fa2b2b2946 100644 (file)
@@ -328,8 +328,6 @@ function register (...) {
 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
@@ -346,9 +344,6 @@ async function register ({
       // All these options are optional and defaults to []
       return {
         inputOptions: [],
-        videoFilters: [
-          'vflip' // flip the video vertically
-        ],
         outputOptions: [
         // Use a custom bitrate
           '-b' + streamString + ' 10K'
@@ -364,7 +359,6 @@ async function register ({
 
     // 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
   }
 
   {
@@ -401,7 +395,6 @@ async function register ({
     const builder = () => {
       return {
         inputOptions: [],
-        videoFilters: [],
         outputOptions: []
       }
     }