]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/helpers/ffprobe-utils.ts
Fix audio encoding params
[github/Chocobozzz/PeerTube.git] / server / helpers / ffprobe-utils.ts
CommitLineData
daf6e480
C
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) {
33ff70ba
C
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
daf6e480
C
45
46 if (type === 'aac') {
47 switch (true) {
33ff70ba
C
48 case bitrate > kToBits(maxKBitrate):
49 return maxKBitrate
daf6e480
C
50
51 default:
52 return -1 // we interpret it as a signal to copy the audio stream as is
53 }
54 }
55
33ff70ba
C
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
daf6e480 64
33ff70ba
C
65 case bitrate <= kToBits(384):
66 return 256
daf6e480 67
33ff70ba
C
68 default:
69 return maxKBitrate
daf6e480 70 }
daf6e480
C
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
5a547f69
C
200 return await canDoQuickVideoTranscode(path, probe) &&
201 await canDoQuickAudioTranscode(path, probe)
202}
203
204async function canDoQuickVideoTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
daf6e480 205 const videoStream = await getVideoStreamFromFile(path, probe)
daf6e480
C
206 const fps = await getVideoFileFPS(path, probe)
207 const bitRate = await getVideoFileBitrate(path, probe)
208 const resolution = await getVideoFileResolution(path, probe)
209
33ff70ba
C
210 // If ffprobe did not manage to guess the bitrate
211 if (!bitRate) return false
212
daf6e480
C
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
5a547f69
C
220 return true
221}
222
223async function canDoQuickAudioTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
224 const parsedAudio = await getAudioStream(path, probe)
225
33ff70ba 226 if (!parsedAudio.audioStream) return true
daf6e480 227
33ff70ba 228 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
daf6e480 229
33ff70ba
C
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
daf6e480
C
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,
5a547f69 253 getVideoStreamFromFile,
daf6e480
C
254 getDurationFromVideoFile,
255 getAudioStream,
256 getVideoFileFPS,
5a547f69 257 ffprobePromise,
daf6e480
C
258 getClosestFramerateStandard,
259 computeResolutionsToTranscode,
260 getVideoFileBitrate,
5a547f69
C
261 canDoQuickTranscode,
262 canDoQuickVideoTranscode,
263 canDoQuickAudioTranscode
daf6e480 264}