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