Chocobozzz 2 лет назад
Родитель
Сommit
c826f34a45

+ 14 - 7
server/helpers/ffmpeg-utils.ts

@@ -6,7 +6,7 @@ import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants'
 import { AvailableEncoders, EncoderOptions, EncoderOptionsBuilder, 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 { computeFPS, ffprobePromise, getAudioStream, getVideoFileBitrate, getVideoFileFPS } from './ffprobe-utils'
 import { processImage } from './image-utils'
 import { logger } from './logger'
 
@@ -218,11 +218,12 @@ async function getLiveTranscodingCommand (options: {
 
   resolutions: number[]
   fps: number
+  bitrate: number
 
   availableEncoders: AvailableEncoders
   profile: string
 }) {
-  const { rtmpUrl, outPath, resolutions, fps, availableEncoders, profile, masterPlaylistName } = options
+  const { rtmpUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName } = options
   const input = rtmpUrl
 
   const command = getFFmpeg(input, 'live')
@@ -253,6 +254,7 @@ async function getLiveTranscodingCommand (options: {
       profile,
 
       fps: resolutionFPS,
+      inputBitrate: bitrate,
       resolution,
       streamNum: i,
       videoType: 'live' as 'live'
@@ -260,7 +262,7 @@ async function getLiveTranscodingCommand (options: {
 
     {
       const streamType: StreamType = 'video'
-      const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType }))
+      const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType })
       if (!builderResult) {
         throw new Error('No available live video encoder found')
       }
@@ -284,7 +286,7 @@ async function getLiveTranscodingCommand (options: {
 
     {
       const streamType: StreamType = 'audio'
-      const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType }))
+      const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType })
       if (!builderResult) {
         throw new Error('No available live audio encoder found')
       }
@@ -510,10 +512,11 @@ async function getEncoderBuilderResult (options: {
   videoType: 'vod' | 'live'
 
   resolution: number
+  inputBitrate: number
   fps?: number
   streamNum?: number
 }) {
-  const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options
+  const { availableEncoders, input, profile, resolution, streamType, fps, inputBitrate, streamNum, videoType } = options
 
   const encodersToTry = availableEncoders.encodersToTry[videoType][streamType]
   const encoders = availableEncoders.available[videoType]
@@ -543,7 +546,7 @@ async function getEncoderBuilderResult (options: {
       }
     }
 
-    const result = await builder({ input, resolution, fps, streamNum })
+    const result = await builder({ input, resolution, inputBitrate, fps, streamNum })
 
     return {
       result,
@@ -573,8 +576,11 @@ async function presetVideo (options: {
 
   addDefaultEncoderGlobalParams({ command })
 
+  const probe = await ffprobePromise(input)
+
   // Audio encoder
-  const parsedAudio = await getAudioStream(input)
+  const parsedAudio = await getAudioStream(input, probe)
+  const bitrate = await getVideoFileBitrate(input, probe)
 
   let streamsToProcess: StreamType[] = [ 'audio', 'video' ]
 
@@ -593,6 +599,7 @@ async function presetVideo (options: {
       availableEncoders,
       profile,
       fps,
+      inputBitrate: bitrate,
       videoType: 'vod' as 'vod'
     })
 

+ 11 - 2
server/helpers/ffprobe-utils.ts

@@ -175,10 +175,19 @@ async function getMetadataFromFile (path: string, existingProbe?: ffmpeg.Ffprobe
   return new VideoFileMetadata(metadata)
 }
 
-async function getVideoFileBitrate (path: string, existingProbe?: ffmpeg.FfprobeData) {
+async function getVideoFileBitrate (path: string, existingProbe?: ffmpeg.FfprobeData): Promise<number> {
   const metadata = await getMetadataFromFile(path, existingProbe)
 
-  return metadata.format.bit_rate as number
+  let bitrate = metadata.format.bit_rate as number
+  if (bitrate && !isNaN(bitrate)) return bitrate
+
+  const videoStream = await getVideoStreamFromFile(path, existingProbe)
+  if (!videoStream) return undefined
+
+  bitrate = videoStream?.bit_rate
+  if (bitrate && !isNaN(bitrate)) return bitrate
+
+  return undefined
 }
 
 async function getDurationFromVideoFile (path: string, existingProbe?: ffmpeg.FfprobeData) {

+ 23 - 5
server/lib/live/live-manager.ts

@@ -1,7 +1,13 @@
 
 import { createServer, Server } from 'net'
 import { isTestInstance } from '@server/helpers/core-utils'
-import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils'
+import {
+  computeResolutionsToTranscode,
+  ffprobePromise,
+  getVideoFileBitrate,
+  getVideoFileFPS,
+  getVideoFileResolution
+} from '@server/helpers/ffprobe-utils'
 import { logger, loggerTagsFactory } from '@server/helpers/logger'
 import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config'
 import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME } from '@server/initializers/constants'
@@ -193,11 +199,20 @@ class LiveManager {
 
     const rtmpUrl = 'rtmp://127.0.0.1:' + config.rtmp.port + streamPath
 
-    const [ { videoFileResolution }, fps ] = await Promise.all([
-      getVideoFileResolution(rtmpUrl),
-      getVideoFileFPS(rtmpUrl)
+    const now = Date.now()
+    const probe = await ffprobePromise(rtmpUrl)
+
+    const [ { videoFileResolution }, fps, bitrate ] = await Promise.all([
+      getVideoFileResolution(rtmpUrl, probe),
+      getVideoFileFPS(rtmpUrl, probe),
+      getVideoFileBitrate(rtmpUrl, probe)
     ])
 
+    logger.info(
+      '%s probing took %d ms (bitrate: %d, fps: %d, resolution: %d)',
+      rtmpUrl, Date.now() - now, bitrate, fps, videoFileResolution, lTags(sessionId, video.uuid)
+    )
+
     const allResolutions = this.buildAllResolutionsToTranscode(videoFileResolution)
 
     logger.info(
@@ -213,6 +228,7 @@ class LiveManager {
       streamingPlaylist,
       rtmpUrl,
       fps,
+      bitrate,
       allResolutions
     })
   }
@@ -223,9 +239,10 @@ class LiveManager {
     streamingPlaylist: MStreamingPlaylistVideo
     rtmpUrl: string
     fps: number
+    bitrate: number
     allResolutions: number[]
   }) {
-    const { sessionId, videoLive, streamingPlaylist, allResolutions, fps, rtmpUrl } = options
+    const { sessionId, videoLive, streamingPlaylist, allResolutions, fps, bitrate, rtmpUrl } = options
     const videoUUID = videoLive.Video.uuid
     const localLTags = lTags(sessionId, videoUUID)
 
@@ -239,6 +256,7 @@ class LiveManager {
       videoLive,
       streamingPlaylist,
       rtmpUrl,
+      bitrate,
       fps,
       allResolutions
     })

+ 5 - 0
server/lib/live/shared/muxing-session.ts

@@ -54,6 +54,7 @@ class MuxingSession extends EventEmitter {
   private readonly streamingPlaylist: MStreamingPlaylistVideo
   private readonly rtmpUrl: string
   private readonly fps: number
+  private readonly bitrate: number
   private readonly allResolutions: number[]
 
   private readonly videoId: number
@@ -83,6 +84,7 @@ class MuxingSession extends EventEmitter {
     streamingPlaylist: MStreamingPlaylistVideo
     rtmpUrl: string
     fps: number
+    bitrate: number
     allResolutions: number[]
   }) {
     super()
@@ -94,6 +96,7 @@ class MuxingSession extends EventEmitter {
     this.streamingPlaylist = options.streamingPlaylist
     this.rtmpUrl = options.rtmpUrl
     this.fps = options.fps
+    this.bitrate = options.bitrate
     this.allResolutions = options.allResolutions
 
     this.videoId = this.videoLive.Video.id
@@ -118,6 +121,8 @@ class MuxingSession extends EventEmitter {
 
         resolutions: this.allResolutions,
         fps: this.fps,
+        bitrate: this.bitrate,
+
         availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
         profile: CONFIG.LIVE.TRANSCODING.PROFILE
       })

+ 11 - 22
server/lib/transcoding/video-transcoding-profiles.ts

@@ -1,14 +1,7 @@
 import { logger } from '@server/helpers/logger'
 import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../../shared/models/videos'
 import { buildStreamSuffix, resetSupportedEncoders } from '../../helpers/ffmpeg-utils'
-import {
-  canDoQuickAudioTranscode,
-  ffprobePromise,
-  getAudioStream,
-  getMaxAudioBitrate,
-  getVideoFileBitrate,
-  getVideoStreamFromFile
-} from '../../helpers/ffprobe-utils'
+import { canDoQuickAudioTranscode, ffprobePromise, getAudioStream, getMaxAudioBitrate } from '../../helpers/ffprobe-utils'
 import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants'
 
 /**
@@ -22,8 +15,8 @@ import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants'
 //  * 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 }) => {
-  const targetBitrate = await buildTargetBitrate({ input, resolution, fps })
+const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ inputBitrate, resolution, fps }) => {
+  const targetBitrate = buildTargetBitrate({ inputBitrate, resolution, fps })
   if (!targetBitrate) return { outputOptions: [ ] }
 
   return {
@@ -36,8 +29,8 @@ const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ input, reso
   }
 }
 
-const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = async ({ resolution, fps, streamNum }) => {
-  const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
+const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = async ({ resolution, fps, inputBitrate, streamNum }) => {
+  const targetBitrate = buildTargetBitrate({ inputBitrate, resolution, fps })
 
   return {
     outputOptions: [
@@ -237,20 +230,16 @@ export {
 }
 
 // ---------------------------------------------------------------------------
-async function buildTargetBitrate (options: {
-  input: string
+
+function buildTargetBitrate (options: {
+  inputBitrate: number
   resolution: VideoResolution
   fps: number
 }) {
-  const { input, resolution, fps } = options
-  const probe = await ffprobePromise(input)
-
-  const videoStream = await getVideoStreamFromFile(input, probe)
-  if (!videoStream) return undefined
+  const { inputBitrate, resolution, fps } = options
 
   const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
+  if (!inputBitrate) return targetBitrate
 
-  // Don't transcode to an higher bitrate than the original file
-  const fileBitrate = await getVideoFileBitrate(input, probe)
-  return Math.min(targetBitrate, fileBitrate)
+  return Math.min(targetBitrate, inputBitrate)
 }

+ 2 - 2
server/tests/api/check-params/live.ts

@@ -417,7 +417,7 @@ describe('Test video lives API validator', function () {
 
       const live = await command.get({ videoId: video.id })
 
-      const ffmpegCommand = sendRTMPStream(live.rtmpUrl, live.streamKey)
+      const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
 
       await command.waitUntilPublished({ videoId: video.id })
       await command.update({ videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
@@ -430,7 +430,7 @@ describe('Test video lives API validator', function () {
 
       const live = await command.get({ videoId: video.id })
 
-      const ffmpegCommand = sendRTMPStream(live.rtmpUrl, live.streamKey)
+      const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
 
       await command.waitUntilPublished({ videoId: video.id })
 

+ 34 - 6
server/tests/api/live/live.ts

@@ -3,7 +3,7 @@
 import 'mocha'
 import * as chai from 'chai'
 import { basename, join } from 'path'
-import { ffprobePromise, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils'
+import { ffprobePromise, getVideoFileBitrate, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils'
 import {
   checkLiveCleanupAfterSave,
   checkLiveSegmentHash,
@@ -302,21 +302,21 @@ describe('Test live', function () {
 
       liveVideo = await createLiveWrapper()
 
-      const command = sendRTMPStream(rtmpUrl + '/bad-live', liveVideo.streamKey)
+      const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/bad-live', streamKey: liveVideo.streamKey })
       await testFfmpegStreamError(command, true)
     })
 
     it('Should not allow a stream without the appropriate stream key', async function () {
       this.timeout(60000)
 
-      const command = sendRTMPStream(rtmpUrl + '/live', 'bad-stream-key')
+      const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: 'bad-stream-key' })
       await testFfmpegStreamError(command, true)
     })
 
     it('Should succeed with the correct params', async function () {
       this.timeout(60000)
 
-      const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey)
+      const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey })
       await testFfmpegStreamError(command, false)
     })
 
@@ -340,7 +340,7 @@ describe('Test live', function () {
 
       await servers[0].blacklist.add({ videoId: liveVideo.uuid })
 
-      const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey)
+      const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey })
       await testFfmpegStreamError(command, true)
     })
 
@@ -351,7 +351,7 @@ describe('Test live', function () {
 
       await servers[0].videos.remove({ id: liveVideo.uuid })
 
-      const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey)
+      const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey })
       await testFfmpegStreamError(command, true)
     })
   })
@@ -468,6 +468,34 @@ describe('Test live', function () {
       await stopFfmpeg(ffmpegCommand)
     })
 
+    it('Should correctly set the appropriate bitrate depending on the input', async function () {
+      this.timeout(120000)
+
+      liveVideoId = await createLiveWrapper(false)
+
+      const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({
+        videoId: liveVideoId,
+        fixtureName: 'video_short.mp4',
+        copyCodecs: true
+      })
+      await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
+      await waitJobs(servers)
+
+      const video = await servers[0].videos.get({ id: liveVideoId })
+
+      const masterPlaylist = video.streamingPlaylists[0].playlistUrl
+      const probe = await ffprobePromise(masterPlaylist)
+
+      const bitrates = probe.streams.map(s => parseInt(s.tags.variant_bitrate))
+      for (const bitrate of bitrates) {
+        expect(bitrate).to.exist
+        expect(isNaN(bitrate)).to.be.false
+        expect(bitrate).to.be.below(61_000_000) // video_short.mp4 bitrate
+      }
+
+      await stopFfmpeg(ffmpegCommand)
+    })
+
     it('Should enable transcoding with some resolutions and correctly save them', async function () {
       this.timeout(200000)
 

+ 3 - 2
shared/extra-utils/videos/live-command.ts

@@ -68,11 +68,12 @@ export class LiveCommand extends AbstractCommand {
   async sendRTMPStreamInVideo (options: OverrideCommandOptions & {
     videoId: number | string
     fixtureName?: string
+    copyCodecs?: boolean
   }) {
-    const { videoId, fixtureName } = options
+    const { videoId, fixtureName, copyCodecs } = options
     const videoLive = await this.get({ videoId })
 
-    return sendRTMPStream(videoLive.rtmpUrl, videoLive.streamKey, fixtureName)
+    return sendRTMPStream({ rtmpBaseUrl: videoLive.rtmpUrl, streamKey: videoLive.streamKey, fixtureName, copyCodecs })
   }
 
   async runAndTestStreamError (options: OverrideCommandOptions & {

+ 18 - 5
shared/extra-utils/videos/live.ts

@@ -7,16 +7,29 @@ import { join } from 'path'
 import { buildAbsoluteFixturePath, wait } from '../miscs'
 import { PeerTubeServer } from '../server/server'
 
-function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = 'video_short.mp4') {
+function sendRTMPStream (options: {
+  rtmpBaseUrl: string
+  streamKey: string
+  fixtureName?: string // default video_short.mp4
+  copyCodecs?: boolean // default false
+}) {
+  const { rtmpBaseUrl, streamKey, fixtureName = 'video_short.mp4', copyCodecs = false } = options
+
   const fixture = buildAbsoluteFixturePath(fixtureName)
 
   const command = ffmpeg(fixture)
   command.inputOption('-stream_loop -1')
   command.inputOption('-re')
-  command.outputOption('-c:v libx264')
-  command.outputOption('-g 50')
-  command.outputOption('-keyint_min 2')
-  command.outputOption('-r 60')
+
+  if (copyCodecs) {
+    command.outputOption('-c:v libx264')
+    command.outputOption('-g 50')
+    command.outputOption('-keyint_min 2')
+    command.outputOption('-r 60')
+  } else {
+    command.outputOption('-c copy')
+  }
+
   command.outputOption('-f flv')
 
   const rtmpUrl = rtmpBaseUrl + '/' + streamKey

+ 1 - 0
shared/models/videos/video-transcoding.model.ts

@@ -5,6 +5,7 @@ import { VideoResolution } from './video-resolution.enum'
 export type EncoderOptionsBuilder = (params: {
   input: string
   resolution: VideoResolution
+  inputBitrate: number
   fps?: number
   streamNum?: number
 }) => Promise<EncoderOptions> | EncoderOptions