diff options
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 43 | ||||
-rw-r--r-- | server/helpers/ffprobe-utils.ts | 21 | ||||
-rw-r--r-- | server/lib/video-transcoding-profiles.ts | 36 | ||||
-rw-r--r-- | server/tests/api/live/live.ts | 9 | ||||
-rw-r--r-- | shared/extra-utils/videos/live.ts | 1 |
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 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { readFile, remove, writeFile } from 'fs-extra' | 2 | import { readFile, remove, writeFile } from 'fs-extra' |
3 | import { dirname, join } from 'path' | 3 | import { dirname, join } from 'path' |
4 | import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants' | 4 | import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS } from '@server/initializers/constants' |
5 | import { VideoResolution } from '../../shared/models/videos' | 5 | import { VideoResolution } from '../../shared/models/videos' |
6 | import { checkFFmpegEncoders } from '../initializers/checker-before-init' | 6 | import { checkFFmpegEncoders } from '../initializers/checker-before-init' |
7 | import { CONFIG } from '../initializers/config' | 7 | import { CONFIG } from '../initializers/config' |
8 | import { getAudioStream, getClosestFramerateStandard, getVideoFileFPS } from './ffprobe-utils' | 8 | import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' |
9 | import { processImage } from './image-utils' | 9 | import { processImage } from './image-utils' |
10 | import { logger } from './logger' | 10 | import { 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 | ||
388 | async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { | 398 | async 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 | ||
250 | function 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 | ||
252 | export { | 272 | export { |
@@ -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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | 1 | import { logger } from '@server/helpers/logger' |
2 | import { getTargetBitrate } from '../../shared/models/videos' | 2 | import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' |
3 | import { AvailableEncoders, buildStreamSuffix, EncoderOptionsBuilder } from '../helpers/ffmpeg-utils' | 3 | import { AvailableEncoders, buildStreamSuffix, EncoderOptionsBuilder } from '../helpers/ffmpeg-utils' |
4 | import { | 4 | import { |
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 | ||
25 | const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ input, resolution, fps }) => { | 25 | const 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 | // --------------------------------------------------------------------------- |
110 | async 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 |