[Cast Streaming] fix heap use-after-free

This patch fixes a use after free, caused by using an iterator
after it has been invalidated by a call to std::map::erase().

Bug: 1491210
Change-Id: I0c546eb6474af82f052b89e819a4886a004270f0
Reviewed-on: https://chromium-review.googlesource.com/c/openscreen/+/4981212
Reviewed-by: Mark Foltz <mfoltz@chromium.org>
Commit-Queue: Jordan Bayles <jophba@chromium.org>
Reviewed-by: Muyao Xu <muyaoxu@google.com>
diff --git a/cast/streaming/statistics_analyzer.cc b/cast/streaming/statistics_analyzer.cc
index b0658f5..81d10c5 100644
--- a/cast/streaming/statistics_analyzer.cc
+++ b/cast/streaming/statistics_analyzer.cc
@@ -144,16 +144,22 @@
 void StatisticsAnalyzer::RecordFrameLatencies(const FrameEvent& frame_event) {
   FrameInfoMap& frame_infos = recent_frame_infos_.Get(frame_event.media_type);
 
+  // Event is too old, don't bother.
+  const bool map_is_full = frame_infos.size() == kMaxRecentFrameInfoMapSize;
+  if (map_is_full && frame_event.rtp_timestamp <= frame_infos.begin()->first) {
+    return;
+  }
+
   auto it = frame_infos.find(frame_event.rtp_timestamp);
   if (it == frame_infos.end()) {
+    if (map_is_full) {
+      frame_infos.erase(frame_infos.begin());
+    }
+
     auto emplace_result =
         frame_infos.emplace(frame_event.rtp_timestamp, FrameInfo{});
     OSP_CHECK(emplace_result.second);
     it = emplace_result.first;
-
-    if (frame_infos.size() >= kMaxRecentFrameInfoMapSize) {
-      frame_infos.erase(frame_infos.begin());
-    }
   }
 
   switch (frame_event.type) {
diff --git a/cast/streaming/statistics_analyzer_unittest.cc b/cast/streaming/statistics_analyzer_unittest.cc
index 22313ce..be89474 100644
--- a/cast/streaming/statistics_analyzer_unittest.cc
+++ b/cast/streaming/statistics_analyzer_unittest.cc
@@ -635,4 +635,90 @@
                                    (frame_interval_ms * num_events)));
 }
 
+TEST_F(StatisticsAnalyzerTest, LotsOfEventsStillWorksProperly) {
+  constexpr std::array<StatisticsEventType, 5> kEventsToReport{
+      StatisticsEventType::kFrameCaptureBegin,
+      StatisticsEventType::kFrameCaptureEnd, StatisticsEventType::kFrameEncoded,
+      StatisticsEventType::kFrameAckSent, StatisticsEventType::kFramePlayedOut};
+  constexpr int kNumFrames = 1000;
+  constexpr int kNumEvents = kNumFrames * kEventsToReport.size();
+
+  constexpr std::array<int, 5> kFramePlayoutDelayDeltasMs{10, 14, 3, 40, 1};
+  constexpr std::array<int, 25> kTimestampOffsetsMs{
+      // clang-format off
+      0, 13, 39, 278, 552,   // Frame One.
+      0, 14, 34, 239, 373,   // Frame Two.
+      0, 19, 29, 245, 389,   // Frame Three.
+      0, 17, 37, 261, 390,   // Frame Four.
+      0, 14, 44, 290, 440,   // Frame Five.
+      // clang-format on
+  };
+
+  constexpr std::array<std::pair<StatisticType, double>, 7> kExpectedStats{{
+      {StatisticType::kNumLateFrames, 1000},
+      {StatisticType::kNumFramesCaptured, 1000},
+      {StatisticType::kAvgEndToEndLatencyMs, 428.8},
+      {StatisticType::kAvgCaptureLatencyMs, 15.4},
+      {StatisticType::kAvgFrameLatencyMs, 226},
+      {StatisticType::kAvgEncodeTimeMs, 21.2},
+      {StatisticType::kEnqueueFps, 40},
+  }};
+
+  constexpr std::array<std::pair<HistogramType, std::array<int, 30>>, 4>
+      kExpectedHistograms{
+          {{HistogramType::kCaptureLatencyMs, {0, 1000}},
+           {HistogramType::kEncodeTimeMs, {0, 200, 800}},
+           {HistogramType::kEndToEndLatencyMs,
+            {0, 0, 0, 0, 0,   0,   0, 0, 0,   0, 0, 0, 0, 0,  0,
+             0, 0, 0, 0, 200, 400, 0, 0, 200, 0, 0, 0, 0, 200
+
+            }},
+           {HistogramType::kFrameLatenessMs, {0, 800, 0, 200}}}};
+
+  // We don't check stats the first 49 times, only the last.
+  {
+    testing::InSequence s;
+    EXPECT_CALL(stats_client_, OnStatisticsUpdated(_)).Times(49);
+    EXPECT_CALL(stats_client_, OnStatisticsUpdated(_))
+        .WillOnce(Invoke([&](const SenderStats& stats) {
+          for (const auto& stat_pair : kExpectedStats) {
+            ExpectStatEq(stats.video_statistics, stat_pair.first,
+                         stat_pair.second);
+          }
+          for (const auto& histogram_pair : kExpectedHistograms) {
+            ExpectHistoBuckets(stats.video_histograms, histogram_pair.first,
+                               histogram_pair.second);
+          }
+        }));
+  }
+
+  analyzer_->ScheduleAnalysis();
+  RtpTimeTicks rtp_timestamp;
+  int current_event = 0;
+  for (int frame_id = 0; frame_id < kNumFrames; frame_id++) {
+    for (StatisticsEventType event_type : kEventsToReport) {
+      FrameEvent event(kDefaultFrameEvent);
+      event.type = event_type;
+      event.frame_id = FrameId(frame_id);
+      event.rtp_timestamp = rtp_timestamp;
+      event.timestamp =
+          fake_clock_.now() +
+          milliseconds(
+              kTimestampOffsetsMs[current_event % kTimestampOffsetsMs.size()]);
+      event.delay_delta = milliseconds(
+          kFramePlayoutDelayDeltasMs[frame_id %
+                                     kFramePlayoutDelayDeltasMs.size()]);
+      collector_->CollectFrameEvent(std::move(event));
+
+      current_event++;
+    }
+    fake_clock_.Advance(
+        milliseconds(kDefaultStatIntervalMs * kEventsToReport.size()));
+    rtp_timestamp += RtpTimeDelta::FromTicks(90);
+  }
+
+  fake_clock_.Advance(milliseconds(kDefaultStatsAnalysisIntervalMs -
+                                   (kDefaultStatIntervalMs * kNumEvents)));
+}
+
 }  // namespace openscreen::cast