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