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