]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame_incremental - server/helpers/ffprobe-utils.ts
Rewrite youtube-dl import
[github/Chocobozzz/PeerTube.git] / server / helpers / ffprobe-utils.ts
... / ...
CommitLineData
1import { ffprobe, FfprobeData } from 'fluent-ffmpeg'
2import { getMaxBitrate } from '@shared/core-utils'
3import { VideoFileMetadata, VideoResolution, VideoTranscodingFPS } from '../../shared/models/videos'
4import { CONFIG } from '../initializers/config'
5import { VIDEO_TRANSCODING_FPS } from '../initializers/constants'
6import { logger } from './logger'
7
8/**
9 *
10 * Helpers to run ffprobe and extract data from the JSON output
11 *
12 */
13
14function 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
24async 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
45function 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
79async 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
87async 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
130async 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
146async 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
158async 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
176async function getMetadataFromFile (path: string, existingProbe?: FfprobeData) {
177 const metadata = existingProbe || await ffprobePromise(path)
178
179 return new VideoFileMetadata(metadata)
180}
181
182async 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
197async function getDurationFromVideoFile (path: string, existingProbe?: FfprobeData) {
198 const metadata = await getMetadataFromFile(path, existingProbe)
199
200 return Math.round(metadata.format.duration)
201}
202
203async 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
209function 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
237async 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
246async 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
265async 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
285function 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
290function 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
306 if (fps < VIDEO_TRANSCODING_FPS.MIN) {
307 throw new Error(`Cannot compute FPS because ${fps} is lower than our minimum value ${VIDEO_TRANSCODING_FPS.MIN}`)
308 }
309
310 return fps
311}
312
313// ---------------------------------------------------------------------------
314
315export {
316 getVideoStreamCodec,
317 getAudioStreamCodec,
318 getVideoStreamSize,
319 getVideoFileResolution,
320 getMetadataFromFile,
321 getMaxAudioBitrate,
322 getVideoStreamFromFile,
323 getDurationFromVideoFile,
324 getAudioStream,
325 computeFPS,
326 getVideoFileFPS,
327 ffprobePromise,
328 getClosestFramerateStandard,
329 computeResolutionsToTranscode,
330 getVideoFileBitrate,
331 canDoQuickTranscode,
332 canDoQuickVideoTranscode,
333 canDoQuickAudioTranscode
334}