]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/ffprobe-utils.ts
Limit live bitrate
[github/Chocobozzz/PeerTube.git] / server / helpers / ffprobe-utils.ts
1 import * as ffmpeg from 'fluent-ffmpeg'
2 import { getMaxBitrate, VideoFileMetadata, VideoResolution } from '../../shared/models/videos'
3 import { CONFIG } from '../initializers/config'
4 import { VIDEO_TRANSCODING_FPS } from '../initializers/constants'
5 import { logger } from './logger'
6
7 /**
8 *
9 * Helpers to run ffprobe and extract data from the JSON output
10 *
11 */
12
13 function ffprobePromise (path: string) {
14 return new Promise<ffmpeg.FfprobeData>((res, rej) => {
15 ffmpeg.ffprobe(path, (err, data) => {
16 if (err) return rej(err)
17
18 return res(data)
19 })
20 })
21 }
22
23 async function getAudioStream (videoPath: string, existingProbe?: ffmpeg.FfprobeData) {
24 // without position, ffprobe considers the last input only
25 // we make it consider the first input only
26 // if you pass a file path to pos, then ffprobe acts on that file directly
27 const data = existingProbe || await ffprobePromise(videoPath)
28
29 if (Array.isArray(data.streams)) {
30 const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio')
31
32 if (audioStream) {
33 return {
34 absolutePath: data.format.filename,
35 audioStream,
36 bitrate: parseInt(audioStream['bit_rate'] + '', 10)
37 }
38 }
39 }
40
41 return { absolutePath: data.format.filename }
42 }
43
44 function getMaxAudioBitrate (type: 'aac' | 'mp3' | string, bitrate: number) {
45 const maxKBitrate = 384
46 const kToBits = (kbits: number) => kbits * 1000
47
48 // If we did not manage to get the bitrate, use an average value
49 if (!bitrate) return 256
50
51 if (type === 'aac') {
52 switch (true) {
53 case bitrate > kToBits(maxKBitrate):
54 return maxKBitrate
55
56 default:
57 return -1 // we interpret it as a signal to copy the audio stream as is
58 }
59 }
60
61 /*
62 a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac.
63 That's why, when using aac, we can go to lower kbit/sec. The equivalences
64 made here are not made to be accurate, especially with good mp3 encoders.
65 */
66 switch (true) {
67 case bitrate <= kToBits(192):
68 return 128
69
70 case bitrate <= kToBits(384):
71 return 256
72
73 default:
74 return maxKBitrate
75 }
76 }
77
78 async function getVideoStreamSize (path: string, existingProbe?: ffmpeg.FfprobeData) {
79 const videoStream = await getVideoStreamFromFile(path, existingProbe)
80
81 return videoStream === null
82 ? { width: 0, height: 0 }
83 : { width: videoStream.width, height: videoStream.height }
84 }
85
86 async function getVideoStreamCodec (path: string) {
87 const videoStream = await getVideoStreamFromFile(path)
88
89 if (!videoStream) return ''
90
91 const videoCodec = videoStream.codec_tag_string
92
93 if (videoCodec === 'vp09') return 'vp09.00.50.08'
94 if (videoCodec === 'hev1') return 'hev1.1.6.L93.B0'
95
96 const baseProfileMatrix = {
97 avc1: {
98 High: '6400',
99 Main: '4D40',
100 Baseline: '42E0'
101 },
102 av01: {
103 High: '1',
104 Main: '0',
105 Professional: '2'
106 }
107 }
108
109 let baseProfile = baseProfileMatrix[videoCodec][videoStream.profile]
110 if (!baseProfile) {
111 logger.warn('Cannot get video profile codec of %s.', path, { videoStream })
112 baseProfile = baseProfileMatrix[videoCodec]['High'] // Fallback
113 }
114
115 if (videoCodec === 'av01') {
116 const level = videoStream.level
117
118 // Guess the tier indicator and bit depth
119 return `${videoCodec}.${baseProfile}.${level}M.08`
120 }
121
122 // Default, h264 codec
123 let level = videoStream.level.toString(16)
124 if (level.length === 1) level = `0${level}`
125
126 return `${videoCodec}.${baseProfile}${level}`
127 }
128
129 async function getAudioStreamCodec (path: string, existingProbe?: ffmpeg.FfprobeData) {
130 const { audioStream } = await getAudioStream(path, existingProbe)
131
132 if (!audioStream) return ''
133
134 const audioCodecName = audioStream.codec_name
135
136 if (audioCodecName === 'opus') return 'opus'
137 if (audioCodecName === 'vorbis') return 'vorbis'
138 if (audioCodecName === 'aac') return 'mp4a.40.2'
139
140 logger.warn('Cannot get audio codec of %s.', path, { audioStream })
141
142 return 'mp4a.40.2' // Fallback
143 }
144
145 async function getVideoFileResolution (path: string, existingProbe?: ffmpeg.FfprobeData) {
146 const size = await getVideoStreamSize(path, existingProbe)
147
148 return {
149 videoFileResolution: Math.min(size.height, size.width),
150 isPortraitMode: size.height > size.width
151 }
152 }
153
154 async function getVideoFileFPS (path: string, existingProbe?: ffmpeg.FfprobeData) {
155 const videoStream = await getVideoStreamFromFile(path, existingProbe)
156 if (videoStream === null) return 0
157
158 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
159 const valuesText: string = videoStream[key]
160 if (!valuesText) continue
161
162 const [ frames, seconds ] = valuesText.split('/')
163 if (!frames || !seconds) continue
164
165 const result = parseInt(frames, 10) / parseInt(seconds, 10)
166 if (result > 0) return Math.round(result)
167 }
168
169 return 0
170 }
171
172 async function getMetadataFromFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
173 const metadata = existingProbe || await ffprobePromise(path)
174
175 return new VideoFileMetadata(metadata)
176 }
177
178 async function getVideoFileBitrate (path: string, existingProbe?: ffmpeg.FfprobeData): Promise<number> {
179 const metadata = await getMetadataFromFile(path, existingProbe)
180
181 let bitrate = metadata.format.bit_rate as number
182 if (bitrate && !isNaN(bitrate)) return bitrate
183
184 const videoStream = await getVideoStreamFromFile(path, existingProbe)
185 if (!videoStream) return undefined
186
187 bitrate = videoStream?.bit_rate
188 if (bitrate && !isNaN(bitrate)) return bitrate
189
190 return undefined
191 }
192
193 async function getDurationFromVideoFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
194 const metadata = await getMetadataFromFile(path, existingProbe)
195
196 return Math.round(metadata.format.duration)
197 }
198
199 async function getVideoStreamFromFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
200 const metadata = await getMetadataFromFile(path, existingProbe)
201
202 return metadata.streams.find(s => s.codec_type === 'video') || null
203 }
204
205 function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') {
206 const configResolutions = type === 'vod'
207 ? CONFIG.TRANSCODING.RESOLUTIONS
208 : CONFIG.LIVE.TRANSCODING.RESOLUTIONS
209
210 const resolutionsEnabled: number[] = []
211
212 // Put in the order we want to proceed jobs
213 const resolutions = [
214 VideoResolution.H_NOVIDEO,
215 VideoResolution.H_480P,
216 VideoResolution.H_360P,
217 VideoResolution.H_720P,
218 VideoResolution.H_240P,
219 VideoResolution.H_1080P,
220 VideoResolution.H_1440P,
221 VideoResolution.H_4K
222 ]
223
224 for (const resolution of resolutions) {
225 if (configResolutions[resolution + 'p'] === true && videoFileResolution > resolution) {
226 resolutionsEnabled.push(resolution)
227 }
228 }
229
230 return resolutionsEnabled
231 }
232
233 async function canDoQuickTranscode (path: string): Promise<boolean> {
234 if (CONFIG.TRANSCODING.PROFILE !== 'default') return false
235
236 const probe = await ffprobePromise(path)
237
238 return await canDoQuickVideoTranscode(path, probe) &&
239 await canDoQuickAudioTranscode(path, probe)
240 }
241
242 async function canDoQuickVideoTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
243 const videoStream = await getVideoStreamFromFile(path, probe)
244 const fps = await getVideoFileFPS(path, probe)
245 const bitRate = await getVideoFileBitrate(path, probe)
246 const resolution = await getVideoFileResolution(path, probe)
247
248 // If ffprobe did not manage to guess the bitrate
249 if (!bitRate) return false
250
251 // check video params
252 if (videoStream == null) return false
253 if (videoStream['codec_name'] !== 'h264') return false
254 if (videoStream['pix_fmt'] !== 'yuv420p') return false
255 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
256 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
257
258 return true
259 }
260
261 async function canDoQuickAudioTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
262 const parsedAudio = await getAudioStream(path, probe)
263
264 if (!parsedAudio.audioStream) return true
265
266 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
267
268 const audioBitrate = parsedAudio.bitrate
269 if (!audioBitrate) return false
270
271 const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
272 if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
273
274 const channelLayout = parsedAudio.audioStream['channel_layout']
275 // Causes playback issues with Chrome
276 if (!channelLayout || channelLayout === 'unknown') return false
277
278 return true
279 }
280
281 function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDARD'): number {
282 return VIDEO_TRANSCODING_FPS[type].slice(0)
283 .sort((a, b) => fps % a - fps % b)[0]
284 }
285
286 function computeFPS (fpsArg: number, resolution: VideoResolution) {
287 let fps = fpsArg
288
289 if (
290 // On small/medium resolutions, limit FPS
291 resolution !== undefined &&
292 resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
293 fps > VIDEO_TRANSCODING_FPS.AVERAGE
294 ) {
295 // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
296 fps = getClosestFramerateStandard(fps, 'STANDARD')
297 }
298
299 // Hard FPS limits
300 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
301 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
302
303 return fps
304 }
305
306 // ---------------------------------------------------------------------------
307
308 export {
309 getVideoStreamCodec,
310 getAudioStreamCodec,
311 getVideoStreamSize,
312 getVideoFileResolution,
313 getMetadataFromFile,
314 getMaxAudioBitrate,
315 getVideoStreamFromFile,
316 getDurationFromVideoFile,
317 getAudioStream,
318 computeFPS,
319 getVideoFileFPS,
320 ffprobePromise,
321 getClosestFramerateStandard,
322 computeResolutionsToTranscode,
323 getVideoFileBitrate,
324 canDoQuickTranscode,
325 canDoQuickVideoTranscode,
326 canDoQuickAudioTranscode
327 }