Implement video versions of RTCInboundRtpStreamStats.jitterBuffer{Target,Minimum}Delay

* https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-jitterbuffertargetdelay
* https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-jitterbufferminimumdelay

Tested: https://jsfiddle.net/pfgzj0yo/17/

Bug: webrtc:14244
Change-Id: I3d949ba63c8339b3881f5d00356559d5789d283d
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/304404
Commit-Queue: Henrik Boström <hbos@webrtc.org>
Reviewed-by: Henrik Boström <hbos@webrtc.org>
Reviewed-by: Åsa Persson <asapersson@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#40157}
diff --git a/call/video_receive_stream.cc b/call/video_receive_stream.cc
index 0e0ad44..3e2a513 100644
--- a/call/video_receive_stream.cc
+++ b/call/video_receive_stream.cc
@@ -80,7 +80,11 @@
   ss << "jitter_delay_ms: " << jitter_buffer_ms << ", ";
   ss << "totalAssemblyTime: " << total_assembly_time.seconds<double>() << ", ";
   ss << "jitterBufferDelay: " << jitter_buffer_delay.seconds<double>() << ", ";
+  ss << "jitterBufferTargetDelay: "
+     << jitter_buffer_target_delay.seconds<double>() << ", ";
   ss << "jitterBufferEmittedCount: " << jitter_buffer_emitted_count << ", ";
+  ss << "jitterBufferMinimumDelay: "
+     << jitter_buffer_minimum_delay.seconds<double>();
   ss << "totalDecodeTime: " << total_decode_time.seconds<double>() << ", ";
   ss << "totalProcessingDelay: " << total_processing_delay.seconds<double>()
      << ", ";
diff --git a/call/video_receive_stream.h b/call/video_receive_stream.h
index b25a126..48a1ad0 100644
--- a/call/video_receive_stream.h
+++ b/call/video_receive_stream.h
@@ -98,8 +98,12 @@
     int jitter_buffer_ms = 0;
     // https://w3c.github.io/webrtc-stats/#dom-rtcinboundrtpstreamstats-jitterbufferdelay
     TimeDelta jitter_buffer_delay = TimeDelta::Zero();
+    // https://w3c.github.io/webrtc-stats/#dom-rtcinboundrtpstreamstats-jitterbuffertargetdelay
+    TimeDelta jitter_buffer_target_delay = TimeDelta::Zero();
     // https://w3c.github.io/webrtc-stats/#dom-rtcinboundrtpstreamstats-jitterbufferemittedcount
     uint64_t jitter_buffer_emitted_count = 0;
+    // https://w3c.github.io/webrtc-stats/#dom-rtcinboundrtpstreamstats-jitterbufferminimumdelay
+    TimeDelta jitter_buffer_minimum_delay = TimeDelta::Zero();
     int min_playout_delay_ms = 0;
     int render_delay_ms = 10;
     int64_t interframe_delay_max_ms = -1;
diff --git a/media/base/media_channel.h b/media/base/media_channel.h
index dbcb30c..02dc693 100644
--- a/media/base/media_channel.h
+++ b/media/base/media_channel.h
@@ -442,15 +442,11 @@
   // https://w3c.github.io/webrtc-stats/#dom-rtcinboundrtpstreamstats-jitterbufferdelay
   double jitter_buffer_delay_seconds = 0.0;
   // Target delay for the jitter buffer (cumulative).
-  // TODO(crbug.com/webrtc/14244): This metric is only implemented for
-  // audio, it should be implemented for video as well.
   // https://w3c.github.io/webrtc-stats/#dom-rtcinboundrtpstreamstats-jitterbuffertargetdelay
-  absl::optional<double> jitter_buffer_target_delay_seconds;
+  double jitter_buffer_target_delay_seconds = 0.0;
   // Minimum obtainable delay for the jitter buffer (cumulative).
-  // TODO(crbug.com/webrtc/14244): This metric is only implemented for
-  // audio, it should be implemented for video as well.
   // https://w3c.github.io/webrtc-stats/#dom-rtcinboundrtpstreamstats-jitterbufferminimumdelay
-  absl::optional<double> jitter_buffer_minimum_delay_seconds;
+  double jitter_buffer_minimum_delay_seconds = 0.0;
   // Number of observations for cumulative jitter latency.
   // https://w3c.github.io/webrtc-stats/#dom-rtcinboundrtpstreamstats-jitterbufferemittedcount
   uint64_t jitter_buffer_emitted_count = 0;
diff --git a/media/engine/webrtc_video_engine.cc b/media/engine/webrtc_video_engine.cc
index dd384b2..3828f00 100644
--- a/media/engine/webrtc_video_engine.cc
+++ b/media/engine/webrtc_video_engine.cc
@@ -3394,7 +3394,11 @@
   info.jitter_buffer_ms = stats.jitter_buffer_ms;
   info.jitter_buffer_delay_seconds =
       stats.jitter_buffer_delay.seconds<double>();
+  info.jitter_buffer_target_delay_seconds =
+      stats.jitter_buffer_target_delay.seconds<double>();
   info.jitter_buffer_emitted_count = stats.jitter_buffer_emitted_count;
+  info.jitter_buffer_minimum_delay_seconds =
+      stats.jitter_buffer_minimum_delay.seconds<double>();
   info.min_playout_delay_ms = stats.min_playout_delay_ms;
   info.render_delay_ms = stats.render_delay_ms;
   info.frames_received =
diff --git a/media/engine/webrtc_video_engine_unittest.cc b/media/engine/webrtc_video_engine_unittest.cc
index e63199d..1c613a0 100644
--- a/media/engine/webrtc_video_engine_unittest.cc
+++ b/media/engine/webrtc_video_engine_unittest.cc
@@ -6557,7 +6557,9 @@
   stats.target_delay_ms = 5;
   stats.jitter_buffer_ms = 6;
   stats.jitter_buffer_delay = TimeDelta::Seconds(60);
+  stats.jitter_buffer_target_delay = TimeDelta::Seconds(55);
   stats.jitter_buffer_emitted_count = 6;
+  stats.jitter_buffer_minimum_delay = TimeDelta::Seconds(50);
   stats.min_playout_delay_ms = 7;
   stats.render_delay_ms = 8;
   stats.width = 9;
@@ -6591,8 +6593,12 @@
   EXPECT_EQ(stats.jitter_buffer_ms, receive_info.receivers[0].jitter_buffer_ms);
   EXPECT_EQ(stats.jitter_buffer_delay.seconds<double>(),
             receive_info.receivers[0].jitter_buffer_delay_seconds);
+  EXPECT_EQ(stats.jitter_buffer_target_delay.seconds<double>(),
+            receive_info.receivers[0].jitter_buffer_target_delay_seconds);
   EXPECT_EQ(stats.jitter_buffer_emitted_count,
             receive_info.receivers[0].jitter_buffer_emitted_count);
+  EXPECT_EQ(stats.jitter_buffer_minimum_delay.seconds<double>(),
+            receive_info.receivers[0].jitter_buffer_minimum_delay_seconds);
   EXPECT_EQ(stats.min_playout_delay_ms,
             receive_info.receivers[0].min_playout_delay_ms);
   EXPECT_EQ(stats.render_delay_ms, receive_info.receivers[0].render_delay_ms);
diff --git a/modules/video_coding/timing/timing.cc b/modules/video_coding/timing/timing.cc
index 1035d6f..735f632 100644
--- a/modules/video_coding/timing/timing.cc
+++ b/modules/video_coding/timing/timing.cc
@@ -252,6 +252,13 @@
                   jitter_delay_ + EstimatedMaxDecodeTime() + render_delay_);
 }
 
+// TODO(crbug.com/webrtc/15197): Centralize delay arithmetic.
+TimeDelta VCMTiming::StatsTargetDelayInternal() const {
+  TimeDelta stats_target_delay =
+      TargetDelayInternal() - (EstimatedMaxDecodeTime() + render_delay_);
+  return std::max(TimeDelta::Zero(), stats_target_delay);
+}
+
 VideoFrame::RenderParameters VCMTiming::RenderParameters() const {
   MutexLock lock(&mutex_);
   return {.use_low_latency_rendering = UseLowLatencyRendering(),
@@ -271,12 +278,12 @@
   MutexLock lock(&mutex_);
   return VideoDelayTimings{
       .num_decoded_frames = num_decoded_frames_,
-      .jitter_delay = jitter_delay_,
+      .minimum_delay = jitter_delay_,
       .estimated_max_decode_time = EstimatedMaxDecodeTime(),
       .render_delay = render_delay_,
       .min_playout_delay = min_playout_delay_,
       .max_playout_delay = max_playout_delay_,
-      .target_delay = TargetDelayInternal(),
+      .target_delay = StatsTargetDelayInternal(),
       .current_delay = current_delay_};
 }
 
diff --git a/modules/video_coding/timing/timing.h b/modules/video_coding/timing/timing.h
index dbac40d..9e7fb87 100644
--- a/modules/video_coding/timing/timing.h
+++ b/modules/video_coding/timing/timing.h
@@ -31,13 +31,15 @@
  public:
   struct VideoDelayTimings {
     size_t num_decoded_frames;
-    // Delay added to smooth out frame delay variation ("jitter") caused by
-    // the network.
-    TimeDelta jitter_delay;
+    // Pre-decode delay added to smooth out frame delay variation ("jitter")
+    // caused by the network. The target delay will be no smaller than this
+    // delay, thus it is called `minimum_delay`.
+    TimeDelta minimum_delay;
     // Estimated time needed to decode a video frame. Obtained as the 95th
     // percentile decode time over a recent time window.
     TimeDelta estimated_max_decode_time;
-    // Estimated time needed to render a frame. Set to a constant.
+    // Post-decode delay added to smooth out frame delay variation caused by
+    // decoding and rendering. Set to a constant.
     TimeDelta render_delay;
     // Minimum total delay used when determining render time for a frame.
     // Obtained from API, `playout-delay` RTP header extension, or A/V sync.
@@ -45,9 +47,9 @@
     // Maximum total delay used when determining render time for a frame.
     // Obtained from `playout-delay` RTP header extension.
     TimeDelta max_playout_delay;
-    // Target delay. Obtained from all the elements above.
+    // Target total delay. Obtained from all the elements above.
     TimeDelta target_delay;
-    // Current delay. Obtained by smoothing out the target delay.
+    // Current total delay. Obtained by smoothening the `target_delay`.
     TimeDelta current_delay;
   };
 
@@ -133,6 +135,8 @@
   Timestamp RenderTimeInternal(uint32_t frame_timestamp, Timestamp now) const
       RTC_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
   TimeDelta TargetDelayInternal() const RTC_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+  TimeDelta StatsTargetDelayInternal() const
+      RTC_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
   bool UseLowLatencyRendering() const RTC_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 
   mutable Mutex mutex_;
diff --git a/modules/video_coding/timing/timing_unittest.cc b/modules/video_coding/timing/timing_unittest.cc
index 8633c0d..4ba8c4d 100644
--- a/modules/video_coding/timing/timing_unittest.cc
+++ b/modules/video_coding/timing/timing_unittest.cc
@@ -13,6 +13,7 @@
 #include "api/units/frequency.h"
 #include "api/units/time_delta.h"
 #include "system_wrappers/include/clock.h"
+#include "test/gmock.h"
 #include "test/gtest.h"
 #include "test/scoped_key_value_config.h"
 
@@ -22,9 +23,54 @@
 constexpr Frequency k25Fps = Frequency::Hertz(25);
 constexpr Frequency k90kHz = Frequency::KiloHertz(90);
 
+MATCHER(HasConsistentVideoDelayTimings, "") {
+  // Delays should be non-negative.
+  bool p1 = arg.minimum_delay >= TimeDelta::Zero();
+  bool p2 = arg.estimated_max_decode_time >= TimeDelta::Zero();
+  bool p3 = arg.render_delay >= TimeDelta::Zero();
+  bool p4 = arg.min_playout_delay >= TimeDelta::Zero();
+  bool p5 = arg.max_playout_delay >= TimeDelta::Zero();
+  bool p6 = arg.target_delay >= TimeDelta::Zero();
+  bool p7 = arg.current_delay >= TimeDelta::Zero();
+  *result_listener << "\np: " << p1 << p2 << p3 << p4 << p5 << p6 << p7;
+  bool p = p1 && p2 && p3 && p4 && p5 && p6 && p7;
+
+  // Delays should be internally consistent.
+  bool m1 = arg.minimum_delay <= arg.target_delay;
+  if (!m1) {
+    *result_listener << "\nminimum_delay: " << arg.minimum_delay << ", "
+                     << "target_delay: " << arg.target_delay << "\n";
+  }
+  bool m2 = arg.minimum_delay <= arg.current_delay;
+  if (!m2) {
+    *result_listener << "\nminimum_delay: " << arg.minimum_delay << ", "
+                     << "current_delay: " << arg.current_delay;
+  }
+  bool m3 = arg.target_delay >= arg.min_playout_delay;
+  if (!m3) {
+    *result_listener << "\ntarget_delay: " << arg.target_delay << ", "
+                     << "min_playout_delay: " << arg.min_playout_delay << "\n";
+  }
+  // TODO(crbug.com/webrtc/15197): Uncomment when this is guaranteed.
+  // bool m4 = arg.target_delay <= arg.max_playout_delay;
+  bool m5 = arg.current_delay >= arg.min_playout_delay;
+  if (!m5) {
+    *result_listener << "\ncurrent_delay: " << arg.current_delay << ", "
+                     << "min_playout_delay: " << arg.min_playout_delay << "\n";
+  }
+  bool m6 = arg.current_delay <= arg.max_playout_delay;
+  if (!m6) {
+    *result_listener << "\ncurrent_delay: " << arg.current_delay << ", "
+                     << "max_playout_delay: " << arg.max_playout_delay << "\n";
+  }
+  bool m = m1 && m2 && m3 && m5 && m6;
+
+  return p && m;
+}
+
 }  // namespace
 
-TEST(ReceiverTimingTest, JitterDelay) {
+TEST(VCMTimingTest, JitterDelay) {
   test::ScopedKeyValueConfig field_trials;
   SimulatedClock clock(0);
   VCMTiming timing(&clock, field_trials);
@@ -115,9 +161,11 @@
   clock.AdvanceTimeMilliseconds(5000);
   timestamp += 5 * 90000;
   timing.UpdateCurrentDelay(timestamp);
+
+  EXPECT_THAT(timing.GetTimings(), HasConsistentVideoDelayTimings());
 }
 
-TEST(ReceiverTimingTest, TimestampWrapAround) {
+TEST(VCMTimingTest, TimestampWrapAround) {
   constexpr auto kStartTime = Timestamp::Millis(1337);
   test::ScopedKeyValueConfig field_trials;
   SimulatedClock clock(kStartTime);
@@ -136,9 +184,11 @@
     EXPECT_EQ(kStartTime + 3 / k25Fps + TimeDelta::Millis(1),
               timing.RenderTime(89u, clock.CurrentTime()));
   }
+
+  EXPECT_THAT(timing.GetTimings(), HasConsistentVideoDelayTimings());
 }
 
-TEST(ReceiverTimingTest, UseLowLatencyRenderer) {
+TEST(VCMTimingTest, UseLowLatencyRenderer) {
   test::ScopedKeyValueConfig field_trials;
   SimulatedClock clock(0);
   VCMTiming timing(&clock, field_trials);
@@ -161,9 +211,11 @@
   // False if max playout delay > 500 ms.
   timing.set_max_playout_delay(TimeDelta::Millis(501));
   EXPECT_FALSE(timing.RenderParameters().use_low_latency_rendering);
+
+  EXPECT_THAT(timing.GetTimings(), HasConsistentVideoDelayTimings());
 }
 
-TEST(ReceiverTimingTest, MaxWaitingTimeIsZeroForZeroRenderTime) {
+TEST(VCMTimingTest, MaxWaitingTimeIsZeroForZeroRenderTime) {
   // This is the default path when the RTP playout delay header extension is set
   // to min==0 and max==0.
   constexpr int64_t kStartTimeUs = 3.15e13;  // About one year in us.
@@ -197,9 +249,11 @@
   EXPECT_LT(timing.MaxWaitingTime(kZeroRenderTime, now,
                                   /*too_many_frames_queued=*/false),
             TimeDelta::Zero());
+
+  EXPECT_THAT(timing.GetTimings(), HasConsistentVideoDelayTimings());
 }
 
-TEST(ReceiverTimingTest, MaxWaitingTimeZeroDelayPacingExperiment) {
+TEST(VCMTimingTest, MaxWaitingTimeZeroDelayPacingExperiment) {
   // The minimum pacing is enabled by a field trial and active if the RTP
   // playout delay header extension is set to min==0.
   constexpr TimeDelta kMinPacing = TimeDelta::Millis(3);
@@ -247,9 +301,11 @@
   EXPECT_EQ(timing.MaxWaitingTime(kZeroRenderTime, now,
                                   /*too_many_frames_queued=*/false),
             kMinPacing);
+
+  EXPECT_THAT(timing.GetTimings(), HasConsistentVideoDelayTimings());
 }
 
-TEST(ReceiverTimingTest, DefaultMaxWaitingTimeUnaffectedByPacingExperiment) {
+TEST(VCMTimingTest, DefaultMaxWaitingTimeUnaffectedByPacingExperiment) {
   // The minimum pacing is enabled by a field trial but should not have any
   // effect if render_time_ms is greater than 0;
   test::ScopedKeyValueConfig field_trials(
@@ -277,9 +333,11 @@
                                     /*too_many_frames_queued=*/false),
               render_time - now - estimated_processing_delay);
   }
+
+  EXPECT_THAT(timing.GetTimings(), HasConsistentVideoDelayTimings());
 }
 
-TEST(ReceiverTimingTest, MaxWaitingTimeReturnsZeroIfTooManyFramesQueuedIsTrue) {
+TEST(VCMTimingTest, MaxWaitingTimeReturnsZeroIfTooManyFramesQueuedIsTrue) {
   // The minimum pacing is enabled by a field trial and active if the RTP
   // playout delay header extension is set to min==0.
   constexpr TimeDelta kMinPacing = TimeDelta::Millis(3);
@@ -314,9 +372,11 @@
   EXPECT_EQ(timing.MaxWaitingTime(kZeroRenderTime, now_ms,
                                   /*too_many_frames_queued=*/true),
             TimeDelta::Zero());
+
+  EXPECT_THAT(timing.GetTimings(), HasConsistentVideoDelayTimings());
 }
 
-TEST(ReceiverTimingTest, UpdateCurrentDelayCapsWhenOffByMicroseconds) {
+TEST(VCMTimingTest, UpdateCurrentDelayCapsWhenOffByMicroseconds) {
   test::ScopedKeyValueConfig field_trials;
   SimulatedClock clock(0);
   VCMTiming timing(&clock, field_trials);
@@ -334,6 +394,52 @@
       decode_time + TimeDelta::Millis(10) + TimeDelta::Micros(37);
   timing.UpdateCurrentDelay(render_time, decode_time);
   EXPECT_EQ(timing.GetTimings().current_delay, timing.TargetVideoDelay());
+
+  // TODO(crbug.com/webrtc/15197): Fix this.
+  // EXPECT_THAT(timing.GetTimings(), HasConsistentVideoDelayTimings());
+}
+
+TEST(VCMTimingTest, GetTimings) {
+  test::ScopedKeyValueConfig field_trials;
+  SimulatedClock clock(33);
+  VCMTiming timing(&clock, field_trials);
+  timing.Reset();
+
+  // Setup.
+  TimeDelta render_delay = TimeDelta::Millis(11);
+  timing.set_render_delay(render_delay);
+  TimeDelta min_playout_delay = TimeDelta::Millis(50);
+  timing.set_min_playout_delay(min_playout_delay);
+  TimeDelta max_playout_delay = TimeDelta::Millis(500);
+  timing.set_max_playout_delay(max_playout_delay);
+
+  // On complete.
+  timing.IncomingTimestamp(3000, clock.CurrentTime());
+  clock.AdvanceTimeMilliseconds(1);
+
+  // On decodable.
+  Timestamp render_time =
+      timing.RenderTime(/*next_temporal_unit_rtp=*/3000, clock.CurrentTime());
+  TimeDelta minimum_delay = TimeDelta::Millis(123);
+  timing.SetJitterDelay(minimum_delay);
+  timing.UpdateCurrentDelay(render_time, clock.CurrentTime());
+  clock.AdvanceTimeMilliseconds(100);
+
+  // On decoded.
+  TimeDelta decode_time = TimeDelta::Millis(4);
+  timing.StopDecodeTimer(decode_time, clock.CurrentTime());
+
+  VCMTiming::VideoDelayTimings timings = timing.GetTimings();
+  EXPECT_EQ(timings.num_decoded_frames, 1u);
+  EXPECT_EQ(timings.minimum_delay, minimum_delay);
+  // A single decoded frame is not enough to calculate p95.
+  EXPECT_EQ(timings.estimated_max_decode_time, TimeDelta::Zero());
+  EXPECT_EQ(timings.render_delay, render_delay);
+  EXPECT_EQ(timings.min_playout_delay, min_playout_delay);
+  EXPECT_EQ(timings.max_playout_delay, max_playout_delay);
+  EXPECT_EQ(timings.target_delay, minimum_delay);
+  EXPECT_EQ(timings.current_delay, minimum_delay);
+  EXPECT_THAT(timings, HasConsistentVideoDelayTimings());
 }
 
 }  // namespace webrtc
diff --git a/pc/rtc_stats_collector.cc b/pc/rtc_stats_collector.cc
index 97456a5..eeee3b8 100644
--- a/pc/rtc_stats_collector.cc
+++ b/pc/rtc_stats_collector.cc
@@ -416,14 +416,10 @@
       static_cast<int32_t>(media_receiver_info.packets_lost);
   inbound_stats->jitter_buffer_delay =
       media_receiver_info.jitter_buffer_delay_seconds;
-  if (media_receiver_info.jitter_buffer_target_delay_seconds.has_value()) {
-    inbound_stats->jitter_buffer_target_delay =
-        *media_receiver_info.jitter_buffer_target_delay_seconds;
-  }
-  if (media_receiver_info.jitter_buffer_minimum_delay_seconds.has_value()) {
-    inbound_stats->jitter_buffer_minimum_delay =
-        *media_receiver_info.jitter_buffer_minimum_delay_seconds;
-  }
+  inbound_stats->jitter_buffer_target_delay =
+      media_receiver_info.jitter_buffer_target_delay_seconds;
+  inbound_stats->jitter_buffer_minimum_delay =
+      media_receiver_info.jitter_buffer_minimum_delay_seconds;
   inbound_stats->jitter_buffer_emitted_count =
       media_receiver_info.jitter_buffer_emitted_count;
   if (media_receiver_info.nacks_sent.has_value()) {
diff --git a/pc/rtc_stats_integrationtest.cc b/pc/rtc_stats_integrationtest.cc
index fa9f226..12a0063 100644
--- a/pc/rtc_stats_integrationtest.cc
+++ b/pc/rtc_stats_integrationtest.cc
@@ -634,6 +634,10 @@
         inbound_stream.jitter_buffer_delay);
     verifier.TestMemberIsNonNegative<uint64_t>(
         inbound_stream.jitter_buffer_emitted_count);
+    verifier.TestMemberIsNonNegative<double>(
+        inbound_stream.jitter_buffer_target_delay);
+    verifier.TestMemberIsNonNegative<double>(
+        inbound_stream.jitter_buffer_minimum_delay);
     if (inbound_stream.kind.is_defined() && *inbound_stream.kind == "video") {
       verifier.TestMemberIsUndefined(inbound_stream.total_samples_received);
       verifier.TestMemberIsUndefined(inbound_stream.concealed_samples);
@@ -643,9 +647,6 @@
           inbound_stream.inserted_samples_for_deceleration);
       verifier.TestMemberIsUndefined(
           inbound_stream.removed_samples_for_acceleration);
-      verifier.TestMemberIsUndefined(inbound_stream.jitter_buffer_target_delay);
-      verifier.TestMemberIsUndefined(
-          inbound_stream.jitter_buffer_minimum_delay);
       verifier.TestMemberIsUndefined(inbound_stream.audio_level);
       verifier.TestMemberIsUndefined(inbound_stream.total_audio_energy);
       verifier.TestMemberIsUndefined(inbound_stream.total_samples_duration);
diff --git a/video/end_to_end_tests/stats_tests.cc b/video/end_to_end_tests/stats_tests.cc
index 3c3799f..967357f 100644
--- a/video/end_to_end_tests/stats_tests.cc
+++ b/video/end_to_end_tests/stats_tests.cc
@@ -129,8 +129,12 @@
 
         receive_stats_filled_["JitterBufferDelay"] =
             stats.jitter_buffer_delay > TimeDelta::Zero();
+        receive_stats_filled_["JitterBufferTargetDelay"] =
+            stats.jitter_buffer_target_delay > TimeDelta::Zero();
         receive_stats_filled_["JitterBufferEmittedCount"] =
             stats.jitter_buffer_emitted_count != 0;
+        receive_stats_filled_["JitterBufferMinimumDelay"] =
+            stats.jitter_buffer_minimum_delay > TimeDelta::Zero();
 
         receive_stats_filled_["CName"] |= !stats.c_name.empty();
 
diff --git a/video/receive_statistics_proxy.cc b/video/receive_statistics_proxy.cc
index faa0ea9..049f212 100644
--- a/video/receive_statistics_proxy.cc
+++ b/video/receive_statistics_proxy.cc
@@ -531,12 +531,15 @@
       }));
 }
 
-void ReceiveStatisticsProxy::OnDecodableFrame(TimeDelta jitter_buffer_delay) {
+void ReceiveStatisticsProxy::OnDecodableFrame(TimeDelta jitter_buffer_delay,
+                                              TimeDelta target_delay,
+                                              TimeDelta minimum_delay) {
   RTC_DCHECK_RUN_ON(&main_thread_);
   // Cumulative stats exposed through standardized GetStats.
-  // TODO(crbug.com/webrtc/14244): Implement targetDelay and minimumDelay here.
   stats_.jitter_buffer_delay += jitter_buffer_delay;
+  stats_.jitter_buffer_target_delay += target_delay;
   ++stats_.jitter_buffer_emitted_count;
+  stats_.jitter_buffer_minimum_delay += minimum_delay;
 }
 
 void ReceiveStatisticsProxy::OnFrameBufferTimingsUpdated(
diff --git a/video/receive_statistics_proxy.h b/video/receive_statistics_proxy.h
index 425eb1a..d8da306 100644
--- a/video/receive_statistics_proxy.h
+++ b/video/receive_statistics_proxy.h
@@ -89,7 +89,9 @@
                        size_t size_bytes,
                        VideoContentType content_type) override;
   void OnDroppedFrames(uint32_t frames_dropped) override;
-  void OnDecodableFrame(TimeDelta jitter_buffer_delay) override;
+  void OnDecodableFrame(TimeDelta jitter_buffer_delay,
+                        TimeDelta target_delay,
+                        TimeDelta minimum_delay) override;
   void OnFrameBufferTimingsUpdated(int estimated_max_decode_time_ms,
                                    int current_delay_ms,
                                    int target_delay_ms,
diff --git a/video/receive_statistics_proxy_unittest.cc b/video/receive_statistics_proxy_unittest.cc
index 7854c79..ec6dd0c 100644
--- a/video/receive_statistics_proxy_unittest.cc
+++ b/video/receive_statistics_proxy_unittest.cc
@@ -560,35 +560,45 @@
 TEST_F(ReceiveStatisticsProxyTest, GetStatsReportsDecodeTimingStats) {
   const int kMaxDecodeMs = 2;
   const int kCurrentDelayMs = 3;
-  const int kTargetDelayMs = 4;
+  const TimeDelta kTargetDelay = TimeDelta::Millis(4);
   const int kJitterDelayMs = 5;
   const int kMinPlayoutDelayMs = 6;
   const int kRenderDelayMs = 7;
   const int64_t kRttMs = 8;
-  const int kJitterBufferDelayMs = 9;
+  const TimeDelta kJitterBufferDelay = TimeDelta::Millis(9);
+  const TimeDelta kMinimumDelay = TimeDelta::Millis(1);
   statistics_proxy_->OnRttUpdate(kRttMs);
   statistics_proxy_->OnFrameBufferTimingsUpdated(
-      kMaxDecodeMs, kCurrentDelayMs, kTargetDelayMs, kJitterDelayMs,
+      kMaxDecodeMs, kCurrentDelayMs, kTargetDelay.ms(), kJitterDelayMs,
       kMinPlayoutDelayMs, kRenderDelayMs);
-  statistics_proxy_->OnDecodableFrame(TimeDelta::Millis(kJitterBufferDelayMs));
+  statistics_proxy_->OnDecodableFrame(kJitterBufferDelay, kTargetDelay,
+                                      kMinimumDelay);
   VideoReceiveStreamInterface::Stats stats = FlushAndGetStats();
   EXPECT_EQ(kMaxDecodeMs, stats.max_decode_ms);
   EXPECT_EQ(kCurrentDelayMs, stats.current_delay_ms);
-  EXPECT_EQ(kTargetDelayMs, stats.target_delay_ms);
+  EXPECT_EQ(kTargetDelay.ms(), stats.target_delay_ms);
   EXPECT_EQ(kJitterDelayMs, stats.jitter_buffer_ms);
   EXPECT_EQ(kMinPlayoutDelayMs, stats.min_playout_delay_ms);
   EXPECT_EQ(kRenderDelayMs, stats.render_delay_ms);
-  EXPECT_EQ(kJitterBufferDelayMs, stats.jitter_buffer_delay.ms());
+  EXPECT_EQ(kJitterBufferDelay, stats.jitter_buffer_delay);
+  EXPECT_EQ(kTargetDelay, stats.jitter_buffer_target_delay);
   EXPECT_EQ(1u, stats.jitter_buffer_emitted_count);
+  EXPECT_EQ(kMinimumDelay, stats.jitter_buffer_minimum_delay);
 }
 
 TEST_F(ReceiveStatisticsProxyTest, CumulativeDecodeGetStatsAccumulate) {
-  const int kJitterBufferDelayMs = 3;
-  statistics_proxy_->OnDecodableFrame(TimeDelta::Millis(kJitterBufferDelayMs));
-  statistics_proxy_->OnDecodableFrame(TimeDelta::Millis(kJitterBufferDelayMs));
+  const TimeDelta kJitterBufferDelay = TimeDelta::Millis(3);
+  const TimeDelta kTargetDelay = TimeDelta::Millis(2);
+  const TimeDelta kMinimumDelay = TimeDelta::Millis(1);
+  statistics_proxy_->OnDecodableFrame(kJitterBufferDelay, kTargetDelay,
+                                      kMinimumDelay);
+  statistics_proxy_->OnDecodableFrame(kJitterBufferDelay, kTargetDelay,
+                                      kMinimumDelay);
   VideoReceiveStreamInterface::Stats stats = FlushAndGetStats();
-  EXPECT_EQ(2 * kJitterBufferDelayMs, stats.jitter_buffer_delay.ms());
+  EXPECT_EQ(2 * kJitterBufferDelay, stats.jitter_buffer_delay);
+  EXPECT_EQ(2 * kTargetDelay, stats.jitter_buffer_target_delay);
   EXPECT_EQ(2u, stats.jitter_buffer_emitted_count);
+  EXPECT_EQ(2 * kMinimumDelay, stats.jitter_buffer_minimum_delay);
 }
 
 TEST_F(ReceiveStatisticsProxyTest, GetStatsReportsRtcpPacketTypeCounts) {
diff --git a/video/video_stream_buffer_controller.cc b/video/video_stream_buffer_controller.cc
index 870c32d..455f064 100644
--- a/video/video_stream_buffer_controller.cc
+++ b/video/video_stream_buffer_controller.cc
@@ -336,7 +336,7 @@
   if (timings.num_decoded_frames) {
     stats_proxy_->OnFrameBufferTimingsUpdated(
         timings.estimated_max_decode_time.ms(), timings.current_delay.ms(),
-        timings.target_delay.ms(), timings.jitter_delay.ms(),
+        timings.target_delay.ms(), timings.minimum_delay.ms(),
         timings.min_playout_delay.ms(), timings.render_delay.ms());
   }
 
@@ -351,7 +351,8 @@
   // https://w3c.github.io/webrtc-stats/#dom-rtcinboundrtpstreamstats-jitterbufferdelay
   TimeDelta jitter_buffer_delay =
       std::max(TimeDelta::Zero(), now - min_receive_time);
-  stats_proxy_->OnDecodableFrame(jitter_buffer_delay);
+  stats_proxy_->OnDecodableFrame(jitter_buffer_delay, timings.target_delay,
+                                 timings.minimum_delay);
 }
 
 void VideoStreamBufferController::UpdateTimingFrameInfo() {
diff --git a/video/video_stream_buffer_controller.h b/video/video_stream_buffer_controller.h
index 96e867a..bb67304 100644
--- a/video/video_stream_buffer_controller.h
+++ b/video/video_stream_buffer_controller.h
@@ -45,8 +45,12 @@
 
   virtual void OnDroppedFrames(uint32_t frames_dropped) = 0;
 
-  // Actual delay experienced by a single frame.
-  virtual void OnDecodableFrame(TimeDelta jitter_buffer_delay) = 0;
+  // `jitter_buffer_delay` is the delay experienced by a single frame,
+  // whereas `target_delay` and `minimum_delay` are the current delays
+  // applied by the jitter buffer.
+  virtual void OnDecodableFrame(TimeDelta jitter_buffer_delay,
+                                TimeDelta target_delay,
+                                TimeDelta minimum_delay) = 0;
 
   // Various jitter buffer delays determined by VCMTiming.
   virtual void OnFrameBufferTimingsUpdated(int estimated_max_decode_time_ms,
diff --git a/video/video_stream_buffer_controller_unittest.cc b/video/video_stream_buffer_controller_unittest.cc
index 4156581..be779ea 100644
--- a/video/video_stream_buffer_controller_unittest.cc
+++ b/video/video_stream_buffer_controller_unittest.cc
@@ -107,7 +107,9 @@
   MOCK_METHOD(void, OnDroppedFrames, (uint32_t num_dropped), (override));
   MOCK_METHOD(void,
               OnDecodableFrame,
-              (TimeDelta jitter_buffer_delay),
+              (TimeDelta jitter_buffer_delay,
+               TimeDelta target_delay,
+               TimeDelta minimum_delay),
               (override));
   MOCK_METHOD(void,
               OnFrameBufferTimingsUpdated,