fix(video): reduce buffering in ffmpeg, avoid overbooking cpu (#8786)

This is an attempt to improve video performance when encoding
does not keep up with frames. This situation can be reproduced
by running multiple encoders at the same time.

Added `utils/video_stress.js` to manually reproduce this issue.

Observing ffmpeg logs, it does not do any encoding initially and
instead does "input analysis / probing" that detects fps and other
parameters. By the time it starts encoding (launches vpx and creates
the video file), we already have many frames in the buffer.
Reducing probing helps:
`-avioflags direct -fpsprobesize 0 -probesize 32 -analyzeduration 0`

Another issue observed is questionable default `-threads` value.
We compile without threads support, so logs say "using emulated threads".
For some reason, setting explicit `-threads 1` (or any other value)
makes it better when cpu is loaded.
This commit is contained in:
Dmitry Gozman 2021-09-09 12:41:06 -07:00 committed by GitHub
parent 1ad6c8af6f
commit eca82eee4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 40 additions and 6 deletions

View File

@ -63,31 +63,41 @@ export class VideoRecorder {
// $ ./third_party/ffmpeg/ffmpeg-mac -h encoder=vp8
// 3. A bit more about passing vp8 options to ffmpeg.
// https://trac.ffmpeg.org/wiki/Encode/VP8
// 4. Tuning for VP9:
// https://developers.google.com/media/vp9/live-encoding
//
// How to stress-test video recording (runs 10 recorders in parallel to book all cpus available):
// $ node ./utils/video_stress.js
//
// We use the following vp8 options:
// "-qmin 0 -qmax 50" - quality variation from 0 to 50.
// Suggested here: https://trac.ffmpeg.org/wiki/Encode/VP8
// "-crf 8" - constant quality mode, 4-63, lower means better quality.
// "-deadline realtime" - do not use too much cpu to keep up with incoming frames.
// "-deadline realtime -speed 8" - do not use too much cpu to keep up with incoming frames.
// "-b:v 1M" - video bitrate. Default value is too low for vp8
// Suggested here: https://trac.ffmpeg.org/wiki/Encode/VP8
// Note that we can switch to "-qmin 20 -qmax 50 -crf 30" for smaller video size but worse quality.
//
// We use "pad" and "crop" video filters (-vf option) to resize incoming frames
// that might be of the different size to the desired video size.
// https://ffmpeg.org/ffmpeg-filters.html#pad-1
// https://ffmpeg.org/ffmpeg-filters.html#crop
//
// We use "image2pipe" mode to pipe frames and get a single video.
// "-f image2pipe -c:v mjpeg -i -" forces input to be read from standard input, and forces
// mjpeg input image format.
// https://trac.ffmpeg.org/wiki/Slideshow
// We use "image2pipe" mode to pipe frames and get a single video - https://trac.ffmpeg.org/wiki/Slideshow
// "-f image2pipe -c:v mjpeg -i -" forces input to be read from standard input, and forces
// mjpeg input image format.
// "-avioflags direct" reduces general buffering.
// "-fpsprobesize 0 -probesize 32 -analyzeduration 0" reduces initial buffering
// while analyzing input fps and other stats.
//
// "-y" means overwrite output.
// "-an" means no audio.
// "-threads 1" means using one thread. This drastically reduces stalling when
// cpu is overbooked. By default vp8 tries to use all available threads?
const w = options.width;
const h = options.height;
const args = `-loglevel error -f image2pipe -c:v mjpeg -i - -y -an -r ${fps} -c:v vp8 -qmin 0 -qmax 50 -crf 8 -deadline realtime -b:v 1M -vf pad=${w}:${h}:0:0:gray,crop=${w}:${h}:0:0`.split(' ');
const args = `-loglevel error -f image2pipe -avioflags direct -fpsprobesize 0 -probesize 32 -analyzeduration 0 -c:v mjpeg -i - -y -an -r ${fps} -c:v vp8 -qmin 0 -qmax 50 -crf 8 -deadline realtime -speed 8 -b:v 1M -threads 1 -vf pad=${w}:${h}:0:0:gray,crop=${w}:${h}:0:0`.split(' ');
args.push(options.outputFile);
const progress = this._progress;

24
utils/video_stress.js Normal file
View File

@ -0,0 +1,24 @@
const { chromium } = require('..');
const videoDir = require('path').join(__dirname, '..', '.tmp');
async function go(browser) {
console.log(`Creating context`);
const context = await browser.newContext({ recordVideo: { dir: videoDir } });
const page = await context.newPage();
await page.goto('https://webkit.org/blog-files/3d-transforms/poster-circle.html');
await page.waitForTimeout(10000);
const time = Date.now();
await context.close();
console.log(`Closing context for ${Date.now() - time}ms`);
const video = await page.video();
console.log(`Recorded video at ${await video.path()}`);
}
(async () => {
const browser = await chromium.launch({ headless: true });
const promises = [];
for (let i = 0; i < 10; i++)
promises.push(go(browser));
await Promise.all(promises);
await browser.close();
})();