Limit live bitrate
This commit is contained in:
parent
421ff4618d
commit
c826f34a45
10 changed files with 122 additions and 51 deletions
|
@ -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'
|
||||
})
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 & {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue