|  |  |  | @ -55,11 +55,11 @@ void Replay::stop() { | 
			
		
	
		
			
				
					|  |  |  |  |   rInfo("shutdown: in progress..."); | 
			
		
	
		
			
				
					|  |  |  |  |   if (stream_thread_ != nullptr) { | 
			
		
	
		
			
				
					|  |  |  |  |     exit_ = true; | 
			
		
	
		
			
				
					|  |  |  |  |     paused_ = true; | 
			
		
	
		
			
				
					|  |  |  |  |     pauseStreamThread(); | 
			
		
	
		
			
				
					|  |  |  |  |     stream_cv_.notify_one(); | 
			
		
	
		
			
				
					|  |  |  |  |     stream_thread_->quit(); | 
			
		
	
		
			
				
					|  |  |  |  |     stream_thread_->wait(); | 
			
		
	
		
			
				
					|  |  |  |  |     delete stream_thread_; | 
			
		
	
		
			
				
					|  |  |  |  |     stream_thread_->deleteLater(); | 
			
		
	
		
			
				
					|  |  |  |  |     stream_thread_ = nullptr; | 
			
		
	
		
			
				
					|  |  |  |  |   } | 
			
		
	
		
			
				
					|  |  |  |  |   timeline_future.waitForFinished(); | 
			
		
	
	
		
			
				
					|  |  |  | @ -104,28 +104,40 @@ void Replay::updateEvents(const std::function<bool()> &update_events_function) { | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | void Replay::seekTo(double seconds, bool relative) { | 
			
		
	
		
			
				
					|  |  |  |  |   updateEvents([&]() { | 
			
		
	
		
			
				
					|  |  |  |  |     seeking_to_seconds_ = relative ? seconds + currentSeconds() : seconds; | 
			
		
	
		
			
				
					|  |  |  |  |     seeking_to_seconds_ = std::max(double(0.0), seeking_to_seconds_); | 
			
		
	
		
			
				
					|  |  |  |  |     int target_segment = (int)seeking_to_seconds_ / 60; | 
			
		
	
		
			
				
					|  |  |  |  |     double target_time = relative ? seconds + currentSeconds() : seconds; | 
			
		
	
		
			
				
					|  |  |  |  |     target_time = std::max(double(0.0), target_time); | 
			
		
	
		
			
				
					|  |  |  |  |     int target_segment = (int)target_time / 60; | 
			
		
	
		
			
				
					|  |  |  |  |     if (segments_.count(target_segment) == 0) { | 
			
		
	
		
			
				
					|  |  |  |  |       rWarning("can't seek to %d s segment %d is invalid", (int)seeking_to_seconds_, target_segment); | 
			
		
	
		
			
				
					|  |  |  |  |       rWarning("Can't seek to %d s segment %d is invalid", (int)target_time, target_segment); | 
			
		
	
		
			
				
					|  |  |  |  |       return true; | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     rInfo("seeking to %d s, segment %d", (int)seeking_to_seconds_, target_segment); | 
			
		
	
		
			
				
					|  |  |  |  |     rInfo("Seeking to %d s, segment %d", (int)target_time, target_segment); | 
			
		
	
		
			
				
					|  |  |  |  |     current_segment_ = target_segment; | 
			
		
	
		
			
				
					|  |  |  |  |     cur_mono_time_ = route_start_ts_ + seeking_to_seconds_ * 1e9; | 
			
		
	
		
			
				
					|  |  |  |  |     bool segment_merged = isSegmentMerged(target_segment); | 
			
		
	
		
			
				
					|  |  |  |  |     if (segment_merged) { | 
			
		
	
		
			
				
					|  |  |  |  |       emit seekedTo(seeking_to_seconds_); | 
			
		
	
		
			
				
					|  |  |  |  |       // Reset seeking_to_seconds_ to indicate completion of seek
 | 
			
		
	
		
			
				
					|  |  |  |  |       seeking_to_seconds_ = -1; | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  |     return segment_merged; | 
			
		
	
		
			
				
					|  |  |  |  |     cur_mono_time_ = route_start_ts_ + target_time * 1e9; | 
			
		
	
		
			
				
					|  |  |  |  |     seeking_to_ = target_time; | 
			
		
	
		
			
				
					|  |  |  |  |     return false; | 
			
		
	
		
			
				
					|  |  |  |  |   }); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   checkSeekProgress(); | 
			
		
	
		
			
				
					|  |  |  |  |   updateSegmentsCache(); | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | void Replay::checkSeekProgress() { | 
			
		
	
		
			
				
					|  |  |  |  |   if (seeking_to_) { | 
			
		
	
		
			
				
					|  |  |  |  |     auto it = segments_.find(int(*seeking_to_ / 60)); | 
			
		
	
		
			
				
					|  |  |  |  |     if (it != segments_.end() && it->second && it->second->isLoaded()) { | 
			
		
	
		
			
				
					|  |  |  |  |       emit seekedTo(*seeking_to_); | 
			
		
	
		
			
				
					|  |  |  |  |       seeking_to_ = std::nullopt; | 
			
		
	
		
			
				
					|  |  |  |  |       // wake up stream thread
 | 
			
		
	
		
			
				
					|  |  |  |  |       updateEvents([]() { return true; }); | 
			
		
	
		
			
				
					|  |  |  |  |     } else { | 
			
		
	
		
			
				
					|  |  |  |  |       // Emit signal indicating the ongoing seek operation
 | 
			
		
	
		
			
				
					|  |  |  |  |       emit seeking(*seeking_to_); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  |   } | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | void Replay::seekToFlag(FindFlag flag) { | 
			
		
	
		
			
				
					|  |  |  |  |   if (auto next = find(flag)) { | 
			
		
	
		
			
				
					|  |  |  |  |     seekTo(*next - 2, false);  // seek to 2 seconds before next
 | 
			
		
	
	
		
			
				
					|  |  |  | @ -150,8 +162,9 @@ void Replay::buildTimeline() { | 
			
		
	
		
			
				
					|  |  |  |  |   const auto &route_segments = route_->segments(); | 
			
		
	
		
			
				
					|  |  |  |  |   for (auto it = route_segments.cbegin(); it != route_segments.cend() && !exit_; ++it) { | 
			
		
	
		
			
				
					|  |  |  |  |     std::shared_ptr<LogReader> log(new LogReader()); | 
			
		
	
		
			
				
					|  |  |  |  |     if (!log->load(it->second.qlog.toStdString(), &exit_, !hasFlag(REPLAY_FLAG_NO_FILE_CACHE), 0, 3)) continue; | 
			
		
	
		
			
				
					|  |  |  |  |     if (!log->load(it->second.qlog.toStdString(), &exit_, !hasFlag(REPLAY_FLAG_NO_FILE_CACHE), 0, 3) || log->events.empty()) continue; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     std::vector<std::tuple<double, double, TimelineType>> timeline; | 
			
		
	
		
			
				
					|  |  |  |  |     for (const Event &e : log->events) { | 
			
		
	
		
			
				
					|  |  |  |  |       if (e.which == cereal::Event::Which::CONTROLS_STATE) { | 
			
		
	
		
			
				
					|  |  |  |  |         capnp::FlatArrayMessageReader reader(e.data); | 
			
		
	
	
		
			
				
					|  |  |  | @ -160,7 +173,6 @@ void Replay::buildTimeline() { | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         if (engaged != cs.getEnabled()) { | 
			
		
	
		
			
				
					|  |  |  |  |           if (engaged) { | 
			
		
	
		
			
				
					|  |  |  |  |             std::lock_guard lk(timeline_lock); | 
			
		
	
		
			
				
					|  |  |  |  |             timeline.push_back({toSeconds(engaged_begin), toSeconds(e.mono_time), TimelineType::Engaged}); | 
			
		
	
		
			
				
					|  |  |  |  |           } | 
			
		
	
		
			
				
					|  |  |  |  |           engaged_begin = e.mono_time; | 
			
		
	
	
		
			
				
					|  |  |  | @ -169,7 +181,6 @@ void Replay::buildTimeline() { | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         if (alert_type != cs.getAlertType().cStr() || alert_status != cs.getAlertStatus()) { | 
			
		
	
		
			
				
					|  |  |  |  |           if (!alert_type.empty() && alert_size != cereal::ControlsState::AlertSize::NONE) { | 
			
		
	
		
			
				
					|  |  |  |  |             std::lock_guard lk(timeline_lock); | 
			
		
	
		
			
				
					|  |  |  |  |             timeline.push_back({toSeconds(alert_begin), toSeconds(e.mono_time), timeline_types[(int)alert_status]}); | 
			
		
	
		
			
				
					|  |  |  |  |           } | 
			
		
	
		
			
				
					|  |  |  |  |           alert_begin = e.mono_time; | 
			
		
	
	
		
			
				
					|  |  |  | @ -178,12 +189,20 @@ void Replay::buildTimeline() { | 
			
		
	
		
			
				
					|  |  |  |  |           alert_status = cs.getAlertStatus(); | 
			
		
	
		
			
				
					|  |  |  |  |         } | 
			
		
	
		
			
				
					|  |  |  |  |       } else if (e.which == cereal::Event::Which::USER_FLAG) { | 
			
		
	
		
			
				
					|  |  |  |  |         std::lock_guard lk(timeline_lock); | 
			
		
	
		
			
				
					|  |  |  |  |         timeline.push_back({toSeconds(e.mono_time), toSeconds(e.mono_time), TimelineType::UserFlag}); | 
			
		
	
		
			
				
					|  |  |  |  |       } | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  |     std::sort(timeline.begin(), timeline.end(), [](auto &l, auto &r) { return std::get<2>(l) < std::get<2>(r); }); | 
			
		
	
		
			
				
					|  |  |  |  |     emit qLogLoaded(it->first, log); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     { | 
			
		
	
		
			
				
					|  |  |  |  |       std::lock_guard lk(timeline_lock); | 
			
		
	
		
			
				
					|  |  |  |  |       timeline_.insert(timeline_.end(), timeline.begin(), timeline.end()); | 
			
		
	
		
			
				
					|  |  |  |  |       std::sort(timeline_.begin(), timeline_.end(), [](auto &l, auto &r) { return std::get<2>(l) < std::get<2>(r); }); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     if (it->first == route_segments.rbegin()->first) { | 
			
		
	
		
			
				
					|  |  |  |  |       emit totalSecondsUpdated(toSeconds(log->events.back().mono_time)); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  |     emit qLogLoaded(log); | 
			
		
	
		
			
				
					|  |  |  |  |   } | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  |  | @ -260,14 +279,13 @@ void Replay::updateSegmentsCache() { | 
			
		
	
		
			
				
					|  |  |  |  |   const auto &cur_segment = cur->second; | 
			
		
	
		
			
				
					|  |  |  |  |   if (stream_thread_ == nullptr && cur_segment->isLoaded()) { | 
			
		
	
		
			
				
					|  |  |  |  |     startStream(cur_segment.get()); | 
			
		
	
		
			
				
					|  |  |  |  |     emit streamStarted(); | 
			
		
	
		
			
				
					|  |  |  |  |   } | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | void Replay::loadSegmentInRange(SegmentMap::iterator begin, SegmentMap::iterator cur, SegmentMap::iterator end) { | 
			
		
	
		
			
				
					|  |  |  |  |   auto loadNext = [this](auto begin, auto end) { | 
			
		
	
		
			
				
					|  |  |  |  |     auto it = std::find_if(begin, end, [](const auto &seg_it) { return !seg_it.second || !seg_it.second->isLoaded(); }); | 
			
		
	
		
			
				
					|  |  |  |  |     if (it != end && !it->second) { | 
			
		
	
		
			
				
					|  |  |  |  |   auto loadNextSegment = [this](auto first, auto last) { | 
			
		
	
		
			
				
					|  |  |  |  |     auto it = std::find_if(first, last, [](const auto &seg_it) { return !seg_it.second || !seg_it.second->isLoaded(); }); | 
			
		
	
		
			
				
					|  |  |  |  |     if (it != last && !it->second) { | 
			
		
	
		
			
				
					|  |  |  |  |       rDebug("loading segment %d...", it->first); | 
			
		
	
		
			
				
					|  |  |  |  |       it->second = std::make_unique<Segment>(it->first, route_->at(it->first), flags_, filters_); | 
			
		
	
		
			
				
					|  |  |  |  |       QObject::connect(it->second.get(), &Segment::loadFinished, this, &Replay::segmentLoadFinished); | 
			
		
	
	
		
			
				
					|  |  |  | @ -276,9 +294,9 @@ void Replay::loadSegmentInRange(SegmentMap::iterator begin, SegmentMap::iterator | 
			
		
	
		
			
				
					|  |  |  |  |     return false; | 
			
		
	
		
			
				
					|  |  |  |  |   }; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   // Load forward segments, then try reverse
 | 
			
		
	
		
			
				
					|  |  |  |  |   if (!loadNext(cur, end)) { | 
			
		
	
		
			
				
					|  |  |  |  |     loadNext(std::make_reverse_iterator(cur), segments_.rend()); | 
			
		
	
		
			
				
					|  |  |  |  |   // Try loading forward segments, then reverse segments
 | 
			
		
	
		
			
				
					|  |  |  |  |   if (!loadNextSegment(cur, end)) { | 
			
		
	
		
			
				
					|  |  |  |  |     loadNextSegment(std::make_reverse_iterator(cur), std::make_reverse_iterator(begin)); | 
			
		
	
		
			
				
					|  |  |  |  |   } | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  |  | @ -316,15 +334,10 @@ void Replay::mergeSegments(const SegmentMap::iterator &begin, const SegmentMap:: | 
			
		
	
		
			
				
					|  |  |  |  |   updateEvents([&]() { | 
			
		
	
		
			
				
					|  |  |  |  |     events_.swap(new_events); | 
			
		
	
		
			
				
					|  |  |  |  |     merged_segments_ = segments_to_merge; | 
			
		
	
		
			
				
					|  |  |  |  |     // Check if seeking is in progress
 | 
			
		
	
		
			
				
					|  |  |  |  |     int target_segment = int(seeking_to_seconds_ / 60); | 
			
		
	
		
			
				
					|  |  |  |  |     if (seeking_to_seconds_ >= 0 && segments_to_merge.count(target_segment) > 0) { | 
			
		
	
		
			
				
					|  |  |  |  |       emit seekedTo(seeking_to_seconds_); | 
			
		
	
		
			
				
					|  |  |  |  |       seeking_to_seconds_ = -1;  // Reset seeking_to_seconds_ to indicate completion of seek
 | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  |     // Wake up the stream thread if the current segment is loaded or invalid.
 | 
			
		
	
		
			
				
					|  |  |  |  |     return isSegmentMerged(current_segment_) || (segments_.count(current_segment_) == 0); | 
			
		
	
		
			
				
					|  |  |  |  |     return !seeking_to_ && (isSegmentMerged(current_segment_) || (segments_.count(current_segment_) == 0)); | 
			
		
	
		
			
				
					|  |  |  |  |   }); | 
			
		
	
		
			
				
					|  |  |  |  |   checkSeekProgress(); | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | void Replay::startStream(const Segment *cur_segment) { | 
			
		
	
	
		
			
				
					|  |  |  | @ -379,6 +392,7 @@ void Replay::startStream(const Segment *cur_segment) { | 
			
		
	
		
			
				
					|  |  |  |  |   stream_thread_->start(); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   timeline_future = QtConcurrent::run(this, &Replay::buildTimeline); | 
			
		
	
		
			
				
					|  |  |  |  |   emit streamStarted(); | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | void Replay::publishMessage(const Event *e) { | 
			
		
	
	
		
			
				
					|  |  |  | @ -473,6 +487,7 @@ std::vector<Event>::const_iterator Replay::publishEvents(std::vector<Event>::con | 
			
		
	
		
			
				
					|  |  |  |  |      // Skip events if socket is not present
 | 
			
		
	
		
			
				
					|  |  |  |  |     if (!sockets_[evt.which]) continue; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     cur_mono_time_ = evt.mono_time; | 
			
		
	
		
			
				
					|  |  |  |  |     const uint64_t current_nanos = nanos_since_boot(); | 
			
		
	
		
			
				
					|  |  |  |  |     const int64_t time_diff = (evt.mono_time - evt_start_ts) / speed_ - (current_nanos - loop_start_ts); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  |  | @ -484,12 +499,11 @@ std::vector<Event>::const_iterator Replay::publishEvents(std::vector<Event>::con | 
			
		
	
		
			
				
					|  |  |  |  |       loop_start_ts = current_nanos; | 
			
		
	
		
			
				
					|  |  |  |  |       prev_replay_speed = speed_; | 
			
		
	
		
			
				
					|  |  |  |  |     } else if (time_diff > 0) { | 
			
		
	
		
			
				
					|  |  |  |  |       precise_nano_sleep(time_diff); | 
			
		
	
		
			
				
					|  |  |  |  |       precise_nano_sleep(time_diff, paused_); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     if (paused_) break; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     cur_mono_time_ = evt.mono_time; | 
			
		
	
		
			
				
					|  |  |  |  |     if (evt.eidx_segnum == -1) { | 
			
		
	
		
			
				
					|  |  |  |  |       publishMessage(&evt); | 
			
		
	
		
			
				
					|  |  |  |  |     } else if (camera_server_) { | 
			
		
	
	
		
			
				
					|  |  |  | 
 |