]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Support encoding profiles
authorChocobozzz <me@florianbigard.com>
Tue, 24 Nov 2020 13:08:23 +0000 (14:08 +0100)
committerChocobozzz <chocobozzz@cpy.re>
Wed, 25 Nov 2020 09:07:51 +0000 (10:07 +0100)
scripts/dev/cli.sh
server/helpers/ffmpeg-utils.ts
server/helpers/ffprobe-utils.ts
server/lib/live-manager.ts
server/lib/video-transcoding-profiles.ts [new file with mode: 0644]
server/lib/video-transcoding.ts
server/tools/test.ts [new file with mode: 0644]
shared/extra-utils/server/servers.ts
shared/models/videos/video-resolution.enum.ts

index dc6f0af0dca08c91088b4b0b2dbde3e319e4de9e..4bf4808b8947d14091b8e46f40d26fc0cdc86656 100755 (executable)
@@ -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
index e297108df2bc46f7256c7becec1e63456be83e6c..712ec757e552e6e2ed54e32dbebff73ddea0185a 100644 (file)
@@ -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> | EncoderOptions
 
 // Options types
 
 export interface EncoderOptions {
+  copy?: boolean
   outputOptions: string[]
 }
 
@@ -37,7 +39,7 @@ export interface EncoderProfile <T> {
 
 export type AvailableEncoders = {
   [ id in 'live' | 'vod' ]: {
-    [ encoder in 'libx264' | 'aac' | 'libfdkAAC' ]: EncoderProfile<EncoderOptionsBuilder>
+    [ encoder in 'libx264' | 'aac' | 'libfdk_aac' ]?: EncoderProfile<EncoderOptionsBuilder>
   }
 }
 
@@ -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' }
+
+    {
+      const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'VIDEO' }))
+      if (!builderResult) {
+        throw new Error('No available live video encoder found')
+      }
+
+      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(`-map [vout${resolution}]`)
-    command.outputOption(`-c:v:${i} libx264`)
-    command.outputOption(`-b:v:${i} ${getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)}`)
+      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`)
-    command.outputOption(`-c:a:${i} aac`)
+      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
-
-  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))
+function addDefaultEncoderParams (options: {
+  command: ffmpeg.FfmpegCommand
+  encoder: 'libx264' | string
+  streamNum?: number
+  fps?: number
+}) {
+  const { command, encoder, fps, streamNum } = options
+
+  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<EncoderOptionsBuilder> = 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 encoder of encodersToTry) {
-      if (!(await checkFFmpegEncoders()).get(encoder)) continue
-
-      const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = 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)
+  for (const streamType of streamsToProcess) {
+    const { profile, resolution, availableEncoders } = transcodeOptions
+
+    const builderResult = await getEncoderBuilderResult({
+      streamType,
+      input,
+      resolution,
+      availableEncoders,
+      profile,
+      fps,
+      videoType: 'vod' as 'vod'
+    })
 
-      localCommand.outputOptions(builderResult.outputOptions)
+    if (!builderResult) {
+      throw new Error('No available encoder found for stream ' + streamType)
+    }
 
-      addEncoderDefaultParams(localCommand, encoder)
+    logger.debug('Apply ffmpeg params from %s.', builderResult.encoder, builderResult)
 
-      streamsFound[stream] = encoder
-      break
+    if (streamType === 'VIDEO') {
+      localCommand.videoCodec(builderResult.encoder)
+    } else if (streamType === 'AUDIO') {
+      localCommand.audioCodec(builderResult.encoder)
     }
 
-    if (!streamsFound[stream]) {
-      throw new Error('No available encoder found ' + encodersToTry.join(', '))
-    }
+    command.addOutputOptions(builderResult.result.outputOptions)
+    addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps })
   }
 
-  localCommand.videoCodec(streamsFound.VIDEO)
-
   return localCommand
 }
 
index 6159d3963bb3eceb493d357a058ed1f68e144462..5545ddbbfd5a4abca0cc7e5161f8e34a8c918f4e 100644 (file)
@@ -198,9 +198,12 @@ function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod'
 async function canDoQuickTranscode (path: string): Promise<boolean> {
   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<boolean> {
   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<boolean> {
   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<boolean> {
+  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
 }
index b9e5d45612e43ab34205b570fc4294e30f87048a..ee0e4de3788991e7a7f055f83c7c4b2dd8e7fae3 100644 (file)
@@ -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 (file)
index 0000000..12e22a1
--- /dev/null
@@ -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
+}
+
+// ---------------------------------------------------------------------------
index 0e6a0e9840e12dca5c71445f903875e7de1af632..aaad219ddda5692defc2187f91d55c51207a8666 100644 (file)
@@ -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 (file)
index 0000000..23bf012
--- /dev/null
@@ -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 <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'
+      }
+    }
+  })
+}
index a647b0eb4e21129b2f7e7cc39d7f2c987ee68d68..75e79cc4112f462a417603ff9b5ee608e7a72d85 100644 (file)
@@ -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 {
index 571ab5d8f1f0a108eaf7a758dbf15fce3081c643..dcd55dad8b10b39668475c5f10c7d8ddeac3f993 100644 (file)
@@ -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))
 }
 
 /**