]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/ffprobe-utils.ts
Fix AP audience
[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 computeLowerResolutionsToTranscode (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: VideoResolution[] = [
218 VideoResolution.H_NOVIDEO,
219 VideoResolution.H_480P,
220 VideoResolution.H_360P,
221 VideoResolution.H_720P,
222 VideoResolution.H_240P,
223 VideoResolution.H_144P,
224 VideoResolution.H_1080P,
225 VideoResolution.H_1440P,
226 VideoResolution.H_4K
227 ]
228
229 for (const resolution of resolutions) {
230 if (configResolutions[resolution + 'p'] === true && videoFileResolution > resolution) {
231 resolutionsEnabled.push(resolution)
232 }
233 }
234
235 return resolutionsEnabled
236 }
237
238 async function canDoQuickTranscode (path: string): Promise<boolean> {
239 if (CONFIG.TRANSCODING.PROFILE !== 'default') return false
240
241 const probe = await ffprobePromise(path)
242
243 return await canDoQuickVideoTranscode(path, probe) &&
244 await canDoQuickAudioTranscode(path, probe)
245 }
246
247 async function canDoQuickVideoTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
248 const videoStream = await getVideoStreamFromFile(path, probe)
249 const fps = await getVideoFileFPS(path, probe)
250 const bitRate = await getVideoFileBitrate(path, probe)
251 const resolutionData = await getVideoFileResolution(path, probe)
252
253 // If ffprobe did not manage to guess the bitrate
254 if (!bitRate) return false
255
256 // check video params
257 if (videoStream == null) return false
258 if (videoStream['codec_name'] !== 'h264') return false
259 if (videoStream['pix_fmt'] !== 'yuv420p') return false
260 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
261 if (bitRate > getMaxBitrate({ ...resolutionData, fps })) return false
262
263 return true
264 }
265
266 async function canDoQuickAudioTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
267 const parsedAudio = await getAudioStream(path, probe)
268
269 if (!parsedAudio.audioStream) return true
270
271 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
272
273 const audioBitrate = parsedAudio.bitrate
274 if (!audioBitrate) return false
275
276 const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
277 if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
278
279 const channelLayout = parsedAudio.audioStream['channel_layout']
280 // Causes playback issues with Chrome
281 if (!channelLayout || channelLayout === 'unknown') return false
282
283 return true
284 }
285
286 function getClosestFramerateStandard <K extends keyof Pick<VideoTranscodingFPS, 'HD_STANDARD' | 'STANDARD'>> (fps: number, type: K) {
287 return VIDEO_TRANSCODING_FPS[type].slice(0)
288 .sort((a, b) => fps % a - fps % b)[0]
289 }
290
291 function computeFPS (fpsArg: number, resolution: VideoResolution) {
292 let fps = fpsArg
293
294 if (
295 // On small/medium resolutions, limit FPS
296 resolution !== undefined &&
297 resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
298 fps > VIDEO_TRANSCODING_FPS.AVERAGE
299 ) {
300 // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
301 fps = getClosestFramerateStandard(fps, 'STANDARD')
302 }
303
304 // Hard FPS limits
305 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
306
307 if (fps < VIDEO_TRANSCODING_FPS.MIN) {
308 throw new Error(`Cannot compute FPS because ${fps} is lower than our minimum value ${VIDEO_TRANSCODING_FPS.MIN}`)
309 }
310
311 return fps
312 }
313
314 // ---------------------------------------------------------------------------
315
316 export {
317 getVideoStreamCodec,
318 getAudioStreamCodec,
319 getVideoStreamSize,
320 getVideoFileResolution,
321 getMetadataFromFile,
322 getMaxAudioBitrate,
323 getVideoStreamFromFile,
324 getDurationFromVideoFile,
325 getAudioStream,
326 computeFPS,
327 getVideoFileFPS,
328 ffprobePromise,
329 getClosestFramerateStandard,
330 computeLowerResolutionsToTranscode,
331 getVideoFileBitrate,
332 canDoQuickTranscode,
333 canDoQuickVideoTranscode,
334 canDoQuickAudioTranscode
335 }