]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/ffprobe-utils.ts
5b1ad9066b215d4b6b975e23ac1bdbbe07d81aee
[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 const baseProfileMatrix = {
95 High: '6400',
96 Main: '4D40',
97 Baseline: '42E0'
98 }
99
100 let baseProfile = baseProfileMatrix[videoStream.profile]
101 if (!baseProfile) {
102 logger.warn('Cannot get video profile codec of %s.', path, { videoStream })
103 baseProfile = baseProfileMatrix['High'] // Fallback
104 }
105
106 let level = videoStream.level.toString(16)
107 if (level.length === 1) level = `0${level}`
108
109 return `${videoCodec}.${baseProfile}${level}`
110 }
111
112 async function getAudioStreamCodec (path: string, existingProbe?: ffmpeg.FfprobeData) {
113 const { audioStream } = await getAudioStream(path, existingProbe)
114
115 if (!audioStream) return ''
116
117 const audioCodec = audioStream.codec_name
118 if (audioCodec === 'aac') return 'mp4a.40.2'
119
120 logger.warn('Cannot get audio codec of %s.', path, { audioStream })
121
122 return 'mp4a.40.2' // Fallback
123 }
124
125 async function getVideoFileResolution (path: string, existingProbe?: ffmpeg.FfprobeData) {
126 const size = await getVideoStreamSize(path, existingProbe)
127
128 return {
129 videoFileResolution: Math.min(size.height, size.width),
130 isPortraitMode: size.height > size.width
131 }
132 }
133
134 async function getVideoFileFPS (path: string, existingProbe?: ffmpeg.FfprobeData) {
135 const videoStream = await getVideoStreamFromFile(path, existingProbe)
136 if (videoStream === null) return 0
137
138 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
139 const valuesText: string = videoStream[key]
140 if (!valuesText) continue
141
142 const [ frames, seconds ] = valuesText.split('/')
143 if (!frames || !seconds) continue
144
145 const result = parseInt(frames, 10) / parseInt(seconds, 10)
146 if (result > 0) return Math.round(result)
147 }
148
149 return 0
150 }
151
152 async function getMetadataFromFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
153 const metadata = existingProbe || await ffprobePromise(path)
154
155 return new VideoFileMetadata(metadata)
156 }
157
158 async function getVideoFileBitrate (path: string, existingProbe?: ffmpeg.FfprobeData) {
159 const metadata = await getMetadataFromFile(path, existingProbe)
160
161 return metadata.format.bit_rate as number
162 }
163
164 async function getDurationFromVideoFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
165 const metadata = await getMetadataFromFile(path, existingProbe)
166
167 return Math.round(metadata.format.duration)
168 }
169
170 async function getVideoStreamFromFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
171 const metadata = await getMetadataFromFile(path, existingProbe)
172
173 return metadata.streams.find(s => s.codec_type === 'video') || null
174 }
175
176 function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') {
177 const configResolutions = type === 'vod'
178 ? CONFIG.TRANSCODING.RESOLUTIONS
179 : CONFIG.LIVE.TRANSCODING.RESOLUTIONS
180
181 const resolutionsEnabled: number[] = []
182
183 // Put in the order we want to proceed jobs
184 const resolutions = [
185 VideoResolution.H_NOVIDEO,
186 VideoResolution.H_480P,
187 VideoResolution.H_360P,
188 VideoResolution.H_720P,
189 VideoResolution.H_240P,
190 VideoResolution.H_1080P,
191 VideoResolution.H_1440P,
192 VideoResolution.H_4K
193 ]
194
195 for (const resolution of resolutions) {
196 if (configResolutions[resolution + 'p'] === true && videoFileResolution > resolution) {
197 resolutionsEnabled.push(resolution)
198 }
199 }
200
201 return resolutionsEnabled
202 }
203
204 async function canDoQuickTranscode (path: string): Promise<boolean> {
205 if (CONFIG.TRANSCODING.PROFILE !== 'default') return false
206
207 const probe = await ffprobePromise(path)
208
209 return await canDoQuickVideoTranscode(path, probe) &&
210 await canDoQuickAudioTranscode(path, probe)
211 }
212
213 async function canDoQuickVideoTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
214 const videoStream = await getVideoStreamFromFile(path, probe)
215 const fps = await getVideoFileFPS(path, probe)
216 const bitRate = await getVideoFileBitrate(path, probe)
217 const resolution = await getVideoFileResolution(path, probe)
218
219 // If ffprobe did not manage to guess the bitrate
220 if (!bitRate) return false
221
222 // check video params
223 if (videoStream == null) return false
224 if (videoStream['codec_name'] !== 'h264') return false
225 if (videoStream['pix_fmt'] !== 'yuv420p') return false
226 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
227 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
228
229 return true
230 }
231
232 async function canDoQuickAudioTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
233 const parsedAudio = await getAudioStream(path, probe)
234
235 if (!parsedAudio.audioStream) return true
236
237 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
238
239 const audioBitrate = parsedAudio.bitrate
240 if (!audioBitrate) return false
241
242 const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
243 if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
244
245 return true
246 }
247
248 function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDARD'): number {
249 return VIDEO_TRANSCODING_FPS[type].slice(0)
250 .sort((a, b) => fps % a - fps % b)[0]
251 }
252
253 function computeFPS (fpsArg: number, resolution: VideoResolution) {
254 let fps = fpsArg
255
256 if (
257 // On small/medium resolutions, limit FPS
258 resolution !== undefined &&
259 resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
260 fps > VIDEO_TRANSCODING_FPS.AVERAGE
261 ) {
262 // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
263 fps = getClosestFramerateStandard(fps, 'STANDARD')
264 }
265
266 // Hard FPS limits
267 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
268 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
269
270 return fps
271 }
272
273 // ---------------------------------------------------------------------------
274
275 export {
276 getVideoStreamCodec,
277 getAudioStreamCodec,
278 getVideoStreamSize,
279 getVideoFileResolution,
280 getMetadataFromFile,
281 getMaxAudioBitrate,
282 getVideoStreamFromFile,
283 getDurationFromVideoFile,
284 getAudioStream,
285 computeFPS,
286 getVideoFileFPS,
287 ffprobePromise,
288 getClosestFramerateStandard,
289 computeResolutionsToTranscode,
290 getVideoFileBitrate,
291 canDoQuickTranscode,
292 canDoQuickVideoTranscode,
293 canDoQuickAudioTranscode
294 }