]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/ffprobe-utils.ts
Fix h265 video import using CLI
[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 if (videoCodec === 'hev1') return 'hev1.1.6.L93.B0'
96
97 const baseProfileMatrix = {
98 avc1: {
99 High: '6400',
100 Main: '4D40',
101 Baseline: '42E0'
102 },
103 av01: {
104 High: '1',
105 Main: '0',
106 Professional: '2'
107 }
108 }
109
110 let baseProfile = baseProfileMatrix[videoCodec][videoStream.profile]
111 if (!baseProfile) {
112 logger.warn('Cannot get video profile codec of %s.', path, { videoStream })
113 baseProfile = baseProfileMatrix[videoCodec]['High'] // Fallback
114 }
115
116 if (videoCodec === 'av01') {
117 const level = videoStream.level
118
119 // Guess the tier indicator and bit depth
120 return `${videoCodec}.${baseProfile}.${level}M.08`
121 }
122
123 // Default, h264 codec
124 let level = videoStream.level.toString(16)
125 if (level.length === 1) level = `0${level}`
126
127 return `${videoCodec}.${baseProfile}${level}`
128 }
129
130 async function getAudioStreamCodec (path: string, existingProbe?: ffmpeg.FfprobeData) {
131 const { audioStream } = await getAudioStream(path, existingProbe)
132
133 if (!audioStream) return ''
134
135 const audioCodecName = audioStream.codec_name
136
137 if (audioCodecName === 'opus') return 'opus'
138 if (audioCodecName === 'vorbis') return 'vorbis'
139 if (audioCodecName === 'aac') return 'mp4a.40.2'
140
141 logger.warn('Cannot get audio codec of %s.', path, { audioStream })
142
143 return 'mp4a.40.2' // Fallback
144 }
145
146 async function getVideoFileResolution (path: string, existingProbe?: ffmpeg.FfprobeData) {
147 const size = await getVideoStreamSize(path, existingProbe)
148
149 return {
150 videoFileResolution: Math.min(size.height, size.width),
151 isPortraitMode: size.height > size.width
152 }
153 }
154
155 async function getVideoFileFPS (path: string, existingProbe?: ffmpeg.FfprobeData) {
156 const videoStream = await getVideoStreamFromFile(path, existingProbe)
157 if (videoStream === null) return 0
158
159 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
160 const valuesText: string = videoStream[key]
161 if (!valuesText) continue
162
163 const [ frames, seconds ] = valuesText.split('/')
164 if (!frames || !seconds) continue
165
166 const result = parseInt(frames, 10) / parseInt(seconds, 10)
167 if (result > 0) return Math.round(result)
168 }
169
170 return 0
171 }
172
173 async function getMetadataFromFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
174 const metadata = existingProbe || await ffprobePromise(path)
175
176 return new VideoFileMetadata(metadata)
177 }
178
179 async function getVideoFileBitrate (path: string, existingProbe?: ffmpeg.FfprobeData) {
180 const metadata = await getMetadataFromFile(path, existingProbe)
181
182 return metadata.format.bit_rate as number
183 }
184
185 async function getDurationFromVideoFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
186 const metadata = await getMetadataFromFile(path, existingProbe)
187
188 return Math.round(metadata.format.duration)
189 }
190
191 async function getVideoStreamFromFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
192 const metadata = await getMetadataFromFile(path, existingProbe)
193
194 return metadata.streams.find(s => s.codec_type === 'video') || null
195 }
196
197 function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') {
198 const configResolutions = type === 'vod'
199 ? CONFIG.TRANSCODING.RESOLUTIONS
200 : CONFIG.LIVE.TRANSCODING.RESOLUTIONS
201
202 const resolutionsEnabled: number[] = []
203
204 // Put in the order we want to proceed jobs
205 const resolutions = [
206 VideoResolution.H_NOVIDEO,
207 VideoResolution.H_480P,
208 VideoResolution.H_360P,
209 VideoResolution.H_720P,
210 VideoResolution.H_240P,
211 VideoResolution.H_1080P,
212 VideoResolution.H_1440P,
213 VideoResolution.H_4K
214 ]
215
216 for (const resolution of resolutions) {
217 if (configResolutions[resolution + 'p'] === true && videoFileResolution > resolution) {
218 resolutionsEnabled.push(resolution)
219 }
220 }
221
222 return resolutionsEnabled
223 }
224
225 async function canDoQuickTranscode (path: string): Promise<boolean> {
226 if (CONFIG.TRANSCODING.PROFILE !== 'default') return false
227
228 const probe = await ffprobePromise(path)
229
230 return await canDoQuickVideoTranscode(path, probe) &&
231 await canDoQuickAudioTranscode(path, probe)
232 }
233
234 async function canDoQuickVideoTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
235 const videoStream = await getVideoStreamFromFile(path, probe)
236 const fps = await getVideoFileFPS(path, probe)
237 const bitRate = await getVideoFileBitrate(path, probe)
238 const resolution = await getVideoFileResolution(path, probe)
239
240 // If ffprobe did not manage to guess the bitrate
241 if (!bitRate) return false
242
243 // check video params
244 if (videoStream == null) return false
245 if (videoStream['codec_name'] !== 'h264') return false
246 if (videoStream['pix_fmt'] !== 'yuv420p') return false
247 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
248 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
249
250 return true
251 }
252
253 async function canDoQuickAudioTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
254 const parsedAudio = await getAudioStream(path, probe)
255
256 if (!parsedAudio.audioStream) return true
257
258 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
259
260 const audioBitrate = parsedAudio.bitrate
261 if (!audioBitrate) return false
262
263 const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
264 if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
265
266 const channelLayout = parsedAudio.audioStream['channel_layout']
267 // Causes playback issues with Chrome
268 if (!channelLayout || channelLayout === 'unknown') return false
269
270 return true
271 }
272
273 function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDARD'): number {
274 return VIDEO_TRANSCODING_FPS[type].slice(0)
275 .sort((a, b) => fps % a - fps % b)[0]
276 }
277
278 function computeFPS (fpsArg: number, resolution: VideoResolution) {
279 let fps = fpsArg
280
281 if (
282 // On small/medium resolutions, limit FPS
283 resolution !== undefined &&
284 resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
285 fps > VIDEO_TRANSCODING_FPS.AVERAGE
286 ) {
287 // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
288 fps = getClosestFramerateStandard(fps, 'STANDARD')
289 }
290
291 // Hard FPS limits
292 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
293 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
294
295 return fps
296 }
297
298 // ---------------------------------------------------------------------------
299
300 export {
301 getVideoStreamCodec,
302 getAudioStreamCodec,
303 getVideoStreamSize,
304 getVideoFileResolution,
305 getMetadataFromFile,
306 getMaxAudioBitrate,
307 getVideoStreamFromFile,
308 getDurationFromVideoFile,
309 getAudioStream,
310 computeFPS,
311 getVideoFileFPS,
312 ffprobePromise,
313 getClosestFramerateStandard,
314 computeResolutionsToTranscode,
315 getVideoFileBitrate,
316 canDoQuickTranscode,
317 canDoQuickVideoTranscode,
318 canDoQuickAudioTranscode
319 }