diff options
Diffstat (limited to 'shared')
-rw-r--r-- | shared/core-utils/index.ts | 1 | ||||
-rw-r--r-- | shared/core-utils/videos/bitrate.ts | 86 | ||||
-rw-r--r-- | shared/core-utils/videos/index.ts | 1 | ||||
-rw-r--r-- | shared/extra-utils/miscs/generate.ts | 14 | ||||
-rw-r--r-- | shared/models/videos/video-resolution.enum.ts | 89 | ||||
-rw-r--r-- | shared/models/videos/video-transcoding.model.ts | 16 |
6 files changed, 115 insertions, 92 deletions
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' | |||
5 | export * from './renderer' | 5 | export * from './renderer' |
6 | export * from './users' | 6 | export * from './users' |
7 | export * from './utils' | 7 | export * from './utils' |
8 | 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 @@ | |||
1 | import { VideoResolution } from "@shared/models" | ||
2 | |||
3 | type BitPerPixel = { [ id in VideoResolution ]: number } | ||
4 | |||
5 | // https://bitmovin.com/video-bitrate-streaming-hls-dash/ | ||
6 | |||
7 | const averageBitPerPixel: BitPerPixel = { | ||
8 | [VideoResolution.H_NOVIDEO]: 0, | ||
9 | [VideoResolution.H_240P]: 0.17, | ||
10 | [VideoResolution.H_360P]: 0.15, | ||
11 | [VideoResolution.H_480P]: 0.12, | ||
12 | [VideoResolution.H_720P]: 0.11, | ||
13 | [VideoResolution.H_1080P]: 0.10, | ||
14 | [VideoResolution.H_1440P]: 0.09, | ||
15 | [VideoResolution.H_4K]: 0.08 | ||
16 | } | ||
17 | |||
18 | const maxBitPerPixel: BitPerPixel = { | ||
19 | [VideoResolution.H_NOVIDEO]: 0, | ||
20 | [VideoResolution.H_240P]: 0.29, | ||
21 | [VideoResolution.H_360P]: 0.26, | ||
22 | [VideoResolution.H_480P]: 0.22, | ||
23 | [VideoResolution.H_720P]: 0.19, | ||
24 | [VideoResolution.H_1080P]: 0.17, | ||
25 | [VideoResolution.H_1440P]: 0.16, | ||
26 | [VideoResolution.H_4K]: 0.14 | ||
27 | } | ||
28 | |||
29 | function getAverageBitrate (options: { | ||
30 | resolution: VideoResolution | ||
31 | ratio: number | ||
32 | fps: number | ||
33 | }) { | ||
34 | const targetBitrate = calculateBitrate({ ...options, bitPerPixel: averageBitPerPixel }) | ||
35 | if (!targetBitrate) return 192 * 1000 | ||
36 | |||
37 | return targetBitrate | ||
38 | } | ||
39 | |||
40 | function getMaxBitrate (options: { | ||
41 | resolution: VideoResolution | ||
42 | ratio: number | ||
43 | fps: number | ||
44 | }) { | ||
45 | const targetBitrate = calculateBitrate({ ...options, bitPerPixel: maxBitPerPixel }) | ||
46 | if (!targetBitrate) return 256 * 1000 | ||
47 | |||
48 | return targetBitrate | ||
49 | } | ||
50 | |||
51 | // --------------------------------------------------------------------------- | ||
52 | |||
53 | export { | ||
54 | getAverageBitrate, | ||
55 | getMaxBitrate | ||
56 | } | ||
57 | |||
58 | // --------------------------------------------------------------------------- | ||
59 | |||
60 | function calculateBitrate (options: { | ||
61 | bitPerPixel: BitPerPixel | ||
62 | resolution: VideoResolution | ||
63 | ratio: number | ||
64 | fps: number | ||
65 | }) { | ||
66 | const { bitPerPixel, resolution, ratio, fps } = options | ||
67 | |||
68 | const resolutionsOrder = [ | ||
69 | VideoResolution.H_4K, | ||
70 | VideoResolution.H_1440P, | ||
71 | VideoResolution.H_1080P, | ||
72 | VideoResolution.H_720P, | ||
73 | VideoResolution.H_480P, | ||
74 | VideoResolution.H_360P, | ||
75 | VideoResolution.H_240P, | ||
76 | VideoResolution.H_NOVIDEO | ||
77 | ] | ||
78 | |||
79 | for (const toTestResolution of resolutionsOrder) { | ||
80 | if (toTestResolution <= resolution) { | ||
81 | return resolution * resolution * ratio * fps * bitPerPixel[toTestResolution] | ||
82 | } | ||
83 | } | ||
84 | |||
85 | throw new Error('Unknown resolution ' + resolution) | ||
86 | } | ||
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 @@ | |||
1 | import { expect } from 'chai' | ||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 2 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { ensureDir, pathExists } from 'fs-extra' | 3 | import { ensureDir, pathExists } from 'fs-extra' |
3 | import { dirname } from 'path' | 4 | import { dirname } from 'path' |
5 | import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' | ||
6 | import { getMaxBitrate } from '@shared/core-utils' | ||
4 | import { buildAbsoluteFixturePath } from './tests' | 7 | import { buildAbsoluteFixturePath } from './tests' |
5 | 8 | ||
9 | async function ensureHasTooBigBitrate (fixturePath: string) { | ||
10 | const bitrate = await getVideoFileBitrate(fixturePath) | ||
11 | const dataResolution = await getVideoFileResolution(fixturePath) | ||
12 | const fps = await getVideoFileFPS(fixturePath) | ||
13 | |||
14 | const maxBitrate = getMaxBitrate({ ...dataResolution, fps }) | ||
15 | expect(bitrate).to.be.above(maxBitrate) | ||
16 | } | ||
17 | |||
6 | async function generateHighBitrateVideo () { | 18 | async function generateHighBitrateVideo () { |
7 | const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true) | 19 | const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true) |
8 | 20 | ||
@@ -28,6 +40,8 @@ async function generateHighBitrateVideo () { | |||
28 | }) | 40 | }) |
29 | } | 41 | } |
30 | 42 | ||
43 | await ensureHasTooBigBitrate(tempFixturePath) | ||
44 | |||
31 | return tempFixturePath | 45 | return tempFixturePath |
32 | } | 46 | } |
33 | 47 | ||
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 @@ | |||
1 | import { VideoTranscodingFPS } from './video-transcoding-fps.model' | ||
2 | |||
3 | export const enum VideoResolution { | 1 | export const enum VideoResolution { |
4 | H_NOVIDEO = 0, | 2 | H_NOVIDEO = 0, |
5 | H_240P = 240, | 3 | H_240P = 240, |
@@ -10,90 +8,3 @@ export const enum VideoResolution { | |||
10 | H_1440P = 1440, | 8 | H_1440P = 1440, |
11 | H_4K = 2160 | 9 | H_4K = 2160 |
12 | } | 10 | } |
13 | |||
14 | /** | ||
15 | * Bitrate targets for different resolutions, at VideoTranscodingFPS.AVERAGE. | ||
16 | * | ||
17 | * Sources for individual quality levels: | ||
18 | * Google Live Encoder: https://support.google.com/youtube/answer/2853702?hl=en | ||
19 | * YouTube Video Info: youtube-dl --list-formats, with sample videos | ||
20 | */ | ||
21 | function getBaseBitrate (resolution: number) { | ||
22 | if (resolution === VideoResolution.H_NOVIDEO) { | ||
23 | // audio-only | ||
24 | return 64 * 1000 | ||
25 | } | ||
26 | |||
27 | if (resolution <= VideoResolution.H_240P) { | ||
28 | // quality according to Google Live Encoder: 300 - 700 Kbps | ||
29 | // Quality according to YouTube Video Info: 285 Kbps | ||
30 | return 320 * 1000 | ||
31 | } | ||
32 | |||
33 | if (resolution <= VideoResolution.H_360P) { | ||
34 | // quality according to Google Live Encoder: 400 - 1,000 Kbps | ||
35 | // Quality according to YouTube Video Info: 700 Kbps | ||
36 | return 780 * 1000 | ||
37 | } | ||
38 | |||
39 | if (resolution <= VideoResolution.H_480P) { | ||
40 | // quality according to Google Live Encoder: 500 - 2,000 Kbps | ||
41 | // Quality according to YouTube Video Info: 1300 Kbps | ||
42 | return 1500 * 1000 | ||
43 | } | ||
44 | |||
45 | if (resolution <= VideoResolution.H_720P) { | ||
46 | // quality according to Google Live Encoder: 1,500 - 4,000 Kbps | ||
47 | // Quality according to YouTube Video Info: 2680 Kbps | ||
48 | return 2800 * 1000 | ||
49 | } | ||
50 | |||
51 | if (resolution <= VideoResolution.H_1080P) { | ||
52 | // quality according to Google Live Encoder: 3000 - 6000 Kbps | ||
53 | // Quality according to YouTube Video Info: 5081 Kbps | ||
54 | return 5200 * 1000 | ||
55 | } | ||
56 | |||
57 | if (resolution <= VideoResolution.H_1440P) { | ||
58 | // quality according to Google Live Encoder: 6000 - 13000 Kbps | ||
59 | // Quality according to YouTube Video Info: 8600 (av01) - 17000 (vp9.2) Kbps | ||
60 | return 10_000 * 1000 | ||
61 | } | ||
62 | |||
63 | // 4K | ||
64 | // quality according to Google Live Encoder: 13000 - 34000 Kbps | ||
65 | return 22_000 * 1000 | ||
66 | } | ||
67 | |||
68 | /** | ||
69 | * Calculate the target bitrate based on video resolution and FPS. | ||
70 | * | ||
71 | * The calculation is based on two values: | ||
72 | * Bitrate at VideoTranscodingFPS.AVERAGE is always the same as | ||
73 | * getBaseBitrate(). Bitrate at VideoTranscodingFPS.MAX is always | ||
74 | * getBaseBitrate() * 1.4. All other values are calculated linearly | ||
75 | * between these two points. | ||
76 | */ | ||
77 | export function getTargetBitrate (resolution: number, fps: number, fpsTranscodingConstants: VideoTranscodingFPS) { | ||
78 | const baseBitrate = getBaseBitrate(resolution) | ||
79 | // The maximum bitrate, used when fps === VideoTranscodingFPS.MAX | ||
80 | // Based on numbers from Youtube, 60 fps bitrate divided by 30 fps bitrate: | ||
81 | // 720p: 2600 / 1750 = 1.49 | ||
82 | // 1080p: 4400 / 3300 = 1.33 | ||
83 | const maxBitrate = baseBitrate * 1.4 | ||
84 | const maxBitrateDifference = maxBitrate - baseBitrate | ||
85 | const maxFpsDifference = fpsTranscodingConstants.MAX - fpsTranscodingConstants.AVERAGE | ||
86 | // For 1080p video with default settings, this results in the following formula: | ||
87 | // 3300 + (x - 30) * (1320/30) | ||
88 | // Example outputs: | ||
89 | // 1080p10: 2420 kbps, 1080p30: 3300 kbps, 1080p60: 4620 kbps | ||
90 | // 720p10: 1283 kbps, 720p30: 1750 kbps, 720p60: 2450 kbps | ||
91 | return Math.floor(baseBitrate + (fps - fpsTranscodingConstants.AVERAGE) * (maxBitrateDifference / maxFpsDifference)) | ||
92 | } | ||
93 | |||
94 | /** | ||
95 | * The maximum bitrate we expect to see on a transcoded video in bytes per second. | ||
96 | */ | ||
97 | export function getMaxBitrate (resolution: VideoResolution, fps: number, fpsTranscodingConstants: VideoTranscodingFPS) { | ||
98 | return getTargetBitrate(resolution, fps, fpsTranscodingConstants) * 2 | ||
99 | } | ||
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' | |||
2 | 2 | ||
3 | // Types used by plugins and ffmpeg-utils | 3 | // Types used by plugins and ffmpeg-utils |
4 | 4 | ||
5 | export type EncoderOptionsBuilder = (params: { | 5 | export type EncoderOptionsBuilderParams = { |
6 | input: string | 6 | input: string |
7 | |||
7 | resolution: VideoResolution | 8 | resolution: VideoResolution |
8 | inputBitrate: number | 9 | |
10 | // Could be null for "merge audio" transcoding | ||
9 | fps?: number | 11 | fps?: number |
12 | |||
13 | // Could be undefined if we could not get input bitrate (some RTMP streams for example) | ||
14 | inputBitrate: number | ||
15 | inputRatio: number | ||
16 | |||
17 | // For lives | ||
10 | streamNum?: number | 18 | streamNum?: number |
11 | }) => Promise<EncoderOptions> | EncoderOptions | 19 | } |
20 | |||
21 | export type EncoderOptionsBuilder = (params: EncoderOptionsBuilderParams) => Promise<EncoderOptions> | EncoderOptions | ||
12 | 22 | ||
13 | export interface EncoderOptions { | 23 | export interface EncoderOptions { |
14 | copy?: boolean // Copy stream? Default to false | 24 | copy?: boolean // Copy stream? Default to false |