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