diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 61c8a6db2..7f84a049f 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/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' }) diff --git a/server/helpers/ffprobe-utils.ts b/server/helpers/ffprobe-utils.ts index ef2aa3f89..bc87e49b1 100644 --- a/server/helpers/ffprobe-utils.ts +++ b/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 { 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) { diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts index f106d69fb..b19ecef6f 100644 --- a/server/lib/live/live-manager.ts +++ b/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 }) diff --git a/server/lib/live/shared/muxing-session.ts b/server/lib/live/shared/muxing-session.ts index 709d6c615..62708b14b 100644 --- a/server/lib/live/shared/muxing-session.ts +++ b/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 }) diff --git a/server/lib/transcoding/video-transcoding-profiles.ts b/server/lib/transcoding/video-transcoding-profiles.ts index c5ea72a5f..2309f38d4 100644 --- a/server/lib/transcoding/video-transcoding-profiles.ts +++ b/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) } diff --git a/server/tests/api/check-params/live.ts b/server/tests/api/check-params/live.ts index 700b4724d..8e1d655d4 100644 --- a/server/tests/api/check-params/live.ts +++ b/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 }) diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index d555cff19..4095cdb1c 100644 --- a/server/tests/api/live/live.ts +++ b/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) diff --git a/shared/extra-utils/videos/live-command.ts b/shared/extra-utils/videos/live-command.ts index bf9486a05..81ae458e0 100644 --- a/shared/extra-utils/videos/live-command.ts +++ b/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 & { diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts index 94f5f5b59..6aa405b19 100644 --- a/shared/extra-utils/videos/live.ts +++ b/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 diff --git a/shared/models/videos/video-transcoding.model.ts b/shared/models/videos/video-transcoding.model.ts index 3f2382ce8..f1fe4609b 100644 --- a/shared/models/videos/video-transcoding.model.ts +++ b/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