|  |  |  | @ -27,6 +27,13 @@ static const QColor timeline_colors[] = { | 
			
		
	
		
			
				
					|  |  |  |  |   [(int)TimelineType::AlertCritical] = QColor(199, 0, 57), | 
			
		
	
		
			
				
					|  |  |  |  | }; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | static Replay *getReplay() { | 
			
		
	
		
			
				
					|  |  |  |  |   auto stream = qobject_cast<ReplayStream *>(can); | 
			
		
	
		
			
				
					|  |  |  |  |   if (!stream) return nullptr; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   return stream->getReplay(); | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { | 
			
		
	
		
			
				
					|  |  |  |  |   setFrameStyle(QFrame::StyledPanel | QFrame::Plain); | 
			
		
	
		
			
				
					|  |  |  |  |   auto main_layout = new QVBoxLayout(this); | 
			
		
	
	
		
			
				
					|  |  |  | @ -76,7 +83,7 @@ QHBoxLayout *VideoWidget::createPlaybackController() { | 
			
		
	
		
			
				
					|  |  |  |  |       // set speed to 1.0
 | 
			
		
	
		
			
				
					|  |  |  |  |       speed_btn->menu()->actions()[7]->setChecked(true); | 
			
		
	
		
			
				
					|  |  |  |  |       can->pause(false); | 
			
		
	
		
			
				
					|  |  |  |  |       can->seekTo(can->totalSeconds() + 1); | 
			
		
	
		
			
				
					|  |  |  |  |       can->seekTo(can->maxSeconds() + 1); | 
			
		
	
		
			
				
					|  |  |  |  |     }); | 
			
		
	
		
			
				
					|  |  |  |  |   } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  |  | @ -141,7 +148,7 @@ QWidget *VideoWidget::createCameraWidget() { | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   QStackedLayout *stacked = new QStackedLayout(); | 
			
		
	
		
			
				
					|  |  |  |  |   stacked->setStackingMode(QStackedLayout::StackAll); | 
			
		
	
		
			
				
					|  |  |  |  |   stacked->addWidget(cam_widget = new CameraWidget("camerad", VISION_STREAM_ROAD, false)); | 
			
		
	
		
			
				
					|  |  |  |  |   stacked->addWidget(cam_widget = new StreamCameraView("camerad", VISION_STREAM_ROAD, false)); | 
			
		
	
		
			
				
					|  |  |  |  |   cam_widget->setMinimumHeight(MIN_VIDEO_HEIGHT); | 
			
		
	
		
			
				
					|  |  |  |  |   cam_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); | 
			
		
	
		
			
				
					|  |  |  |  |   stacked->addWidget(alert_label = new InfoLabel(this)); | 
			
		
	
	
		
			
				
					|  |  |  | @ -149,9 +156,11 @@ QWidget *VideoWidget::createCameraWidget() { | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   l->addWidget(slider = new Slider(w)); | 
			
		
	
		
			
				
					|  |  |  |  |   slider->setSingleStep(0); | 
			
		
	
		
			
				
					|  |  |  |  |   slider->setTimeRange(can->minSeconds(), can->maxSeconds()); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   setMaximumTime(can->totalSeconds()); | 
			
		
	
		
			
				
					|  |  |  |  |   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); | 
			
		
	
	
		
			
				
					|  |  |  | @ -161,7 +170,7 @@ QWidget *VideoWidget::createCameraWidget() { | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   auto replay = static_cast<ReplayStream*>(can)->getReplay(); | 
			
		
	
		
			
				
					|  |  |  |  |   QObject::connect(replay, &Replay::qLogLoaded, slider, &Slider::parseQLog, Qt::QueuedConnection); | 
			
		
	
		
			
				
					|  |  |  |  |   QObject::connect(replay, &Replay::totalSecondsUpdated, this, &VideoWidget::setMaximumTime, Qt::QueuedConnection); | 
			
		
	
		
			
				
					|  |  |  |  |   QObject::connect(replay, &Replay::minMaxTimeChanged, this, &VideoWidget::timeRangeChanged, Qt::QueuedConnection); | 
			
		
	
		
			
				
					|  |  |  |  |   return w; | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  |  | @ -185,7 +194,7 @@ void VideoWidget::vipcAvailableStreamsUpdated(std::set<VisionStreamType> streams | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | void VideoWidget::loopPlaybackClicked() { | 
			
		
	
		
			
				
					|  |  |  |  |   auto replay = qobject_cast<ReplayStream *>(can)->getReplay(); | 
			
		
	
		
			
				
					|  |  |  |  |   auto replay = getReplay(); | 
			
		
	
		
			
				
					|  |  |  |  |   if (!replay) return; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   if (replay->hasFlag(REPLAY_FLAG_NO_LOOP)) { | 
			
		
	
	
		
			
				
					|  |  |  | @ -197,18 +206,15 @@ void VideoWidget::loopPlaybackClicked() { | 
			
		
	
		
			
				
					|  |  |  |  |   } | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | void VideoWidget::setMaximumTime(double sec) { | 
			
		
	
		
			
				
					|  |  |  |  |   maximum_time = sec; | 
			
		
	
		
			
				
					|  |  |  |  |   slider->setTimeRange(0, sec); | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | void VideoWidget::timeRangeChanged(const std::optional<std::pair<double, double>> &time_range) { | 
			
		
	
		
			
				
					|  |  |  |  | void VideoWidget::timeRangeChanged() { | 
			
		
	
		
			
				
					|  |  |  |  |   const auto time_range = can->timeRange(); | 
			
		
	
		
			
				
					|  |  |  |  |   if (can->liveStreaming()) { | 
			
		
	
		
			
				
					|  |  |  |  |     skip_to_end_btn->setEnabled(!time_range.has_value()); | 
			
		
	
		
			
				
					|  |  |  |  |     return; | 
			
		
	
		
			
				
					|  |  |  |  |   } | 
			
		
	
		
			
				
					|  |  |  |  |   time_range ? slider->setTimeRange(time_range->first, time_range->second) | 
			
		
	
		
			
				
					|  |  |  |  |              : slider->setTimeRange(0, maximum_time); | 
			
		
	
		
			
				
					|  |  |  |  |              : slider->setTimeRange(can->minSeconds(), can->maxSeconds()); | 
			
		
	
		
			
				
					|  |  |  |  |   updateState(); | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | QString VideoWidget::formatTime(double sec, bool include_milliseconds) { | 
			
		
	
	
		
			
				
					|  |  |  | @ -219,8 +225,9 @@ QString VideoWidget::formatTime(double sec, bool include_milliseconds) { | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | void VideoWidget::updateState() { | 
			
		
	
		
			
				
					|  |  |  |  |   if (slider) { | 
			
		
	
		
			
				
					|  |  |  |  |     if (!slider->isSliderDown()) | 
			
		
	
		
			
				
					|  |  |  |  |     if (!slider->isSliderDown()) { | 
			
		
	
		
			
				
					|  |  |  |  |       slider->setCurrentSecond(can->currentSec()); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  |     alert_label->showAlert(slider->alertInfo(can->currentSec())); | 
			
		
	
		
			
				
					|  |  |  |  |     time_btn->setText(QString("%1 / %2").arg(formatTime(can->currentSec(), true), | 
			
		
	
		
			
				
					|  |  |  |  |                                              formatTime(slider->maximum() / slider->factor))); | 
			
		
	
	
		
			
				
					|  |  |  | @ -242,15 +249,14 @@ Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) { | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | AlertInfo Slider::alertInfo(double seconds) { | 
			
		
	
		
			
				
					|  |  |  |  |   uint64_t mono_time = (seconds + can->routeStartTime()) * 1e9; | 
			
		
	
		
			
				
					|  |  |  |  |   uint64_t mono_time = can->toMonoTime(seconds); | 
			
		
	
		
			
				
					|  |  |  |  |   auto alert_it = alerts.lower_bound(mono_time); | 
			
		
	
		
			
				
					|  |  |  |  |   bool has_alert = (alert_it != alerts.end()) && ((alert_it->first - mono_time) <= 1e8); | 
			
		
	
		
			
				
					|  |  |  |  |   return has_alert ? alert_it->second : AlertInfo{}; | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | QPixmap Slider::thumbnail(double seconds)  { | 
			
		
	
		
			
				
					|  |  |  |  |   uint64_t mono_time = (seconds + can->routeStartTime()) * 1e9; | 
			
		
	
		
			
				
					|  |  |  |  |   auto it = thumbnails.lowerBound(mono_time); | 
			
		
	
		
			
				
					|  |  |  |  |   auto it = thumbnails.lowerBound(can->toMonoTime(seconds)); | 
			
		
	
		
			
				
					|  |  |  |  |   return it != thumbnails.end() ? it.value() : QPixmap(); | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  |  | @ -298,16 +304,18 @@ void Slider::paintEvent(QPaintEvent *ev) { | 
			
		
	
		
			
				
					|  |  |  |  |     p.fillRect(r, color); | 
			
		
	
		
			
				
					|  |  |  |  |   }; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   const auto replay = qobject_cast<ReplayStream *>(can)->getReplay(); | 
			
		
	
		
			
				
					|  |  |  |  |   for (auto [begin, end, type] : replay->getTimeline()) { | 
			
		
	
		
			
				
					|  |  |  |  |     fillRange(begin, end, timeline_colors[(int)type]); | 
			
		
	
		
			
				
					|  |  |  |  |   } | 
			
		
	
		
			
				
					|  |  |  |  |   auto replay = getReplay(); | 
			
		
	
		
			
				
					|  |  |  |  |   if (replay) { | 
			
		
	
		
			
				
					|  |  |  |  |     for (auto [begin, end, type] : replay->getTimeline()) { | 
			
		
	
		
			
				
					|  |  |  |  |       fillRange(begin, end, timeline_colors[(int)type]); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   QColor empty_color = palette().color(QPalette::Window); | 
			
		
	
		
			
				
					|  |  |  |  |   empty_color.setAlpha(160); | 
			
		
	
		
			
				
					|  |  |  |  |   for (const auto &[n, seg] : replay->segments()) { | 
			
		
	
		
			
				
					|  |  |  |  |     if (!(seg && seg->isLoaded())) | 
			
		
	
		
			
				
					|  |  |  |  |       fillRange(n * 60.0, (n + 1) * 60.0, empty_color); | 
			
		
	
		
			
				
					|  |  |  |  |     QColor empty_color = palette().color(QPalette::Window); | 
			
		
	
		
			
				
					|  |  |  |  |     empty_color.setAlpha(160); | 
			
		
	
		
			
				
					|  |  |  |  |     for (const auto &[n, seg] : replay->segments()) { | 
			
		
	
		
			
				
					|  |  |  |  |       if (!(seg && seg->isLoaded())) | 
			
		
	
		
			
				
					|  |  |  |  |         fillRange(n * 60.0, (n + 1) * 60.0, empty_color); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  |   } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   QStyleOptionSlider opt; | 
			
		
	
	
		
			
				
					|  |  |  | @ -411,3 +419,22 @@ void InfoLabel::paintEvent(QPaintEvent *event) { | 
			
		
	
		
			
				
					|  |  |  |  |     p.drawText(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text); | 
			
		
	
		
			
				
					|  |  |  |  |   } | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | StreamCameraView::StreamCameraView(std::string stream_name, VisionStreamType stream_type, bool zoom, QWidget *parent) | 
			
		
	
		
			
				
					|  |  |  |  |     : CameraWidget(stream_name, stream_type, zoom, parent) { | 
			
		
	
		
			
				
					|  |  |  |  |   fade_animation = new QPropertyAnimation(this, "overlayOpacity"); | 
			
		
	
		
			
				
					|  |  |  |  |   fade_animation->setDuration(500); | 
			
		
	
		
			
				
					|  |  |  |  |   fade_animation->setStartValue(0.2f); | 
			
		
	
		
			
				
					|  |  |  |  |   fade_animation->setEndValue(0.7f); | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | void StreamCameraView::paintGL() { | 
			
		
	
		
			
				
					|  |  |  |  |   CameraWidget::paintGL(); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   if (can->isPaused()) { | 
			
		
	
		
			
				
					|  |  |  |  |     QPainter p(this); | 
			
		
	
		
			
				
					|  |  |  |  |     p.setPen(QColor(200, 200, 200, static_cast<int>(255 * overlay_opacity))); | 
			
		
	
		
			
				
					|  |  |  |  |     p.setFont(QFont(font().family(), 16, QFont::Bold)); | 
			
		
	
		
			
				
					|  |  |  |  |     p.drawText(rect(), Qt::AlignCenter, tr("PAUSED")); | 
			
		
	
		
			
				
					|  |  |  |  |   } | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
	
		
			
				
					|  |  |  | 
 |