Limit live bitrate

pull/4317/head
Chocobozzz 2021-08-06 10:39:40 +02:00
parent 421ff4618d
commit c826f34a45
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
10 changed files with 122 additions and 51 deletions

View File

@ -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'
})

View File

@ -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) {

View File

@ -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
})

View File

@ -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
})

View File

@ -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)
}

View File

@ -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 })

View File

@ -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)

View File

@ -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 & {

View File

@ -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

View File

@ -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