]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/ffprobe-utils.ts
Try to support other codecs
[github/Chocobozzz/PeerTube.git] / server / helpers / ffprobe-utils.ts
1 import * as ffmpeg from 'fluent-ffmpeg'
2 import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata'
3 import { getMaxBitrate, VideoResolution } 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<ffmpeg.FfprobeData>((res, rej) => {
16 ffmpeg.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?: ffmpeg.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?: ffmpeg.FfprobeData) {
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
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) {
179 const metadata = await getMetadataFromFile(path, existingProbe)
180
181 return metadata.format.bit_rate as number
182 }
183
184 async function getDurationFromVideoFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
185 const metadata = await getMetadataFromFile(path, existingProbe)
186
187 return Math.round(metadata.format.duration)
188 }
189
190 async function getVideoStreamFromFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
191 const metadata = await getMetadataFromFile(path, existingProbe)
192
193 return metadata.streams.find(s => s.codec_type === 'video') || null
194 }
195
196 function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') {
197 const configResolutions = type === 'vod'
198 ? CONFIG.TRANSCODING.RESOLUTIONS
199 : CONFIG.LIVE.TRANSCODING.RESOLUTIONS
200
201 const resolutionsEnabled: number[] = []
202
203 // Put in the order we want to proceed jobs
204 const resolutions = [
205 VideoResolution.H_NOVIDEO,
206 VideoResolution.H_480P,
207 VideoResolution.H_360P,
208 VideoResolution.H_720P,
209 VideoResolution.H_240P,
210 VideoResolution.H_1080P,
211 VideoResolution.H_1440P,
212 VideoResolution.H_4K
213 ]
214
215 for (const resolution of resolutions) {
216 if (configResolutions[resolution + 'p'] === true && videoFileResolution > resolution) {
217 resolutionsEnabled.push(resolution)
218 }
219 }
220
221 return resolutionsEnabled
222 }
223
224 async function canDoQuickTranscode (path: string): Promise<boolean> {
225 if (CONFIG.TRANSCODING.PROFILE !== 'default') return false
226
227 const probe = await ffprobePromise(path)
228
229 return await canDoQuickVideoTranscode(path, probe) &&
230 await canDoQuickAudioTranscode(path, probe)
231 }
232
233 async function canDoQuickVideoTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
234 const videoStream = await getVideoStreamFromFile(path, probe)
235 const fps = await getVideoFileFPS(path, probe)
236 const bitRate = await getVideoFileBitrate(path, probe)
237 const resolution = await getVideoFileResolution(path, probe)
238
239 // If ffprobe did not manage to guess the bitrate
240 if (!bitRate) return false
241
242 // check video params
243 if (videoStream == null) return false
244 if (videoStream['codec_name'] !== 'h264') return false
245 if (videoStream['pix_fmt'] !== 'yuv420p') return false
246 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
247 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
248
249 return true
250 }
251
252 async function canDoQuickAudioTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
253 const parsedAudio = await getAudioStream(path, probe)
254
255 if (!parsedAudio.audioStream) return true
256
257 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
258
259 const audioBitrate = parsedAudio.bitrate
260 if (!audioBitrate) return false
261
262 const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
263 if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
264
265 return true
266 }
267
268 function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDARD'): number {
269 return VIDEO_TRANSCODING_FPS[type].slice(0)
270 .sort((a, b) => fps % a - fps % b)[0]
271 }
272
273 function computeFPS (fpsArg: number, resolution: VideoResolution) {
274 let fps = fpsArg
275
276 if (
277 // On small/medium resolutions, limit FPS
278 resolution !== undefined &&
279 resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
280 fps > VIDEO_TRANSCODING_FPS.AVERAGE
281 ) {
282 // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
283 fps = getClosestFramerateStandard(fps, 'STANDARD')
284 }
285
286 // Hard FPS limits
287 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
288 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
289
290 return fps
291 }
292
293 // ---------------------------------------------------------------------------
294
295 export {
296 getVideoStreamCodec,
297 getAudioStreamCodec,
298 getVideoStreamSize,
299 getVideoFileResolution,
300 getMetadataFromFile,
301 getMaxAudioBitrate,
302 getVideoStreamFromFile,
303 getDurationFromVideoFile,
304 getAudioStream,
305 computeFPS,
306 getVideoFileFPS,
307 ffprobePromise,
308 getClosestFramerateStandard,
309 computeResolutionsToTranscode,
310 getVideoFileBitrate,
311 canDoQuickTranscode,
312 canDoQuickVideoTranscode,
313 canDoQuickAudioTranscode
314 }