diff options
author | Chocobozzz <me@florianbigard.com> | 2023-04-21 14:55:10 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2023-05-09 08:57:34 +0200 |
commit | 0c9668f77901e7540e2c7045eb0f2974a4842a69 (patch) | |
tree | 226d3dd1565b0bb56588897af3b8530e6216e96b /server/helpers/ffmpeg/ffprobe-utils.ts | |
parent | 6bcb854cdea8688a32240bc5719c7d139806e00b (diff) | |
download | PeerTube-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.ts | 254 |
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 @@ | |||
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 { toEven } from '../core-utils' | ||
19 | import { 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 | |||
31 | async 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 | |||
74 | async 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 | |||
95 | function 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 | |||
148 | async 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 | |||
157 | async 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 | |||
177 | async 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 | |||
200 | function 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 | |||
205 | function 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 | |||
230 | export { | ||
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 | } | ||