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