Limit live bitrate
This commit is contained in:
parent
421ff4618d
commit
c826f34a45
|
@ -6,7 +6,7 @@ import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants'
|
||||||
import { AvailableEncoders, EncoderOptions, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos'
|
import { AvailableEncoders, EncoderOptions, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos'
|
||||||
import { CONFIG } from '../initializers/config'
|
import { CONFIG } from '../initializers/config'
|
||||||
import { execPromise, promisify0 } from './core-utils'
|
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 { processImage } from './image-utils'
|
||||||
import { logger } from './logger'
|
import { logger } from './logger'
|
||||||
|
|
||||||
|
@ -218,11 +218,12 @@ async function getLiveTranscodingCommand (options: {
|
||||||
|
|
||||||
resolutions: number[]
|
resolutions: number[]
|
||||||
fps: number
|
fps: number
|
||||||
|
bitrate: number
|
||||||
|
|
||||||
availableEncoders: AvailableEncoders
|
availableEncoders: AvailableEncoders
|
||||||
profile: string
|
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 input = rtmpUrl
|
||||||
|
|
||||||
const command = getFFmpeg(input, 'live')
|
const command = getFFmpeg(input, 'live')
|
||||||
|
@ -253,6 +254,7 @@ async function getLiveTranscodingCommand (options: {
|
||||||
profile,
|
profile,
|
||||||
|
|
||||||
fps: resolutionFPS,
|
fps: resolutionFPS,
|
||||||
|
inputBitrate: bitrate,
|
||||||
resolution,
|
resolution,
|
||||||
streamNum: i,
|
streamNum: i,
|
||||||
videoType: 'live' as 'live'
|
videoType: 'live' as 'live'
|
||||||
|
@ -260,7 +262,7 @@ async function getLiveTranscodingCommand (options: {
|
||||||
|
|
||||||
{
|
{
|
||||||
const streamType: StreamType = 'video'
|
const streamType: StreamType = 'video'
|
||||||
const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType }))
|
const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType })
|
||||||
if (!builderResult) {
|
if (!builderResult) {
|
||||||
throw new Error('No available live video encoder found')
|
throw new Error('No available live video encoder found')
|
||||||
}
|
}
|
||||||
|
@ -284,7 +286,7 @@ async function getLiveTranscodingCommand (options: {
|
||||||
|
|
||||||
{
|
{
|
||||||
const streamType: StreamType = 'audio'
|
const streamType: StreamType = 'audio'
|
||||||
const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType }))
|
const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType })
|
||||||
if (!builderResult) {
|
if (!builderResult) {
|
||||||
throw new Error('No available live audio encoder found')
|
throw new Error('No available live audio encoder found')
|
||||||
}
|
}
|
||||||
|
@ -510,10 +512,11 @@ async function getEncoderBuilderResult (options: {
|
||||||
videoType: 'vod' | 'live'
|
videoType: 'vod' | 'live'
|
||||||
|
|
||||||
resolution: number
|
resolution: number
|
||||||
|
inputBitrate: number
|
||||||
fps?: number
|
fps?: number
|
||||||
streamNum?: 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 encodersToTry = availableEncoders.encodersToTry[videoType][streamType]
|
||||||
const encoders = availableEncoders.available[videoType]
|
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 {
|
return {
|
||||||
result,
|
result,
|
||||||
|
@ -573,8 +576,11 @@ async function presetVideo (options: {
|
||||||
|
|
||||||
addDefaultEncoderGlobalParams({ command })
|
addDefaultEncoderGlobalParams({ command })
|
||||||
|
|
||||||
|
const probe = await ffprobePromise(input)
|
||||||
|
|
||||||
// Audio encoder
|
// Audio encoder
|
||||||
const parsedAudio = await getAudioStream(input)
|
const parsedAudio = await getAudioStream(input, probe)
|
||||||
|
const bitrate = await getVideoFileBitrate(input, probe)
|
||||||
|
|
||||||
let streamsToProcess: StreamType[] = [ 'audio', 'video' ]
|
let streamsToProcess: StreamType[] = [ 'audio', 'video' ]
|
||||||
|
|
||||||
|
@ -593,6 +599,7 @@ async function presetVideo (options: {
|
||||||
availableEncoders,
|
availableEncoders,
|
||||||
profile,
|
profile,
|
||||||
fps,
|
fps,
|
||||||
|
inputBitrate: bitrate,
|
||||||
videoType: 'vod' as 'vod'
|
videoType: 'vod' as 'vod'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -175,10 +175,19 @@ async function getMetadataFromFile (path: string, existingProbe?: ffmpeg.Ffprobe
|
||||||
return new VideoFileMetadata(metadata)
|
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)
|
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) {
|
async function getDurationFromVideoFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
|
|
||||||
import { createServer, Server } from 'net'
|
import { createServer, Server } from 'net'
|
||||||
import { isTestInstance } from '@server/helpers/core-utils'
|
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 { logger, loggerTagsFactory } from '@server/helpers/logger'
|
||||||
import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config'
|
import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config'
|
||||||
import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME } from '@server/initializers/constants'
|
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 rtmpUrl = 'rtmp://127.0.0.1:' + config.rtmp.port + streamPath
|
||||||
|
|
||||||
const [ { videoFileResolution }, fps ] = await Promise.all([
|
const now = Date.now()
|
||||||
getVideoFileResolution(rtmpUrl),
|
const probe = await ffprobePromise(rtmpUrl)
|
||||||
getVideoFileFPS(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)
|
const allResolutions = this.buildAllResolutionsToTranscode(videoFileResolution)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
@ -213,6 +228,7 @@ class LiveManager {
|
||||||
streamingPlaylist,
|
streamingPlaylist,
|
||||||
rtmpUrl,
|
rtmpUrl,
|
||||||
fps,
|
fps,
|
||||||
|
bitrate,
|
||||||
allResolutions
|
allResolutions
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -223,9 +239,10 @@ class LiveManager {
|
||||||
streamingPlaylist: MStreamingPlaylistVideo
|
streamingPlaylist: MStreamingPlaylistVideo
|
||||||
rtmpUrl: string
|
rtmpUrl: string
|
||||||
fps: number
|
fps: number
|
||||||
|
bitrate: number
|
||||||
allResolutions: 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 videoUUID = videoLive.Video.uuid
|
||||||
const localLTags = lTags(sessionId, videoUUID)
|
const localLTags = lTags(sessionId, videoUUID)
|
||||||
|
|
||||||
|
@ -239,6 +256,7 @@ class LiveManager {
|
||||||
videoLive,
|
videoLive,
|
||||||
streamingPlaylist,
|
streamingPlaylist,
|
||||||
rtmpUrl,
|
rtmpUrl,
|
||||||
|
bitrate,
|
||||||
fps,
|
fps,
|
||||||
allResolutions
|
allResolutions
|
||||||
})
|
})
|
||||||
|
|
|
@ -54,6 +54,7 @@ class MuxingSession extends EventEmitter {
|
||||||
private readonly streamingPlaylist: MStreamingPlaylistVideo
|
private readonly streamingPlaylist: MStreamingPlaylistVideo
|
||||||
private readonly rtmpUrl: string
|
private readonly rtmpUrl: string
|
||||||
private readonly fps: number
|
private readonly fps: number
|
||||||
|
private readonly bitrate: number
|
||||||
private readonly allResolutions: number[]
|
private readonly allResolutions: number[]
|
||||||
|
|
||||||
private readonly videoId: number
|
private readonly videoId: number
|
||||||
|
@ -83,6 +84,7 @@ class MuxingSession extends EventEmitter {
|
||||||
streamingPlaylist: MStreamingPlaylistVideo
|
streamingPlaylist: MStreamingPlaylistVideo
|
||||||
rtmpUrl: string
|
rtmpUrl: string
|
||||||
fps: number
|
fps: number
|
||||||
|
bitrate: number
|
||||||
allResolutions: number[]
|
allResolutions: number[]
|
||||||
}) {
|
}) {
|
||||||
super()
|
super()
|
||||||
|
@ -94,6 +96,7 @@ class MuxingSession extends EventEmitter {
|
||||||
this.streamingPlaylist = options.streamingPlaylist
|
this.streamingPlaylist = options.streamingPlaylist
|
||||||
this.rtmpUrl = options.rtmpUrl
|
this.rtmpUrl = options.rtmpUrl
|
||||||
this.fps = options.fps
|
this.fps = options.fps
|
||||||
|
this.bitrate = options.bitrate
|
||||||
this.allResolutions = options.allResolutions
|
this.allResolutions = options.allResolutions
|
||||||
|
|
||||||
this.videoId = this.videoLive.Video.id
|
this.videoId = this.videoLive.Video.id
|
||||||
|
@ -118,6 +121,8 @@ class MuxingSession extends EventEmitter {
|
||||||
|
|
||||||
resolutions: this.allResolutions,
|
resolutions: this.allResolutions,
|
||||||
fps: this.fps,
|
fps: this.fps,
|
||||||
|
bitrate: this.bitrate,
|
||||||
|
|
||||||
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
||||||
profile: CONFIG.LIVE.TRANSCODING.PROFILE
|
profile: CONFIG.LIVE.TRANSCODING.PROFILE
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
import { logger } from '@server/helpers/logger'
|
import { logger } from '@server/helpers/logger'
|
||||||
import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../../shared/models/videos'
|
import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../../shared/models/videos'
|
||||||
import { buildStreamSuffix, resetSupportedEncoders } from '../../helpers/ffmpeg-utils'
|
import { buildStreamSuffix, resetSupportedEncoders } from '../../helpers/ffmpeg-utils'
|
||||||
import {
|
import { canDoQuickAudioTranscode, ffprobePromise, getAudioStream, getMaxAudioBitrate } from '../../helpers/ffprobe-utils'
|
||||||
canDoQuickAudioTranscode,
|
|
||||||
ffprobePromise,
|
|
||||||
getAudioStream,
|
|
||||||
getMaxAudioBitrate,
|
|
||||||
getVideoFileBitrate,
|
|
||||||
getVideoStreamFromFile
|
|
||||||
} from '../../helpers/ffprobe-utils'
|
|
||||||
import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants'
|
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://slhck.info/video/2017/03/01/rate-control.html
|
||||||
// * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
|
// * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
|
||||||
|
|
||||||
const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ input, resolution, fps }) => {
|
const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ inputBitrate, resolution, fps }) => {
|
||||||
const targetBitrate = await buildTargetBitrate({ input, resolution, fps })
|
const targetBitrate = buildTargetBitrate({ inputBitrate, resolution, fps })
|
||||||
if (!targetBitrate) return { outputOptions: [ ] }
|
if (!targetBitrate) return { outputOptions: [ ] }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -36,8 +29,8 @@ const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ input, reso
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = async ({ resolution, fps, streamNum }) => {
|
const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = async ({ resolution, fps, inputBitrate, streamNum }) => {
|
||||||
const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
|
const targetBitrate = buildTargetBitrate({ inputBitrate, resolution, fps })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -237,20 +230,16 @@ export {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
async function buildTargetBitrate (options: {
|
|
||||||
input: string
|
function buildTargetBitrate (options: {
|
||||||
|
inputBitrate: number
|
||||||
resolution: VideoResolution
|
resolution: VideoResolution
|
||||||
fps: number
|
fps: number
|
||||||
}) {
|
}) {
|
||||||
const { input, resolution, fps } = options
|
const { inputBitrate, resolution, fps } = options
|
||||||
const probe = await ffprobePromise(input)
|
|
||||||
|
|
||||||
const videoStream = await getVideoStreamFromFile(input, probe)
|
|
||||||
if (!videoStream) return undefined
|
|
||||||
|
|
||||||
const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
|
const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
|
||||||
|
if (!inputBitrate) return targetBitrate
|
||||||
|
|
||||||
// Don't transcode to an higher bitrate than the original file
|
return Math.min(targetBitrate, inputBitrate)
|
||||||
const fileBitrate = await getVideoFileBitrate(input, probe)
|
|
||||||
return Math.min(targetBitrate, fileBitrate)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -417,7 +417,7 @@ describe('Test video lives API validator', function () {
|
||||||
|
|
||||||
const live = await command.get({ videoId: video.id })
|
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.waitUntilPublished({ videoId: video.id })
|
||||||
await command.update({ videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
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 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.waitUntilPublished({ videoId: video.id })
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
import { basename, join } from 'path'
|
import { basename, join } from 'path'
|
||||||
import { ffprobePromise, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils'
|
import { ffprobePromise, getVideoFileBitrate, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils'
|
||||||
import {
|
import {
|
||||||
checkLiveCleanupAfterSave,
|
checkLiveCleanupAfterSave,
|
||||||
checkLiveSegmentHash,
|
checkLiveSegmentHash,
|
||||||
|
@ -302,21 +302,21 @@ describe('Test live', function () {
|
||||||
|
|
||||||
liveVideo = await createLiveWrapper()
|
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)
|
await testFfmpegStreamError(command, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not allow a stream without the appropriate stream key', async function () {
|
it('Should not allow a stream without the appropriate stream key', async function () {
|
||||||
this.timeout(60000)
|
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)
|
await testFfmpegStreamError(command, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should succeed with the correct params', async function () {
|
it('Should succeed with the correct params', async function () {
|
||||||
this.timeout(60000)
|
this.timeout(60000)
|
||||||
|
|
||||||
const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey)
|
const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey })
|
||||||
await testFfmpegStreamError(command, false)
|
await testFfmpegStreamError(command, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -340,7 +340,7 @@ describe('Test live', function () {
|
||||||
|
|
||||||
await servers[0].blacklist.add({ videoId: liveVideo.uuid })
|
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)
|
await testFfmpegStreamError(command, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -351,7 +351,7 @@ describe('Test live', function () {
|
||||||
|
|
||||||
await servers[0].videos.remove({ id: liveVideo.uuid })
|
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)
|
await testFfmpegStreamError(command, true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -468,6 +468,34 @@ describe('Test live', function () {
|
||||||
await stopFfmpeg(ffmpegCommand)
|
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 () {
|
it('Should enable transcoding with some resolutions and correctly save them', async function () {
|
||||||
this.timeout(200000)
|
this.timeout(200000)
|
||||||
|
|
||||||
|
|
|
@ -68,11 +68,12 @@ export class LiveCommand extends AbstractCommand {
|
||||||
async sendRTMPStreamInVideo (options: OverrideCommandOptions & {
|
async sendRTMPStreamInVideo (options: OverrideCommandOptions & {
|
||||||
videoId: number | string
|
videoId: number | string
|
||||||
fixtureName?: string
|
fixtureName?: string
|
||||||
|
copyCodecs?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { videoId, fixtureName } = options
|
const { videoId, fixtureName, copyCodecs } = options
|
||||||
const videoLive = await this.get({ videoId })
|
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 & {
|
async runAndTestStreamError (options: OverrideCommandOptions & {
|
||||||
|
|
|
@ -7,16 +7,29 @@ import { join } from 'path'
|
||||||
import { buildAbsoluteFixturePath, wait } from '../miscs'
|
import { buildAbsoluteFixturePath, wait } from '../miscs'
|
||||||
import { PeerTubeServer } from '../server/server'
|
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 fixture = buildAbsoluteFixturePath(fixtureName)
|
||||||
|
|
||||||
const command = ffmpeg(fixture)
|
const command = ffmpeg(fixture)
|
||||||
command.inputOption('-stream_loop -1')
|
command.inputOption('-stream_loop -1')
|
||||||
command.inputOption('-re')
|
command.inputOption('-re')
|
||||||
command.outputOption('-c:v libx264')
|
|
||||||
command.outputOption('-g 50')
|
if (copyCodecs) {
|
||||||
command.outputOption('-keyint_min 2')
|
command.outputOption('-c:v libx264')
|
||||||
command.outputOption('-r 60')
|
command.outputOption('-g 50')
|
||||||
|
command.outputOption('-keyint_min 2')
|
||||||
|
command.outputOption('-r 60')
|
||||||
|
} else {
|
||||||
|
command.outputOption('-c copy')
|
||||||
|
}
|
||||||
|
|
||||||
command.outputOption('-f flv')
|
command.outputOption('-f flv')
|
||||||
|
|
||||||
const rtmpUrl = rtmpBaseUrl + '/' + streamKey
|
const rtmpUrl = rtmpBaseUrl + '/' + streamKey
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { VideoResolution } from './video-resolution.enum'
|
||||||
export type EncoderOptionsBuilder = (params: {
|
export type EncoderOptionsBuilder = (params: {
|
||||||
input: string
|
input: string
|
||||||
resolution: VideoResolution
|
resolution: VideoResolution
|
||||||
|
inputBitrate: number
|
||||||
fps?: number
|
fps?: number
|
||||||
streamNum?: number
|
streamNum?: number
|
||||||
}) => Promise<EncoderOptions> | EncoderOptions
|
}) => Promise<EncoderOptions> | EncoderOptions
|
||||||
|
|
Loading…
Reference in New Issue