From 679c12e69c9f3a2d003ee3abe8b8da49f25b2bd3 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 6 Aug 2021 13:35:25 +0200 Subject: Improve target bitrate calculation --- shared/core-utils/index.ts | 1 + shared/core-utils/videos/bitrate.ts | 86 ++++++++++++++++++++++++ shared/core-utils/videos/index.ts | 1 + shared/extra-utils/miscs/generate.ts | 14 ++++ shared/models/videos/video-resolution.enum.ts | 89 ------------------------- shared/models/videos/video-transcoding.model.ts | 16 ++++- 6 files changed, 115 insertions(+), 92 deletions(-) create mode 100644 shared/core-utils/videos/bitrate.ts create mode 100644 shared/core-utils/videos/index.ts (limited to 'shared') diff --git a/shared/core-utils/index.ts b/shared/core-utils/index.ts index 2a7d4d982..e0a6a8087 100644 --- a/shared/core-utils/index.ts +++ b/shared/core-utils/index.ts @@ -5,3 +5,4 @@ export * from './plugins' export * from './renderer' export * from './users' export * from './utils' +export * from './videos' diff --git a/shared/core-utils/videos/bitrate.ts b/shared/core-utils/videos/bitrate.ts new file mode 100644 index 000000000..3d4e47906 --- /dev/null +++ b/shared/core-utils/videos/bitrate.ts @@ -0,0 +1,86 @@ +import { VideoResolution } from "@shared/models" + +type BitPerPixel = { [ id in VideoResolution ]: number } + +// https://bitmovin.com/video-bitrate-streaming-hls-dash/ + +const averageBitPerPixel: BitPerPixel = { + [VideoResolution.H_NOVIDEO]: 0, + [VideoResolution.H_240P]: 0.17, + [VideoResolution.H_360P]: 0.15, + [VideoResolution.H_480P]: 0.12, + [VideoResolution.H_720P]: 0.11, + [VideoResolution.H_1080P]: 0.10, + [VideoResolution.H_1440P]: 0.09, + [VideoResolution.H_4K]: 0.08 +} + +const maxBitPerPixel: BitPerPixel = { + [VideoResolution.H_NOVIDEO]: 0, + [VideoResolution.H_240P]: 0.29, + [VideoResolution.H_360P]: 0.26, + [VideoResolution.H_480P]: 0.22, + [VideoResolution.H_720P]: 0.19, + [VideoResolution.H_1080P]: 0.17, + [VideoResolution.H_1440P]: 0.16, + [VideoResolution.H_4K]: 0.14 +} + +function getAverageBitrate (options: { + resolution: VideoResolution + ratio: number + fps: number +}) { + const targetBitrate = calculateBitrate({ ...options, bitPerPixel: averageBitPerPixel }) + if (!targetBitrate) return 192 * 1000 + + return targetBitrate +} + +function getMaxBitrate (options: { + resolution: VideoResolution + ratio: number + fps: number +}) { + const targetBitrate = calculateBitrate({ ...options, bitPerPixel: maxBitPerPixel }) + if (!targetBitrate) return 256 * 1000 + + return targetBitrate +} + +// --------------------------------------------------------------------------- + +export { + getAverageBitrate, + getMaxBitrate +} + +// --------------------------------------------------------------------------- + +function calculateBitrate (options: { + bitPerPixel: BitPerPixel + resolution: VideoResolution + ratio: number + fps: number +}) { + const { bitPerPixel, resolution, ratio, fps } = options + + const resolutionsOrder = [ + VideoResolution.H_4K, + VideoResolution.H_1440P, + VideoResolution.H_1080P, + VideoResolution.H_720P, + VideoResolution.H_480P, + VideoResolution.H_360P, + VideoResolution.H_240P, + VideoResolution.H_NOVIDEO + ] + + for (const toTestResolution of resolutionsOrder) { + if (toTestResolution <= resolution) { + return resolution * resolution * ratio * fps * bitPerPixel[toTestResolution] + } + } + + throw new Error('Unknown resolution ' + resolution) +} diff --git a/shared/core-utils/videos/index.ts b/shared/core-utils/videos/index.ts new file mode 100644 index 000000000..5a1145f1a --- /dev/null +++ b/shared/core-utils/videos/index.ts @@ -0,0 +1 @@ +export * from './bitrate' diff --git a/shared/extra-utils/miscs/generate.ts b/shared/extra-utils/miscs/generate.ts index 8d6435481..a03a20049 100644 --- a/shared/extra-utils/miscs/generate.ts +++ b/shared/extra-utils/miscs/generate.ts @@ -1,8 +1,20 @@ +import { expect } from 'chai' import * as ffmpeg from 'fluent-ffmpeg' import { ensureDir, pathExists } from 'fs-extra' import { dirname } from 'path' +import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' +import { getMaxBitrate } from '@shared/core-utils' import { buildAbsoluteFixturePath } from './tests' +async function ensureHasTooBigBitrate (fixturePath: string) { + const bitrate = await getVideoFileBitrate(fixturePath) + const dataResolution = await getVideoFileResolution(fixturePath) + const fps = await getVideoFileFPS(fixturePath) + + const maxBitrate = getMaxBitrate({ ...dataResolution, fps }) + expect(bitrate).to.be.above(maxBitrate) +} + async function generateHighBitrateVideo () { const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true) @@ -28,6 +40,8 @@ async function generateHighBitrateVideo () { }) } + await ensureHasTooBigBitrate(tempFixturePath) + return tempFixturePath } diff --git a/shared/models/videos/video-resolution.enum.ts b/shared/models/videos/video-resolution.enum.ts index a5d2ac7fa..24cd2d04d 100644 --- a/shared/models/videos/video-resolution.enum.ts +++ b/shared/models/videos/video-resolution.enum.ts @@ -1,5 +1,3 @@ -import { VideoTranscodingFPS } from './video-transcoding-fps.model' - export const enum VideoResolution { H_NOVIDEO = 0, H_240P = 240, @@ -10,90 +8,3 @@ export const enum VideoResolution { H_1440P = 1440, H_4K = 2160 } - -/** - * Bitrate targets for different resolutions, at VideoTranscodingFPS.AVERAGE. - * - * Sources for individual quality levels: - * Google Live Encoder: https://support.google.com/youtube/answer/2853702?hl=en - * YouTube Video Info: youtube-dl --list-formats, with sample videos - */ -function getBaseBitrate (resolution: number) { - if (resolution === VideoResolution.H_NOVIDEO) { - // audio-only - return 64 * 1000 - } - - if (resolution <= VideoResolution.H_240P) { - // quality according to Google Live Encoder: 300 - 700 Kbps - // Quality according to YouTube Video Info: 285 Kbps - return 320 * 1000 - } - - if (resolution <= VideoResolution.H_360P) { - // quality according to Google Live Encoder: 400 - 1,000 Kbps - // Quality according to YouTube Video Info: 700 Kbps - return 780 * 1000 - } - - if (resolution <= VideoResolution.H_480P) { - // quality according to Google Live Encoder: 500 - 2,000 Kbps - // Quality according to YouTube Video Info: 1300 Kbps - return 1500 * 1000 - } - - if (resolution <= VideoResolution.H_720P) { - // quality according to Google Live Encoder: 1,500 - 4,000 Kbps - // Quality according to YouTube Video Info: 2680 Kbps - return 2800 * 1000 - } - - if (resolution <= VideoResolution.H_1080P) { - // quality according to Google Live Encoder: 3000 - 6000 Kbps - // Quality according to YouTube Video Info: 5081 Kbps - return 5200 * 1000 - } - - if (resolution <= VideoResolution.H_1440P) { - // quality according to Google Live Encoder: 6000 - 13000 Kbps - // Quality according to YouTube Video Info: 8600 (av01) - 17000 (vp9.2) Kbps - return 10_000 * 1000 - } - - // 4K - // quality according to Google Live Encoder: 13000 - 34000 Kbps - return 22_000 * 1000 -} - -/** - * Calculate the target bitrate based on video resolution and FPS. - * - * The calculation is based on two values: - * Bitrate at VideoTranscodingFPS.AVERAGE is always the same as - * getBaseBitrate(). Bitrate at VideoTranscodingFPS.MAX is always - * getBaseBitrate() * 1.4. All other values are calculated linearly - * between these two points. - */ -export function getTargetBitrate (resolution: number, fps: number, fpsTranscodingConstants: VideoTranscodingFPS) { - const baseBitrate = getBaseBitrate(resolution) - // The maximum bitrate, used when fps === VideoTranscodingFPS.MAX - // Based on numbers from Youtube, 60 fps bitrate divided by 30 fps bitrate: - // 720p: 2600 / 1750 = 1.49 - // 1080p: 4400 / 3300 = 1.33 - const maxBitrate = baseBitrate * 1.4 - const maxBitrateDifference = maxBitrate - baseBitrate - const maxFpsDifference = fpsTranscodingConstants.MAX - fpsTranscodingConstants.AVERAGE - // For 1080p video with default settings, this results in the following formula: - // 3300 + (x - 30) * (1320/30) - // Example outputs: - // 1080p10: 2420 kbps, 1080p30: 3300 kbps, 1080p60: 4620 kbps - // 720p10: 1283 kbps, 720p30: 1750 kbps, 720p60: 2450 kbps - return Math.floor(baseBitrate + (fps - fpsTranscodingConstants.AVERAGE) * (maxBitrateDifference / maxFpsDifference)) -} - -/** - * The maximum bitrate we expect to see on a transcoded video in bytes per second. - */ -export function getMaxBitrate (resolution: VideoResolution, fps: number, fpsTranscodingConstants: VideoTranscodingFPS) { - return getTargetBitrate(resolution, fps, fpsTranscodingConstants) * 2 -} diff --git a/shared/models/videos/video-transcoding.model.ts b/shared/models/videos/video-transcoding.model.ts index f1fe4609b..83b8e98a0 100644 --- a/shared/models/videos/video-transcoding.model.ts +++ b/shared/models/videos/video-transcoding.model.ts @@ -2,13 +2,23 @@ import { VideoResolution } from './video-resolution.enum' // Types used by plugins and ffmpeg-utils -export type EncoderOptionsBuilder = (params: { +export type EncoderOptionsBuilderParams = { input: string + resolution: VideoResolution - inputBitrate: number + + // Could be null for "merge audio" transcoding fps?: number + + // Could be undefined if we could not get input bitrate (some RTMP streams for example) + inputBitrate: number + inputRatio: number + + // For lives streamNum?: number -}) => Promise | EncoderOptions +} + +export type EncoderOptionsBuilder = (params: EncoderOptionsBuilderParams) => Promise | EncoderOptions export interface EncoderOptions { copy?: boolean // Copy stream? Default to false -- cgit v1.2.3