]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/ffmpeg/ffprobe-utils.ts
Merge branch 'release/4.3.0' into develop
[github/Chocobozzz/PeerTube.git] / server / helpers / ffmpeg / ffprobe-utils.ts
1 import { FfprobeData } from 'fluent-ffmpeg'
2 import { getMaxBitrate } from '@shared/core-utils'
3 import {
4 buildFileMetadata,
5 ffprobePromise,
6 getAudioStream,
7 getMaxAudioBitrate,
8 getVideoStream,
9 getVideoStreamBitrate,
10 getVideoStreamDimensionsInfo,
11 getVideoStreamDuration,
12 getVideoStreamFPS,
13 hasAudioStream
14 } from '@shared/extra-utils/ffprobe'
15 import { VideoResolution, VideoTranscodingFPS } from '@shared/models'
16 import { CONFIG } from '../../initializers/config'
17 import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants'
18 import { logger } from '../logger'
19
20 /**
21 *
22 * Helpers to run ffprobe and extract data from the JSON output
23 *
24 */
25
26 // ---------------------------------------------------------------------------
27 // Codecs
28 // ---------------------------------------------------------------------------
29
30 async function getVideoStreamCodec (path: string) {
31 const videoStream = await getVideoStream(path)
32 if (!videoStream) return ''
33
34 const videoCodec = videoStream.codec_tag_string
35
36 if (videoCodec === 'vp09') return 'vp09.00.50.08'
37 if (videoCodec === 'hev1') return 'hev1.1.6.L93.B0'
38
39 const baseProfileMatrix = {
40 avc1: {
41 High: '6400',
42 Main: '4D40',
43 Baseline: '42E0'
44 },
45 av01: {
46 High: '1',
47 Main: '0',
48 Professional: '2'
49 }
50 }
51
52 let baseProfile = baseProfileMatrix[videoCodec][videoStream.profile]
53 if (!baseProfile) {
54 logger.warn('Cannot get video profile codec of %s.', path, { videoStream })
55 baseProfile = baseProfileMatrix[videoCodec]['High'] // Fallback
56 }
57
58 if (videoCodec === 'av01') {
59 let level = videoStream.level.toString()
60 if (level.length === 1) level = `0${level}`
61
62 // Guess the tier indicator and bit depth
63 return `${videoCodec}.${baseProfile}.${level}M.08`
64 }
65
66 let level = videoStream.level.toString(16)
67 if (level.length === 1) level = `0${level}`
68
69 // Default, h264 codec
70 return `${videoCodec}.${baseProfile}${level}`
71 }
72
73 async function getAudioStreamCodec (path: string, existingProbe?: FfprobeData) {
74 const { audioStream } = await getAudioStream(path, existingProbe)
75
76 if (!audioStream) return ''
77
78 const audioCodecName = audioStream.codec_name
79
80 if (audioCodecName === 'opus') return 'opus'
81 if (audioCodecName === 'vorbis') return 'vorbis'
82 if (audioCodecName === 'aac') return 'mp4a.40.2'
83 if (audioCodecName === 'mp3') return 'mp4a.40.34'
84
85 logger.warn('Cannot get audio codec of %s.', path, { audioStream })
86
87 return 'mp4a.40.2' // Fallback
88 }
89
90 // ---------------------------------------------------------------------------
91 // Resolutions
92 // ---------------------------------------------------------------------------
93
94 function computeResolutionsToTranscode (options: {
95 input: number
96 type: 'vod' | 'live'
97 includeInput: boolean
98 strictLower: boolean
99 }) {
100 const { input, type, includeInput, strictLower } = options
101
102 const configResolutions = type === 'vod'
103 ? CONFIG.TRANSCODING.RESOLUTIONS
104 : CONFIG.LIVE.TRANSCODING.RESOLUTIONS
105
106 const resolutionsEnabled = new Set<number>()
107
108 // Put in the order we want to proceed jobs
109 const availableResolutions: VideoResolution[] = [
110 VideoResolution.H_NOVIDEO,
111 VideoResolution.H_480P,
112 VideoResolution.H_360P,
113 VideoResolution.H_720P,
114 VideoResolution.H_240P,
115 VideoResolution.H_144P,
116 VideoResolution.H_1080P,
117 VideoResolution.H_1440P,
118 VideoResolution.H_4K
119 ]
120
121 for (const resolution of availableResolutions) {
122 // Resolution not enabled
123 if (configResolutions[resolution + 'p'] !== true) continue
124 // Too big resolution for input file
125 if (input < resolution) continue
126 // We only want lower resolutions than input file
127 if (strictLower && input === resolution) continue
128
129 resolutionsEnabled.add(resolution)
130 }
131
132 if (includeInput) {
133 resolutionsEnabled.add(input)
134 }
135
136 return Array.from(resolutionsEnabled)
137 }
138
139 // ---------------------------------------------------------------------------
140 // Can quick transcode
141 // ---------------------------------------------------------------------------
142
143 async function canDoQuickTranscode (path: string): Promise<boolean> {
144 if (CONFIG.TRANSCODING.PROFILE !== 'default') return false
145
146 const probe = await ffprobePromise(path)
147
148 return await canDoQuickVideoTranscode(path, probe) &&
149 await canDoQuickAudioTranscode(path, probe)
150 }
151
152 async function canDoQuickAudioTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
153 const parsedAudio = await getAudioStream(path, probe)
154
155 if (!parsedAudio.audioStream) return true
156
157 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
158
159 const audioBitrate = parsedAudio.bitrate
160 if (!audioBitrate) return false
161
162 const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
163 if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
164
165 const channelLayout = parsedAudio.audioStream['channel_layout']
166 // Causes playback issues with Chrome
167 if (!channelLayout || channelLayout === 'unknown' || channelLayout === 'quad') return false
168
169 return true
170 }
171
172 async function canDoQuickVideoTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
173 const videoStream = await getVideoStream(path, probe)
174 const fps = await getVideoStreamFPS(path, probe)
175 const bitRate = await getVideoStreamBitrate(path, probe)
176 const resolutionData = await getVideoStreamDimensionsInfo(path, probe)
177
178 // If ffprobe did not manage to guess the bitrate
179 if (!bitRate) return false
180
181 // check video params
182 if (!videoStream) return false
183 if (videoStream['codec_name'] !== 'h264') return false
184 if (videoStream['pix_fmt'] !== 'yuv420p') return false
185 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
186 if (bitRate > getMaxBitrate({ ...resolutionData, fps })) return false
187
188 return true
189 }
190
191 // ---------------------------------------------------------------------------
192 // Framerate
193 // ---------------------------------------------------------------------------
194
195 function getClosestFramerateStandard <K extends keyof Pick<VideoTranscodingFPS, 'HD_STANDARD' | 'STANDARD'>> (fps: number, type: K) {
196 return VIDEO_TRANSCODING_FPS[type].slice(0)
197 .sort((a, b) => fps % a - fps % b)[0]
198 }
199
200 function computeFPS (fpsArg: number, resolution: VideoResolution) {
201 let fps = fpsArg
202
203 if (
204 // On small/medium resolutions, limit FPS
205 resolution !== undefined &&
206 resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
207 fps > VIDEO_TRANSCODING_FPS.AVERAGE
208 ) {
209 // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
210 fps = getClosestFramerateStandard(fps, 'STANDARD')
211 }
212
213 // Hard FPS limits
214 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
215
216 if (fps < VIDEO_TRANSCODING_FPS.MIN) {
217 throw new Error(`Cannot compute FPS because ${fps} is lower than our minimum value ${VIDEO_TRANSCODING_FPS.MIN}`)
218 }
219
220 return fps
221 }
222
223 // ---------------------------------------------------------------------------
224
225 export {
226 // Re export ffprobe utils
227 getVideoStreamDimensionsInfo,
228 buildFileMetadata,
229 getMaxAudioBitrate,
230 getVideoStream,
231 getVideoStreamDuration,
232 getAudioStream,
233 hasAudioStream,
234 getVideoStreamFPS,
235 ffprobePromise,
236 getVideoStreamBitrate,
237
238 getVideoStreamCodec,
239 getAudioStreamCodec,
240
241 computeFPS,
242 getClosestFramerateStandard,
243
244 computeResolutionsToTranscode,
245
246 canDoQuickTranscode,
247 canDoQuickVideoTranscode,
248 canDoQuickAudioTranscode
249 }