]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/transcoding/video-transcoding-profiles.ts
dcc8d4c5cdb87cf6612606a6fbf4d0d5aca8026b
[github/Chocobozzz/PeerTube.git] / server / lib / transcoding / video-transcoding-profiles.ts
1
2 import { logger } from '@server/helpers/logger'
3 import { getAverageBitrate, getMinLimitBitrate } from '@shared/core-utils'
4 import { AvailableEncoders, EncoderOptionsBuilder, EncoderOptionsBuilderParams, VideoResolution } from '../../../shared/models/videos'
5 import { buildStreamSuffix, resetSupportedEncoders } from '../../helpers/ffmpeg-utils'
6 import { canDoQuickAudioTranscode, ffprobePromise, getAudioStream, getMaxAudioBitrate } from '../../helpers/ffprobe-utils'
7
8 /**
9 *
10 * Available encoders and profiles for the transcoding jobs
11 * These functions are used by ffmpeg-utils that will get the encoders and options depending on the chosen profile
12 *
13 * Resources:
14 * * https://slhck.info/video/2017/03/01/rate-control.html
15 * * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
16 */
17
18 const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = (options: EncoderOptionsBuilderParams) => {
19 const { fps, inputRatio, inputBitrate, resolution } = options
20 if (!fps) return { outputOptions: [ ] }
21
22 const targetBitrate = getTargetBitrate({ inputBitrate, ratio: inputRatio, fps, resolution })
23
24 return {
25 outputOptions: [
26 ...getCommonOutputOptions(targetBitrate),
27
28 `-r ${fps}`
29 ]
30 }
31 }
32
33 const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = (options: EncoderOptionsBuilderParams) => {
34 const { streamNum, fps, inputBitrate, inputRatio, resolution } = options
35
36 const targetBitrate = getTargetBitrate({ inputBitrate, ratio: inputRatio, fps, resolution })
37
38 return {
39 outputOptions: [
40 ...getCommonOutputOptions(targetBitrate),
41
42 `${buildStreamSuffix('-r:v', streamNum)} ${fps}`,
43 `${buildStreamSuffix('-b:v', streamNum)} ${targetBitrate}`
44 ]
45 }
46 }
47
48 const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNum }) => {
49 const probe = await ffprobePromise(input)
50
51 if (await canDoQuickAudioTranscode(input, probe)) {
52 logger.debug('Copy audio stream %s by AAC encoder.', input)
53 return { copy: true, outputOptions: [ ] }
54 }
55
56 const parsedAudio = await getAudioStream(input, probe)
57
58 // We try to reduce the ceiling bitrate by making rough matches of bitrates
59 // Of course this is far from perfect, but it might save some space in the end
60
61 const audioCodecName = parsedAudio.audioStream['codec_name']
62
63 const bitrate = getMaxAudioBitrate(audioCodecName, parsedAudio.bitrate)
64
65 logger.debug('Calculating audio bitrate of %s by AAC encoder.', input, { bitrate: parsedAudio.bitrate, audioCodecName })
66
67 if (bitrate !== -1) {
68 return { outputOptions: [ buildStreamSuffix('-b:a', streamNum), bitrate + 'k' ] }
69 }
70
71 return { outputOptions: [ ] }
72 }
73
74 const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum }) => {
75 return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] }
76 }
77
78 // Used to get and update available encoders
79 class VideoTranscodingProfilesManager {
80 private static instance: VideoTranscodingProfilesManager
81
82 // 1 === less priority
83 private readonly encodersPriorities = {
84 vod: this.buildDefaultEncodersPriorities(),
85 live: this.buildDefaultEncodersPriorities()
86 }
87
88 private readonly availableEncoders = {
89 vod: {
90 libx264: {
91 default: defaultX264VODOptionsBuilder
92 },
93 aac: {
94 default: defaultAACOptionsBuilder
95 },
96 libfdk_aac: {
97 default: defaultLibFDKAACVODOptionsBuilder
98 }
99 },
100 live: {
101 libx264: {
102 default: defaultX264LiveOptionsBuilder
103 },
104 aac: {
105 default: defaultAACOptionsBuilder
106 }
107 }
108 }
109
110 private availableProfiles = {
111 vod: [] as string[],
112 live: [] as string[]
113 }
114
115 private constructor () {
116 this.buildAvailableProfiles()
117 }
118
119 getAvailableEncoders (): AvailableEncoders {
120 return {
121 available: this.availableEncoders,
122 encodersToTry: {
123 vod: {
124 video: this.getEncodersByPriority('vod', 'video'),
125 audio: this.getEncodersByPriority('vod', 'audio')
126 },
127 live: {
128 video: this.getEncodersByPriority('live', 'video'),
129 audio: this.getEncodersByPriority('live', 'audio')
130 }
131 }
132 }
133 }
134
135 getAvailableProfiles (type: 'vod' | 'live') {
136 return this.availableProfiles[type]
137 }
138
139 addProfile (options: {
140 type: 'vod' | 'live'
141 encoder: string
142 profile: string
143 builder: EncoderOptionsBuilder
144 }) {
145 const { type, encoder, profile, builder } = options
146
147 const encoders = this.availableEncoders[type]
148
149 if (!encoders[encoder]) encoders[encoder] = {}
150 encoders[encoder][profile] = builder
151
152 this.buildAvailableProfiles()
153 }
154
155 removeProfile (options: {
156 type: 'vod' | 'live'
157 encoder: string
158 profile: string
159 }) {
160 const { type, encoder, profile } = options
161
162 delete this.availableEncoders[type][encoder][profile]
163 this.buildAvailableProfiles()
164 }
165
166 addEncoderPriority (type: 'vod' | 'live', streamType: 'audio' | 'video', encoder: string, priority: number) {
167 this.encodersPriorities[type][streamType].push({ name: encoder, priority })
168
169 resetSupportedEncoders()
170 }
171
172 removeEncoderPriority (type: 'vod' | 'live', streamType: 'audio' | 'video', encoder: string, priority: number) {
173 this.encodersPriorities[type][streamType] = this.encodersPriorities[type][streamType]
174 .filter(o => o.name !== encoder && o.priority !== priority)
175
176 resetSupportedEncoders()
177 }
178
179 private getEncodersByPriority (type: 'vod' | 'live', streamType: 'audio' | 'video') {
180 return this.encodersPriorities[type][streamType]
181 .sort((e1, e2) => {
182 if (e1.priority > e2.priority) return -1
183 else if (e1.priority === e2.priority) return 0
184
185 return 1
186 })
187 .map(e => e.name)
188 }
189
190 private buildAvailableProfiles () {
191 for (const type of [ 'vod', 'live' ]) {
192 const result = new Set()
193
194 const encoders = this.availableEncoders[type]
195
196 for (const encoderName of Object.keys(encoders)) {
197 for (const profile of Object.keys(encoders[encoderName])) {
198 result.add(profile)
199 }
200 }
201
202 this.availableProfiles[type] = Array.from(result)
203 }
204
205 logger.debug('Available transcoding profiles built.', { availableProfiles: this.availableProfiles })
206 }
207
208 private buildDefaultEncodersPriorities () {
209 return {
210 video: [
211 { name: 'libx264', priority: 100 }
212 ],
213
214 // Try the first one, if not available try the second one etc
215 audio: [
216 // we favor VBR, if a good AAC encoder is available
217 { name: 'libfdk_aac', priority: 200 },
218 { name: 'aac', priority: 100 }
219 ]
220 }
221 }
222
223 static get Instance () {
224 return this.instance || (this.instance = new this())
225 }
226 }
227
228 // ---------------------------------------------------------------------------
229
230 export {
231 VideoTranscodingProfilesManager
232 }
233
234 // ---------------------------------------------------------------------------
235
236 function getTargetBitrate (options: {
237 inputBitrate: number
238 resolution: VideoResolution
239 ratio: number
240 fps: number
241 }) {
242 const { inputBitrate, resolution, ratio, fps } = options
243
244 const capped = capBitrate(inputBitrate, getAverageBitrate({ resolution, fps, ratio }))
245 const limit = getMinLimitBitrate({ resolution, fps, ratio })
246
247 return Math.max(limit, capped)
248 }
249
250 function capBitrate (inputBitrate: number, targetBitrate: number) {
251 if (!inputBitrate) return targetBitrate
252
253 // Add 30% margin to input bitrate
254 const inputBitrateWithMargin = inputBitrate + (inputBitrate * 0.3)
255
256 return Math.min(targetBitrate, inputBitrateWithMargin)
257 }
258
259 function getCommonOutputOptions (targetBitrate: number) {
260 return [
261 `-preset veryfast`,
262 `-maxrate ${targetBitrate}`,
263 `-bufsize ${targetBitrate * 2}`,
264
265 // NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it
266 `-b_strategy 1`,
267 // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
268 `-bf 16`
269 ]
270 }