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