aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/helpers/ffmpeg-utils.ts43
-rw-r--r--server/helpers/ffprobe-utils.ts21
-rw-r--r--server/lib/video-transcoding-profiles.ts36
-rw-r--r--server/tests/api/live/live.ts9
-rw-r--r--shared/extra-utils/videos/live.ts1
5 files changed, 68 insertions, 42 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 9755dd67c..3cc062b8c 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,11 +1,11 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { readFile, remove, writeFile } from 'fs-extra' 2import { readFile, remove, writeFile } from 'fs-extra'
3import { dirname, join } from 'path' 3import { dirname, join } from 'path'
4import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants' 4import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS } from '@server/initializers/constants'
5import { VideoResolution } from '../../shared/models/videos' 5import { VideoResolution } from '../../shared/models/videos'
6import { checkFFmpegEncoders } from '../initializers/checker-before-init' 6import { checkFFmpegEncoders } from '../initializers/checker-before-init'
7import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
8import { getAudioStream, getClosestFramerateStandard, getVideoFileFPS } from './ffprobe-utils' 8import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils'
9import { processImage } from './image-utils' 9import { processImage } from './image-utils'
10import { logger } from './logger' 10import { logger } from './logger'
11 11
@@ -223,7 +223,17 @@ async function getLiveTranscodingCommand (options: {
223 223
224 for (let i = 0; i < resolutions.length; i++) { 224 for (let i = 0; i < resolutions.length; i++) {
225 const resolution = resolutions[i] 225 const resolution = resolutions[i]
226 const baseEncoderBuilderParams = { input, availableEncoders, profile, fps, resolution, streamNum: i, videoType: 'live' as 'live' } 226 const resolutionFPS = computeFPS(fps, resolution)
227
228 const baseEncoderBuilderParams = {
229 input,
230 availableEncoders,
231 profile,
232 fps: resolutionFPS,
233 resolution,
234 streamNum: i,
235 videoType: 'live' as 'live'
236 }
227 237
228 { 238 {
229 const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'VIDEO' })) 239 const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'VIDEO' }))
@@ -233,7 +243,7 @@ async function getLiveTranscodingCommand (options: {
233 243
234 command.outputOption(`-map [vout${resolution}]`) 244 command.outputOption(`-map [vout${resolution}]`)
235 245
236 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps, streamNum: i }) 246 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
237 247
238 logger.debug('Apply ffmpeg live video params from %s.', builderResult.encoder, builderResult) 248 logger.debug('Apply ffmpeg live video params from %s.', builderResult.encoder, builderResult)
239 249
@@ -249,7 +259,7 @@ async function getLiveTranscodingCommand (options: {
249 259
250 command.outputOption('-map a:0') 260 command.outputOption('-map a:0')
251 261
252 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps, streamNum: i }) 262 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
253 263
254 logger.debug('Apply ffmpeg live audio params from %s.', builderResult.encoder, builderResult) 264 logger.debug('Apply ffmpeg live audio params from %s.', builderResult.encoder, builderResult)
255 265
@@ -387,15 +397,7 @@ function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string
387 397
388async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { 398async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
389 let fps = await getVideoFileFPS(options.inputPath) 399 let fps = await getVideoFileFPS(options.inputPath)
390 if ( 400 fps = computeFPS(fps, options.resolution)
391 // On small/medium resolutions, limit FPS
392 options.resolution !== undefined &&
393 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
394 fps > VIDEO_TRANSCODING_FPS.AVERAGE
395 ) {
396 // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
397 fps = getClosestFramerateStandard(fps, 'STANDARD')
398 }
399 401
400 command = await presetVideo(command, options.inputPath, options, fps) 402 command = await presetVideo(command, options.inputPath, options, fps)
401 403
@@ -408,12 +410,6 @@ async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: Tran
408 command = command.size(size) 410 command = command.size(size)
409 } 411 }
410 412
411 // Hard FPS limits
412 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
413 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
414
415 command = command.withFPS(fps)
416
417 return command 413 return command
418} 414}
419 415
@@ -422,13 +418,6 @@ async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: M
422 418
423 command = await presetVideo(command, options.audioPath, options) 419 command = await presetVideo(command, options.audioPath, options)
424 420
425 /*
426 MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html
427 Our target situation is closer to a livestream than a stream,
428 since we want to reduce as much a possible the encoding burden,
429 although not to the point of a livestream where there is a hard
430 constraint on the frames per second to be encoded.
431 */
432 command.outputOption('-preset:v veryfast') 421 command.outputOption('-preset:v veryfast')
433 422
434 command = command.input(options.audioPath) 423 command = command.input(options.audioPath)
diff --git a/server/helpers/ffprobe-utils.ts b/server/helpers/ffprobe-utils.ts
index 16b295bbd..1cf397767 100644
--- a/server/helpers/ffprobe-utils.ts
+++ b/server/helpers/ffprobe-utils.ts
@@ -247,6 +247,26 @@ function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDA
247 .sort((a, b) => fps % a - fps % b)[0] 247 .sort((a, b) => fps % a - fps % b)[0]
248} 248}
249 249
250function computeFPS (fpsArg: number, resolution: VideoResolution) {
251 let fps = fpsArg
252
253 if (
254 // On small/medium resolutions, limit FPS
255 resolution !== undefined &&
256 resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
257 fps > VIDEO_TRANSCODING_FPS.AVERAGE
258 ) {
259 // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
260 fps = getClosestFramerateStandard(fps, 'STANDARD')
261 }
262
263 // Hard FPS limits
264 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
265 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
266
267 return fps
268}
269
250// --------------------------------------------------------------------------- 270// ---------------------------------------------------------------------------
251 271
252export { 272export {
@@ -259,6 +279,7 @@ export {
259 getVideoStreamFromFile, 279 getVideoStreamFromFile,
260 getDurationFromVideoFile, 280 getDurationFromVideoFile,
261 getAudioStream, 281 getAudioStream,
282 computeFPS,
262 getVideoFileFPS, 283 getVideoFileFPS,
263 ffprobePromise, 284 ffprobePromise,
264 getClosestFramerateStandard, 285 getClosestFramerateStandard,
diff --git a/server/lib/video-transcoding-profiles.ts b/server/lib/video-transcoding-profiles.ts
index 03c26f236..3bf83d6a8 100644
--- a/server/lib/video-transcoding-profiles.ts
+++ b/server/lib/video-transcoding-profiles.ts
@@ -1,5 +1,5 @@
1import { logger } from '@server/helpers/logger' 1import { logger } from '@server/helpers/logger'
2import { getTargetBitrate } from '../../shared/models/videos' 2import { getTargetBitrate, VideoResolution } from '../../shared/models/videos'
3import { AvailableEncoders, buildStreamSuffix, EncoderOptionsBuilder } from '../helpers/ffmpeg-utils' 3import { AvailableEncoders, buildStreamSuffix, EncoderOptionsBuilder } from '../helpers/ffmpeg-utils'
4import { 4import {
5 canDoQuickAudioTranscode, 5 canDoQuickAudioTranscode,
@@ -23,21 +23,12 @@ import { VIDEO_TRANSCODING_FPS } from '../initializers/constants'
23// * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate 23// * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
24 24
25const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ input, resolution, fps }) => { 25const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ input, resolution, fps }) => {
26 let targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) 26 const targetBitrate = await buildTargetBitrate({ input, resolution, fps })
27 27 if (!targetBitrate) return { outputOptions: [ ] }
28 const probe = await ffprobePromise(input)
29
30 const videoStream = await getVideoStreamFromFile(input, probe)
31 if (!videoStream) {
32 return { outputOptions: [ ] }
33 }
34
35 // Don't transcode to an higher bitrate than the original file
36 const fileBitrate = await getVideoFileBitrate(input, probe)
37 targetBitrate = Math.min(targetBitrate, fileBitrate)
38 28
39 return { 29 return {
40 outputOptions: [ 30 outputOptions: [
31 `-r ${fps}`,
41 `-maxrate ${targetBitrate}`, 32 `-maxrate ${targetBitrate}`,
42 `-bufsize ${targetBitrate * 2}` 33 `-bufsize ${targetBitrate * 2}`
43 ] 34 ]
@@ -49,6 +40,7 @@ const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = async ({ resolution
49 40
50 return { 41 return {
51 outputOptions: [ 42 outputOptions: [
43 `${buildStreamSuffix('-r:v', streamNum)} ${fps}`,
52 `${buildStreamSuffix('-b:v', streamNum)} ${targetBitrate}`, 44 `${buildStreamSuffix('-b:v', streamNum)} ${targetBitrate}`,
53 `-maxrate ${targetBitrate}`, 45 `-maxrate ${targetBitrate}`,
54 `-bufsize ${targetBitrate * 2}` 46 `-bufsize ${targetBitrate * 2}`
@@ -115,3 +107,21 @@ export {
115} 107}
116 108
117// --------------------------------------------------------------------------- 109// ---------------------------------------------------------------------------
110async function buildTargetBitrate (options: {
111 input: string
112 resolution: VideoResolution
113 fps: number
114
115}) {
116 const { input, resolution, fps } = options
117 const probe = await ffprobePromise(input)
118
119 const videoStream = await getVideoStreamFromFile(input, probe)
120 if (!videoStream) return undefined
121
122 const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
123
124 // Don't transcode to an higher bitrate than the original file
125 const fileBitrate = await getVideoFileBitrate(input, probe)
126 return Math.min(targetBitrate, fileBitrate)
127}
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts
index 0786db554..4f84882ff 100644
--- a/server/tests/api/live/live.ts
+++ b/server/tests/api/live/live.ts
@@ -416,7 +416,7 @@ describe('Test live', function () {
416 await waitJobs(servers) 416 await waitJobs(servers)
417 417
418 const bitrateLimits = { 418 const bitrateLimits = {
419 720: 3000 * 1000, 419 720: 4000 * 1000, // 60FPS
420 360: 1100 * 1000, 420 360: 1100 * 1000,
421 240: 600 * 1000 421 240: 600 * 1000
422 } 422 }
@@ -436,9 +436,14 @@ describe('Test live', function () {
436 const file = hlsPlaylist.files.find(f => f.resolution.id === resolution) 436 const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
437 437
438 expect(file).to.exist 438 expect(file).to.exist
439 expect(file.fps).to.be.approximately(30, 5)
440 expect(file.size).to.be.greaterThan(1) 439 expect(file.size).to.be.greaterThan(1)
441 440
441 if (resolution >= 720) {
442 expect(file.fps).to.be.approximately(60, 2)
443 } else {
444 expect(file.fps).to.be.approximately(30, 2)
445 }
446
442 const filename = `${video.uuid}-${resolution}-fragmented.mp4` 447 const filename = `${video.uuid}-${resolution}-fragmented.mp4`
443 const segmentPath = buildServerDirectory(servers[0], join('streaming-playlists', 'hls', video.uuid, filename)) 448 const segmentPath = buildServerDirectory(servers[0], join('streaming-playlists', 'hls', video.uuid, filename))
444 449
diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts
index c8acb90da..266baaed3 100644
--- a/shared/extra-utils/videos/live.ts
+++ b/shared/extra-utils/videos/live.ts
@@ -69,6 +69,7 @@ function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = '
69 command.outputOption('-c:v libx264') 69 command.outputOption('-c:v libx264')
70 command.outputOption('-g 50') 70 command.outputOption('-g 50')
71 command.outputOption('-keyint_min 2') 71 command.outputOption('-keyint_min 2')
72 command.outputOption('-r 60')
72 command.outputOption('-f flv') 73 command.outputOption('-f flv')
73 74
74 const rtmpUrl = rtmpBaseUrl + '/' + streamKey 75 const rtmpUrl = rtmpBaseUrl + '/' + streamKey