summaryrefslogtreecommitdiffstats
path: root/pica-client/videoscaler.h
diff options
context:
space:
mode:
authorAnton <anton@picapica.im>2026-10-03 20:33:04 +0400
committerAnton <anton@picapica.im>2026-10-03 20:33:04 +0400
commit835b124bd871afecd88c1e5e9105e3af4d4c5b27 (patch)
tree42ee00cb6f37472fe36c802530cdc9a288ba87bd /pica-client/videoscaler.h
parent76225a538e1e960eda9e31a34846f4bc62adef87 (diff)
downloadpica-pica-835b124bd871afecd88c1e5e9105e3af4d4c5b27.tar.gz
pica-pica-835b124bd871afecd88c1e5e9105e3af4d4c5b27.zip
pica-client: callwindow layout, video resize
Diffstat (limited to 'pica-client/videoscaler.h')
-rw-r--r--pica-client/videoscaler.h128
1 files changed, 128 insertions, 0 deletions
diff --git a/pica-client/videoscaler.h b/pica-client/videoscaler.h
new file mode 100644
index 0000000..4d05d16
--- /dev/null
+++ b/pica-client/videoscaler.h
@@ -0,0 +1,128 @@
+/*
+ (c) Copyright 2012 - 2026 Anton Sviridenko
+ https://picapica.im
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, version 3.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+#ifndef VIDEOSCALER_H
+#define VIDEOSCALER_H
+
+#include <QImage>
+#include <QSize>
+
+struct SwsContext;
+
+// Decides what size a video frame should be drawn at, and resizes one when
+// something else has not already done it.
+//
+// Frames arrive at whatever resolution the sender negotiated, which has nothing
+// to do with the size of the window they end up in, so each one has to be
+// resized before it is drawn - at the frame rate of the call, for as long as it
+// lasts. Where that resizing happens turned out to matter a great deal, and
+// tests/test_videoscaler is the measurement it was settled by:
+//
+// - Resizing packed RGB in the window, which is what used to happen, is the
+// worst of the options. libswscale is especially bad at it - it has no
+// scaler for packed formats, so it unpacks to planar YUV, scales, and packs
+// the result back, losing to QImage::scaled() by four to five times.
+//
+// - Resizing inside the colour conversion the decode loop already runs is
+// between 1.1 and 4.6 times faster than the whole two pass pipeline, best
+// when the picture is being shrunk, since nothing is converted only to be
+// thrown away. libswscale is good at this one: the source is planar YUV,
+// which is what it wants, and the resize rides along inside a pass over
+// the picture that had to happen anyway.
+//
+// - Handing the result over without copying it saves another pass again. See
+// VideoDevice::Play(), which allocates each frame's QImage rather than
+// reusing one buffer, so the queued signal that carries it to the GUI
+// thread only has a reference count to bump.
+//
+// So VideoDevice::Play() does the resizing, and this class is left with two
+// jobs. fitted() is the single authority on what size to aim for, shared so
+// that the decoder and the window cannot disagree about it. scale() is the
+// window's fallback for the frames that arrive at the wrong size anyway -
+// during a resize, there are always a few already in flight - and is a no-op
+// when the size is already right, which is the steady state.
+//
+// The libswscale backend is kept for the benchmark to compare against, and to
+// keep the conclusion above honest if any of these libraries change. It only
+// understands a handful of QImage formats; anything else, and any failure to
+// build a scaler, falls back to Qt rather than losing the picture.
+class VideoScaler
+{
+public:
+ // Not named after Qt itself - a value called Qt would shadow the
+ // namespace of the same name inside this class.
+ enum Backend
+ {
+ QtScaled, // QImage::scaled(), the default - measurably the faster one here
+ Swscale // libswscale, for comparison; falls back to the above when it cannot help
+ };
+
+ explicit VideoScaler(Backend backend = QtScaled);
+ ~VideoScaler();
+
+ // The size `frame` becomes when fitted into `area` without cropping or
+ // changing its aspect ratio - never larger than `area`, and never
+ // smaller than 2x1 so long as both arguments are valid. An empty size
+ // if either argument is empty.
+ //
+ // The width is always even, which is worth a factor of four and a half
+ // downstream; the reason is with the implementation. Every part of the
+ // call that needs to know the display size of a frame goes through here,
+ // so that they all arrive at the same answer.
+ static QSize fitted(QSize frame, QSize area);
+
+ // Returns `frame` resized to fitted(frame.size(), area), ready to be
+ // handed to QPixmap::fromImage(). A null image means nothing could be
+ // produced and the previous picture should be left alone.
+ //
+ // The returned image may reference a buffer this object reuses, so it
+ // is only valid until the next call to scale() - copy it if it needs to
+ // outlive that.
+ QImage scale(const QImage &frame, QSize area);
+
+ // Which implementation the last scale() actually used. Not the same as
+ // the backend asked for: Swscale falls back to QtScaled when it meets a
+ // frame format it has no mapping for.
+ Backend lastBackendUsed() const { return m_lastUsed; }
+
+private:
+ QImage scaleWithSwscale(const QImage &frame, QSize target);
+ static QImage scaleWithQt(const QImage &frame, QSize target);
+
+ Backend m_backend;
+ Backend m_lastUsed;
+
+ // Building a scaler means computing filter tables, so the context is
+ // kept across frames and only rebuilt when the geometry or the format
+ // it was built for changes - in practice on the first frame and on a
+ // window resize.
+ SwsContext *m_sws;
+ int m_srcWidth;
+ int m_srcHeight;
+ int m_srcFormat; // AVPixelFormat, kept as int to keep ffmpeg out of this header
+ int m_dstWidth;
+ int m_dstHeight;
+
+ // Destination of the libswscale path, reused frame to frame. Qt images
+ // are copy on write, and writing through the raw pointer libswscale is
+ // given would go behind that, so scale() makes sure this is not shared
+ // before handing the pointer over.
+ QImage m_dst;
+
+ bool m_loggedFailure;
+};
+
+#endif