summaryrefslogtreecommitdiffstats
path: root/pica-client/callwindow.cpp
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/callwindow.cpp
parent76225a538e1e960eda9e31a34846f4bc62adef87 (diff)
downloadpica-pica-835b124bd871afecd88c1e5e9105e3af4d4c5b27.tar.gz
pica-pica-835b124bd871afecd88c1e5e9105e3af4d4c5b27.zip
pica-client: callwindow layout, video resize
Diffstat (limited to 'pica-client/callwindow.cpp')
-rw-r--r--pica-client/callwindow.cpp151
1 files changed, 137 insertions, 14 deletions
diff --git a/pica-client/callwindow.cpp b/pica-client/callwindow.cpp
index 81959e3..0b7d309 100644
--- a/pica-client/callwindow.cpp
+++ b/pica-client/callwindow.cpp
@@ -19,10 +19,19 @@
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QCloseEvent>
+#include <QEvent>
+#include <QGuiApplication>
+#include <QScreen>
#include <QDebug>
+// Smallest the video area is allowed to become. Low enough that the window
+// stays freely resizable - the video area is the part that gives, so a floor
+// here is a floor under the whole window - while still leaving something
+// recognizable on screen.
+static const QSize kMinVideoSize(160, 120);
+
CallWindow::CallWindow(QByteArray peer_id, bool incoming)
- : m_peer_id(peer_id), is_incoming(incoming)
+ : m_peer_id(peer_id), is_incoming(incoming), videoAreaOpened(false)
{
setAttribute(Qt::WA_DeleteOnClose);
@@ -40,30 +49,55 @@ CallWindow::CallWindow(QByteArray peer_id, bool incoming)
lbTransport->hide();
lbVideo = new QLabel(this);
lbVideo->setAlignment(Qt::AlignCenter);
- lbVideo->setMinimumSize(320, 240);
+ // A label holding a pixmap reports that pixmap's size as both its
+ // preferred and its minimum size, which would make the video area's size
+ // depend on the picture currently in it: the window could not be made
+ // smaller than the last frame drawn, and the layout would be recomputed on
+ // every frame. Ignored breaks that - the label is given whatever space the
+ // stretch factors below say and never asks for any.
+ lbVideo->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
+ // Small enough to let the window be resized freely; the size the video
+ // area actually opens at is set once by openVideoArea().
+ lbVideo->setMinimumSize(kMinVideoSize);
lbVideo->hide();
#ifdef HAVE_VAAPI
videoGpu = VaapiRenderWidget::create(this);
- videoGpu->widget()->setMinimumSize(320, 240);
+ videoGpu->widget()->setMinimumSize(kMinVideoSize);
videoGpu->widget()->hide();
connect(videoGpu->widget(), SIGNAL(renderingFailed(QString)), this, SLOT(gpu_rendering_failed(QString)));
#endif
callTimer = new QTimer(this);
callElapsedSeconds = 0;
- lv->addWidget(lbTimer);
- lv->addWidget(lbTransport);
- lv->addWidget(lbVideo);
+ // Only the video gets a stretch factor, so it takes all the height the
+ // rows around it do not need. Left at the default of zero, every item
+ // whose size policy allows it to grow gets an equal share of the spare
+ // height instead - which for two QLabels and the video meant the video
+ // got about a third of it and the timer and the transport line sat in
+ // tall empty bands.
+ //
+ // Only one of the two video widgets is ever visible and a hidden widget is
+ // given no space, so both can be stretched.
+ lv->addWidget(lbTimer, 0);
+ lv->addWidget(lbTransport, 0);
+ lv->addWidget(lbVideo, 1);
#ifdef HAVE_VAAPI
- lv->addWidget(videoGpu->widget());
+ lv->addWidget(videoGpu->widget(), 1);
#endif
- lh->addWidget(pbAccept, Qt::AlignLeft);
- lh->addWidget(pbCall, Qt::AlignLeft);
- lh->addWidget(pbHang, Qt::AlignRight);
- lv->addLayout(lh);
+ // Note that the second argument of addWidget() is a stretch factor and not
+ // an alignment - passing Qt::AlignLeft here, as this did, compiles, since
+ // Qt::Alignment converts to int, and quietly means a stretch of 1. A
+ // spacer does what the alignment was meant to.
+ lh->addWidget(pbAccept);
+ lh->addWidget(pbCall);
+ lh->addStretch(1);
+ lh->addWidget(pbHang);
+ lv->addLayout(lh, 0);
setLayout(lv);
+ lbVideo->installEventFilter(this);
+
connect(pbCall, SIGNAL(clicked()), this, SLOT(call()));
connect(pbHang, SIGNAL(clicked()), this, SLOT(hang()));
connect(pbAccept, SIGNAL(clicked()), this, SLOT(accept()));
@@ -112,12 +146,78 @@ void CallWindow::showRemoteFrame(QImage frame)
if (frame.isNull())
return;
+ lastRemoteFrame = frame;
+
if (lbVideo->isHidden())
+ {
+ openVideoArea(frame.size());
lbVideo->show();
+ reportVideoArea();
+ }
+
+ updateRemoteFrame();
+}
+
+void CallWindow::updateRemoteFrame()
+{
+ if (lastRemoteFrame.isNull() || lbVideo->isHidden())
+ return;
+
+ // Normally a no-op: the decoder was told the size of this area and the
+ // frame already arrived at it, and scale() returns the frame untouched
+ // when there is nothing to do. It earns its place across a resize, when
+ // the frames already decoded at the old size have still to be drawn at
+ // the new one.
+ QImage scaled = videoScaler.scale(lastRemoteFrame, lbVideo->size());
+
+ if (scaled.isNull())
+ return;
+
+ lbVideo->setPixmap(QPixmap::fromImage(scaled));
+}
+
+void CallWindow::reportVideoArea()
+{
+ // While the label is hidden there is nothing to size frames for, and its
+ // geometry is meaningless anyway. isHidden() rather than isVisible(): what
+ // matters is whether this label is the renderer in use, not whether the
+ // window happens to be on screen right now. When the GPU widget has the
+ // video area it scales frames itself, on the GPU, and wants them at their
+ // own resolution - see showRemoteHwFrame().
+ if (lbVideo->isHidden())
+ return;
- lbVideo->setPixmap(QPixmap::fromImage(frame).scaled(lbVideo->size(),
- Qt::KeepAspectRatio,
- Qt::SmoothTransformation));
+ emit video_area_changed(lbVideo->size());
+}
+
+void CallWindow::openVideoArea(QSize frame)
+{
+ if (videoAreaOpened || frame.isEmpty())
+ return;
+
+ videoAreaOpened = true;
+
+ // The window has been sized for a call with no video in it, so the video
+ // area is about to appear into whatever the layout can spare - which is
+ // its minimum. Grow the window by what the video needs on top of that.
+ //
+ // Only the first frame does this. After it the window's size is the user's
+ // business, and a later frame must not undo a deliberate resize.
+ QSize wanted = frame;
+ QScreen *screen = QGuiApplication::primaryScreen();
+
+ if (screen)
+ {
+ // Leave the rows above and below the video the height they have
+ // now - a peer sending 1080p to a 1080 high screen should not open
+ // a window taller than the desktop.
+ QSize available = screen->availableGeometry().size() - QSize(0, height());
+
+ wanted = VideoScaler::fitted(frame, available.expandedTo(kMinVideoSize));
+ }
+
+ resize(qMax(width(), wanted.width()),
+ height() + qMax(0, wanted.height() - lbVideo->height()));
}
#ifdef HAVE_VAAPI
@@ -138,8 +238,14 @@ void CallWindow::showRemoteHwFrame(AVFramePtr frame)
// show them - the GL widget takes over the video area instead.
if (videoGpu->widget()->isHidden())
{
+ openVideoArea(QSize(frame->width, frame->height));
lbVideo->hide();
videoGpu->widget()->show();
+
+ // The widget scales the surface itself, on the GPU, so the decoder
+ // has no reason to produce frames at any particular size - and it
+ // is not producing these ones at all.
+ emit video_area_changed(QSize());
}
videoGpu->setFrame(frame);
@@ -155,6 +261,12 @@ void CallWindow::gpu_rendering_failed(QString message)
videoGpu->widget()->hide();
lbVideo->show();
+ // Frames are about to start arriving in system memory, so the decoder
+ // needs the size to produce them at. Sent explicitly rather than left to
+ // the label's resize, which does not fire if showing it did not change its
+ // geometry.
+ reportVideoArea();
+
emit video_rendering_failed();
}
#endif
@@ -198,6 +310,17 @@ void CallWindow::accept()
emit accept_call_pressed();
}
+bool CallWindow::eventFilter(QObject *watched, QEvent *event)
+{
+ if (watched == lbVideo && event->type() == QEvent::Resize)
+ {
+ reportVideoArea();
+ updateRemoteFrame();
+ }
+
+ return QWidget::eventFilter(watched, event);
+}
+
void CallWindow::closeEvent(QCloseEvent *e)
{
emit callwindow_closed(this);