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