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