]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/ffprobe-utils.ts
Fix live FPS limit
[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.floor(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_4K
192 ]
193
194 for (const resolution of resolutions) {
195 if (configResolutions[resolution + 'p'] === true && videoFileResolution > resolution) {
196 resolutionsEnabled.push(resolution)
197 }
198 }
199
200 return resolutionsEnabled
201 }
202
203 async function canDoQuickTranscode (path: string): Promise<boolean> {
204 const probe = await ffprobePromise(path)
205
206 return await canDoQuickVideoTranscode(path, probe) &&
207 await canDoQuickAudioTranscode(path, probe)
208 }
209
210 async function canDoQuickVideoTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
211 const videoStream = await getVideoStreamFromFile(path, probe)
212 const fps = await getVideoFileFPS(path, probe)
213 const bitRate = await getVideoFileBitrate(path, probe)
214 const resolution = await getVideoFileResolution(path, probe)
215
216 // If ffprobe did not manage to guess the bitrate
217 if (!bitRate) return false
218
219 // check video params
220 if (videoStream == null) return false
221 if (videoStream['codec_name'] !== 'h264') return false
222 if (videoStream['pix_fmt'] !== 'yuv420p') return false
223 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
224 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
225
226 return true
227 }
228
229 async function canDoQuickAudioTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
230 const parsedAudio = await getAudioStream(path, probe)
231
232 if (!parsedAudio.audioStream) return true
233
234 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
235
236 const audioBitrate = parsedAudio.bitrate
237 if (!audioBitrate) return false
238
239 const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
240 if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
241
242 return true
243 }
244
245 function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDARD'): number {
246 return VIDEO_TRANSCODING_FPS[type].slice(0)
247 .sort((a, b) => fps % a - fps % b)[0]
248 }
249
250 function computeFPS (fpsArg: number, resolution: VideoResolution) {
251 let fps = fpsArg
252
253 if (
254 // On small/medium resolutions, limit FPS
255 resolution !== undefined &&
256 resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
257 fps > VIDEO_TRANSCODING_FPS.AVERAGE
258 ) {
259 // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
260 fps = getClosestFramerateStandard(fps, 'STANDARD')
261 }
262
263 // Hard FPS limits
264 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
265 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
266
267 return fps
268 }
269
270 // ---------------------------------------------------------------------------
271
272 export {
273 getVideoStreamCodec,
274 getAudioStreamCodec,
275 getVideoStreamSize,
276 getVideoFileResolution,
277 getMetadataFromFile,
278 getMaxAudioBitrate,
279 getVideoStreamFromFile,
280 getDurationFromVideoFile,
281 getAudioStream,
282 computeFPS,
283 getVideoFileFPS,
284 ffprobePromise,
285 getClosestFramerateStandard,
286 computeResolutionsToTranscode,
287 getVideoFileBitrate,
288 canDoQuickTranscode,
289 canDoQuickVideoTranscode,
290 canDoQuickAudioTranscode
291 }