aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/helpers/ffmpeg/ffprobe-utils.ts
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-04-21 14:55:10 +0200
committerChocobozzz <chocobozzz@cpy.re>2023-05-09 08:57:34 +0200
commit0c9668f77901e7540e2c7045eb0f2974a4842a69 (patch)
tree226d3dd1565b0bb56588897af3b8530e6216e96b /server/helpers/ffmpeg/ffprobe-utils.ts
parent6bcb854cdea8688a32240bc5719c7d139806e00b (diff)
downloadPeerTube-0c9668f77901e7540e2c7045eb0f2974a4842a69.tar.gz
PeerTube-0c9668f77901e7540e2c7045eb0f2974a4842a69.tar.zst
PeerTube-0c9668f77901e7540e2c7045eb0f2974a4842a69.zip
Implement remote runner jobs in server
Move ffmpeg functions to @shared
Diffstat (limited to 'server/helpers/ffmpeg/ffprobe-utils.ts')
-rw-r--r--server/helpers/ffmpeg/ffprobe-utils.ts254
1 files changed, 0 insertions, 254 deletions
diff --git a/server/helpers/ffmpeg/ffprobe-utils.ts b/server/helpers/ffmpeg/ffprobe-utils.ts
deleted file mode 100644
index fb270b3cb..000000000
--- a/server/helpers/ffmpeg/ffprobe-utils.ts
+++ /dev/null
@@ -1,254 +0,0 @@
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 { toEven } from '../core-utils'
19import { logger } from '../logger'
20
21/**
22 *
23 * Helpers to run ffprobe and extract data from the JSON output
24 *
25 */
26
27// ---------------------------------------------------------------------------
28// Codecs
29// ---------------------------------------------------------------------------
30
31async function getVideoStreamCodec (path: string) {
32 const videoStream = await getVideoStream(path)
33 if (!videoStream) return ''
34
35 const videoCodec = videoStream.codec_tag_string
36
37 if (videoCodec === 'vp09') return 'vp09.00.50.08'
38 if (videoCodec === 'hev1') return 'hev1.1.6.L93.B0'
39
40 const baseProfileMatrix = {
41 avc1: {
42 High: '6400',
43 Main: '4D40',
44 Baseline: '42E0'
45 },
46 av01: {
47 High: '1',
48 Main: '0',
49 Professional: '2'
50 }
51 }
52
53 let baseProfile = baseProfileMatrix[videoCodec][videoStream.profile]
54 if (!baseProfile) {
55 logger.warn('Cannot get video profile codec of %s.', path, { videoStream })
56 baseProfile = baseProfileMatrix[videoCodec]['High'] // Fallback
57 }
58
59 if (videoCodec === 'av01') {
60 let level = videoStream.level.toString()
61 if (level.length === 1) level = `0${level}`
62
63 // Guess the tier indicator and bit depth
64 return `${videoCodec}.${baseProfile}.${level}M.08`
65 }
66
67 let level = videoStream.level.toString(16)
68 if (level.length === 1) level = `0${level}`
69
70 // Default, h264 codec
71 return `${videoCodec}.${baseProfile}${level}`
72}
73
74async function getAudioStreamCodec (path: string, existingProbe?: FfprobeData) {
75 const { audioStream } = await getAudioStream(path, existingProbe)
76
77 if (!audioStream) return ''
78
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'
84 if (audioCodecName === 'mp3') return 'mp4a.40.34'
85
86 logger.warn('Cannot get audio codec of %s.', path, { audioStream })
87
88 return 'mp4a.40.2' // Fallback
89}
90
91// ---------------------------------------------------------------------------
92// Resolutions
93// ---------------------------------------------------------------------------
94
95function computeResolutionsToTranscode (options: {
96 input: number
97 type: 'vod' | 'live'
98 includeInput: boolean
99 strictLower: boolean
100 hasAudio: boolean
101}) {
102 const { input, type, includeInput, strictLower, hasAudio } = options
103
104 const configResolutions = type === 'vod'
105 ? CONFIG.TRANSCODING.RESOLUTIONS
106 : CONFIG.LIVE.TRANSCODING.RESOLUTIONS
107
108 const resolutionsEnabled = new Set<number>()
109
110 // Put in the order we want to proceed jobs
111 const availableResolutions: VideoResolution[] = [
112 VideoResolution.H_NOVIDEO,
113 VideoResolution.H_480P,
114 VideoResolution.H_360P,
115 VideoResolution.H_720P,
116 VideoResolution.H_240P,
117 VideoResolution.H_144P,
118 VideoResolution.H_1080P,
119 VideoResolution.H_1440P,
120 VideoResolution.H_4K
121 ]
122
123 for (const resolution of availableResolutions) {
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
130 // Audio resolutio but no audio in the video
131 if (resolution === VideoResolution.H_NOVIDEO && !hasAudio) continue
132
133 resolutionsEnabled.add(resolution)
134 }
135
136 if (includeInput) {
137 // Always use an even resolution to avoid issues with ffmpeg
138 resolutionsEnabled.add(toEven(input))
139 }
140
141 return Array.from(resolutionsEnabled)
142}
143
144// ---------------------------------------------------------------------------
145// Can quick transcode
146// ---------------------------------------------------------------------------
147
148async function canDoQuickTranscode (path: string): Promise<boolean> {
149 if (CONFIG.TRANSCODING.PROFILE !== 'default') return false
150
151 const probe = await ffprobePromise(path)
152
153 return await canDoQuickVideoTranscode(path, probe) &&
154 await canDoQuickAudioTranscode(path, probe)
155}
156
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
172 if (!channelLayout || channelLayout === 'unknown' || channelLayout === 'quad') return false
173
174 return true
175}
176
177async function canDoQuickVideoTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
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)
182
183 // If ffprobe did not manage to guess the bitrate
184 if (!bitRate) return false
185
186 // check video params
187 if (!videoStream) return false
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
191 if (bitRate > getMaxBitrate({ ...resolutionData, fps })) return false
192
193 return true
194}
195
196// ---------------------------------------------------------------------------
197// Framerate
198// ---------------------------------------------------------------------------
199
200function getClosestFramerateStandard <K extends keyof Pick<VideoTranscodingFPS, 'HD_STANDARD' | 'STANDARD'>> (fps: number, type: K) {
201 return VIDEO_TRANSCODING_FPS[type].slice(0)
202 .sort((a, b) => fps % a - fps % b)[0]
203}
204
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')
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 }
224
225 return fps
226}
227
228// ---------------------------------------------------------------------------
229
230export {
231 // Re export ffprobe utils
232 getVideoStreamDimensionsInfo,
233 buildFileMetadata,
234 getMaxAudioBitrate,
235 getVideoStream,
236 getVideoStreamDuration,
237 getAudioStream,
238 hasAudioStream,
239 getVideoStreamFPS,
240 ffprobePromise,
241 getVideoStreamBitrate,
242
243 getVideoStreamCodec,
244 getAudioStreamCodec,
245
246 computeFPS,
247 getClosestFramerateStandard,
248
249 computeResolutionsToTranscode,
250
251 canDoQuickTranscode,
252 canDoQuickVideoTranscode,
253 canDoQuickAudioTranscode
254}