Skip to content

Add support for the libvpx-vp9 encoder

Noah Davis requested to merge work/ndavis/vp9 into master

When it comes to capturing windows being moved around, the smoothness of libvpx-vp9 is roughly the same as libvpx (VP8) and x264. Moving windows around looks choppy, but recording a video that is playing on the screen is perfectly fine. The image is sharp and the compression seems to be noticeably better than the libvpx encoder. My tests weren't rigorous, but a libvpx-vp9 video may take up half as much space as a libvpx video of the same content using the same settings (except for VP9-only settings). The actual difference depends on the content of the video. The one thing that's noticeably worse than libvpx is the RAM consumption. libvpx might make Spectacle consume another ~150MB of RAM, but libvpx-vp9 will make Spectacle consume ~350MB more than the normal amount. That could be a problem for low end devices. libvpx-vp9 still takes up less RAM than x264 with which Spectacle uses ~500MB RAM more than normal.

If you want to do the test I did with VP8 vs VP9, here's the modified VP8 encoder file I used: libvpxencoder.cpp

Click to see the diff for libvpxencoder.cpp
--- src/libvpxencoder.cpp
+++ src/libvpxencoder.cpp
@@ -7,6 +7,7 @@
 */

 #include "libvpxencoder_p.h"
+#include "pipewireproduce_p.h"

 #include <QSize>
 #include <QThread>
@@ -20,6 +21,12 @@ extern "C" {

 #include "logging_record.h"

+template<typename T>
+inline const char *numberCStr(T number)
+{
+    return QByteArray::number(number).constData();
+}
+
 LibVpxEncoder::LibVpxEncoder(PipeWireProduce *produce)
     : SoftwareEncoder(produce)
 {
@@ -40,35 +47,51 @@ bool LibVpxEncoder::initialize(const QSize &size)
         qCWarning(PIPEWIRERECORD_LOGGING) << "Could not allocate video codec context";
         return false;
     }
-    m_avCodecContext->bit_rate = size.width() * size.height() * 2;

     Q_ASSERT(!size.isEmpty());
     m_avCodecContext->width = size.width();
     m_avCodecContext->height = size.height();
-    m_avCodecContext->max_b_frames = 0;
-    m_avCodecContext->gop_size = 100;
     m_avCodecContext->pix_fmt = AV_PIX_FMT_YUV420P;
     m_avCodecContext->time_base = AVRational{1, 1000};
-    m_avCodecContext->global_quality = 35;
-
-    if (m_quality) {
-        m_avCodecContext->global_quality = percentageToAbsoluteQuality(m_quality);
-    } else {
-        m_avCodecContext->global_quality = 35;
-    }

     AVDictionary *options = nullptr;
-    av_dict_set_int(&options, "threads", qMin(16, QThread::idealThreadCount()), 0);
-    av_dict_set(&options, "preset", "veryfast", 0);
+
+    // We're probably capturing a screen
     av_dict_set(&options, "tune-content", "screen", 0);
+
+    const auto area = size.width() * size.height();
+    // m_avCodecContext->framerate is not set, so we use m_produce->maxFramerate() instead.
+    const auto maxFramerate = m_produce->maxFramerate();
+    const auto fps = qreal(maxFramerate.numerator) / std::max(quint32(1), maxFramerate.denominator);
+
+    m_avCodecContext->gop_size = fps * 2;
+
+    // 30FPS gets 1x bitrate, 60FPS gets 2x bitrate, etc.
+    const qreal fpsFactor = std::max(fps / 30, 1.0);
+    m_avCodecContext->bit_rate = std::round(area * fpsFactor);
+    m_avCodecContext->rc_min_rate = std::round(area * fpsFactor / 2);
+    m_avCodecContext->rc_max_rate = std::round(area * fpsFactor * 1.5);
+
+    m_avCodecContext->rc_buffer_size = m_avCodecContext->bit_rate;
+
+    // Lower crf is higher quality. Max 0, min 63. libvpx-vp9 doesn't use global_quality.
+    int crf = 31;
+    if (m_quality) {
+        crf = percentageToAbsoluteQuality(m_quality);
+    }
+    av_dict_set(&options, "crf", numberCStr(crf), 0);
+    m_avCodecContext->qmin = std::clamp(crf / 2, 0, crf);
+    m_avCodecContext->qmax = std::clamp(qRound(crf * 1.5), crf, 63);
+
+    // 0-4 are for Video-On-Demand with the good or best deadline.
+    // Don't use best, it's not worth it.
+    // 5-8 are for streaming with the realtime deadline.
+    // Lower is higher quality.
+    int cpuUsed = 5 + std::max(1, int(3 - std::round(m_quality.value_or(50) / 100.0 * 3)));
+    av_dict_set(&options, "cpu-used", numberCStr(cpuUsed), 0);
     av_dict_set(&options, "deadline", "realtime", 0);
-    // In theory a lower number should be faster, but the opposite seems to be true
-    // av_dict_set(&options, "quality", "40", 0);
-    // Disable motion estimation, not great while dragging windows but speeds up encoding by an order of magnitude
-    av_dict_set(&options, "flags", "+mv4", 0);
-    // Disable in-loop filtering
-    av_dict_set(&options, "-flags", "+loop", 0);
-    av_dict_set(&options, "crf", "45", 0);
+
+    m_avCodecContext->thread_count = QThread::idealThreadCount();

     if (int result = avcodec_open2(m_avCodecContext, codec, &options); result < 0) {
         qCWarning(PIPEWIRERECORD_LOGGING) << "Could not open codec" << av_err2str(result);
Edited by Noah Davis

Merge request reports