aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/helpers/ffmpeg-utils.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/helpers/ffmpeg-utils.ts')
-rw-r--r--server/helpers/ffmpeg-utils.ts166
1 files changed, 102 insertions, 64 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 22bc25476..a108d46a0 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,6 +1,6 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { join } from 'path' 2import { join } from 'path'
3import { VideoResolution } from '../../shared/models/videos' 3import { getTargetBitrate, VideoResolution } from '../../shared/models/videos'
4import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' 4import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers'
5import { processImage } from './image-utils' 5import { processImage } from './image-utils'
6import { logger } from './logger' 6import { logger } from './logger'
@@ -55,6 +55,16 @@ async function getVideoFileFPS (path: string) {
55 return 0 55 return 0
56} 56}
57 57
58async function getVideoFileBitrate (path: string) {
59 return new Promise<number>((res, rej) => {
60 ffmpeg.ffprobe(path, (err, metadata) => {
61 if (err) return rej(err)
62
63 return res(metadata.format.bit_rate)
64 })
65 })
66}
67
58function getDurationFromVideoFile (path: string) { 68function getDurationFromVideoFile (path: string) {
59 return new Promise<number>((res, rej) => { 69 return new Promise<number>((res, rej) => {
60 ffmpeg.ffprobe(path, (err, metadata) => { 70 ffmpeg.ffprobe(path, (err, metadata) => {
@@ -106,45 +116,50 @@ type TranscodeOptions = {
106 116
107function transcode (options: TranscodeOptions) { 117function transcode (options: TranscodeOptions) {
108 return new Promise<void>(async (res, rej) => { 118 return new Promise<void>(async (res, rej) => {
109 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) 119 try {
110 .output(options.outputPath) 120 let fps = await getVideoFileFPS(options.inputPath)
111 .preset(standard)
112
113 if (CONFIG.TRANSCODING.THREADS > 0) {
114 // if we don't set any threads ffmpeg will chose automatically
115 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
116 }
117
118 let fps = await getVideoFileFPS(options.inputPath)
119 if (options.resolution !== undefined) {
120 // '?x720' or '720x?' for example
121 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
122 command = command.size(size)
123
124 // On small/medium resolutions, limit FPS 121 // On small/medium resolutions, limit FPS
125 if ( 122 if (
123 options.resolution !== undefined &&
126 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && 124 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
127 fps > VIDEO_TRANSCODING_FPS.AVERAGE 125 fps > VIDEO_TRANSCODING_FPS.AVERAGE
128 ) { 126 ) {
129 fps = VIDEO_TRANSCODING_FPS.AVERAGE 127 fps = VIDEO_TRANSCODING_FPS.AVERAGE
130 } 128 }
131 }
132 129
133 if (fps) { 130 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
134 // Hard FPS limits 131 .output(options.outputPath)
135 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX 132 command = await presetH264(command, options.resolution, fps)
136 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
137 133
138 command = command.withFPS(fps) 134 if (CONFIG.TRANSCODING.THREADS > 0) {
139 } 135 // if we don't set any threads ffmpeg will chose automatically
136 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
137 }
138
139 if (options.resolution !== undefined) {
140 // '?x720' or '720x?' for example
141 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
142 command = command.size(size)
143 }
144
145 if (fps) {
146 // Hard FPS limits
147 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX
148 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
140 149
141 command 150 command = command.withFPS(fps)
142 .on('error', (err, stdout, stderr) => { 151 }
143 logger.error('Error in transcoding job.', { stdout, stderr }) 152
144 return rej(err) 153 command
145 }) 154 .on('error', (err, stdout, stderr) => {
146 .on('end', res) 155 logger.error('Error in transcoding job.', { stdout, stderr })
147 .run() 156 return rej(err)
157 })
158 .on('end', res)
159 .run()
160 } catch (err) {
161 return rej(err)
162 }
148 }) 163 })
149} 164}
150 165
@@ -157,7 +172,8 @@ export {
157 transcode, 172 transcode,
158 getVideoFileFPS, 173 getVideoFileFPS,
159 computeResolutionsToTranscode, 174 computeResolutionsToTranscode,
160 audio 175 audio,
176 getVideoFileBitrate
161} 177}
162 178
163// --------------------------------------------------------------------------- 179// ---------------------------------------------------------------------------
@@ -182,11 +198,10 @@ function getVideoFileStream (path: string) {
182 * and quality. Superfast and ultrafast will give you better 198 * and quality. Superfast and ultrafast will give you better
183 * performance, but then quality is noticeably worse. 199 * performance, but then quality is noticeably worse.
184 */ 200 */
185function veryfast (_ffmpeg) { 201async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> {
186 _ffmpeg 202 let localCommand = await presetH264(command, resolution, fps)
187 .preset(standard) 203 localCommand = localCommand.outputOption('-preset:v veryfast')
188 .outputOption('-preset:v veryfast') 204 .outputOption([ '--aq-mode=2', '--aq-strength=1.3' ])
189 .outputOption(['--aq-mode=2', '--aq-strength=1.3'])
190 /* 205 /*
191 MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html 206 MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html
192 Our target situation is closer to a livestream than a stream, 207 Our target situation is closer to a livestream than a stream,
@@ -198,31 +213,39 @@ function veryfast (_ffmpeg) {
198 Make up for most of the loss of grain and macroblocking 213 Make up for most of the loss of grain and macroblocking
199 with less computing power. 214 with less computing power.
200 */ 215 */
216
217 return localCommand
201} 218}
202 219
203/** 220/**
204 * A preset optimised for a stillimage audio video 221 * A preset optimised for a stillimage audio video
205 */ 222 */
206function audio (_ffmpeg) { 223async function presetStillImageWithAudio (
207 _ffmpeg 224 command: ffmpeg.FfmpegCommand,
208 .preset(veryfast) 225 resolution: VideoResolution,
209 .outputOption('-tune stillimage') 226 fps: number
227): Promise<ffmpeg.FfmpegCommand> {
228 let localCommand = await presetH264VeryFast(command, resolution, fps)
229 localCommand = localCommand.outputOption('-tune stillimage')
230
231 return localCommand
210} 232}
211 233
212/** 234/**
213 * A toolbox to play with audio 235 * A toolbox to play with audio
214 */ 236 */
215namespace audio { 237namespace audio {
216 export const get = (_ffmpeg, pos: number | string = 0) => { 238 export const get = (option: ffmpeg.FfmpegCommand | string) => {
217 // without position, ffprobe considers the last input only 239 // without position, ffprobe considers the last input only
218 // we make it consider the first input only 240 // we make it consider the first input only
219 // if you pass a file path to pos, then ffprobe acts on that file directly 241 // if you pass a file path to pos, then ffprobe acts on that file directly
220 return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { 242 return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => {
221 _ffmpeg.ffprobe(pos, (err,data) => { 243
244 function parseFfprobe (err: any, data: ffmpeg.FfprobeData) {
222 if (err) return rej(err) 245 if (err) return rej(err)
223 246
224 if ('streams' in data) { 247 if ('streams' in data) {
225 const audioStream = data['streams'].find(stream => stream['codec_type'] === 'audio') 248 const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio')
226 if (audioStream) { 249 if (audioStream) {
227 return res({ 250 return res({
228 absolutePath: data.format.filename, 251 absolutePath: data.format.filename,
@@ -230,8 +253,15 @@ namespace audio {
230 }) 253 })
231 } 254 }
232 } 255 }
256
233 return res({ absolutePath: data.format.filename }) 257 return res({ absolutePath: data.format.filename })
234 }) 258 }
259
260 if (typeof option === 'string') {
261 return ffmpeg.ffprobe(option, parseFfprobe)
262 }
263
264 return option.ffprobe(parseFfprobe)
235 }) 265 })
236 } 266 }
237 267
@@ -273,8 +303,8 @@ namespace audio {
273 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel 303 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel
274 * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr 304 * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr
275 */ 305 */
276async function standard (_ffmpeg) { 306async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> {
277 let localFfmpeg = _ffmpeg 307 let localCommand = command
278 .format('mp4') 308 .format('mp4')
279 .videoCodec('libx264') 309 .videoCodec('libx264')
280 .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution 310 .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution
@@ -282,30 +312,38 @@ async function standard (_ffmpeg) {
282 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 312 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
283 .outputOption('-map_metadata -1') // strip all metadata 313 .outputOption('-map_metadata -1') // strip all metadata
284 .outputOption('-movflags faststart') 314 .outputOption('-movflags faststart')
285 const _audio = await audio.get(localFfmpeg)
286 315
287 if (!_audio.audioStream) { 316 const parsedAudio = await audio.get(localCommand)
288 return localFfmpeg.noAudio()
289 }
290 317
291 // we favor VBR, if a good AAC encoder is available 318 if (!parsedAudio.audioStream) {
292 if ((await checkFFmpegEncoders()).get('libfdk_aac')) { 319 localCommand = localCommand.noAudio()
293 return localFfmpeg 320 } else if ((await checkFFmpegEncoders()).get('libfdk_aac')) { // we favor VBR, if a good AAC encoder is available
321 localCommand = localCommand
294 .audioCodec('libfdk_aac') 322 .audioCodec('libfdk_aac')
295 .audioQuality(5) 323 .audioQuality(5)
324 } else {
325 // we try to reduce the ceiling bitrate by making rough correspondances of bitrates
326 // of course this is far from perfect, but it might save some space in the end
327 const audioCodecName = parsedAudio.audioStream[ 'codec_name' ]
328 let bitrate: number
329 if (audio.bitrate[ audioCodecName ]) {
330 bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ])
331
332 if (bitrate === -1) localCommand = localCommand.audioCodec('copy')
333 else if (bitrate !== undefined) localCommand = localCommand.audioBitrate(bitrate)
334 }
296 } 335 }
297 336
298 // we try to reduce the ceiling bitrate by making rough correspondances of bitrates 337 // Constrained Encoding (VBV)
299 // of course this is far from perfect, but it might save some space in the end 338 // https://slhck.info/video/2017/03/01/rate-control.html
300 const audioCodecName = _audio.audioStream['codec_name'] 339 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
301 let bitrate: number 340 const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
302 if (audio.bitrate[audioCodecName]) { 341 localCommand = localCommand.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`])
303 bitrate = audio.bitrate[audioCodecName](_audio.audioStream['bit_rate'])
304
305 if (bitrate === -1) return localFfmpeg.audioCodec('copy')
306 }
307 342
308 if (bitrate !== undefined) return localFfmpeg.audioBitrate(bitrate) 343 // Keyframe interval of 2 seconds for faster seeking and resolution switching.
344 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
345 // https://superuser.com/a/908325
346 localCommand = localCommand.outputOption(`-g ${ fps * 2 }`)
309 347
310 return localFfmpeg 348 return localCommand
311} 349}