]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame_incremental - server/helpers/ffprobe-utils.ts
Fix audio encoding params
[github/Chocobozzz/PeerTube.git] / server / helpers / ffprobe-utils.ts
... / ...
CommitLineData
1import * as ffmpeg from 'fluent-ffmpeg'
2import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata'
3import { getMaxBitrate, VideoResolution } from '../../shared/models/videos'
4import { CONFIG } from '../initializers/config'
5import { VIDEO_TRANSCODING_FPS } from '../initializers/constants'
6import { logger } from './logger'
7
8function ffprobePromise (path: string) {
9 return new Promise<ffmpeg.FfprobeData>((res, rej) => {
10 ffmpeg.ffprobe(path, (err, data) => {
11 if (err) return rej(err)
12
13 return res(data)
14 })
15 })
16}
17
18async function getAudioStream (videoPath: string, existingProbe?: ffmpeg.FfprobeData) {
19 // without position, ffprobe considers the last input only
20 // we make it consider the first input only
21 // if you pass a file path to pos, then ffprobe acts on that file directly
22 const data = existingProbe || await ffprobePromise(videoPath)
23
24 if (Array.isArray(data.streams)) {
25 const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio')
26
27 if (audioStream) {
28 return {
29 absolutePath: data.format.filename,
30 audioStream,
31 bitrate: parseInt(audioStream['bit_rate'] + '', 10)
32 }
33 }
34 }
35
36 return { absolutePath: data.format.filename }
37}
38
39function getMaxAudioBitrate (type: 'aac' | 'mp3' | string, bitrate: number) {
40 const maxKBitrate = 384
41 const kToBits = (kbits: number) => kbits * 1000
42
43 // If we did not manage to get the bitrate, use an average value
44 if (!bitrate) return 256
45
46 if (type === 'aac') {
47 switch (true) {
48 case bitrate > kToBits(maxKBitrate):
49 return maxKBitrate
50
51 default:
52 return -1 // we interpret it as a signal to copy the audio stream as is
53 }
54 }
55
56 /*
57 a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac.
58 That's why, when using aac, we can go to lower kbit/sec. The equivalences
59 made here are not made to be accurate, especially with good mp3 encoders.
60 */
61 switch (true) {
62 case bitrate <= kToBits(192):
63 return 128
64
65 case bitrate <= kToBits(384):
66 return 256
67
68 default:
69 return maxKBitrate
70 }
71}
72
73async function getVideoStreamSize (path: string, existingProbe?: ffmpeg.FfprobeData) {
74 const videoStream = await getVideoStreamFromFile(path, existingProbe)
75
76 return videoStream === null
77 ? { width: 0, height: 0 }
78 : { width: videoStream.width, height: videoStream.height }
79}
80
81async function getVideoStreamCodec (path: string) {
82 const videoStream = await getVideoStreamFromFile(path)
83
84 if (!videoStream) return ''
85
86 const videoCodec = videoStream.codec_tag_string
87
88 const baseProfileMatrix = {
89 High: '6400',
90 Main: '4D40',
91 Baseline: '42E0'
92 }
93
94 let baseProfile = baseProfileMatrix[videoStream.profile]
95 if (!baseProfile) {
96 logger.warn('Cannot get video profile codec of %s.', path, { videoStream })
97 baseProfile = baseProfileMatrix['High'] // Fallback
98 }
99
100 let level = videoStream.level.toString(16)
101 if (level.length === 1) level = `0${level}`
102
103 return `${videoCodec}.${baseProfile}${level}`
104}
105
106async function getAudioStreamCodec (path: string, existingProbe?: ffmpeg.FfprobeData) {
107 const { audioStream } = await getAudioStream(path, existingProbe)
108
109 if (!audioStream) return ''
110
111 const audioCodec = audioStream.codec_name
112 if (audioCodec === 'aac') return 'mp4a.40.2'
113
114 logger.warn('Cannot get audio codec of %s.', path, { audioStream })
115
116 return 'mp4a.40.2' // Fallback
117}
118
119async function getVideoFileResolution (path: string, existingProbe?: ffmpeg.FfprobeData) {
120 const size = await getVideoStreamSize(path, existingProbe)
121
122 return {
123 videoFileResolution: Math.min(size.height, size.width),
124 isPortraitMode: size.height > size.width
125 }
126}
127
128async function getVideoFileFPS (path: string, existingProbe?: ffmpeg.FfprobeData) {
129 const videoStream = await getVideoStreamFromFile(path, existingProbe)
130 if (videoStream === null) return 0
131
132 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
133 const valuesText: string = videoStream[key]
134 if (!valuesText) continue
135
136 const [ frames, seconds ] = valuesText.split('/')
137 if (!frames || !seconds) continue
138
139 const result = parseInt(frames, 10) / parseInt(seconds, 10)
140 if (result > 0) return Math.round(result)
141 }
142
143 return 0
144}
145
146async function getMetadataFromFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
147 const metadata = existingProbe || await ffprobePromise(path)
148
149 return new VideoFileMetadata(metadata)
150}
151
152async function getVideoFileBitrate (path: string, existingProbe?: ffmpeg.FfprobeData) {
153 const metadata = await getMetadataFromFile(path, existingProbe)
154
155 return metadata.format.bit_rate as number
156}
157
158async function getDurationFromVideoFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
159 const metadata = await getMetadataFromFile(path, existingProbe)
160
161 return Math.floor(metadata.format.duration)
162}
163
164async function getVideoStreamFromFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
165 const metadata = await getMetadataFromFile(path, existingProbe)
166
167 return metadata.streams.find(s => s.codec_type === 'video') || null
168}
169
170function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') {
171 const configResolutions = type === 'vod'
172 ? CONFIG.TRANSCODING.RESOLUTIONS
173 : CONFIG.LIVE.TRANSCODING.RESOLUTIONS
174
175 const resolutionsEnabled: number[] = []
176
177 // Put in the order we want to proceed jobs
178 const resolutions = [
179 VideoResolution.H_NOVIDEO,
180 VideoResolution.H_480P,
181 VideoResolution.H_360P,
182 VideoResolution.H_720P,
183 VideoResolution.H_240P,
184 VideoResolution.H_1080P,
185 VideoResolution.H_4K
186 ]
187
188 for (const resolution of resolutions) {
189 if (configResolutions[resolution + 'p'] === true && videoFileResolution > resolution) {
190 resolutionsEnabled.push(resolution)
191 }
192 }
193
194 return resolutionsEnabled
195}
196
197async function canDoQuickTranscode (path: string): Promise<boolean> {
198 const probe = await ffprobePromise(path)
199
200 return await canDoQuickVideoTranscode(path, probe) &&
201 await canDoQuickAudioTranscode(path, probe)
202}
203
204async function canDoQuickVideoTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
205 const videoStream = await getVideoStreamFromFile(path, probe)
206 const fps = await getVideoFileFPS(path, probe)
207 const bitRate = await getVideoFileBitrate(path, probe)
208 const resolution = await getVideoFileResolution(path, probe)
209
210 // If ffprobe did not manage to guess the bitrate
211 if (!bitRate) return false
212
213 // check video params
214 if (videoStream == null) return false
215 if (videoStream['codec_name'] !== 'h264') return false
216 if (videoStream['pix_fmt'] !== 'yuv420p') return false
217 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
218 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
219
220 return true
221}
222
223async function canDoQuickAudioTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
224 const parsedAudio = await getAudioStream(path, probe)
225
226 if (!parsedAudio.audioStream) return true
227
228 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
229
230 const audioBitrate = parsedAudio.bitrate
231 if (!audioBitrate) return false
232
233 const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
234 if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
235
236 return true
237}
238
239function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDARD'): number {
240 return VIDEO_TRANSCODING_FPS[type].slice(0)
241 .sort((a, b) => fps % a - fps % b)[0]
242}
243
244// ---------------------------------------------------------------------------
245
246export {
247 getVideoStreamCodec,
248 getAudioStreamCodec,
249 getVideoStreamSize,
250 getVideoFileResolution,
251 getMetadataFromFile,
252 getMaxAudioBitrate,
253 getVideoStreamFromFile,
254 getDurationFromVideoFile,
255 getAudioStream,
256 getVideoFileFPS,
257 ffprobePromise,
258 getClosestFramerateStandard,
259 computeResolutionsToTranscode,
260 getVideoFileBitrate,
261 canDoQuickTranscode,
262 canDoQuickVideoTranscode,
263 canDoQuickAudioTranscode
264}