Skip to content

Commit

Permalink
cabana: real-time cursor and video frame sync for chart and video (#3…
Browse files Browse the repository at this point in the history
…4301)

* sync cursor and thumbnail between chart and video

* Revert "replay: Update video immediately after seek when paused. (#34237)"

This reverts commit 3363881.

* use thumbnails while scrubing

* draw alert

* no update on resume

* draw timestamp

* cleanup
  • Loading branch information
deanlee authored Dec 21, 2024
1 parent 9e8815d commit 822f613
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 54 deletions.
15 changes: 5 additions & 10 deletions tools/cabana/chart/chartswidget.cc
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ QRect ChartsWidget::chartVisibleRect(ChartView *chart) {
}

void ChartsWidget::showValueTip(double sec) {
emit showTip(sec);
if (sec < 0 && !value_tip_visible_) return;

value_tip_visible_ = sec >= 0;
Expand Down Expand Up @@ -548,20 +549,14 @@ void ChartsContainer::dropEvent(QDropEvent *event) {

void ChartsContainer::paintEvent(QPaintEvent *ev) {
if (!drop_indictor_pos.isNull() && !childAt(drop_indictor_pos)) {
QRect r;
QRect r = geometry();
r.setHeight(CHART_SPACING);
if (auto insert_after = getDropAfter(drop_indictor_pos)) {
QRect area = insert_after->geometry();
r = QRect(area.left(), area.bottom() + 1, area.width(), CHART_SPACING);
} else {
r = geometry();
r.setHeight(CHART_SPACING);
r.moveTop(insert_after->geometry().bottom());
}

QPainter p(this);
p.setPen(QPen(palette().highlight(), 2));
p.drawLine(r.topLeft() + QPoint(1, 0), r.bottomLeft() + QPoint(1, 0));
p.drawLine(r.topLeft() + QPoint(0, r.height() / 2), r.topRight() + QPoint(0, r.height() / 2));
p.drawLine(r.topRight(), r.bottomRight());
p.fillRect(r, palette().highlight());
}
}

Expand Down
1 change: 1 addition & 0 deletions tools/cabana/chart/chartswidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public slots:
signals:
void toggleChartsDocking();
void seriesChanged();
void showTip(double seconds);

private:
QSize minimumSizeHint() const override;
Expand Down
1 change: 1 addition & 0 deletions tools/cabana/mainwin.cc
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ void MainWindow::createDockWidgets() {
video_splitter->handle(1)->setEnabled(!can->liveStreaming());
video_dock->setWidget(video_splitter);
QObject::connect(charts_widget, &ChartsWidget::toggleChartsDocking, this, &MainWindow::toggleChartsDocking);
QObject::connect(charts_widget, &ChartsWidget::showTip, video_widget, &VideoWidget::showThumbnail);
}

void MainWindow::createStatusBar() {
Expand Down
83 changes: 55 additions & 28 deletions tools/cabana/videowidget.cc
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,14 @@ QWidget *VideoWidget::createCameraWidget() {

QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->currentSecond()); });
QObject::connect(can, &AbstractStream::paused, cam_widget, [c = cam_widget]() { c->showPausedOverlay(); });
QObject::connect(can, &AbstractStream::resume, cam_widget, [c = cam_widget]() { c->update(); });
QObject::connect(can, &AbstractStream::eventsMerged, this, [this]() { slider->update(); });
QObject::connect(cam_widget, &CameraWidget::clicked, []() { can->pause(!can->isPaused()); });
QObject::connect(cam_widget, &CameraWidget::vipcAvailableStreamsUpdated, this, &VideoWidget::vipcAvailableStreamsUpdated);
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<ReplayStream*>(can), &ReplayStream::qLogLoaded, cam_widget, &StreamCameraView::parseQLog, Qt::QueuedConnection);
slider->installEventFilter(cam_widget);
slider->installEventFilter(this);
return w;
}

Expand Down Expand Up @@ -222,8 +221,24 @@ void VideoWidget::updatePlayBtnState() {
play_toggle_action->setToolTip(can->isPaused() ? tr("Play") : tr("Pause"));
}

// Slider
void VideoWidget::showThumbnail(double seconds) {
cam_widget->thumbnail_dispaly_time = seconds;
slider->thumbnail_dispaly_time = seconds;
cam_widget->update();
slider->update();
}

bool VideoWidget::eventFilter(QObject *obj, QEvent *event) {
if (event->type() == QEvent::MouseMove) {
auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds()));
showThumbnail(min_sec + static_cast<QMouseEvent *>(event)->pos().x() * (max_sec - min_sec) / slider->width());
} else if (event->type() == QEvent::Leave) {
showThumbnail(-1);
}
return false;
}

// Slider
Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) {
setMouseTracking(true);
}
Expand Down Expand Up @@ -265,6 +280,14 @@ void Slider::paintEvent(QPaintEvent *ev) {
opt.subControls = QStyle::SC_SliderHandle;
opt.sliderPosition = value();
style()->drawComplexControl(QStyle::CC_Slider, &opt, &p);

if (thumbnail_dispaly_time >= 0) {
int left = (thumbnail_dispaly_time - min) * width() / (max - min) - 1;
QRect rc(left, rect().top() + 1, 2, rect().height() - 2);
p.setBrush(palette().highlight());
p.setPen(Qt::NoPen);
p.drawRoundedRect(rc, 1.5, 1.5);
}
}

void Slider::mousePressEvent(QMouseEvent *e) {
Expand All @@ -276,7 +299,6 @@ void Slider::mousePressEvent(QMouseEvent *e) {
}

// StreamCameraView

StreamCameraView::StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent)
: CameraWidget(stream_name, stream_type, parent) {
fade_animation = new QPropertyAnimation(this, "overlayOpacity");
Expand All @@ -298,6 +320,7 @@ void StreamCameraView::parseQLog(std::shared_ptr<LogReader> qlog) {
QPixmap generated_thumb = generateThumbnail(thumb, can->toSeconds(thumb_data.getTimestampEof()));
std::lock_guard lock(mutex);
thumbnails[thumb_data.getTimestampEof()] = generated_thumb;
big_thumbnails[thumb_data.getTimestampEof()] = thumb;
}
}
});
Expand All @@ -308,12 +331,15 @@ void StreamCameraView::paintGL() {
CameraWidget::paintGL();

QPainter p(this);
if (auto alert = getReplay()->findAlertAtTime(can->currentSec())) {
drawAlert(p, rect(), *alert);
bool scrubbing = false;
if (thumbnail_dispaly_time >= 0) {
scrubbing = can->isPaused();
scrubbing ? drawScrubThumbnail(p) : drawThumbnail(p);
}
if (thumbnail_pt_) {
drawThumbnail(p);
if (auto alert = getReplay()->findAlertAtTime(scrubbing ? thumbnail_dispaly_time : can->currentSec())) {
drawAlert(p, rect(), *alert);
}

if (can->isPaused()) {
p.setPen(QColor(200, 200, 200, static_cast<int>(255 * fade_animation->currentValue().toFloat())));
p.setFont(QFont(font().family(), 16, QFont::Bold));
Expand All @@ -333,25 +359,37 @@ QPixmap StreamCameraView::generateThumbnail(QPixmap thumb, double seconds) {
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();
void StreamCameraView::drawScrubThumbnail(QPainter &p) {
p.fillRect(rect(), Qt::black);
auto it = big_thumbnails.lowerBound(can->toMonoTime(thumbnail_dispaly_time));
if (it != big_thumbnails.end()) {
QPixmap scaled_thumb = it.value().scaled(rect().size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
QRect thumb_rect(rect().center() - scaled_thumb.rect().center(), scaled_thumb.size());
p.drawPixmap(thumb_rect.topLeft(), scaled_thumb);
drawTime(p, thumb_rect, thumbnail_dispaly_time);
}
}

auto it = thumbnails.lowerBound(can->toMonoTime(seconds));
void StreamCameraView::drawThumbnail(QPainter &p) {
auto it = thumbnails.lowerBound(can->toMonoTime(thumbnail_dispaly_time));
if (it != thumbnails.end()) {
const QPixmap &thumb = it.value();
auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds()));
int pos = (thumbnail_dispaly_time - min_sec) * width() / (max_sec - min_sec);
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.setFont(QFont(font().family(), 10));
p.drawText(x, y, thumb.width(), thumb.height() - THUMBNAIL_MARGIN,
Qt::AlignHCenter | Qt::AlignBottom, QString::number(seconds, 'f', 3));
drawTime(p, QRect{x, y, thumb.width(), thumb.height()}, thumbnail_dispaly_time);
}
}

void StreamCameraView::drawTime(QPainter &p, const QRect &rect, double seconds) {
p.setPen(palette().color(QPalette::BrightText));
p.setFont(QFont(font().family(), 10));
p.drawText(rect.adjusted(0, 0, 0, -THUMBNAIL_MARGIN), Qt::AlignHCenter | Qt::AlignBottom, QString::number(seconds, 'f', 3));
}

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)];
Expand All @@ -364,14 +402,3 @@ void StreamCameraView::drawAlert(QPainter &p, const QRect &rect, const Timeline:
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<QMouseEvent *>(event)->pos();
update();
} else if (event->type() == QEvent::Leave) {
thumbnail_pt_ = std::nullopt;
update();
}
return false;
}
11 changes: 8 additions & 3 deletions tools/cabana/videowidget.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#pragma once

#include <memory>
#include <optional>
#include <set>
#include <string>
#include <utility>
Expand All @@ -28,6 +27,7 @@ class Slider : public QSlider {
void mousePressEvent(QMouseEvent *e) override;
void paintEvent(QPaintEvent *ev) override;
const double factor = 1000.0;
double thumbnail_dispaly_time = -1;
};

class StreamCameraView : public CameraWidget {
Expand All @@ -43,20 +43,25 @@ class StreamCameraView : public CameraWidget {
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;
void drawScrubThumbnail(QPainter &p);
void drawTime(QPainter &p, const QRect &rect, double seconds);

QPropertyAnimation *fade_animation;
QMap<uint64_t, QPixmap> big_thumbnails;
QMap<uint64_t, QPixmap> thumbnails;
std::optional<QPoint> thumbnail_pt_;
double thumbnail_dispaly_time = -1;
friend class VideoWidget;
};

class VideoWidget : public QFrame {
Q_OBJECT

public:
VideoWidget(QWidget *parnet = nullptr);
void showThumbnail(double seconds);

protected:
bool eventFilter(QObject *obj, QEvent *event) override;
QString formatTime(double sec, bool include_milliseconds = false);
void timeRangeChanged();
void updateState();
Expand Down
13 changes: 1 addition & 12 deletions tools/replay/replay.cc
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ void Replay::interruptStream(const std::function<bool()> &update_fn) {
interrupt_requested_ = true;
std::unique_lock lock(stream_lock_);
events_ready_ = update_fn();
interrupt_requested_ = user_paused_ && !pause_after_next_frame_;
interrupt_requested_ = user_paused_;
}
stream_cv_.notify_one();
}
Expand All @@ -116,9 +116,6 @@ void Replay::seekTo(double seconds, bool relative) {
seeked_to_sec = *seeking_to_;
seeking_to_.reset();
}

// if paused, resume for exactly one frame to update
pause_after_next_frame_ = user_paused_;
return false;
});

Expand Down Expand Up @@ -147,7 +144,6 @@ void Replay::pause(bool pause) {
interruptStream([=]() {
rWarning("%s at %.2f s", pause ? "paused..." : "resuming", currentSeconds());
user_paused_ = pause;
pause_after_next_frame_ = false;
return !pause;
});
}
Expand Down Expand Up @@ -309,7 +305,6 @@ std::vector<Event>::const_iterator Replay::publishEvents(std::vector<Event>::con
uint64_t evt_start_ts = cur_mono_time_;
uint64_t loop_start_ts = nanos_since_boot();
double prev_replay_speed = speed_;
uint64_t first_mono_time = first->mono_time;

for (; !interrupt_requested_ && first != last; ++first) {
const Event &evt = *first;
Expand Down Expand Up @@ -348,12 +343,6 @@ std::vector<Event>::const_iterator Replay::publishEvents(std::vector<Event>::con
}
publishFrame(&evt);
}

const auto T_ONE_FRAME = 0.050;
if (pause_after_next_frame_ && abs(toSeconds(evt.mono_time) - toSeconds(first_mono_time)) > T_ONE_FRAME) {
pause_after_next_frame_ = false;
interrupt_requested_ = true;
}
}

return first;
Expand Down
1 change: 0 additions & 1 deletion tools/replay/replay.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ class Replay {
std::thread stream_thread_;
std::mutex stream_lock_;
bool user_paused_ = false;
bool pause_after_next_frame_ = false;
std::condition_variable stream_cv_;
int current_segment_ = 0;
std::optional<double> seeking_to_;
Expand Down

0 comments on commit 822f613

Please sign in to comment.