aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/helpers
diff options
context:
space:
mode:
Diffstat (limited to 'server/helpers')
-rw-r--r--server/helpers/ffmpeg-utils.ts154
1 files changed, 86 insertions, 68 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 2fdf34cb7..c180da832 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -117,37 +117,50 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
117 } 117 }
118} 118}
119 119
120type TranscodeOptions = { 120type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio'
121
122interface BaseTranscodeOptions {
123 type: TranscodeOptionsType
121 inputPath: string 124 inputPath: string
122 outputPath: string 125 outputPath: string
123 resolution: VideoResolution 126 resolution: VideoResolution
124 isPortraitMode?: boolean 127 isPortraitMode?: boolean
125 doQuickTranscode?: Boolean 128}
126 129
127 hlsPlaylist?: { 130interface HLSTranscodeOptions extends BaseTranscodeOptions {
131 type: 'hls'
132 hlsPlaylist: {
128 videoFilename: string 133 videoFilename: string
129 } 134 }
130} 135}
131 136
137interface QuickTranscodeOptions extends BaseTranscodeOptions {
138 type: 'quick-transcode'
139}
140
141interface VideoTranscodeOptions extends BaseTranscodeOptions {
142 type: 'video'
143}
144
145interface MergeAudioTranscodeOptions extends BaseTranscodeOptions {
146 type: 'merge-audio'
147 audioPath: string
148}
149
150type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | QuickTranscodeOptions
151
132function transcode (options: TranscodeOptions) { 152function transcode (options: TranscodeOptions) {
133 return new Promise<void>(async (res, rej) => { 153 return new Promise<void>(async (res, rej) => {
134 try { 154 try {
135 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) 155 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
136 .output(options.outputPath) 156 .output(options.outputPath)
137 157
138 if (options.doQuickTranscode) { 158 if (options.type === 'quick-transcode') {
139 if (options.hlsPlaylist) { 159 command = await buildQuickTranscodeCommand(command)
140 throw(Error("Quick transcode and HLS can't be used at the same time")) 160 } else if (options.type === 'hls') {
141 }
142
143 command
144 .format('mp4')
145 .addOption('-c:v copy')
146 .addOption('-c:a copy')
147 .outputOption('-map_metadata -1') // strip all metadata
148 .outputOption('-movflags faststart')
149 } else if (options.hlsPlaylist) {
150 command = await buildHLSCommand(command, options) 161 command = await buildHLSCommand(command, options)
162 } else if (options.type === 'merge-audio') {
163 command = await buildAudioMergeCommand(command, options)
151 } else { 164 } else {
152 command = await buildx264Command(command, options) 165 command = await buildx264Command(command, options)
153 } 166 }
@@ -163,7 +176,7 @@ function transcode (options: TranscodeOptions) {
163 return rej(err) 176 return rej(err)
164 }) 177 })
165 .on('end', () => { 178 .on('end', () => {
166 return onTranscodingSuccess(options) 179 return fixHLSPlaylistIfNeeded(options)
167 .then(() => res()) 180 .then(() => res())
168 .catch(err => rej(err)) 181 .catch(err => rej(err))
169 }) 182 })
@@ -205,6 +218,8 @@ export {
205 getVideoFileResolution, 218 getVideoFileResolution,
206 getDurationFromVideoFile, 219 getDurationFromVideoFile,
207 generateImageFromVideoFile, 220 generateImageFromVideoFile,
221 TranscodeOptions,
222 TranscodeOptionsType,
208 transcode, 223 transcode,
209 getVideoFileFPS, 224 getVideoFileFPS,
210 computeResolutionsToTranscode, 225 computeResolutionsToTranscode,
@@ -215,7 +230,7 @@ export {
215 230
216// --------------------------------------------------------------------------- 231// ---------------------------------------------------------------------------
217 232
218async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { 233async function buildx264Command (command: ffmpeg.FfmpegCommand, options: VideoTranscodeOptions) {
219 let fps = await getVideoFileFPS(options.inputPath) 234 let fps = await getVideoFileFPS(options.inputPath)
220 // On small/medium resolutions, limit FPS 235 // On small/medium resolutions, limit FPS
221 if ( 236 if (
@@ -226,7 +241,7 @@ async function buildx264Command (command: ffmpeg.FfmpegCommand, options: Transco
226 fps = VIDEO_TRANSCODING_FPS.AVERAGE 241 fps = VIDEO_TRANSCODING_FPS.AVERAGE
227 } 242 }
228 243
229 command = await presetH264(command, options.resolution, fps) 244 command = await presetH264(command, options.inputPath, options.resolution, fps)
230 245
231 if (options.resolution !== undefined) { 246 if (options.resolution !== undefined) {
232 // '?x720' or '720x?' for example 247 // '?x720' or '720x?' for example
@@ -245,7 +260,29 @@ async function buildx264Command (command: ffmpeg.FfmpegCommand, options: Transco
245 return command 260 return command
246} 261}
247 262
248async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { 263async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) {
264 command = command.loop(undefined)
265
266 command = await presetH264VeryFast(command, options.audioPath, options.resolution)
267
268 command = command.input(options.audioPath)
269 .videoFilter('scale=trunc(iw/2)*2:trunc(ih/2)*2') // Avoid "height not divisible by 2" error
270 .outputOption('-tune stillimage')
271 .outputOption('-shortest')
272
273 return command
274}
275
276async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) {
277 command = await presetCopy(command)
278
279 command = command.outputOption('-map_metadata -1') // strip all metadata
280 .outputOption('-movflags faststart')
281
282 return command
283}
284
285async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) {
249 const videoPath = getHLSVideoPath(options) 286 const videoPath = getHLSVideoPath(options)
250 287
251 command = await presetCopy(command) 288 command = await presetCopy(command)
@@ -261,19 +298,19 @@ async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: Transcod
261 return command 298 return command
262} 299}
263 300
264function getHLSVideoPath (options: TranscodeOptions) { 301function getHLSVideoPath (options: HLSTranscodeOptions) {
265 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` 302 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
266} 303}
267 304
268async function onTranscodingSuccess (options: TranscodeOptions) { 305async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) {
269 if (!options.hlsPlaylist) return 306 if (options.type !== 'hls') return
270 307
271 // Fix wrong mapping with some ffmpeg versions
272 const fileContent = await readFile(options.outputPath) 308 const fileContent = await readFile(options.outputPath)
273 309
274 const videoFileName = options.hlsPlaylist.videoFilename 310 const videoFileName = options.hlsPlaylist.videoFilename
275 const videoFilePath = getHLSVideoPath(options) 311 const videoFilePath = getHLSVideoPath(options)
276 312
313 // Fix wrong mapping with some ffmpeg versions
277 const newContent = fileContent.toString() 314 const newContent = fileContent.toString()
278 .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`) 315 .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
279 316
@@ -300,44 +337,27 @@ function getVideoStreamFromFile (path: string) {
300 * and quality. Superfast and ultrafast will give you better 337 * and quality. Superfast and ultrafast will give you better
301 * performance, but then quality is noticeably worse. 338 * performance, but then quality is noticeably worse.
302 */ 339 */
303async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { 340async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, input: string, resolution: VideoResolution, fps?: number) {
304 let localCommand = await presetH264(command, resolution, fps) 341 let localCommand = await presetH264(command, input, resolution, fps)
342
305 localCommand = localCommand.outputOption('-preset:v veryfast') 343 localCommand = localCommand.outputOption('-preset:v veryfast')
306 .outputOption([ '--aq-mode=2', '--aq-strength=1.3' ]) 344
307 /* 345 /*
308 MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html 346 MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html
309 Our target situation is closer to a livestream than a stream, 347 Our target situation is closer to a livestream than a stream,
310 since we want to reduce as much a possible the encoding burden, 348 since we want to reduce as much a possible the encoding burden,
311 altough not to the point of a livestream where there is a hard 349 although not to the point of a livestream where there is a hard
312 constraint on the frames per second to be encoded. 350 constraint on the frames per second to be encoded.
313
314 why '--aq-mode=2 --aq-strength=1.3' instead of '-profile:v main'?
315 Make up for most of the loss of grain and macroblocking
316 with less computing power.
317 */ 351 */
318 352
319 return localCommand 353 return localCommand
320} 354}
321 355
322/** 356/**
323 * A preset optimised for a stillimage audio video
324 */
325async function presetStillImageWithAudio (
326 command: ffmpeg.FfmpegCommand,
327 resolution: VideoResolution,
328 fps: number
329): Promise<ffmpeg.FfmpegCommand> {
330 let localCommand = await presetH264VeryFast(command, resolution, fps)
331 localCommand = localCommand.outputOption('-tune stillimage')
332
333 return localCommand
334}
335
336/**
337 * A toolbox to play with audio 357 * A toolbox to play with audio
338 */ 358 */
339namespace audio { 359namespace audio {
340 export const get = (option: ffmpeg.FfmpegCommand | string) => { 360 export const get = (option: string) => {
341 // without position, ffprobe considers the last input only 361 // without position, ffprobe considers the last input only
342 // we make it consider the first input only 362 // we make it consider the first input only
343 // if you pass a file path to pos, then ffprobe acts on that file directly 363 // if you pass a file path to pos, then ffprobe acts on that file directly
@@ -359,11 +379,7 @@ namespace audio {
359 return res({ absolutePath: data.format.filename }) 379 return res({ absolutePath: data.format.filename })
360 } 380 }
361 381
362 if (typeof option === 'string') { 382 return ffmpeg.ffprobe(option, parseFfprobe)
363 return ffmpeg.ffprobe(option, parseFfprobe)
364 }
365
366 return option.ffprobe(parseFfprobe)
367 }) 383 })
368 } 384 }
369 385
@@ -405,7 +421,7 @@ namespace audio {
405 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel 421 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel
406 * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr 422 * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr
407 */ 423 */
408async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { 424async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolution: VideoResolution, fps?: number) {
409 let localCommand = command 425 let localCommand = command
410 .format('mp4') 426 .format('mp4')
411 .videoCodec('libx264') 427 .videoCodec('libx264')
@@ -416,7 +432,7 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol
416 .outputOption('-map_metadata -1') // strip all metadata 432 .outputOption('-map_metadata -1') // strip all metadata
417 .outputOption('-movflags faststart') 433 .outputOption('-movflags faststart')
418 434
419 const parsedAudio = await audio.get(localCommand) 435 const parsedAudio = await audio.get(input)
420 436
421 if (!parsedAudio.audioStream) { 437 if (!parsedAudio.audioStream) {
422 localCommand = localCommand.noAudio() 438 localCommand = localCommand.noAudio()
@@ -425,28 +441,30 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol
425 .audioCodec('libfdk_aac') 441 .audioCodec('libfdk_aac')
426 .audioQuality(5) 442 .audioQuality(5)
427 } else { 443 } else {
428 // we try to reduce the ceiling bitrate by making rough correspondances of bitrates 444 // we try to reduce the ceiling bitrate by making rough matches of bitrates
429 // of course this is far from perfect, but it might save some space in the end 445 // of course this is far from perfect, but it might save some space in the end
446 localCommand = localCommand.audioCodec('aac')
447
430 const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] 448 const audioCodecName = parsedAudio.audioStream[ 'codec_name' ]
431 let bitrate: number
432 if (audio.bitrate[ audioCodecName ]) {
433 localCommand = localCommand.audioCodec('aac')
434 449
435 bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) 450 if (audio.bitrate[ audioCodecName ]) {
451 const bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ])
436 if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) 452 if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate)
437 } 453 }
438 } 454 }
439 455
440 // Constrained Encoding (VBV) 456 if (fps) {
441 // https://slhck.info/video/2017/03/01/rate-control.html 457 // Constrained Encoding (VBV)
442 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate 458 // https://slhck.info/video/2017/03/01/rate-control.html
443 const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) 459 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
444 localCommand = localCommand.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`]) 460 const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
445 461 localCommand = localCommand.outputOptions([ `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}` ])
446 // Keyframe interval of 2 seconds for faster seeking and resolution switching. 462
447 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html 463 // Keyframe interval of 2 seconds for faster seeking and resolution switching.
448 // https://superuser.com/a/908325 464 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
449 localCommand = localCommand.outputOption(`-g ${ fps * 2 }`) 465 // https://superuser.com/a/908325
466 localCommand = localCommand.outputOption(`-g ${fps * 2}`)
467 }
450 468
451 return localCommand 469 return localCommand
452} 470}