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