]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/ffprobe-utils.ts
Translated using Weblate (Russian)
[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 const channelLayout = parsedAudio.audioStream['channel_layout']
266 // Causes playback issues with Chrome
267 if (!channelLayout || channelLayout === 'unknown') return false
268
269 return true
270 }
271
272 function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDARD'): number {
273 return VIDEO_TRANSCODING_FPS[type].slice(0)
274 .sort((a, b) => fps % a - fps % b)[0]
275 }
276
277 function computeFPS (fpsArg: number, resolution: VideoResolution) {
278 let fps = fpsArg
279
280 if (
281 // On small/medium resolutions, limit FPS
282 resolution !== undefined &&
283 resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
284 fps > VIDEO_TRANSCODING_FPS.AVERAGE
285 ) {
286 // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
287 fps = getClosestFramerateStandard(fps, 'STANDARD')
288 }
289
290 // Hard FPS limits
291 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
292 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
293
294 return fps
295 }
296
297 // ---------------------------------------------------------------------------
298
299 export {
300 getVideoStreamCodec,
301 getAudioStreamCodec,
302 getVideoStreamSize,
303 getVideoFileResolution,
304 getMetadataFromFile,
305 getMaxAudioBitrate,
306 getVideoStreamFromFile,
307 getDurationFromVideoFile,
308 getAudioStream,
309 computeFPS,
310 getVideoFileFPS,
311 ffprobePromise,
312 getClosestFramerateStandard,
313 computeResolutionsToTranscode,
314 getVideoFileBitrate,
315 canDoQuickTranscode,
316 canDoQuickVideoTranscode,
317 canDoQuickAudioTranscode
318 }