]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/ffmpeg/ffprobe-utils.ts
Add basic video editor support
[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
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 }