From 474607ba0656026c53108cedf2e4e71abf5c691d Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Tue, 5 Nov 2024 02:01:14 +0800 Subject: [PATCH] cabana: refactor video widget for simplified layout and enhanced rendering (#33909) simplify video widget --- tools/cabana/mainwin.cc | 2 +- tools/cabana/videowidget.cc | 236 ++++++++++++++---------------------- tools/cabana/videowidget.h | 43 ++----- tools/replay/replay.h | 4 +- tools/replay/timeline.cc | 4 +- 5 files changed, 109 insertions(+), 180 deletions(-) diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index ee8f42ea727dac..189f8ac18fb873 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -51,7 +51,7 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW emit static_main_win->updateProgressBar(cur, total, success); }); qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &context, const QString &msg) { - if (type == QtDebugMsg) std::cout << msg.toStdString() << std::endl; + if (type == QtDebugMsg) return; emit static_main_win->showMessage(msg, 2000); }); installMessageHandler([](ReplyMsgType type, const std::string msg) { qInfo() << msg.c_str(); }); diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index 7f9e3d696091de..3dddc0fb646389 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -1,14 +1,12 @@ #include "tools/cabana/videowidget.h" #include -#include #include #include #include #include #include -#include #include #include #include @@ -27,9 +25,7 @@ static const QColor timeline_colors[] = { static Replay *getReplay() { auto stream = qobject_cast(can); - if (!stream) return nullptr; - - return stream->getReplay(); + return stream ? stream->getReplay() : nullptr; } VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { @@ -144,13 +140,9 @@ QWidget *VideoWidget::createCameraWidget() { camera_tab->setAutoHide(true); camera_tab->setExpanding(false); - QStackedLayout *stacked = new QStackedLayout(); - stacked->setStackingMode(QStackedLayout::StackAll); - stacked->addWidget(cam_widget = new StreamCameraView("camerad", VISION_STREAM_ROAD)); + l->addWidget(cam_widget = new StreamCameraView("camerad", VISION_STREAM_ROAD)); cam_widget->setMinimumHeight(MIN_VIDEO_HEIGHT); cam_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); - stacked->addWidget(alert_label = new InfoLabel(this)); - l->addLayout(stacked); l->addWidget(slider = new Slider(w)); slider->setSingleStep(0); @@ -165,16 +157,13 @@ QWidget *VideoWidget::createCameraWidget() { QObject::connect(camera_tab, &QTabBar::currentChanged, [this](int index) { if (index != -1) cam_widget->setStreamType((VisionStreamType)camera_tab->tabData(index).toInt()); }); - QObject::connect(static_cast(can), &ReplayStream::qLogLoaded, slider, &Slider::parseQLog, Qt::QueuedConnection); + QObject::connect(static_cast(can), &ReplayStream::qLogLoaded, cam_widget, &StreamCameraView::parseQLog, Qt::QueuedConnection); + slider->installEventFilter(cam_widget); return w; } void VideoWidget::vipcAvailableStreamsUpdated(std::set streams) { - static const QString stream_names[] = { - [VISION_STREAM_ROAD] = "Road camera", - [VISION_STREAM_WIDE_ROAD] = "Wide road camera", - [VISION_STREAM_DRIVER] = "Driver camera"}; - + static const QString stream_names[] = {"Road camera", "Driver camera", "Wide road camera"}; for (int i = 0; i < streams.size(); ++i) { if (camera_tab->count() <= i) { camera_tab->addTab(QString()); @@ -189,16 +178,9 @@ void VideoWidget::vipcAvailableStreamsUpdated(std::set streams } void VideoWidget::loopPlaybackClicked() { - auto replay = getReplay(); - if (!replay) return; - - if (replay->hasFlag(REPLAY_FLAG_NO_LOOP)) { - replay->removeFlag(REPLAY_FLAG_NO_LOOP); - loop_btn->setIcon("repeat"); - } else { - replay->addFlag(REPLAY_FLAG_NO_LOOP); - loop_btn->setIcon("repeat-1"); - } + bool is_looping = getReplay()->loop(); + getReplay()->setLoop(!is_looping); + loop_btn->setIcon(!is_looping ? "repeat" : "repeat-1"); } void VideoWidget::timeRangeChanged() { @@ -223,7 +205,9 @@ void VideoWidget::updateState() { if (!slider->isSliderDown()) { slider->setCurrentSecond(can->currentSec()); } - alert_label->showAlert(slider->alertInfo(can->currentSec())); + if (camera_tab->count() == 0) { // No streams available + cam_widget->update(); // Manually refresh to show alert events + } time_btn->setText(QString("%1 / %2").arg(formatTime(can->currentSec(), true), formatTime(slider->maximum() / slider->factor))); } else { @@ -239,41 +223,9 @@ void VideoWidget::updatePlayBtnState() { // Slider Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) { - thumbnail_label = new InfoLabel(parent); setMouseTracking(true); } -std::optional Slider::alertInfo(double seconds) { - return getReplay()->findAlertAtTime(seconds); -} - -QPixmap Slider::thumbnail(double seconds) { - auto it = thumbnails.lowerBound(can->toMonoTime(seconds)); - return it != thumbnails.end() ? it.value() : QPixmap(); -} - -void Slider::setTimeRange(double min, double max) { - assert(min < max); - setRange(min * factor, max * factor); -} - -void Slider::parseQLog(std::shared_ptr qlog) { - std::mutex mutex; - QtConcurrent::blockingMap(qlog->events.cbegin(), qlog->events.cend(), [&mutex, this](const Event &e) { - if (e.which == cereal::Event::Which::THUMBNAIL) { - capnp::FlatArrayMessageReader reader(e.data); - auto thumb = reader.getRoot().getThumbnail(); - auto data = thumb.getThumbnail(); - if (QPixmap pm; pm.loadFromData(data.begin(), data.size(), "jpeg")) { - QPixmap scaled = pm.scaledToHeight(MIN_VIDEO_HEIGHT - THUMBNAIL_MARGIN * 2, Qt::SmoothTransformation); - std::lock_guard lk(mutex); - thumbnails[thumb.getTimestampEof()] = scaled; - } - } - }); - update(); -} - void Slider::paintEvent(QPaintEvent *ev) { QPainter p(this); QRect r = rect().adjusted(0, 4, 0, -4); @@ -288,9 +240,8 @@ void Slider::paintEvent(QPaintEvent *ev) { p.fillRect(r, color); }; - auto replay = getReplay(); - if (replay) { - for (const auto &entry: *replay->getTimeline()) { + if (auto replay = getReplay()) { + for (const auto &entry : *replay->getTimeline()) { fillRange(entry.start_time, entry.end_time, timeline_colors[(int)entry.type]); } @@ -319,84 +270,7 @@ void Slider::mousePressEvent(QMouseEvent *e) { } } -void Slider::mouseMoveEvent(QMouseEvent *e) { - int pos = std::clamp(e->pos().x(), 0, width()); - double seconds = (minimum() + pos * ((maximum() - minimum()) / (double)width())) / factor; - QPixmap thumb = thumbnail(seconds); - if (!thumb.isNull()) { - int x = std::clamp(pos - thumb.width() / 2, THUMBNAIL_MARGIN, width() - thumb.width() - THUMBNAIL_MARGIN + 1); - int y = -thumb.height() - THUMBNAIL_MARGIN; - thumbnail_label->showPixmap(mapToParent(QPoint(x, y)), utils::formatSeconds(seconds), thumb, alertInfo(seconds)); - } else { - thumbnail_label->hide(); - } - QSlider::mouseMoveEvent(e); -} - -bool Slider::event(QEvent *event) { - switch (event->type()) { - case QEvent::WindowActivate: - case QEvent::WindowDeactivate: - case QEvent::FocusIn: - case QEvent::FocusOut: - case QEvent::Leave: - thumbnail_label->hide(); - break; - default: - break; - } - return QSlider::event(event); -} - -// InfoLabel - -InfoLabel::InfoLabel(QWidget *parent) : QWidget(parent, Qt::WindowStaysOnTopHint) { - setAttribute(Qt::WA_ShowWithoutActivating); - setAttribute(Qt::WA_TransparentForMouseEvents); - setVisible(false); -} - -void InfoLabel::showPixmap(const QPoint &pt, const QString &sec, const QPixmap &pm, const std::optional &alert) { - second = sec; - pixmap = pm; - alert_info = alert; - setGeometry(QRect(pt, pm.size())); - setVisible(true); - update(); -} - -void InfoLabel::showAlert(const std::optional &alert) { - alert_info = alert; - pixmap = {}; - setVisible(alert_info.has_value()); - update(); -} - -void InfoLabel::paintEvent(QPaintEvent *event) { - QPainter p(this); - p.setPen(QPen(palette().color(QPalette::BrightText), 2)); - if (!pixmap.isNull()) { - p.drawPixmap(0, 0, pixmap); - p.drawRect(rect()); - p.drawText(rect().adjusted(0, 0, 0, -THUMBNAIL_MARGIN), second, Qt::AlignHCenter | Qt::AlignBottom); - } - if (alert_info) { - QColor color = timeline_colors[int(alert_info->type)]; - color.setAlphaF(0.5); - QString text = QString::fromStdString(alert_info->text1); - if (!alert_info->text2.empty()) text += "\n" + QString::fromStdString(alert_info->text2); - - if (!pixmap.isNull()) { - QFont font; - font.setPixelSize(11); - p.setFont(font); - } - QRect text_rect = rect().adjusted(1, 1, -1, -1); - QRect r = p.fontMetrics().boundingRect(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text); - p.fillRect(text_rect.left(), r.top(), text_rect.width(), r.height(), color); - p.drawText(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text); - } -} +// StreamCameraView StreamCameraView::StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent) : CameraWidget(stream_name, stream_type, parent) { @@ -404,15 +278,93 @@ StreamCameraView::StreamCameraView(std::string stream_name, VisionStreamType str fade_animation->setDuration(500); fade_animation->setStartValue(0.2f); fade_animation->setEndValue(0.7f); + fade_animation->setEasingCurve(QEasingCurve::InOutQuad); + connect(fade_animation, &QPropertyAnimation::valueChanged, this, QOverload<>::of(&StreamCameraView::update)); +} + +void StreamCameraView::parseQLog(std::shared_ptr qlog) { + std::mutex mutex; + QtConcurrent::blockingMap(qlog->events.cbegin(), qlog->events.cend(), [this, &mutex](const Event &e) { + if (e.which == cereal::Event::Which::THUMBNAIL) { + capnp::FlatArrayMessageReader reader(e.data); + auto thumb_data = reader.getRoot().getThumbnail(); + auto image_data = thumb_data.getThumbnail(); + if (QPixmap thumb; thumb.loadFromData(image_data.begin(), image_data.size(), "jpeg")) { + QPixmap generated_thumb = generateThumbnail(thumb, can->toSeconds(thumb_data.getTimestampEof())); + std::lock_guard lock(mutex); + thumbnails[thumb_data.getTimestampEof()] = generated_thumb; + } + } + }); + update(); } void StreamCameraView::paintGL() { CameraWidget::paintGL(); + QPainter p(this); + if (auto alert = getReplay()->findAlertAtTime(can->currentSec())) { + drawAlert(p, rect(), *alert); + } + if (thumbnail_pt_) { + drawThumbnail(p); + } if (can->isPaused()) { - QPainter p(this); - p.setPen(QColor(200, 200, 200, static_cast(255 * overlay_opacity))); + p.setPen(QColor(200, 200, 200, static_cast(255 * fade_animation->currentValue().toFloat()))); p.setFont(QFont(font().family(), 16, QFont::Bold)); p.drawText(rect(), Qt::AlignCenter, tr("PAUSED")); } } + +QPixmap StreamCameraView::generateThumbnail(QPixmap thumb, double seconds) { + QPixmap scaled = thumb.scaledToHeight(MIN_VIDEO_HEIGHT - THUMBNAIL_MARGIN * 2, Qt::SmoothTransformation); + QPainter p(&scaled); + p.setPen(QPen(palette().color(QPalette::BrightText), 2)); + p.drawRect(scaled.rect()); + if (auto alert = getReplay()->findAlertAtTime(seconds)) { + p.setFont(QFont(font().family(), 10)); + drawAlert(p, scaled.rect(), *alert); + } + return scaled; +} + +void StreamCameraView::drawThumbnail(QPainter &p) { + int pos = std::clamp(thumbnail_pt_->x(), 0, width()); + auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds())); + double seconds = min_sec + pos * (max_sec - min_sec) / width(); + + auto it = thumbnails.lowerBound(can->toMonoTime(seconds)); + if (it != thumbnails.end()) { + const QPixmap &thumb = it.value(); + int x = std::clamp(pos - thumb.width() / 2, THUMBNAIL_MARGIN, width() - thumb.width() - THUMBNAIL_MARGIN + 1); + int y = height() - thumb.height() - THUMBNAIL_MARGIN; + + p.drawPixmap(x, y, thumb); + p.setPen(QPen(palette().color(QPalette::BrightText), 2)); + p.drawText(x, y, thumb.width(), thumb.height() - THUMBNAIL_MARGIN, Qt::AlignHCenter | Qt::AlignBottom, QString::number(seconds)); + } +} + +void StreamCameraView::drawAlert(QPainter &p, const QRect &rect, const Timeline::Entry &alert) { + p.setPen(QPen(palette().color(QPalette::BrightText), 2)); + QColor color = timeline_colors[int(alert.type)]; + color.setAlphaF(0.5); + QString text = QString::fromStdString(alert.text1); + if (!alert.text2.empty()) text += "\n" + QString::fromStdString(alert.text2); + + QRect text_rect = rect.adjusted(1, 1, -1, -1); + QRect r = p.fontMetrics().boundingRect(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text); + p.fillRect(text_rect.left(), r.top(), text_rect.width(), r.height(), color); + p.drawText(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text); +} + +bool StreamCameraView::eventFilter(QObject *, QEvent *event) { + if (event->type() == QEvent::MouseMove) { + thumbnail_pt_ = static_cast(event)->pos(); + update(); + } else if (event->type() == QEvent::Leave) { + thumbnail_pt_ = std::nullopt; + update(); + } + return false; +} diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h index d3342c34d7b7be..78503365e5b80b 100644 --- a/tools/cabana/videowidget.h +++ b/tools/cabana/videowidget.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -18,17 +17,6 @@ #include "tools/replay/logreader.h" #include "tools/cabana/streams/replaystream.h" -class InfoLabel : public QWidget { -public: - InfoLabel(QWidget *parent); - void showPixmap(const QPoint &pt, const QString &sec, const QPixmap &pm, const std::optional &alert); - void showAlert(const std::optional &alert); - void paintEvent(QPaintEvent *event) override; - QPixmap pixmap; - QString second; - std::optional alert_info; -}; - class Slider : public QSlider { Q_OBJECT @@ -36,40 +24,30 @@ class Slider : public QSlider { Slider(QWidget *parent); double currentSecond() const { return value() / factor; } void setCurrentSecond(double sec) { setValue(sec * factor); } - void setTimeRange(double min, double max); - std::optional alertInfo(double sec); - QPixmap thumbnail(double sec); - void parseQLog(std::shared_ptr qlog); - - const double factor = 1000.0; - -private: + void setTimeRange(double min, double max) { setRange(min * factor, max * factor); } void mousePressEvent(QMouseEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; - bool event(QEvent *event) override; void paintEvent(QPaintEvent *ev) override; - - QMap thumbnails; - InfoLabel *thumbnail_label; + const double factor = 1000.0; }; class StreamCameraView : public CameraWidget { Q_OBJECT - Q_PROPERTY(float overlayOpacity READ overlayOpacity WRITE setOverlayOpacity) public: StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent = nullptr); void paintGL() override; void showPausedOverlay() { fade_animation->start(); } - float overlayOpacity() const { return overlay_opacity; } - void setOverlayOpacity(float opacity) { - overlay_opacity = opacity; - update(); - } + void parseQLog(std::shared_ptr qlog); private: - float overlay_opacity; + QPixmap generateThumbnail(QPixmap thumbnail, double seconds); + void drawAlert(QPainter &p, const QRect &rect, const Timeline::Entry &alert); + void drawThumbnail(QPainter &p); + bool eventFilter(QObject *obj, QEvent *event) override; + QPropertyAnimation *fade_animation; + QMap thumbnails; + std::optional thumbnail_pt_; }; class VideoWidget : public QFrame { @@ -96,7 +74,6 @@ class VideoWidget : public QFrame { ToolButton *loop_btn = nullptr; QToolButton *speed_btn = nullptr; ToolButton *skip_to_end_btn = nullptr; - InfoLabel *alert_label = nullptr; Slider *slider = nullptr; QTabBar *camera_tab = nullptr; }; diff --git a/tools/replay/replay.h b/tools/replay/replay.h index d3f542530889f0..0b4906532ea82d 100644 --- a/tools/replay/replay.h +++ b/tools/replay/replay.h @@ -60,8 +60,8 @@ class Replay : public QObject { inline int segmentCacheLimit() const { return segment_cache_limit; } inline void setSegmentCacheLimit(int n) { segment_cache_limit = std::max(MIN_SEGMENTS_CACHE, n); } inline bool hasFlag(REPLAY_FLAGS flag) const { return flags_ & flag; } - inline void addFlag(REPLAY_FLAGS flag) { flags_ |= flag; } - inline void removeFlag(REPLAY_FLAGS flag) { flags_ &= ~flag; } + void setLoop(bool loop) { loop ? flags_ &= ~REPLAY_FLAG_NO_LOOP : flags_ |= REPLAY_FLAG_NO_LOOP; } + bool loop() const { return !(flags_ & REPLAY_FLAG_NO_LOOP); } inline const Route* route() const { return route_.get(); } inline double currentSeconds() const { return double(cur_mono_time_ - route_start_ts_) / 1e9; } inline std::time_t routeDateTime() const { return route_date_time_; } diff --git a/tools/replay/timeline.cc b/tools/replay/timeline.cc index 5a4f58a46f3150..8c33e95f11d5ca 100644 --- a/tools/replay/timeline.cc +++ b/tools/replay/timeline.cc @@ -70,11 +70,11 @@ void Timeline::buildTimeline(const Route &route, uint64_t route_start_ts, bool l } } - callback(log); // Notify the callback once the log is processed - // Sort and finalize the timeline entries std::sort(staging_entries_.begin(), staging_entries_.end(), [](auto &a, auto &b) { return a.start_time < b.start_time; }); timeline_entries_ = std::make_shared>(staging_entries_); + + callback(log); // Notify the callback once the log is processed } }