]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/ffmpeg/ffprobe-utils.ts
7bcd27665829756d5b8b4706ff09c97573b20445
[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 inputResolution: number
95 type: 'vod' | 'live'
96 includeInputResolution: boolean
97 }) {
98 const { inputResolution, type, includeInputResolution } = options
99
100 const configResolutions = type === 'vod'
101 ? CONFIG.TRANSCODING.RESOLUTIONS
102 : CONFIG.LIVE.TRANSCODING.RESOLUTIONS
103
104 const resolutionsEnabled = new Set<number>()
105
106 // Put in the order we want to proceed jobs
107 const availableResolutions: VideoResolution[] = [
108 VideoResolution.H_NOVIDEO,
109 VideoResolution.H_480P,
110 VideoResolution.H_360P,
111 VideoResolution.H_720P,
112 VideoResolution.H_240P,
113 VideoResolution.H_144P,
114 VideoResolution.H_1080P,
115 VideoResolution.H_1440P,
116 VideoResolution.H_4K
117 ]
118
119 for (const resolution of availableResolutions) {
120 if (configResolutions[resolution + 'p'] === true && inputResolution > resolution) {
121 resolutionsEnabled.add(resolution)
122 }
123 }
124
125 if (includeInputResolution) {
126 resolutionsEnabled.add(inputResolution)
127 }
128
129 return Array.from(resolutionsEnabled)
130 }
131
132 // ---------------------------------------------------------------------------
133 // Can quick transcode
134 // ---------------------------------------------------------------------------
135
136 async function canDoQuickTranscode (path: string): Promise<boolean> {
137 if (CONFIG.TRANSCODING.PROFILE !== 'default') return false
138
139 const probe = await ffprobePromise(path)
140
141 return await canDoQuickVideoTranscode(path, probe) &&
142 await canDoQuickAudioTranscode(path, probe)
143 }
144
145 async function canDoQuickAudioTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
146 const parsedAudio = await getAudioStream(path, probe)
147
148 if (!parsedAudio.audioStream) return true
149
150 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
151
152 const audioBitrate = parsedAudio.bitrate
153 if (!audioBitrate) return false
154
155 const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
156 if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
157
158 const channelLayout = parsedAudio.audioStream['channel_layout']
159 // Causes playback issues with Chrome
160 if (!channelLayout || channelLayout === 'unknown') return false
161
162 return true
163 }
164
165 async function canDoQuickVideoTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
166 const videoStream = await getVideoStream(path, probe)
167 const fps = await getVideoStreamFPS(path, probe)
168 const bitRate = await getVideoStreamBitrate(path, probe)
169 const resolutionData = await getVideoStreamDimensionsInfo(path, probe)
170
171 // If ffprobe did not manage to guess the bitrate
172 if (!bitRate) return false
173
174 // check video params
175 if (!videoStream) return false
176 if (videoStream['codec_name'] !== 'h264') return false
177 if (videoStream['pix_fmt'] !== 'yuv420p') return false
178 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
179 if (bitRate > getMaxBitrate({ ...resolutionData, fps })) return false
180
181 return true
182 }
183
184 // ---------------------------------------------------------------------------
185 // Framerate
186 // ---------------------------------------------------------------------------
187
188 function getClosestFramerateStandard <K extends keyof Pick<VideoTranscodingFPS, 'HD_STANDARD' | 'STANDARD'>> (fps: number, type: K) {
189 return VIDEO_TRANSCODING_FPS[type].slice(0)
190 .sort((a, b) => fps % a - fps % b)[0]
191 }
192
193 function computeFPS (fpsArg: number, resolution: VideoResolution) {
194 let fps = fpsArg
195
196 if (
197 // On small/medium resolutions, limit FPS
198 resolution !== undefined &&
199 resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
200 fps > VIDEO_TRANSCODING_FPS.AVERAGE
201 ) {
202 // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
203 fps = getClosestFramerateStandard(fps, 'STANDARD')
204 }
205
206 // Hard FPS limits
207 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
208
209 if (fps < VIDEO_TRANSCODING_FPS.MIN) {
210 throw new Error(`Cannot compute FPS because ${fps} is lower than our minimum value ${VIDEO_TRANSCODING_FPS.MIN}`)
211 }
212
213 return fps
214 }
215
216 // ---------------------------------------------------------------------------
217
218 export {
219 // Re export ffprobe utils
220 getVideoStreamDimensionsInfo,
221 buildFileMetadata,
222 getMaxAudioBitrate,
223 getVideoStream,
224 getVideoStreamDuration,
225 getAudioStream,
226 hasAudioStream,
227 getVideoStreamFPS,
228 ffprobePromise,
229 getVideoStreamBitrate,
230
231 getVideoStreamCodec,
232 getAudioStreamCodec,
233
234 computeFPS,
235 getClosestFramerateStandard,
236
237 computeResolutionsToTranscode,
238
239 canDoQuickTranscode,
240 canDoQuickVideoTranscode,
241 canDoQuickAudioTranscode
242 }